radiant-docs-validator 0.1.15 → 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 +309 -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",
|
|
@@ -486,6 +523,18 @@ function validateRadiantComponentProps(componentName, props, options) {
|
|
|
486
523
|
validateImage(props, options);
|
|
487
524
|
return;
|
|
488
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
|
+
}
|
|
489
538
|
if (componentName === "Columns") {
|
|
490
539
|
assertNoUnknownProps("Columns", props, ["columns"], options.sourceFile);
|
|
491
540
|
assertType("Columns", "columns", props.columns, ["number", "string"], options.sourceFile);
|
|
@@ -696,6 +745,16 @@ function configureDocsValidator({
|
|
|
696
745
|
lastMtime = 0;
|
|
697
746
|
}
|
|
698
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
|
+
]);
|
|
699
758
|
function isUrl(str) {
|
|
700
759
|
try {
|
|
701
760
|
const url = new URL(str);
|
|
@@ -750,14 +809,19 @@ function validateIcon(icon, currentPath) {
|
|
|
750
809
|
}
|
|
751
810
|
return;
|
|
752
811
|
}
|
|
753
|
-
const localRelativePath =
|
|
754
|
-
|
|
755
|
-
|
|
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)) {
|
|
756
819
|
throwConfigError(
|
|
757
|
-
`Icon
|
|
820
|
+
`Icon must be a valid image file (.svg, .png, .jpg, .jpeg, .webp, .gif, .ico, .avif). Found: ${icon}`,
|
|
758
821
|
currentPath
|
|
759
822
|
);
|
|
760
823
|
}
|
|
824
|
+
return;
|
|
761
825
|
}
|
|
762
826
|
function validateComponentIcon(icon, currentPath) {
|
|
763
827
|
const trimmedIcon = icon.trim();
|
|
@@ -809,13 +873,25 @@ var throwConfigError = (message, currentPath) => {
|
|
|
809
873
|
throw new Error(`${message}${location}
|
|
810
874
|
`);
|
|
811
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
|
+
}
|
|
812
888
|
function checkType(value, type, currentPath, label) {
|
|
813
|
-
if (value === void 0
|
|
889
|
+
if (value === void 0) return;
|
|
814
890
|
if (type === "array") {
|
|
815
891
|
if (!Array.isArray(value))
|
|
816
892
|
throwConfigError(`${label} must be an array.`, currentPath);
|
|
817
893
|
} else if (type === "object") {
|
|
818
|
-
if (typeof value !== "object" || value === null)
|
|
894
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
819
895
|
throwConfigError(`${label} must be an object.`, currentPath);
|
|
820
896
|
} else {
|
|
821
897
|
if (typeof value !== type)
|
|
@@ -949,8 +1025,13 @@ function normalizeSeedValue(value, currentPath, label) {
|
|
|
949
1025
|
return trimmedValue;
|
|
950
1026
|
}
|
|
951
1027
|
function validateFileExistence(filePath, currentPath) {
|
|
952
|
-
const
|
|
953
|
-
|
|
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()) {
|
|
954
1035
|
throwConfigError(
|
|
955
1036
|
`Referenced file not found. Expected: ${filePath}`,
|
|
956
1037
|
currentPath
|
|
@@ -982,12 +1063,90 @@ function normalizeDocsPagePath(value, currentPath, label = "Page path") {
|
|
|
982
1063
|
currentPath
|
|
983
1064
|
);
|
|
984
1065
|
}
|
|
985
|
-
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(/\/+$/, "");
|
|
986
1074
|
if (normalizedPath === "") {
|
|
987
1075
|
throwConfigError(`${label} cannot be '/'`, currentPath);
|
|
988
1076
|
}
|
|
989
1077
|
return normalizedPath;
|
|
990
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
|
+
}
|
|
991
1150
|
function splitHrefPathAndSuffix(href) {
|
|
992
1151
|
const match = href.match(/^([^?#]*)(.*)$/);
|
|
993
1152
|
return {
|
|
@@ -1049,7 +1208,12 @@ async function loadOpenApiSpec(filePathOrUrl) {
|
|
|
1049
1208
|
);
|
|
1050
1209
|
}
|
|
1051
1210
|
} else {
|
|
1052
|
-
const
|
|
1211
|
+
const normalizedPath = normalizeLocalDocsPath(
|
|
1212
|
+
filePathOrUrl,
|
|
1213
|
+
[],
|
|
1214
|
+
"OpenAPI file"
|
|
1215
|
+
);
|
|
1216
|
+
const fullPath = path.join(DOCS_DIR, normalizedPath);
|
|
1053
1217
|
fileContent = fs.readFileSync(fullPath, "utf-8");
|
|
1054
1218
|
}
|
|
1055
1219
|
const trimmedContent = fileContent.trim();
|
|
@@ -1090,13 +1254,7 @@ async function validateOpenApiFile(filePathOrUrl, currentPath) {
|
|
|
1090
1254
|
currentPath
|
|
1091
1255
|
);
|
|
1092
1256
|
}
|
|
1093
|
-
|
|
1094
|
-
if (!fs.existsSync(fullPath)) {
|
|
1095
|
-
throwConfigError(
|
|
1096
|
-
`Referenced OpenAPI file not found. Expected: ${filePathOrUrl}`,
|
|
1097
|
-
currentPath
|
|
1098
|
-
);
|
|
1099
|
-
}
|
|
1257
|
+
validateLocalDocsFile(filePathOrUrl, currentPath, "OpenAPI file");
|
|
1100
1258
|
} else {
|
|
1101
1259
|
try {
|
|
1102
1260
|
const url = new URL(filePathOrUrl);
|
|
@@ -1224,6 +1382,12 @@ function parseEndpointString(endpointStr) {
|
|
|
1224
1382
|
}
|
|
1225
1383
|
async function validateNavOpenApiPage(navOpenApiPage, currentPath) {
|
|
1226
1384
|
checkType(navOpenApiPage, "object", currentPath, "Open API page");
|
|
1385
|
+
assertNoUnknownKeys(
|
|
1386
|
+
navOpenApiPage,
|
|
1387
|
+
["source", "endpoint"],
|
|
1388
|
+
currentPath,
|
|
1389
|
+
"Open API page"
|
|
1390
|
+
);
|
|
1227
1391
|
if (typeof navOpenApiPage.source !== "string") {
|
|
1228
1392
|
throwConfigError(
|
|
1229
1393
|
"Open API page must include a 'source' property that is a string.",
|
|
@@ -1273,6 +1437,12 @@ async function validateNavigationNode(item, currentPath, groupDepth = 0) {
|
|
|
1273
1437
|
}
|
|
1274
1438
|
if (isGroup) {
|
|
1275
1439
|
const path2 = [...currentPath];
|
|
1440
|
+
assertNoUnknownKeys(
|
|
1441
|
+
item,
|
|
1442
|
+
["group", "pages", "icon", "expanded", "tag"],
|
|
1443
|
+
path2,
|
|
1444
|
+
"Navigation group"
|
|
1445
|
+
);
|
|
1276
1446
|
if (groupDepth >= 2) {
|
|
1277
1447
|
throwConfigError("Groups can only be nested up to 2 levels deep.", path2);
|
|
1278
1448
|
}
|
|
@@ -1301,6 +1471,12 @@ async function validateNavigationNode(item, currentPath, groupDepth = 0) {
|
|
|
1301
1471
|
}
|
|
1302
1472
|
if (isPage) {
|
|
1303
1473
|
const path2 = [...currentPath];
|
|
1474
|
+
assertNoUnknownKeys(
|
|
1475
|
+
item,
|
|
1476
|
+
["page", "icon", "tag", "title"],
|
|
1477
|
+
path2,
|
|
1478
|
+
"Navigation page"
|
|
1479
|
+
);
|
|
1304
1480
|
const normalizedPagePath = normalizeDocsPagePath(item.page, [
|
|
1305
1481
|
...path2,
|
|
1306
1482
|
"page"
|
|
@@ -1321,6 +1497,12 @@ async function validateNavigationNode(item, currentPath, groupDepth = 0) {
|
|
|
1321
1497
|
}
|
|
1322
1498
|
if (isOpenApiPage) {
|
|
1323
1499
|
const path2 = [...currentPath];
|
|
1500
|
+
assertNoUnknownKeys(
|
|
1501
|
+
item,
|
|
1502
|
+
["openapi", "title", "tag"],
|
|
1503
|
+
path2,
|
|
1504
|
+
"Open API navigation page"
|
|
1505
|
+
);
|
|
1324
1506
|
if ("icon" in item) {
|
|
1325
1507
|
throwConfigError(
|
|
1326
1508
|
"Open API page items cannot have an 'icon'. Method badges are displayed automatically.",
|
|
@@ -1414,6 +1596,12 @@ function getFirstRouteFromNavigation(navigation) {
|
|
|
1414
1596
|
}
|
|
1415
1597
|
async function validateNavOpenApi(navOpenApi, currentPath) {
|
|
1416
1598
|
checkType(navOpenApi, "object", currentPath, "Open API object");
|
|
1599
|
+
assertNoUnknownKeys(
|
|
1600
|
+
navOpenApi,
|
|
1601
|
+
["source", "include", "exclude"],
|
|
1602
|
+
currentPath,
|
|
1603
|
+
"Open API object"
|
|
1604
|
+
);
|
|
1417
1605
|
if (typeof navOpenApi.source !== "string") {
|
|
1418
1606
|
throwConfigError(
|
|
1419
1607
|
"Open API object must have an 'source' property that is a string.",
|
|
@@ -1509,6 +1697,12 @@ async function validateNavOpenApi(navOpenApi, currentPath) {
|
|
|
1509
1697
|
}
|
|
1510
1698
|
async function validateNavMenuItem(item, currentPath) {
|
|
1511
1699
|
checkType(item, "object", currentPath, "Menu item");
|
|
1700
|
+
assertNoUnknownKeys(
|
|
1701
|
+
item,
|
|
1702
|
+
["label", "submenu", "icon"],
|
|
1703
|
+
currentPath,
|
|
1704
|
+
"Menu item"
|
|
1705
|
+
);
|
|
1512
1706
|
validateIcon(item.icon, [...currentPath, "icon"]);
|
|
1513
1707
|
if (!item.label) {
|
|
1514
1708
|
throwConfigError("Menu item must have a 'label' property.", [
|
|
@@ -1603,6 +1797,7 @@ async function validateNavMenuItem(item, currentPath) {
|
|
|
1603
1797
|
}
|
|
1604
1798
|
async function validateNavMenu(menu, currentPath) {
|
|
1605
1799
|
checkType(menu, "object", currentPath, "Menu");
|
|
1800
|
+
assertNoUnknownKeys(menu, ["type", "label", "items"], currentPath, "Menu");
|
|
1606
1801
|
if (menu.type !== void 0) {
|
|
1607
1802
|
if (menu.type !== "dropdown" && menu.type !== "segmented") {
|
|
1608
1803
|
throwConfigError(
|
|
@@ -1626,6 +1821,12 @@ async function validateNavMenu(menu, currentPath) {
|
|
|
1626
1821
|
function validateNavbarItem(item, currentPath) {
|
|
1627
1822
|
if (item === void 0) return null;
|
|
1628
1823
|
checkType(item, "object", currentPath, "Navbar item");
|
|
1824
|
+
assertNoUnknownKeys(
|
|
1825
|
+
item,
|
|
1826
|
+
["text", "href", "icon", "color"],
|
|
1827
|
+
currentPath,
|
|
1828
|
+
"Navbar item"
|
|
1829
|
+
);
|
|
1629
1830
|
if (typeof item.text !== "string") {
|
|
1630
1831
|
throwConfigError("Navbar item must have a 'text' property.", [
|
|
1631
1832
|
...currentPath,
|
|
@@ -1677,24 +1878,16 @@ function validateLogoPaddingValue(value, currentPath, label) {
|
|
|
1677
1878
|
}
|
|
1678
1879
|
}
|
|
1679
1880
|
function validateLogoImagePath(imagePath, currentPath, label) {
|
|
1680
|
-
const
|
|
1681
|
-
const
|
|
1682
|
-
|
|
1683
|
-
);
|
|
1881
|
+
const normalizedPath = normalizeLocalDocsPath(imagePath, currentPath, label);
|
|
1882
|
+
const extension = path.extname(normalizedPath).toLowerCase();
|
|
1883
|
+
const hasValidExtension = LOCAL_IMAGE_EXTENSIONS.has(extension);
|
|
1684
1884
|
if (!hasValidExtension) {
|
|
1685
1885
|
throwConfigError(
|
|
1686
|
-
`${label} must be a valid image file (.svg, .png, .jpg, .jpeg, .webp, .gif)`,
|
|
1687
|
-
currentPath
|
|
1688
|
-
);
|
|
1689
|
-
}
|
|
1690
|
-
const normalizedPath = imagePath.startsWith("/") ? imagePath.slice(1) : imagePath;
|
|
1691
|
-
const fullPath = path.join(DOCS_DIR, normalizedPath);
|
|
1692
|
-
if (!fs.existsSync(fullPath)) {
|
|
1693
|
-
throwConfigError(
|
|
1694
|
-
`${label} file not found. Expected: ${normalizedPath}`,
|
|
1886
|
+
`${label} must be a valid image file (.svg, .png, .jpg, .jpeg, .webp, .gif, .ico, .avif)`,
|
|
1695
1887
|
currentPath
|
|
1696
1888
|
);
|
|
1697
1889
|
}
|
|
1890
|
+
validateLocalDocsFile(imagePath, currentPath, label);
|
|
1698
1891
|
}
|
|
1699
1892
|
function validateAssistantIconSource(iconSource, currentPath) {
|
|
1700
1893
|
checkType(iconSource, "string", currentPath, "Assistant icon source");
|
|
@@ -1743,14 +1936,7 @@ function validateAssistantIconSource(iconSource, currentPath) {
|
|
|
1743
1936
|
currentPath
|
|
1744
1937
|
);
|
|
1745
1938
|
}
|
|
1746
|
-
|
|
1747
|
-
const fullPath = path.join(DOCS_DIR, normalizedPath);
|
|
1748
|
-
if (!fs.existsSync(fullPath)) {
|
|
1749
|
-
throwConfigError(
|
|
1750
|
-
`Assistant icon source file not found. Expected: ${normalizedPath}`,
|
|
1751
|
-
currentPath
|
|
1752
|
-
);
|
|
1753
|
-
}
|
|
1939
|
+
validateLocalDocsFile(parsed.pathname, currentPath, "Assistant icon source");
|
|
1754
1940
|
return trimmedSource;
|
|
1755
1941
|
}
|
|
1756
1942
|
function validateLogoVariant(variant, currentPath, mode) {
|
|
@@ -1815,6 +2001,12 @@ function validateLogoVariant(variant, currentPath, mode) {
|
|
|
1815
2001
|
function validateLogo(logo) {
|
|
1816
2002
|
if (logo === void 0) return;
|
|
1817
2003
|
checkType(logo, "object", ["logo"], "Logo configuration");
|
|
2004
|
+
assertNoUnknownKeys(
|
|
2005
|
+
logo,
|
|
2006
|
+
["light", "dark", "href", "pill"],
|
|
2007
|
+
["logo"],
|
|
2008
|
+
"Logo configuration"
|
|
2009
|
+
);
|
|
1818
2010
|
validateLogoVariant(logo.light, ["logo", "light"], "light");
|
|
1819
2011
|
validateLogoVariant(logo.dark, ["logo", "dark"], "dark");
|
|
1820
2012
|
if (logo.href !== void 0) {
|
|
@@ -2429,6 +2621,12 @@ function validateNavbar(navbar) {
|
|
|
2429
2621
|
const hiddenPageRoutes = [];
|
|
2430
2622
|
if (navbar === void 0) return hiddenPageRoutes;
|
|
2431
2623
|
checkType(navbar, "object", ["navbar"], "Navbar configuration");
|
|
2624
|
+
assertNoUnknownKeys(
|
|
2625
|
+
navbar,
|
|
2626
|
+
["blur", "primary", "secondary", "links"],
|
|
2627
|
+
["navbar"],
|
|
2628
|
+
"Navbar configuration"
|
|
2629
|
+
);
|
|
2432
2630
|
checkType(navbar.blur, "boolean", ["navbar", "blur"], "Navbar blur setting");
|
|
2433
2631
|
const primaryPageRoute = validateNavbarItem(navbar.primary, [
|
|
2434
2632
|
"navbar",
|
|
@@ -2459,6 +2657,12 @@ function validateFooter(footer) {
|
|
|
2459
2657
|
const hiddenPageRoutes = [];
|
|
2460
2658
|
if (footer === void 0) return hiddenPageRoutes;
|
|
2461
2659
|
checkType(footer, "object", ["footer"], "Footer configuration");
|
|
2660
|
+
assertNoUnknownKeys(
|
|
2661
|
+
footer,
|
|
2662
|
+
["socials", "links"],
|
|
2663
|
+
["footer"],
|
|
2664
|
+
"Footer configuration"
|
|
2665
|
+
);
|
|
2462
2666
|
if (footer.socials !== void 0) {
|
|
2463
2667
|
checkType(
|
|
2464
2668
|
footer.socials,
|
|
@@ -2512,6 +2716,12 @@ function validateFooter(footer) {
|
|
|
2512
2716
|
checkType(footer.links, "array", ["footer", "links"], "Footer links");
|
|
2513
2717
|
footer.links.forEach((link, i) => {
|
|
2514
2718
|
checkType(link, "object", ["footer", "links", i], "Footer link");
|
|
2719
|
+
assertNoUnknownKeys(
|
|
2720
|
+
link,
|
|
2721
|
+
["text", "href"],
|
|
2722
|
+
["footer", "links", i],
|
|
2723
|
+
"Footer link"
|
|
2724
|
+
);
|
|
2515
2725
|
if (typeof link.text !== "string") {
|
|
2516
2726
|
throwConfigError("Footer link must have a 'text' property.", [
|
|
2517
2727
|
"footer",
|
|
@@ -2543,6 +2753,12 @@ function validateFooter(footer) {
|
|
|
2543
2753
|
}
|
|
2544
2754
|
async function validateNavigation(navigation) {
|
|
2545
2755
|
checkType(navigation, "object", ["navigation"], "Navigation");
|
|
2756
|
+
assertNoUnknownKeys(
|
|
2757
|
+
navigation,
|
|
2758
|
+
["pages", "menu", "openapi"],
|
|
2759
|
+
["navigation"],
|
|
2760
|
+
"Navigation"
|
|
2761
|
+
);
|
|
2546
2762
|
const keys = Object.keys(navigation);
|
|
2547
2763
|
const validKeys = ["pages", "menu", "openapi"];
|
|
2548
2764
|
const navKeys = keys.filter((key) => validKeys.includes(key));
|
|
@@ -2587,6 +2803,25 @@ async function validateNavigation(navigation) {
|
|
|
2587
2803
|
}
|
|
2588
2804
|
}
|
|
2589
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
|
+
);
|
|
2590
2825
|
validateTitle(config.title);
|
|
2591
2826
|
validateLogo(config.logo);
|
|
2592
2827
|
validateTheme(config.theme);
|
|
@@ -2615,6 +2850,12 @@ async function validateConfig(config) {
|
|
|
2615
2850
|
config.hiddenPageRoutes = Array.from(dedupedHiddenPageRoutes.values());
|
|
2616
2851
|
if (config.playground !== void 0) {
|
|
2617
2852
|
checkType(config.playground, "object", ["playground"], "Playground");
|
|
2853
|
+
assertNoUnknownKeys(
|
|
2854
|
+
config.playground,
|
|
2855
|
+
["proxy"],
|
|
2856
|
+
["playground"],
|
|
2857
|
+
"Playground"
|
|
2858
|
+
);
|
|
2618
2859
|
if (config.playground.proxy !== void 0) {
|
|
2619
2860
|
checkType(
|
|
2620
2861
|
config.playground.proxy,
|
|
@@ -2874,16 +3115,38 @@ function validateDocsRootAbsoluteHref(args) {
|
|
|
2874
3115
|
}
|
|
2875
3116
|
if (resolvedHref.kind === "local-asset") {
|
|
2876
3117
|
const fullAssetPath = path.join(DOCS_DIR, resolvedHref.filePath);
|
|
2877
|
-
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()) {
|
|
2878
3125
|
throw new Error(
|
|
2879
3126
|
`Invalid local asset "${args.href}" in ${args.sourceFile}. No matching file was found. Expected "${resolvedHref.filePath}" under the docs root.`
|
|
2880
3127
|
);
|
|
2881
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
|
+
}
|
|
2882
3134
|
if (!resolvedHref.publishable) {
|
|
2883
3135
|
throw new Error(
|
|
2884
3136
|
`Invalid local asset "${args.href}" in ${args.sourceFile}. Files with extension "${resolvedHref.extension}" are not published as docs assets.`
|
|
2885
3137
|
);
|
|
2886
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
|
+
}
|
|
2887
3150
|
return;
|
|
2888
3151
|
}
|
|
2889
3152
|
if (args.expectedTarget === "asset") {
|
|
@@ -2942,12 +3205,15 @@ function createInternalLinkValidationPlugin(args) {
|
|
|
2942
3205
|
if (hrefAttribute.name !== "href" && hrefAttribute.name !== "src") {
|
|
2943
3206
|
continue;
|
|
2944
3207
|
}
|
|
2945
|
-
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"';
|
|
2946
3213
|
throw new Error(
|
|
2947
|
-
`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.`
|
|
2948
3215
|
);
|
|
2949
3216
|
}
|
|
2950
|
-
if (typeof hrefAttribute.value !== "string") continue;
|
|
2951
3217
|
validateDocsRootAbsoluteHref({
|
|
2952
3218
|
href: hrefAttribute.value,
|
|
2953
3219
|
sourceFile: args.sourceFile,
|