radiant-docs-validator 0.1.14 → 0.1.16
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 +312 -43
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -161,6 +161,13 @@ function readStaticProps(node, componentName, sourceFile) {
|
|
|
161
161
|
sourceFile
|
|
162
162
|
);
|
|
163
163
|
}
|
|
164
|
+
if (Object.prototype.hasOwnProperty.call(props, attribute.name)) {
|
|
165
|
+
componentError(
|
|
166
|
+
componentName,
|
|
167
|
+
`Duplicate prop "${attribute.name}" is not supported`,
|
|
168
|
+
sourceFile
|
|
169
|
+
);
|
|
170
|
+
}
|
|
164
171
|
if (attribute.value === null) {
|
|
165
172
|
props[attribute.name] = true;
|
|
166
173
|
} else if (typeof attribute.value === "string") {
|
|
@@ -390,6 +397,12 @@ function validateImage(props, options) {
|
|
|
390
397
|
options.validateAssetHref?.(props.src);
|
|
391
398
|
} else if (typeof props.src === "object") {
|
|
392
399
|
assertPlainObject("Image", "src", props.src, options.sourceFile);
|
|
400
|
+
assertNoUnknownProps(
|
|
401
|
+
"Image.src",
|
|
402
|
+
props.src,
|
|
403
|
+
["light", "dark"],
|
|
404
|
+
options.sourceFile
|
|
405
|
+
);
|
|
393
406
|
const light = props.src.light;
|
|
394
407
|
const dark = props.src.dark;
|
|
395
408
|
if (typeof light !== "string" || light.trim().length === 0) {
|
|
@@ -406,8 +419,15 @@ function validateImage(props, options) {
|
|
|
406
419
|
options.sourceFile
|
|
407
420
|
);
|
|
408
421
|
}
|
|
422
|
+
if (typeof dark === "string" && dark.trim().length === 0) {
|
|
423
|
+
componentError(
|
|
424
|
+
"Image",
|
|
425
|
+
'Invalid prop "src.dark": expected a non-empty string',
|
|
426
|
+
options.sourceFile
|
|
427
|
+
);
|
|
428
|
+
}
|
|
409
429
|
options.validateAssetHref?.(light);
|
|
410
|
-
if (typeof dark === "string"
|
|
430
|
+
if (typeof dark === "string") {
|
|
411
431
|
options.validateAssetHref?.(dark);
|
|
412
432
|
}
|
|
413
433
|
}
|
|
@@ -422,17 +442,22 @@ function validateRadiantComponentProps(componentName, props, options) {
|
|
|
422
442
|
"Image",
|
|
423
443
|
"Columns",
|
|
424
444
|
"Column",
|
|
445
|
+
"Tabs",
|
|
446
|
+
"Steps",
|
|
447
|
+
"AccordionGroup",
|
|
425
448
|
"CodeGroup",
|
|
426
449
|
"ComponentPreview"
|
|
427
450
|
].includes(componentName)) {
|
|
428
451
|
return;
|
|
429
452
|
}
|
|
430
453
|
if (componentName === "Step") {
|
|
454
|
+
assertNoUnknownProps("Step", props, ["title"], options.sourceFile);
|
|
431
455
|
assertRequired("Step", "title", props.title, options.sourceFile);
|
|
432
456
|
assertType("Step", "title", props.title, ["string"], options.sourceFile);
|
|
433
457
|
return;
|
|
434
458
|
}
|
|
435
459
|
if (componentName === "Tab") {
|
|
460
|
+
assertNoUnknownProps("Tab", props, ["label", "icon"], options.sourceFile);
|
|
436
461
|
assertRequired("Tab", "label", props.label, options.sourceFile);
|
|
437
462
|
assertType("Tab", "label", props.label, ["string"], options.sourceFile);
|
|
438
463
|
assertType("Tab", "icon", props.icon, ["string"], options.sourceFile);
|
|
@@ -440,6 +465,12 @@ function validateRadiantComponentProps(componentName, props, options) {
|
|
|
440
465
|
return;
|
|
441
466
|
}
|
|
442
467
|
if (componentName === "Accordion") {
|
|
468
|
+
assertNoUnknownProps(
|
|
469
|
+
"Accordion",
|
|
470
|
+
props,
|
|
471
|
+
["title", "icon", "defaultOpen", "titleSize"],
|
|
472
|
+
options.sourceFile
|
|
473
|
+
);
|
|
443
474
|
assertRequired("Accordion", "title", props.title, options.sourceFile);
|
|
444
475
|
assertType("Accordion", "title", props.title, ["string"], options.sourceFile);
|
|
445
476
|
assertType("Accordion", "icon", props.icon, ["string"], options.sourceFile);
|
|
@@ -455,6 +486,12 @@ function validateRadiantComponentProps(componentName, props, options) {
|
|
|
455
486
|
return;
|
|
456
487
|
}
|
|
457
488
|
if (componentName === "Callout") {
|
|
489
|
+
assertNoUnknownProps(
|
|
490
|
+
"Callout",
|
|
491
|
+
props,
|
|
492
|
+
["type", "title", "icon", "accent", "color"],
|
|
493
|
+
options.sourceFile
|
|
494
|
+
);
|
|
458
495
|
assertEnum(
|
|
459
496
|
"Callout",
|
|
460
497
|
"type",
|
|
@@ -467,6 +504,9 @@ function validateRadiantComponentProps(componentName, props, options) {
|
|
|
467
504
|
validateIconProp("Callout", "icon", props.icon, options);
|
|
468
505
|
assertType("Callout", "accent", props.accent, ["boolean"], options.sourceFile);
|
|
469
506
|
assertType("Callout", "color", props.color, ["string"], options.sourceFile);
|
|
507
|
+
if (props.color !== void 0) {
|
|
508
|
+
assertHexColor("Callout", "color", props.color, options.sourceFile);
|
|
509
|
+
}
|
|
470
510
|
if (props.title === true) {
|
|
471
511
|
componentError("Callout", 'Invalid prop "title": expected string or false, got true', options.sourceFile);
|
|
472
512
|
}
|
|
@@ -483,6 +523,18 @@ function validateRadiantComponentProps(componentName, props, options) {
|
|
|
483
523
|
validateImage(props, options);
|
|
484
524
|
return;
|
|
485
525
|
}
|
|
526
|
+
if (componentName === "Tabs") {
|
|
527
|
+
assertNoUnknownProps("Tabs", props, [], options.sourceFile);
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
if (componentName === "Steps") {
|
|
531
|
+
assertNoUnknownProps("Steps", props, [], options.sourceFile);
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
if (componentName === "AccordionGroup") {
|
|
535
|
+
assertNoUnknownProps("AccordionGroup", props, [], options.sourceFile);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
486
538
|
if (componentName === "Columns") {
|
|
487
539
|
assertNoUnknownProps("Columns", props, ["columns"], options.sourceFile);
|
|
488
540
|
assertType("Columns", "columns", props.columns, ["number", "string"], options.sourceFile);
|
|
@@ -693,6 +745,16 @@ function configureDocsValidator({
|
|
|
693
745
|
lastMtime = 0;
|
|
694
746
|
}
|
|
695
747
|
var iconSets = /* @__PURE__ */ new Map();
|
|
748
|
+
var LOCAL_IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
749
|
+
".avif",
|
|
750
|
+
".gif",
|
|
751
|
+
".ico",
|
|
752
|
+
".jpeg",
|
|
753
|
+
".jpg",
|
|
754
|
+
".png",
|
|
755
|
+
".svg",
|
|
756
|
+
".webp"
|
|
757
|
+
]);
|
|
696
758
|
function isUrl(str) {
|
|
697
759
|
try {
|
|
698
760
|
const url = new URL(str);
|
|
@@ -747,14 +809,19 @@ function validateIcon(icon, currentPath) {
|
|
|
747
809
|
}
|
|
748
810
|
return;
|
|
749
811
|
}
|
|
750
|
-
const localRelativePath =
|
|
751
|
-
|
|
752
|
-
|
|
812
|
+
const localRelativePath = validateLocalDocsFile(
|
|
813
|
+
icon,
|
|
814
|
+
currentPath,
|
|
815
|
+
"Icon"
|
|
816
|
+
);
|
|
817
|
+
const extension = path.extname(localRelativePath).toLowerCase();
|
|
818
|
+
if (!LOCAL_IMAGE_EXTENSIONS.has(extension)) {
|
|
753
819
|
throwConfigError(
|
|
754
|
-
`Icon
|
|
820
|
+
`Icon must be a valid image file (.svg, .png, .jpg, .jpeg, .webp, .gif, .ico, .avif). Found: ${icon}`,
|
|
755
821
|
currentPath
|
|
756
822
|
);
|
|
757
823
|
}
|
|
824
|
+
return;
|
|
758
825
|
}
|
|
759
826
|
function validateComponentIcon(icon, currentPath) {
|
|
760
827
|
const trimmedIcon = icon.trim();
|
|
@@ -806,13 +873,25 @@ var throwConfigError = (message, currentPath) => {
|
|
|
806
873
|
throw new Error(`${message}${location}
|
|
807
874
|
`);
|
|
808
875
|
};
|
|
876
|
+
function assertNoUnknownKeys(value, allowedKeys, currentPath, label) {
|
|
877
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
878
|
+
const allowed = new Set(allowedKeys);
|
|
879
|
+
for (const key of Object.keys(value)) {
|
|
880
|
+
if (!allowed.has(key)) {
|
|
881
|
+
throwConfigError(
|
|
882
|
+
`${label} does not support '${key}'. Supported keys: ${allowedKeys.join(", ")}.`,
|
|
883
|
+
[...currentPath, key]
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
}
|
|
809
888
|
function checkType(value, type, currentPath, label) {
|
|
810
|
-
if (value === void 0
|
|
889
|
+
if (value === void 0) return;
|
|
811
890
|
if (type === "array") {
|
|
812
891
|
if (!Array.isArray(value))
|
|
813
892
|
throwConfigError(`${label} must be an array.`, currentPath);
|
|
814
893
|
} else if (type === "object") {
|
|
815
|
-
if (typeof value !== "object" || value === null)
|
|
894
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
816
895
|
throwConfigError(`${label} must be an object.`, currentPath);
|
|
817
896
|
} else {
|
|
818
897
|
if (typeof value !== type)
|
|
@@ -946,8 +1025,13 @@ function normalizeSeedValue(value, currentPath, label) {
|
|
|
946
1025
|
return trimmedValue;
|
|
947
1026
|
}
|
|
948
1027
|
function validateFileExistence(filePath, currentPath) {
|
|
949
|
-
const
|
|
950
|
-
|
|
1028
|
+
const normalizedPath = normalizeLocalDocsPath(
|
|
1029
|
+
`${filePath}.mdx`,
|
|
1030
|
+
currentPath,
|
|
1031
|
+
"Page path"
|
|
1032
|
+
);
|
|
1033
|
+
const fullPath = path.join(DOCS_DIR, normalizedPath);
|
|
1034
|
+
if (!fs.existsSync(fullPath) || !fs.statSync(fullPath).isFile()) {
|
|
951
1035
|
throwConfigError(
|
|
952
1036
|
`Referenced file not found. Expected: ${filePath}`,
|
|
953
1037
|
currentPath
|
|
@@ -979,12 +1063,90 @@ function normalizeDocsPagePath(value, currentPath, label = "Page path") {
|
|
|
979
1063
|
currentPath
|
|
980
1064
|
);
|
|
981
1065
|
}
|
|
982
|
-
const
|
|
1066
|
+
const slashPath = trimmedPath.replace(/\\/g, "/");
|
|
1067
|
+
if (containsPathTraversal(slashPath)) {
|
|
1068
|
+
throwConfigError(
|
|
1069
|
+
`${label} cannot contain '..' path traversal segments`,
|
|
1070
|
+
currentPath
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
1073
|
+
const normalizedPath = slashPath.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
983
1074
|
if (normalizedPath === "") {
|
|
984
1075
|
throwConfigError(`${label} cannot be '/'`, currentPath);
|
|
985
1076
|
}
|
|
986
1077
|
return normalizedPath;
|
|
987
1078
|
}
|
|
1079
|
+
function normalizeLocalDocsPath(value, currentPath, label) {
|
|
1080
|
+
const trimmedPath = value.trim();
|
|
1081
|
+
if (trimmedPath.length === 0) {
|
|
1082
|
+
throwConfigError(`${label} cannot be empty.`, currentPath);
|
|
1083
|
+
}
|
|
1084
|
+
if (trimmedPath.startsWith("#") || trimmedPath.startsWith("?") || trimmedPath.startsWith("//")) {
|
|
1085
|
+
throwConfigError(`${label} must be a local file path in the docs root.`, currentPath);
|
|
1086
|
+
}
|
|
1087
|
+
const { pathname } = splitHrefPathAndSuffix(trimmedPath);
|
|
1088
|
+
const slashPath = pathname.replace(/\\/g, "/");
|
|
1089
|
+
if (containsPathTraversal(slashPath)) {
|
|
1090
|
+
throwConfigError(
|
|
1091
|
+
`${label} cannot contain '..' path traversal segments.`,
|
|
1092
|
+
currentPath
|
|
1093
|
+
);
|
|
1094
|
+
}
|
|
1095
|
+
const withoutLeadingSlash = slashPath.replace(/^\/+/, "");
|
|
1096
|
+
const normalizedPath = path.posix.normalize(`/${withoutLeadingSlash}`).replace(/^\/+/, "");
|
|
1097
|
+
if (!normalizedPath || normalizedPath === ".") {
|
|
1098
|
+
throwConfigError(`${label} cannot be '/'.`, currentPath);
|
|
1099
|
+
}
|
|
1100
|
+
const fullPath = path.resolve(DOCS_DIR, normalizedPath);
|
|
1101
|
+
const relativePath = path.relative(DOCS_DIR, fullPath);
|
|
1102
|
+
if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
|
|
1103
|
+
throwConfigError(`${label} must stay inside the docs root.`, currentPath);
|
|
1104
|
+
}
|
|
1105
|
+
return normalizedPath;
|
|
1106
|
+
}
|
|
1107
|
+
function getSvgValidationError(fullPath, label) {
|
|
1108
|
+
let content;
|
|
1109
|
+
try {
|
|
1110
|
+
content = fs.readFileSync(fullPath, "utf-8");
|
|
1111
|
+
} catch (error) {
|
|
1112
|
+
return `${label} could not be read as an SVG file.`;
|
|
1113
|
+
}
|
|
1114
|
+
if (!/<svg(?:\s|>)/i.test(content)) {
|
|
1115
|
+
return `${label} must contain an <svg> element.`;
|
|
1116
|
+
}
|
|
1117
|
+
if (/<script(?:\s|>)/i.test(content)) {
|
|
1118
|
+
return `${label} cannot contain <script> tags.`;
|
|
1119
|
+
}
|
|
1120
|
+
return null;
|
|
1121
|
+
}
|
|
1122
|
+
function validateLocalFileContents(fullPath, relativePath, currentPath, label) {
|
|
1123
|
+
const stats = fs.statSync(fullPath);
|
|
1124
|
+
if (stats.size === 0) {
|
|
1125
|
+
throwConfigError(
|
|
1126
|
+
`${label} file is empty. Expected a non-empty file: ${relativePath}`,
|
|
1127
|
+
currentPath
|
|
1128
|
+
);
|
|
1129
|
+
}
|
|
1130
|
+
if (path.extname(relativePath).toLowerCase() === ".svg") {
|
|
1131
|
+
const svgError = getSvgValidationError(fullPath, label);
|
|
1132
|
+
if (svgError) {
|
|
1133
|
+
throwConfigError(svgError, currentPath);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
function validateLocalDocsFile(value, currentPath, label) {
|
|
1138
|
+
const normalizedPath = normalizeLocalDocsPath(value, currentPath, label);
|
|
1139
|
+
const fullPath = path.join(DOCS_DIR, normalizedPath);
|
|
1140
|
+
if (!fs.existsSync(fullPath)) {
|
|
1141
|
+
throwConfigError(`${label} file not found. Expected: ${normalizedPath}`, currentPath);
|
|
1142
|
+
}
|
|
1143
|
+
const stats = fs.statSync(fullPath);
|
|
1144
|
+
if (!stats.isFile()) {
|
|
1145
|
+
throwConfigError(`${label} file not found. Expected: ${normalizedPath}`, currentPath);
|
|
1146
|
+
}
|
|
1147
|
+
validateLocalFileContents(fullPath, normalizedPath, currentPath, label);
|
|
1148
|
+
return normalizedPath;
|
|
1149
|
+
}
|
|
988
1150
|
function splitHrefPathAndSuffix(href) {
|
|
989
1151
|
const match = href.match(/^([^?#]*)(.*)$/);
|
|
990
1152
|
return {
|
|
@@ -1046,7 +1208,12 @@ async function loadOpenApiSpec(filePathOrUrl) {
|
|
|
1046
1208
|
);
|
|
1047
1209
|
}
|
|
1048
1210
|
} else {
|
|
1049
|
-
const
|
|
1211
|
+
const normalizedPath = normalizeLocalDocsPath(
|
|
1212
|
+
filePathOrUrl,
|
|
1213
|
+
[],
|
|
1214
|
+
"OpenAPI file"
|
|
1215
|
+
);
|
|
1216
|
+
const fullPath = path.join(DOCS_DIR, normalizedPath);
|
|
1050
1217
|
fileContent = fs.readFileSync(fullPath, "utf-8");
|
|
1051
1218
|
}
|
|
1052
1219
|
const trimmedContent = fileContent.trim();
|
|
@@ -1087,13 +1254,7 @@ async function validateOpenApiFile(filePathOrUrl, currentPath) {
|
|
|
1087
1254
|
currentPath
|
|
1088
1255
|
);
|
|
1089
1256
|
}
|
|
1090
|
-
|
|
1091
|
-
if (!fs.existsSync(fullPath)) {
|
|
1092
|
-
throwConfigError(
|
|
1093
|
-
`Referenced OpenAPI file not found. Expected: ${filePathOrUrl}`,
|
|
1094
|
-
currentPath
|
|
1095
|
-
);
|
|
1096
|
-
}
|
|
1257
|
+
validateLocalDocsFile(filePathOrUrl, currentPath, "OpenAPI file");
|
|
1097
1258
|
} else {
|
|
1098
1259
|
try {
|
|
1099
1260
|
const url = new URL(filePathOrUrl);
|
|
@@ -1221,6 +1382,12 @@ function parseEndpointString(endpointStr) {
|
|
|
1221
1382
|
}
|
|
1222
1383
|
async function validateNavOpenApiPage(navOpenApiPage, currentPath) {
|
|
1223
1384
|
checkType(navOpenApiPage, "object", currentPath, "Open API page");
|
|
1385
|
+
assertNoUnknownKeys(
|
|
1386
|
+
navOpenApiPage,
|
|
1387
|
+
["source", "endpoint"],
|
|
1388
|
+
currentPath,
|
|
1389
|
+
"Open API page"
|
|
1390
|
+
);
|
|
1224
1391
|
if (typeof navOpenApiPage.source !== "string") {
|
|
1225
1392
|
throwConfigError(
|
|
1226
1393
|
"Open API page must include a 'source' property that is a string.",
|
|
@@ -1270,6 +1437,12 @@ async function validateNavigationNode(item, currentPath, groupDepth = 0) {
|
|
|
1270
1437
|
}
|
|
1271
1438
|
if (isGroup) {
|
|
1272
1439
|
const path2 = [...currentPath];
|
|
1440
|
+
assertNoUnknownKeys(
|
|
1441
|
+
item,
|
|
1442
|
+
["group", "pages", "icon", "expanded", "tag"],
|
|
1443
|
+
path2,
|
|
1444
|
+
"Navigation group"
|
|
1445
|
+
);
|
|
1273
1446
|
if (groupDepth >= 2) {
|
|
1274
1447
|
throwConfigError("Groups can only be nested up to 2 levels deep.", path2);
|
|
1275
1448
|
}
|
|
@@ -1298,6 +1471,12 @@ async function validateNavigationNode(item, currentPath, groupDepth = 0) {
|
|
|
1298
1471
|
}
|
|
1299
1472
|
if (isPage) {
|
|
1300
1473
|
const path2 = [...currentPath];
|
|
1474
|
+
assertNoUnknownKeys(
|
|
1475
|
+
item,
|
|
1476
|
+
["page", "icon", "tag", "title"],
|
|
1477
|
+
path2,
|
|
1478
|
+
"Navigation page"
|
|
1479
|
+
);
|
|
1301
1480
|
const normalizedPagePath = normalizeDocsPagePath(item.page, [
|
|
1302
1481
|
...path2,
|
|
1303
1482
|
"page"
|
|
@@ -1318,6 +1497,12 @@ async function validateNavigationNode(item, currentPath, groupDepth = 0) {
|
|
|
1318
1497
|
}
|
|
1319
1498
|
if (isOpenApiPage) {
|
|
1320
1499
|
const path2 = [...currentPath];
|
|
1500
|
+
assertNoUnknownKeys(
|
|
1501
|
+
item,
|
|
1502
|
+
["openapi", "title", "tag"],
|
|
1503
|
+
path2,
|
|
1504
|
+
"Open API navigation page"
|
|
1505
|
+
);
|
|
1321
1506
|
if ("icon" in item) {
|
|
1322
1507
|
throwConfigError(
|
|
1323
1508
|
"Open API page items cannot have an 'icon'. Method badges are displayed automatically.",
|
|
@@ -1411,6 +1596,12 @@ function getFirstRouteFromNavigation(navigation) {
|
|
|
1411
1596
|
}
|
|
1412
1597
|
async function validateNavOpenApi(navOpenApi, currentPath) {
|
|
1413
1598
|
checkType(navOpenApi, "object", currentPath, "Open API object");
|
|
1599
|
+
assertNoUnknownKeys(
|
|
1600
|
+
navOpenApi,
|
|
1601
|
+
["source", "include", "exclude"],
|
|
1602
|
+
currentPath,
|
|
1603
|
+
"Open API object"
|
|
1604
|
+
);
|
|
1414
1605
|
if (typeof navOpenApi.source !== "string") {
|
|
1415
1606
|
throwConfigError(
|
|
1416
1607
|
"Open API object must have an 'source' property that is a string.",
|
|
@@ -1506,6 +1697,12 @@ async function validateNavOpenApi(navOpenApi, currentPath) {
|
|
|
1506
1697
|
}
|
|
1507
1698
|
async function validateNavMenuItem(item, currentPath) {
|
|
1508
1699
|
checkType(item, "object", currentPath, "Menu item");
|
|
1700
|
+
assertNoUnknownKeys(
|
|
1701
|
+
item,
|
|
1702
|
+
["label", "submenu", "icon"],
|
|
1703
|
+
currentPath,
|
|
1704
|
+
"Menu item"
|
|
1705
|
+
);
|
|
1509
1706
|
validateIcon(item.icon, [...currentPath, "icon"]);
|
|
1510
1707
|
if (!item.label) {
|
|
1511
1708
|
throwConfigError("Menu item must have a 'label' property.", [
|
|
@@ -1600,6 +1797,7 @@ async function validateNavMenuItem(item, currentPath) {
|
|
|
1600
1797
|
}
|
|
1601
1798
|
async function validateNavMenu(menu, currentPath) {
|
|
1602
1799
|
checkType(menu, "object", currentPath, "Menu");
|
|
1800
|
+
assertNoUnknownKeys(menu, ["type", "label", "items"], currentPath, "Menu");
|
|
1603
1801
|
if (menu.type !== void 0) {
|
|
1604
1802
|
if (menu.type !== "dropdown" && menu.type !== "segmented") {
|
|
1605
1803
|
throwConfigError(
|
|
@@ -1623,6 +1821,12 @@ async function validateNavMenu(menu, currentPath) {
|
|
|
1623
1821
|
function validateNavbarItem(item, currentPath) {
|
|
1624
1822
|
if (item === void 0) return null;
|
|
1625
1823
|
checkType(item, "object", currentPath, "Navbar item");
|
|
1824
|
+
assertNoUnknownKeys(
|
|
1825
|
+
item,
|
|
1826
|
+
["text", "href", "icon", "color"],
|
|
1827
|
+
currentPath,
|
|
1828
|
+
"Navbar item"
|
|
1829
|
+
);
|
|
1626
1830
|
if (typeof item.text !== "string") {
|
|
1627
1831
|
throwConfigError("Navbar item must have a 'text' property.", [
|
|
1628
1832
|
...currentPath,
|
|
@@ -1674,24 +1878,16 @@ function validateLogoPaddingValue(value, currentPath, label) {
|
|
|
1674
1878
|
}
|
|
1675
1879
|
}
|
|
1676
1880
|
function validateLogoImagePath(imagePath, currentPath, label) {
|
|
1677
|
-
const
|
|
1678
|
-
const
|
|
1679
|
-
|
|
1680
|
-
);
|
|
1881
|
+
const normalizedPath = normalizeLocalDocsPath(imagePath, currentPath, label);
|
|
1882
|
+
const extension = path.extname(normalizedPath).toLowerCase();
|
|
1883
|
+
const hasValidExtension = LOCAL_IMAGE_EXTENSIONS.has(extension);
|
|
1681
1884
|
if (!hasValidExtension) {
|
|
1682
1885
|
throwConfigError(
|
|
1683
|
-
`${label} must be a valid image file (.svg, .png, .jpg, .jpeg, .webp, .gif)`,
|
|
1684
|
-
currentPath
|
|
1685
|
-
);
|
|
1686
|
-
}
|
|
1687
|
-
const normalizedPath = imagePath.startsWith("/") ? imagePath.slice(1) : imagePath;
|
|
1688
|
-
const fullPath = path.join(DOCS_DIR, normalizedPath);
|
|
1689
|
-
if (!fs.existsSync(fullPath)) {
|
|
1690
|
-
throwConfigError(
|
|
1691
|
-
`${label} file not found. Expected: ${normalizedPath}`,
|
|
1886
|
+
`${label} must be a valid image file (.svg, .png, .jpg, .jpeg, .webp, .gif, .ico, .avif)`,
|
|
1692
1887
|
currentPath
|
|
1693
1888
|
);
|
|
1694
1889
|
}
|
|
1890
|
+
validateLocalDocsFile(imagePath, currentPath, label);
|
|
1695
1891
|
}
|
|
1696
1892
|
function validateAssistantIconSource(iconSource, currentPath) {
|
|
1697
1893
|
checkType(iconSource, "string", currentPath, "Assistant icon source");
|
|
@@ -1740,14 +1936,7 @@ function validateAssistantIconSource(iconSource, currentPath) {
|
|
|
1740
1936
|
currentPath
|
|
1741
1937
|
);
|
|
1742
1938
|
}
|
|
1743
|
-
|
|
1744
|
-
const fullPath = path.join(DOCS_DIR, normalizedPath);
|
|
1745
|
-
if (!fs.existsSync(fullPath)) {
|
|
1746
|
-
throwConfigError(
|
|
1747
|
-
`Assistant icon source file not found. Expected: ${normalizedPath}`,
|
|
1748
|
-
currentPath
|
|
1749
|
-
);
|
|
1750
|
-
}
|
|
1939
|
+
validateLocalDocsFile(parsed.pathname, currentPath, "Assistant icon source");
|
|
1751
1940
|
return trimmedSource;
|
|
1752
1941
|
}
|
|
1753
1942
|
function validateLogoVariant(variant, currentPath, mode) {
|
|
@@ -1812,6 +2001,12 @@ function validateLogoVariant(variant, currentPath, mode) {
|
|
|
1812
2001
|
function validateLogo(logo) {
|
|
1813
2002
|
if (logo === void 0) return;
|
|
1814
2003
|
checkType(logo, "object", ["logo"], "Logo configuration");
|
|
2004
|
+
assertNoUnknownKeys(
|
|
2005
|
+
logo,
|
|
2006
|
+
["light", "dark", "href", "pill"],
|
|
2007
|
+
["logo"],
|
|
2008
|
+
"Logo configuration"
|
|
2009
|
+
);
|
|
1815
2010
|
validateLogoVariant(logo.light, ["logo", "light"], "light");
|
|
1816
2011
|
validateLogoVariant(logo.dark, ["logo", "dark"], "dark");
|
|
1817
2012
|
if (logo.href !== void 0) {
|
|
@@ -2426,6 +2621,12 @@ function validateNavbar(navbar) {
|
|
|
2426
2621
|
const hiddenPageRoutes = [];
|
|
2427
2622
|
if (navbar === void 0) return hiddenPageRoutes;
|
|
2428
2623
|
checkType(navbar, "object", ["navbar"], "Navbar configuration");
|
|
2624
|
+
assertNoUnknownKeys(
|
|
2625
|
+
navbar,
|
|
2626
|
+
["blur", "primary", "secondary", "links"],
|
|
2627
|
+
["navbar"],
|
|
2628
|
+
"Navbar configuration"
|
|
2629
|
+
);
|
|
2429
2630
|
checkType(navbar.blur, "boolean", ["navbar", "blur"], "Navbar blur setting");
|
|
2430
2631
|
const primaryPageRoute = validateNavbarItem(navbar.primary, [
|
|
2431
2632
|
"navbar",
|
|
@@ -2456,6 +2657,12 @@ function validateFooter(footer) {
|
|
|
2456
2657
|
const hiddenPageRoutes = [];
|
|
2457
2658
|
if (footer === void 0) return hiddenPageRoutes;
|
|
2458
2659
|
checkType(footer, "object", ["footer"], "Footer configuration");
|
|
2660
|
+
assertNoUnknownKeys(
|
|
2661
|
+
footer,
|
|
2662
|
+
["socials", "links"],
|
|
2663
|
+
["footer"],
|
|
2664
|
+
"Footer configuration"
|
|
2665
|
+
);
|
|
2459
2666
|
if (footer.socials !== void 0) {
|
|
2460
2667
|
checkType(
|
|
2461
2668
|
footer.socials,
|
|
@@ -2509,6 +2716,12 @@ function validateFooter(footer) {
|
|
|
2509
2716
|
checkType(footer.links, "array", ["footer", "links"], "Footer links");
|
|
2510
2717
|
footer.links.forEach((link, i) => {
|
|
2511
2718
|
checkType(link, "object", ["footer", "links", i], "Footer link");
|
|
2719
|
+
assertNoUnknownKeys(
|
|
2720
|
+
link,
|
|
2721
|
+
["text", "href"],
|
|
2722
|
+
["footer", "links", i],
|
|
2723
|
+
"Footer link"
|
|
2724
|
+
);
|
|
2512
2725
|
if (typeof link.text !== "string") {
|
|
2513
2726
|
throwConfigError("Footer link must have a 'text' property.", [
|
|
2514
2727
|
"footer",
|
|
@@ -2540,6 +2753,12 @@ function validateFooter(footer) {
|
|
|
2540
2753
|
}
|
|
2541
2754
|
async function validateNavigation(navigation) {
|
|
2542
2755
|
checkType(navigation, "object", ["navigation"], "Navigation");
|
|
2756
|
+
assertNoUnknownKeys(
|
|
2757
|
+
navigation,
|
|
2758
|
+
["pages", "menu", "openapi"],
|
|
2759
|
+
["navigation"],
|
|
2760
|
+
"Navigation"
|
|
2761
|
+
);
|
|
2543
2762
|
const keys = Object.keys(navigation);
|
|
2544
2763
|
const validKeys = ["pages", "menu", "openapi"];
|
|
2545
2764
|
const navKeys = keys.filter((key) => validKeys.includes(key));
|
|
@@ -2584,6 +2803,25 @@ async function validateNavigation(navigation) {
|
|
|
2584
2803
|
}
|
|
2585
2804
|
}
|
|
2586
2805
|
async function validateConfig(config) {
|
|
2806
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) {
|
|
2807
|
+
throwConfigError("docs.json must be an object.", []);
|
|
2808
|
+
}
|
|
2809
|
+
assertNoUnknownKeys(
|
|
2810
|
+
config,
|
|
2811
|
+
[
|
|
2812
|
+
"title",
|
|
2813
|
+
"logo",
|
|
2814
|
+
"theme",
|
|
2815
|
+
"assistant",
|
|
2816
|
+
"home",
|
|
2817
|
+
"navigation",
|
|
2818
|
+
"navbar",
|
|
2819
|
+
"playground",
|
|
2820
|
+
"footer"
|
|
2821
|
+
],
|
|
2822
|
+
[],
|
|
2823
|
+
"docs.json"
|
|
2824
|
+
);
|
|
2587
2825
|
validateTitle(config.title);
|
|
2588
2826
|
validateLogo(config.logo);
|
|
2589
2827
|
validateTheme(config.theme);
|
|
@@ -2612,6 +2850,12 @@ async function validateConfig(config) {
|
|
|
2612
2850
|
config.hiddenPageRoutes = Array.from(dedupedHiddenPageRoutes.values());
|
|
2613
2851
|
if (config.playground !== void 0) {
|
|
2614
2852
|
checkType(config.playground, "object", ["playground"], "Playground");
|
|
2853
|
+
assertNoUnknownKeys(
|
|
2854
|
+
config.playground,
|
|
2855
|
+
["proxy"],
|
|
2856
|
+
["playground"],
|
|
2857
|
+
"Playground"
|
|
2858
|
+
);
|
|
2615
2859
|
if (config.playground.proxy !== void 0) {
|
|
2616
2860
|
checkType(
|
|
2617
2861
|
config.playground.proxy,
|
|
@@ -2871,16 +3115,38 @@ function validateDocsRootAbsoluteHref(args) {
|
|
|
2871
3115
|
}
|
|
2872
3116
|
if (resolvedHref.kind === "local-asset") {
|
|
2873
3117
|
const fullAssetPath = path.join(DOCS_DIR, resolvedHref.filePath);
|
|
2874
|
-
if (!fs.existsSync(fullAssetPath)
|
|
3118
|
+
if (!fs.existsSync(fullAssetPath)) {
|
|
3119
|
+
throw new Error(
|
|
3120
|
+
`Invalid local asset "${args.href}" in ${args.sourceFile}. No matching file was found. Expected "${resolvedHref.filePath}" under the docs root.`
|
|
3121
|
+
);
|
|
3122
|
+
}
|
|
3123
|
+
const assetStats = fs.statSync(fullAssetPath);
|
|
3124
|
+
if (!assetStats.isFile()) {
|
|
2875
3125
|
throw new Error(
|
|
2876
3126
|
`Invalid local asset "${args.href}" in ${args.sourceFile}. No matching file was found. Expected "${resolvedHref.filePath}" under the docs root.`
|
|
2877
3127
|
);
|
|
2878
3128
|
}
|
|
3129
|
+
if (assetStats.size === 0) {
|
|
3130
|
+
throw new Error(
|
|
3131
|
+
`Invalid local asset "${args.href}" in ${args.sourceFile}. The referenced file is empty.`
|
|
3132
|
+
);
|
|
3133
|
+
}
|
|
2879
3134
|
if (!resolvedHref.publishable) {
|
|
2880
3135
|
throw new Error(
|
|
2881
3136
|
`Invalid local asset "${args.href}" in ${args.sourceFile}. Files with extension "${resolvedHref.extension}" are not published as docs assets.`
|
|
2882
3137
|
);
|
|
2883
3138
|
}
|
|
3139
|
+
if (resolvedHref.extension === ".svg") {
|
|
3140
|
+
const svgError = getSvgValidationError(
|
|
3141
|
+
fullAssetPath,
|
|
3142
|
+
`SVG asset "${args.href}"`
|
|
3143
|
+
);
|
|
3144
|
+
if (svgError) {
|
|
3145
|
+
throw new Error(
|
|
3146
|
+
`Invalid local asset "${args.href}" in ${args.sourceFile}. ${svgError}`
|
|
3147
|
+
);
|
|
3148
|
+
}
|
|
3149
|
+
}
|
|
2884
3150
|
return;
|
|
2885
3151
|
}
|
|
2886
3152
|
if (args.expectedTarget === "asset") {
|
|
@@ -2939,12 +3205,15 @@ function createInternalLinkValidationPlugin(args) {
|
|
|
2939
3205
|
if (hrefAttribute.name !== "href" && hrefAttribute.name !== "src") {
|
|
2940
3206
|
continue;
|
|
2941
3207
|
}
|
|
2942
|
-
if (
|
|
3208
|
+
if (typeof hrefAttribute.value !== "string") {
|
|
3209
|
+
if (hrefAttribute.name === "src" && element.name === "Image") {
|
|
3210
|
+
continue;
|
|
3211
|
+
}
|
|
3212
|
+
const example = hrefAttribute.name === "src" ? 'src="/images/example.png"' : 'href="/guides/quickstart"';
|
|
2943
3213
|
throw new Error(
|
|
2944
|
-
`Invalid JSX
|
|
3214
|
+
`Invalid JSX ${hrefAttribute.name} in ${args.sourceFile}. Direct JSX ${hrefAttribute.name} props must use quoted strings, such as ${example}, not JSX expressions.`
|
|
2945
3215
|
);
|
|
2946
3216
|
}
|
|
2947
|
-
if (typeof hrefAttribute.value !== "string") continue;
|
|
2948
3217
|
validateDocsRootAbsoluteHref({
|
|
2949
3218
|
href: hrefAttribute.value,
|
|
2950
3219
|
sourceFile: args.sourceFile,
|