next-validate-link
Version:
An utility to validate links in markdown file
1 lines • 50.1 kB
Source Map (JSON)
{"version":3,"file":"index.mjs","names":["scanURLs","fs","scanURLs","scanURLs","scanURLs","scanURLs","scanURLs","Astro.scanURLs","Nuxt.scanURLs","ReactRouter.scanURLs","TanStackStart.scanURLs","Waku.scanURLs","Next.scanURLs"],"sources":["../src/print.ts","../src/utils/frontmatter.ts","../src/sample.ts","../src/presets/shared.ts","../src/presets/astro.ts","../src/utils/fs.ts","../src/presets/next.ts","../src/presets/nuxt.ts","../src/presets/react-router.ts","../src/presets/tanstack-start.ts","../src/presets/waku.ts","../src/scan.ts","../src/utils/url.ts","../src/validate/markdown.ts","../src/utils/external-link.ts","../src/validate.ts"],"sourcesContent":["import picocolors from \"picocolors\";\nimport type { ValidateResult } from \"@/validate\";\n\n/**\n * Print validation errors\n */\nexport function printErrors(results: ValidateResult[], throwError = false) {\n let totalErrors = 0;\n const logs: string[] = [];\n\n for (const result of results) {\n logs.push(\n picocolors.bold(picocolors.redBright(`Invalid URLs in ${result.file}:`)),\n );\n\n for (const error of result.errors) {\n const message =\n error.reason instanceof Error ? error.reason.message : error.reason;\n\n logs.push(\n `${picocolors.bold(error.url)}: ${message} at ${result.file}:${error.line}:${error.column}`,\n );\n }\n\n logs.push(picocolors.dim(\"------\"));\n\n totalErrors += result.errors.length;\n }\n\n const summary = `${results.length} errored file, ${totalErrors} errors`;\n logs.push(\n picocolors.bold(\n totalErrors > 0\n ? picocolors.redBright(summary)\n : picocolors.greenBright(summary),\n ),\n );\n\n if (throwError && totalErrors > 0) {\n console.error(logs.join(\"\\n\"));\n process.exit(1);\n } else {\n console.log(logs.join(\"\\n\"));\n }\n}\n","/**\n * Inspired by https://github.com/jonschlinkert/gray-matter\n */\nimport { parse } from \"yaml\";\n\ninterface Output {\n /**\n * The matter section, including the delimiter.\n */\n matter: string;\n content: string;\n data: unknown;\n}\n\nconst regex = /^---\\r?\\n(.+?)\\r?\\n---\\r?\\n?/s;\n\n/**\n * parse frontmatter, it supports only yaml format\n */\nexport function frontmatter(input: string): Output {\n const output: Output = { matter: \"\", data: {}, content: input };\n const match = regex.exec(input);\n if (!match) {\n return output;\n }\n\n // get the raw front-matter block\n output.matter = match[0];\n output.content = input.slice(match[0].length);\n\n const loaded = parse(match[1]);\n output.data = loaded ?? {};\n\n return output;\n}\n","import fs from \"node:fs/promises\";\nimport { glob } from \"tinyglobby\";\nimport type { FileObject } from \"./validate\";\nimport { frontmatter } from \"./utils/frontmatter\";\n\nexport type PathToUrl = (path: string) => string | undefined;\n\nexport async function readFileFromPath(\n file: string,\n pathToUrl?: PathToUrl,\n): Promise<FileObject> {\n const content = await fs.readFile(file, \"utf-8\");\n const parsed = frontmatter(content);\n\n return {\n path: file,\n data: parsed.data as object | undefined,\n // apply offset to ensure line numbers are correct\n content:\n \"\\n\".repeat(countLine(content) - countLine(parsed.content)) +\n parsed.content,\n url: pathToUrl ? pathToUrl(file) : undefined,\n };\n}\n\nexport async function readFiles(\n patterns: string | readonly string[],\n options: Partial<{\n pathToUrl?: PathToUrl;\n }> = {},\n): Promise<FileObject[]> {\n const files = await glob(patterns);\n\n return await Promise.all(\n files.map((file) => readFileFromPath(file, options.pathToUrl)),\n );\n}\n\nfunction countLine(s: string) {\n let out = 0;\n for (const c of s) {\n if (c === \"\\n\") out++;\n }\n\n return out;\n}\n","import type { PopulateParams, ScanOptions, ScanResult, UrlMeta } from \"@/scan\";\n\nconst defaultPopulate: PopulateParams[string] = [{}];\n\nconst OPTIONAL_CATCH_ALL = /^\\[\\[\\.\\.\\.(.+)\\]\\]$/;\nconst CALCH_ALL = /^\\[\\.\\.\\.(.+)\\]$/;\n\nfunction parseSegments(segments: string[]): {\n path: string[];\n params: (\"required\" | \"optional\" | null)[];\n} {\n const path: string[] = [];\n const params: (\"required\" | \"optional\" | null)[] = [];\n\n for (let i = 0; i < segments.length; i++) {\n const segment = segments[i];\n\n // route groups\n if (segment.startsWith(\"(\") && segment.endsWith(\")\")) continue;\n let match = OPTIONAL_CATCH_ALL.exec(segment);\n\n if (match) {\n params.push(\"optional\");\n path.push(match[1]);\n continue;\n }\n\n match = CALCH_ALL.exec(segment);\n if (match) {\n params.push(\"required\");\n path.push(match[1]);\n continue;\n }\n\n if (segment.startsWith(\"[\") && segment.endsWith(\"]\")) {\n params.push(\"required\");\n path.push(segment.slice(1, -1));\n continue;\n }\n\n params.push(null);\n path.push(segment);\n }\n\n return { path, params };\n}\n\ninterface PopulatedRoute {\n url: string | RegExp;\n meta?: UrlMeta;\n}\n\nexport function populate(\n segments: string[],\n options: ScanOptions,\n): PopulatedRoute[] {\n const parsed = parseSegments(segments);\n const countParams = parsed.params.filter((param) => param !== null).length;\n\n // static\n if (countParams === 0) {\n const meta =\n options.meta?.[segments.length === 0 ? \"/\" : segments.join(\"/\")];\n\n return [\n {\n url: `/${parsed.path.join(\"/\")}`,\n meta,\n },\n ];\n }\n\n const out: PopulatedRoute[] = [];\n\n let params: PopulateParams[string] | undefined;\n if (options.populate) {\n params = options.populate[\"/\"];\n const searchPath = [...segments];\n\n while (!params && searchPath.length > 0) {\n params = options.populate[searchPath.join(\"/\")];\n searchPath.pop();\n }\n }\n\n params ??= defaultPopulate;\n\n for (const param of params) {\n // filled url\n let url = [...parsed.path];\n\n if (\n countParams > 1 &&\n (Array.isArray(param.value) || typeof param.value === \"string\")\n ) {\n console.warn(\n `path ${segments.join(\n \"/\",\n )} requires multiple params, an object value for populate is expected.`,\n );\n }\n\n let isFallback = false;\n for (let i = 0; i < parsed.params.length; i++) {\n if (parsed.params[i] === null) continue;\n\n const name = parsed.path[i];\n let value: string | string[] | undefined;\n\n if (Array.isArray(param.value) || typeof param.value === \"string\") {\n value = param.value;\n } else if (param.value && name in param.value) {\n value = param.value[name];\n }\n\n if (value) {\n url[i] = typeof value === \"string\" ? value : value.join(\"/\");\n continue;\n }\n\n if (parsed.params[i] === \"optional\") {\n if (i !== parsed.params.length - 1) {\n throw new Error(\"Invalid position of optional catch-all\");\n }\n\n // without param (optional case)\n out.push({\n url: `/${url.slice(0, -1).join(\"/\")}`,\n meta: param,\n });\n }\n\n url[i] = \"(.+)\";\n isFallback = true;\n }\n\n url = url.filter(Boolean);\n\n out.push({\n url: isFallback\n ? new RegExp(`^\\\\/${url.join(\"\\\\/\")}$`)\n : `/${url.join(\"/\")}`,\n meta: param,\n });\n }\n\n return out;\n}\n\n/**\n * Populate Next-like route file paths\n *\n * ```\n * docs/page\n * docs/[slug]/[nested]\n * docs/(group)/[[...optional_catch_all]]\n * docs/(group)/[...catch_all]\n * ```\n */\nexport function populateToScanResult(\n segments: string[],\n options: ScanOptions,\n result: ScanResult,\n) {\n const out = populate(segments, options);\n\n for (const entry of out) {\n if (typeof entry.url === \"string\") {\n result.urls.set(entry.url, entry.meta ?? {});\n continue;\n }\n\n result.fallbackUrls.push({\n url: entry.url,\n meta: entry.meta ?? {},\n });\n }\n}\n","import * as path from \"node:path\";\nimport { glob } from \"tinyglobby\";\nimport type { ScanOptions, ScanResult } from \"@/scan\";\nimport { populateToScanResult } from \"./shared\";\n\nexport async function scanURLs(options: ScanOptions = {}): Promise<ScanResult> {\n const ext = options.extensions ?? [\"astro\", \"md\", \"mdx\"];\n const cwd = options.cwd ?? process.cwd();\n\n async function getFiles() {\n const suffix = ext.length > 0 ? `.{${ext.join(\",\")}}` : \"\";\n\n const pagesFiles = await glob(`**/*${suffix}`, {\n cwd: path.join(cwd, \"src/pages\"),\n });\n\n return pagesFiles.map((file) => {\n const parsed = path.parse(file);\n if (parsed.name === \"index\") return parsed.dir;\n\n return path.join(parsed.dir, parsed.name);\n });\n }\n\n const result: ScanResult = { urls: new Map(), fallbackUrls: [] };\n const files = options.pages ?? (await getFiles());\n\n for (const file of files) {\n populateToScanResult(file.split(path.sep), options, result);\n }\n\n return result;\n}\n","import * as fs from \"node:fs/promises\";\n\nexport function isDirExists(dir: string): Promise<boolean> {\n return fs\n .stat(dir)\n .then((res) => res.isDirectory())\n .catch(() => false);\n}\n\nexport async function isFileExists(file: string) {\n try {\n await fs.access(file);\n return true;\n } catch (_error) {\n return false;\n }\n}\n","import * as path from \"node:path\";\nimport { glob } from \"tinyglobby\";\nimport type { ScanOptions, ScanResult } from \"@/scan\";\nimport { isDirExists } from \"@/utils/fs\";\nimport { populateToScanResult } from \"./shared\";\n\nexport async function scanURLs(options: ScanOptions = {}): Promise<ScanResult> {\n const ext = options.extensions ?? [\"js\", \"jsx\", \"tsx\", \"md\", \"mdx\"];\n const cwd = options.cwd ?? process.cwd();\n\n async function getFiles() {\n const suffix = ext.length > 0 ? `.{${ext.join(\",\")}}` : \"\";\n\n const appFiles = await glob(`**/page${suffix}`, {\n cwd: (await isDirExists(path.join(cwd, \"src/app\")))\n ? path.join(cwd, \"src/app\")\n : path.join(cwd, \"app\"),\n });\n\n const pagesFiles = await glob(`**/*${suffix}`, {\n cwd: (await isDirExists(path.join(cwd, \"src/pages\")))\n ? path.join(cwd, \"src/pages\")\n : path.join(cwd, \"pages\"),\n });\n\n if (options.pages) appFiles.push(...options.pages);\n\n return [\n ...appFiles.map((file) => {\n const dir = path.dirname(file);\n\n return dir === \".\" ? \"\" : dir;\n }),\n ...pagesFiles.map((file) => {\n const parsed = path.parse(file);\n if (parsed.name === \"index\") return parsed.dir;\n\n return path.join(parsed.dir, parsed.name);\n }),\n ];\n }\n\n // compatibility\n if (options.meta)\n for (const key of Object.keys(options.meta)) {\n if (!key.endsWith(\"page.tsx\")) continue;\n let newKey = path.dirname(key);\n if (newKey === \".\") newKey = \"/\";\n options.meta[newKey] = options.meta[key];\n delete options.meta[key];\n }\n\n const result: ScanResult = { urls: new Map(), fallbackUrls: [] };\n const files = await getFiles();\n\n for (const file of files) {\n populateToScanResult(\n file.length === 0 ? [] : file.split(path.sep),\n options,\n result,\n );\n }\n\n return result;\n}\n","import * as path from \"node:path\";\nimport { glob } from \"tinyglobby\";\nimport type { ScanOptions, ScanResult } from \"@/scan\";\nimport { isDirExists } from \"@/utils/fs\";\nimport { populateToScanResult } from \"./shared\";\n\nexport async function scanURLs(options: ScanOptions = {}): Promise<ScanResult> {\n const ext = options.extensions ?? [\"vue\", \"md\", \"mdx\"];\n const cwd = options.cwd ?? process.cwd();\n\n async function getFiles() {\n const suffix = ext.length > 0 ? `.{${ext.join(\",\")}}` : \"\";\n\n const pagesFiles = await glob(`**/*${suffix}`, {\n cwd: (await isDirExists(path.join(cwd, \"src/pages\")))\n ? path.join(cwd, \"src/pages\")\n : path.join(cwd, \"pages\"),\n });\n\n return pagesFiles.map((file) => {\n const parsed = path.parse(file);\n if (parsed.name === \"index\") return parsed.dir;\n\n return path.join(parsed.dir, parsed.name);\n });\n }\n\n const result: ScanResult = { urls: new Map(), fallbackUrls: [] };\n const files = options.pages ?? (await getFiles());\n\n for (const file of files) {\n populateToScanResult(file.split(path.sep), options, result);\n }\n\n return result;\n}\n","import type { RouteConfig, RouteConfigEntry } from \"@react-router/dev/routes\";\nimport type { ScanOptions, ScanResult } from \"@/scan\";\nimport { populateToScanResult } from \"./shared\";\n\nexport interface ReactRouterScanOptions extends ScanOptions {\n preset: \"react-router\";\n routerConfig: RouteConfig;\n}\n\nexport async function scanURLs(\n options: ReactRouterScanOptions,\n): Promise<ScanResult> {\n const { routerConfig } = options;\n\n async function getFiles() {\n if (options.pages) return options.pages;\n const files: string[] = [];\n const resolved = await routerConfig;\n\n for (const route of resolved) {\n resolveEntryFiles(files, route);\n }\n\n return files;\n }\n\n const result: ScanResult = { urls: new Map(), fallbackUrls: [] };\n\n for (const file of await getFiles()) {\n populateToScanResult(file.split(\"/\"), options, result);\n }\n\n return result;\n}\n\nfunction resolveEntryFiles(\n outputFiles: string[],\n entry: RouteConfigEntry,\n parent?: string[],\n) {\n const fullPath = entry.path?.split(\"/\") ?? [];\n if (parent) fullPath.unshift(...parent);\n\n if (entry.path) {\n const combinations: string[][] = [[]];\n function pushSegment(item: string, newCombination: boolean) {\n if (newCombination) {\n const next = combinations.map((combination) => [...combination, item]);\n\n combinations.push(...next);\n } else {\n for (const combination of combinations) {\n combination.push(item);\n }\n }\n }\n\n for (let i = 0; i < fullPath.length; i++) {\n let name = fullPath[i];\n if (name.length === 0) continue;\n\n const isOptional = name.endsWith(\"?\");\n if (isOptional) {\n name = name.slice(0, -1);\n }\n\n // param\n if (name.startsWith(\":\")) {\n const paramName = name.slice(1);\n pushSegment(`[${paramName}]`, isOptional);\n continue;\n }\n\n // splat\n if (name === \"*\") {\n pushSegment(`[[...splat]]`, isOptional);\n continue;\n }\n\n pushSegment(name, isOptional);\n }\n\n for (const combination of combinations) {\n outputFiles.push(combination.join(\"/\"));\n }\n }\n\n if (entry.children) {\n for (const child of entry.children) {\n resolveEntryFiles(outputFiles, child, fullPath);\n }\n }\n}\n","import * as path from \"node:path\";\nimport { glob } from \"tinyglobby\";\nimport type { ScanOptions, ScanResult } from \"@/scan\";\nimport { isDirExists } from \"@/utils/fs\";\nimport { populateToScanResult } from \"./shared\";\n\nexport async function scanURLs(options: ScanOptions = {}): Promise<ScanResult> {\n const ext = options.extensions ?? [\"tsx\", \"ts\", \"jsx\", \"js\"];\n const cwd = options.cwd ?? process.cwd();\n\n async function getFiles() {\n const suffix = ext.length > 0 ? `.{${ext.join(\",\")}}` : \"\";\n\n const routesFiles = await glob(`**/*${suffix}`, {\n cwd: (await isDirExists(path.join(cwd, \"src/routes\")))\n ? path.join(cwd, \"src/routes\")\n : path.join(cwd, \"routes\"),\n });\n\n const outFiles: string[] = [];\n\n for (const file of routesFiles) {\n let segments = file.replaceAll(\"[.]\", \"#\").split(/[/\\\\.]/);\n // remove file extension\n segments.pop();\n\n // escaped dots -> dots\n segments = segments.map((segment) => segment.replaceAll(\"#\", \".\"));\n if (segments.at(-1)?.startsWith(\"_\")) continue;\n if (segments.at(-1) === \"index\") segments.pop();\n\n const outSegments: string[] = [];\n for (const name of segments) {\n if (name.length === 0) continue;\n\n if (name === \"$\") {\n outSegments.push(`[[..._splat]]`);\n continue;\n }\n\n if (name.startsWith(\"$\")) {\n const paramName = name.slice(1);\n outSegments.push(`[${paramName}]`);\n continue;\n }\n\n outSegments.push(name);\n }\n\n outFiles.push(outSegments.join(\"/\"));\n }\n\n return outFiles;\n }\n\n const result: ScanResult = { urls: new Map(), fallbackUrls: [] };\n const files = options.pages ?? (await getFiles());\n\n for (const file of files) {\n populateToScanResult(file.split(\"/\"), options, result);\n }\n\n return result;\n}\n","import * as path from \"node:path\";\nimport { glob } from \"tinyglobby\";\nimport type { ScanOptions, ScanResult } from \"@/scan\";\nimport { isDirExists } from \"@/utils/fs\";\nimport { populateToScanResult } from \"./shared\";\n\nexport async function scanURLs(options: ScanOptions = {}): Promise<ScanResult> {\n const ext = options.extensions ?? [\"tsx\", \"ts\", \"jsx\", \"js\"];\n const cwd = options.cwd ?? process.cwd();\n\n async function getFiles() {\n const suffix = ext.length > 0 ? `.{${ext.join(\",\")}}` : \"\";\n const pagesFiles = await glob(`**/*${suffix}`, {\n cwd: (await isDirExists(path.join(cwd, \"src/pages\")))\n ? path.join(cwd, \"src/pages\")\n : path.join(cwd, \"pages\"),\n });\n\n const outFiles: string[] = [];\n\n for (const file of pagesFiles) {\n const parsed = path.parse(file);\n if (parsed.name.startsWith(\"_\")) continue;\n\n const segments = parsed.dir.split(path.sep);\n if (parsed.name !== \"index\") segments.push(parsed.name);\n\n const outSegments: string[] = [];\n for (const name of segments) {\n // [...param] is optional in Waku\n if (name.startsWith(\"[...\") && name.endsWith(\"]\")) {\n outSegments.push(`[${name}]`);\n } else if (name.length > 0) {\n outSegments.push(name);\n }\n }\n\n outFiles.push(outSegments.join(\"/\"));\n }\n\n return outFiles;\n }\n\n const result: ScanResult = { urls: new Map(), fallbackUrls: [] };\n const files = options.pages ?? (await getFiles());\n\n for (const file of files) {\n populateToScanResult(file.split(\"/\"), options, result);\n }\n\n return result;\n}\n","import * as Astro from \"./presets/astro\";\nimport * as Next from \"./presets/next\";\nimport * as Nuxt from \"./presets/nuxt\";\nimport * as ReactRouter from \"./presets/react-router\";\nimport * as TanStackStart from \"./presets/tanstack-start\";\nimport * as Waku from \"./presets/waku\";\n\nexport type PopulateParams = Record<\n string,\n {\n value?: string[] | string | Record<string, string[] | string>;\n hashes?: string[];\n queries?: Record<string, string>[];\n }[]\n>;\n\nexport interface ScanOptions {\n /**\n * path of pages (e.g. `/docs/page.tsx`)\n *\n * App Router format only\n **/\n pages?: string[];\n cwd?: string;\n\n populate?: PopulateParams;\n meta?: Record<string, UrlMeta>;\n extensions?: string[];\n}\n\nexport type ScanPresetOptions =\n | (ScanOptions & {\n /**\n * @default next\n */\n preset?: \"next\" | \"astro\" | \"nuxt\" | \"waku\" | \"tanstack-start\";\n })\n | ReactRouter.ReactRouterScanOptions;\n\nexport interface ScanResult {\n urls: Map<string, UrlMeta>;\n\n fallbackUrls: {\n url: RegExp;\n meta: UrlMeta;\n }[];\n}\n\nexport interface UrlMeta {\n hashes?: string[];\n queries?: Record<string, string>[];\n}\n\nexport async function scanURLs(\n options: ScanPresetOptions = {},\n): Promise<ScanResult> {\n switch (options.preset) {\n case \"astro\":\n return Astro.scanURLs(options);\n case \"nuxt\":\n return Nuxt.scanURLs(options);\n case \"react-router\":\n return ReactRouter.scanURLs(options);\n case \"tanstack-start\":\n return TanStackStart.scanURLs(options);\n case \"waku\":\n return Waku.scanURLs(options);\n default:\n return Next.scanURLs(options);\n }\n}\n","/**\n * Split path into segments, trailing/leading slashes are removed\n */\nfunction splitPath(path: string): string[] {\n return path.split(\"/\").filter((p) => p.length > 0);\n}\n\nexport function resolveUrl(base: string, relative: string) {\n const v1 = splitPath(base);\n const v2 = splitPath(relative);\n\n while (v2.length > 0) {\n switch (v2[0]) {\n case \"..\":\n v1.pop();\n break;\n case \".\":\n break;\n default:\n v1.push(v2[0]);\n }\n\n v2.shift();\n }\n\n return v1.join(\"/\");\n}\n","import type { Root, RootContent } from \"mdast\";\nimport { remark } from \"remark\";\nimport remarkGfm from \"remark-gfm\";\nimport remarkMdx from \"remark-mdx\";\nimport type { PluggableList } from \"unified\";\nimport { visit } from \"unist-util-visit\";\nimport type {\n Detector,\n FileObject,\n ResolutionConfig,\n ValidateError,\n} from \"@/validate\";\n\nexport interface MarkdownConfig {\n /**\n * scan href from attributes in MDX components\n */\n components?: Record<\n string,\n {\n attributes: string[];\n }\n >;\n\n remarkPlugins?: PluggableList;\n\n /**\n * control how URLs are scanned from Markdown content, override all default behaviours.\n */\n onNode?: (node: RootContent) => {\n hrefs: string[];\n };\n}\n\nexport function createMarkdownValidator(\n config: MarkdownConfig,\n detector: Detector,\n) {\n const {\n components = {},\n remarkPlugins = [],\n onNode = (node) => {\n if (node.type === \"link\") {\n return {\n hrefs: [node.url],\n };\n }\n\n if (\n (node.type === \"mdxJsxFlowElement\" ||\n node.type === \"mdxJsxTextElement\") &&\n node.name &&\n node.name in components\n ) {\n const analyze = components[node.name];\n\n const hrefs: string[] = [];\n for (const attr of node.attributes) {\n // cannot analyze non-primitive inputs\n if (attr.type !== \"mdxJsxAttribute\" || typeof attr.value !== \"string\")\n continue;\n if (!analyze.attributes.includes(attr.name)) continue;\n\n hrefs.push(attr.value);\n }\n\n return { hrefs };\n }\n },\n } = config;\n\n const mdProcessor = remark().use(remarkGfm).use(remarkPlugins);\n const mdxProcessor = remark()\n .use(remarkMdx)\n .use(remarkGfm)\n .use(remarkPlugins);\n\n return {\n async validate(\n file: FileObject,\n resolution: ResolutionConfig,\n ): Promise<ValidateError[]> {\n const errors: ValidateError[] = [];\n const tasks: Promise<void>[] = [];\n const processor = file.path.endsWith(\".mdx\") ? mdxProcessor : mdProcessor;\n const vfile = {\n path: file.path,\n value: file.content,\n };\n\n let tree = processor.parse(vfile);\n tree = (await processor.run(tree, vfile)) as Root;\n\n visit(tree, (node) => {\n // ignore generated nodes\n if (!node.position || node.type === \"root\") return;\n const pos = node.position;\n const scanned = onNode(node);\n if (!scanned) return;\n\n for (const href of scanned.hrefs) {\n tasks.push(\n detector\n .detect(href, resolution)\n .then((err) => {\n if (!err || err.type !== \"error\") return;\n\n errors.push({\n url: href,\n line: pos.start.line,\n column: pos.start.column,\n reason: err.reason,\n });\n })\n .catch((err: Error) => {\n errors.push({\n url: href,\n line: pos.start.line,\n column: pos.start.column,\n reason: err,\n });\n }),\n );\n }\n });\n\n await Promise.all(tasks);\n return errors;\n },\n };\n}\n","export interface ExternalLinkConfig {\n validate?: (url: URL) => Promise<ExternalLinkResult>;\n}\n\nexport type ExternalLinkResult =\n | {\n success: true;\n }\n | {\n success: false;\n message?: string;\n };\n\nexport function externalLink(config: ExternalLinkConfig) {\n const { validate } = config;\n\n return async (url: string): Promise<ExternalLinkResult> => {\n const parsed = new URL(url);\n if (validate) return validate(parsed);\n\n if (parsed.hostname === \"localhost\") return { success: true };\n\n try {\n const res = await fetch(parsed, {\n method: \"HEAD\",\n });\n\n if (!res.ok) {\n if (res.status === 404) return { success: false, message: \"not found\" };\n // ignore redirect etc.\n if (res.status >= 300 && res.status < 400) return { success: true };\n\n return {\n success: false,\n message: `${url} responded status ${res.status}`,\n };\n }\n\n return { success: true };\n } catch (e) {\n if (e instanceof Error) return { success: false, message: e.message };\n return { success: false };\n }\n };\n}\n","import * as path from \"node:path\";\nimport type { ScanResult } from \"@/scan\";\nimport { type PathToUrl, readFileFromPath } from \"./sample\";\nimport { isFileExists } from \"./utils/fs\";\nimport { resolveUrl } from \"./utils/url\";\nimport {\n createMarkdownValidator,\n type MarkdownConfig,\n} from \"./validate/markdown\";\nimport { externalLink, type ExternalLinkConfig } from \"./utils/external-link\";\n\nexport interface ValidateResult {\n file: string;\n\n /**]\n * @deprecated use `errors` instead\n */\n detected: DetectedError[];\n errors: ValidateError[];\n}\n\nexport interface ValidateError {\n url: string;\n line: number;\n column: number;\n reason: ErrorReason | Error;\n}\n\nexport type ErrorReason = \"not-found\" | \"invalid-fragment\" | \"invalid-query\";\n\nexport interface ResolutionConfig {\n /**\n * Base URL to resolve relative URLs\n */\n baseUrl?: string;\n\n /**\n * Base directory to resolve relative file paths\n */\n baseDir?: string;\n\n /**\n * Generate URL from file paths, used for relative file path detection.\n *\n * Default to searching in input files.\n */\n pathToUrl?: PathToUrl;\n}\n\nexport interface DetectorConfig {\n /**\n * Available URLs (including hashes and query parameters)\n */\n scanned: ScanResult;\n\n /**\n * don't validate the fragment/hash of URLs\n *\n * @defaultValue false\n */\n ignoreFragment?: boolean;\n\n /**\n * don't validate the query of URLs\n *\n * @defaultValue false\n */\n ignoreQuery?: boolean;\n\n /**\n * Check external urls\n *\n * @defaultValue false\n */\n checkExternal?: boolean | ExternalLinkConfig;\n\n /**\n * Check relative paths (e.g. `[My File](./my-file.md)`)\n *\n * - `exists`: ensure the file exists.\n * - `as-url`: resolve & check the public URL of referenced file, requires one of these to be defined:\n * - `pathToUrl` option.\n * - `file.url` in input file objects.\n * - `false` (default): ignore.\n */\n checkRelativePaths?: \"exists\" | \"as-url\" | false;\n\n /**\n * Check relative URLs (e.g. `[My File](./my-page)`)\n *\n * - `true` (default): resolve & check the relative URL, requires one of these to be defined:\n * - `file.url` in input file objects.\n * - `pathToUrl` option.\n * - `baseUrl` option.\n * - `false`: ignore.\n */\n checkRelativeUrls?: boolean;\n\n /**\n * Allowed hrefs, can be:\n * - a list of hrefs\n * - a function that returns `true` for allowed href\n */\n whitelist?: string[] | ((url: string) => boolean);\n\n /**\n * Determinate the type of pathname\n */\n determinatePathname?: (pathname: string) => Awaitable<PathnameType>;\n}\n\nexport interface ValidateConfig extends ResolutionConfig, DetectorConfig {\n markdown?: MarkdownConfig;\n}\n\ntype PathnameType = \"url\" | \"relative-file-path\" | \"relative-url\";\n\ntype Awaitable<T> = T | Promise<T>;\n\nexport interface FileObject {\n path: string;\n content: string;\n\n data?: object;\n\n /**\n * URL of page, required for relative url detection\n */\n url?: string;\n}\n\nconst mdExtensions = [\".md\", \".mdx\"];\nconst supportedExtensions = mdExtensions;\n\n/**\n * Validate markdown files\n *\n * @param files - file paths or file objects\n * @param config - configurations\n */\nexport async function validateFiles(\n files: (string | FileObject)[],\n config: ValidateConfig,\n): Promise<ValidateResult[]> {\n const detector = createDetector(config);\n const markdownValidator = createMarkdownValidator(\n config.markdown ?? {},\n detector,\n );\n\n const normalized = await Promise.all(\n files.map(async (file) =>\n typeof file === \"string\"\n ? await readFileFromPath(file, config.pathToUrl)\n : file,\n ),\n );\n const defaultPathToUrl: PathToUrl = (path) => {\n for (const file of normalized) {\n if (file.path === path && file.url) return file.url;\n }\n };\n\n async function run(file: FileObject): Promise<ValidateResult> {\n const resolution: ResolutionConfig = {\n baseUrl: file.url\n ? file.url.split(\"/\").slice(0, -1).join(\"/\")\n : config.baseUrl,\n baseDir: path.dirname(file.path),\n pathToUrl: config.pathToUrl ?? defaultPathToUrl,\n };\n const ext = path.extname(file.path);\n\n let errors: ValidateError[] = [];\n if (mdExtensions.includes(ext)) {\n errors = await markdownValidator.validate(file, resolution);\n } else {\n console.warn(\n `format unsupported: ${ext}, supported: ${supportedExtensions.join(\n \", \",\n )}`,\n );\n }\n\n return {\n file: file.path,\n errors,\n get detected() {\n return errors.map(generateLegacyError);\n },\n };\n }\n\n return (await Promise.all(normalized.map(run))).filter(\n (err) => err.errors.length > 0,\n );\n}\n\nexport interface Detector {\n detect: (\n href: string,\n resolution: ResolutionConfig,\n ) => Promise<{ type: \"error\"; reason: Error | ErrorReason } | undefined>;\n}\n\nfunction createDetector(config: DetectorConfig): Detector {\n const PathnameRegex = /^([^?#]*)(\\?[^#]*)?(#.*)?$/;\n const {\n checkRelativePaths = false,\n checkExternal = false,\n ignoreFragment = false,\n ignoreQuery = false,\n checkRelativeUrls = true,\n whitelist,\n determinatePathname = (pathname) => {\n if (!pathname.startsWith(\".\")) return \"url\";\n\n if (pathname.endsWith(\".md\") || pathname.endsWith(\".mdx\")) {\n return \"relative-file-path\";\n }\n\n return \"relative-url\";\n },\n } = config;\n const externalLinkChecker =\n checkExternal === false\n ? null\n : externalLink(typeof checkExternal === \"object\" ? checkExternal : {});\n\n let isWhiteListed: ((href: string) => boolean) | undefined;\n if (typeof whitelist === \"function\") {\n isWhiteListed = whitelist;\n } else if (Array.isArray(whitelist)) {\n const whitelistSet = new Set(whitelist);\n isWhiteListed = (href) => whitelistSet.has(href);\n }\n\n function parsePathname(pathname: string) {\n const match = PathnameRegex.exec(pathname);\n if (!match) return { pathname };\n\n return {\n pathname: match[1],\n query: match[2]?.slice(1),\n fragment: match[3]?.slice(1),\n };\n }\n\n return {\n async detect(href, { baseDir, baseUrl, pathToUrl }) {\n if (href.startsWith(\"mailto:\") || isWhiteListed?.(href)) return;\n\n if (href.match(/https?:\\/\\//)) {\n if (!externalLinkChecker) return;\n\n const result = await externalLinkChecker(href);\n if (result.success) return;\n\n return {\n type: \"error\",\n reason: result.message ? new Error(result.message) : \"not-found\",\n };\n }\n\n let { pathname, query, fragment } = parsePathname(href);\n\n if (pathname.length === 0 || pathname === \"./\") return;\n\n switch (await determinatePathname(pathname)) {\n case \"relative-url\":\n if (!checkRelativeUrls) return;\n if (!baseUrl)\n throw new Error(\n `relative URL ${pathname} detected, but 'baseUrl' option is missing.`,\n );\n pathname = resolveUrl(baseUrl, pathname);\n break;\n case \"relative-file-path\": {\n if (!checkRelativePaths) return;\n\n const filePath = path.join(baseDir ?? \"\", pathname);\n if (checkRelativePaths === \"exists\") {\n return (await isFileExists(filePath))\n ? undefined\n : { type: \"error\", reason: \"not-found\" };\n } else if (checkRelativePaths === \"as-url\") {\n if (!pathToUrl)\n throw new Error(\n `'checkRelativePaths: as-url' is set, but 'pathToUrl' option is missing.`,\n );\n\n const url = pathToUrl(filePath);\n if (!url) return;\n pathname = url;\n }\n break;\n }\n }\n\n if (!pathname.startsWith(\"/\")) pathname = `/${pathname}`;\n let meta = config.scanned.urls.get(pathname);\n if (!meta) {\n meta = config.scanned.fallbackUrls.find((fallbackUrl) => {\n return fallbackUrl.url.test(pathname);\n })?.meta;\n }\n\n if (!meta)\n return {\n type: \"error\",\n reason: \"not-found\",\n };\n\n if (\n fragment &&\n !ignoreFragment &&\n meta.hashes &&\n !meta.hashes.includes(fragment)\n ) {\n return { type: \"error\", reason: \"invalid-fragment\" };\n }\n\n if (\n query &&\n !ignoreQuery &&\n meta.queries &&\n !meta.queries.some(\n (item) => new URLSearchParams(item).toString() === query,\n )\n ) {\n return { type: \"error\", reason: \"invalid-query\" };\n }\n },\n };\n}\n\nexport type DetectedError = [\n url: string,\n line: number,\n column: number,\n reason: ErrorReason | Error,\n];\n\nfunction generateLegacyError(v: ValidateError): DetectedError {\n return [v.url, v.line, v.column, v.reason];\n}\n\nexport type {\n ExternalLinkConfig,\n ExternalLinkResult,\n} from \"./utils/external-link\";\n"],"mappings":"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