radiant-docs 0.1.7 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +28 -5
- package/package.json +3 -3
- package/template/astro.config.mjs +76 -3
- package/template/package-lock.json +924 -737
- package/template/package.json +7 -5
- package/template/scripts/generate-og-images.mjs +335 -0
- package/template/scripts/generate-og-metadata.mjs +173 -0
- package/template/scripts/rewrite-static-asset-host.mjs +408 -0
- package/template/scripts/stamp-image-versions.mjs +277 -0
- package/template/scripts/stamp-og-image-versions.mjs +199 -0
- package/template/scripts/stamp-pagefind-runtime-version.mjs +140 -0
- package/template/src/assets/fonts/geist-mono/cyrillic.woff2 +0 -0
- package/template/src/assets/fonts/geist-mono/latin-ext.woff2 +0 -0
- package/template/src/assets/fonts/geist-mono/latin.woff2 +0 -0
- package/template/src/assets/fonts/google-sans-flex/canadian-aboriginal.woff2 +0 -0
- package/template/src/assets/fonts/google-sans-flex/cherokee.woff2 +0 -0
- package/template/src/assets/fonts/google-sans-flex/latin-ext.woff2 +0 -0
- package/template/src/assets/fonts/google-sans-flex/latin.woff2 +0 -0
- package/template/src/assets/fonts/google-sans-flex/math.woff2 +0 -0
- package/template/src/assets/fonts/google-sans-flex/nushu.woff2 +0 -0
- package/template/src/assets/fonts/google-sans-flex/symbols.woff2 +0 -0
- package/template/src/assets/fonts/google-sans-flex/syriac.woff2 +0 -0
- package/template/src/assets/fonts/google-sans-flex/tifinagh.woff2 +0 -0
- package/template/src/assets/fonts/google-sans-flex/vietnamese.woff2 +0 -0
- package/template/src/components/Footer.astro +94 -0
- package/template/src/components/Header.astro +11 -66
- package/template/src/components/LogoLink.astro +103 -0
- package/template/src/components/MdxPage.astro +126 -11
- package/template/src/components/OpenApiPage.astro +1036 -69
- package/template/src/components/Search.astro +0 -2
- package/template/src/components/SidebarDropdown.astro +34 -14
- package/template/src/components/SidebarGroup.astro +3 -6
- package/template/src/components/SidebarLink.astro +22 -12
- package/template/src/components/SidebarMenu.astro +19 -16
- package/template/src/components/SidebarSegmented.astro +99 -0
- package/template/src/components/SidebarSubgroup.astro +12 -12
- package/template/src/components/ThemeSwitcher.astro +30 -7
- package/template/src/components/endpoint/PlaygroundBar.astro +32 -36
- package/template/src/components/endpoint/PlaygroundButton.astro +40 -4
- package/template/src/components/endpoint/PlaygroundField.astro +1068 -22
- package/template/src/components/endpoint/PlaygroundForm.astro +559 -61
- package/template/src/components/endpoint/RequestSnippets.astro +342 -193
- package/template/src/components/endpoint/ResponseDisplay.astro +161 -147
- package/template/src/components/endpoint/ResponseFieldTree.astro +134 -0
- package/template/src/components/endpoint/ResponseFields.astro +711 -68
- package/template/src/components/endpoint/ResponseSnippets.astro +299 -173
- package/template/src/components/sidebar/SidebarEndpointLink.astro +1 -1
- package/template/src/components/ui/CodeLanguageIcon.astro +19 -0
- package/template/src/components/ui/CodeTabEdge.astro +79 -0
- package/template/src/components/ui/Field.astro +103 -20
- package/template/src/components/ui/Icon.astro +32 -0
- package/template/src/components/ui/ListChevronsToggle.astro +31 -0
- package/template/src/components/ui/Tag.astro +1 -1
- package/template/src/components/user/{Accordian.astro → Accordion.astro} +6 -6
- package/template/src/components/user/Callout.astro +5 -9
- package/template/src/components/user/CodeBlock.astro +400 -0
- package/template/src/components/user/CodeGroup.astro +225 -0
- package/template/src/components/user/ComponentPreview.astro +1 -0
- package/template/src/components/user/ComponentPreviewBlock.astro +181 -0
- package/template/src/components/user/Image.astro +132 -0
- package/template/src/components/user/Steps.astro +1 -3
- package/template/src/components/user/Tabs.astro +2 -2
- package/template/src/content.config.ts +1 -0
- package/template/src/layouts/Layout.astro +109 -8
- package/template/src/lib/code/code-block.ts +546 -0
- package/template/src/lib/frontmatter-schema.ts +8 -7
- package/template/src/lib/mdx/remark-code-block-component.ts +342 -0
- package/template/src/lib/mdx/remark-demote-h1.ts +16 -0
- package/template/src/lib/pagefind.ts +19 -5
- package/template/src/lib/routes.ts +49 -31
- package/template/src/lib/utils.ts +20 -0
- package/template/src/lib/validation.ts +638 -200
- package/template/src/pages/[...slug].astro +18 -5
- package/template/src/styles/geist-mono.css +33 -0
- package/template/src/styles/global.css +89 -84
- package/template/src/styles/google-sans-flex.css +143 -0
- package/template/ec.config.mjs +0 -51
- /package/template/src/components/user/{AccordianGroup.astro → AccordionGroup.astro} +0 -0
|
@@ -0,0 +1,408 @@
|
|
|
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();
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
const CWD = process.cwd();
|
|
6
|
+
const DIST_DIR = path.join(CWD, "dist");
|
|
7
|
+
const VERSION_LENGTH = 12;
|
|
8
|
+
const DIST_ROOT = path.resolve(DIST_DIR);
|
|
9
|
+
const VERSIONED_EXTENSIONS = new Set([
|
|
10
|
+
".avif",
|
|
11
|
+
".gif",
|
|
12
|
+
".ico",
|
|
13
|
+
".jpg",
|
|
14
|
+
".jpeg",
|
|
15
|
+
".m4a",
|
|
16
|
+
".mp3",
|
|
17
|
+
".mp4",
|
|
18
|
+
".oga",
|
|
19
|
+
".ogg",
|
|
20
|
+
".ogv",
|
|
21
|
+
".opus",
|
|
22
|
+
".png",
|
|
23
|
+
".svg",
|
|
24
|
+
".wav",
|
|
25
|
+
".webm",
|
|
26
|
+
".webp",
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
const hashCache = new Map();
|
|
30
|
+
|
|
31
|
+
function findHtmlFiles(dir, files = []) {
|
|
32
|
+
if (!fs.existsSync(dir)) return files;
|
|
33
|
+
|
|
34
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
35
|
+
for (const entry of entries) {
|
|
36
|
+
const fullPath = path.join(dir, entry.name);
|
|
37
|
+
|
|
38
|
+
if (entry.isDirectory()) {
|
|
39
|
+
findHtmlFiles(fullPath, files);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (entry.isFile() && entry.name.endsWith(".html")) {
|
|
44
|
+
files.push(fullPath);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return files;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function htmlBasePathname(filePath) {
|
|
52
|
+
const relative = path.relative(DIST_DIR, filePath).replace(/\\/g, "/");
|
|
53
|
+
if (relative === "index.html") return "/";
|
|
54
|
+
|
|
55
|
+
if (relative.endsWith("/index.html")) {
|
|
56
|
+
return `/${relative.slice(0, -"index.html".length)}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const slashIndex = relative.lastIndexOf("/");
|
|
60
|
+
if (slashIndex === -1) return "/";
|
|
61
|
+
return `/${relative.slice(0, slashIndex + 1)}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function getHash(filePath) {
|
|
65
|
+
const cached = hashCache.get(filePath);
|
|
66
|
+
if (cached) return cached;
|
|
67
|
+
|
|
68
|
+
const buffer = fs.readFileSync(filePath);
|
|
69
|
+
const hash = crypto
|
|
70
|
+
.createHash("sha256")
|
|
71
|
+
.update(buffer)
|
|
72
|
+
.digest("hex")
|
|
73
|
+
.slice(0, VERSION_LENGTH);
|
|
74
|
+
|
|
75
|
+
hashCache.set(filePath, hash);
|
|
76
|
+
return hash;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function resolveLocalImagePath(urlValue, filePath) {
|
|
80
|
+
const trimmed = urlValue.trim();
|
|
81
|
+
if (!trimmed) return null;
|
|
82
|
+
|
|
83
|
+
const decoded = trimmed.replace(/&/g, "&");
|
|
84
|
+
if (
|
|
85
|
+
decoded.startsWith("data:") ||
|
|
86
|
+
decoded.startsWith("blob:") ||
|
|
87
|
+
decoded.startsWith("mailto:") ||
|
|
88
|
+
decoded.startsWith("tel:") ||
|
|
89
|
+
decoded.startsWith("//")
|
|
90
|
+
) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Ignore fully-qualified URLs.
|
|
95
|
+
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(decoded)) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let parsed;
|
|
100
|
+
try {
|
|
101
|
+
parsed = new URL(decoded, `https://radiant.invalid${htmlBasePathname(filePath)}`);
|
|
102
|
+
} catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const extension = path.extname(parsed.pathname).toLowerCase();
|
|
107
|
+
if (!VERSIONED_EXTENSIONS.has(extension)) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const normalizedPathname = path.posix
|
|
112
|
+
.normalize(parsed.pathname)
|
|
113
|
+
.replace(/^\/+/, "");
|
|
114
|
+
const absolutePath = path.resolve(DIST_DIR, normalizedPathname);
|
|
115
|
+
|
|
116
|
+
if (
|
|
117
|
+
absolutePath !== DIST_ROOT &&
|
|
118
|
+
!absolutePath.startsWith(`${DIST_ROOT}${path.sep}`)
|
|
119
|
+
) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!fs.existsSync(absolutePath) || !fs.statSync(absolutePath).isFile()) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return { parsed, absolutePath };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function formatUpdatedUrl(parsed) {
|
|
131
|
+
const pathnameWithParams = `${parsed.pathname}${parsed.search}${parsed.hash}`;
|
|
132
|
+
return pathnameWithParams.replace(/&/g, "&");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function stampSingleUrl(urlValue, filePath) {
|
|
136
|
+
const resolved = resolveLocalImagePath(urlValue, filePath);
|
|
137
|
+
if (!resolved) {
|
|
138
|
+
return { value: urlValue, changed: false };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const version = getHash(resolved.absolutePath);
|
|
142
|
+
resolved.parsed.searchParams.set("v", version);
|
|
143
|
+
|
|
144
|
+
const updated = formatUpdatedUrl(resolved.parsed);
|
|
145
|
+
return { value: updated, changed: updated !== urlValue };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function stampSrcset(srcsetValue, filePath) {
|
|
149
|
+
const candidates = srcsetValue.split(",");
|
|
150
|
+
let changed = false;
|
|
151
|
+
|
|
152
|
+
const updated = candidates.map((candidate) => {
|
|
153
|
+
const trimmed = candidate.trim();
|
|
154
|
+
if (!trimmed) return candidate;
|
|
155
|
+
|
|
156
|
+
const whitespaceIndex = trimmed.search(/\s/);
|
|
157
|
+
const urlPart =
|
|
158
|
+
whitespaceIndex === -1 ? trimmed : trimmed.slice(0, whitespaceIndex);
|
|
159
|
+
const descriptor = whitespaceIndex === -1 ? "" : trimmed.slice(whitespaceIndex);
|
|
160
|
+
|
|
161
|
+
const stamped = stampSingleUrl(urlPart, filePath);
|
|
162
|
+
if (stamped.changed) changed = true;
|
|
163
|
+
|
|
164
|
+
return `${stamped.value}${descriptor}`;
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
value: changed ? updated.join(", ") : srcsetValue,
|
|
169
|
+
changed,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function replaceAttribute(html, filePath, attribute) {
|
|
174
|
+
let tagAlternation = "img|source";
|
|
175
|
+
if (attribute === "src") {
|
|
176
|
+
tagAlternation = "img|source|video|audio|track";
|
|
177
|
+
} else if (attribute === "poster") {
|
|
178
|
+
tagAlternation = "video";
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const pattern = new RegExp(
|
|
182
|
+
`(<(?:${tagAlternation})\\b[^>]*\\b${attribute}\\s*=\\s*["'])([^"']*)(["'][^>]*>)`,
|
|
183
|
+
"gi",
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
let changed = false;
|
|
187
|
+
|
|
188
|
+
const nextHtml = html.replace(
|
|
189
|
+
pattern,
|
|
190
|
+
(fullMatch, prefix, value, suffix) => {
|
|
191
|
+
const stamped =
|
|
192
|
+
attribute === "srcset"
|
|
193
|
+
? stampSrcset(value, filePath)
|
|
194
|
+
: stampSingleUrl(value, filePath);
|
|
195
|
+
|
|
196
|
+
if (!stamped.changed) return fullMatch;
|
|
197
|
+
|
|
198
|
+
changed = true;
|
|
199
|
+
return `${prefix}${stamped.value}${suffix}`;
|
|
200
|
+
},
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
return { html: nextHtml, changed };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function hasIconRel(tagSource) {
|
|
207
|
+
const relMatch = tagSource.match(/\brel\s*=\s*["']([^"']+)["']/i);
|
|
208
|
+
if (!relMatch) return false;
|
|
209
|
+
|
|
210
|
+
const relTokens = relMatch[1]
|
|
211
|
+
.toLowerCase()
|
|
212
|
+
.split(/\s+/)
|
|
213
|
+
.filter(Boolean);
|
|
214
|
+
|
|
215
|
+
return relTokens.includes("icon") || relTokens.includes("apple-touch-icon");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function replaceIconLinkHref(html, filePath) {
|
|
219
|
+
const pattern =
|
|
220
|
+
/(<link\b[^>]*\bhref\s*=\s*["'])([^"']*)(["'][^>]*>)/gi;
|
|
221
|
+
let changed = false;
|
|
222
|
+
|
|
223
|
+
const nextHtml = html.replace(pattern, (fullMatch, prefix, value, suffix) => {
|
|
224
|
+
if (!hasIconRel(fullMatch)) {
|
|
225
|
+
return fullMatch;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const stamped = stampSingleUrl(value, filePath);
|
|
229
|
+
if (!stamped.changed) {
|
|
230
|
+
return fullMatch;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
changed = true;
|
|
234
|
+
return `${prefix}${stamped.value}${suffix}`;
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
return { html: nextHtml, changed };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function main() {
|
|
241
|
+
if (!fs.existsSync(DIST_DIR)) {
|
|
242
|
+
console.warn("Skipping image version stamping: dist directory not found.");
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const htmlFiles = findHtmlFiles(DIST_DIR).sort();
|
|
247
|
+
if (htmlFiles.length === 0) {
|
|
248
|
+
console.warn("Skipping image version stamping: no HTML files found in dist.");
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
let updatedCount = 0;
|
|
253
|
+
|
|
254
|
+
for (const htmlFile of htmlFiles) {
|
|
255
|
+
const sourceHtml = fs.readFileSync(htmlFile, "utf8");
|
|
256
|
+
const srcResult = replaceAttribute(sourceHtml, htmlFile, "src");
|
|
257
|
+
const srcSetResult = replaceAttribute(srcResult.html, htmlFile, "srcset");
|
|
258
|
+
const posterResult = replaceAttribute(srcSetResult.html, htmlFile, "poster");
|
|
259
|
+
const iconHrefResult = replaceIconLinkHref(posterResult.html, htmlFile);
|
|
260
|
+
const fileChanged =
|
|
261
|
+
srcResult.changed ||
|
|
262
|
+
srcSetResult.changed ||
|
|
263
|
+
posterResult.changed ||
|
|
264
|
+
iconHrefResult.changed;
|
|
265
|
+
|
|
266
|
+
if (fileChanged) {
|
|
267
|
+
fs.writeFileSync(htmlFile, iconHrefResult.html, "utf8");
|
|
268
|
+
updatedCount += 1;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
console.log(
|
|
273
|
+
`✅ Image version stamping complete. Updated ${updatedCount}/${htmlFiles.length} HTML files.`,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
main();
|