radiant-docs 0.1.7 → 0.1.8
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.js +28 -5
- package/package.json +3 -3
- package/template/astro.config.mjs +76 -3
- package/template/package-lock.json +924 -737
- package/template/package.json +7 -5
- package/template/scripts/generate-og-images.mjs +335 -0
- package/template/scripts/generate-og-metadata.mjs +173 -0
- package/template/scripts/rewrite-static-asset-host.mjs +408 -0
- package/template/scripts/stamp-image-versions.mjs +277 -0
- package/template/scripts/stamp-og-image-versions.mjs +199 -0
- package/template/scripts/stamp-pagefind-runtime-version.mjs +140 -0
- package/template/src/assets/fonts/geist-mono/cyrillic.woff2 +0 -0
- package/template/src/assets/fonts/geist-mono/latin-ext.woff2 +0 -0
- package/template/src/assets/fonts/geist-mono/latin.woff2 +0 -0
- package/template/src/assets/fonts/google-sans-flex/canadian-aboriginal.woff2 +0 -0
- package/template/src/assets/fonts/google-sans-flex/cherokee.woff2 +0 -0
- package/template/src/assets/fonts/google-sans-flex/latin-ext.woff2 +0 -0
- package/template/src/assets/fonts/google-sans-flex/latin.woff2 +0 -0
- package/template/src/assets/fonts/google-sans-flex/math.woff2 +0 -0
- package/template/src/assets/fonts/google-sans-flex/nushu.woff2 +0 -0
- package/template/src/assets/fonts/google-sans-flex/symbols.woff2 +0 -0
- package/template/src/assets/fonts/google-sans-flex/syriac.woff2 +0 -0
- package/template/src/assets/fonts/google-sans-flex/tifinagh.woff2 +0 -0
- package/template/src/assets/fonts/google-sans-flex/vietnamese.woff2 +0 -0
- package/template/src/components/Footer.astro +94 -0
- package/template/src/components/Header.astro +11 -66
- package/template/src/components/LogoLink.astro +103 -0
- package/template/src/components/MdxPage.astro +126 -11
- package/template/src/components/OpenApiPage.astro +1036 -69
- package/template/src/components/Search.astro +0 -2
- package/template/src/components/SidebarDropdown.astro +34 -14
- package/template/src/components/SidebarGroup.astro +3 -6
- package/template/src/components/SidebarLink.astro +22 -12
- package/template/src/components/SidebarMenu.astro +19 -16
- package/template/src/components/SidebarSegmented.astro +99 -0
- package/template/src/components/SidebarSubgroup.astro +12 -12
- package/template/src/components/ThemeSwitcher.astro +30 -7
- package/template/src/components/endpoint/PlaygroundBar.astro +32 -36
- package/template/src/components/endpoint/PlaygroundButton.astro +40 -4
- package/template/src/components/endpoint/PlaygroundField.astro +1068 -22
- package/template/src/components/endpoint/PlaygroundForm.astro +559 -61
- package/template/src/components/endpoint/RequestSnippets.astro +342 -193
- package/template/src/components/endpoint/ResponseDisplay.astro +161 -147
- package/template/src/components/endpoint/ResponseFieldTree.astro +134 -0
- package/template/src/components/endpoint/ResponseFields.astro +711 -68
- package/template/src/components/endpoint/ResponseSnippets.astro +299 -173
- package/template/src/components/sidebar/SidebarEndpointLink.astro +1 -1
- package/template/src/components/ui/CodeLanguageIcon.astro +19 -0
- package/template/src/components/ui/CodeTabEdge.astro +79 -0
- package/template/src/components/ui/Field.astro +103 -20
- package/template/src/components/ui/Icon.astro +32 -0
- package/template/src/components/ui/ListChevronsToggle.astro +31 -0
- package/template/src/components/ui/Tag.astro +1 -1
- package/template/src/components/user/{Accordian.astro → Accordion.astro} +6 -6
- package/template/src/components/user/Callout.astro +5 -9
- package/template/src/components/user/CodeBlock.astro +400 -0
- package/template/src/components/user/CodeGroup.astro +225 -0
- package/template/src/components/user/ComponentPreview.astro +1 -0
- package/template/src/components/user/ComponentPreviewBlock.astro +181 -0
- package/template/src/components/user/Image.astro +132 -0
- package/template/src/components/user/Steps.astro +1 -3
- package/template/src/components/user/Tabs.astro +2 -2
- package/template/src/content.config.ts +1 -0
- package/template/src/layouts/Layout.astro +109 -8
- package/template/src/lib/code/code-block.ts +546 -0
- package/template/src/lib/frontmatter-schema.ts +8 -7
- package/template/src/lib/mdx/remark-code-block-component.ts +342 -0
- package/template/src/lib/mdx/remark-demote-h1.ts +16 -0
- package/template/src/lib/pagefind.ts +19 -5
- package/template/src/lib/routes.ts +49 -31
- package/template/src/lib/utils.ts +20 -0
- package/template/src/lib/validation.ts +638 -200
- package/template/src/pages/[...slug].astro +18 -5
- package/template/src/styles/geist-mono.css +33 -0
- package/template/src/styles/global.css +89 -84
- package/template/src/styles/google-sans-flex.css +143 -0
- package/template/ec.config.mjs +0 -51
- /package/template/src/components/user/{AccordianGroup.astro → AccordionGroup.astro} +0 -0
|
@@ -12,6 +12,95 @@ const CWD = process.cwd();
|
|
|
12
12
|
const DOCS_DIR = path.join(CWD, "src/content/docs");
|
|
13
13
|
const CONFIG_PATH = path.join(DOCS_DIR, "docs.json");
|
|
14
14
|
|
|
15
|
+
// Cache for icon sets (key: prefix, value: Set of icon names)
|
|
16
|
+
const iconSets = new Map<string, Set<string>>();
|
|
17
|
+
|
|
18
|
+
// Helper function to check if a string is a URL
|
|
19
|
+
function isUrl(str: string): boolean {
|
|
20
|
+
try {
|
|
21
|
+
const url = new URL(str);
|
|
22
|
+
return url.protocol === "http:" || url.protocol === "https:";
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getIconSet(prefix: string): Set<string> | null {
|
|
29
|
+
if (iconSets.has(prefix)) return iconSets.get(prefix)!;
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const iconsPath = path.join(
|
|
33
|
+
CWD,
|
|
34
|
+
`node_modules/@iconify-json/${prefix}/icons.json`,
|
|
35
|
+
);
|
|
36
|
+
if (!fs.existsSync(iconsPath)) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const iconsData = JSON.parse(fs.readFileSync(iconsPath, "utf-8"));
|
|
40
|
+
const set = new Set(Object.keys(iconsData.icons));
|
|
41
|
+
iconSets.set(prefix, set);
|
|
42
|
+
return set;
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error(`Failed to load icon set for prefix "${prefix}":`, error);
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function validateIcon(icon: any, currentPath: Path): void {
|
|
50
|
+
if (icon === undefined || icon === null) return;
|
|
51
|
+
|
|
52
|
+
if (typeof icon !== "string") {
|
|
53
|
+
throwConfigError("Icon must be a string.", currentPath);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 1. Handle remote URLs
|
|
57
|
+
if (isUrl(icon)) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 2. Handle Iconify icons (prefix:name)
|
|
62
|
+
if (icon.includes(":")) {
|
|
63
|
+
const parts = icon.split(":");
|
|
64
|
+
|
|
65
|
+
if (parts.length !== 2 || !parts[0] || !parts[1]) {
|
|
66
|
+
throwConfigError(
|
|
67
|
+
`Invalid library icon format: "${icon}". Icons must follow the "library-prefix:name" format (e.g., "lucide:home") or be a local path.`,
|
|
68
|
+
currentPath,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const [prefix, name] = parts;
|
|
73
|
+
const icons = getIconSet(prefix);
|
|
74
|
+
|
|
75
|
+
if (icons) {
|
|
76
|
+
if (!icons.has(name)) {
|
|
77
|
+
throwConfigError(
|
|
78
|
+
`Invalid icon name: "${name}" for library "${prefix}". Is this a typo?`,
|
|
79
|
+
currentPath,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
throwConfigError(
|
|
84
|
+
`Invalid icon library: "${prefix}". Is this package installed in @iconify-json?`,
|
|
85
|
+
currentPath,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 3. Handle local icons
|
|
92
|
+
// Check if it's a file path (must exist in DOCS_DIR)
|
|
93
|
+
const localRelativePath = icon.startsWith("/") ? icon.slice(1) : icon;
|
|
94
|
+
const localPath = path.join(DOCS_DIR, localRelativePath);
|
|
95
|
+
|
|
96
|
+
if (!fs.existsSync(localPath)) {
|
|
97
|
+
throwConfigError(
|
|
98
|
+
`Icon not found: "${icon}". Local icons must exist in your repository. Did you mean to use an library icon like "lucide:home"?`,
|
|
99
|
+
currentPath,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
15
104
|
// Define the list of available user components for MDX
|
|
16
105
|
const AVAILABLE_COMPONENTS = [
|
|
17
106
|
"Callout",
|
|
@@ -19,15 +108,27 @@ const AVAILABLE_COMPONENTS = [
|
|
|
19
108
|
"Tab",
|
|
20
109
|
"Steps",
|
|
21
110
|
"Step",
|
|
22
|
-
"
|
|
23
|
-
"
|
|
111
|
+
"Accordion",
|
|
112
|
+
"AccordionGroup",
|
|
113
|
+
"Image",
|
|
114
|
+
"CodeGroup",
|
|
115
|
+
"ComponentPreview",
|
|
24
116
|
];
|
|
25
117
|
|
|
26
|
-
|
|
118
|
+
// Internal components can be valid in MDX while remaining hidden from
|
|
119
|
+
// user-facing error guidance.
|
|
120
|
+
const INTERNAL_ONLY_COMPONENTS = new Set(["ComponentPreview"]);
|
|
121
|
+
|
|
122
|
+
export type NavPage = {
|
|
123
|
+
page: string;
|
|
124
|
+
icon?: string | null;
|
|
125
|
+
tag?: string;
|
|
126
|
+
title?: string;
|
|
127
|
+
};
|
|
27
128
|
export type NavGroup = {
|
|
28
129
|
group: string;
|
|
29
130
|
pages: (string | NavPage | NavGroup)[];
|
|
30
|
-
icon?: string;
|
|
131
|
+
icon?: string | null;
|
|
31
132
|
expanded?: boolean; // need to add this logic
|
|
32
133
|
tag?: string;
|
|
33
134
|
};
|
|
@@ -37,8 +138,7 @@ export type NavOpenApi = {
|
|
|
37
138
|
exclude?: string[];
|
|
38
139
|
};
|
|
39
140
|
export type NavigationItem = {
|
|
40
|
-
pages?: (string | NavPage)[];
|
|
41
|
-
groups?: NavGroup[];
|
|
141
|
+
pages?: (string | NavPage | NavGroup)[];
|
|
42
142
|
menu?: NavMenu;
|
|
43
143
|
openapi?: string | NavOpenApi;
|
|
44
144
|
};
|
|
@@ -46,22 +146,30 @@ export type NavigationItem = {
|
|
|
46
146
|
export type NavMenuItem = {
|
|
47
147
|
label: string;
|
|
48
148
|
submenu: Omit<NavigationItem, "menu">;
|
|
49
|
-
icon?: string;
|
|
149
|
+
icon?: string | null;
|
|
50
150
|
};
|
|
51
151
|
export type NavMenu = {
|
|
52
|
-
type?: "dropdown" | "
|
|
152
|
+
type?: "dropdown" | "segmented";
|
|
53
153
|
label?: string;
|
|
54
154
|
items: NavMenuItem[];
|
|
55
155
|
};
|
|
56
156
|
export type NavbarItem = {
|
|
57
157
|
text: string;
|
|
58
158
|
href: string;
|
|
59
|
-
icon?: string;
|
|
159
|
+
icon?: string | null;
|
|
160
|
+
};
|
|
161
|
+
export type LogoVariant = string | {
|
|
162
|
+
image: string;
|
|
163
|
+
padding?: {
|
|
164
|
+
top?: number;
|
|
165
|
+
bottom?: number;
|
|
166
|
+
};
|
|
60
167
|
};
|
|
61
168
|
export type Logo = {
|
|
62
|
-
light?:
|
|
63
|
-
dark?:
|
|
169
|
+
light?: LogoVariant;
|
|
170
|
+
dark?: LogoVariant;
|
|
64
171
|
href?: string;
|
|
172
|
+
pill?: string | false;
|
|
65
173
|
};
|
|
66
174
|
export type DocsConfig = {
|
|
67
175
|
title: string;
|
|
@@ -74,6 +182,36 @@ export type DocsConfig = {
|
|
|
74
182
|
secondary?: NavbarItem;
|
|
75
183
|
links?: NavbarItem[];
|
|
76
184
|
};
|
|
185
|
+
playground?: {
|
|
186
|
+
proxy?: boolean;
|
|
187
|
+
};
|
|
188
|
+
footer?: Footer;
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
export type SocialPlatform =
|
|
192
|
+
| "x"
|
|
193
|
+
| "website"
|
|
194
|
+
| "facebook"
|
|
195
|
+
| "youtube"
|
|
196
|
+
| "discord"
|
|
197
|
+
| "slack"
|
|
198
|
+
| "github"
|
|
199
|
+
| "linkedin"
|
|
200
|
+
| "instagram"
|
|
201
|
+
| "hacker-news"
|
|
202
|
+
| "medium"
|
|
203
|
+
| "telegram"
|
|
204
|
+
| "bluesky"
|
|
205
|
+
| "threads"
|
|
206
|
+
| "reddit"
|
|
207
|
+
| "podcast";
|
|
208
|
+
export type FooterLink = {
|
|
209
|
+
text: string;
|
|
210
|
+
href: string;
|
|
211
|
+
};
|
|
212
|
+
export type Footer = {
|
|
213
|
+
socials?: Partial<Record<SocialPlatform, string>>;
|
|
214
|
+
links?: FooterLink[];
|
|
77
215
|
};
|
|
78
216
|
type Path = (string | number)[];
|
|
79
217
|
|
|
@@ -92,9 +230,9 @@ function checkType(
|
|
|
92
230
|
value: any,
|
|
93
231
|
type: "string" | "boolean" | "array" | "object",
|
|
94
232
|
currentPath: Path,
|
|
95
|
-
label: string
|
|
233
|
+
label: string,
|
|
96
234
|
): void {
|
|
97
|
-
if (value === undefined) return;
|
|
235
|
+
if (value === undefined || value === null) return;
|
|
98
236
|
|
|
99
237
|
if (type === "array") {
|
|
100
238
|
if (!Array.isArray(value))
|
|
@@ -115,19 +253,37 @@ function validateFileExistence(filePath: string, currentPath: Path): void {
|
|
|
115
253
|
if (!fs.existsSync(fullPath)) {
|
|
116
254
|
throwConfigError(
|
|
117
255
|
`Referenced file not found. Expected: ${filePath}`,
|
|
118
|
-
currentPath
|
|
256
|
+
currentPath,
|
|
119
257
|
);
|
|
120
258
|
}
|
|
121
259
|
}
|
|
122
260
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
261
|
+
function normalizeDocsPagePath(
|
|
262
|
+
value: string,
|
|
263
|
+
currentPath: Path,
|
|
264
|
+
label: string = "Page path",
|
|
265
|
+
): string {
|
|
266
|
+
checkType(value, "string", currentPath, label);
|
|
267
|
+
|
|
268
|
+
const trimmedPath = value.trim();
|
|
269
|
+
if (trimmedPath === "") {
|
|
270
|
+
throwConfigError(`${label} cannot be an empty string`, currentPath);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (isUrl(trimmedPath)) {
|
|
274
|
+
throwConfigError(
|
|
275
|
+
`${label} must reference a documentation page path, not a URL`,
|
|
276
|
+
currentPath,
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const normalizedPath = trimmedPath.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
281
|
+
|
|
282
|
+
if (normalizedPath === "") {
|
|
283
|
+
throwConfigError(`${label} cannot be '/'`, currentPath);
|
|
130
284
|
}
|
|
285
|
+
|
|
286
|
+
return normalizedPath;
|
|
131
287
|
}
|
|
132
288
|
|
|
133
289
|
// Cache for OpenAPI specs (key: filePathOrUrl, value: parsed spec)
|
|
@@ -150,7 +306,7 @@ export async function loadOpenApiSpec(filePathOrUrl: string): Promise<any> {
|
|
|
150
306
|
const response = await fetch(filePathOrUrl);
|
|
151
307
|
if (!response.ok) {
|
|
152
308
|
throw new Error(
|
|
153
|
-
`Failed to fetch OpenAPI spec: ${response.status} ${response.statusText}
|
|
309
|
+
`Failed to fetch OpenAPI spec: ${response.status} ${response.statusText}`,
|
|
154
310
|
);
|
|
155
311
|
}
|
|
156
312
|
fileContent = await response.text();
|
|
@@ -158,7 +314,7 @@ export async function loadOpenApiSpec(filePathOrUrl: string): Promise<any> {
|
|
|
158
314
|
throw new Error(
|
|
159
315
|
`Failed to fetch OpenAPI spec from URL: ${
|
|
160
316
|
error instanceof Error ? error.message : String(error)
|
|
161
|
-
}
|
|
317
|
+
}`,
|
|
162
318
|
);
|
|
163
319
|
}
|
|
164
320
|
} else {
|
|
@@ -174,7 +330,7 @@ export async function loadOpenApiSpec(filePathOrUrl: string): Promise<any> {
|
|
|
174
330
|
trimmedContent.startsWith("<html")
|
|
175
331
|
) {
|
|
176
332
|
throw new Error(
|
|
177
|
-
"The URL does not return a valid OpenAPI specification. The URL appears to return HTML instead of JSON or YAML."
|
|
333
|
+
"The URL does not return a valid OpenAPI specification. The URL appears to return HTML instead of JSON or YAML.",
|
|
178
334
|
);
|
|
179
335
|
}
|
|
180
336
|
|
|
@@ -193,7 +349,7 @@ export async function loadOpenApiSpec(filePathOrUrl: string): Promise<any> {
|
|
|
193
349
|
} catch (parseError) {
|
|
194
350
|
if (parseError instanceof SyntaxError) {
|
|
195
351
|
throw new Error(
|
|
196
|
-
`The URL does not return a valid OpenAPI specification. Failed to parse as JSON or YAML: ${parseError.message}
|
|
352
|
+
`The URL does not return a valid OpenAPI specification. Failed to parse as JSON or YAML: ${parseError.message}`,
|
|
197
353
|
);
|
|
198
354
|
}
|
|
199
355
|
throw parseError;
|
|
@@ -212,13 +368,13 @@ async function validateOpenApiFile(filePathOrUrl: string, currentPath: Path) {
|
|
|
212
368
|
// For local files, validate extension and existence
|
|
213
369
|
const validExtensions = [".json", ".yaml", ".yml"];
|
|
214
370
|
const hasValidExtension = validExtensions.some((ext) =>
|
|
215
|
-
filePathOrUrl.toLowerCase().endsWith(ext)
|
|
371
|
+
filePathOrUrl.toLowerCase().endsWith(ext),
|
|
216
372
|
);
|
|
217
373
|
|
|
218
374
|
if (!hasValidExtension) {
|
|
219
375
|
throwConfigError(
|
|
220
376
|
`OpenAPI file must have a valid extension (.json, .yaml, or .yml). Found: ${filePathOrUrl}`,
|
|
221
|
-
currentPath
|
|
377
|
+
currentPath,
|
|
222
378
|
);
|
|
223
379
|
}
|
|
224
380
|
|
|
@@ -227,7 +383,7 @@ async function validateOpenApiFile(filePathOrUrl: string, currentPath: Path) {
|
|
|
227
383
|
if (!fs.existsSync(fullPath)) {
|
|
228
384
|
throwConfigError(
|
|
229
385
|
`Referenced OpenAPI file not found. Expected: ${filePathOrUrl}`,
|
|
230
|
-
currentPath
|
|
386
|
+
currentPath,
|
|
231
387
|
);
|
|
232
388
|
}
|
|
233
389
|
} else {
|
|
@@ -237,13 +393,13 @@ async function validateOpenApiFile(filePathOrUrl: string, currentPath: Path) {
|
|
|
237
393
|
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
238
394
|
throwConfigError(
|
|
239
395
|
`OpenAPI URL must use http:// or https:// protocol. Found: ${filePathOrUrl}`,
|
|
240
|
-
currentPath
|
|
396
|
+
currentPath,
|
|
241
397
|
);
|
|
242
398
|
}
|
|
243
399
|
} catch (error) {
|
|
244
400
|
throwConfigError(
|
|
245
401
|
`Invalid OpenAPI URL format: ${filePathOrUrl}`,
|
|
246
|
-
currentPath
|
|
402
|
+
currentPath,
|
|
247
403
|
);
|
|
248
404
|
}
|
|
249
405
|
}
|
|
@@ -251,37 +407,70 @@ async function validateOpenApiFile(filePathOrUrl: string, currentPath: Path) {
|
|
|
251
407
|
// Validate the OpenAPI spec using Spectral (works for both files and URLs)
|
|
252
408
|
try {
|
|
253
409
|
const document = await loadOpenApiSpec(filePathOrUrl);
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
formats: oas.formats,
|
|
410
|
+
const tolerantRuleset = {
|
|
411
|
+
...oas,
|
|
257
412
|
rules: {
|
|
258
|
-
|
|
413
|
+
...oas.rules,
|
|
414
|
+
"oas3-schema": {
|
|
415
|
+
...oas.rules["oas3-schema"],
|
|
416
|
+
severity: 1,
|
|
417
|
+
},
|
|
418
|
+
"oas2-schema": {
|
|
419
|
+
...oas.rules["oas2-schema"],
|
|
420
|
+
severity: 1,
|
|
421
|
+
},
|
|
259
422
|
},
|
|
260
423
|
};
|
|
261
424
|
|
|
262
425
|
const spectral = new Spectral();
|
|
263
426
|
|
|
264
|
-
spectral.setRuleset(
|
|
427
|
+
spectral.setRuleset(tolerantRuleset);
|
|
265
428
|
|
|
266
429
|
let results = await spectral.run(document);
|
|
430
|
+
const blockingResults = results.filter(
|
|
431
|
+
(result) => result.code === "unrecognized-format",
|
|
432
|
+
);
|
|
267
433
|
|
|
268
|
-
if (
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
.
|
|
272
|
-
|
|
273
|
-
const pathStr =
|
|
274
|
-
result.path.length > 0 ? result.path.join(".") : "root";
|
|
275
|
-
return `${result.message} (at ${pathStr})`;
|
|
276
|
-
});
|
|
434
|
+
if (blockingResults.length > 0) {
|
|
435
|
+
const errorMessages = blockingResults.slice(0, 5).map((result) => {
|
|
436
|
+
const pathStr = result.path.length > 0 ? result.path.join(".") : "root";
|
|
437
|
+
return `${result.message} (at ${pathStr})`;
|
|
438
|
+
});
|
|
277
439
|
|
|
278
440
|
const errorText = errorMessages.join("; ");
|
|
279
441
|
const moreErrors =
|
|
280
|
-
|
|
442
|
+
blockingResults.length > 5
|
|
443
|
+
? ` (and ${blockingResults.length - 5} more errors)`
|
|
444
|
+
: "";
|
|
281
445
|
|
|
282
446
|
throwConfigError(
|
|
283
447
|
`Invalid OpenAPI specification: ${errorText}${moreErrors}`,
|
|
284
|
-
currentPath
|
|
448
|
+
currentPath,
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const nonBlockingResults = results.filter(
|
|
453
|
+
(result) => result.severity !== 0,
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
if (nonBlockingResults.length > 0) {
|
|
457
|
+
const warningMessages = nonBlockingResults.slice(0, 5).map((result) => {
|
|
458
|
+
const pathStr = result.path.length > 0 ? result.path.join(".") : "root";
|
|
459
|
+
return `${result.message} (at ${pathStr})`;
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
const warningText = warningMessages.join("; ");
|
|
463
|
+
const moreWarnings =
|
|
464
|
+
nonBlockingResults.length > warningMessages.length
|
|
465
|
+
? ` (and ${
|
|
466
|
+
nonBlockingResults.length - warningMessages.length
|
|
467
|
+
} more warnings)`
|
|
468
|
+
: "";
|
|
469
|
+
const sourcePath =
|
|
470
|
+
currentPath.length > 0 ? ` (at: ${currentPath.join(".")})` : "";
|
|
471
|
+
|
|
472
|
+
console.warn(
|
|
473
|
+
`[OPENAPI_VALIDATION_WARNING] ${warningText}${moreWarnings}${sourcePath}`,
|
|
285
474
|
);
|
|
286
475
|
}
|
|
287
476
|
} catch (error) {
|
|
@@ -289,17 +478,20 @@ async function validateOpenApiFile(filePathOrUrl: string, currentPath: Path) {
|
|
|
289
478
|
if (error instanceof SyntaxError) {
|
|
290
479
|
throwConfigError(
|
|
291
480
|
`Failed to parse OpenAPI file: ${error.message}`,
|
|
292
|
-
currentPath
|
|
481
|
+
currentPath,
|
|
293
482
|
);
|
|
294
483
|
} else if (error instanceof Error) {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
)
|
|
484
|
+
const baseMessage = error.message || String(error);
|
|
485
|
+
const prefixedMessage = baseMessage.startsWith(
|
|
486
|
+
"Invalid OpenAPI specification:",
|
|
487
|
+
)
|
|
488
|
+
? baseMessage
|
|
489
|
+
: `Invalid OpenAPI specification: ${baseMessage}`;
|
|
490
|
+
throwConfigError(prefixedMessage, currentPath);
|
|
299
491
|
} else {
|
|
300
492
|
throwConfigError(
|
|
301
493
|
`Invalid OpenAPI specification: ${String(error)}`,
|
|
302
|
-
currentPath
|
|
494
|
+
currentPath,
|
|
303
495
|
);
|
|
304
496
|
}
|
|
305
497
|
}
|
|
@@ -338,7 +530,7 @@ function extractAvailableEndpoints(openApiDoc: any): Set<string> {
|
|
|
338
530
|
|
|
339
531
|
// Helper function to parse endpoint string (e.g., "get /burgers" or "POST /burgers")
|
|
340
532
|
function parseEndpointString(
|
|
341
|
-
endpointStr: string
|
|
533
|
+
endpointStr: string,
|
|
342
534
|
): { method: string; path: string } | null {
|
|
343
535
|
const trimmed = endpointStr.trim();
|
|
344
536
|
const parts = trimmed.split(/\s+/);
|
|
@@ -361,10 +553,15 @@ function parseEndpointString(
|
|
|
361
553
|
return { method, path: normalizedPath };
|
|
362
554
|
}
|
|
363
555
|
|
|
364
|
-
function validateNavigationNode(
|
|
556
|
+
function validateNavigationNode(
|
|
557
|
+
item: any,
|
|
558
|
+
currentPath: Path,
|
|
559
|
+
groupDepth: number = 0,
|
|
560
|
+
): void {
|
|
365
561
|
// A) Base Case: Simple string path
|
|
366
562
|
if (typeof item === "string") {
|
|
367
|
-
|
|
563
|
+
const normalizedPath = normalizeDocsPagePath(item, currentPath);
|
|
564
|
+
validateFileExistence(normalizedPath, currentPath);
|
|
368
565
|
return;
|
|
369
566
|
}
|
|
370
567
|
|
|
@@ -379,25 +576,41 @@ function validateNavigationNode(item: any, currentPath: Path): void {
|
|
|
379
576
|
if (typeCount !== 1) {
|
|
380
577
|
throwConfigError(
|
|
381
578
|
"Object must contain exactly one key: 'page' or 'group'.",
|
|
382
|
-
currentPath
|
|
579
|
+
currentPath,
|
|
383
580
|
);
|
|
384
581
|
}
|
|
385
582
|
|
|
386
583
|
// --- Validate Group (Recursive) ---
|
|
387
584
|
if (isGroup) {
|
|
388
585
|
const path = [...currentPath];
|
|
586
|
+
|
|
587
|
+
// Enforce max group nesting depth of 2
|
|
588
|
+
if (groupDepth >= 2) {
|
|
589
|
+
throwConfigError("Groups can only be nested up to 2 levels deep.", path);
|
|
590
|
+
}
|
|
591
|
+
|
|
389
592
|
checkType(item.group, "string", [...path, "group"], "Group name");
|
|
390
593
|
|
|
391
594
|
// C.2: THE EXPANDED CHECK (Kept clean)
|
|
392
595
|
checkType(item.expanded, "boolean", [...path, "expanded"], "Expanded");
|
|
393
596
|
|
|
597
|
+
validateIcon(item.icon, [...path, "icon"]);
|
|
598
|
+
|
|
394
599
|
// Check if pages array exists and validate children
|
|
395
600
|
if (!item.pages)
|
|
396
601
|
throwConfigError("Group must have a 'pages' array.", [...path, "pages"]);
|
|
397
602
|
checkType(item.pages, "array", [...path, "pages"], "Group pages");
|
|
398
603
|
|
|
399
604
|
item.pages.forEach((child: any, i: number) => {
|
|
400
|
-
|
|
605
|
+
if (typeof child === "string") {
|
|
606
|
+
const childPath = [...path, "pages", i];
|
|
607
|
+
const normalizedPagePath = normalizeDocsPagePath(child, childPath);
|
|
608
|
+
item.pages[i] = normalizedPagePath;
|
|
609
|
+
validateFileExistence(normalizedPagePath, childPath);
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
validateNavigationNode(child, [...path, "pages", i], groupDepth + 1);
|
|
401
614
|
});
|
|
402
615
|
return;
|
|
403
616
|
}
|
|
@@ -405,9 +618,17 @@ function validateNavigationNode(item: any, currentPath: Path): void {
|
|
|
405
618
|
// --- Validate Page ---
|
|
406
619
|
if (isPage) {
|
|
407
620
|
const path = [...currentPath];
|
|
408
|
-
|
|
621
|
+
const normalizedPagePath = normalizeDocsPagePath(item.page, [
|
|
622
|
+
...path,
|
|
623
|
+
"page",
|
|
624
|
+
]);
|
|
625
|
+
item.page = normalizedPagePath;
|
|
626
|
+
validateFileExistence(normalizedPagePath, [...path, "page"]);
|
|
627
|
+
|
|
628
|
+
validateIcon(item.icon, [...path, "icon"]);
|
|
409
629
|
|
|
410
|
-
|
|
630
|
+
// Validate optional title
|
|
631
|
+
checkType(item.title, "string", [...path, "title"], "Page title");
|
|
411
632
|
|
|
412
633
|
// Check D.2/D.3: Page cannot have group properties
|
|
413
634
|
if ("expanded" in item)
|
|
@@ -421,9 +642,56 @@ function validateNavigationNode(item: any, currentPath: Path): void {
|
|
|
421
642
|
}
|
|
422
643
|
}
|
|
423
644
|
|
|
645
|
+
function getFirstPagePathFromPageItems(
|
|
646
|
+
items: (string | NavPage | NavGroup)[],
|
|
647
|
+
): string | undefined {
|
|
648
|
+
for (const item of items) {
|
|
649
|
+
if (typeof item === "string") {
|
|
650
|
+
return item;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
if ("page" in item) {
|
|
654
|
+
return item.page;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
if ("group" in item) {
|
|
658
|
+
const nestedPath = getFirstPagePathFromPageItems(item.pages);
|
|
659
|
+
if (nestedPath) {
|
|
660
|
+
return nestedPath;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
return undefined;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function getFirstPagePathFromNavigation(
|
|
669
|
+
navigation: DocsConfig["navigation"],
|
|
670
|
+
): string | undefined {
|
|
671
|
+
if (navigation.pages) {
|
|
672
|
+
return getFirstPagePathFromPageItems(navigation.pages);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
if (navigation.menu) {
|
|
676
|
+
for (const menuItem of navigation.menu.items) {
|
|
677
|
+
const submenuPages = menuItem.submenu.pages;
|
|
678
|
+
if (!submenuPages) {
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const firstPath = getFirstPagePathFromPageItems(submenuPages);
|
|
683
|
+
if (firstPath) {
|
|
684
|
+
return firstPath;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
return undefined;
|
|
690
|
+
}
|
|
691
|
+
|
|
424
692
|
async function validateNavOpenApi(
|
|
425
693
|
navOpenApi: any,
|
|
426
|
-
currentPath: Path
|
|
694
|
+
currentPath: Path,
|
|
427
695
|
): Promise<void> {
|
|
428
696
|
checkType(navOpenApi, "object", currentPath, "Open API object");
|
|
429
697
|
|
|
@@ -431,7 +699,7 @@ async function validateNavOpenApi(
|
|
|
431
699
|
if (typeof navOpenApi.source !== "string") {
|
|
432
700
|
throwConfigError(
|
|
433
701
|
"Open API object must have an 'source' property that is a string.",
|
|
434
|
-
[...currentPath, "source"]
|
|
702
|
+
[...currentPath, "source"],
|
|
435
703
|
);
|
|
436
704
|
}
|
|
437
705
|
|
|
@@ -445,7 +713,7 @@ async function validateNavOpenApi(
|
|
|
445
713
|
if (hasInclude && hasExclude) {
|
|
446
714
|
throwConfigError(
|
|
447
715
|
"Open API object cannot have both 'include' and 'exclude' properties. They are mutually exclusive.",
|
|
448
|
-
currentPath
|
|
716
|
+
currentPath,
|
|
449
717
|
);
|
|
450
718
|
}
|
|
451
719
|
|
|
@@ -464,7 +732,7 @@ async function validateNavOpenApi(
|
|
|
464
732
|
navOpenApi.include,
|
|
465
733
|
"array",
|
|
466
734
|
[...currentPath, "include"],
|
|
467
|
-
"Include array"
|
|
735
|
+
"Include array",
|
|
468
736
|
);
|
|
469
737
|
|
|
470
738
|
if (navOpenApi.include.length === 0) {
|
|
@@ -479,7 +747,7 @@ async function validateNavOpenApi(
|
|
|
479
747
|
if (typeof entry !== "string") {
|
|
480
748
|
throwConfigError(
|
|
481
749
|
`Include entry at index ${i} must be a string in the format "METHOD /path".`,
|
|
482
|
-
[...currentPath, "include", i]
|
|
750
|
+
[...currentPath, "include", i],
|
|
483
751
|
);
|
|
484
752
|
}
|
|
485
753
|
|
|
@@ -487,7 +755,7 @@ async function validateNavOpenApi(
|
|
|
487
755
|
if (!parsed) {
|
|
488
756
|
throwConfigError(
|
|
489
757
|
`Include entry at index ${i} must be in the format "METHOD /path". Found: ${entry}`,
|
|
490
|
-
[...currentPath, "include", i]
|
|
758
|
+
[...currentPath, "include", i],
|
|
491
759
|
);
|
|
492
760
|
}
|
|
493
761
|
|
|
@@ -496,7 +764,7 @@ async function validateNavOpenApi(
|
|
|
496
764
|
if (!availableEndpoints.has(endpointKey)) {
|
|
497
765
|
throwConfigError(
|
|
498
766
|
`Include entry at index ${i} does not match any endpoint in the OpenAPI spec. Found: ${entry}. Expected format: "METHOD /path".`,
|
|
499
|
-
[...currentPath, "include", i]
|
|
767
|
+
[...currentPath, "include", i],
|
|
500
768
|
);
|
|
501
769
|
}
|
|
502
770
|
}
|
|
@@ -508,7 +776,7 @@ async function validateNavOpenApi(
|
|
|
508
776
|
navOpenApi.exclude,
|
|
509
777
|
"array",
|
|
510
778
|
[...currentPath, "exclude"],
|
|
511
|
-
"Exclude array"
|
|
779
|
+
"Exclude array",
|
|
512
780
|
);
|
|
513
781
|
|
|
514
782
|
if (navOpenApi.exclude.length === 0) {
|
|
@@ -523,7 +791,7 @@ async function validateNavOpenApi(
|
|
|
523
791
|
if (typeof entry !== "string") {
|
|
524
792
|
throwConfigError(
|
|
525
793
|
`Exclude entry at index ${i} must be a string in the format "METHOD /path".`,
|
|
526
|
-
[...currentPath, "exclude", i]
|
|
794
|
+
[...currentPath, "exclude", i],
|
|
527
795
|
);
|
|
528
796
|
}
|
|
529
797
|
|
|
@@ -531,7 +799,7 @@ async function validateNavOpenApi(
|
|
|
531
799
|
if (!parsed) {
|
|
532
800
|
throwConfigError(
|
|
533
801
|
`Exclude entry at index ${i} must be in the format "METHOD /path" (e.g., "get /burgers"). Found: ${entry}`,
|
|
534
|
-
[...currentPath, "exclude", i]
|
|
802
|
+
[...currentPath, "exclude", i],
|
|
535
803
|
);
|
|
536
804
|
}
|
|
537
805
|
|
|
@@ -540,7 +808,7 @@ async function validateNavOpenApi(
|
|
|
540
808
|
if (!availableEndpoints.has(endpointKey)) {
|
|
541
809
|
throwConfigError(
|
|
542
810
|
`Exclude entry at index ${i} does not match any endpoint in the OpenAPI spec. Found: ${entry}. Expected format: "METHOD /path" (case-insensitive).`,
|
|
543
|
-
[...currentPath, "exclude", i]
|
|
811
|
+
[...currentPath, "exclude", i],
|
|
544
812
|
);
|
|
545
813
|
}
|
|
546
814
|
}
|
|
@@ -550,7 +818,7 @@ async function validateNavOpenApi(
|
|
|
550
818
|
async function validateNavMenuItem(item: NavMenuItem, currentPath: Path) {
|
|
551
819
|
checkType(item, "object", currentPath, "Menu item");
|
|
552
820
|
|
|
553
|
-
|
|
821
|
+
validateIcon(item.icon, [...currentPath, "icon"]);
|
|
554
822
|
|
|
555
823
|
// Required: label
|
|
556
824
|
if (!item.label) {
|
|
@@ -572,12 +840,12 @@ async function validateNavMenuItem(item: NavMenuItem, currentPath: Path) {
|
|
|
572
840
|
|
|
573
841
|
const submenu = item.submenu;
|
|
574
842
|
const submenuKeys = Object.keys(submenu);
|
|
575
|
-
const validSubmenuKeys = ["pages", "
|
|
843
|
+
const validSubmenuKeys = ["pages", "openapi"];
|
|
576
844
|
const presentSubmenuKeys = submenuKeys.filter((key) =>
|
|
577
|
-
validSubmenuKeys.includes(key)
|
|
845
|
+
validSubmenuKeys.includes(key),
|
|
578
846
|
);
|
|
579
847
|
const invalidSubmenuKeys = submenuKeys.filter(
|
|
580
|
-
(key) => !validSubmenuKeys.includes(key)
|
|
848
|
+
(key) => !validSubmenuKeys.includes(key),
|
|
581
849
|
);
|
|
582
850
|
|
|
583
851
|
// Submenu must have exactly one key total
|
|
@@ -585,16 +853,16 @@ async function validateNavMenuItem(item: NavMenuItem, currentPath: Path) {
|
|
|
585
853
|
if (submenuKeys.length === 0) {
|
|
586
854
|
throwConfigError(
|
|
587
855
|
`Submenu must contain exactly one key (${validSubmenuKeys.join(
|
|
588
|
-
", "
|
|
856
|
+
", ",
|
|
589
857
|
)}). Found no keys.`,
|
|
590
|
-
[...currentPath, "submenu"]
|
|
858
|
+
[...currentPath, "submenu"],
|
|
591
859
|
);
|
|
592
860
|
} else {
|
|
593
861
|
throwConfigError(
|
|
594
862
|
`Submenu must contain exactly one key (${validSubmenuKeys.join(
|
|
595
|
-
", "
|
|
863
|
+
", ",
|
|
596
864
|
)}). Found ${submenuKeys.length} key(s): ${submenuKeys.join(", ")}.`,
|
|
597
|
-
[...currentPath, "submenu"]
|
|
865
|
+
[...currentPath, "submenu"],
|
|
598
866
|
);
|
|
599
867
|
}
|
|
600
868
|
}
|
|
@@ -604,9 +872,9 @@ async function validateNavMenuItem(item: NavMenuItem, currentPath: Path) {
|
|
|
604
872
|
const invalidKey = invalidSubmenuKeys[0];
|
|
605
873
|
throwConfigError(
|
|
606
874
|
`Submenu must contain exactly one key (${validSubmenuKeys.join(
|
|
607
|
-
", "
|
|
875
|
+
", ",
|
|
608
876
|
)}). Found invalid key: ${invalidKey}.`,
|
|
609
|
-
[...currentPath, "submenu"]
|
|
877
|
+
[...currentPath, "submenu"],
|
|
610
878
|
);
|
|
611
879
|
}
|
|
612
880
|
|
|
@@ -620,31 +888,20 @@ async function validateNavMenuItem(item: NavMenuItem, currentPath: Path) {
|
|
|
620
888
|
submenuValue,
|
|
621
889
|
"array",
|
|
622
890
|
[...currentPath, "submenu", "pages"],
|
|
623
|
-
"Submenu pages"
|
|
891
|
+
"Submenu pages",
|
|
624
892
|
);
|
|
625
893
|
(submenuValue as NavigationItem["pages"])?.forEach(
|
|
626
|
-
(
|
|
627
|
-
|
|
628
|
-
|
|
894
|
+
(item: string | NavPage | NavGroup, i: number) => {
|
|
895
|
+
const itemPath = [...currentPath, "submenu", "pages", i];
|
|
896
|
+
if (typeof item === "string") {
|
|
897
|
+
const normalizedPagePath = normalizeDocsPagePath(item, itemPath);
|
|
898
|
+
(submenuValue as (string | NavPage | NavGroup)[])[i] =
|
|
899
|
+
normalizedPagePath;
|
|
900
|
+
validateFileExistence(normalizedPagePath, itemPath);
|
|
629
901
|
} else {
|
|
630
|
-
validateNavigationNode(
|
|
902
|
+
validateNavigationNode(item, itemPath);
|
|
631
903
|
}
|
|
632
|
-
}
|
|
633
|
-
);
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
// Validate groups array
|
|
637
|
-
if (submenuKey === "groups") {
|
|
638
|
-
checkType(
|
|
639
|
-
submenuValue,
|
|
640
|
-
"array",
|
|
641
|
-
[...currentPath, "submenu", "groups"],
|
|
642
|
-
"Submenu groups"
|
|
643
|
-
);
|
|
644
|
-
(submenuValue as NavigationItem["groups"])?.forEach(
|
|
645
|
-
(group: NavGroup, i: number) => {
|
|
646
|
-
validateNavigationNode(group, [...currentPath, "submenu", "groups", i]);
|
|
647
|
-
}
|
|
904
|
+
},
|
|
648
905
|
);
|
|
649
906
|
}
|
|
650
907
|
|
|
@@ -667,7 +924,7 @@ async function validateNavMenuItem(item: NavMenuItem, currentPath: Path) {
|
|
|
667
924
|
} else {
|
|
668
925
|
throwConfigError(
|
|
669
926
|
"OpenAPI must be either a string (file path or hosted file) or an object.",
|
|
670
|
-
[...currentPath, "submenu", "openapi"]
|
|
927
|
+
[...currentPath, "submenu", "openapi"],
|
|
671
928
|
);
|
|
672
929
|
}
|
|
673
930
|
}
|
|
@@ -678,10 +935,10 @@ async function validateNavMenu(menu: any, currentPath: Path) {
|
|
|
678
935
|
|
|
679
936
|
// Optional: type
|
|
680
937
|
if (menu.type !== undefined) {
|
|
681
|
-
if (menu.type !== "dropdown" && menu.type !== "
|
|
938
|
+
if (menu.type !== "dropdown" && menu.type !== "segmented") {
|
|
682
939
|
throwConfigError(
|
|
683
|
-
"Menu type must be 'dropdown' or '
|
|
684
|
-
[...currentPath, "type"]
|
|
940
|
+
"Menu type must be 'dropdown' or 'segmented' if provided. Defaults to 'dropdown'",
|
|
941
|
+
[...currentPath, "type"],
|
|
685
942
|
);
|
|
686
943
|
}
|
|
687
944
|
}
|
|
@@ -725,7 +982,7 @@ function validateNavbarItem(item: any, currentPath: Path): void {
|
|
|
725
982
|
}
|
|
726
983
|
|
|
727
984
|
// Optional property
|
|
728
|
-
|
|
985
|
+
validateIcon(item.icon, [...currentPath, "icon"]);
|
|
729
986
|
}
|
|
730
987
|
|
|
731
988
|
// --- Top-Level Validation Functions (Your Clean API) ---
|
|
@@ -735,75 +992,131 @@ function validateTitle(title: DocsConfig["title"]) {
|
|
|
735
992
|
if (!title) throwConfigError("Title is missing.", ["title"]);
|
|
736
993
|
}
|
|
737
994
|
|
|
738
|
-
function
|
|
739
|
-
|
|
740
|
-
if (logo === undefined) return;
|
|
995
|
+
function validateLogoPaddingValue(value: unknown, currentPath: Path, label: string): void {
|
|
996
|
+
if (value === undefined) return;
|
|
741
997
|
|
|
742
|
-
|
|
743
|
-
|
|
998
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
999
|
+
throwConfigError(`${label} must be a finite number.`, currentPath);
|
|
1000
|
+
}
|
|
744
1001
|
|
|
745
|
-
|
|
746
|
-
if (
|
|
747
|
-
|
|
1002
|
+
const numericValue = value as number;
|
|
1003
|
+
if (numericValue < 0) {
|
|
1004
|
+
throwConfigError(`${label} cannot be negative.`, currentPath);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
748
1007
|
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
1008
|
+
function validateLogoImagePath(
|
|
1009
|
+
imagePath: string,
|
|
1010
|
+
currentPath: Path,
|
|
1011
|
+
label: string,
|
|
1012
|
+
): void {
|
|
1013
|
+
const validExtensions = [".svg", ".png", ".jpg", ".jpeg", ".webp", ".gif"];
|
|
1014
|
+
const hasValidExtension = validExtensions.some((ext) =>
|
|
1015
|
+
imagePath.toLowerCase().endsWith(ext),
|
|
1016
|
+
);
|
|
1017
|
+
if (!hasValidExtension) {
|
|
1018
|
+
throwConfigError(
|
|
1019
|
+
`${label} must be a valid image file (.svg, .png, .jpg, .jpeg, .webp, .gif)`,
|
|
1020
|
+
currentPath,
|
|
753
1021
|
);
|
|
754
|
-
|
|
755
|
-
throwConfigError(
|
|
756
|
-
"Logo light must be a valid image file (.svg, .png, .jpg, .jpeg, .webp, .gif)",
|
|
757
|
-
["logo", "light"]
|
|
758
|
-
);
|
|
759
|
-
}
|
|
1022
|
+
}
|
|
760
1023
|
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
: logo.light;
|
|
766
|
-
const fullPath = path.join(DOCS_DIR, normalizedPath);
|
|
1024
|
+
const normalizedPath = imagePath.startsWith("/")
|
|
1025
|
+
? imagePath.slice(1)
|
|
1026
|
+
: imagePath;
|
|
1027
|
+
const fullPath = path.join(DOCS_DIR, normalizedPath);
|
|
767
1028
|
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
}
|
|
1029
|
+
if (!fs.existsSync(fullPath)) {
|
|
1030
|
+
throwConfigError(
|
|
1031
|
+
`${label} file not found. Expected: ${normalizedPath} (relative to content/docs folder)`,
|
|
1032
|
+
currentPath,
|
|
1033
|
+
);
|
|
774
1034
|
}
|
|
1035
|
+
}
|
|
775
1036
|
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
1037
|
+
function validateLogoVariant(
|
|
1038
|
+
variant: LogoVariant | undefined,
|
|
1039
|
+
currentPath: Path,
|
|
1040
|
+
mode: "light" | "dark",
|
|
1041
|
+
): void {
|
|
1042
|
+
if (variant === undefined) return;
|
|
779
1043
|
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
1044
|
+
if (typeof variant === "string") {
|
|
1045
|
+
validateLogoImagePath(variant, currentPath, `Logo ${mode}`);
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
if (typeof variant !== "object" || variant === null || Array.isArray(variant)) {
|
|
1050
|
+
throwConfigError(
|
|
1051
|
+
`Logo ${mode} must be a string path or an object with 'image' and optional 'padding'.`,
|
|
1052
|
+
currentPath,
|
|
784
1053
|
);
|
|
785
|
-
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
const supportedKeys = new Set(["image", "padding"]);
|
|
1057
|
+
for (const key of Object.keys(variant)) {
|
|
1058
|
+
if (!supportedKeys.has(key)) {
|
|
786
1059
|
throwConfigError(
|
|
787
|
-
|
|
788
|
-
[
|
|
1060
|
+
`Logo ${mode} object only supports 'image' and 'padding'.`,
|
|
1061
|
+
[...currentPath, key],
|
|
789
1062
|
);
|
|
790
1063
|
}
|
|
1064
|
+
}
|
|
791
1065
|
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
1066
|
+
if (typeof variant.image !== "string") {
|
|
1067
|
+
throwConfigError(
|
|
1068
|
+
`Logo ${mode} object must include an 'image' string.`,
|
|
1069
|
+
[...currentPath, "image"],
|
|
1070
|
+
);
|
|
1071
|
+
}
|
|
798
1072
|
|
|
799
|
-
|
|
1073
|
+
validateLogoImagePath(variant.image, [...currentPath, "image"], `Logo ${mode} image`);
|
|
1074
|
+
|
|
1075
|
+
if (variant.padding === undefined) return;
|
|
1076
|
+
|
|
1077
|
+
if (
|
|
1078
|
+
typeof variant.padding !== "object" ||
|
|
1079
|
+
variant.padding === null ||
|
|
1080
|
+
Array.isArray(variant.padding)
|
|
1081
|
+
) {
|
|
1082
|
+
throwConfigError(
|
|
1083
|
+
`Logo ${mode} padding must be an object with optional 'top' and 'bottom'.`,
|
|
1084
|
+
[...currentPath, "padding"],
|
|
1085
|
+
);
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
const paddingKeys = Object.keys(variant.padding);
|
|
1089
|
+
for (const key of paddingKeys) {
|
|
1090
|
+
if (key !== "top" && key !== "bottom") {
|
|
800
1091
|
throwConfigError(
|
|
801
|
-
`Logo
|
|
802
|
-
["
|
|
1092
|
+
`Logo ${mode} padding only supports 'top' and 'bottom'.`,
|
|
1093
|
+
[...currentPath, "padding", key],
|
|
803
1094
|
);
|
|
804
1095
|
}
|
|
805
1096
|
}
|
|
806
1097
|
|
|
1098
|
+
validateLogoPaddingValue(
|
|
1099
|
+
variant.padding.top,
|
|
1100
|
+
[...currentPath, "padding", "top"],
|
|
1101
|
+
`Logo ${mode} padding top`,
|
|
1102
|
+
);
|
|
1103
|
+
validateLogoPaddingValue(
|
|
1104
|
+
variant.padding.bottom,
|
|
1105
|
+
[...currentPath, "padding", "bottom"],
|
|
1106
|
+
`Logo ${mode} padding bottom`,
|
|
1107
|
+
);
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
function validateLogo(logo: DocsConfig["logo"]) {
|
|
1111
|
+
// Logo is optional, so if it's undefined, we're done
|
|
1112
|
+
if (logo === undefined) return;
|
|
1113
|
+
|
|
1114
|
+
// If logo is provided, it must be an object
|
|
1115
|
+
checkType(logo, "object", ["logo"], "Logo configuration");
|
|
1116
|
+
|
|
1117
|
+
validateLogoVariant(logo.light, ["logo", "light"], "light");
|
|
1118
|
+
validateLogoVariant(logo.dark, ["logo", "dark"], "dark");
|
|
1119
|
+
|
|
807
1120
|
// Validate 'href' if provided
|
|
808
1121
|
if (logo.href !== undefined) {
|
|
809
1122
|
checkType(logo.href, "string", ["logo", "href"], "Logo href");
|
|
@@ -827,14 +1140,32 @@ function validateLogo(logo: DocsConfig["logo"]) {
|
|
|
827
1140
|
if (!isUrl && !isInternalPath) {
|
|
828
1141
|
throwConfigError(
|
|
829
1142
|
"Logo href must be either a valid URL (http:// or https://) or an internal path (starting with /)",
|
|
830
|
-
["logo", "href"]
|
|
1143
|
+
["logo", "href"],
|
|
831
1144
|
);
|
|
832
1145
|
}
|
|
833
1146
|
}
|
|
1147
|
+
|
|
1148
|
+
if (logo.pill !== undefined) {
|
|
1149
|
+
if (typeof logo.pill === "string") {
|
|
1150
|
+
if (logo.pill.trim() === "") {
|
|
1151
|
+
throwConfigError(
|
|
1152
|
+
"Logo pill text cannot be an empty string. Use false to hide the pill.",
|
|
1153
|
+
["logo", "pill"],
|
|
1154
|
+
);
|
|
1155
|
+
}
|
|
1156
|
+
} else if (logo.pill !== false) {
|
|
1157
|
+
throwConfigError("Logo pill must be a string or false.", ["logo", "pill"]);
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
|
|
834
1161
|
}
|
|
835
1162
|
|
|
836
|
-
function validateHome(home: DocsConfig["home"]) {
|
|
837
|
-
|
|
1163
|
+
function validateHome(home: DocsConfig["home"]): string | undefined {
|
|
1164
|
+
if (home === undefined) return undefined;
|
|
1165
|
+
|
|
1166
|
+
const normalizedHome = normalizeDocsPagePath(home, ["home"], "Home path");
|
|
1167
|
+
validateFileExistence(normalizedHome, ["home"]);
|
|
1168
|
+
return normalizedHome;
|
|
838
1169
|
}
|
|
839
1170
|
|
|
840
1171
|
function validateNavbar(navbar: DocsConfig["navbar"]) {
|
|
@@ -868,40 +1199,142 @@ function validateNavbar(navbar: DocsConfig["navbar"]) {
|
|
|
868
1199
|
}
|
|
869
1200
|
}
|
|
870
1201
|
|
|
1202
|
+
function validateFooter(footer: DocsConfig["footer"]) {
|
|
1203
|
+
if (footer === undefined) return;
|
|
1204
|
+
|
|
1205
|
+
checkType(footer, "object", ["footer"], "Footer configuration");
|
|
1206
|
+
|
|
1207
|
+
// Validate socials
|
|
1208
|
+
if (footer.socials !== undefined) {
|
|
1209
|
+
checkType(
|
|
1210
|
+
footer.socials,
|
|
1211
|
+
"object",
|
|
1212
|
+
["footer", "socials"],
|
|
1213
|
+
"Footer socials",
|
|
1214
|
+
);
|
|
1215
|
+
const validSocials = [
|
|
1216
|
+
"x",
|
|
1217
|
+
"website",
|
|
1218
|
+
"facebook",
|
|
1219
|
+
"youtube",
|
|
1220
|
+
"discord",
|
|
1221
|
+
"slack",
|
|
1222
|
+
"github",
|
|
1223
|
+
"linkedin",
|
|
1224
|
+
"instagram",
|
|
1225
|
+
"hacker-news",
|
|
1226
|
+
"medium",
|
|
1227
|
+
"telegram",
|
|
1228
|
+
"bluesky",
|
|
1229
|
+
"threads",
|
|
1230
|
+
"reddit",
|
|
1231
|
+
"podcast",
|
|
1232
|
+
];
|
|
1233
|
+
for (const [key, value] of Object.entries(footer.socials)) {
|
|
1234
|
+
if (!validSocials.includes(key)) {
|
|
1235
|
+
throwConfigError(
|
|
1236
|
+
`Invalid social platform: ${key}. Valid options are: ${validSocials.join(
|
|
1237
|
+
", ",
|
|
1238
|
+
)}`,
|
|
1239
|
+
["footer", "socials", key],
|
|
1240
|
+
);
|
|
1241
|
+
}
|
|
1242
|
+
checkType(
|
|
1243
|
+
value,
|
|
1244
|
+
"string",
|
|
1245
|
+
["footer", "socials", key],
|
|
1246
|
+
`Social link for ${key}`,
|
|
1247
|
+
);
|
|
1248
|
+
if (!isUrl(value as string)) {
|
|
1249
|
+
throwConfigError(`Social link for ${key} must be a valid URL.`, [
|
|
1250
|
+
"footer",
|
|
1251
|
+
"socials",
|
|
1252
|
+
key,
|
|
1253
|
+
]);
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
// Validate links
|
|
1259
|
+
if (footer.links !== undefined) {
|
|
1260
|
+
checkType(footer.links, "array", ["footer", "links"], "Footer links");
|
|
1261
|
+
footer.links.forEach((link: any, i: number) => {
|
|
1262
|
+
checkType(link, "object", ["footer", "links", i], "Footer link");
|
|
1263
|
+
|
|
1264
|
+
if (typeof link.text !== "string") {
|
|
1265
|
+
throwConfigError("Footer link must have a 'text' property.", [
|
|
1266
|
+
"footer",
|
|
1267
|
+
"links",
|
|
1268
|
+
i,
|
|
1269
|
+
"text",
|
|
1270
|
+
]);
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
if (typeof link.href !== "string") {
|
|
1274
|
+
throwConfigError("Footer link must have an 'href' property.", [
|
|
1275
|
+
"footer",
|
|
1276
|
+
"links",
|
|
1277
|
+
i,
|
|
1278
|
+
"href",
|
|
1279
|
+
]);
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
const trimmedHref = link.href.trim();
|
|
1283
|
+
const isExternal =
|
|
1284
|
+
trimmedHref.startsWith("http://") || trimmedHref.startsWith("https://");
|
|
1285
|
+
const isInternal = trimmedHref.startsWith("/");
|
|
1286
|
+
|
|
1287
|
+
if (!isExternal && !isInternal) {
|
|
1288
|
+
throwConfigError(
|
|
1289
|
+
"Footer link href must be either a valid URL (http:// or https://) or an internal path (starting with /)",
|
|
1290
|
+
["footer", "links", i, "href"],
|
|
1291
|
+
);
|
|
1292
|
+
}
|
|
1293
|
+
});
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
|
|
871
1297
|
async function validateNavigation(navigation: DocsConfig["navigation"]) {
|
|
872
1298
|
checkType(navigation, "object", ["navigation"], "Navigation");
|
|
873
1299
|
|
|
874
1300
|
const keys = Object.keys(navigation);
|
|
875
|
-
const validKeys = ["pages", "
|
|
1301
|
+
const validKeys = ["pages", "menu", "openapi"];
|
|
876
1302
|
const navKeys = keys.filter((key) => validKeys.includes(key));
|
|
877
1303
|
|
|
878
1304
|
if (navKeys.length !== 1) {
|
|
879
1305
|
throwConfigError(
|
|
880
1306
|
`Navigation must contain exactly one top-level item (${validKeys.join(
|
|
881
|
-
", "
|
|
1307
|
+
", ",
|
|
882
1308
|
)}). Found ${navKeys.length}.`,
|
|
883
|
-
["navigation"]
|
|
1309
|
+
["navigation"],
|
|
884
1310
|
);
|
|
885
1311
|
}
|
|
886
1312
|
|
|
887
1313
|
const navKey = navKeys[0];
|
|
888
1314
|
const navValue = (navigation as any)[navKey];
|
|
889
1315
|
|
|
890
|
-
// Handle "menu" as an object, "pages"
|
|
1316
|
+
// Handle "menu" as an object, "pages" as an array
|
|
891
1317
|
if (navKey === "menu") {
|
|
892
1318
|
await validateNavMenu(navValue, ["navigation", "menu"]);
|
|
893
1319
|
} else {
|
|
894
|
-
// Validate the container itself is an array for pages
|
|
1320
|
+
// Validate the container itself is an array for pages
|
|
895
1321
|
checkType(
|
|
896
1322
|
navValue,
|
|
897
1323
|
"array",
|
|
898
1324
|
["navigation", navKey],
|
|
899
|
-
`Navigation container '${navKey}'
|
|
1325
|
+
`Navigation container '${navKey}'`,
|
|
900
1326
|
);
|
|
901
1327
|
|
|
902
1328
|
// Route to Recursive Structural Validation
|
|
903
1329
|
navValue.forEach((item: NavigationItem, i: number) => {
|
|
904
|
-
|
|
1330
|
+
const itemPath = ["navigation", navKey, i] as Path;
|
|
1331
|
+
if (typeof item === "string") {
|
|
1332
|
+
const normalizedPagePath = normalizeDocsPagePath(item, itemPath);
|
|
1333
|
+
navValue[i] = normalizedPagePath;
|
|
1334
|
+
validateFileExistence(normalizedPagePath, itemPath);
|
|
1335
|
+
} else {
|
|
1336
|
+
validateNavigationNode(item, itemPath);
|
|
1337
|
+
}
|
|
905
1338
|
});
|
|
906
1339
|
}
|
|
907
1340
|
}
|
|
@@ -912,9 +1345,34 @@ async function validateConfig(config: any): Promise<DocsConfig> {
|
|
|
912
1345
|
// Execute top-level checks sequentially
|
|
913
1346
|
validateTitle(config.title);
|
|
914
1347
|
validateLogo(config.logo);
|
|
915
|
-
validateHome(config.home);
|
|
916
1348
|
validateNavbar(config.navbar);
|
|
1349
|
+
validateFooter(config.footer);
|
|
917
1350
|
await validateNavigation(config.navigation);
|
|
1351
|
+
config.home = validateHome(config.home);
|
|
1352
|
+
|
|
1353
|
+
if (config.home === undefined) {
|
|
1354
|
+
const fallbackHome = getFirstPagePathFromNavigation(config.navigation);
|
|
1355
|
+
if (!fallbackHome) {
|
|
1356
|
+
throwConfigError(
|
|
1357
|
+
"Home is undefined and no documentation page exists in navigation to use as fallback.",
|
|
1358
|
+
["home"],
|
|
1359
|
+
);
|
|
1360
|
+
}
|
|
1361
|
+
config.home = fallbackHome;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
// --- 4. Validate Playground ---
|
|
1365
|
+
if (config.playground !== undefined) {
|
|
1366
|
+
checkType(config.playground, "object", ["playground"], "Playground");
|
|
1367
|
+
if (config.playground.proxy !== undefined) {
|
|
1368
|
+
checkType(
|
|
1369
|
+
config.playground.proxy,
|
|
1370
|
+
"boolean",
|
|
1371
|
+
["playground", "proxy"],
|
|
1372
|
+
"Proxy",
|
|
1373
|
+
);
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
918
1376
|
|
|
919
1377
|
return config as DocsConfig;
|
|
920
1378
|
}
|
|
@@ -926,7 +1384,7 @@ export async function getConfig(): Promise<DocsConfig> {
|
|
|
926
1384
|
// 1. Check if docs.json exists
|
|
927
1385
|
if (!fs.existsSync(CONFIG_PATH)) {
|
|
928
1386
|
throw new Error(
|
|
929
|
-
"[USER_ERROR]: Invalid docs.json: docs.json missing at root of documentation repo."
|
|
1387
|
+
"[USER_ERROR]: Invalid docs.json: `docs.json` missing at root of documentation repo.",
|
|
930
1388
|
);
|
|
931
1389
|
}
|
|
932
1390
|
|
|
@@ -947,7 +1405,7 @@ export async function getConfig(): Promise<DocsConfig> {
|
|
|
947
1405
|
throw new Error(
|
|
948
1406
|
`[USER_ERROR]: Invalid docs.json: Invalid JSON syntax: ${
|
|
949
1407
|
e instanceof Error ? e.message : e
|
|
950
|
-
}
|
|
1408
|
+
}`,
|
|
951
1409
|
);
|
|
952
1410
|
}
|
|
953
1411
|
// ---
|
|
@@ -961,7 +1419,7 @@ export async function getConfig(): Promise<DocsConfig> {
|
|
|
961
1419
|
throw new Error(
|
|
962
1420
|
`[USER_ERROR]: Invalid docs.json: ${
|
|
963
1421
|
error instanceof Error ? error.message : error
|
|
964
|
-
}
|
|
1422
|
+
}`,
|
|
965
1423
|
);
|
|
966
1424
|
}
|
|
967
1425
|
})();
|
|
@@ -973,32 +1431,7 @@ export async function getConfig(): Promise<DocsConfig> {
|
|
|
973
1431
|
function validateComponentUsage(content: string): void {
|
|
974
1432
|
// Remove frontmatter before checking
|
|
975
1433
|
const contentWithoutFrontmatter = content.replace(/^---[\s\S]*?---\n?/, "");
|
|
976
|
-
|
|
977
|
-
// Extract imported component names from MDX import statements
|
|
978
|
-
// Matches: import ComponentName from "..." or import { ComponentName } from "..."
|
|
979
|
-
const importedComponents: string[] = [];
|
|
980
|
-
const defaultImportRegex = /import\s+([A-Z][a-zA-Z0-9]*)\s+from/g;
|
|
981
|
-
const namedImportRegex = /import\s*\{([^}]+)\}\s*from/g;
|
|
982
|
-
|
|
983
|
-
let importMatch;
|
|
984
|
-
while (
|
|
985
|
-
(importMatch = defaultImportRegex.exec(contentWithoutFrontmatter)) !== null
|
|
986
|
-
) {
|
|
987
|
-
importedComponents.push(importMatch[1]);
|
|
988
|
-
}
|
|
989
|
-
while (
|
|
990
|
-
(importMatch = namedImportRegex.exec(contentWithoutFrontmatter)) !== null
|
|
991
|
-
) {
|
|
992
|
-
// Parse named imports like { Foo, Bar as Baz }
|
|
993
|
-
const names = importMatch[1].split(",").map((n) => {
|
|
994
|
-
const parts = n.trim().split(/\s+as\s+/);
|
|
995
|
-
return parts[parts.length - 1].trim(); // Use the alias if present
|
|
996
|
-
});
|
|
997
|
-
importedComponents.push(...names.filter((n) => /^[A-Z]/.test(n)));
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
// Combine available components with imported ones
|
|
1001
|
-
const allowedComponents = [...AVAILABLE_COMPONENTS, ...importedComponents];
|
|
1434
|
+
const allowedComponentSet = new Set(AVAILABLE_COMPONENTS);
|
|
1002
1435
|
|
|
1003
1436
|
// Remove code blocks, inline code, and JSX string expressions to avoid false positives
|
|
1004
1437
|
const contentWithoutCode = contentWithoutFrontmatter
|
|
@@ -1016,7 +1449,7 @@ function validateComponentUsage(content: string): void {
|
|
|
1016
1449
|
|
|
1017
1450
|
while ((match = componentRegex.exec(contentWithoutCode)) !== null) {
|
|
1018
1451
|
const componentName = match[1];
|
|
1019
|
-
if (!
|
|
1452
|
+
if (!allowedComponentSet.has(componentName)) {
|
|
1020
1453
|
// Avoid duplicate entries
|
|
1021
1454
|
if (!unknownComponents.includes(componentName)) {
|
|
1022
1455
|
unknownComponents.push(componentName);
|
|
@@ -1026,10 +1459,15 @@ function validateComponentUsage(content: string): void {
|
|
|
1026
1459
|
|
|
1027
1460
|
if (unknownComponents.length > 0) {
|
|
1028
1461
|
const componentList = unknownComponents.map((c) => `<${c}>`).join(", ");
|
|
1462
|
+
const visibleComponents = AVAILABLE_COMPONENTS.filter(
|
|
1463
|
+
(component) => !INTERNAL_ONLY_COMPONENTS.has(component),
|
|
1464
|
+
);
|
|
1029
1465
|
throw new Error(
|
|
1030
1466
|
`Unknown component(s): ${componentList}. ` +
|
|
1031
|
-
`Available components are: ${
|
|
1032
|
-
|
|
1467
|
+
`Available components are: ${visibleComponents.join(", ")}. ` +
|
|
1468
|
+
"If writing ABOUT a component, use literal backticks: " +
|
|
1469
|
+
"\`<ComponentName>\` or a JSX string: " +
|
|
1470
|
+
"\`{'<ComponentName />'}\`.",
|
|
1033
1471
|
);
|
|
1034
1472
|
}
|
|
1035
1473
|
}
|
|
@@ -1073,7 +1511,7 @@ export async function validateMdxContent() {
|
|
|
1073
1511
|
const pathStr = issue.path.join(".");
|
|
1074
1512
|
// Throw clean error
|
|
1075
1513
|
throw new Error(
|
|
1076
|
-
`Frontmatter validation failed: ${issue.message} (at: ${pathStr})
|
|
1514
|
+
`Frontmatter validation failed: ${issue.message} (at: ${pathStr})`,
|
|
1077
1515
|
);
|
|
1078
1516
|
}
|
|
1079
1517
|
}
|
|
@@ -1090,7 +1528,7 @@ export async function validateMdxContent() {
|
|
|
1090
1528
|
|
|
1091
1529
|
// Throw clean error
|
|
1092
1530
|
throw new Error(
|
|
1093
|
-
`[USER_ERROR]: Invalid MDX in ${relativePath}${location} -> ${reason}
|
|
1531
|
+
`[USER_ERROR]: Invalid MDX in ${relativePath}${location} -> ${reason}`,
|
|
1094
1532
|
);
|
|
1095
1533
|
}
|
|
1096
1534
|
}
|