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
|
@@ -125,9 +125,18 @@ export type NavPage = {
|
|
|
125
125
|
tag?: string;
|
|
126
126
|
title?: string;
|
|
127
127
|
};
|
|
128
|
+
export type NavOpenApiPageRef = {
|
|
129
|
+
source: string;
|
|
130
|
+
endpoint: string;
|
|
131
|
+
};
|
|
132
|
+
export type NavOpenApiPage = {
|
|
133
|
+
openapi: NavOpenApiPageRef;
|
|
134
|
+
title?: string;
|
|
135
|
+
tag?: string;
|
|
136
|
+
};
|
|
128
137
|
export type NavGroup = {
|
|
129
138
|
group: string;
|
|
130
|
-
pages: (string | NavPage | NavGroup)[];
|
|
139
|
+
pages: (string | NavPage | NavGroup | NavOpenApiPage)[];
|
|
131
140
|
icon?: string | null;
|
|
132
141
|
expanded?: boolean; // need to add this logic
|
|
133
142
|
tag?: string;
|
|
@@ -138,7 +147,7 @@ export type NavOpenApi = {
|
|
|
138
147
|
exclude?: string[];
|
|
139
148
|
};
|
|
140
149
|
export type NavigationItem = {
|
|
141
|
-
pages?: (string | NavPage | NavGroup)[];
|
|
150
|
+
pages?: (string | NavPage | NavGroup | NavOpenApiPage)[];
|
|
142
151
|
menu?: NavMenu;
|
|
143
152
|
openapi?: string | NavOpenApi;
|
|
144
153
|
};
|
|
@@ -553,11 +562,53 @@ function parseEndpointString(
|
|
|
553
562
|
return { method, path: normalizedPath };
|
|
554
563
|
}
|
|
555
564
|
|
|
556
|
-
function
|
|
565
|
+
async function validateNavOpenApiPage(
|
|
566
|
+
navOpenApiPage: any,
|
|
567
|
+
currentPath: Path,
|
|
568
|
+
): Promise<void> {
|
|
569
|
+
checkType(navOpenApiPage, "object", currentPath, "Open API page");
|
|
570
|
+
|
|
571
|
+
if (typeof navOpenApiPage.source !== "string") {
|
|
572
|
+
throwConfigError(
|
|
573
|
+
"Open API page must include a 'source' property that is a string.",
|
|
574
|
+
[...currentPath, "source"],
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
if (typeof navOpenApiPage.endpoint !== "string") {
|
|
579
|
+
throwConfigError(
|
|
580
|
+
"Open API page must include an 'endpoint' property that is a string in the format \"METHOD /path\".",
|
|
581
|
+
[...currentPath, "endpoint"],
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const parsedEndpoint = parseEndpointString(navOpenApiPage.endpoint);
|
|
586
|
+
if (!parsedEndpoint) {
|
|
587
|
+
throwConfigError(
|
|
588
|
+
`Open API page endpoint must be in the format "METHOD /path". Found: ${navOpenApiPage.endpoint}`,
|
|
589
|
+
[...currentPath, "endpoint"],
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
await validateOpenApiFile(navOpenApiPage.source, [...currentPath, "source"]);
|
|
594
|
+
|
|
595
|
+
const openApiDoc = await loadOpenApiSpec(navOpenApiPage.source);
|
|
596
|
+
const availableEndpoints = extractAvailableEndpoints(openApiDoc);
|
|
597
|
+
const endpointKey = `${parsedEndpoint!.method} ${parsedEndpoint!.path}`;
|
|
598
|
+
|
|
599
|
+
if (!availableEndpoints.has(endpointKey)) {
|
|
600
|
+
throwConfigError(
|
|
601
|
+
`Open API page endpoint does not match any endpoint in the OpenAPI spec. Found: ${navOpenApiPage.endpoint}. Expected format: "METHOD /path".`,
|
|
602
|
+
[...currentPath, "endpoint"],
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
async function validateNavigationNode(
|
|
557
608
|
item: any,
|
|
558
609
|
currentPath: Path,
|
|
559
610
|
groupDepth: number = 0,
|
|
560
|
-
): void {
|
|
611
|
+
): Promise<void> {
|
|
561
612
|
// A) Base Case: Simple string path
|
|
562
613
|
if (typeof item === "string") {
|
|
563
614
|
const normalizedPath = normalizeDocsPagePath(item, currentPath);
|
|
@@ -571,11 +622,12 @@ function validateNavigationNode(
|
|
|
571
622
|
// Determine item type by key presence (Strict XOR enforcement)
|
|
572
623
|
const isGroup = "group" in item;
|
|
573
624
|
const isPage = "page" in item;
|
|
625
|
+
const isOpenApiPage = "openapi" in item;
|
|
574
626
|
|
|
575
|
-
const typeCount = [isGroup, isPage].filter(Boolean).length;
|
|
627
|
+
const typeCount = [isGroup, isPage, isOpenApiPage].filter(Boolean).length;
|
|
576
628
|
if (typeCount !== 1) {
|
|
577
629
|
throwConfigError(
|
|
578
|
-
"Object must contain exactly one key: 'page' or '
|
|
630
|
+
"Object must contain exactly one key: 'page', 'group', or 'openapi'.",
|
|
579
631
|
currentPath,
|
|
580
632
|
);
|
|
581
633
|
}
|
|
@@ -601,17 +653,17 @@ function validateNavigationNode(
|
|
|
601
653
|
throwConfigError("Group must have a 'pages' array.", [...path, "pages"]);
|
|
602
654
|
checkType(item.pages, "array", [...path, "pages"], "Group pages");
|
|
603
655
|
|
|
604
|
-
item.pages.
|
|
656
|
+
for (const [i, child] of item.pages.entries()) {
|
|
605
657
|
if (typeof child === "string") {
|
|
606
658
|
const childPath = [...path, "pages", i];
|
|
607
659
|
const normalizedPagePath = normalizeDocsPagePath(child, childPath);
|
|
608
660
|
item.pages[i] = normalizedPagePath;
|
|
609
661
|
validateFileExistence(normalizedPagePath, childPath);
|
|
610
|
-
|
|
662
|
+
continue;
|
|
611
663
|
}
|
|
612
664
|
|
|
613
|
-
validateNavigationNode(child, [...path, "pages", i], groupDepth + 1);
|
|
614
|
-
}
|
|
665
|
+
await validateNavigationNode(child, [...path, "pages", i], groupDepth + 1);
|
|
666
|
+
}
|
|
615
667
|
return;
|
|
616
668
|
}
|
|
617
669
|
|
|
@@ -640,10 +692,47 @@ function validateNavigationNode(
|
|
|
640
692
|
throwConfigError("Page items cannot have children.", [...path, "pages"]);
|
|
641
693
|
return;
|
|
642
694
|
}
|
|
695
|
+
|
|
696
|
+
if (isOpenApiPage) {
|
|
697
|
+
const path = [...currentPath];
|
|
698
|
+
|
|
699
|
+
if ("icon" in item) {
|
|
700
|
+
throwConfigError(
|
|
701
|
+
"Open API page items cannot have an 'icon'. Method badges are displayed automatically.",
|
|
702
|
+
[...path, "icon"],
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
await validateNavOpenApiPage(item.openapi, [...path, "openapi"]);
|
|
707
|
+
checkType(item.title, "string", [...path, "title"], "Open API page title");
|
|
708
|
+
checkType(item.tag, "string", [...path, "tag"], "Open API page tag");
|
|
709
|
+
|
|
710
|
+
if ("expanded" in item)
|
|
711
|
+
throwConfigError("Open API page items cannot have 'expanded'.", [
|
|
712
|
+
...path,
|
|
713
|
+
"expanded",
|
|
714
|
+
]);
|
|
715
|
+
if ("pages" in item)
|
|
716
|
+
throwConfigError("Open API page items cannot have children.", [
|
|
717
|
+
...path,
|
|
718
|
+
"pages",
|
|
719
|
+
]);
|
|
720
|
+
if ("group" in item)
|
|
721
|
+
throwConfigError("Open API page items cannot have 'group'.", [
|
|
722
|
+
...path,
|
|
723
|
+
"group",
|
|
724
|
+
]);
|
|
725
|
+
if ("page" in item)
|
|
726
|
+
throwConfigError("Open API page items cannot have 'page'.", [
|
|
727
|
+
...path,
|
|
728
|
+
"page",
|
|
729
|
+
]);
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
643
732
|
}
|
|
644
733
|
|
|
645
734
|
function getFirstPagePathFromPageItems(
|
|
646
|
-
items: (string | NavPage | NavGroup)[],
|
|
735
|
+
items: (string | NavPage | NavGroup | NavOpenApiPage)[],
|
|
647
736
|
): string | undefined {
|
|
648
737
|
for (const item of items) {
|
|
649
738
|
if (typeof item === "string") {
|
|
@@ -890,19 +979,18 @@ async function validateNavMenuItem(item: NavMenuItem, currentPath: Path) {
|
|
|
890
979
|
[...currentPath, "submenu", "pages"],
|
|
891
980
|
"Submenu pages",
|
|
892
981
|
);
|
|
893
|
-
(submenuValue as NavigationItem["pages"])
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
);
|
|
982
|
+
const pages = (submenuValue as NavigationItem["pages"]) ?? [];
|
|
983
|
+
for (const [i, item] of pages.entries()) {
|
|
984
|
+
const itemPath = [...currentPath, "submenu", "pages", i];
|
|
985
|
+
if (typeof item === "string") {
|
|
986
|
+
const normalizedPagePath = normalizeDocsPagePath(item, itemPath);
|
|
987
|
+
(submenuValue as (string | NavPage | NavGroup | NavOpenApiPage)[])[i] =
|
|
988
|
+
normalizedPagePath;
|
|
989
|
+
validateFileExistence(normalizedPagePath, itemPath);
|
|
990
|
+
} else {
|
|
991
|
+
await validateNavigationNode(item, itemPath);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
906
994
|
}
|
|
907
995
|
|
|
908
996
|
// Validate openapi - can be string or NavOpenApi object
|
|
@@ -1326,16 +1414,16 @@ async function validateNavigation(navigation: DocsConfig["navigation"]) {
|
|
|
1326
1414
|
);
|
|
1327
1415
|
|
|
1328
1416
|
// Route to Recursive Structural Validation
|
|
1329
|
-
|
|
1417
|
+
for (const [i, item] of navValue.entries()) {
|
|
1330
1418
|
const itemPath = ["navigation", navKey, i] as Path;
|
|
1331
1419
|
if (typeof item === "string") {
|
|
1332
1420
|
const normalizedPagePath = normalizeDocsPagePath(item, itemPath);
|
|
1333
1421
|
navValue[i] = normalizedPagePath;
|
|
1334
1422
|
validateFileExistence(normalizedPagePath, itemPath);
|
|
1335
1423
|
} else {
|
|
1336
|
-
validateNavigationNode(item, itemPath);
|
|
1424
|
+
await validateNavigationNode(item, itemPath);
|
|
1337
1425
|
}
|
|
1338
|
-
}
|
|
1426
|
+
}
|
|
1339
1427
|
}
|
|
1340
1428
|
}
|
|
1341
1429
|
|
|
@@ -1,408 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
|
|
4
|
-
const CWD = process.cwd();
|
|
5
|
-
const DIST_DIR = path.join(CWD, "dist");
|
|
6
|
-
const LOCAL_ORIGIN = "https://radiant.invalid";
|
|
7
|
-
|
|
8
|
-
const STATIC_EXTENSIONS = new Set([
|
|
9
|
-
".avif",
|
|
10
|
-
".css",
|
|
11
|
-
".eot",
|
|
12
|
-
".gif",
|
|
13
|
-
".ico",
|
|
14
|
-
".jpeg",
|
|
15
|
-
".jpg",
|
|
16
|
-
".js",
|
|
17
|
-
".json",
|
|
18
|
-
".mjs",
|
|
19
|
-
".mp4",
|
|
20
|
-
".otf",
|
|
21
|
-
".pdf",
|
|
22
|
-
".png",
|
|
23
|
-
".svg",
|
|
24
|
-
".ttf",
|
|
25
|
-
".txt",
|
|
26
|
-
".wasm",
|
|
27
|
-
".webmanifest",
|
|
28
|
-
".webp",
|
|
29
|
-
".woff",
|
|
30
|
-
".woff2",
|
|
31
|
-
".xml",
|
|
32
|
-
]);
|
|
33
|
-
|
|
34
|
-
const STATIC_PATH_PREFIXES = ["/_astro/", "/_og/", "/pagefind/"];
|
|
35
|
-
|
|
36
|
-
function normalizeHostUrl(input) {
|
|
37
|
-
const withScheme = /^https?:\/\//i.test(input) ? input : `https://${input}`;
|
|
38
|
-
return new URL(withScheme);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function normalizePrefix(input) {
|
|
42
|
-
return input.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function findFilesByExtension(dir, extension, files = []) {
|
|
46
|
-
if (!fs.existsSync(dir)) return files;
|
|
47
|
-
|
|
48
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
49
|
-
for (const entry of entries) {
|
|
50
|
-
const fullPath = path.join(dir, entry.name);
|
|
51
|
-
|
|
52
|
-
if (entry.isDirectory()) {
|
|
53
|
-
findFilesByExtension(fullPath, extension, files);
|
|
54
|
-
continue;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (entry.isFile() && entry.name.endsWith(extension)) {
|
|
58
|
-
files.push(fullPath);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return files;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function htmlBasePathname(filePath) {
|
|
66
|
-
const relative = path.relative(DIST_DIR, filePath).replace(/\\/g, "/");
|
|
67
|
-
if (relative === "index.html") return "/";
|
|
68
|
-
|
|
69
|
-
if (relative.endsWith("/index.html")) {
|
|
70
|
-
return `/${relative.slice(0, -"index.html".length)}`;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const slashIndex = relative.lastIndexOf("/");
|
|
74
|
-
if (slashIndex === -1) return "/";
|
|
75
|
-
return `/${relative.slice(0, slashIndex + 1)}`;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function decodeValue(value) {
|
|
79
|
-
return value.trim().replace(/&/g, "&");
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function isSkippableUrl(value) {
|
|
83
|
-
return (
|
|
84
|
-
!value ||
|
|
85
|
-
value.startsWith("data:") ||
|
|
86
|
-
value.startsWith("blob:") ||
|
|
87
|
-
value.startsWith("mailto:") ||
|
|
88
|
-
value.startsWith("tel:") ||
|
|
89
|
-
value.startsWith("javascript:") ||
|
|
90
|
-
value.startsWith("//")
|
|
91
|
-
);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function isStaticAssetPath(pathname) {
|
|
95
|
-
const normalizedPath = pathname.toLowerCase();
|
|
96
|
-
if (STATIC_PATH_PREFIXES.some((prefix) => normalizedPath.startsWith(prefix))) {
|
|
97
|
-
return true;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const extension = path.extname(normalizedPath);
|
|
101
|
-
return STATIC_EXTENSIONS.has(extension);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function withPrefix(pathname, prefix) {
|
|
105
|
-
const normalizedPathname = pathname.startsWith("/") ? pathname : `/${pathname}`;
|
|
106
|
-
if (
|
|
107
|
-
normalizedPathname === `/${prefix}` ||
|
|
108
|
-
normalizedPathname.startsWith(`/${prefix}/`)
|
|
109
|
-
) {
|
|
110
|
-
return normalizedPathname;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return `/${prefix}${normalizedPathname}`.replace(/\/+/g, "/");
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function rewriteSingleUrl(value, filePath, hostUrl, prefix) {
|
|
117
|
-
const decoded = decodeValue(value);
|
|
118
|
-
if (isSkippableUrl(decoded)) {
|
|
119
|
-
return { value, changed: false };
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
let parsed;
|
|
123
|
-
try {
|
|
124
|
-
parsed = new URL(decoded, `${LOCAL_ORIGIN}${htmlBasePathname(filePath)}`);
|
|
125
|
-
} catch {
|
|
126
|
-
return { value, changed: false };
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (parsed.origin === hostUrl.origin) {
|
|
130
|
-
const prefixedPathname = withPrefix(parsed.pathname, prefix);
|
|
131
|
-
if (prefixedPathname === parsed.pathname) {
|
|
132
|
-
return { value, changed: false };
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const updated = `${hostUrl.origin}${prefixedPathname}${parsed.search}${parsed.hash}`;
|
|
136
|
-
return {
|
|
137
|
-
value: updated.replace(/&/g, "&"),
|
|
138
|
-
changed: true,
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (parsed.origin !== LOCAL_ORIGIN) {
|
|
143
|
-
return { value, changed: false };
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (!isStaticAssetPath(parsed.pathname)) {
|
|
147
|
-
return { value, changed: false };
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const prefixedPathname = withPrefix(parsed.pathname, prefix);
|
|
151
|
-
const updated = `${hostUrl.origin}${prefixedPathname}${parsed.search}${parsed.hash}`;
|
|
152
|
-
|
|
153
|
-
return {
|
|
154
|
-
value: updated.replace(/&/g, "&"),
|
|
155
|
-
changed: updated !== decoded,
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function rewriteSrcset(value, filePath, hostUrl, prefix) {
|
|
160
|
-
const candidates = value.split(",");
|
|
161
|
-
let changed = false;
|
|
162
|
-
|
|
163
|
-
const rewritten = candidates.map((candidate) => {
|
|
164
|
-
const trimmed = candidate.trim();
|
|
165
|
-
if (!trimmed) return candidate;
|
|
166
|
-
|
|
167
|
-
const whitespaceIndex = trimmed.search(/\s/);
|
|
168
|
-
const urlPart =
|
|
169
|
-
whitespaceIndex === -1 ? trimmed : trimmed.slice(0, whitespaceIndex);
|
|
170
|
-
const descriptor = whitespaceIndex === -1 ? "" : trimmed.slice(whitespaceIndex);
|
|
171
|
-
|
|
172
|
-
const rewrittenCandidate = rewriteSingleUrl(urlPart, filePath, hostUrl, prefix);
|
|
173
|
-
if (rewrittenCandidate.changed) changed = true;
|
|
174
|
-
|
|
175
|
-
return `${rewrittenCandidate.value}${descriptor}`;
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
return {
|
|
179
|
-
value: changed ? rewritten.join(", ") : value,
|
|
180
|
-
changed,
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function rewriteAttribute(html, filePath, tagName, attribute, hostUrl, prefix) {
|
|
185
|
-
const pattern = new RegExp(
|
|
186
|
-
`(<${tagName}\\b[^>]*\\b${attribute}\\s*=\\s*["'])([^"']*)(["'][^>]*>)`,
|
|
187
|
-
"gi",
|
|
188
|
-
);
|
|
189
|
-
|
|
190
|
-
let changed = false;
|
|
191
|
-
|
|
192
|
-
const rewritten = html.replace(pattern, (full, before, value, after) => {
|
|
193
|
-
const result =
|
|
194
|
-
attribute === "srcset"
|
|
195
|
-
? rewriteSrcset(value, filePath, hostUrl, prefix)
|
|
196
|
-
: rewriteSingleUrl(value, filePath, hostUrl, prefix);
|
|
197
|
-
|
|
198
|
-
if (!result.changed) return full;
|
|
199
|
-
changed = true;
|
|
200
|
-
return `${before}${result.value}${after}`;
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
return { html: rewritten, changed };
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function rewriteMetaImageContent(html, propertyName, hostUrl, prefix) {
|
|
207
|
-
const patterns = [
|
|
208
|
-
new RegExp(
|
|
209
|
-
`(<meta\\s+[^>]*property\\s*=\\s*["']${propertyName}["'][^>]*\\bcontent\\s*=\\s*["'])([^"']*)(["'][^>]*>)`,
|
|
210
|
-
"gi",
|
|
211
|
-
),
|
|
212
|
-
new RegExp(
|
|
213
|
-
`(<meta\\s+[^>]*\\bcontent\\s*=\\s*["'])([^"']*)(["'][^>]*property\\s*=\\s*["']${propertyName}["'][^>]*>)`,
|
|
214
|
-
"gi",
|
|
215
|
-
),
|
|
216
|
-
];
|
|
217
|
-
|
|
218
|
-
let changed = false;
|
|
219
|
-
let nextHtml = html;
|
|
220
|
-
|
|
221
|
-
for (const pattern of patterns) {
|
|
222
|
-
nextHtml = nextHtml.replace(pattern, (full, before, value, after) => {
|
|
223
|
-
const result = rewriteSingleUrl(value, DIST_DIR, hostUrl, prefix);
|
|
224
|
-
if (!result.changed) return full;
|
|
225
|
-
changed = true;
|
|
226
|
-
return `${before}${result.value}${after}`;
|
|
227
|
-
});
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
return { html: nextHtml, changed };
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function rewriteMetaNameContent(html, name, hostUrl, prefix) {
|
|
234
|
-
const patterns = [
|
|
235
|
-
new RegExp(
|
|
236
|
-
`(<meta\\s+[^>]*name\\s*=\\s*["']${name}["'][^>]*\\bcontent\\s*=\\s*["'])([^"']*)(["'][^>]*>)`,
|
|
237
|
-
"gi",
|
|
238
|
-
),
|
|
239
|
-
new RegExp(
|
|
240
|
-
`(<meta\\s+[^>]*\\bcontent\\s*=\\s*["'])([^"']*)(["'][^>]*name\\s*=\\s*["']${name}["'][^>]*>)`,
|
|
241
|
-
"gi",
|
|
242
|
-
),
|
|
243
|
-
];
|
|
244
|
-
|
|
245
|
-
let changed = false;
|
|
246
|
-
let nextHtml = html;
|
|
247
|
-
|
|
248
|
-
for (const pattern of patterns) {
|
|
249
|
-
nextHtml = nextHtml.replace(pattern, (full, before, value, after) => {
|
|
250
|
-
const result = rewriteSingleUrl(value, DIST_DIR, hostUrl, prefix);
|
|
251
|
-
if (!result.changed) return full;
|
|
252
|
-
changed = true;
|
|
253
|
-
return `${before}${result.value}${after}`;
|
|
254
|
-
});
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
return { html: nextHtml, changed };
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function rewriteInlinePagefindRuntimeImports(html, filePath, hostUrl, prefix) {
|
|
261
|
-
const pattern =
|
|
262
|
-
/(["'])(https?:\/\/[^"'`)]*\/pagefind\/pagefind\.js(?:\?[^"'`)]*)?|\/pagefind\/pagefind\.js(?:\?[^"'`)]*)?)\1/g;
|
|
263
|
-
let changed = false;
|
|
264
|
-
|
|
265
|
-
const rewritten = html.replace(pattern, (full, quote, urlValue) => {
|
|
266
|
-
const result = rewriteSingleUrl(urlValue, filePath, hostUrl, prefix);
|
|
267
|
-
if (!result.changed) return full;
|
|
268
|
-
|
|
269
|
-
changed = true;
|
|
270
|
-
return `${quote}${result.value}${quote}`;
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
return { html: rewritten, changed };
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function rewriteCssUrls(css, filePath, hostUrl, prefix) {
|
|
277
|
-
const pattern = /url\(\s*(['"]?)([^"')]+)\1\s*\)/gi;
|
|
278
|
-
let changed = false;
|
|
279
|
-
|
|
280
|
-
const rewritten = css.replace(pattern, (full, quote, value) => {
|
|
281
|
-
const result = rewriteSingleUrl(value, filePath, hostUrl, prefix);
|
|
282
|
-
if (!result.changed) return full;
|
|
283
|
-
|
|
284
|
-
changed = true;
|
|
285
|
-
return `url(${quote}${result.value}${quote})`;
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
return { css: rewritten, changed };
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
function main() {
|
|
292
|
-
const staticAssetHostInput = process.env.STATIC_ASSET_HOST?.trim();
|
|
293
|
-
const staticAssetPrefixInput =
|
|
294
|
-
process.env.R2_BUCKET_PREFIX?.trim() ??
|
|
295
|
-
process.env.STATIC_ASSET_PREFIX?.trim();
|
|
296
|
-
|
|
297
|
-
if (!staticAssetHostInput) {
|
|
298
|
-
console.log(
|
|
299
|
-
"Skipping static asset host rewrite: STATIC_ASSET_HOST is not configured.",
|
|
300
|
-
);
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
if (!staticAssetPrefixInput) {
|
|
305
|
-
console.log(
|
|
306
|
-
"Skipping static asset host rewrite: R2_BUCKET_PREFIX or STATIC_ASSET_PREFIX is not configured.",
|
|
307
|
-
);
|
|
308
|
-
return;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
if (!fs.existsSync(DIST_DIR)) {
|
|
312
|
-
console.warn("Skipping static asset host rewrite: dist directory not found.");
|
|
313
|
-
return;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
const hostUrl = normalizeHostUrl(staticAssetHostInput);
|
|
317
|
-
const prefix = normalizePrefix(staticAssetPrefixInput);
|
|
318
|
-
const htmlFiles = findFilesByExtension(DIST_DIR, ".html").sort();
|
|
319
|
-
|
|
320
|
-
if (htmlFiles.length === 0) {
|
|
321
|
-
console.warn("Skipping static asset host rewrite: no HTML files found in dist.");
|
|
322
|
-
return;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
let updatedHtmlCount = 0;
|
|
326
|
-
let updatedCssCount = 0;
|
|
327
|
-
|
|
328
|
-
for (const htmlFile of htmlFiles) {
|
|
329
|
-
const sourceHtml = fs.readFileSync(htmlFile, "utf8");
|
|
330
|
-
let nextHtml = sourceHtml;
|
|
331
|
-
let fileChanged = false;
|
|
332
|
-
|
|
333
|
-
const rewrites = [
|
|
334
|
-
["link", "href"],
|
|
335
|
-
["script", "src"],
|
|
336
|
-
["img", "src"],
|
|
337
|
-
["img", "srcset"],
|
|
338
|
-
["source", "src"],
|
|
339
|
-
["source", "srcset"],
|
|
340
|
-
["video", "src"],
|
|
341
|
-
["video", "poster"],
|
|
342
|
-
["audio", "src"],
|
|
343
|
-
];
|
|
344
|
-
|
|
345
|
-
for (const [tagName, attribute] of rewrites) {
|
|
346
|
-
const result = rewriteAttribute(
|
|
347
|
-
nextHtml,
|
|
348
|
-
htmlFile,
|
|
349
|
-
tagName,
|
|
350
|
-
attribute,
|
|
351
|
-
hostUrl,
|
|
352
|
-
prefix,
|
|
353
|
-
);
|
|
354
|
-
nextHtml = result.html;
|
|
355
|
-
fileChanged = fileChanged || result.changed;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
const ogResult = rewriteMetaImageContent(
|
|
359
|
-
nextHtml,
|
|
360
|
-
"og:image",
|
|
361
|
-
hostUrl,
|
|
362
|
-
prefix,
|
|
363
|
-
);
|
|
364
|
-
nextHtml = ogResult.html;
|
|
365
|
-
fileChanged = fileChanged || ogResult.changed;
|
|
366
|
-
|
|
367
|
-
const twitterResult = rewriteMetaNameContent(
|
|
368
|
-
nextHtml,
|
|
369
|
-
"twitter:image",
|
|
370
|
-
hostUrl,
|
|
371
|
-
prefix,
|
|
372
|
-
);
|
|
373
|
-
nextHtml = twitterResult.html;
|
|
374
|
-
fileChanged = fileChanged || twitterResult.changed;
|
|
375
|
-
|
|
376
|
-
const pagefindImportResult = rewriteInlinePagefindRuntimeImports(
|
|
377
|
-
nextHtml,
|
|
378
|
-
htmlFile,
|
|
379
|
-
hostUrl,
|
|
380
|
-
prefix,
|
|
381
|
-
);
|
|
382
|
-
nextHtml = pagefindImportResult.html;
|
|
383
|
-
fileChanged = fileChanged || pagefindImportResult.changed;
|
|
384
|
-
|
|
385
|
-
if (fileChanged) {
|
|
386
|
-
fs.writeFileSync(htmlFile, nextHtml, "utf8");
|
|
387
|
-
updatedHtmlCount += 1;
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
const cssFiles = findFilesByExtension(DIST_DIR, ".css").sort();
|
|
392
|
-
|
|
393
|
-
for (const cssFile of cssFiles) {
|
|
394
|
-
const sourceCss = fs.readFileSync(cssFile, "utf8");
|
|
395
|
-
const result = rewriteCssUrls(sourceCss, cssFile, hostUrl, prefix);
|
|
396
|
-
|
|
397
|
-
if (!result.changed) continue;
|
|
398
|
-
|
|
399
|
-
fs.writeFileSync(cssFile, result.css, "utf8");
|
|
400
|
-
updatedCssCount += 1;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
console.log(
|
|
404
|
-
`✅ Static asset host rewrite complete. Updated HTML ${updatedHtmlCount}/${htmlFiles.length}, CSS ${updatedCssCount}/${cssFiles.length}.`,
|
|
405
|
-
);
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
main();
|