radiant-docs 0.1.39 → 0.1.41
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 -7
- package/template/package-lock.json +19 -7
- package/template/package.json +3 -3
- package/template/public/favicon.svg +16 -8
- package/template/scripts/generate-robots-txt.mjs +29 -1
- package/template/scripts/remove-assistant-for-non-pro.mjs +28 -0
- package/template/scripts/stamp-image-versions.mjs +59 -33
- package/template/src/components/Footer.astro +2 -1
- package/template/src/components/Header.astro +10 -8
- package/template/src/components/LogoLink.astro +2 -1
- package/template/src/components/MdxPage.astro +15 -4
- package/template/src/components/PagePagination.astro +61 -0
- package/template/src/components/SidebarDropdown.astro +12 -8
- package/template/src/components/SidebarGroup.astro +1 -1
- package/template/src/components/SidebarMenu.astro +1 -1
- package/template/src/components/SidebarSegmented.astro +6 -5
- package/template/src/components/TableOfContents.astro +4 -13
- package/template/src/components/chat/AskAiWidget.tsx +274 -39
- package/template/src/components/chat/AssistantDocsWidget.astro +16 -0
- package/template/src/components/chat/AssistantDocsWidget.tsx +402 -0
- package/template/src/components/chat/AssistantEmbedPanel.tsx +1693 -0
- package/template/src/components/chat/AssistantEmbedPanelPage.astro +95 -0
- package/template/src/components/endpoint/PlaygroundForm.astro +2 -1
- package/template/src/components/user/Callout.astro +10 -4
- package/template/src/components/user/CodeBlock.astro +1 -1
- package/template/src/components/user/CodeGroup.astro +16 -1
- package/template/src/components/user/ComponentPreviewBlock.astro +1 -0
- package/template/src/components/user/Image.astro +43 -53
- package/template/src/layouts/Layout.astro +104 -35
- package/template/src/lib/assistant-chrome-defaults.ts +74 -0
- package/template/src/lib/assistant-chrome.ts +39 -0
- package/template/src/lib/assistant-embed-script.ts +897 -0
- package/template/src/lib/assistant-panel-config.ts +80 -0
- package/template/src/lib/base-path.ts +98 -0
- package/template/src/lib/component-error.ts +49 -10
- package/template/src/lib/favicon.ts +31 -0
- package/template/src/lib/mdx/remark-resolve-internal-links.ts +128 -18
- package/template/src/lib/pagefind.ts +62 -14
- package/template/src/lib/routes.ts +49 -1
- package/template/src/lib/static-asset-url.ts +3 -1
- package/template/src/lib/theme-css.ts +176 -0
- package/template/src/lib/utils.ts +12 -4
- package/template/src/lib/validation.ts +754 -37
- package/template/src/pages/-/assistant/embed.js.ts +15 -0
- package/template/src/pages/-/assistant/panel.astro +5 -0
- package/template/src/pages/404.astro +6 -5
- package/template/src/pages/[...slug].astro +68 -6
- package/template/src/styles/global.css +62 -1
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AssistantChromeConfig,
|
|
3
|
+
getAssistantChromeConfig,
|
|
4
|
+
} from "./assistant-chrome";
|
|
5
|
+
import { withBasePath } from "./base-path";
|
|
6
|
+
import { getAssistantLauncherIconConfig } from "./assistant-embed-script";
|
|
7
|
+
import type { DocsConfig } from "./validation";
|
|
8
|
+
|
|
9
|
+
export type AssistantPanelRuntimeConfig = {
|
|
10
|
+
apiPath: string;
|
|
11
|
+
docsTitle: string;
|
|
12
|
+
isChatAvailable: boolean;
|
|
13
|
+
canSendChatRequest: boolean;
|
|
14
|
+
launcherThemeColor: string;
|
|
15
|
+
launcherThemeColors: {
|
|
16
|
+
light: string;
|
|
17
|
+
dark: string;
|
|
18
|
+
};
|
|
19
|
+
launcherIconColor: string;
|
|
20
|
+
launcherIconColors: {
|
|
21
|
+
light: string;
|
|
22
|
+
dark: string;
|
|
23
|
+
};
|
|
24
|
+
launcherIconImageSrc: string;
|
|
25
|
+
emptyStateHeading?: string;
|
|
26
|
+
emptyStateQuestions?: string[];
|
|
27
|
+
devProxyToken?: string;
|
|
28
|
+
chrome: AssistantChromeConfig;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export function getAssistantPanelRuntimeConfig(
|
|
32
|
+
config: DocsConfig,
|
|
33
|
+
): AssistantPanelRuntimeConfig {
|
|
34
|
+
const assistantLauncherIcon = getAssistantLauncherIconConfig(config);
|
|
35
|
+
const assistantConfig = config.assistant;
|
|
36
|
+
const parsedOrgTier = Number.parseInt(
|
|
37
|
+
(import.meta.env.ORG_TIER ?? "1").toString(),
|
|
38
|
+
10,
|
|
39
|
+
);
|
|
40
|
+
const orgTier =
|
|
41
|
+
Number.isFinite(parsedOrgTier) && parsedOrgTier > 0 ? parsedOrgTier : 1;
|
|
42
|
+
const isDev = import.meta.env.DEV;
|
|
43
|
+
const assistantDevHost = (import.meta.env.ASK_AI_DEV_HOST ?? "")
|
|
44
|
+
.toString()
|
|
45
|
+
.trim();
|
|
46
|
+
const assistantDevProxySecret = (
|
|
47
|
+
import.meta.env.ASK_AI_DEV_PROXY_SECRET ?? ""
|
|
48
|
+
)
|
|
49
|
+
.toString()
|
|
50
|
+
.trim();
|
|
51
|
+
const hasAssistantDevConfig =
|
|
52
|
+
assistantDevHost.length > 0 && assistantDevProxySecret.length > 0;
|
|
53
|
+
const isChatAvailable = isDev || orgTier >= 3;
|
|
54
|
+
const canSendChatRequest = isDev ? hasAssistantDevConfig : orgTier >= 3;
|
|
55
|
+
let apiPath = withBasePath("/_platform/assistant");
|
|
56
|
+
|
|
57
|
+
if (isDev && hasAssistantDevConfig) {
|
|
58
|
+
try {
|
|
59
|
+
apiPath = new URL("/_platform/assistant", assistantDevHost).toString();
|
|
60
|
+
} catch {
|
|
61
|
+
apiPath = withBasePath("/_platform/assistant");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
apiPath,
|
|
67
|
+
docsTitle: config.title,
|
|
68
|
+
isChatAvailable,
|
|
69
|
+
canSendChatRequest,
|
|
70
|
+
launcherThemeColor: assistantLauncherIcon.themeColor,
|
|
71
|
+
launcherThemeColors: assistantLauncherIcon.themeColors,
|
|
72
|
+
launcherIconColor: assistantLauncherIcon.color,
|
|
73
|
+
launcherIconColors: assistantLauncherIcon.colors,
|
|
74
|
+
launcherIconImageSrc: assistantLauncherIcon.imageSrc,
|
|
75
|
+
emptyStateHeading: assistantConfig?.heading,
|
|
76
|
+
emptyStateQuestions: assistantConfig?.questions,
|
|
77
|
+
devProxyToken: isDev ? assistantDevProxySecret : undefined,
|
|
78
|
+
chrome: getAssistantChromeConfig(config),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
const EXTERNAL_PROTOCOL_REGEX = /^[a-zA-Z][a-zA-Z\d+\-.]*:/;
|
|
2
|
+
|
|
3
|
+
declare global {
|
|
4
|
+
// Set by astro.config.mjs so build-time MDX remark plugins can see the
|
|
5
|
+
// configured base before Astro injects import.meta.env.BASE_URL.
|
|
6
|
+
var __RADIANT_DOCS_BASE_PATH__: string | undefined;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function normalizeBasePath(value: string | null | undefined): string {
|
|
10
|
+
const trimmed = value?.trim() ?? "";
|
|
11
|
+
if (!trimmed || trimmed === "/") return "";
|
|
12
|
+
|
|
13
|
+
const pathname = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
14
|
+
const normalized = pathname.replace(/\/{2,}/g, "/").replace(/\/+$/, "");
|
|
15
|
+
return normalized === "/" ? "" : normalized;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getDocsBasePath(): string {
|
|
19
|
+
const astroBasePath = normalizeBasePath(import.meta.env.BASE_URL);
|
|
20
|
+
return (
|
|
21
|
+
astroBasePath ||
|
|
22
|
+
normalizeBasePath(globalThis.__RADIANT_DOCS_BASE_PATH__)
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function splitPathSuffix(value: string): { pathname: string; suffix: string } {
|
|
27
|
+
const match = value.match(/^([^?#]*)(.*)$/);
|
|
28
|
+
return {
|
|
29
|
+
pathname: match?.[1] ?? value,
|
|
30
|
+
suffix: match?.[2] ?? "",
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isExternalOrDocumentLocalHref(value: string): boolean {
|
|
35
|
+
if (!value) return true;
|
|
36
|
+
if (value.startsWith("#") || value.startsWith("?")) return true;
|
|
37
|
+
if (value.startsWith("//")) return true;
|
|
38
|
+
if (value.startsWith("./") || value.startsWith("../")) return true;
|
|
39
|
+
return EXTERNAL_PROTOCOL_REGEX.test(value);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function applyBasePath(
|
|
43
|
+
href: string,
|
|
44
|
+
options: { preserveAlreadyPrefixed: boolean },
|
|
45
|
+
): string {
|
|
46
|
+
const value = href.trim();
|
|
47
|
+
if (!value || isExternalOrDocumentLocalHref(value)) {
|
|
48
|
+
return href;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const basePath = getDocsBasePath();
|
|
52
|
+
if (!basePath) {
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const { pathname, suffix } = splitPathSuffix(value);
|
|
57
|
+
const normalizedPathname = pathname.startsWith("/")
|
|
58
|
+
? pathname
|
|
59
|
+
: `/${pathname}`;
|
|
60
|
+
|
|
61
|
+
if (
|
|
62
|
+
options.preserveAlreadyPrefixed &&
|
|
63
|
+
(normalizedPathname === basePath ||
|
|
64
|
+
normalizedPathname.startsWith(`${basePath}/`))
|
|
65
|
+
) {
|
|
66
|
+
return `${normalizedPathname}${suffix}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (normalizedPathname === "/") {
|
|
70
|
+
return `${basePath}${suffix}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return `${basePath}${normalizedPathname}${suffix}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function withBasePath(href: string): string {
|
|
77
|
+
return applyBasePath(href, { preserveAlreadyPrefixed: true });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function prependBasePath(href: string): string {
|
|
81
|
+
return applyBasePath(href, { preserveAlreadyPrefixed: false });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function stripBasePath(pathname: string): string {
|
|
85
|
+
const value = pathname || "/";
|
|
86
|
+
const basePath = getDocsBasePath();
|
|
87
|
+
if (!basePath) return value;
|
|
88
|
+
|
|
89
|
+
if (value === basePath) {
|
|
90
|
+
return "/";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (value.startsWith(`${basePath}/`)) {
|
|
94
|
+
return value.slice(basePath.length) || "/";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
@@ -85,27 +85,33 @@ export function validateType(
|
|
|
85
85
|
componentName: string,
|
|
86
86
|
propName: string,
|
|
87
87
|
value: unknown,
|
|
88
|
-
expectedType:
|
|
88
|
+
expectedType:
|
|
89
|
+
| "string"
|
|
90
|
+
| "number"
|
|
91
|
+
| "boolean"
|
|
92
|
+
| "object"
|
|
93
|
+
| "array"
|
|
94
|
+
| readonly ("string" | "number" | "boolean" | "object" | "array")[],
|
|
89
95
|
pathname: string
|
|
90
96
|
): void {
|
|
91
97
|
// Skip if undefined (optional props)
|
|
92
98
|
if (value === undefined) return;
|
|
93
99
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
100
|
+
const expectedTypes = Array.isArray(expectedType)
|
|
101
|
+
? expectedType
|
|
102
|
+
: [expectedType];
|
|
103
|
+
const isValid = expectedTypes.some((type) =>
|
|
104
|
+
type === "array" ? Array.isArray(value) : typeof value === type
|
|
105
|
+
);
|
|
101
106
|
|
|
102
107
|
if (!isValid) {
|
|
103
108
|
const sourceFile = getSourceFile(pathname);
|
|
104
109
|
const actualType = Array.isArray(value) ? "array" : typeof value;
|
|
110
|
+
const expectedTypeLabel = expectedTypes.join(" or ");
|
|
105
111
|
throw new Error(
|
|
106
112
|
formatError(
|
|
107
113
|
componentName,
|
|
108
|
-
`Invalid prop "${propName}": expected ${
|
|
114
|
+
`Invalid prop "${propName}": expected ${expectedTypeLabel}, got ${actualType}`,
|
|
109
115
|
sourceFile
|
|
110
116
|
)
|
|
111
117
|
);
|
|
@@ -123,10 +129,43 @@ export function validateType(
|
|
|
123
129
|
*/
|
|
124
130
|
export type PropSchema = {
|
|
125
131
|
required?: boolean;
|
|
126
|
-
type?:
|
|
132
|
+
type?:
|
|
133
|
+
| "string"
|
|
134
|
+
| "number"
|
|
135
|
+
| "boolean"
|
|
136
|
+
| "object"
|
|
137
|
+
| "array"
|
|
138
|
+
| readonly ("string" | "number" | "boolean" | "object" | "array")[];
|
|
127
139
|
enum?: readonly string[];
|
|
128
140
|
};
|
|
129
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Validates that no unsupported props are passed to a component.
|
|
144
|
+
*/
|
|
145
|
+
export function validateNoUnknownProps(
|
|
146
|
+
componentName: string,
|
|
147
|
+
props: Record<string, unknown>,
|
|
148
|
+
allowedProps: readonly string[],
|
|
149
|
+
pathname: string
|
|
150
|
+
): void {
|
|
151
|
+
const allowed = new Set(allowedProps);
|
|
152
|
+
const unknownProps = Object.keys(props).filter((key) => !allowed.has(key));
|
|
153
|
+
|
|
154
|
+
if (unknownProps.length === 0) return;
|
|
155
|
+
|
|
156
|
+
const sourceFile = getSourceFile(pathname);
|
|
157
|
+
const unknownLabel = unknownProps.map((name) => `"${name}"`).join(", ");
|
|
158
|
+
const allowedLabel = allowedProps.map((name) => `"${name}"`).join(", ");
|
|
159
|
+
const propLabel = unknownProps.length === 1 ? "prop" : "props";
|
|
160
|
+
throw new Error(
|
|
161
|
+
formatError(
|
|
162
|
+
componentName,
|
|
163
|
+
`Unsupported ${propLabel}: ${unknownLabel}. Allowed props: ${allowedLabel}`,
|
|
164
|
+
sourceFile
|
|
165
|
+
)
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
130
169
|
export function validateProps(
|
|
131
170
|
componentName: string,
|
|
132
171
|
props: Record<string, unknown>,
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export type FaviconConfig = {
|
|
5
|
+
href: string;
|
|
6
|
+
type: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const DOCS_DIR = path.join(process.cwd(), "src/content/docs");
|
|
10
|
+
const DEFAULT_FAVICON: FaviconConfig = {
|
|
11
|
+
href: "/favicon.svg",
|
|
12
|
+
type: "image/svg+xml",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const FAVICON_CANDIDATES: FaviconConfig[] = [
|
|
16
|
+
DEFAULT_FAVICON,
|
|
17
|
+
{ href: "/favicon.ico", type: "image/x-icon" },
|
|
18
|
+
{ href: "/favicon.png", type: "image/png" },
|
|
19
|
+
{ href: "/favicon.webp", type: "image/webp" },
|
|
20
|
+
{ href: "/favicon.avif", type: "image/avif" },
|
|
21
|
+
{ href: "/favicon.jpg", type: "image/jpeg" },
|
|
22
|
+
{ href: "/favicon.jpeg", type: "image/jpeg" },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
export function getFaviconConfig(): FaviconConfig {
|
|
26
|
+
const favicon = FAVICON_CANDIDATES.find((candidate) =>
|
|
27
|
+
fs.existsSync(path.join(DOCS_DIR, candidate.href.replace(/^\/+/, ""))),
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
return favicon ?? DEFAULT_FAVICON;
|
|
31
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
import type { Link, Root } from "mdast";
|
|
3
4
|
import type { Plugin } from "unified";
|
|
@@ -6,15 +7,17 @@ import {
|
|
|
6
7
|
getConfig,
|
|
7
8
|
loadOpenApiSpec,
|
|
8
9
|
type DocsConfig,
|
|
10
|
+
type HiddenPageRoute,
|
|
9
11
|
type NavGroup,
|
|
10
12
|
type NavMenuItem,
|
|
11
13
|
type NavOpenApi,
|
|
12
14
|
type NavOpenApiPage,
|
|
13
15
|
type NavPage,
|
|
14
16
|
} from "../validation";
|
|
17
|
+
import { prependBasePath, withBasePath } from "../base-path";
|
|
15
18
|
import {
|
|
16
19
|
buildMdxPageHref,
|
|
17
|
-
|
|
20
|
+
buildOpenApiEndpointHref,
|
|
18
21
|
parseOpenApiEndpoint,
|
|
19
22
|
slugify,
|
|
20
23
|
} from "../utils";
|
|
@@ -25,6 +28,7 @@ type ResolvedRouteIndex = {
|
|
|
25
28
|
canonicalHrefByFilePath: Map<string, string>;
|
|
26
29
|
allHrefsByFilePath: Map<string, string[]>;
|
|
27
30
|
validRoutePaths: Set<string>;
|
|
31
|
+
allDocsFilePaths: Set<string>;
|
|
28
32
|
};
|
|
29
33
|
|
|
30
34
|
type LinkSplit = {
|
|
@@ -145,6 +149,38 @@ function getCurrentDocFilePath(filePath: string | undefined): string | null {
|
|
|
145
149
|
return normalizeDocsFilePath(relativePath);
|
|
146
150
|
}
|
|
147
151
|
|
|
152
|
+
function collectDocsFilePaths(directory: string): string[] {
|
|
153
|
+
let entries: fs.Dirent[];
|
|
154
|
+
try {
|
|
155
|
+
entries = fs.readdirSync(directory, { withFileTypes: true });
|
|
156
|
+
} catch {
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const results: string[] = [];
|
|
161
|
+
for (const entry of entries) {
|
|
162
|
+
const entryPath = path.join(directory, entry.name);
|
|
163
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (entry.isDirectory()) {
|
|
168
|
+
results.push(...collectDocsFilePaths(entryPath));
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (entry.isFile() && /\.(md|mdx)$/i.test(entry.name)) {
|
|
173
|
+
const relativePath = path.relative(DOCS_ROOT, entryPath);
|
|
174
|
+
const normalizedFilePath = normalizeDocsFilePath(relativePath);
|
|
175
|
+
if (normalizedFilePath) {
|
|
176
|
+
results.push(normalizedFilePath);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return results;
|
|
182
|
+
}
|
|
183
|
+
|
|
148
184
|
function buildFilePathCandidates(args: {
|
|
149
185
|
baseHref: string;
|
|
150
186
|
currentDocFilePath: string | null;
|
|
@@ -235,11 +271,11 @@ function addMdxRoute(args: {
|
|
|
235
271
|
}
|
|
236
272
|
|
|
237
273
|
if (args.homePath && normalizedFilePath === args.homePath) {
|
|
238
|
-
addValidRoutePath(args.index, "/");
|
|
239
|
-
if (!aliases.includes(
|
|
240
|
-
aliases.unshift(
|
|
274
|
+
const homeHref = addValidRoutePath(args.index, prependBasePath("/"));
|
|
275
|
+
if (!aliases.includes(homeHref)) {
|
|
276
|
+
aliases.unshift(homeHref);
|
|
241
277
|
}
|
|
242
|
-
args.index.canonicalHrefByFilePath.set(normalizedFilePath,
|
|
278
|
+
args.index.canonicalHrefByFilePath.set(normalizedFilePath, homeHref);
|
|
243
279
|
}
|
|
244
280
|
|
|
245
281
|
args.index.allHrefsByFilePath.set(normalizedFilePath, aliases);
|
|
@@ -251,11 +287,33 @@ function addOpenApiEndpointRoute(args: {
|
|
|
251
287
|
method: string;
|
|
252
288
|
endpointPath: string;
|
|
253
289
|
}): void {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
290
|
+
addValidRoutePath(
|
|
291
|
+
args.index,
|
|
292
|
+
buildOpenApiEndpointHref({
|
|
293
|
+
path: args.endpointPath,
|
|
294
|
+
method: args.method,
|
|
295
|
+
groupSlug: args.parentSlug,
|
|
296
|
+
}),
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function addHiddenPageRoute(
|
|
301
|
+
index: ResolvedRouteIndex,
|
|
302
|
+
route: HiddenPageRoute,
|
|
303
|
+
): void {
|
|
304
|
+
const normalizedFilePath = normalizeDocsFilePath(route.filePath);
|
|
305
|
+
if (!normalizedFilePath) return;
|
|
306
|
+
|
|
307
|
+
const href = addValidRoutePath(index, prependBasePath(route.href));
|
|
308
|
+
if (!index.canonicalHrefByFilePath.has(normalizedFilePath)) {
|
|
309
|
+
index.canonicalHrefByFilePath.set(normalizedFilePath, href);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const aliases = index.allHrefsByFilePath.get(normalizedFilePath) ?? [];
|
|
313
|
+
if (!aliases.includes(href)) {
|
|
314
|
+
aliases.push(href);
|
|
315
|
+
}
|
|
316
|
+
index.allHrefsByFilePath.set(normalizedFilePath, aliases);
|
|
259
317
|
}
|
|
260
318
|
|
|
261
319
|
function shouldIncludeEndpoint(
|
|
@@ -431,13 +489,20 @@ async function processMenuItems(args: {
|
|
|
431
489
|
}
|
|
432
490
|
}
|
|
433
491
|
|
|
434
|
-
async function buildRouteIndex(
|
|
492
|
+
async function buildRouteIndex(
|
|
493
|
+
config: DocsConfig,
|
|
494
|
+
): Promise<ResolvedRouteIndex> {
|
|
435
495
|
const index: ResolvedRouteIndex = {
|
|
436
496
|
canonicalHrefByFilePath: new Map<string, string>(),
|
|
437
497
|
allHrefsByFilePath: new Map<string, string[]>(),
|
|
438
498
|
validRoutePaths: new Set<string>(),
|
|
499
|
+
allDocsFilePaths: new Set<string>(),
|
|
439
500
|
};
|
|
440
501
|
|
|
502
|
+
for (const docFilePath of collectDocsFilePaths(DOCS_ROOT)) {
|
|
503
|
+
index.allDocsFilePaths.add(docFilePath);
|
|
504
|
+
}
|
|
505
|
+
|
|
441
506
|
const homePath =
|
|
442
507
|
typeof config.home === "string" ? normalizeDocsFilePath(config.home) : null;
|
|
443
508
|
|
|
@@ -459,7 +524,10 @@ async function buildRouteIndex(config: DocsConfig): Promise<ResolvedRouteIndex>
|
|
|
459
524
|
}
|
|
460
525
|
|
|
461
526
|
const rootOpenApi = (config.navigation as any).openapi;
|
|
462
|
-
if (
|
|
527
|
+
if (
|
|
528
|
+
typeof rootOpenApi === "string" ||
|
|
529
|
+
(rootOpenApi && typeof rootOpenApi === "object")
|
|
530
|
+
) {
|
|
463
531
|
await processOpenApiFile({
|
|
464
532
|
index,
|
|
465
533
|
parentSlug: "",
|
|
@@ -467,7 +535,10 @@ async function buildRouteIndex(config: DocsConfig): Promise<ResolvedRouteIndex>
|
|
|
467
535
|
});
|
|
468
536
|
} else if (Array.isArray(rootOpenApi)) {
|
|
469
537
|
for (const openApiItem of rootOpenApi) {
|
|
470
|
-
if (
|
|
538
|
+
if (
|
|
539
|
+
typeof openApiItem !== "string" &&
|
|
540
|
+
(!openApiItem || typeof openApiItem !== "object")
|
|
541
|
+
) {
|
|
471
542
|
continue;
|
|
472
543
|
}
|
|
473
544
|
|
|
@@ -480,7 +551,11 @@ async function buildRouteIndex(config: DocsConfig): Promise<ResolvedRouteIndex>
|
|
|
480
551
|
}
|
|
481
552
|
|
|
482
553
|
if (homePath) {
|
|
483
|
-
addValidRoutePath(index, "/");
|
|
554
|
+
addValidRoutePath(index, prependBasePath("/"));
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
for (const hiddenPageRoute of config.hiddenPageRoutes ?? []) {
|
|
558
|
+
addHiddenPageRoute(index, hiddenPageRoute);
|
|
484
559
|
}
|
|
485
560
|
|
|
486
561
|
return index;
|
|
@@ -501,11 +576,24 @@ function buildInvalidInternalLinkError(args: {
|
|
|
501
576
|
baseHref: string;
|
|
502
577
|
currentDocFilePath: string | null;
|
|
503
578
|
filePathCandidates: string[];
|
|
579
|
+
existingFilePathCandidates: string[];
|
|
504
580
|
}): Error {
|
|
505
581
|
const sourceFile = args.currentDocFilePath
|
|
506
582
|
? `${args.currentDocFilePath}.mdx`
|
|
507
583
|
: "the current MDX file";
|
|
508
584
|
|
|
585
|
+
if (args.existingFilePathCandidates.length > 0) {
|
|
586
|
+
const existingFiles = args.existingFilePathCandidates
|
|
587
|
+
.map((candidate) => `"${candidate}.mdx"`)
|
|
588
|
+
.join(", ");
|
|
589
|
+
|
|
590
|
+
return new Error(
|
|
591
|
+
`[USER_ERROR]: Invalid internal link "${args.baseHref}" in ${sourceFile}. ` +
|
|
592
|
+
`The target file exists (${existingFiles}), but it is not a routable docs page. ` +
|
|
593
|
+
`Add it to docs.json navigation, set it as the home page, or link to it from the navbar or footer so it is included in the built site.`,
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
|
|
509
597
|
const candidateHint =
|
|
510
598
|
args.filePathCandidates.length > 0
|
|
511
599
|
? ` Tried file path candidates: ${args.filePathCandidates
|
|
@@ -515,7 +603,7 @@ function buildInvalidInternalLinkError(args: {
|
|
|
515
603
|
|
|
516
604
|
return new Error(
|
|
517
605
|
`[USER_ERROR]: Invalid internal link "${args.baseHref}" in ${sourceFile}. ` +
|
|
518
|
-
`
|
|
606
|
+
`No matching docs page file was found. Create the target .mdx file or update the link.${candidateHint}`,
|
|
519
607
|
);
|
|
520
608
|
}
|
|
521
609
|
|
|
@@ -532,12 +620,29 @@ function resolveLinkToCanonicalHref(args: {
|
|
|
532
620
|
}
|
|
533
621
|
|
|
534
622
|
const normalizedBaseRoute = normalizeRoutePath(baseHref);
|
|
623
|
+
const normalizedBaseRouteWithBase = normalizeRoutePath(
|
|
624
|
+
baseHref.startsWith("/")
|
|
625
|
+
? withBasePath(normalizedBaseRoute)
|
|
626
|
+
: prependBasePath(normalizedBaseRoute),
|
|
627
|
+
);
|
|
628
|
+
if (args.routeIndex.validRoutePaths.has(normalizedBaseRouteWithBase)) {
|
|
629
|
+
if (
|
|
630
|
+
baseHref.startsWith("/") &&
|
|
631
|
+
normalizedBaseRoute === normalizedBaseRouteWithBase
|
|
632
|
+
) {
|
|
633
|
+
return null;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
return `${normalizedBaseRouteWithBase}${parts.suffix}`;
|
|
637
|
+
}
|
|
638
|
+
|
|
535
639
|
if (args.routeIndex.validRoutePaths.has(normalizedBaseRoute)) {
|
|
536
|
-
|
|
640
|
+
const publicRoute = normalizeRoutePath(withBasePath(normalizedBaseRoute));
|
|
641
|
+
if (baseHref.startsWith("/") && normalizedBaseRoute === publicRoute) {
|
|
537
642
|
return null;
|
|
538
643
|
}
|
|
539
644
|
|
|
540
|
-
return `${
|
|
645
|
+
return `${publicRoute}${parts.suffix}`;
|
|
541
646
|
}
|
|
542
647
|
|
|
543
648
|
const filePathCandidates = buildFilePathCandidates({
|
|
@@ -556,14 +661,19 @@ function resolveLinkToCanonicalHref(args: {
|
|
|
556
661
|
);
|
|
557
662
|
}
|
|
558
663
|
|
|
559
|
-
return `${canonicalHref}${parts.suffix}`;
|
|
664
|
+
return `${withBasePath(canonicalHref)}${parts.suffix}`;
|
|
560
665
|
}
|
|
561
666
|
|
|
562
667
|
if (shouldEnforceInternalRouteValidation(baseHref)) {
|
|
668
|
+
const existingFilePathCandidates = filePathCandidates.filter((filePath) =>
|
|
669
|
+
args.routeIndex.allDocsFilePaths.has(filePath),
|
|
670
|
+
);
|
|
671
|
+
|
|
563
672
|
throw buildInvalidInternalLinkError({
|
|
564
673
|
baseHref,
|
|
565
674
|
currentDocFilePath: args.currentDocFilePath,
|
|
566
675
|
filePathCandidates,
|
|
676
|
+
existingFilePathCandidates,
|
|
567
677
|
});
|
|
568
678
|
}
|
|
569
679
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// Pagefind TypeScript types and wrapper
|
|
2
|
+
import { getDocsBasePath, withBasePath } from "./base-path";
|
|
2
3
|
import { resolveStaticAssetUrl } from "./static-asset-url";
|
|
3
4
|
|
|
4
5
|
export interface PagefindSearchResult {
|
|
@@ -38,11 +39,11 @@ export interface PagefindSearchResponse {
|
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
export interface PagefindInstance {
|
|
41
|
-
options?: (options: { baseUrl?: string
|
|
42
|
+
options?: (options: { baseUrl?: string }) => Promise<void>;
|
|
42
43
|
init: () => Promise<void>;
|
|
43
44
|
search: (
|
|
44
45
|
query: string,
|
|
45
|
-
options?: { filters?: Record<string, string> }
|
|
46
|
+
options?: { filters?: Record<string, string> },
|
|
46
47
|
) => Promise<PagefindSearchResponse>;
|
|
47
48
|
filters: () => Promise<Record<string, Record<string, number>>>;
|
|
48
49
|
preload: (query: string) => Promise<void>;
|
|
@@ -54,6 +55,57 @@ function getPagefindScriptUrl(): string {
|
|
|
54
55
|
return resolveStaticAssetUrl("/pagefind/pagefind.js");
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
function getPagefindResultBaseUrl(): string {
|
|
59
|
+
if (typeof window === "undefined") return "/";
|
|
60
|
+
|
|
61
|
+
return `${window.location.origin}${getDocsBasePath()}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function withBasePathForSearchResultUrl(url: string): string {
|
|
65
|
+
const basePath = getDocsBasePath();
|
|
66
|
+
if (!basePath || typeof window === "undefined") {
|
|
67
|
+
return withBasePath(url);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const isAbsoluteUrl =
|
|
71
|
+
/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(url) || url.startsWith("//");
|
|
72
|
+
if (!isAbsoluteUrl) {
|
|
73
|
+
return withBasePath(url);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const parsed = new URL(url, window.location.href);
|
|
78
|
+
if (
|
|
79
|
+
parsed.origin === window.location.origin &&
|
|
80
|
+
parsed.pathname !== basePath &&
|
|
81
|
+
!parsed.pathname.startsWith(`${basePath}/`)
|
|
82
|
+
) {
|
|
83
|
+
const pathname = parsed.pathname.startsWith("/")
|
|
84
|
+
? parsed.pathname
|
|
85
|
+
: `/${parsed.pathname}`;
|
|
86
|
+
parsed.pathname = `${basePath}${pathname}`;
|
|
87
|
+
return parsed.toString();
|
|
88
|
+
}
|
|
89
|
+
} catch {
|
|
90
|
+
// Fall through to normal relative URL handling.
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return withBasePath(url);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function withBasePathForSearchResult(
|
|
97
|
+
data: PagefindResultData,
|
|
98
|
+
): PagefindResultData {
|
|
99
|
+
return {
|
|
100
|
+
...data,
|
|
101
|
+
url: withBasePathForSearchResultUrl(data.url),
|
|
102
|
+
sub_results: data.sub_results?.map((subResult) => ({
|
|
103
|
+
...subResult,
|
|
104
|
+
url: withBasePathForSearchResultUrl(subResult.url),
|
|
105
|
+
})),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
57
109
|
export async function getPagefind(): Promise<PagefindInstance | null> {
|
|
58
110
|
if (pagefindInstance) return pagefindInstance;
|
|
59
111
|
|
|
@@ -62,17 +114,15 @@ export async function getPagefind(): Promise<PagefindInstance | null> {
|
|
|
62
114
|
// This keeps Pagefind loading purely runtime and allows versioned query params.
|
|
63
115
|
const importPagefind = new Function(
|
|
64
116
|
"moduleUrl",
|
|
65
|
-
"return import(moduleUrl)"
|
|
117
|
+
"return import(moduleUrl)",
|
|
66
118
|
) as (moduleUrl: string) => Promise<PagefindInstance>;
|
|
67
119
|
|
|
68
120
|
const pagefind = await importPagefind(getPagefindScriptUrl());
|
|
69
|
-
// Pagefind uses the
|
|
70
|
-
//
|
|
71
|
-
//
|
|
121
|
+
// Pagefind uses the pagefind.js import URL to locate its index files.
|
|
122
|
+
// Only override result URLs; passing Pagefind's basePath would move
|
|
123
|
+
// pagefind-entry.json and chunk fetches back onto the docs origin.
|
|
72
124
|
if (typeof pagefind.options === "function") {
|
|
73
|
-
|
|
74
|
-
typeof window !== "undefined" ? window.location.origin : "/";
|
|
75
|
-
await pagefind.options({ baseUrl });
|
|
125
|
+
await pagefind.options({ baseUrl: getPagefindResultBaseUrl() });
|
|
76
126
|
}
|
|
77
127
|
await pagefind.init();
|
|
78
128
|
pagefindInstance = pagefind;
|
|
@@ -85,7 +135,7 @@ export async function getPagefind(): Promise<PagefindInstance | null> {
|
|
|
85
135
|
|
|
86
136
|
export async function search(
|
|
87
137
|
query: string,
|
|
88
|
-
limit: number = 8
|
|
138
|
+
limit: number = 8,
|
|
89
139
|
): Promise<PagefindResultData[]> {
|
|
90
140
|
const pagefind = await getPagefind();
|
|
91
141
|
if (!pagefind || !query.trim()) return [];
|
|
@@ -94,10 +144,8 @@ export async function search(
|
|
|
94
144
|
|
|
95
145
|
// Load full data for top results
|
|
96
146
|
const results = await Promise.all(
|
|
97
|
-
response.results.slice(0, limit).map((result) => result.data())
|
|
147
|
+
response.results.slice(0, limit).map((result) => result.data()),
|
|
98
148
|
);
|
|
99
149
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
return results;
|
|
150
|
+
return results.map(withBasePathForSearchResult);
|
|
103
151
|
}
|