Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 2 additions & 22 deletions docs/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -468,16 +468,8 @@
"to": "reference/type-aliases/DeepValue"
},
{
"label": "Types / FieldInfo",
"to": "reference/type-aliases/FieldInfo"
},
{
"label": "Types / FieldMeta",
"to": "reference/type-aliases/FieldMeta"
},
{
"label": "Types / FieldState",
"to": "reference/type-aliases/FieldState"
"label": "Types / AnyFieldMeta",
"to": "reference/type-aliases/AnyFieldMeta"
},
{
"label": "Types / BaseFormState",
Expand Down Expand Up @@ -524,10 +516,6 @@
"label": "Functions / useForm",
"to": "framework/react/reference/functions/useForm"
},
{
"label": "Functions / useTransform",
"to": "framework/react/reference/functions/useTransform"
},
{
"label": "Types / FieldComponent",
"to": "framework/react/reference/type-aliases/FieldComponent"
Expand Down Expand Up @@ -589,10 +577,6 @@
{
"label": "Types / FieldComponent",
"to": "framework/vue/reference/type-aliases/FieldComponent"
},
{
"label": "Types / UseField",
"to": "framework/vue/reference/type-aliases/UseField"
}
]
},
Expand All @@ -615,10 +599,6 @@
"label": "Functions / Field",
"to": "framework/solid/reference/functions/Field"
},
{
"label": "Types / CreateField",
"to": "framework/solid/reference/type-aliases/CreateField"
},
{
"label": "Types / FieldComponent",
"to": "framework/solid/reference/type-aliases/FieldComponent"
Expand Down
113 changes: 89 additions & 24 deletions scripts/verify-links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const errors: Array<{
link: string
resolvedPath: string
reason: string
nav?: string
}> = []

function isRelativeLink(link: string) {
Expand All @@ -27,6 +28,29 @@ function stripExtension(p: string): string {
return p.replace(`${extname(p)}`, '')
}

/**
* Map a resolved `/docs` path to the file or directory that actually serves it,
* and report whether that target exists.
*/
function resolveDocTarget(absPath: string): { path: string; exists: boolean } {
// Examples live outside /docs: /docs/framework/{framework}/examples/{name}
// is served from /examples/{framework}/{name}
if (absPath.includes('/examples/')) {
const examplePath = absPath.replace(
/\/docs\/framework\/([^/]+)\/examples\//,
'/examples/$1/',
)
return {
path: examplePath,
exists: existsSync(examplePath) && statSync(examplePath).isDirectory(),
}
}

// Everything else is a markdown page
const mdPath = absPath.endsWith('.md') ? absPath : `${absPath}.md`
return { path: mdPath, exists: existsSync(mdPath) }
}

function relativeLinkExists(link: string, file: string): boolean {
// Remove hash if present
const linkWithoutHash = link.split('#')[0]
Expand All @@ -39,7 +63,7 @@ function relativeLinkExists(link: string, file: string): boolean {

// Resolve the path relative to the markdown file's directory
// Nav up a level to simulate how links are resolved on the web
let absPath = resolve(filePath, '..', linkPath)
const absPath = resolve(filePath, '..', linkPath)

// Ensure the resolved path is within /docs
const docsRoot = resolve('docs')
Expand All @@ -53,32 +77,13 @@ function relativeLinkExists(link: string, file: string): boolean {
return false
}

// Check if this is an example path
const isExample = absPath.includes('/examples/')

let exists = false

if (isExample) {
// Transform /docs/framework/{framework}/examples/ to /examples/{framework}/
absPath = absPath.replace(
/\/docs\/framework\/([^/]+)\/examples\//,
'/examples/$1/',
)
// For examples, we want to check if the directory exists
exists = existsSync(absPath) && statSync(absPath).isDirectory()
} else {
// For non-examples, we want to check if the .md file exists
if (!absPath.endsWith('.md')) {
absPath = `${absPath}.md`
}
exists = existsSync(absPath)
}
const { path: resolvedPath, exists } = resolveDocTarget(absPath)

if (!exists) {
errors.push({
link,
file,
resolvedPath: absPath,
resolvedPath,
reason: 'Not found',
})
}
Expand Down Expand Up @@ -108,12 +113,69 @@ async function verifyMarkdownLinks() {
})
}
}
}

interface ConfigNode {
label?: string
to?: string
children?: Array<ConfigNode>
frameworks?: Array<ConfigNode>
}

/**
* Every `to` in docs/config.json becomes a sidebar link on tanstack.com, so an
* entry pointing at a page that no longer exists renders as a 404. These are
* invisible to the markdown scan above, which only reads links written inside
* .md files.
*/
function verifyConfigLinks() {
const configPath = 'docs/config.json'
const config = JSON.parse(readFileSync(configPath, 'utf-8')) as {
sections?: Array<ConfigNode>
}

const docsRoot = resolve('docs')
let checked = 0

function walk(node: ConfigNode, breadcrumb: Array<string>) {
const trail = node.label ? [...breadcrumb, node.label] : breadcrumb

if (node.to) {
checked++
const { path: resolvedPath, exists } = resolveDocTarget(
resolve(docsRoot, node.to),
)

if (!exists) {
errors.push({
file: configPath,
link: node.to,
resolvedPath,
reason: 'Not found',
nav: trail.join(' > '),
})
}
}

node.children?.forEach((child) => walk(child, trail))
node.frameworks?.forEach((framework) => walk(framework, trail))
}

const sections = config.sections ?? []
sections.forEach((section) => walk(section, []))

console.log(`Found ${checked} nav entries in ${configPath}\n`)
}

async function verifyLinks() {
await verifyMarkdownLinks()
verifyConfigLinks()

if (errors.length > 0) {
console.log(`\n❌ Found ${errors.length} broken links:`)
errors.forEach((err) => {
console.log(
`${err.file}\n link: ${err.link}\n resolved: ${err.resolvedPath}\n why: ${err.reason}\n`,
`${err.file}${err.nav ? `\n nav: ${err.nav}` : ''}\n link: ${err.link}\n resolved: ${err.resolvedPath}\n why: ${err.reason}\n`,
)
})
process.exit(1)
Expand All @@ -122,4 +184,7 @@ async function verifyMarkdownLinks() {
}
}

verifyMarkdownLinks().catch(console.error)
verifyLinks().catch((error) => {
console.error(error)
process.exitCode = 1
})
Loading