radiant-docs 0.1.60 → 0.1.62

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.
Files changed (41) hide show
  1. package/package.json +1 -1
  2. package/template/package-lock.json +10 -4
  3. package/template/package.json +11 -2
  4. package/template/scripts/generate-proxy-allowed-origins.mjs +14 -6
  5. package/template/scripts/publish-shiki-platform-assets.mjs +1151 -0
  6. package/template/src/components/Header.astro +6 -1
  7. package/template/src/components/NavigationTabList.astro +65 -0
  8. package/template/src/components/NavigationTabs.astro +109 -0
  9. package/template/src/components/OpenApiPage.astro +178 -14
  10. package/template/src/components/Sidebar.astro +2 -2
  11. package/template/src/components/SidebarDropdown.astro +105 -44
  12. package/template/src/components/SidebarMenu.astro +3 -0
  13. package/template/src/components/SidebarSegmented.astro +87 -52
  14. package/template/src/components/SidebarTabs.astro +86 -0
  15. package/template/src/components/chat/AssistantDocsWidget.tsx +127 -2
  16. package/template/src/components/chat/AssistantEmbedPanel.tsx +287 -290
  17. package/template/src/components/endpoint/PlaygroundBar.astro +54 -9
  18. package/template/src/components/endpoint/PlaygroundForm.astro +1 -1
  19. package/template/src/components/endpoint/RequestSnippets.astro +6 -1
  20. package/template/src/components/endpoint/ResponseFieldTree.astro +17 -13
  21. package/template/src/components/endpoint/ResponseFields.astro +4 -6
  22. package/template/src/components/endpoint/ResponseSnippets.astro +6 -1
  23. package/template/src/components/sidebar/SidebarEndpointLink.astro +9 -12
  24. package/template/src/components/sidebar/SidebarOpenApi.astro +3 -9
  25. package/template/src/components/ui/Field.astro +18 -15
  26. package/template/src/components/ui/Tag.astro +16 -2
  27. package/template/src/components/user/Accordion.astro +1 -1
  28. package/template/src/components/user/Callout.astro +2 -2
  29. package/template/src/components/user/CodeBlock.astro +58 -7
  30. package/template/src/components/user/CodeGroup.astro +52 -1
  31. package/template/src/components/user/Column.astro +1 -1
  32. package/template/src/components/user/Step.astro +1 -1
  33. package/template/src/components/user/Tabs.astro +1 -1
  34. package/template/src/generated/shiki-platform-assets.json +24 -0
  35. package/template/src/layouts/Layout.astro +111 -8
  36. package/template/src/lib/assistant-panel-config.ts +59 -0
  37. package/template/src/lib/assistant-shiki-client.ts +506 -0
  38. package/template/src/lib/mdx/remark-resolve-internal-links.ts +334 -17
  39. package/template/src/lib/routes.ts +66 -24
  40. package/template/src/lib/utils.ts +11 -0
  41. package/template/src/styles/global.css +12 -0
