radiant-docs 0.1.61 → 0.1.63
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/package.json +1 -1
- package/template/astro.config.mjs +38 -27
- package/template/package-lock.json +2858 -1140
- package/template/package.json +18 -13
- package/template/scripts/generate-proxy-allowed-origins.mjs +10 -179
- package/template/scripts/publish-shiki-platform-assets.mjs +1177 -0
- package/template/src/components/Header.astro +6 -1
- package/template/src/components/NavigationTabList.astro +65 -0
- package/template/src/components/NavigationTabs.astro +109 -0
- package/template/src/components/OpenApiPage.astro +17 -1
- package/template/src/components/Sidebar.astro +2 -2
- package/template/src/components/SidebarDropdown.astro +105 -44
- package/template/src/components/SidebarMenu.astro +3 -0
- package/template/src/components/SidebarSegmented.astro +87 -52
- package/template/src/components/SidebarTabs.astro +86 -0
- package/template/src/components/chat/AssistantDocsWidget.tsx +127 -2
- package/template/src/components/chat/AssistantEmbedPanel.tsx +401 -283
- package/template/src/components/endpoint/PlaygroundForm.astro +69 -55
- package/template/src/components/endpoint/ResponseDisplay.astro +2 -2
- package/template/src/components/user/Accordion.astro +1 -1
- package/template/src/components/user/Callout.astro +2 -2
- package/template/src/components/user/CodeBlock.astro +58 -7
- package/template/src/components/user/CodeGroup.astro +52 -1
- package/template/src/components/user/Column.astro +1 -1
- package/template/src/components/user/Step.astro +1 -1
- package/template/src/components/user/Tabs.astro +1 -1
- package/template/src/generated/shiki-platform-assets.json +24 -0
- package/template/src/layouts/Layout.astro +111 -8
- package/template/src/lib/assistant-panel-config.ts +4 -0
- package/template/src/lib/assistant-shiki-client.ts +522 -0
- package/template/src/lib/client-shiki-config.ts +60 -0
- package/template/src/lib/dev-playground-proxy.mjs +597 -0
- package/template/src/lib/mdx/remark-resolve-internal-links.ts +334 -17
- package/template/src/lib/proxy-allowed-origins.mjs +189 -0
- package/template/src/lib/routes.ts +66 -24
- package/template/src/styles/global.css +16 -4
- package/template/src/components/ui/demo/CodeDemo.astro +0 -15
- package/template/src/components/ui/demo/Demo.astro +0 -3
- package/template/src/components/ui/demo/UiDisplay.astro +0 -13
|
@@ -9,10 +9,12 @@ import {
|
|
|
9
9
|
resolveDocsHref,
|
|
10
10
|
type DocsConfig,
|
|
11
11
|
type NavGroup,
|
|
12
|
+
type NavMenu,
|
|
12
13
|
type NavMenuItem,
|
|
13
14
|
type NavOpenApi,
|
|
14
15
|
type NavOpenApiPage,
|
|
15
16
|
type NavPage,
|
|
17
|
+
type NavTabItem,
|
|
16
18
|
} from "../validation";
|
|
17
19
|
import { prependBasePath, withBasePath } from "../base-path";
|
|
18
20
|
import {
|
|
@@ -26,6 +28,11 @@ import {
|
|
|
26
28
|
} from "../utils";
|
|
27
29
|
|
|
28
30
|
type MdxNavItem = string | NavPage | NavGroup | NavOpenApiPage;
|
|
31
|
+
type NavigationContentContainer = {
|
|
32
|
+
pages?: MdxNavItem[];
|
|
33
|
+
menu?: NavMenu;
|
|
34
|
+
openapi?: string | NavOpenApi;
|
|
35
|
+
};
|
|
29
36
|
|
|
30
37
|
type AuxiliaryPageRef = {
|
|
31
38
|
filePath: string;
|
|
@@ -36,9 +43,24 @@ type DocsConfigWithAuxiliaryRefs = DocsConfig & {
|
|
|
36
43
|
auxiliaryPageRefs?: AuxiliaryPageRef[];
|
|
37
44
|
};
|
|
38
45
|
|
|
46
|
+
type OpenApiEndpointLinkRef = {
|
|
47
|
+
source?: string;
|
|
48
|
+
method: string;
|
|
49
|
+
path: string;
|
|
50
|
+
endpoint: string;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
type OpenApiEndpointRouteTarget = OpenApiEndpointLinkRef & {
|
|
54
|
+
source: string;
|
|
55
|
+
targetKey: string;
|
|
56
|
+
};
|
|
57
|
+
|
|
39
58
|
type ResolvedRouteIndex = {
|
|
40
59
|
canonicalHrefByFilePath: Map<string, string>;
|
|
41
60
|
allHrefsByFilePath: Map<string, string[]>;
|
|
61
|
+
canonicalHrefByOpenApiEndpointKey: Map<string, string>;
|
|
62
|
+
allHrefsByOpenApiEndpointKey: Map<string, string[]>;
|
|
63
|
+
openApiEndpointTargetsByEndpoint: Map<string, OpenApiEndpointRouteTarget[]>;
|
|
42
64
|
validRoutePaths: Set<string>;
|
|
43
65
|
allDocsFilePaths: Set<string>;
|
|
44
66
|
routeSlugs: ReturnType<typeof createRouteSlugRegistry>;
|
|
@@ -76,10 +98,24 @@ const HTTP_METHODS = [
|
|
|
76
98
|
"options",
|
|
77
99
|
"trace",
|
|
78
100
|
];
|
|
101
|
+
const HTTP_METHOD_SET = new Set<string>(HTTP_METHODS);
|
|
79
102
|
|
|
80
103
|
let cachedRouteIndex: ResolvedRouteIndex | null = null;
|
|
81
104
|
let cachedConfig: DocsConfig | null = null;
|
|
82
105
|
|
|
106
|
+
function isUrl(value: string): boolean {
|
|
107
|
+
try {
|
|
108
|
+
const url = new URL(value);
|
|
109
|
+
return url.protocol === "http:" || url.protocol === "https:";
|
|
110
|
+
} catch {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function containsPathTraversal(value: string): boolean {
|
|
116
|
+
return value.replace(/\\/g, "/").split("/").includes("..");
|
|
117
|
+
}
|
|
118
|
+
|
|
83
119
|
function normalizeDocsFilePath(value: string): string {
|
|
84
120
|
let normalized = value.replace(/\\/g, "/").trim();
|
|
85
121
|
if (!normalized) return "";
|
|
@@ -94,6 +130,102 @@ function normalizeDocsFilePath(value: string): string {
|
|
|
94
130
|
return posixNormalized === "." ? "" : posixNormalized;
|
|
95
131
|
}
|
|
96
132
|
|
|
133
|
+
function normalizeOpenApiLinkSource(source: string): string {
|
|
134
|
+
const trimmedSource = source.trim();
|
|
135
|
+
if (isUrl(trimmedSource)) {
|
|
136
|
+
return trimmedSource;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
let normalizedSource = trimmedSource.replace(/\\/g, "/").trim();
|
|
140
|
+
normalizedSource = normalizedSource.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
141
|
+
normalizedSource = normalizedSource.replace(/^src\/content\/docs\//, "");
|
|
142
|
+
|
|
143
|
+
const posixNormalized = path.posix
|
|
144
|
+
.normalize(`/${normalizedSource}`)
|
|
145
|
+
.replace(/^\/+/, "");
|
|
146
|
+
|
|
147
|
+
return posixNormalized === "." ? "" : posixNormalized;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function formatOpenApiEndpoint(
|
|
151
|
+
method: string,
|
|
152
|
+
endpointPath: string,
|
|
153
|
+
): string {
|
|
154
|
+
return `${method.toUpperCase()} ${endpointPath.toLowerCase()}`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function formatOpenApiEndpointTargetKey(
|
|
158
|
+
source: string,
|
|
159
|
+
method: string,
|
|
160
|
+
endpointPath: string,
|
|
161
|
+
): string {
|
|
162
|
+
return `${normalizeOpenApiLinkSource(source)}\0${formatOpenApiEndpoint(
|
|
163
|
+
method,
|
|
164
|
+
endpointPath,
|
|
165
|
+
)}`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function parseOpenApiEndpointLinkEndpoint(
|
|
169
|
+
endpointStr: string,
|
|
170
|
+
): Pick<OpenApiEndpointLinkRef, "method" | "path" | "endpoint"> | null {
|
|
171
|
+
const trimmed = endpointStr.trim();
|
|
172
|
+
const parts = trimmed.split(/\s+/);
|
|
173
|
+
|
|
174
|
+
if (parts.length !== 2) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const method = parts[0].toUpperCase();
|
|
179
|
+
const endpointPath = parts[1];
|
|
180
|
+
if (
|
|
181
|
+
!HTTP_METHOD_SET.has(method.toLowerCase()) ||
|
|
182
|
+
!endpointPath.startsWith("/")
|
|
183
|
+
) {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const normalizedPath = endpointPath.toLowerCase();
|
|
188
|
+
return {
|
|
189
|
+
method,
|
|
190
|
+
path: normalizedPath,
|
|
191
|
+
endpoint: formatOpenApiEndpoint(method, normalizedPath),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function parseOpenApiEndpointLinkHref(
|
|
196
|
+
href: string,
|
|
197
|
+
): OpenApiEndpointLinkRef | null {
|
|
198
|
+
const trimmedHref = href.trim();
|
|
199
|
+
if (!trimmedHref) return null;
|
|
200
|
+
|
|
201
|
+
const hashIndex = trimmedHref.indexOf("#");
|
|
202
|
+
if (hashIndex > 0) {
|
|
203
|
+
const rawSource = trimmedHref.slice(0, hashIndex).trim();
|
|
204
|
+
if (!isUrl(rawSource) && containsPathTraversal(rawSource)) {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const source = normalizeOpenApiLinkSource(rawSource);
|
|
209
|
+
if (!source) {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const parsedEndpoint = parseOpenApiEndpointLinkEndpoint(
|
|
214
|
+
trimmedHref.slice(hashIndex + 1),
|
|
215
|
+
);
|
|
216
|
+
if (!parsedEndpoint) {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
source,
|
|
222
|
+
...parsedEndpoint,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return parseOpenApiEndpointLinkEndpoint(trimmedHref);
|
|
227
|
+
}
|
|
228
|
+
|
|
97
229
|
function normalizeRoutePath(value: string): string {
|
|
98
230
|
const trimmed = value.trim();
|
|
99
231
|
if (!trimmed || trimmed === "/") {
|
|
@@ -208,6 +340,38 @@ function addMdxRoute(args: {
|
|
|
208
340
|
args.index.allHrefsByFilePath.set(normalizedFilePath, aliases);
|
|
209
341
|
}
|
|
210
342
|
|
|
343
|
+
function createOpenApiEndpointRouteTarget(args: {
|
|
344
|
+
source: string;
|
|
345
|
+
method: string;
|
|
346
|
+
endpointPath: string;
|
|
347
|
+
}): OpenApiEndpointRouteTarget {
|
|
348
|
+
const source = normalizeOpenApiLinkSource(args.source);
|
|
349
|
+
const endpoint = formatOpenApiEndpoint(args.method, args.endpointPath);
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
source,
|
|
353
|
+
method: args.method.toUpperCase(),
|
|
354
|
+
path: args.endpointPath.toLowerCase(),
|
|
355
|
+
endpoint,
|
|
356
|
+
targetKey: formatOpenApiEndpointTargetKey(
|
|
357
|
+
source,
|
|
358
|
+
args.method,
|
|
359
|
+
args.endpointPath,
|
|
360
|
+
),
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function addOpenApiEndpointTargetToMap(
|
|
365
|
+
map: Map<string, OpenApiEndpointRouteTarget[]>,
|
|
366
|
+
target: OpenApiEndpointRouteTarget,
|
|
367
|
+
): void {
|
|
368
|
+
const targets = map.get(target.endpoint) ?? [];
|
|
369
|
+
if (!targets.some((candidate) => candidate.targetKey === target.targetKey)) {
|
|
370
|
+
targets.push(target);
|
|
371
|
+
}
|
|
372
|
+
map.set(target.endpoint, targets);
|
|
373
|
+
}
|
|
374
|
+
|
|
211
375
|
function addOpenApiEndpointRoute(args: {
|
|
212
376
|
index: ResolvedRouteIndex;
|
|
213
377
|
parentSlug: string;
|
|
@@ -232,10 +396,30 @@ function addOpenApiEndpointRoute(args: {
|
|
|
232
396
|
}),
|
|
233
397
|
identity: `openapi:${args.source}:${args.method.toUpperCase()} ${args.endpointPath.toLowerCase()}`,
|
|
234
398
|
}).slug;
|
|
235
|
-
addValidRoutePath(
|
|
399
|
+
const href = addValidRoutePath(
|
|
236
400
|
args.index,
|
|
237
401
|
prependBasePath(`/${registeredSlug}`),
|
|
238
402
|
);
|
|
403
|
+
const target = createOpenApiEndpointRouteTarget({
|
|
404
|
+
source: args.source,
|
|
405
|
+
method: args.method,
|
|
406
|
+
endpointPath: args.endpointPath,
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
if (!args.index.canonicalHrefByOpenApiEndpointKey.has(target.targetKey)) {
|
|
410
|
+
args.index.canonicalHrefByOpenApiEndpointKey.set(target.targetKey, href);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const aliases =
|
|
414
|
+
args.index.allHrefsByOpenApiEndpointKey.get(target.targetKey) ?? [];
|
|
415
|
+
if (!aliases.includes(href)) {
|
|
416
|
+
aliases.push(href);
|
|
417
|
+
}
|
|
418
|
+
args.index.allHrefsByOpenApiEndpointKey.set(target.targetKey, aliases);
|
|
419
|
+
addOpenApiEndpointTargetToMap(
|
|
420
|
+
args.index.openApiEndpointTargetsByEndpoint,
|
|
421
|
+
target,
|
|
422
|
+
);
|
|
239
423
|
}
|
|
240
424
|
|
|
241
425
|
function addAuxiliaryPageRef(
|
|
@@ -414,27 +598,69 @@ async function processMenuItems(args: {
|
|
|
414
598
|
index: ResolvedRouteIndex;
|
|
415
599
|
homePath: string | null;
|
|
416
600
|
items: NavMenuItem[];
|
|
601
|
+
parentSlug?: string;
|
|
417
602
|
}): Promise<void> {
|
|
418
603
|
for (const menuItem of args.items) {
|
|
419
604
|
const menuSlug = slugify(menuItem.label);
|
|
420
|
-
const parentSlug =
|
|
605
|
+
const parentSlug = args.parentSlug
|
|
606
|
+
? `${args.parentSlug}/${menuSlug}`
|
|
607
|
+
: menuSlug;
|
|
608
|
+
|
|
609
|
+
await processNavigationContent({
|
|
610
|
+
index: args.index,
|
|
611
|
+
parentSlug,
|
|
612
|
+
homePath: args.homePath,
|
|
613
|
+
item: menuItem,
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
}
|
|
421
617
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
618
|
+
async function processTabItems(args: {
|
|
619
|
+
index: ResolvedRouteIndex;
|
|
620
|
+
homePath: string | null;
|
|
621
|
+
items: NavTabItem[];
|
|
622
|
+
}): Promise<void> {
|
|
623
|
+
for (const tabItem of args.items) {
|
|
624
|
+
const tabSlug = slugify(tabItem.slug || tabItem.label);
|
|
625
|
+
await processNavigationContent({
|
|
626
|
+
index: args.index,
|
|
627
|
+
parentSlug: tabSlug,
|
|
628
|
+
homePath: args.homePath,
|
|
629
|
+
item: tabItem,
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
}
|
|
430
633
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
634
|
+
async function processNavigationContent(args: {
|
|
635
|
+
index: ResolvedRouteIndex;
|
|
636
|
+
parentSlug: string;
|
|
637
|
+
homePath: string | null;
|
|
638
|
+
item: NavigationContentContainer;
|
|
639
|
+
}): Promise<void> {
|
|
640
|
+
if (Array.isArray(args.item.pages)) {
|
|
641
|
+
await processPages({
|
|
642
|
+
index: args.index,
|
|
643
|
+
parentSlug: args.parentSlug,
|
|
644
|
+
homePath: args.homePath,
|
|
645
|
+
items: args.item.pages as MdxNavItem[],
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
if (args.item.menu?.items) {
|
|
650
|
+
await processMenuItems({
|
|
651
|
+
index: args.index,
|
|
652
|
+
parentSlug: args.parentSlug,
|
|
653
|
+
homePath: args.homePath,
|
|
654
|
+
items: args.item.menu.items,
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
if (args.item.openapi) {
|
|
659
|
+
await processOpenApiFile({
|
|
660
|
+
index: args.index,
|
|
661
|
+
parentSlug: args.parentSlug,
|
|
662
|
+
openApiPathOrConfig: args.item.openapi,
|
|
663
|
+
});
|
|
438
664
|
}
|
|
439
665
|
}
|
|
440
666
|
|
|
@@ -444,6 +670,12 @@ async function buildRouteIndex(
|
|
|
444
670
|
const index: ResolvedRouteIndex = {
|
|
445
671
|
canonicalHrefByFilePath: new Map<string, string>(),
|
|
446
672
|
allHrefsByFilePath: new Map<string, string[]>(),
|
|
673
|
+
canonicalHrefByOpenApiEndpointKey: new Map<string, string>(),
|
|
674
|
+
allHrefsByOpenApiEndpointKey: new Map<string, string[]>(),
|
|
675
|
+
openApiEndpointTargetsByEndpoint: new Map<
|
|
676
|
+
string,
|
|
677
|
+
OpenApiEndpointRouteTarget[]
|
|
678
|
+
>(),
|
|
447
679
|
validRoutePaths: new Set<string>(),
|
|
448
680
|
allDocsFilePaths: new Set<string>(),
|
|
449
681
|
routeSlugs: createRouteSlugRegistry(),
|
|
@@ -473,6 +705,14 @@ async function buildRouteIndex(
|
|
|
473
705
|
});
|
|
474
706
|
}
|
|
475
707
|
|
|
708
|
+
if (config.navigation.tabs?.items) {
|
|
709
|
+
await processTabItems({
|
|
710
|
+
index,
|
|
711
|
+
homePath,
|
|
712
|
+
items: config.navigation.tabs.items,
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
|
|
476
716
|
const rootOpenApi = (config.navigation as any).openapi;
|
|
477
717
|
if (
|
|
478
718
|
typeof rootOpenApi === "string" ||
|
|
@@ -527,12 +767,89 @@ function buildInternalLinkRewriteError(args: {
|
|
|
527
767
|
);
|
|
528
768
|
}
|
|
529
769
|
|
|
770
|
+
function formatOpenApiEndpointSourceList(
|
|
771
|
+
targets: OpenApiEndpointRouteTarget[],
|
|
772
|
+
): string {
|
|
773
|
+
const sources = [...new Set(targets.map((target) => target.source))];
|
|
774
|
+
return sources.map((source) => `"${source}"`).join(", ");
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
function resolveOpenApiEndpointLinkToCanonicalHref(args: {
|
|
778
|
+
rawHref: string;
|
|
779
|
+
endpointLink: OpenApiEndpointLinkRef;
|
|
780
|
+
currentDocFilePath: string | null;
|
|
781
|
+
routeIndex: ResolvedRouteIndex;
|
|
782
|
+
warn: (message: string) => void;
|
|
783
|
+
}): string {
|
|
784
|
+
const target =
|
|
785
|
+
args.endpointLink.source !== undefined
|
|
786
|
+
? {
|
|
787
|
+
source: args.endpointLink.source,
|
|
788
|
+
targetKey: formatOpenApiEndpointTargetKey(
|
|
789
|
+
args.endpointLink.source,
|
|
790
|
+
args.endpointLink.method,
|
|
791
|
+
args.endpointLink.path,
|
|
792
|
+
),
|
|
793
|
+
}
|
|
794
|
+
: (() => {
|
|
795
|
+
const targets =
|
|
796
|
+
args.routeIndex.openApiEndpointTargetsByEndpoint.get(
|
|
797
|
+
args.endpointLink.endpoint,
|
|
798
|
+
) ?? [];
|
|
799
|
+
if (targets.length === 1) {
|
|
800
|
+
return targets[0];
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
throw buildInternalLinkRewriteError({
|
|
804
|
+
rawHref: args.rawHref,
|
|
805
|
+
currentDocFilePath: args.currentDocFilePath,
|
|
806
|
+
detail:
|
|
807
|
+
targets.length > 1
|
|
808
|
+
? `The endpoint "${args.endpointLink.endpoint}" is ambiguous across OpenAPI sources (${formatOpenApiEndpointSourceList(
|
|
809
|
+
targets,
|
|
810
|
+
)}).`
|
|
811
|
+
: `No generated OpenAPI endpoint route matched "${args.endpointLink.endpoint}".`,
|
|
812
|
+
});
|
|
813
|
+
})();
|
|
814
|
+
|
|
815
|
+
const canonicalHref =
|
|
816
|
+
args.routeIndex.canonicalHrefByOpenApiEndpointKey.get(target.targetKey);
|
|
817
|
+
if (!canonicalHref) {
|
|
818
|
+
throw buildInternalLinkRewriteError({
|
|
819
|
+
rawHref: args.rawHref,
|
|
820
|
+
currentDocFilePath: args.currentDocFilePath,
|
|
821
|
+
detail: `The OpenAPI endpoint "${args.endpointLink.endpoint}" from "${target.source}" did not resolve to a canonical route.`,
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
const aliases =
|
|
826
|
+
args.routeIndex.allHrefsByOpenApiEndpointKey.get(target.targetKey) ?? [];
|
|
827
|
+
if (aliases.length > 1) {
|
|
828
|
+
args.warn(
|
|
829
|
+
`[INTERNAL_LINK_WARNING] "${args.rawHref}" matched OpenAPI endpoint "${args.endpointLink.endpoint}" in "${target.source}", which has multiple URLs (${aliases.join(", ")}). Rewriting to canonical URL "${canonicalHref}".`,
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
return withBasePath(canonicalHref);
|
|
834
|
+
}
|
|
835
|
+
|
|
530
836
|
function resolveLinkToCanonicalHref(args: {
|
|
531
837
|
rawHref: string;
|
|
532
838
|
currentDocFilePath: string | null;
|
|
533
839
|
routeIndex: ResolvedRouteIndex;
|
|
534
840
|
warn: (message: string) => void;
|
|
535
841
|
}): string | null {
|
|
842
|
+
const endpointLink = parseOpenApiEndpointLinkHref(args.rawHref);
|
|
843
|
+
if (endpointLink) {
|
|
844
|
+
return resolveOpenApiEndpointLinkToCanonicalHref({
|
|
845
|
+
rawHref: args.rawHref,
|
|
846
|
+
endpointLink,
|
|
847
|
+
currentDocFilePath: args.currentDocFilePath,
|
|
848
|
+
routeIndex: args.routeIndex,
|
|
849
|
+
warn: args.warn,
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
|
|
536
853
|
const resolvedHref = resolveDocsHref(args.rawHref);
|
|
537
854
|
if (resolvedHref.kind === "ignored" || resolvedHref.kind === "local-asset") {
|
|
538
855
|
return null;
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import YAML from "yaml";
|
|
4
|
+
|
|
5
|
+
export function isRecord(value) {
|
|
6
|
+
return typeof value === "object" && value !== null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function isHttpUrl(value) {
|
|
10
|
+
return /^https?:\/\//i.test(String(value).trim());
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function collectOpenApiSourcesFromNavigation(navigationNode, target) {
|
|
14
|
+
if (!isRecord(navigationNode)) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const openApiConfig = navigationNode.openapi;
|
|
19
|
+
if (typeof openApiConfig === "string") {
|
|
20
|
+
const trimmed = openApiConfig.trim();
|
|
21
|
+
if (trimmed) {
|
|
22
|
+
target.add(trimmed);
|
|
23
|
+
}
|
|
24
|
+
} else if (
|
|
25
|
+
isRecord(openApiConfig) &&
|
|
26
|
+
typeof openApiConfig.source === "string"
|
|
27
|
+
) {
|
|
28
|
+
const trimmed = openApiConfig.source.trim();
|
|
29
|
+
if (trimmed) {
|
|
30
|
+
target.add(trimmed);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const menu = navigationNode.menu;
|
|
35
|
+
if (isRecord(menu) && Array.isArray(menu.items)) {
|
|
36
|
+
for (const menuItem of menu.items) {
|
|
37
|
+
if (!isRecord(menuItem)) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
collectOpenApiSourcesFromNavigation(menuItem, target);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const tabs = navigationNode.tabs;
|
|
45
|
+
if (isRecord(tabs) && Array.isArray(tabs.items)) {
|
|
46
|
+
for (const tabItem of tabs.items) {
|
|
47
|
+
if (!isRecord(tabItem)) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
collectOpenApiSourcesFromNavigation(tabItem, target);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function resolveServerTemplateUrl(rawUrl, rawVariables) {
|
|
56
|
+
let unresolved = false;
|
|
57
|
+
const variables = isRecord(rawVariables) ? rawVariables : null;
|
|
58
|
+
|
|
59
|
+
const resolved = rawUrl.replace(/\{([^}]+)\}/g, (_match, tokenName) => {
|
|
60
|
+
if (!variables) {
|
|
61
|
+
unresolved = true;
|
|
62
|
+
return "";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const variableConfig = variables[tokenName];
|
|
66
|
+
if (
|
|
67
|
+
!isRecord(variableConfig) ||
|
|
68
|
+
typeof variableConfig.default !== "string"
|
|
69
|
+
) {
|
|
70
|
+
unresolved = true;
|
|
71
|
+
return "";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const defaultValue = variableConfig.default.trim();
|
|
75
|
+
if (!defaultValue) {
|
|
76
|
+
unresolved = true;
|
|
77
|
+
return "";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return defaultValue;
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
if (unresolved) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return resolved;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function normalizeAllowedOrigin(rawUrl) {
|
|
91
|
+
let parsed;
|
|
92
|
+
try {
|
|
93
|
+
parsed = new URL(rawUrl);
|
|
94
|
+
} catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (parsed.protocol !== "https:") {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!parsed.hostname || parsed.username || parsed.password) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return parsed.origin.toLowerCase();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function loadOpenApiSpec(source, { docsDir, fetchImpl }) {
|
|
110
|
+
let fileContent;
|
|
111
|
+
|
|
112
|
+
if (isHttpUrl(source)) {
|
|
113
|
+
const response = await fetchImpl(source);
|
|
114
|
+
if (!response.ok) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
`Failed to fetch OpenAPI spec (${response.status} ${response.statusText})`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
fileContent = await response.text();
|
|
120
|
+
} else {
|
|
121
|
+
const absolutePath = path.join(docsDir, source);
|
|
122
|
+
fileContent = await fs.readFile(absolutePath, "utf8");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const trimmed = fileContent.trim();
|
|
126
|
+
if (trimmed.startsWith("<!DOCTYPE") || trimmed.startsWith("<html")) {
|
|
127
|
+
throw new Error("OpenAPI source returned HTML instead of JSON or YAML");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
return JSON.parse(fileContent);
|
|
132
|
+
} catch {
|
|
133
|
+
return YAML.parse(fileContent);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export async function buildAllowedOrigins({
|
|
138
|
+
cwd = process.cwd(),
|
|
139
|
+
docsDir = path.join(cwd, "src/content/docs"),
|
|
140
|
+
docsConfigPath = path.join(docsDir, "docs.json"),
|
|
141
|
+
fetchImpl = fetch,
|
|
142
|
+
onSourceError = undefined,
|
|
143
|
+
} = {}) {
|
|
144
|
+
const docsConfigRaw = await fs.readFile(docsConfigPath, "utf8");
|
|
145
|
+
const docsConfig = JSON.parse(docsConfigRaw);
|
|
146
|
+
|
|
147
|
+
if (!isRecord(docsConfig) || !isRecord(docsConfig.navigation)) {
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const sources = new Set();
|
|
152
|
+
collectOpenApiSourcesFromNavigation(docsConfig.navigation, sources);
|
|
153
|
+
if (sources.size === 0) {
|
|
154
|
+
return [];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const allowedOrigins = new Set();
|
|
158
|
+
|
|
159
|
+
for (const source of sources) {
|
|
160
|
+
try {
|
|
161
|
+
const spec = await loadOpenApiSpec(source, { docsDir, fetchImpl });
|
|
162
|
+
const servers =
|
|
163
|
+
isRecord(spec) && Array.isArray(spec.servers) ? spec.servers : [];
|
|
164
|
+
|
|
165
|
+
for (const server of servers) {
|
|
166
|
+
if (!isRecord(server) || typeof server.url !== "string") {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const resolvedUrl = resolveServerTemplateUrl(
|
|
171
|
+
server.url,
|
|
172
|
+
server.variables,
|
|
173
|
+
);
|
|
174
|
+
if (!resolvedUrl) {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const normalizedOrigin = normalizeAllowedOrigin(resolvedUrl);
|
|
179
|
+
if (normalizedOrigin) {
|
|
180
|
+
allowedOrigins.add(normalizedOrigin);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
} catch (error) {
|
|
184
|
+
onSourceError?.(source, error);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return Array.from(allowedOrigins).sort();
|
|
189
|
+
}
|