radiant-docs 0.1.33 → 0.1.37
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 +25 -0
- package/template/package-lock.json +1027 -513
- package/template/package.json +3 -2
- package/template/scripts/generate-proxy-allowed-origins.mjs +217 -0
- package/template/scripts/generate-robots-txt.mjs +19 -0
- package/template/scripts/stamp-image-versions.mjs +63 -11
- package/template/src/components/Header.astro +4 -4
- package/template/src/components/LogoLink.astro +2 -1
- package/template/src/components/SidebarDropdown.astro +3 -3
- package/template/src/components/SidebarGroup.astro +3 -0
- package/template/src/components/SidebarMenu.astro +14 -1
- package/template/src/components/SidebarSubgroup.astro +35 -12
- package/template/src/components/chat/AskAiWidget.tsx +7 -3
- package/template/src/components/endpoint/PlaygroundButton.astro +2 -2
- package/template/src/components/endpoint/PlaygroundForm.astro +20 -16
- package/template/src/components/sidebar/SidebarEndpointLink.astro +18 -15
- package/template/src/components/sidebar/SidebarOpenApiPageLink.astro +56 -0
- package/template/src/components/ui/Icon.astro +2 -1
- package/template/src/components/user/Image.astro +4 -0
- package/template/src/layouts/Layout.astro +8 -3
- package/template/src/lib/pagefind.ts +2 -1
- package/template/src/lib/routes.ts +134 -58
- package/template/src/lib/static-asset-url.ts +62 -0
- package/template/src/lib/utils.ts +48 -0
- package/template/src/lib/validation.ts +115 -27
- package/template/scripts/rewrite-static-asset-host.mjs +0 -408
|
@@ -33,10 +33,12 @@ const {
|
|
|
33
33
|
bodyDefaultKind,
|
|
34
34
|
} = Astro.props;
|
|
35
35
|
const config = await getConfig();
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
const configuredProxyUrl =
|
|
37
|
+
typeof import.meta.env.PUBLIC_PROXY_URL === "string"
|
|
38
|
+
? import.meta.env.PUBLIC_PROXY_URL.trim()
|
|
39
|
+
: "";
|
|
40
|
+
const proxyUrl = configuredProxyUrl || "/_platform/proxy";
|
|
41
|
+
const proxyEnabled = config.playground?.proxy !== false && proxyUrl.length > 0;
|
|
40
42
|
const formattedBodyDescription = bodyDescription
|
|
41
43
|
? await renderMarkdown(bodyDescription)
|
|
42
44
|
: null;
|
|
@@ -538,20 +540,22 @@ const sectionVariantFieldNames = Object.fromEntries(
|
|
|
538
540
|
<button
|
|
539
541
|
@click="sendRequest($event)"
|
|
540
542
|
:disabled="loading"
|
|
541
|
-
class="m-px flex items-center gap-
|
|
543
|
+
class="m-px font-[350] relative flex h-8 items-center gap-2 rounded-lg [corner-shape:superellipse(1.2)] bg-linear-to-b from-neutral-900/85 to-neutral-900 px-3 text-[13px] text-white shadow-sm dark:bg-white dark:text-neutral-900 transition-all duration-200 whitespace-nowrap cursor-pointer disabled:opacity-70 disabled:cursor-not-allowed"
|
|
542
544
|
>
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
545
|
+
<span class="flex items-center gap-2">
|
|
546
|
+
<Icon
|
|
547
|
+
x-show="!loading"
|
|
548
|
+
class="size-3.5 -ml-px"
|
|
549
|
+
name="lucide:square-arrow-up-right"
|
|
550
|
+
/>
|
|
551
|
+
<Icon
|
|
552
|
+
x-show="loading"
|
|
553
|
+
x-cloak
|
|
554
|
+
class="size-3.5 animate-spin -ml-px"
|
|
555
|
+
name="lucide:loader"
|
|
556
|
+
/>
|
|
547
557
|
Send <span class="hidden xs:inline">Request</span>
|
|
548
|
-
<Icon class="size-4" name="lucide:square-arrow-up-right" />
|
|
549
558
|
</span>
|
|
550
|
-
<Icon
|
|
551
|
-
x-show="loading"
|
|
552
|
-
class="size-4 absolute left-1/2 -translate-x-1/2 top-1/2 -translate-y-1/2 animate-spin **:stroke-3"
|
|
553
|
-
name="lucide:loader"
|
|
554
|
-
/>
|
|
555
559
|
</button>
|
|
556
560
|
</PlaygroundBar>
|
|
557
561
|
|
|
@@ -578,7 +582,7 @@ const sectionVariantFieldNames = Object.fromEntries(
|
|
|
578
582
|
}
|
|
579
583
|
|
|
580
584
|
return (
|
|
581
|
-
<div class="border border-neutral-200 shadow-xs rounded-xl p-4">
|
|
585
|
+
<div class="border border-neutral-200 shadow-xs rounded-xl p-4 pb-0 [&_[role='region']]:border-b-0">
|
|
582
586
|
<Accordion title={headers[key]} defaultOpen titleSize="xl">
|
|
583
587
|
{key === "body" && formattedBodyDescription && (
|
|
584
588
|
<div
|
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
---
|
|
2
|
-
import
|
|
2
|
+
import Tag from "../ui/Tag.astro";
|
|
3
|
+
import { buildOpenApiEndpointHref } from "../../lib/utils";
|
|
3
4
|
import { methodColors } from "../../lib/utils";
|
|
4
5
|
|
|
5
6
|
interface Props {
|
|
6
7
|
method: string;
|
|
7
8
|
path: string;
|
|
8
9
|
summary?: string;
|
|
10
|
+
title?: string;
|
|
11
|
+
tag?: string;
|
|
9
12
|
parentSlug?: string;
|
|
10
13
|
}
|
|
11
14
|
|
|
12
|
-
const { method, path: pathStr, summary, parentSlug = "" } = Astro.props;
|
|
15
|
+
const { method, path: pathStr, summary, title, tag, parentSlug = "" } = Astro.props;
|
|
13
16
|
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const methodSlug = slugify(method);
|
|
21
|
-
const endpointSlug = pathSlug ? `${pathSlug}-${methodSlug}` : methodSlug;
|
|
22
|
-
const href = parentSlug ? `/${parentSlug}/${endpointSlug}` : `/${endpointSlug}`;
|
|
17
|
+
const normalizedMethod = method.toLowerCase();
|
|
18
|
+
const href = buildOpenApiEndpointHref({
|
|
19
|
+
path: pathStr,
|
|
20
|
+
method: normalizedMethod,
|
|
21
|
+
groupSlug: parentSlug,
|
|
22
|
+
});
|
|
23
23
|
|
|
24
24
|
// Use summary for title, fallback to method + path
|
|
25
|
-
const text = summary || pathStr
|
|
25
|
+
const text = title || summary || `${normalizedMethod.toUpperCase()} ${pathStr}`;
|
|
26
26
|
|
|
27
27
|
// Normalize paths for comparison (remove trailing slashes)
|
|
28
28
|
const currentPath = Astro.url.pathname.replace(/\/$/, "");
|
|
@@ -42,10 +42,13 @@ const isActive = currentPath === targetPath;
|
|
|
42
42
|
<span
|
|
43
43
|
class:list={[
|
|
44
44
|
"px-1 py-px mr-1.5 border rounded-md text-[10px] font-semibold uppercase",
|
|
45
|
-
methodColors[
|
|
45
|
+
methodColors[normalizedMethod] ?? methodColors.get,
|
|
46
46
|
]}
|
|
47
47
|
>
|
|
48
|
-
{
|
|
48
|
+
{normalizedMethod !== "delete" ? normalizedMethod : "del"}
|
|
49
49
|
</span>
|
|
50
|
-
|
|
50
|
+
<div class="flex items-center gap-2 min-w-0">
|
|
51
|
+
<span>{text}</span>
|
|
52
|
+
{tag && <Tag>{tag}</Tag>}
|
|
53
|
+
</div>
|
|
51
54
|
</a>
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { NavOpenApiPage } from "../../lib/validation";
|
|
3
|
+
import { loadOpenApiSpec } from "../../lib/validation";
|
|
4
|
+
import { parseOpenApiEndpoint } from "../../lib/utils";
|
|
5
|
+
import SidebarEndpointLink from "./SidebarEndpointLink.astro";
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
item: NavOpenApiPage;
|
|
9
|
+
parentSlug?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const { item, parentSlug = "" } = Astro.props;
|
|
13
|
+
|
|
14
|
+
const parsedEndpoint = parseOpenApiEndpoint(item.openapi.endpoint);
|
|
15
|
+
if (!parsedEndpoint) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
`Invalid OpenAPI endpoint format "${item.openapi.endpoint}". Expected "METHOD /path".`,
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const openApiDoc = await loadOpenApiSpec(item.openapi.source);
|
|
22
|
+
const openApiPaths = openApiDoc?.paths ?? {};
|
|
23
|
+
const endpointMethod = parsedEndpoint.method.toLowerCase();
|
|
24
|
+
|
|
25
|
+
let matchedPath: string | null = null;
|
|
26
|
+
let endpointSummary: string | undefined;
|
|
27
|
+
|
|
28
|
+
for (const [pathStr, pathItem] of Object.entries(openApiPaths)) {
|
|
29
|
+
if (!pathItem || typeof pathItem !== "object") continue;
|
|
30
|
+
if (pathStr.toLowerCase() !== parsedEndpoint.path) continue;
|
|
31
|
+
|
|
32
|
+
const operation = (pathItem as any)[endpointMethod];
|
|
33
|
+
if (!operation) continue;
|
|
34
|
+
|
|
35
|
+
matchedPath = pathStr;
|
|
36
|
+
if (typeof operation.summary === "string") {
|
|
37
|
+
endpointSummary = operation.summary;
|
|
38
|
+
}
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!matchedPath) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`OpenAPI endpoint "${item.openapi.endpoint}" was not found in source "${item.openapi.source}".`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
<SidebarEndpointLink
|
|
50
|
+
method={endpointMethod}
|
|
51
|
+
path={matchedPath}
|
|
52
|
+
summary={endpointSummary}
|
|
53
|
+
title={item.title}
|
|
54
|
+
tag={item.tag}
|
|
55
|
+
parentSlug={parentSlug}
|
|
56
|
+
/>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
import { Icon as AstroIcon } from "astro-icon/components";
|
|
3
|
+
import { resolveStaticAssetUrl } from "../../lib/static-asset-url";
|
|
3
4
|
|
|
4
5
|
interface Props {
|
|
5
6
|
name: string | undefined | null;
|
|
@@ -19,7 +20,7 @@ const isLocal = !isUrl && !isIconify;
|
|
|
19
20
|
// Files from src/content/docs are copied to public root by copyContentAssets plugin
|
|
20
21
|
let src = name;
|
|
21
22
|
if (isLocal) {
|
|
22
|
-
src = name.startsWith("/") ? name : `/${name}
|
|
23
|
+
src = resolveStaticAssetUrl(name.startsWith("/") ? name : `/${name}`);
|
|
23
24
|
}
|
|
24
25
|
---
|
|
25
26
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import type { HTMLAttributes } from "astro/types";
|
|
3
3
|
import { validateProps } from "../../lib/component-error";
|
|
4
4
|
import { renderMarkdown } from "../../lib/utils";
|
|
5
|
+
import { resolveStaticAssetUrl } from "../../lib/static-asset-url";
|
|
5
6
|
|
|
6
7
|
interface Props extends HTMLAttributes<"img"> {
|
|
7
8
|
src: string;
|
|
@@ -11,6 +12,9 @@ interface Props extends HTMLAttributes<"img"> {
|
|
|
11
12
|
const { title, zoom = true, ...attrs } = Astro.props as Props;
|
|
12
13
|
const zoomEnabled = zoom !== false;
|
|
13
14
|
const attrsRecord = attrs as Record<string, unknown>;
|
|
15
|
+
if (typeof attrsRecord.src === "string") {
|
|
16
|
+
attrsRecord.src = resolveStaticAssetUrl(attrsRecord.src);
|
|
17
|
+
}
|
|
14
18
|
const rawStyle = attrsRecord.style;
|
|
15
19
|
const rawWidth = attrsRecord.width;
|
|
16
20
|
const zoomAttrs: Record<string, unknown> = { ...attrsRecord };
|
|
@@ -12,6 +12,7 @@ import Header from "../components/Header.astro";
|
|
|
12
12
|
import Footer from "../components/Footer.astro";
|
|
13
13
|
import AskAiWidget from "../components/chat/AskAiWidget";
|
|
14
14
|
import { ClientRouter } from "astro:transitions";
|
|
15
|
+
import { resolveStaticAssetUrl } from "../lib/static-asset-url";
|
|
15
16
|
|
|
16
17
|
interface Props {
|
|
17
18
|
pageTitle?: string;
|
|
@@ -52,7 +53,7 @@ const canonicalUrl = new URL(
|
|
|
52
53
|
Astro.site ?? Astro.url,
|
|
53
54
|
).toString();
|
|
54
55
|
const ogImageUrl = new URL(
|
|
55
|
-
routePathToOgImagePath(Astro.url.pathname),
|
|
56
|
+
resolveStaticAssetUrl(routePathToOgImagePath(Astro.url.pathname)),
|
|
56
57
|
Astro.site ?? Astro.url,
|
|
57
58
|
);
|
|
58
59
|
const ogImageHref = ogImageUrl.toString();
|
|
@@ -144,7 +145,11 @@ if (isDev && hasAskAiDevConfig) {
|
|
|
144
145
|
/>
|
|
145
146
|
<meta charset="UTF-8" />
|
|
146
147
|
<meta name="viewport" content="width=device-width" />
|
|
147
|
-
<link
|
|
148
|
+
<link
|
|
149
|
+
rel="icon"
|
|
150
|
+
type="image/svg+xml"
|
|
151
|
+
href={resolveStaticAssetUrl("/favicon.svg")}
|
|
152
|
+
/>
|
|
148
153
|
<meta name="generator" content={Astro.generator} />
|
|
149
154
|
<link rel="canonical" href={canonicalUrl} />
|
|
150
155
|
<meta property="og:url" content={canonicalUrl} />
|
|
@@ -211,7 +216,7 @@ if (isDev && hasAskAiDevConfig) {
|
|
|
211
216
|
|
|
212
217
|
<!-- Main Content -->
|
|
213
218
|
<div
|
|
214
|
-
class="mx-1 mt-1 px-4 sm:px-6 lg:pl-[calc(288px+32px)] pt-16 lg:pr-8 bg-
|
|
219
|
+
class="mx-1 mt-1 px-4 sm:px-6 lg:pl-[calc(288px+32px)] pt-16 lg:pr-8 bg-background"
|
|
215
220
|
data-vaul-scale-chrome
|
|
216
221
|
>
|
|
217
222
|
<main
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// Pagefind TypeScript types and wrapper
|
|
2
|
+
import { resolveStaticAssetUrl } from "./static-asset-url";
|
|
2
3
|
|
|
3
4
|
export interface PagefindSearchResult {
|
|
4
5
|
id: string;
|
|
@@ -50,7 +51,7 @@ export interface PagefindInstance {
|
|
|
50
51
|
let pagefindInstance: PagefindInstance | null = null;
|
|
51
52
|
|
|
52
53
|
function getPagefindScriptUrl(): string {
|
|
53
|
-
return "/pagefind/pagefind.js";
|
|
54
|
+
return resolveStaticAssetUrl("/pagefind/pagefind.js");
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
export async function getPagefind(): Promise<PagefindInstance | null> {
|
|
@@ -2,17 +2,20 @@ import {
|
|
|
2
2
|
getConfig,
|
|
3
3
|
type NavGroup,
|
|
4
4
|
type NavPage,
|
|
5
|
+
type NavOpenApiPage,
|
|
5
6
|
type NavMenuItem,
|
|
6
7
|
type NavOpenApi,
|
|
7
8
|
loadOpenApiSpec,
|
|
8
9
|
} from "./validation";
|
|
9
|
-
import {
|
|
10
|
-
|
|
10
|
+
import {
|
|
11
|
+
buildOpenApiEndpointSlug,
|
|
12
|
+
deriveTitleFromEntryId,
|
|
13
|
+
parseOpenApiEndpoint,
|
|
14
|
+
slugify,
|
|
15
|
+
} from "./utils";
|
|
11
16
|
import { getCollection } from "astro:content";
|
|
12
17
|
|
|
13
|
-
|
|
14
|
-
// Since the input array is already validated, we can use the types from validation.ts
|
|
15
|
-
type NavPageItem = string | NavPage;
|
|
18
|
+
type MdxNavPageItem = string | NavPage;
|
|
16
19
|
|
|
17
20
|
// Base route interface
|
|
18
21
|
export interface BaseRoute {
|
|
@@ -55,7 +58,7 @@ export function resolveMdxPageTitle(args: {
|
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
function processPageItem(
|
|
58
|
-
item:
|
|
61
|
+
item: MdxNavPageItem,
|
|
59
62
|
parentSlug: string = "",
|
|
60
63
|
docs: any[],
|
|
61
64
|
): MdxRoute {
|
|
@@ -92,53 +95,106 @@ function processPageItem(
|
|
|
92
95
|
};
|
|
93
96
|
}
|
|
94
97
|
|
|
95
|
-
|
|
98
|
+
type OpenApiOperationLookup = {
|
|
99
|
+
method: string;
|
|
100
|
+
path: string;
|
|
101
|
+
operation: any;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
function findOpenApiOperation(args: {
|
|
105
|
+
openApiDoc: any;
|
|
106
|
+
endpointMethod: string;
|
|
107
|
+
endpointPath: string;
|
|
108
|
+
}): OpenApiOperationLookup | null {
|
|
109
|
+
const paths = args.openApiDoc?.paths ?? {};
|
|
110
|
+
const targetMethod = args.endpointMethod.toLowerCase();
|
|
111
|
+
const targetPath = args.endpointPath.toLowerCase();
|
|
112
|
+
|
|
113
|
+
for (const [pathStr, pathItem] of Object.entries(paths)) {
|
|
114
|
+
if (!pathItem || typeof pathItem !== "object") continue;
|
|
115
|
+
if (pathStr.toLowerCase() !== targetPath) continue;
|
|
116
|
+
|
|
117
|
+
const operation = (pathItem as any)[targetMethod];
|
|
118
|
+
if (!operation) continue;
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
method: targetMethod,
|
|
122
|
+
path: pathStr,
|
|
123
|
+
operation,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function processOpenApiPageItem(
|
|
131
|
+
item: NavOpenApiPage,
|
|
132
|
+
parentSlug: string = "",
|
|
133
|
+
): Promise<OpenApiRoute> {
|
|
134
|
+
const parsedEndpoint = parseOpenApiEndpoint(item.openapi.endpoint);
|
|
135
|
+
if (!parsedEndpoint) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
`Invalid OpenAPI endpoint format "${item.openapi.endpoint}". Expected "METHOD /path".`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const openApiDoc = await loadOpenApiSpec(item.openapi.source);
|
|
142
|
+
const matchedOperation = findOpenApiOperation({
|
|
143
|
+
openApiDoc,
|
|
144
|
+
endpointMethod: parsedEndpoint.method,
|
|
145
|
+
endpointPath: parsedEndpoint.path,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
if (!matchedOperation) {
|
|
149
|
+
throw new Error(
|
|
150
|
+
`OpenAPI endpoint "${item.openapi.endpoint}" was not found in source "${item.openapi.source}".`,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const endpointSlug = buildOpenApiEndpointSlug(
|
|
155
|
+
matchedOperation.path,
|
|
156
|
+
matchedOperation.method,
|
|
157
|
+
);
|
|
158
|
+
const fullSlug = parentSlug ? `${parentSlug}/${endpointSlug}` : endpointSlug;
|
|
159
|
+
const title =
|
|
160
|
+
normalizeTitle(item.title) ??
|
|
161
|
+
normalizeTitle(matchedOperation.operation.summary) ??
|
|
162
|
+
`${matchedOperation.method.toUpperCase()} ${matchedOperation.path}`;
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
type: "openapi",
|
|
166
|
+
slug: fullSlug,
|
|
167
|
+
filePath: item.openapi.source,
|
|
168
|
+
title,
|
|
169
|
+
openApiPath: matchedOperation.path,
|
|
170
|
+
openApiMethod: matchedOperation.method,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function processGroup(
|
|
96
175
|
group: NavGroup,
|
|
97
176
|
parentSlug: string = "",
|
|
98
177
|
docs: any[],
|
|
99
|
-
): Route[] {
|
|
178
|
+
): Promise<Route[]> {
|
|
100
179
|
let routes: Route[] = [];
|
|
101
180
|
const groupSlug = slugify(group.group);
|
|
102
181
|
const currentPrefix = parentSlug ? `${parentSlug}/${groupSlug}` : groupSlug;
|
|
103
182
|
|
|
104
|
-
group.pages
|
|
183
|
+
for (const item of group.pages) {
|
|
105
184
|
// Check if it's a simple page or a page object
|
|
106
185
|
if (typeof item === "string" || "page" in item) {
|
|
107
|
-
routes.push(processPageItem(item
|
|
186
|
+
routes.push(processPageItem(item, currentPrefix, docs));
|
|
108
187
|
} else if ("group" in item) {
|
|
109
188
|
// Nested group
|
|
110
189
|
routes = routes.concat(
|
|
111
|
-
processGroup(item as NavGroup, currentPrefix, docs),
|
|
190
|
+
await processGroup(item as NavGroup, currentPrefix, docs),
|
|
112
191
|
);
|
|
192
|
+
} else if ("openapi" in item) {
|
|
193
|
+
routes.push(await processOpenApiPageItem(item, currentPrefix));
|
|
113
194
|
}
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
return routes;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Helper function to parse endpoint string (same as in validation.ts)
|
|
120
|
-
function parseEndpointString(
|
|
121
|
-
endpointStr: string,
|
|
122
|
-
): { method: string; path: string } | null {
|
|
123
|
-
const trimmed = endpointStr.trim();
|
|
124
|
-
const parts = trimmed.split(/\s+/);
|
|
125
|
-
|
|
126
|
-
if (parts.length !== 2) {
|
|
127
|
-
return null;
|
|
128
195
|
}
|
|
129
196
|
|
|
130
|
-
|
|
131
|
-
let path = parts[1];
|
|
132
|
-
|
|
133
|
-
// Ensure path starts with /
|
|
134
|
-
if (!path.startsWith("/")) {
|
|
135
|
-
path = "/" + path;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Normalize path to lowercase for comparison
|
|
139
|
-
const normalizedPath = path.toLowerCase();
|
|
140
|
-
|
|
141
|
-
return { method, path: normalizedPath };
|
|
197
|
+
return routes;
|
|
142
198
|
}
|
|
143
199
|
|
|
144
200
|
// Helper function to check if an endpoint matches include/exclude filters
|
|
@@ -156,7 +212,7 @@ function shouldIncludeEndpoint(
|
|
|
156
212
|
// If include is specified, only include matching endpoints
|
|
157
213
|
if (include) {
|
|
158
214
|
return include.some((entry) => {
|
|
159
|
-
const parsed =
|
|
215
|
+
const parsed = parseOpenApiEndpoint(entry);
|
|
160
216
|
if (!parsed) return false;
|
|
161
217
|
return `${parsed.method} ${parsed.path}` === endpointKey;
|
|
162
218
|
});
|
|
@@ -165,7 +221,7 @@ function shouldIncludeEndpoint(
|
|
|
165
221
|
// If exclude is specified, exclude matching endpoints
|
|
166
222
|
if (exclude) {
|
|
167
223
|
return !exclude.some((entry) => {
|
|
168
|
-
const parsed =
|
|
224
|
+
const parsed = parseOpenApiEndpoint(entry);
|
|
169
225
|
if (!parsed) return false;
|
|
170
226
|
return `${parsed.method} ${parsed.path}` === endpointKey;
|
|
171
227
|
});
|
|
@@ -180,8 +236,6 @@ async function processOpenApiFile(
|
|
|
180
236
|
parentSlug: string = "",
|
|
181
237
|
): Promise<OpenApiRoute[]> {
|
|
182
238
|
const routes: OpenApiRoute[] = [];
|
|
183
|
-
const CWD = process.cwd();
|
|
184
|
-
const DOCS_DIR = path.join(CWD, "src/content/docs");
|
|
185
239
|
|
|
186
240
|
// Extract file path and filter options
|
|
187
241
|
let openApiPath: string;
|
|
@@ -232,15 +286,7 @@ async function processOpenApiFile(
|
|
|
232
286
|
continue;
|
|
233
287
|
}
|
|
234
288
|
|
|
235
|
-
|
|
236
|
-
// e.g., "/burgers" + "get" -> "burgers-get"
|
|
237
|
-
const pathSlug = pathStr
|
|
238
|
-
.replace(/^\//, "") // Remove leading slash
|
|
239
|
-
.replace(/\//g, "-") // Replace slashes with hyphens
|
|
240
|
-
.replace(/\{[^}]+\}/g, "param"); // Replace path params like {id} with "param"
|
|
241
|
-
|
|
242
|
-
const methodSlug = slugify(method);
|
|
243
|
-
const endpointSlug = pathSlug ? `${pathSlug}-${methodSlug}` : methodSlug;
|
|
289
|
+
const endpointSlug = buildOpenApiEndpointSlug(pathStr, method);
|
|
244
290
|
const fullSlug = parentSlug
|
|
245
291
|
? `${parentSlug}/${endpointSlug}`
|
|
246
292
|
: endpointSlug;
|
|
@@ -277,15 +323,17 @@ async function processMenuItem(
|
|
|
277
323
|
|
|
278
324
|
// Process pages if they exist (pages array can contain pages and groups)
|
|
279
325
|
if (submenu.pages) {
|
|
280
|
-
submenu.pages
|
|
326
|
+
for (const item of submenu.pages) {
|
|
281
327
|
if (typeof item === "string" || "page" in item) {
|
|
282
|
-
routes.push(processPageItem(item
|
|
328
|
+
routes.push(processPageItem(item, currentPrefix, docs));
|
|
283
329
|
} else if ("group" in item) {
|
|
284
330
|
routes = routes.concat(
|
|
285
|
-
processGroup(item as NavGroup, currentPrefix, docs),
|
|
331
|
+
await processGroup(item as NavGroup, currentPrefix, docs),
|
|
286
332
|
);
|
|
333
|
+
} else if ("openapi" in item) {
|
|
334
|
+
routes.push(await processOpenApiPageItem(item, currentPrefix));
|
|
287
335
|
}
|
|
288
|
-
}
|
|
336
|
+
}
|
|
289
337
|
}
|
|
290
338
|
|
|
291
339
|
// Process OpenAPI file if it exists
|
|
@@ -300,6 +348,31 @@ async function processMenuItem(
|
|
|
300
348
|
return routes;
|
|
301
349
|
}
|
|
302
350
|
|
|
351
|
+
function assertUniqueRouteSlugs(routes: Route[]): void {
|
|
352
|
+
const seenBySlug = new Map<string, Route>();
|
|
353
|
+
|
|
354
|
+
for (const route of routes) {
|
|
355
|
+
const existing = seenBySlug.get(route.slug);
|
|
356
|
+
if (!existing) {
|
|
357
|
+
seenBySlug.set(route.slug, route);
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const existingLabel =
|
|
362
|
+
existing.type === "mdx"
|
|
363
|
+
? `mdx:${existing.filePath}`
|
|
364
|
+
: `openapi:${existing.filePath}:${existing.openApiMethod.toUpperCase()} ${existing.openApiPath}`;
|
|
365
|
+
const candidateLabel =
|
|
366
|
+
route.type === "mdx"
|
|
367
|
+
? `mdx:${route.filePath}`
|
|
368
|
+
: `openapi:${route.filePath}:${route.openApiMethod.toUpperCase()} ${route.openApiPath}`;
|
|
369
|
+
|
|
370
|
+
throw new Error(
|
|
371
|
+
`Duplicate route slug "${route.slug}" generated by "${existingLabel}" and "${candidateLabel}".`,
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
303
376
|
export async function getAllRoutes(): Promise<Route[]> {
|
|
304
377
|
// 1. Get the validated config directly
|
|
305
378
|
const config = await getConfig();
|
|
@@ -314,13 +387,15 @@ export async function getAllRoutes(): Promise<Route[]> {
|
|
|
314
387
|
// We use the XOR guarantee from validateNavigation (only one key exists)
|
|
315
388
|
if (navigation.pages) {
|
|
316
389
|
// Case 1: Pages array at the top level (can contain pages and groups)
|
|
317
|
-
navigation.pages
|
|
390
|
+
for (const item of navigation.pages) {
|
|
318
391
|
if (typeof item === "string" || "page" in item) {
|
|
319
|
-
allRoutes.push(processPageItem(item
|
|
392
|
+
allRoutes.push(processPageItem(item, "", docs));
|
|
320
393
|
} else if ("group" in item) {
|
|
321
|
-
allRoutes = allRoutes.concat(processGroup(item as NavGroup, "", docs));
|
|
394
|
+
allRoutes = allRoutes.concat(await processGroup(item as NavGroup, "", docs));
|
|
395
|
+
} else if ("openapi" in item) {
|
|
396
|
+
allRoutes.push(await processOpenApiPageItem(item, ""));
|
|
322
397
|
}
|
|
323
|
-
}
|
|
398
|
+
}
|
|
324
399
|
} else if (navigation.menu) {
|
|
325
400
|
// Case 3: Menu object at the top level
|
|
326
401
|
// Need to await all menu items since processMenuItem is now async
|
|
@@ -330,5 +405,6 @@ export async function getAllRoutes(): Promise<Route[]> {
|
|
|
330
405
|
}
|
|
331
406
|
}
|
|
332
407
|
|
|
408
|
+
assertUniqueRouteSlugs(allRoutes);
|
|
333
409
|
return allRoutes;
|
|
334
410
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
type AssetsPrefixValue = string | Record<string, string> | undefined;
|
|
2
|
+
|
|
3
|
+
function normalizePrefix(value: string): string {
|
|
4
|
+
const normalized = value.trim().replace(/\/+$/, "");
|
|
5
|
+
if (!normalized) return normalized;
|
|
6
|
+
if (hasProtocol(normalized) || normalized.startsWith("//")) {
|
|
7
|
+
return normalized;
|
|
8
|
+
}
|
|
9
|
+
if (normalized.startsWith("/")) {
|
|
10
|
+
return normalized;
|
|
11
|
+
}
|
|
12
|
+
return `https://${normalized}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function resolveAssetsPrefix(value: AssetsPrefixValue): string | null {
|
|
16
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
17
|
+
return normalizePrefix(value);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!value || typeof value !== "object") {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const fallback = value.fallback;
|
|
25
|
+
if (typeof fallback === "string" && fallback.trim().length > 0) {
|
|
26
|
+
return normalizePrefix(fallback);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
for (const candidate of Object.values(value)) {
|
|
30
|
+
if (typeof candidate === "string" && candidate.trim().length > 0) {
|
|
31
|
+
return normalizePrefix(candidate);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function hasProtocol(url: string): boolean {
|
|
39
|
+
return /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(url);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function resolveStaticAssetUrl(rawPath: string): string {
|
|
43
|
+
const value = rawPath.trim();
|
|
44
|
+
if (!value) return value;
|
|
45
|
+
|
|
46
|
+
if (hasProtocol(value) || value.startsWith("//")) {
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const parsed = new URL(value, "https://radiant.invalid");
|
|
51
|
+
const normalizedPathname = parsed.pathname.startsWith("/")
|
|
52
|
+
? parsed.pathname
|
|
53
|
+
: `/${parsed.pathname}`;
|
|
54
|
+
|
|
55
|
+
const prefix = resolveAssetsPrefix(import.meta.env.ASSETS_PREFIX);
|
|
56
|
+
if (!prefix) {
|
|
57
|
+
return `${normalizedPathname}${parsed.search}${parsed.hash}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const normalizedPrefixPath = `${prefix}/${normalizedPathname.replace(/^\/+/, "")}`;
|
|
61
|
+
return `${normalizedPrefixPath}${parsed.search}${parsed.hash}`;
|
|
62
|
+
}
|
|
@@ -79,3 +79,51 @@ export function buildMdxPageHref(args: {
|
|
|
79
79
|
? `/${normalizedGroupSlug}/${pageSlug}`
|
|
80
80
|
: `/${pageSlug}`;
|
|
81
81
|
}
|
|
82
|
+
|
|
83
|
+
export function parseOpenApiEndpoint(
|
|
84
|
+
endpointStr: string,
|
|
85
|
+
): { method: string; path: string } | null {
|
|
86
|
+
const trimmed = endpointStr.trim();
|
|
87
|
+
const parts = trimmed.split(/\s+/);
|
|
88
|
+
|
|
89
|
+
if (parts.length !== 2) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const method = parts[0].toUpperCase();
|
|
94
|
+
let openApiPath = parts[1];
|
|
95
|
+
|
|
96
|
+
if (!openApiPath.startsWith("/")) {
|
|
97
|
+
openApiPath = `/${openApiPath}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
method,
|
|
102
|
+
path: openApiPath.toLowerCase(),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function buildOpenApiEndpointSlug(pathStr: string, method: string): string {
|
|
107
|
+
const normalizedPath = pathStr.startsWith("/") ? pathStr : `/${pathStr}`;
|
|
108
|
+
const pathSlug = normalizedPath
|
|
109
|
+
.replace(/^\//, "")
|
|
110
|
+
.replace(/\//g, "-")
|
|
111
|
+
.replace(/\{[^}]+\}/g, "param");
|
|
112
|
+
const methodSlug = slugify(method);
|
|
113
|
+
return pathSlug ? `${pathSlug}-${methodSlug}` : methodSlug;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function buildOpenApiEndpointHref(args: {
|
|
117
|
+
path: string;
|
|
118
|
+
method: string;
|
|
119
|
+
groupSlug?: string;
|
|
120
|
+
}): string {
|
|
121
|
+
const endpointSlug = buildOpenApiEndpointSlug(args.path, args.method);
|
|
122
|
+
const normalizedGroupSlug = (args.groupSlug || "")
|
|
123
|
+
.replace(/^\/+/, "")
|
|
124
|
+
.replace(/\/+$/, "");
|
|
125
|
+
|
|
126
|
+
return normalizedGroupSlug
|
|
127
|
+
? `/${normalizedGroupSlug}/${endpointSlug}`
|
|
128
|
+
: `/${endpointSlug}`;
|
|
129
|
+
}
|