@@ -0,0 +1,1151 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ HeadObjectCommand,
4
+ PutObjectCommand,
5
+ S3Client,
6
+ } from "@aws-sdk/client-s3";
7
+ import crypto from "node:crypto";
8
+ import fs from "node:fs/promises";
9
+ import os from "node:os";
10
+ import path from "node:path";
11
+ import { fileURLToPath, pathToFileURL } from "node:url";
12
+ import { build as esbuild } from "esbuild";
13
+
14
+ const __filename = fileURLToPath(import.meta.url);
15
+ const __dirname = path.dirname(__filename);
16
+ const FRAMEWORK_DIR = path.resolve(__dirname, "..");
17
+ const NODE_MODULES_DIR = path.join(FRAMEWORK_DIR, "node_modules");
18
+ const CURRENT_ASSET_FILE = path.join(
19
+ FRAMEWORK_DIR,
20
+ "src",
21
+ "generated",
22
+ "shiki-platform-assets.json",
23
+ );
24
+ const LANGUAGE_DIST_DIR = path.join(
25
+ NODE_MODULES_DIR,
26
+ "@shikijs",
27
+ "langs",
28
+ "dist",
29
+ );
30
+ const THEME_DIST_DIR = path.join(
31
+ NODE_MODULES_DIR,
32
+ "@shikijs",
33
+ "themes",
34
+ "dist",
35
+ );
36
+ const DEFAULT_PLATFORM_PREFIX = "_platform/shiki";
37
+ const DEFAULT_PROGRESS_INTERVAL = 25;
38
+ const DEFAULT_R2_REQUEST_TIMEOUT_MS = 30_000;
39
+ const IMMUTABLE_CACHE_CONTROL = "public, max-age=31536000, immutable";
40
+ const MARKER_CACHE_CONTROL = "no-store";
41
+ const UPLOAD_COMPLETE_MARKER = ".radiant-upload-complete.json";
42
+ const UPLOAD_MARKER_VERSION = 1;
43
+
44
+ const LICENSE_PACKAGES = [
45
+ "shiki",
46
+ "@shikijs/core",
47
+ "@shikijs/engine-javascript",
48
+ "@shikijs/langs",
49
+ "@shikijs/themes",
50
+ "@shikijs/vscode-textmate",
51
+ "oniguruma-to-es",
52
+ ];
53
+
54
+ const RUNTIME_ENTRY_SOURCE = `
55
+ import {
56
+ createHighlighterCore,
57
+ createHighlighterCoreSync,
58
+ getSingletonHighlighterCore,
59
+ } from "@shikijs/core";
60
+ import { createJavaScriptRegexEngine } from "@shikijs/engine-javascript";
61
+
62
+ export {
63
+ createHighlighterCore,
64
+ createHighlighterCoreSync,
65
+ createJavaScriptRegexEngine,
66
+ getSingletonHighlighterCore,
67
+ };
68
+
69
+ export async function createRadiantShikiHighlighter(options = {}) {
70
+ const {
71
+ themes = [],
72
+ langs = [],
73
+ langAlias,
74
+ warnings = false,
75
+ } = options;
76
+
77
+ return createHighlighterCore({
78
+ themes,
79
+ langs,
80
+ langAlias,
81
+ warnings,
82
+ engine: createJavaScriptRegexEngine(),
83
+ });
84
+ }
85
+ `;
86
+
87
+ function findEnvFileArg(argv) {
88
+ for (let index = 0; index < argv.length; index += 1) {
89
+ if (argv[index] === "--env-file") {
90
+ const nextValue = argv[index + 1];
91
+ if (!nextValue || nextValue.startsWith("--")) {
92
+ throw new Error("Missing value for --env-file");
93
+ }
94
+ return nextValue;
95
+ }
96
+ }
97
+ return process.env.SHIKI_PLATFORM_ENV_FILE ?? "";
98
+ }
99
+
100
+ function parseEnvLine(line) {
101
+ const trimmed = line.trim();
102
+ if (!trimmed || trimmed.startsWith("#")) return null;
103
+
104
+ const normalized = trimmed.startsWith("export ")
105
+ ? trimmed.slice("export ".length).trim()
106
+ : trimmed;
107
+ const equalsIndex = normalized.indexOf("=");
108
+ if (equalsIndex === -1) return null;
109
+
110
+ const key = normalized.slice(0, equalsIndex).trim();
111
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) return null;
112
+
113
+ let value = normalized.slice(equalsIndex + 1).trim();
114
+ const quote = value[0];
115
+ if (
116
+ (quote === `"` || quote === "'") &&
117
+ value.length >= 2 &&
118
+ value[value.length - 1] === quote
119
+ ) {
120
+ value = value.slice(1, -1);
121
+ if (quote === `"`) {
122
+ value = value
123
+ .replaceAll("\\n", "\n")
124
+ .replaceAll("\\r", "\r")
125
+ .replaceAll("\\t", "\t")
126
+ .replaceAll('\\"', '"')
127
+ .replaceAll("\\\\", "\\");
128
+ }
129
+ } else {
130
+ value = value.replace(/\s+#.*$/, "").trim();
131
+ }
132
+
133
+ return [key, value];
134
+ }
135
+
136
+ async function loadEnvFile(envFile) {
137
+ if (!envFile) return null;
138
+
139
+ const resolved = path.resolve(process.cwd(), envFile);
140
+ const contents = await fs.readFile(resolved, "utf8");
141
+ let loadedCount = 0;
142
+
143
+ for (const line of contents.split(/\r?\n/)) {
144
+ const parsed = parseEnvLine(line);
145
+ if (!parsed) continue;
146
+ const [key, value] = parsed;
147
+ if (process.env[key] === undefined) {
148
+ process.env[key] = value;
149
+ loadedCount += 1;
150
+ }
151
+ }
152
+
153
+ return {
154
+ loadedCount,
155
+ path: resolved,
156
+ };
157
+ }
158
+
159
+ function readPositiveInteger(value, fallback) {
160
+ const parsed = Number.parseInt(String(value ?? ""), 10);
161
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
162
+ }
163
+
164
+ function parseArgs(argv) {
165
+ const args = {
166
+ accountId:
167
+ process.env.SHIKI_PLATFORM_R2_ACCOUNT_ID ??
168
+ process.env.R2_ACCOUNT_ID ??
169
+ process.env.CLOUDFLARE_ACCOUNT_ID ??
170
+ "",
171
+ accessKeyId:
172
+ process.env.SHIKI_PLATFORM_R2_ACCESS_KEY_ID ??
173
+ process.env.R2_ACCESS_KEY_ID ??
174
+ "",
175
+ assetVersion: process.env.SHIKI_PLATFORM_ASSET_VERSION ?? "",
176
+ bucket:
177
+ process.env.SHIKI_PLATFORM_R2_BUCKET_NAME ??
178
+ process.env.SHIKI_PLATFORM_R2_BUCKET ??
179
+ process.env.R2_BUCKET_NAME ??
180
+ "",
181
+ checkCurrent: false,
182
+ currentFile:
183
+ process.env.SHIKI_PLATFORM_CURRENT_FILE ?? CURRENT_ASSET_FILE,
184
+ dryRun: false,
185
+ endpoint:
186
+ process.env.SHIKI_PLATFORM_R2_ENDPOINT ?? process.env.R2_ENDPOINT ?? "",
187
+ envFile: process.env.SHIKI_PLATFORM_ENV_FILE ?? "",
188
+ keepOutput: false,
189
+ outDir: "",
190
+ platformPrefix:
191
+ process.env.SHIKI_PLATFORM_ASSET_PREFIX ?? DEFAULT_PLATFORM_PREFIX,
192
+ prepareOnly: false,
193
+ progressInterval: readPositiveInteger(
194
+ process.env.SHIKI_PLATFORM_UPLOAD_PROGRESS_INTERVAL,
195
+ DEFAULT_PROGRESS_INTERVAL,
196
+ ),
197
+ region:
198
+ process.env.SHIKI_PLATFORM_R2_REGION ?? process.env.R2_REGION ?? "auto",
199
+ requestTimeoutMs: readPositiveInteger(
200
+ process.env.SHIKI_PLATFORM_R2_REQUEST_TIMEOUT_MS,
201
+ DEFAULT_R2_REQUEST_TIMEOUT_MS,
202
+ ),
203
+ secretAccessKey:
204
+ process.env.SHIKI_PLATFORM_R2_SECRET_ACCESS_KEY ??
205
+ process.env.R2_SECRET_ACCESS_KEY ??
206
+ "",
207
+ writeCurrent: false,
208
+ };
209
+
210
+ for (let index = 0; index < argv.length; index += 1) {
211
+ const arg = argv[index];
212
+ if (arg === "--dry-run") {
213
+ args.dryRun = true;
214
+ continue;
215
+ }
216
+ if (arg === "--check-current") {
217
+ args.checkCurrent = true;
218
+ continue;
219
+ }
220
+ if (arg === "--keep-output") {
221
+ args.keepOutput = true;
222
+ continue;
223
+ }
224
+ if (arg === "--prepare-only") {
225
+ args.prepareOnly = true;
226
+ continue;
227
+ }
228
+ if (arg === "--write-current") {
229
+ args.writeCurrent = true;
230
+ continue;
231
+ }
232
+
233
+ const nextValue = argv[index + 1];
234
+ if (!nextValue || nextValue.startsWith("--")) {
235
+ throw new Error(`Missing value for ${arg}`);
236
+ }
237
+ index += 1;
238
+
239
+ switch (arg) {
240
+ case "--access-key-id":
241
+ args.accessKeyId = nextValue;
242
+ break;
243
+ case "--account-id":
244
+ args.accountId = nextValue;
245
+ break;
246
+ case "--asset-version":
247
+ args.assetVersion = nextValue;
248
+ break;
249
+ case "--bucket":
250
+ args.bucket = nextValue;
251
+ break;
252
+ case "--current-file":
253
+ args.currentFile = nextValue;
254
+ break;
255
+ case "--endpoint":
256
+ args.endpoint = nextValue;
257
+ break;
258
+ case "--env-file":
259
+ args.envFile = nextValue;
260
+ break;
261
+ case "--out-dir":
262
+ args.outDir = nextValue;
263
+ break;
264
+ case "--prefix":
265
+ args.platformPrefix = nextValue;
266
+ break;
267
+ case "--progress-interval":
268
+ args.progressInterval = readPositiveInteger(
269
+ nextValue,
270
+ DEFAULT_PROGRESS_INTERVAL,
271
+ );
272
+ break;
273
+ case "--region":
274
+ args.region = nextValue;
275
+ break;
276
+ case "--request-timeout-ms":
277
+ args.requestTimeoutMs = readPositiveInteger(
278
+ nextValue,
279
+ DEFAULT_R2_REQUEST_TIMEOUT_MS,
280
+ );
281
+ break;
282
+ case "--secret-access-key":
283
+ args.secretAccessKey = nextValue;
284
+ break;
285
+ default:
286
+ throw new Error(`Unknown option: ${arg}`);
287
+ }
288
+ }
289
+
290
+ if (args.checkCurrent && args.writeCurrent) {
291
+ throw new Error("Use either --check-current or --write-current, not both.");
292
+ }
293
+
294
+ return args;
295
+ }
296
+
297
+ function normalizePrefix(value) {
298
+ return value.trim().replace(/^\/+|\/+$/g, "");
299
+ }
300
+
301
+ function toPosixPath(value) {
302
+ return value.split(path.sep).join("/");
303
+ }
304
+
305
+ function joinPosix(...segments) {
306
+ return segments
307
+ .map((segment) => String(segment).trim().replace(/^\/+|\/+$/g, ""))
308
+ .filter(Boolean)
309
+ .join("/");
310
+ }
311
+
312
+ async function readJson(filePath) {
313
+ return JSON.parse(await fs.readFile(filePath, "utf8"));
314
+ }
315
+
316
+ function sortJsonValue(value) {
317
+ if (Array.isArray(value)) {
318
+ return value.map((item) => sortJsonValue(item));
319
+ }
320
+ if (value && typeof value === "object") {
321
+ return Object.fromEntries(
322
+ Object.entries(value)
323
+ .sort(([firstKey], [secondKey]) => firstKey.localeCompare(secondKey))
324
+ .map(([key, item]) => [key, sortJsonValue(item)]),
325
+ );
326
+ }
327
+ return value;
328
+ }
329
+
330
+ function formatJson(value) {
331
+ return `${JSON.stringify(sortJsonValue(value), null, 2)}\n`;
332
+ }
333
+
334
+ function canonicalJson(value) {
335
+ return JSON.stringify(sortJsonValue(value));
336
+ }
337
+
338
+ async function pathExists(filePath) {
339
+ try {
340
+ await fs.access(filePath);
341
+ return true;
342
+ } catch {
343
+ return false;
344
+ }
345
+ }
346
+
347
+ async function readSortedMjsFiles(dir) {
348
+ const entries = await fs.readdir(dir, { withFileTypes: true });
349
+ return entries
350
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".mjs"))
351
+ .map((entry) => entry.name)
352
+ .sort((a, b) => a.localeCompare(b));
353
+ }
354
+
355
+ async function copyFileEnsuringDir(source, destination) {
356
+ await fs.mkdir(path.dirname(destination), { recursive: true });
357
+ await fs.copyFile(source, destination);
358
+ }
359
+
360
+ function hashBuffer(buffer, algorithm = "sha256") {
361
+ return crypto.createHash(algorithm).update(buffer).digest("hex");
362
+ }
363
+
364
+ async function listFilesRecursive(dir) {
365
+ const entries = await fs.readdir(dir, { withFileTypes: true });
366
+ const files = [];
367
+
368
+ for (const entry of entries) {
369
+ const entryPath = path.join(dir, entry.name);
370
+ if (entry.isDirectory()) {
371
+ files.push(...(await listFilesRecursive(entryPath)));
372
+ } else if (entry.isFile()) {
373
+ files.push(entryPath);
374
+ }
375
+ }
376
+
377
+ return files.sort((a, b) => a.localeCompare(b));
378
+ }
379
+
380
+ async function assertSafeOutputDir(outDir) {
381
+ const resolved = path.resolve(outDir);
382
+ const forbiddenDirs = new Set([
383
+ path.parse(resolved).root,
384
+ os.homedir(),
385
+ FRAMEWORK_DIR,
386
+ path.join(FRAMEWORK_DIR, "scripts"),
387
+ process.cwd(),
388
+ ]);
389
+
390
+ if (forbiddenDirs.has(resolved)) {
391
+ throw new Error(`Refusing to clear unsafe output directory: ${resolved}`);
392
+ }
393
+ }
394
+
395
+ async function resetOutputDir(outDir) {
396
+ await assertSafeOutputDir(outDir);
397
+ await fs.rm(outDir, { force: true, recursive: true });
398
+ await fs.mkdir(outDir, { recursive: true });
399
+ }
400
+
401
+ async function buildRuntime(outDir) {
402
+ const tempEntryPath = path.join(outDir, ".runtime-entry.mjs");
403
+ const runtimePath = path.join(outDir, "runtime.mjs");
404
+
405
+ await fs.writeFile(tempEntryPath, RUNTIME_ENTRY_SOURCE, "utf8");
406
+ await esbuild({
407
+ absWorkingDir: FRAMEWORK_DIR,
408
+ bundle: true,
409
+ entryPoints: [tempEntryPath],
410
+ format: "esm",
411
+ logLevel: "silent",
412
+ minify: true,
413
+ nodePaths: [NODE_MODULES_DIR],
414
+ outfile: runtimePath,
415
+ platform: "browser",
416
+ target: "es2022",
417
+ treeShaking: true,
418
+ });
419
+ await fs.rm(tempEntryPath, { force: true });
420
+ }
421
+
422
+ function normalizeModuleDefault(value) {
423
+ const moduleDefault = value?.default ?? value;
424
+ if (Array.isArray(moduleDefault)) return moduleDefault;
425
+ return moduleDefault ? [moduleDefault] : [];
426
+ }
427
+
428
+ async function buildLanguageManifestEntries(languageFileNames) {
429
+ const languages = {};
430
+ const aliasCandidates = new Map();
431
+
432
+ for (const fileName of languageFileNames) {
433
+ const sourcePath = path.join(LANGUAGE_DIST_DIR, fileName);
434
+ const moduleUrl = pathToFileURL(sourcePath).href;
435
+ const moduleValue = await import(moduleUrl);
436
+ const registrations = normalizeModuleDefault(moduleValue);
437
+ const fileStem = fileName.replace(/\.mjs$/, "");
438
+ const registrationNames = registrations
439
+ .map((registration) => registration?.name)
440
+ .filter((name) => typeof name === "string" && name.trim().length > 0);
441
+ const canonicalName = registrationNames[0] ?? fileStem;
442
+ const aliases = new Set([fileStem, canonicalName]);
443
+
444
+ for (const registration of registrations) {
445
+ if (typeof registration?.name === "string") {
446
+ aliases.add(registration.name);
447
+ }
448
+ for (const alias of registration?.aliases ?? []) {
449
+ aliases.add(alias);
450
+ }
451
+ }
452
+
453
+ languages[fileStem] = {
454
+ aliases: Array.from(aliases).sort((a, b) => a.localeCompare(b)),
455
+ displayName:
456
+ typeof registrations[0]?.displayName === "string"
457
+ ? registrations[0].displayName
458
+ : canonicalName,
459
+ module: `langs/${fileName}`,
460
+ registrations: registrationNames,
461
+ };
462
+
463
+ for (const alias of aliases) {
464
+ const normalizedAlias = String(alias).trim().toLowerCase();
465
+ if (!normalizedAlias) continue;
466
+ const candidates = aliasCandidates.get(normalizedAlias) ?? [];
467
+ candidates.push({
468
+ canonicalName,
469
+ fileStem,
470
+ registrationNames,
471
+ });
472
+ aliasCandidates.set(normalizedAlias, candidates);
473
+ }
474
+ }
475
+
476
+ const languageAliases = {};
477
+ for (const [alias, candidates] of aliasCandidates) {
478
+ const preferredCandidate =
479
+ candidates.find(
480
+ (candidate) =>
481
+ candidate.fileStem.toLowerCase() ===
482
+ candidate.canonicalName.toLowerCase(),
483
+ ) ??
484
+ candidates.find((candidate) => candidate.fileStem.toLowerCase() === alias) ??
485
+ candidates.find(
486
+ (candidate) => candidate.canonicalName.toLowerCase() === alias,
487
+ ) ??
488
+ candidates[0];
489
+
490
+ if (preferredCandidate) {
491
+ languageAliases[alias] = preferredCandidate.fileStem;
492
+ }
493
+ }
494
+
495
+ return {
496
+ languageAliases: Object.fromEntries(
497
+ Object.entries(languageAliases).sort(([firstKey], [secondKey]) =>
498
+ firstKey.localeCompare(secondKey),
499
+ ),
500
+ ),
501
+ languages,
502
+ };
503
+ }
504
+
505
+ async function buildThemeManifestEntries(themeFileNames) {
506
+ const themes = {};
507
+
508
+ for (const fileName of themeFileNames) {
509
+ const sourcePath = path.join(THEME_DIST_DIR, fileName);
510
+ const moduleUrl = pathToFileURL(sourcePath).href;
511
+ const moduleValue = await import(moduleUrl);
512
+ const theme = moduleValue.default ?? moduleValue;
513
+ const fileStem = fileName.replace(/\.mjs$/, "");
514
+ const themeName =
515
+ typeof theme?.name === "string" && theme.name.trim().length > 0
516
+ ? theme.name
517
+ : fileStem;
518
+
519
+ themes[themeName] = {
520
+ displayName:
521
+ typeof theme?.displayName === "string" ? theme.displayName : themeName,
522
+ module: `themes/${fileName}`,
523
+ type: typeof theme?.type === "string" ? theme.type : undefined,
524
+ };
525
+ }
526
+
527
+ return themes;
528
+ }
529
+
530
+ async function copyLanguageModules(outDir) {
531
+ const fileNames = await readSortedMjsFiles(LANGUAGE_DIST_DIR);
532
+ for (const fileName of fileNames) {
533
+ await copyFileEnsuringDir(
534
+ path.join(LANGUAGE_DIST_DIR, fileName),
535
+ path.join(outDir, "langs", fileName),
536
+ );
537
+ }
538
+ return fileNames;
539
+ }
540
+
541
+ async function copyThemeModules(outDir) {
542
+ const fileNames = await readSortedMjsFiles(THEME_DIST_DIR);
543
+ for (const fileName of fileNames) {
544
+ await copyFileEnsuringDir(
545
+ path.join(THEME_DIST_DIR, fileName),
546
+ path.join(outDir, "themes", fileName),
547
+ );
548
+ }
549
+ return fileNames;
550
+ }
551
+
552
+ function licenseDestinationName(packageName, licensePath) {
553
+ const extension = path.extname(licensePath) || ".txt";
554
+ return `${packageName.replaceAll("/", "__")}${extension}`;
555
+ }
556
+
557
+ async function copyLicenses(outDir) {
558
+ const copiedLicenses = [];
559
+
560
+ for (const packageName of LICENSE_PACKAGES) {
561
+ const packageDir = path.join(NODE_MODULES_DIR, packageName);
562
+ const entries = await fs.readdir(packageDir, { withFileTypes: true });
563
+ const licenseEntry = entries.find(
564
+ (entry) => entry.isFile() && /^license(?:\..*)?$/i.test(entry.name),
565
+ );
566
+ if (!licenseEntry) continue;
567
+
568
+ const sourcePath = path.join(packageDir, licenseEntry.name);
569
+ const destinationName = licenseDestinationName(
570
+ packageName,
571
+ licenseEntry.name,
572
+ );
573
+ await copyFileEnsuringDir(
574
+ sourcePath,
575
+ path.join(outDir, "licenses", destinationName),
576
+ );
577
+ copiedLicenses.push({
578
+ module: `licenses/${destinationName}`,
579
+ package: packageName,
580
+ });
581
+ }
582
+
583
+ return copiedLicenses.sort((a, b) => a.package.localeCompare(b.package));
584
+ }
585
+
586
+ async function buildFileManifest(outDir) {
587
+ const absoluteFiles = await listFilesRecursive(outDir);
588
+ const files = {};
589
+
590
+ for (const absolutePath of absoluteFiles) {
591
+ const relativePath = toPosixPath(path.relative(outDir, absolutePath));
592
+ if (relativePath === "manifest.json") continue;
593
+
594
+ const buffer = await fs.readFile(absolutePath);
595
+ files[relativePath] = {
596
+ bytes: buffer.byteLength,
597
+ sha256: hashBuffer(buffer),
598
+ };
599
+ }
600
+
601
+ return Object.fromEntries(
602
+ Object.entries(files).sort(([firstKey], [secondKey]) =>
603
+ firstKey.localeCompare(secondKey),
604
+ ),
605
+ );
606
+ }
607
+
608
+ function buildAssetFingerprint(manifestBase) {
609
+ return crypto
610
+ .createHash("sha256")
611
+ .update(JSON.stringify(manifestBase))
612
+ .digest("hex")
613
+ .slice(0, 12);
614
+ }
615
+
616
+ function buildCurrentAssetDescriptor({
617
+ manifest,
618
+ manifestBytes,
619
+ manifestSha256,
620
+ platformPrefix,
621
+ }) {
622
+ return {
623
+ assetVersion: manifest.assetVersion,
624
+ counts: {
625
+ languages: Object.keys(manifest.languages).length,
626
+ themes: Object.keys(manifest.themes).length,
627
+ },
628
+ manifest: {
629
+ bytes: manifestBytes,
630
+ module: "manifest.json",
631
+ sha256: manifestSha256,
632
+ },
633
+ packageVersions: manifest.packageVersions,
634
+ prefix: platformPrefix,
635
+ runtime: manifest.runtime,
636
+ };
637
+ }
638
+
639
+ async function writeCurrentAssetFile(filePath, descriptor) {
640
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
641
+ await fs.writeFile(filePath, formatJson(descriptor), "utf8");
642
+ }
643
+
644
+ function describeCurrentDescriptor(descriptor) {
645
+ const packageVersions = Object.entries(descriptor.packageVersions)
646
+ .map(([name, version]) => `${name}@${version}`)
647
+ .join(", ");
648
+
649
+ return [
650
+ `assetVersion=${descriptor.assetVersion}`,
651
+ `prefix=${descriptor.prefix}`,
652
+ `packages=${packageVersions}`,
653
+ `manifestSha256=${descriptor.manifest.sha256}`,
654
+ ].join("; ");
655
+ }
656
+
657
+ async function checkCurrentAssetFile(filePath, descriptor) {
658
+ if (!(await pathExists(filePath))) {
659
+ throw new Error(
660
+ `Current Shiki platform asset file is missing: ${filePath}\n` +
661
+ "Run: npm --prefix framework run shiki:publish-platform-assets -- --prepare-only --write-current",
662
+ );
663
+ }
664
+
665
+ const current = await readJson(filePath);
666
+ if (canonicalJson(current) === canonicalJson(descriptor)) {
667
+ return;
668
+ }
669
+
670
+ throw new Error(
671
+ `Current Shiki platform asset file is out of date: ${filePath}\n` +
672
+ `Expected: ${describeCurrentDescriptor(descriptor)}\n` +
673
+ `Found: ${describeCurrentDescriptor(current)}\n` +
674
+ "Run: npm --prefix framework run shiki:publish-platform-assets -- --prepare-only --write-current",
675
+ );
676
+ }
677
+
678
+ async function prepareAssets({ assetVersionOverride, outDir }) {
679
+ await resetOutputDir(outDir);
680
+
681
+ const shikiPackage = await readJson(
682
+ path.join(NODE_MODULES_DIR, "shiki", "package.json"),
683
+ );
684
+ const corePackage = await readJson(
685
+ path.join(NODE_MODULES_DIR, "@shikijs", "core", "package.json"),
686
+ );
687
+ const enginePackage = await readJson(
688
+ path.join(
689
+ NODE_MODULES_DIR,
690
+ "@shikijs",
691
+ "engine-javascript",
692
+ "package.json",
693
+ ),
694
+ );
695
+ const languagePackage = await readJson(
696
+ path.join(NODE_MODULES_DIR, "@shikijs", "langs", "package.json"),
697
+ );
698
+ const themePackage = await readJson(
699
+ path.join(NODE_MODULES_DIR, "@shikijs", "themes", "package.json"),
700
+ );
701
+
702
+ await buildRuntime(outDir);
703
+ const languageFileNames = await copyLanguageModules(outDir);
704
+ const themeFileNames = await copyThemeModules(outDir);
705
+ const licenses = await copyLicenses(outDir);
706
+ const files = await buildFileManifest(outDir);
707
+ const languageManifest = await buildLanguageManifestEntries(languageFileNames);
708
+ const themes = await buildThemeManifestEntries(themeFileNames);
709
+ const manifestBase = {
710
+ files,
711
+ languageAliases: languageManifest.languageAliases,
712
+ languages: languageManifest.languages,
713
+ licenses,
714
+ packageVersions: {
715
+ "@shikijs/core": corePackage.version,
716
+ "@shikijs/engine-javascript": enginePackage.version,
717
+ "@shikijs/langs": languagePackage.version,
718
+ "@shikijs/themes": themePackage.version,
719
+ shiki: shikiPackage.version,
720
+ },
721
+ runtime: {
722
+ engine: "javascript-regexp",
723
+ module: "runtime.mjs",
724
+ },
725
+ themes,
726
+ };
727
+ const fingerprint = buildAssetFingerprint(manifestBase);
728
+ const assetVersion =
729
+ assetVersionOverride.trim() ||
730
+ `shiki-${shikiPackage.version}-${fingerprint}`;
731
+ const manifest = {
732
+ assetVersion,
733
+ ...manifestBase,
734
+ };
735
+ const manifestJson = `${JSON.stringify(manifest, null, 2)}\n`;
736
+ const manifestBuffer = Buffer.from(manifestJson);
737
+
738
+ await fs.writeFile(
739
+ path.join(outDir, "manifest.json"),
740
+ manifestJson,
741
+ "utf8",
742
+ );
743
+
744
+ return {
745
+ assetVersion,
746
+ files: await buildFileManifest(outDir),
747
+ languageCount: languageFileNames.length,
748
+ manifest,
749
+ manifestBytes: manifestBuffer.byteLength,
750
+ manifestSha256: hashBuffer(manifestBuffer),
751
+ outDir,
752
+ shikiVersion: shikiPackage.version,
753
+ themeCount: themeFileNames.length,
754
+ uploadFileCount: (await listFilesRecursive(outDir)).length,
755
+ };
756
+ }
757
+
758
+ function getContentType(relativePath) {
759
+ if (relativePath.endsWith(".mjs")) return "text/javascript; charset=utf-8";
760
+ if (relativePath.endsWith(".json")) return "application/json; charset=utf-8";
761
+ if (relativePath.endsWith(".md")) return "text/markdown; charset=utf-8";
762
+ return "text/plain; charset=utf-8";
763
+ }
764
+
765
+ function createR2Client(args) {
766
+ const endpoint =
767
+ args.endpoint ||
768
+ (args.accountId
769
+ ? `https://${args.accountId}.r2.cloudflarestorage.com`
770
+ : "");
771
+ if (!endpoint) {
772
+ throw new Error("Missing R2 endpoint. Set R2_ACCOUNT_ID or R2_ENDPOINT.");
773
+ }
774
+ if (!args.accessKeyId || !args.secretAccessKey) {
775
+ throw new Error(
776
+ "Missing R2 credentials. Set R2_ACCESS_KEY_ID and R2_SECRET_ACCESS_KEY.",
777
+ );
778
+ }
779
+
780
+ return new S3Client({
781
+ credentials: {
782
+ accessKeyId: args.accessKeyId,
783
+ secretAccessKey: args.secretAccessKey,
784
+ },
785
+ endpoint,
786
+ maxAttempts: 2,
787
+ region: args.region,
788
+ });
789
+ }
790
+
791
+ async function sendR2CommandWithTimeout(s3, command, { label, timeoutMs }) {
792
+ const controller = new AbortController();
793
+ const timeout = setTimeout(() => {
794
+ controller.abort();
795
+ }, timeoutMs);
796
+
797
+ try {
798
+ return await s3.send(command, {
799
+ abortSignal: controller.signal,
800
+ });
801
+ } catch (error) {
802
+ if (error?.name === "AbortError") {
803
+ throw new Error(`Timed out after ${timeoutMs}ms while ${label}.`);
804
+ }
805
+ throw error;
806
+ } finally {
807
+ clearTimeout(timeout);
808
+ }
809
+ }
810
+
811
+ async function objectHasSameHash(s3, bucket, key, sha256, requestTimeoutMs) {
812
+ try {
813
+ const response = await sendR2CommandWithTimeout(
814
+ s3,
815
+ new HeadObjectCommand({
816
+ Bucket: bucket,
817
+ Key: key,
818
+ }),
819
+ {
820
+ label: `checking ${bucket}/${key}`,
821
+ timeoutMs: requestTimeoutMs,
822
+ },
823
+ );
824
+ return response.Metadata?.["radiant-sha256"] === sha256;
825
+ } catch (error) {
826
+ const statusCode = error?.$metadata?.httpStatusCode;
827
+ if (statusCode === 403 || statusCode === 404) return false;
828
+ throw error;
829
+ }
830
+ }
831
+
832
+ async function completionMarkerMatches({
833
+ assetVersion,
834
+ bucket,
835
+ fileCount,
836
+ key,
837
+ manifestSha256,
838
+ requestTimeoutMs,
839
+ s3,
840
+ }) {
841
+ try {
842
+ const response = await sendR2CommandWithTimeout(
843
+ s3,
844
+ new HeadObjectCommand({
845
+ Bucket: bucket,
846
+ Key: key,
847
+ }),
848
+ {
849
+ label: `checking completion marker ${bucket}/${key}`,
850
+ timeoutMs: requestTimeoutMs,
851
+ },
852
+ );
853
+ const metadata = response.Metadata ?? {};
854
+
855
+ return (
856
+ metadata["radiant-shiki-asset-version"] === assetVersion &&
857
+ metadata["radiant-shiki-file-count"] === String(fileCount) &&
858
+ metadata["radiant-shiki-manifest-sha256"] === manifestSha256 &&
859
+ metadata["radiant-shiki-marker-version"] ===
860
+ String(UPLOAD_MARKER_VERSION)
861
+ );
862
+ } catch (error) {
863
+ const statusCode = error?.$metadata?.httpStatusCode;
864
+ if (statusCode === 403 || statusCode === 404) return false;
865
+ throw error;
866
+ }
867
+ }
868
+
869
+ function buildCompletionMarker({
870
+ assetVersion,
871
+ fileCount,
872
+ manifestBytes,
873
+ manifestSha256,
874
+ uploadPrefix,
875
+ }) {
876
+ return {
877
+ assetVersion,
878
+ completedAt: new Date().toISOString(),
879
+ fileCount,
880
+ manifest: {
881
+ bytes: manifestBytes,
882
+ module: "manifest.json",
883
+ sha256: manifestSha256,
884
+ },
885
+ markerVersion: UPLOAD_MARKER_VERSION,
886
+ prefix: uploadPrefix,
887
+ };
888
+ }
889
+
890
+ async function uploadCompletionMarker({
891
+ assetVersion,
892
+ bucket,
893
+ fileCount,
894
+ key,
895
+ manifestBytes,
896
+ manifestSha256,
897
+ requestTimeoutMs,
898
+ s3,
899
+ uploadPrefix,
900
+ }) {
901
+ const marker = buildCompletionMarker({
902
+ assetVersion,
903
+ fileCount,
904
+ manifestBytes,
905
+ manifestSha256,
906
+ uploadPrefix,
907
+ });
908
+ const body = Buffer.from(formatJson(marker));
909
+
910
+ await sendR2CommandWithTimeout(
911
+ s3,
912
+ new PutObjectCommand({
913
+ Body: body,
914
+ Bucket: bucket,
915
+ CacheControl: MARKER_CACHE_CONTROL,
916
+ ContentType: "application/json; charset=utf-8",
917
+ Key: key,
918
+ Metadata: {
919
+ "radiant-sha256": hashBuffer(body),
920
+ "radiant-shiki-asset-version": assetVersion,
921
+ "radiant-shiki-file-count": String(fileCount),
922
+ "radiant-shiki-manifest-sha256": manifestSha256,
923
+ "radiant-shiki-marker-version": String(UPLOAD_MARKER_VERSION),
924
+ },
925
+ }),
926
+ {
927
+ label: `uploading completion marker ${bucket}/${key}`,
928
+ timeoutMs: requestTimeoutMs,
929
+ },
930
+ );
931
+ }
932
+
933
+ async function uploadAssets({
934
+ args,
935
+ assetVersion,
936
+ fileCount,
937
+ manifestBytes,
938
+ manifestSha256,
939
+ outDir,
940
+ }) {
941
+ if (!args.bucket) {
942
+ throw new Error("Missing R2 bucket. Set R2_BUCKET_NAME or pass --bucket.");
943
+ }
944
+
945
+ const s3 = createR2Client(args);
946
+ try {
947
+ const absoluteFiles = await listFilesRecursive(outDir);
948
+ const platformPrefix = normalizePrefix(args.platformPrefix);
949
+ const uploadPrefix = joinPosix(platformPrefix, assetVersion);
950
+ const markerKey = joinPosix(uploadPrefix, UPLOAD_COMPLETE_MARKER);
951
+ const stats = {
952
+ checked: 0,
953
+ marker: "missing",
954
+ uploaded: 0,
955
+ skipped: 0,
956
+ };
957
+ const total = fileCount;
958
+
959
+ if (
960
+ await completionMarkerMatches({
961
+ assetVersion,
962
+ bucket: args.bucket,
963
+ fileCount,
964
+ key: markerKey,
965
+ manifestSha256,
966
+ requestTimeoutMs: args.requestTimeoutMs,
967
+ s3,
968
+ })
969
+ ) {
970
+ console.log(
971
+ `Completion marker matches; skipping ${total} asset checks for ${args.bucket}/${uploadPrefix}`,
972
+ );
973
+
974
+ return {
975
+ ...stats,
976
+ marker: "matched",
977
+ prefix: uploadPrefix,
978
+ skipped: total,
979
+ };
980
+ }
981
+
982
+ console.log(`Checking/uploading ${total} files to ${args.bucket}/${uploadPrefix}`);
983
+
984
+ for (const absolutePath of absoluteFiles) {
985
+ const relativePath = toPosixPath(path.relative(outDir, absolutePath));
986
+ const body = await fs.readFile(absolutePath);
987
+ const sha256 = hashBuffer(body);
988
+ const key = joinPosix(uploadPrefix, relativePath);
989
+ const shouldSkip = await objectHasSameHash(
990
+ s3,
991
+ args.bucket,
992
+ key,
993
+ sha256,
994
+ args.requestTimeoutMs,
995
+ );
996
+ stats.checked += 1;
997
+
998
+ if (shouldSkip) {
999
+ stats.skipped += 1;
1000
+ if (
1001
+ stats.checked % args.progressInterval === 0 ||
1002
+ stats.checked === total
1003
+ ) {
1004
+ console.log(
1005
+ `Progress: ${stats.checked}/${total}; uploaded: ${stats.uploaded}; skipped: ${stats.skipped}`,
1006
+ );
1007
+ }
1008
+ continue;
1009
+ }
1010
+
1011
+ await sendR2CommandWithTimeout(
1012
+ s3,
1013
+ new PutObjectCommand({
1014
+ Body: body,
1015
+ Bucket: args.bucket,
1016
+ CacheControl: IMMUTABLE_CACHE_CONTROL,
1017
+ ContentType: getContentType(relativePath),
1018
+ Key: key,
1019
+ Metadata: {
1020
+ "radiant-sha256": sha256,
1021
+ "radiant-shiki-asset-version": assetVersion,
1022
+ },
1023
+ }),
1024
+ {
1025
+ label: `uploading ${args.bucket}/${key}`,
1026
+ timeoutMs: args.requestTimeoutMs,
1027
+ },
1028
+ );
1029
+ stats.uploaded += 1;
1030
+ if (
1031
+ stats.checked % args.progressInterval === 0 ||
1032
+ stats.checked === total
1033
+ ) {
1034
+ console.log(
1035
+ `Progress: ${stats.checked}/${total}; uploaded: ${stats.uploaded}; skipped: ${stats.skipped}`,
1036
+ );
1037
+ }
1038
+ }
1039
+
1040
+ await uploadCompletionMarker({
1041
+ assetVersion,
1042
+ bucket: args.bucket,
1043
+ fileCount,
1044
+ key: markerKey,
1045
+ manifestBytes,
1046
+ manifestSha256,
1047
+ requestTimeoutMs: args.requestTimeoutMs,
1048
+ s3,
1049
+ uploadPrefix,
1050
+ });
1051
+ stats.marker = "uploaded";
1052
+
1053
+ return {
1054
+ ...stats,
1055
+ prefix: uploadPrefix,
1056
+ };
1057
+ } finally {
1058
+ s3.destroy();
1059
+ }
1060
+ }
1061
+
1062
+ async function main() {
1063
+ const argv = process.argv.slice(2);
1064
+ const loadedEnvFile = await loadEnvFile(findEnvFileArg(argv));
1065
+ if (loadedEnvFile) {
1066
+ console.log(
1067
+ `Loaded ${loadedEnvFile.loadedCount} environment variables from ${loadedEnvFile.path}`,
1068
+ );
1069
+ }
1070
+
1071
+ const args = parseArgs(argv);
1072
+ const outDir = path.resolve(
1073
+ args.outDir ||
1074
+ path.join(
1075
+ os.tmpdir(),
1076
+ `radiant-shiki-platform-assets-${process.pid}-${Date.now()}`,
1077
+ ),
1078
+ );
1079
+
1080
+ const prepared = await prepareAssets({
1081
+ assetVersionOverride: args.assetVersion,
1082
+ outDir,
1083
+ });
1084
+ const platformPrefix = normalizePrefix(args.platformPrefix);
1085
+ const uploadPrefix = joinPosix(platformPrefix, prepared.assetVersion);
1086
+ const currentDescriptor = buildCurrentAssetDescriptor({
1087
+ manifest: prepared.manifest,
1088
+ manifestBytes: prepared.manifestBytes,
1089
+ manifestSha256: prepared.manifestSha256,
1090
+ platformPrefix,
1091
+ });
1092
+
1093
+ console.log(
1094
+ `Prepared Shiki platform assets ${prepared.assetVersion} in ${prepared.outDir}`,
1095
+ );
1096
+ console.log(
1097
+ `Runtime: runtime.mjs; languages: ${prepared.languageCount}; themes: ${prepared.themeCount}`,
1098
+ );
1099
+
1100
+ if (args.writeCurrent) {
1101
+ await writeCurrentAssetFile(path.resolve(args.currentFile), currentDescriptor);
1102
+ console.log(
1103
+ `Wrote current Shiki platform asset file to ${path.resolve(args.currentFile)}`,
1104
+ );
1105
+ }
1106
+
1107
+ if (args.checkCurrent) {
1108
+ await checkCurrentAssetFile(path.resolve(args.currentFile), currentDescriptor);
1109
+ console.log("Current Shiki platform asset file is up to date.");
1110
+ }
1111
+
1112
+ if (args.prepareOnly) {
1113
+ console.log("Prepare-only mode; no upload attempted.");
1114
+ return;
1115
+ }
1116
+
1117
+ if (args.dryRun) {
1118
+ console.log(
1119
+ `Dry run; would upload/check ${prepared.uploadFileCount} asset files plus completion marker at ${args.bucket || "<bucket>"}/${uploadPrefix}`,
1120
+ );
1121
+ if (!args.keepOutput && !args.outDir && (await pathExists(outDir))) {
1122
+ await fs.rm(outDir, { force: true, recursive: true });
1123
+ }
1124
+ return;
1125
+ }
1126
+
1127
+ const uploadStats = await uploadAssets({
1128
+ args,
1129
+ assetVersion: prepared.assetVersion,
1130
+ fileCount: prepared.uploadFileCount,
1131
+ manifestBytes: prepared.manifestBytes,
1132
+ manifestSha256: prepared.manifestSha256,
1133
+ outDir,
1134
+ });
1135
+
1136
+ console.log(
1137
+ `Uploaded Shiki platform assets to ${args.bucket}/${uploadStats.prefix}`,
1138
+ );
1139
+ console.log(
1140
+ `Uploaded: ${uploadStats.uploaded}; skipped: ${uploadStats.skipped}; marker: ${uploadStats.marker}`,
1141
+ );
1142
+
1143
+ if (!args.keepOutput && !args.outDir && (await pathExists(outDir))) {
1144
+ await fs.rm(outDir, { force: true, recursive: true });
1145
+ }
1146
+ }
1147
+
1148
+ main().catch((error) => {
1149
+ console.error(error instanceof Error ? error.message : String(error));
1150
+ process.exit(1);
1151
+ });