radiant-docs-validator 0.1.0 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +19 -1
- package/dist/index.js +211 -3
- package/package.json +2 -1
package/dist/index.d.ts
CHANGED
|
@@ -163,8 +163,26 @@ type Footer = {
|
|
|
163
163
|
socials?: Partial<Record<SocialPlatform, string>>;
|
|
164
164
|
links?: FooterLink[];
|
|
165
165
|
};
|
|
166
|
+
type DocsPageHrefResolution = {
|
|
167
|
+
kind: "ignored";
|
|
168
|
+
} | {
|
|
169
|
+
kind: "invalid";
|
|
170
|
+
reason: "not-root-absolute" | "generated-route" | "empty-target";
|
|
171
|
+
href: string;
|
|
172
|
+
pathname: string;
|
|
173
|
+
suffix: string;
|
|
174
|
+
filePath: string;
|
|
175
|
+
suggestedHref?: string;
|
|
176
|
+
} | {
|
|
177
|
+
kind: "docs-page";
|
|
178
|
+
href: string;
|
|
179
|
+
pathname: string;
|
|
180
|
+
suffix: string;
|
|
181
|
+
filePath: string;
|
|
182
|
+
};
|
|
166
183
|
declare function loadOpenApiSpec(filePathOrUrl: string): Promise<any>;
|
|
167
184
|
declare function getConfig(): Promise<DocsConfig>;
|
|
185
|
+
declare function resolveDocsPageHref(href: string): DocsPageHrefResolution;
|
|
168
186
|
declare function validateMdxContent(): Promise<void>;
|
|
169
187
|
|
|
170
|
-
export { type AssistantButtonConfig, type AssistantButtonSize, type AssistantConfig, type AssistantIcon, type AssistantNavbarButtonConfig, BASE_COLOR_OPTIONS, type BaseColorByMode, type BaseColorOption, type CardButtonTheme, type CardCoverTheme, type CardTheme, type CodeSyntaxThemeConfig, type CodeTheme, DEFAULT_THEME_COLOR_DARK, DEFAULT_THEME_COLOR_LIGHT, type DocsConfig, type DocsTheme, type DocsValidatorOptions, type Footer, type FooterLink, type HiddenPageRoute, type Logo, type LogoVariant, type NavGroup, type NavMenu, type NavMenuItem, type NavOpenApi, type NavOpenApiPage, type NavOpenApiPageRef, type NavPage, type NavTag, type NavbarItem, type NavigationItem, type SocialPlatform, type TagTheme, type ThemeColorByMode, configureDocsValidator, getConfig, loadOpenApiSpec, validateMdxContent };
|
|
188
|
+
export { type AssistantButtonConfig, type AssistantButtonSize, type AssistantConfig, type AssistantIcon, type AssistantNavbarButtonConfig, BASE_COLOR_OPTIONS, type BaseColorByMode, type BaseColorOption, type CardButtonTheme, type CardCoverTheme, type CardTheme, type CodeSyntaxThemeConfig, type CodeTheme, DEFAULT_THEME_COLOR_DARK, DEFAULT_THEME_COLOR_LIGHT, type DocsConfig, type DocsPageHrefResolution, type DocsTheme, type DocsValidatorOptions, type Footer, type FooterLink, type HiddenPageRoute, type Logo, type LogoVariant, type NavGroup, type NavMenu, type NavMenuItem, type NavOpenApi, type NavOpenApiPage, type NavOpenApiPageRef, type NavPage, type NavTag, type NavbarItem, type NavigationItem, type SocialPlatform, type TagTheme, type ThemeColorByMode, configureDocsValidator, getConfig, loadOpenApiSpec, resolveDocsPageHref, validateMdxContent };
|
package/dist/index.js
CHANGED
|
@@ -289,6 +289,14 @@ function validateFileExistence(filePath, currentPath) {
|
|
|
289
289
|
);
|
|
290
290
|
}
|
|
291
291
|
}
|
|
292
|
+
function normalizeDocsFilePath(value) {
|
|
293
|
+
let normalized = value.replace(/\\/g, "/").trim();
|
|
294
|
+
if (!normalized) return "";
|
|
295
|
+
normalized = normalized.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
296
|
+
normalized = normalized.replace(/\.(md|mdx)$/i, "");
|
|
297
|
+
const posixNormalized = path.posix.normalize(`/${normalized}`).replace(/^\/+/, "");
|
|
298
|
+
return posixNormalized === "." ? "" : posixNormalized;
|
|
299
|
+
}
|
|
292
300
|
function normalizeDocsPagePath(value, currentPath, label = "Page path") {
|
|
293
301
|
checkType(value, "string", currentPath, label);
|
|
294
302
|
const trimmedPath = value.trim();
|
|
@@ -1975,6 +1983,195 @@ function validateComponentUsage(content) {
|
|
|
1975
1983
|
);
|
|
1976
1984
|
}
|
|
1977
1985
|
}
|
|
1986
|
+
var EXTERNAL_PROTOCOL_REGEX = /^[a-zA-Z][a-zA-Z\d+\-.]*:/;
|
|
1987
|
+
var DOCS_PAGE_SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".md", ".mdx"]);
|
|
1988
|
+
var DOCS_PAGE_ROUTE_EXTENSIONS = /* @__PURE__ */ new Set([".htm", ".html"]);
|
|
1989
|
+
function isIgnoredHref(pathname) {
|
|
1990
|
+
if (!pathname) return true;
|
|
1991
|
+
if (pathname.startsWith("#")) return true;
|
|
1992
|
+
if (pathname.startsWith("?")) return true;
|
|
1993
|
+
if (pathname.startsWith("//")) return true;
|
|
1994
|
+
return EXTERNAL_PROTOCOL_REGEX.test(pathname);
|
|
1995
|
+
}
|
|
1996
|
+
function getHrefExtension(pathname) {
|
|
1997
|
+
return path.posix.extname(pathname.replace(/\\/g, "/")).toLowerCase();
|
|
1998
|
+
}
|
|
1999
|
+
function isDocsPageLikeHref(pathname) {
|
|
2000
|
+
if (isIgnoredHref(pathname)) return false;
|
|
2001
|
+
const extension = getHrefExtension(pathname);
|
|
2002
|
+
if (!extension) return true;
|
|
2003
|
+
if (DOCS_PAGE_SOURCE_EXTENSIONS.has(extension)) return true;
|
|
2004
|
+
return DOCS_PAGE_ROUTE_EXTENSIONS.has(extension);
|
|
2005
|
+
}
|
|
2006
|
+
function resolveDocsPageHref(href) {
|
|
2007
|
+
const { pathname, suffix } = splitHrefPathAndSuffix(href);
|
|
2008
|
+
const baseHref = pathname.trim();
|
|
2009
|
+
if (!isDocsPageLikeHref(baseHref)) {
|
|
2010
|
+
return { kind: "ignored" };
|
|
2011
|
+
}
|
|
2012
|
+
const filePath = normalizeDocsFilePath(baseHref);
|
|
2013
|
+
const baseResult = {
|
|
2014
|
+
href,
|
|
2015
|
+
pathname: baseHref,
|
|
2016
|
+
suffix,
|
|
2017
|
+
filePath
|
|
2018
|
+
};
|
|
2019
|
+
if (!baseHref.startsWith("/")) {
|
|
2020
|
+
return {
|
|
2021
|
+
kind: "invalid",
|
|
2022
|
+
reason: "not-root-absolute",
|
|
2023
|
+
...baseResult,
|
|
2024
|
+
suggestedHref: filePath ? `/${filePath}` : void 0
|
|
2025
|
+
};
|
|
2026
|
+
}
|
|
2027
|
+
const extension = getHrefExtension(baseHref);
|
|
2028
|
+
if (extension && !DOCS_PAGE_SOURCE_EXTENSIONS.has(extension)) {
|
|
2029
|
+
return {
|
|
2030
|
+
kind: "invalid",
|
|
2031
|
+
reason: "generated-route",
|
|
2032
|
+
...baseResult
|
|
2033
|
+
};
|
|
2034
|
+
}
|
|
2035
|
+
if (!filePath) {
|
|
2036
|
+
return {
|
|
2037
|
+
kind: "invalid",
|
|
2038
|
+
reason: "empty-target",
|
|
2039
|
+
...baseResult
|
|
2040
|
+
};
|
|
2041
|
+
}
|
|
2042
|
+
return {
|
|
2043
|
+
kind: "docs-page",
|
|
2044
|
+
...baseResult
|
|
2045
|
+
};
|
|
2046
|
+
}
|
|
2047
|
+
function addRoutableDocsFilePath(routableFilePaths, filePath) {
|
|
2048
|
+
if (typeof filePath !== "string") return;
|
|
2049
|
+
const normalizedFilePath = normalizeDocsFilePath(filePath);
|
|
2050
|
+
if (normalizedFilePath) {
|
|
2051
|
+
routableFilePaths.add(normalizedFilePath);
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
function collectRoutablePageItems(routableFilePaths, items) {
|
|
2055
|
+
if (!Array.isArray(items)) return;
|
|
2056
|
+
for (const item of items) {
|
|
2057
|
+
if (typeof item === "string") {
|
|
2058
|
+
addRoutableDocsFilePath(routableFilePaths, item);
|
|
2059
|
+
continue;
|
|
2060
|
+
}
|
|
2061
|
+
if (!item || typeof item !== "object") continue;
|
|
2062
|
+
if ("page" in item) {
|
|
2063
|
+
addRoutableDocsFilePath(routableFilePaths, item.page);
|
|
2064
|
+
continue;
|
|
2065
|
+
}
|
|
2066
|
+
if ("group" in item) {
|
|
2067
|
+
collectRoutablePageItems(
|
|
2068
|
+
routableFilePaths,
|
|
2069
|
+
item.pages
|
|
2070
|
+
);
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
function collectRoutableNavigationPages(routableFilePaths, config) {
|
|
2075
|
+
collectRoutablePageItems(
|
|
2076
|
+
routableFilePaths,
|
|
2077
|
+
config.navigation.pages
|
|
2078
|
+
);
|
|
2079
|
+
for (const item of config.navigation.menu?.items ?? []) {
|
|
2080
|
+
collectRoutablePageItems(
|
|
2081
|
+
routableFilePaths,
|
|
2082
|
+
item.submenu.pages
|
|
2083
|
+
);
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
function buildDocsLinkIndex(config, files) {
|
|
2087
|
+
const allDocsFilePaths = /* @__PURE__ */ new Set();
|
|
2088
|
+
const routableDocsFilePaths = /* @__PURE__ */ new Set();
|
|
2089
|
+
for (const file of files) {
|
|
2090
|
+
const relativePath = path.relative(DOCS_DIR, file);
|
|
2091
|
+
const normalizedFilePath = normalizeDocsFilePath(relativePath);
|
|
2092
|
+
if (normalizedFilePath) {
|
|
2093
|
+
allDocsFilePaths.add(normalizedFilePath);
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
collectRoutableNavigationPages(routableDocsFilePaths, config);
|
|
2097
|
+
addRoutableDocsFilePath(routableDocsFilePaths, config.home);
|
|
2098
|
+
for (const route of config.hiddenPageRoutes ?? []) {
|
|
2099
|
+
addRoutableDocsFilePath(routableDocsFilePaths, route.filePath);
|
|
2100
|
+
}
|
|
2101
|
+
return {
|
|
2102
|
+
allDocsFilePaths,
|
|
2103
|
+
routableDocsFilePaths
|
|
2104
|
+
};
|
|
2105
|
+
}
|
|
2106
|
+
function validateDocsRootAbsoluteHref(args) {
|
|
2107
|
+
const resolvedHref = resolveDocsPageHref(args.href);
|
|
2108
|
+
if (resolvedHref.kind === "ignored") return;
|
|
2109
|
+
if (resolvedHref.kind === "invalid") {
|
|
2110
|
+
if (resolvedHref.reason === "not-root-absolute") {
|
|
2111
|
+
const suggestion = resolvedHref.suggestedHref ? ` Use "${resolvedHref.suggestedHref}" instead.` : "";
|
|
2112
|
+
throw new Error(
|
|
2113
|
+
`Invalid internal link "${args.href}" in ${args.sourceFile}. Docs page links must be docs-root absolute paths like "/guides/quickstart".${suggestion}`
|
|
2114
|
+
);
|
|
2115
|
+
}
|
|
2116
|
+
if (resolvedHref.reason === "generated-route") {
|
|
2117
|
+
throw new Error(
|
|
2118
|
+
`Invalid internal link "${args.href}" in ${args.sourceFile}. Docs page links must point to MDX source paths from the docs root, not generated HTML routes. Use a path like "/guides/quickstart" or "/guides/quickstart.mdx".`
|
|
2119
|
+
);
|
|
2120
|
+
}
|
|
2121
|
+
throw new Error(
|
|
2122
|
+
`Invalid internal link "${args.href}" in ${args.sourceFile}. Docs page links must point to a docs page file from the docs root, such as "/guides/quickstart".`
|
|
2123
|
+
);
|
|
2124
|
+
}
|
|
2125
|
+
if (args.linkIndex.routableDocsFilePaths.has(resolvedHref.filePath)) {
|
|
2126
|
+
return;
|
|
2127
|
+
}
|
|
2128
|
+
if (args.linkIndex.allDocsFilePaths.has(resolvedHref.filePath)) {
|
|
2129
|
+
throw new Error(
|
|
2130
|
+
`Invalid internal link "${args.href}" in ${args.sourceFile}. The target file exists ("${resolvedHref.filePath}.mdx"), but it is not a routable docs page. Add it to docs.json navigation, set it as the home page, or link to it from the navbar or footer so it is included in the built site.`
|
|
2131
|
+
);
|
|
2132
|
+
}
|
|
2133
|
+
throw new Error(
|
|
2134
|
+
`Invalid internal link "${args.href}" in ${args.sourceFile}. No matching docs page file was found. Expected "${resolvedHref.filePath}.mdx" under the docs root.`
|
|
2135
|
+
);
|
|
2136
|
+
}
|
|
2137
|
+
function visitMdxTree(node, visitor) {
|
|
2138
|
+
if (!node || typeof node !== "object") return;
|
|
2139
|
+
const typedNode = node;
|
|
2140
|
+
visitor(node);
|
|
2141
|
+
if (!Array.isArray(typedNode.children)) return;
|
|
2142
|
+
for (const child of typedNode.children) {
|
|
2143
|
+
visitMdxTree(child, visitor);
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
function createInternalLinkValidationPlugin(args) {
|
|
2147
|
+
return () => (tree) => {
|
|
2148
|
+
visitMdxTree(tree, (node) => {
|
|
2149
|
+
if ((node.type === "link" || node.type === "definition") && typeof node.url === "string") {
|
|
2150
|
+
validateDocsRootAbsoluteHref({
|
|
2151
|
+
href: node.url,
|
|
2152
|
+
sourceFile: args.sourceFile,
|
|
2153
|
+
linkIndex: args.linkIndex
|
|
2154
|
+
});
|
|
2155
|
+
}
|
|
2156
|
+
if (node.type !== "mdxJsxFlowElement" && node.type !== "mdxJsxTextElement") {
|
|
2157
|
+
return;
|
|
2158
|
+
}
|
|
2159
|
+
const element = node;
|
|
2160
|
+
if (!Array.isArray(element.attributes)) return;
|
|
2161
|
+
for (const attribute of element.attributes) {
|
|
2162
|
+
const hrefAttribute = attribute;
|
|
2163
|
+
if (hrefAttribute.type !== "mdxJsxAttribute") continue;
|
|
2164
|
+
if (hrefAttribute.name !== "href") continue;
|
|
2165
|
+
if (typeof hrefAttribute.value !== "string") continue;
|
|
2166
|
+
validateDocsRootAbsoluteHref({
|
|
2167
|
+
href: hrefAttribute.value,
|
|
2168
|
+
sourceFile: args.sourceFile,
|
|
2169
|
+
linkIndex: args.linkIndex
|
|
2170
|
+
});
|
|
2171
|
+
}
|
|
2172
|
+
});
|
|
2173
|
+
};
|
|
2174
|
+
}
|
|
1978
2175
|
function getMdxFiles(dir) {
|
|
1979
2176
|
let results = [];
|
|
1980
2177
|
const list = fs.readdirSync(dir);
|
|
@@ -1983,7 +2180,7 @@ function getMdxFiles(dir) {
|
|
|
1983
2180
|
const stat = fs.statSync(file);
|
|
1984
2181
|
if (stat && stat.isDirectory()) {
|
|
1985
2182
|
results = results.concat(getMdxFiles(file));
|
|
1986
|
-
} else if (
|
|
2183
|
+
} else if (/\.(md|mdx)$/i.test(file)) {
|
|
1987
2184
|
results.push(file);
|
|
1988
2185
|
}
|
|
1989
2186
|
});
|
|
@@ -1991,8 +2188,11 @@ function getMdxFiles(dir) {
|
|
|
1991
2188
|
}
|
|
1992
2189
|
async function validateMdxContent() {
|
|
1993
2190
|
assertConfigured();
|
|
2191
|
+
const config = await getConfig();
|
|
1994
2192
|
const files = getMdxFiles(DOCS_DIR);
|
|
2193
|
+
const linkIndex = buildDocsLinkIndex(config, files);
|
|
1995
2194
|
for (const file of files) {
|
|
2195
|
+
const relativePath = path.relative(DOCS_DIR, file);
|
|
1996
2196
|
try {
|
|
1997
2197
|
const content = fs.readFileSync(file, "utf-8");
|
|
1998
2198
|
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
@@ -2008,9 +2208,16 @@ async function validateMdxContent() {
|
|
|
2008
2208
|
}
|
|
2009
2209
|
}
|
|
2010
2210
|
validateComponentUsage(content);
|
|
2011
|
-
await compile(content, {
|
|
2211
|
+
await compile(content, {
|
|
2212
|
+
jsx: true,
|
|
2213
|
+
remarkPlugins: [
|
|
2214
|
+
createInternalLinkValidationPlugin({
|
|
2215
|
+
sourceFile: relativePath,
|
|
2216
|
+
linkIndex
|
|
2217
|
+
})
|
|
2218
|
+
]
|
|
2219
|
+
});
|
|
2012
2220
|
} catch (e) {
|
|
2013
|
-
const relativePath = path.relative(DOCS_DIR, file);
|
|
2014
2221
|
const location = e.line ? `:${e.line}:${e.column}` : "";
|
|
2015
2222
|
const reason = e.reason || e.message;
|
|
2016
2223
|
throw new Error(
|
|
@@ -2031,5 +2238,6 @@ export {
|
|
|
2031
2238
|
getConfig,
|
|
2032
2239
|
isBundledShikiThemeName,
|
|
2033
2240
|
loadOpenApiSpec,
|
|
2241
|
+
resolveDocsPageHref,
|
|
2034
2242
|
validateMdxContent
|
|
2035
2243
|
};
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "radiant-docs-validator",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Shared validation for Radiant documentation repositories",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"build": "tsup src/index.ts src/frontmatter-schema.ts src/shiki-theme-config.ts --format esm --dts --clean",
|
|
8
|
+
"test": "npm run build && node --test test/*.test.mjs",
|
|
8
9
|
"prepublishOnly": "npm run build",
|
|
9
10
|
"release": "node scripts/release.js"
|
|
10
11
|
},
|