@riebeckite/honox 0.0.21 → 0.0.23

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 CHANGED
@@ -1,18 +1,42 @@
1
1
  // src/config_loader.ts
2
- import fs2 from "node:fs/promises";
3
- import path3 from "node:path";
2
+ import fs3 from "node:fs/promises";
3
+ import path4 from "node:path";
4
4
  import { build as buildWithEsbuild } from "esbuild";
5
5
 
6
- // src/workspace_packages.ts
6
+ // src/site_externals.ts
7
7
  import fs from "node:fs";
8
8
  import path from "node:path";
9
+ function siteRiebeckiteExternals(siteRoot) {
10
+ const packageJsonPath = path.join(siteRoot, "package.json");
11
+ let contents;
12
+ try {
13
+ contents = fs.readFileSync(packageJsonPath, "utf8");
14
+ } catch {
15
+ return [];
16
+ }
17
+ let packageJson;
18
+ try {
19
+ packageJson = JSON.parse(contents);
20
+ } catch {
21
+ return [];
22
+ }
23
+ const dependencies = packageJson.dependencies;
24
+ if (!dependencies || typeof dependencies !== "object") return [];
25
+ return Object.keys(dependencies).filter(
26
+ (name) => name === "@riebeckite/core" || name.startsWith("@riebeckite/plugin-") || name.startsWith("@riebeckite/theme-")
27
+ );
28
+ }
29
+
30
+ // src/workspace_packages.ts
31
+ import fs2 from "node:fs";
32
+ import path2 from "node:path";
9
33
  function createWorkspacePackageAliases(workspaceRoot) {
10
- const packagesRoot = path.join(workspaceRoot, "packages");
11
- if (!fs.existsSync(packagesRoot)) return [];
34
+ const packagesRoot = path2.join(workspaceRoot, "packages");
35
+ if (!fs2.existsSync(packagesRoot)) return [];
12
36
  return findWorkspacePackageDirectories(packagesRoot).flatMap(
13
37
  (packageDirectory) => {
14
- const packageJsonPath = path.join(packageDirectory, "package.json");
15
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
38
+ const packageJsonPath = path2.join(packageDirectory, "package.json");
39
+ const packageJson = JSON.parse(fs2.readFileSync(packageJsonPath, "utf8"));
16
40
  if (typeof packageJson.name !== "string") return [];
17
41
  return createPackageAliases(
18
42
  packageJson.name,
@@ -37,10 +61,10 @@ function workspacePackageResolver(workspaceRoot) {
37
61
  };
38
62
  }
39
63
  function findWorkspacePackageDirectories(rootDirectory) {
40
- const packageJsonPath = path.join(rootDirectory, "package.json");
41
- if (fs.existsSync(packageJsonPath)) return [rootDirectory];
42
- return fs.readdirSync(rootDirectory, { withFileTypes: true }).filter((entry) => entry.isDirectory()).flatMap(
43
- (entry) => findWorkspacePackageDirectories(path.join(rootDirectory, entry.name))
64
+ const packageJsonPath = path2.join(rootDirectory, "package.json");
65
+ if (fs2.existsSync(packageJsonPath)) return [rootDirectory];
66
+ return fs2.readdirSync(rootDirectory, { withFileTypes: true }).filter((entry) => entry.isDirectory()).flatMap(
67
+ (entry) => findWorkspacePackageDirectories(path2.join(rootDirectory, entry.name))
44
68
  );
45
69
  }
46
70
  function createPackageAliases(packageName, packageDirectory, packageJson) {
@@ -49,7 +73,7 @@ function createPackageAliases(packageName, packageDirectory, packageJson) {
49
73
  if (mainEntry) {
50
74
  aliases.push({
51
75
  find: new RegExp(`^${escapeRegExp(packageName)}$`),
52
- replacement: path.join(packageDirectory, mainEntry)
76
+ replacement: path2.join(packageDirectory, mainEntry)
53
77
  });
54
78
  }
55
79
  if (!packageJson.exports || typeof packageJson.exports !== "object") {
@@ -61,7 +85,7 @@ function createPackageAliases(packageName, packageDirectory, packageJson) {
61
85
  if (!resolvedTarget) continue;
62
86
  aliases.push({
63
87
  find: new RegExp(`^${escapeRegExp(packageName + subpath.slice(1))}$`),
64
- replacement: path.join(packageDirectory, resolvedTarget)
88
+ replacement: path2.join(packageDirectory, resolvedTarget)
65
89
  });
66
90
  }
67
91
  return aliases;
@@ -108,15 +132,15 @@ function escapeRegExp(value) {
108
132
  }
109
133
 
110
134
  // src/resolved_config.ts
111
- import path2 from "node:path";
135
+ import path3 from "node:path";
112
136
  function resolveHonoxConfig(config, appRoot) {
113
137
  return {
114
138
  ...config,
115
- buildDirectory: path2.join(appRoot, ".riebeckite"),
116
- outputDirectory: path2.join(appRoot, "dist"),
139
+ buildDirectory: path3.join(appRoot, ".riebeckite"),
140
+ outputDirectory: path3.join(appRoot, "dist"),
117
141
  content: {
118
142
  ...config.content,
119
- directory: path2.resolve(appRoot, config.content.directory)
143
+ directory: path3.resolve(appRoot, config.content.directory)
120
144
  }
121
145
  };
122
146
  }
@@ -124,15 +148,15 @@ function resolveHonoxConfig(config, appRoot) {
124
148
  // src/config_loader.ts
125
149
  async function loadRiebeckiteConfig(options) {
126
150
  const configFile = options.configFile ?? "riebeckite.config.ts";
127
- const temporaryDirectory = await fs2.mkdtemp(
128
- path3.join(options.configRoot, ".riebeckite-config-")
151
+ const temporaryDirectory = await fs3.mkdtemp(
152
+ path4.join(options.configRoot, ".riebeckite-config-")
129
153
  );
130
- const outputFile = path3.join(temporaryDirectory, "config.mjs");
154
+ const outputFile = path4.join(temporaryDirectory, "config.mjs");
131
155
  try {
132
156
  await buildWithEsbuild({
133
157
  stdin: {
134
158
  contents: `
135
- import rawConfig from ${JSON.stringify(path3.resolve(options.configRoot, configFile))};
159
+ import rawConfig from ${JSON.stringify(path4.resolve(options.configRoot, configFile))};
136
160
  import { resolveConfigModule } from "@riebeckite/core";
137
161
  export default resolveConfigModule(rawConfig);
138
162
  `,
@@ -143,6 +167,7 @@ async function loadRiebeckiteConfig(options) {
143
167
  bundle: true,
144
168
  platform: "node",
145
169
  format: "esm",
170
+ external: options.workspaceRoot ? [] : ["@riebeckite/core", ...siteRiebeckiteExternals(options.configRoot)],
146
171
  banner: {
147
172
  js: 'import { createRequire as __riebeckiteCreateRequire } from "node:module"; const require = __riebeckiteCreateRequire(import.meta.url);'
148
173
  },
@@ -151,7 +176,7 @@ async function loadRiebeckiteConfig(options) {
151
176
  const module = await import(`${pathToFileUrl(outputFile)}?t=${Date.now()}`);
152
177
  return module.default;
153
178
  } finally {
154
- await fs2.rm(temporaryDirectory, { recursive: true, force: true });
179
+ await fs3.rm(temporaryDirectory, { recursive: true, force: true });
155
180
  }
156
181
  }
157
182
  function pathToFileUrl(filePath) {
@@ -159,8 +184,8 @@ function pathToFileUrl(filePath) {
159
184
  }
160
185
 
161
186
  // src/honox_islands.ts
162
- import fs3 from "node:fs/promises";
163
- import path4 from "node:path";
187
+ import fs4 from "node:fs/promises";
188
+ import path5 from "node:path";
164
189
  import { parse } from "@babel/parser";
165
190
  var honoxIslandDependencyPluginName = "inject-importing-islands";
166
191
  var importingIslandsExport = "__importing_islands";
@@ -187,8 +212,8 @@ function scopedIslandDependencyPlugin() {
187
212
  return {
188
213
  name: "riebeckite-inject-importing-islands",
189
214
  configResolved(config) {
190
- appDirectory = path4.resolve(config.root, "app");
191
- islandDirectory = path4.join(appDirectory, "islands");
215
+ appDirectory = path5.resolve(config.root, "app");
216
+ islandDirectory = path5.join(appDirectory, "islands");
192
217
  },
193
218
  async transform(sourceCode, id) {
194
219
  const modulePath = toFilePath(id);
@@ -222,7 +247,7 @@ export const ${importingIslandsExport} = true;
222
247
  continue;
223
248
  }
224
249
  try {
225
- const dependencySourceCode = await fs3.readFile(
250
+ const dependencySourceCode = await fs4.readFile(
226
251
  dependencyPath,
227
252
  "utf8"
228
253
  );
@@ -251,8 +276,8 @@ function toFilePath(id) {
251
276
  return id.split("?", 1)[0] ?? id;
252
277
  }
253
278
  function isInDirectory(filePath, directory) {
254
- const relativePath = path4.relative(directory, filePath);
255
- return relativePath !== "" && !relativePath.startsWith(`..${path4.sep}`) && !path4.isAbsolute(relativePath);
279
+ const relativePath = path5.relative(directory, filePath);
280
+ return relativePath !== "" && !relativePath.startsWith(`..${path5.sep}`) && !path5.isAbsolute(relativePath);
256
281
  }
257
282
  function isJavaScriptModule(filePath) {
258
283
  return /\.[cm]?[jt]sx?$/.test(filePath);
@@ -260,7 +285,10 @@ function isJavaScriptModule(filePath) {
260
285
 
261
286
  // src/ssg_plugin.ts
262
287
  import { createHash, randomUUID } from "node:crypto";
288
+ import { createReadStream } from "node:fs";
263
289
  import {
290
+ copyFile,
291
+ lstat,
264
292
  mkdir,
265
293
  readdir,
266
294
  readFile,
@@ -271,14 +299,14 @@ import {
271
299
  } from "node:fs/promises";
272
300
  import { dirname, join, relative, resolve } from "node:path";
273
301
  import { performance } from "node:perf_hooks";
274
- import { defaultExtensionMap, toSSG } from "hono/ssg";
302
+ import { defaultExtensionMap, redirectPlugin, toSSG } from "hono/ssg";
275
303
  import {
276
304
  createServer
277
305
  } from "vite";
278
306
 
279
307
  // src/content_assets.ts
280
- import fs4 from "node:fs/promises";
281
- import path6 from "node:path";
308
+ import fs5 from "node:fs/promises";
309
+ import path7 from "node:path";
282
310
  import {
283
311
  isIgnoredContentPath as isIgnoredContentPath2,
284
312
  isImagePath as isImagePath2,
@@ -286,7 +314,7 @@ import {
286
314
  } from "@riebeckite/core";
287
315
 
288
316
  // src/content_watch.ts
289
- import path5 from "node:path";
317
+ import path6 from "node:path";
290
318
  import {
291
319
  isAttachmentPath,
292
320
  isIgnoredContentPath,
@@ -407,13 +435,13 @@ function isPathInsideDirectory(directory, file) {
407
435
  }
408
436
  function toLogicalPath(directory, file) {
409
437
  return normalizeContentPath(
410
- path5.relative(normalizeContentPath(directory), normalizeContentPath(file))
438
+ path6.relative(normalizeContentPath(directory), normalizeContentPath(file))
411
439
  );
412
440
  }
413
441
  function invalidateApplicationRuntime(server, appRoot) {
414
442
  const moduleGraph = server.environments?.ssr?.moduleGraph;
415
443
  if (!moduleGraph) return 0;
416
- const appDirectory = path5.join(appRoot, "app") + path5.sep;
444
+ const appDirectory = path6.join(appRoot, "app") + path6.sep;
417
445
  const seen = /* @__PURE__ */ new Set();
418
446
  const timestamp = Date.now();
419
447
  let invalidated = 0;
@@ -462,7 +490,7 @@ function riebeckiteContentAssets(options) {
462
490
  enforce: "pre",
463
491
  configureServer(server) {
464
492
  const contentRoot = options.contentRoot();
465
- const entry = path6.join(options.appRoot(), "app", "server.ts");
493
+ const entry = path7.join(options.appRoot(), "app", "server.ts");
466
494
  const base = server.config.base;
467
495
  const publicDir = server.config.publicDir;
468
496
  let cachedManifest;
@@ -502,7 +530,7 @@ function riebeckiteContentAssets(options) {
502
530
  next();
503
531
  return;
504
532
  }
505
- const file = path6.join(contentRoot, logicalPath);
533
+ const file = path7.join(contentRoot, logicalPath);
506
534
  if (!isPathInsideDirectory(contentRoot, file)) {
507
535
  next();
508
536
  return;
@@ -511,7 +539,7 @@ function riebeckiteContentAssets(options) {
511
539
  next();
512
540
  return;
513
541
  }
514
- const content = await fs4.readFile(file);
542
+ const content = await fs5.readFile(file);
515
543
  response.statusCode = 200;
516
544
  response.setHeader("Content-Type", contentType);
517
545
  response.setHeader("Content-Length", String(content.byteLength));
@@ -549,16 +577,16 @@ function collectPublicImagePaths(manifest) {
549
577
  async function collectSiteOwnedOutputPaths(publicDir) {
550
578
  const paths = /* @__PURE__ */ new Set();
551
579
  if (!publicDir) return paths;
552
- const root = path6.resolve(publicDir);
580
+ const root = path7.resolve(publicDir);
553
581
  const walk = async (directory) => {
554
- const entries = await fs4.readdir(directory, { withFileTypes: true }).catch(() => void 0);
582
+ const entries = await fs5.readdir(directory, { withFileTypes: true }).catch(() => void 0);
555
583
  if (!entries) return;
556
584
  for (const entry of entries) {
557
- const full = path6.join(directory, entry.name);
585
+ const full = path7.join(directory, entry.name);
558
586
  if (entry.isDirectory()) {
559
587
  await walk(full);
560
588
  } else {
561
- paths.add(path6.relative(root, full).replaceAll("\\", "/"));
589
+ paths.add(path7.relative(root, full).replaceAll("\\", "/"));
562
590
  }
563
591
  }
564
592
  };
@@ -593,22 +621,33 @@ function imageContentType(logicalPath) {
593
621
  }
594
622
  async function hasSiteOwnedFile(publicDir, logicalPath) {
595
623
  if (!publicDir) return false;
596
- const file = path6.join(publicDir, logicalPath);
624
+ const file = path7.join(publicDir, logicalPath);
597
625
  try {
598
- return (await fs4.stat(file)).isFile();
626
+ return (await fs5.stat(file)).isFile();
599
627
  } catch {
600
628
  return false;
601
629
  }
602
630
  }
603
631
 
604
632
  // src/ssg_plugin.ts
633
+ function createNoChangeFastPathOutputMetrics() {
634
+ return {
635
+ outputInventoryValidationDurationMs: 0,
636
+ outputHashValidationDurationMs: 0,
637
+ outputFilesHashed: 0,
638
+ outputBytesHashed: 0
639
+ };
640
+ }
605
641
  var defaultEntry = "./src/index.tsx";
642
+ var faultInjectionCounts = /* @__PURE__ */ new Map();
606
643
  var cloudflareWorkerAssetLimit = 25 * 1024 * 1024;
607
644
  function riebeckiteSsg(options = {}) {
608
645
  const virtualId = "virtual:riebeckite-ssg-void-entry";
609
646
  const resolvedVirtualId = `\0${virtualId}`;
610
647
  const entry = options.entry ?? defaultEntry;
611
648
  let resolvedConfig;
649
+ let pendingOutputCache;
650
+ let clientBuildArtifacts;
612
651
  return {
613
652
  name: "riebeckite-ssg",
614
653
  apply: shouldApplyRiebeckiteSsg,
@@ -625,6 +664,25 @@ function riebeckiteSsg(options = {}) {
625
664
  configResolved(config) {
626
665
  resolvedConfig = config;
627
666
  },
667
+ async buildStart() {
668
+ const config = resolvedConfig;
669
+ if (!config) return;
670
+ if (!config.plugins.some(
671
+ (plugin) => plugin.name.startsWith("@hono/vite-build/")
672
+ )) {
673
+ return;
674
+ }
675
+ const outDir = resolve(config.root, config.build.outDir);
676
+ clientBuildArtifacts = await preserveClientBuildArtifacts(outDir);
677
+ logSsgDiagnostics(
678
+ this,
679
+ `client manifest ${clientBuildArtifacts ? `preserved (${clientBuildArtifacts.paths.length} files)` : "not found"}`
680
+ );
681
+ await rm(outDir, {
682
+ recursive: true,
683
+ force: true
684
+ });
685
+ },
628
686
  resolveId(id) {
629
687
  return id === virtualId ? resolvedVirtualId : void 0;
630
688
  },
@@ -638,7 +696,8 @@ function riebeckiteSsg(options = {}) {
638
696
  "Riebeckite SSG could not resolve the Vite configuration."
639
697
  );
640
698
  }
641
- const timingsMs = {};
699
+ const timingsMs = process.env.RIEBECKITE_SSG_METRICS_FILE ? {} : void 0;
700
+ const outDir = resolve(config.root, config.build.outDir);
642
701
  removeVirtualEntryChunk(bundle, resolvedVirtualId);
643
702
  const server = await measure(
644
703
  timingsMs,
@@ -652,11 +711,19 @@ function riebeckiteSsg(options = {}) {
652
711
  },
653
712
  plugins: [],
654
713
  build: { ssr: true },
714
+ environments: {
715
+ ssr: { resolve: { noExternal: [/^honox(?:\/.*)?$/] } }
716
+ },
655
717
  mode: config.mode,
656
718
  server: { watch: null }
657
719
  })
658
720
  );
659
721
  try {
722
+ await restoreClientBuildArtifacts(outDir, clientBuildArtifacts);
723
+ logSsgDiagnostics(
724
+ this,
725
+ `SSR noExternal=${formatSsrNoExternal()} · client manifest ${clientBuildArtifacts ? "restored" : "not found"}`
726
+ );
660
727
  const module = await measure(
661
728
  timingsMs,
662
729
  "ssrModuleLoad",
@@ -700,23 +767,31 @@ function riebeckiteSsg(options = {}) {
700
767
  `Build outputs take precedence over site public assets: ${siteOutputCollisions.join(", ")}`
701
768
  );
702
769
  }
703
- const canUseIncremental = outputChangeSet && !outputChangeSet.fullRegenerationRequired && previousOutputCache && previousOutputCache.appFingerprint === appFingerprint && outputChangeSet.unchanged.every(
770
+ const canUseIncremental = outputChangeSet && !outputChangeSet.fullRegenerationRequired && previousOutputCache && previousOutputCache.appFingerprint === appFingerprint && previousOutputCache.clientManifestFingerprint === clientBuildArtifacts?.manifestFingerprint && outputChangeSet.unchanged.every(
704
771
  (output) => siteOwnedOutputPaths.has(output.path) || previousOutputCache.outputs[output.path]
705
772
  );
706
773
  const reusablePaths = new Set(
707
774
  canUseIncremental ? outputChangeSet.unchanged.map((output) => output.path) : []
708
775
  );
776
+ logSsgDiagnostics(
777
+ this,
778
+ `output cache ${canUseIncremental ? "hit" : "miss"} · ${reusablePaths.size} reusable outputs`
779
+ );
709
780
  const shadowedOutputs = /* @__PURE__ */ new Set();
710
781
  const skippedRouteCount = canUseIncremental ? outputChangeSet.unchanged.filter(
711
782
  (output) => output.kind !== "generated"
712
783
  ).length : 0;
713
784
  const generatedHtml = [];
714
785
  const nextOutputCache = {
715
- version: 1,
786
+ version: 4,
716
787
  appFingerprint,
717
- outputs: {}
788
+ clientManifestFingerprint: clientBuildArtifacts?.manifestFingerprint,
789
+ outputs: {},
790
+ configFile: config.configFile
718
791
  };
719
792
  const metrics = {
793
+ processedContentCount: 0,
794
+ reusedContentCount: 0,
720
795
  candidateOutputCount: outputChangeSet?.candidateOutputCount ?? 0,
721
796
  affectedOutputCount: outputChangeSet?.affectedOutputCount ?? 0,
722
797
  removedOutputCount: outputChangeSet?.removedOutputCount ?? 0,
@@ -735,10 +810,16 @@ function riebeckiteSsg(options = {}) {
735
810
  enumeratedRouteCount: 0,
736
811
  renderedRouteCount: 0,
737
812
  skippedRouteCount,
738
- timingsMs
813
+ manifestHooksExecuted: 0,
814
+ manifestHooksReused: 0,
815
+ manifestHookCacheHit: 0,
816
+ manifestHookCacheMiss: 0,
817
+ manifestHookDurationMs: 0,
818
+ manifestHookPlugins: {},
819
+ timingsMs: timingsMs ?? {}
739
820
  };
740
821
  process.env.RIEBECKITE_SSG_FULL_REGENERATION = canUseIncremental ? "" : "1";
741
- const outDir = resolve(config.root, config.build.outDir);
822
+ process.env.RIEBECKITE_SSG_SKIP_UNCHANGED = canUseIncremental ? "1" : "";
742
823
  const result = await measure(
743
824
  timingsMs,
744
825
  "toSSG",
@@ -750,7 +831,6 @@ function riebeckiteSsg(options = {}) {
750
831
  "\\",
751
832
  "/"
752
833
  );
753
- metrics.enumeratedRouteCount += 1;
754
834
  if (siteOwnedOutputPaths.has(fileName)) {
755
835
  shadowedOutputs.add(fileName);
756
836
  return;
@@ -760,49 +840,72 @@ function riebeckiteSsg(options = {}) {
760
840
  }
761
841
  if (fileName.endsWith(".html") && typeof data === "string") {
762
842
  metrics.renderedOutputCount += 1;
843
+ metrics.renderedRouteCount += 1;
763
844
  metrics.htmlOutputCount += 1;
764
845
  generatedHtml.push({ path: fileName, html: data });
765
846
  } else {
766
847
  metrics.assetOutputCount += 1;
767
848
  }
768
- this.emitFile({
769
- type: "asset",
770
- fileName,
771
- source: data
772
- });
849
+ measureSync(
850
+ timingsMs,
851
+ "outputWrite",
852
+ () => this.emitFile({
853
+ type: "asset",
854
+ fileName,
855
+ source: data
856
+ })
857
+ );
773
858
  metrics.emittedOutputCount += 1;
774
- nextOutputCache.outputs[fileName] = encodeOutput(data);
859
+ nextOutputCache.outputs[fileName] = {
860
+ cachePath: "",
861
+ checksum: ""
862
+ };
775
863
  },
776
864
  async mkdir() {
777
865
  }
778
866
  },
779
867
  {
780
868
  dir: outDir,
781
- plugins: [...options.plugins ?? []],
869
+ plugins: [redirectPlugin(), ...options.plugins ?? []],
782
870
  extensionMap: options.extensionMap ?? defaultExtensionMap
783
871
  }
784
872
  )
785
873
  );
786
874
  delete process.env.RIEBECKITE_SSG_FULL_REGENERATION;
787
- metrics.renderedRouteCount = result.files.length;
875
+ delete process.env.RIEBECKITE_SSG_SKIP_UNCHANGED;
876
+ metrics.enumeratedRouteCount = result.files.length;
788
877
  if (!result.success) throw result.error;
878
+ metrics.processedContentCount = module.content?.getProcessedContentCount() ?? 0;
879
+ metrics.reusedContentCount = module.content?.getReusedContentCount() ?? 0;
880
+ const manifestHookMetrics = module.content?.getManifestHookMetrics();
881
+ metrics.manifestHooksExecuted = manifestHookMetrics?.manifestHooksExecuted ?? 0;
882
+ metrics.manifestHooksReused = manifestHookMetrics?.manifestHooksReused ?? 0;
883
+ metrics.manifestHookCacheHit = manifestHookMetrics?.manifestHookCacheHit ?? 0;
884
+ metrics.manifestHookCacheMiss = manifestHookMetrics?.manifestHookCacheMiss ?? 0;
885
+ metrics.manifestHookDurationMs = manifestHookMetrics?.manifestHookDurationMs ?? 0;
886
+ metrics.manifestHookPlugins = manifestHookMetrics?.byPlugin ?? {};
789
887
  const emittedGeneratedOutputCount = await measure(
790
888
  timingsMs,
791
889
  "generatedOutputs",
792
890
  () => emitGeneratedOutputs(
793
891
  module,
794
892
  (asset) => {
795
- this.emitFile(asset);
796
- metrics.emittedOutputCount += 1;
797
- nextOutputCache.outputs[asset.fileName] = encodeOutput(
798
- asset.source
893
+ measureSync(
894
+ timingsMs,
895
+ "outputWrite",
896
+ () => this.emitFile(asset)
799
897
  );
898
+ metrics.emittedOutputCount += 1;
899
+ nextOutputCache.outputs[asset.fileName] = {
900
+ cachePath: "",
901
+ checksum: ""
902
+ };
800
903
  },
801
904
  canUseIncremental ? new Set(
802
905
  outputChangeSet.affected.filter((output) => output.kind === "generated").map((output) => output.path)
803
906
  ) : void 0,
804
907
  siteOwnedOutputPaths,
805
- (path10) => shadowedOutputs.add(path10)
908
+ (path11) => shadowedOutputs.add(path11)
806
909
  )
807
910
  );
808
911
  if (!outputChangeSet)
@@ -810,21 +913,21 @@ function riebeckiteSsg(options = {}) {
810
913
  if (canUseIncremental) {
811
914
  await measure(timingsMs, "unchangedOutputReuse", async () => {
812
915
  for (const output of outputChangeSet.unchanged) {
813
- const path10 = output.path;
814
- if (siteOwnedOutputPaths.has(path10)) {
815
- shadowedOutputs.add(path10);
916
+ const path11 = output.path;
917
+ if (siteOwnedOutputPaths.has(path11)) {
918
+ shadowedOutputs.add(path11);
816
919
  continue;
817
920
  }
818
- const cached = previousOutputCache.outputs[path10];
921
+ const cached = previousOutputCache.outputs[path11];
819
922
  if (!cached) continue;
820
923
  this.emitFile({
821
924
  type: "asset",
822
- fileName: path10,
823
- source: decodeOutput(cached)
925
+ fileName: path11,
926
+ source: await decodeOutput(outputCachePath, cached)
824
927
  });
825
928
  metrics.emittedOutputCount += 1;
826
929
  metrics.reusedOutputCount += 1;
827
- nextOutputCache.outputs[path10] = cached;
930
+ nextOutputCache.outputs[path11] = cached;
828
931
  }
829
932
  });
830
933
  }
@@ -848,14 +951,15 @@ function riebeckiteSsg(options = {}) {
848
951
  metrics.cacheEntriesSerialized = Object.keys(
849
952
  nextOutputCache.outputs
850
953
  ).length;
851
- try {
852
- await saveOutputCache(outputCachePath, nextOutputCache, timingsMs);
853
- } catch (error) {
854
- this.warn(
855
- `SSG output cache could not be saved; the next build regenerates every output. ${error instanceof Error ? error.message : String(error)}`
856
- );
857
- }
858
- await writeSsgOutputMetrics(metrics);
954
+ pendingOutputCache = {
955
+ path: outputCachePath,
956
+ state: nextOutputCache,
957
+ metrics,
958
+ timingsMs,
959
+ outDir,
960
+ publicDir: config.publicDir,
961
+ configFile: config.configFile
962
+ };
859
963
  this.info(
860
964
  `SSG outputs: ${metrics.renderedOutputCount} rendered · ${metrics.reusedOutputCount} reused · ${metrics.deletedOutputCount} removed`
861
965
  );
@@ -863,18 +967,58 @@ function riebeckiteSsg(options = {}) {
863
967
  warn: (message) => this.warn(message),
864
968
  info: (message) => this.info(message)
865
969
  });
970
+ logGeneratedHtmlAssetDiagnostics(this, generatedHtml);
866
971
  } finally {
867
972
  await server.close();
973
+ await discardClientBuildArtifacts(clientBuildArtifacts);
974
+ clientBuildArtifacts = void 0;
868
975
  }
869
976
  },
870
- async closeBundle() {
871
- if (!resolvedConfig) return;
872
- const outDir = resolve(resolvedConfig.root, resolvedConfig.build.outDir);
873
- const oversizedAssets = await findOversizedAssets(outDir);
874
- for (const asset of oversizedAssets) {
875
- this.warn(
876
- `Generated asset ${asset.path} is ${formatBytes(asset.size)}, exceeding Cloudflare Workers' ${formatBytes(cloudflareWorkerAssetLimit)} asset limit.`
977
+ closeBundle: {
978
+ order: "post",
979
+ async handler() {
980
+ if (!resolvedConfig) return;
981
+ const outDir = resolve(
982
+ resolvedConfig.root,
983
+ resolvedConfig.build.outDir
877
984
  );
985
+ const oversizedAssets = await findOversizedAssets(outDir);
986
+ for (const asset of oversizedAssets) {
987
+ this.warn(
988
+ `Generated asset ${asset.path} is ${formatBytes(asset.size)}, exceeding Cloudflare Workers' ${formatBytes(cloudflareWorkerAssetLimit)} asset limit.`
989
+ );
990
+ }
991
+ const outputCache = pendingOutputCache;
992
+ if (!outputCache) return;
993
+ try {
994
+ outputCache.state.finalOutputInventory = await measure(
995
+ outputCache.timingsMs,
996
+ "outputInventory",
997
+ () => fingerprintOutputDirectory(outputCache.outDir)
998
+ );
999
+ const publicDir = outputCache.publicDir;
1000
+ outputCache.state.publicDirectory = publicDir ? resolve(publicDir) : void 0;
1001
+ outputCache.state.publicFingerprint = publicDir ? await measure(
1002
+ outputCache.timingsMs,
1003
+ "publicFingerprint",
1004
+ () => fingerprintDirectory(resolve(publicDir))
1005
+ ) : void 0;
1006
+ await waitForFaultInjection("build-state-before");
1007
+ await saveOutputCache(
1008
+ outputCache.path,
1009
+ outputCache.state,
1010
+ outputCache.outDir,
1011
+ outputCache.timingsMs
1012
+ );
1013
+ await waitForFaultInjection("build-state-committed");
1014
+ } catch (error) {
1015
+ this.warn(
1016
+ `SSG output cache could not be saved; the next build regenerates every output. ${error instanceof Error ? error.message : String(error)}`
1017
+ );
1018
+ } finally {
1019
+ await writeSsgOutputMetrics(outputCache.metrics);
1020
+ pendingOutputCache = void 0;
1021
+ }
878
1022
  }
879
1023
  }
880
1024
  };
@@ -888,16 +1032,16 @@ async function findOversizedAssets(directory, relativeDirectory = "") {
888
1032
  );
889
1033
  const nestedAssets = await Promise.all(
890
1034
  entries.map(async (entry) => {
891
- const path10 = join(directory, entry.name);
1035
+ const path11 = join(directory, entry.name);
892
1036
  const relativePath = join(relativeDirectory, entry.name).replaceAll(
893
1037
  "\\",
894
1038
  "/"
895
1039
  );
896
1040
  if (entry.isDirectory()) {
897
- return findOversizedAssets(path10, relativePath);
1041
+ return findOversizedAssets(path11, relativePath);
898
1042
  }
899
1043
  if (!entry.isFile()) return [];
900
- const size = (await stat(path10)).size;
1044
+ const size = (await stat(path11)).size;
901
1045
  return size > cloudflareWorkerAssetLimit ? [{ path: relativePath, size }] : [];
902
1046
  })
903
1047
  );
@@ -909,19 +1053,146 @@ function isMissingDirectoryError(error) {
909
1053
  function formatBytes(bytes) {
910
1054
  return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
911
1055
  }
1056
+ async function preserveClientBuildArtifacts(outDir) {
1057
+ const manifestPath = ".vite/manifest.json";
1058
+ let manifestSource;
1059
+ try {
1060
+ manifestSource = await readFile(join(outDir, manifestPath), "utf8");
1061
+ } catch (error) {
1062
+ if (isNotFoundError(error)) return void 0;
1063
+ throw error;
1064
+ }
1065
+ const paths = [manifestPath, ...collectClientAssetPaths(manifestSource)];
1066
+ const stagingDirectory = join(
1067
+ dirname(outDir),
1068
+ ".riebeckite",
1069
+ `client-build-${randomUUID()}`
1070
+ );
1071
+ for (const relativePath of paths) {
1072
+ const source = resolveBuildArtifactPath(outDir, relativePath);
1073
+ const destination = resolveBuildArtifactPath(
1074
+ stagingDirectory,
1075
+ relativePath
1076
+ );
1077
+ await mkdir(dirname(destination), { recursive: true });
1078
+ await rename(source, destination);
1079
+ }
1080
+ return {
1081
+ stagingDirectory,
1082
+ paths,
1083
+ manifestFingerprint: createHash("sha256").update(manifestSource).digest("hex")
1084
+ };
1085
+ }
1086
+ async function restoreClientBuildArtifacts(outDir, artifacts) {
1087
+ if (!artifacts) return;
1088
+ for (const relativePath of artifacts.paths) {
1089
+ const source = resolveBuildArtifactPath(
1090
+ artifacts.stagingDirectory,
1091
+ relativePath
1092
+ );
1093
+ const destination = resolveBuildArtifactPath(outDir, relativePath);
1094
+ await mkdir(dirname(destination), { recursive: true });
1095
+ await rename(source, destination);
1096
+ }
1097
+ }
1098
+ async function discardClientBuildArtifacts(artifacts) {
1099
+ if (!artifacts) return;
1100
+ await rm(artifacts.stagingDirectory, { recursive: true, force: true });
1101
+ }
1102
+ function collectClientAssetPaths(manifestSource) {
1103
+ let manifest;
1104
+ try {
1105
+ manifest = JSON.parse(manifestSource);
1106
+ } catch (error) {
1107
+ throw new Error(
1108
+ `Vite client manifest could not be parsed: ${error instanceof Error ? error.message : String(error)}`
1109
+ );
1110
+ }
1111
+ if (typeof manifest !== "object" || manifest === null || Array.isArray(manifest)) {
1112
+ throw new Error("Vite client manifest must be an object.");
1113
+ }
1114
+ const paths = /* @__PURE__ */ new Set();
1115
+ for (const entry of Object.values(manifest)) {
1116
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
1117
+ continue;
1118
+ }
1119
+ const assets = entry;
1120
+ addClientAssetPath(paths, assets.file);
1121
+ addClientAssetPaths(paths, assets.css);
1122
+ addClientAssetPaths(paths, assets.assets);
1123
+ }
1124
+ return [...paths].sort();
1125
+ }
1126
+ function addClientAssetPath(paths, value) {
1127
+ if (typeof value === "string") paths.add(value);
1128
+ }
1129
+ function addClientAssetPaths(paths, value) {
1130
+ if (!Array.isArray(value)) return;
1131
+ for (const path11 of value) addClientAssetPath(paths, path11);
1132
+ }
1133
+ function resolveBuildArtifactPath(directory, relativePath) {
1134
+ if (!relativePath || relativePath.startsWith("/") || relativePath.includes("\0")) {
1135
+ throw new Error(
1136
+ `Vite client manifest has an invalid asset path: ${relativePath}`
1137
+ );
1138
+ }
1139
+ const file = resolve(directory, relativePath);
1140
+ const relativePathFromDirectory = relative(directory, file);
1141
+ if (relativePathFromDirectory.startsWith("..") || relativePathFromDirectory === "" || isAbsolutePath(relativePathFromDirectory)) {
1142
+ throw new Error(
1143
+ `Vite client manifest has an unsafe asset path: ${relativePath}`
1144
+ );
1145
+ }
1146
+ return file;
1147
+ }
1148
+ function isAbsolutePath(path11) {
1149
+ return /^([a-z]:)?[\\/]/iu.test(path11);
1150
+ }
1151
+ function formatSsrNoExternal() {
1152
+ return "/^honox(?:\\/.*)?$/";
1153
+ }
1154
+ function logSsgDiagnostics(logger, message) {
1155
+ if (process.env.RIEBECKITE_SSG_DIAGNOSTICS !== "1") return;
1156
+ logger.info(`[riebeckite-ssg] ${message}`);
1157
+ }
1158
+ function logGeneratedHtmlAssetDiagnostics(logger, pages) {
1159
+ if (process.env.RIEBECKITE_SSG_DIAGNOSTICS !== "1") return;
1160
+ const renderedPages = pages.filter(
1161
+ (page) => !/<meta\b[^>]*\bhttp-equiv=["']refresh["'][^>]*\bcontent=["'][^"']*\burl=/iu.test(
1162
+ page.html
1163
+ )
1164
+ );
1165
+ const cssPages = renderedPages.filter(
1166
+ (page) => /<link\b[^>]*\brel=["'][^"']*\bstylesheet\b[^"']*["']/iu.test(page.html)
1167
+ );
1168
+ const scriptPages = renderedPages.filter(
1169
+ (page) => /<script\b[^>]*\bsrc=["'][^"']+["']/iu.test(page.html)
1170
+ );
1171
+ const missingCss = renderedPages.filter((page) => !cssPages.includes(page)).map((page) => page.path);
1172
+ const missingScript = renderedPages.filter((page) => !scriptPages.includes(page)).map((page) => page.path);
1173
+ logger.info(
1174
+ `[riebeckite-ssg] rendered HTML assets: ${pages.length} documents (${renderedPages.length} pages, ${pages.length - renderedPages.length} redirects) · CSS ${cssPages.length} · JavaScript ${scriptPages.length} · missing CSS ${missingCss.length} · missing JavaScript ${missingScript.length}`
1175
+ );
1176
+ if (missingCss.length > 0 || missingScript.length > 0) {
1177
+ logger.info(
1178
+ `[riebeckite-ssg] missing asset references: CSS ${missingCss.join(", ") || "none"} · JavaScript ${missingScript.join(", ") || "none"}`
1179
+ );
1180
+ }
1181
+ }
912
1182
  function shouldApplyRiebeckiteSsg(_config, env) {
913
1183
  return env.command === "build" && env.mode !== "client";
914
1184
  }
915
1185
  function findBuildOutputSiteCollisions(buildOutputPaths, siteOwnedOutputPaths) {
916
1186
  const collisions = /* @__PURE__ */ new Set();
917
- for (const path10 of buildOutputPaths) {
918
- if (siteOwnedOutputPaths.has(path10)) collisions.add(path10);
1187
+ for (const path11 of buildOutputPaths) {
1188
+ if (siteOwnedOutputPaths.has(path11)) collisions.add(path11);
919
1189
  }
920
1190
  return [...collisions].sort();
921
1191
  }
922
1192
  async function emitGeneratedOutputs(module, emit, affectedPaths, shadowedPaths, onShadowed) {
923
1193
  const content = module.content;
924
1194
  if (!content) return 0;
1195
+ if (affectedPaths?.size === 0) return 0;
925
1196
  const manifest = await content.getManifest();
926
1197
  let count = 0;
927
1198
  for (const output of manifest.generatedOutputs ?? []) {
@@ -975,6 +1246,92 @@ async function computeAppFingerprint(root, configFile) {
975
1246
  }
976
1247
  return hash.digest("hex");
977
1248
  }
1249
+ async function verifyNoChangeFastPath(input) {
1250
+ const outputCachePath = join(
1251
+ input.root,
1252
+ ".riebeckite",
1253
+ "ssg-output-cache.json"
1254
+ );
1255
+ const cache = await loadFastPathCache(fastPathCachePath(outputCachePath));
1256
+ if (!cache)
1257
+ return { used: false, blockReason: "output-cache-missing-or-invalid" };
1258
+ if (cache.outputCacheChecksum !== await checksumFile(outputCachePath)) {
1259
+ return { used: false, blockReason: "output-cache-missing-or-invalid" };
1260
+ }
1261
+ if (cache.appFingerprint !== await computeAppFingerprint(input.root, cache.configFile)) {
1262
+ return { used: false, blockReason: "app-fingerprint-changed" };
1263
+ }
1264
+ if (cache.publicDirectory && cache.publicFingerprint !== await fingerprintDirectory(cache.publicDirectory)) {
1265
+ return { used: false, blockReason: "static-assets-changed" };
1266
+ }
1267
+ if (input.expectedOutputPaths.some(
1268
+ (output) => !cache.outputPaths.includes(output) || !cache.finalOutputInventory[output]
1269
+ )) {
1270
+ return { used: false, blockReason: "output-cache-incomplete" };
1271
+ }
1272
+ const inventoryStartedAt = performance.now();
1273
+ const currentInventory = await fingerprintOutputDirectory(
1274
+ input.outputDirectory,
1275
+ "",
1276
+ input.metrics
1277
+ );
1278
+ if (input.metrics)
1279
+ input.metrics.outputInventoryValidationDurationMs += performance.now() - inventoryStartedAt;
1280
+ if (!inventoriesEqual(cache.finalOutputInventory, currentInventory)) {
1281
+ return { used: false, blockReason: "output-inventory-mismatch" };
1282
+ }
1283
+ return { used: true };
1284
+ }
1285
+ async function fingerprintDirectory(directory) {
1286
+ const inventory = await fingerprintOutputDirectory(directory);
1287
+ const hash = createHash("sha256");
1288
+ for (const [path11, checksum] of Object.entries(inventory).sort(
1289
+ ([left], [right]) => left.localeCompare(right)
1290
+ )) {
1291
+ hash.update(path11).update("\0").update(checksum).update("\0");
1292
+ }
1293
+ return hash.digest("hex");
1294
+ }
1295
+ async function fingerprintOutputDirectory(directory, relativeDirectory = "", metrics) {
1296
+ const entries = await readdir(directory, { withFileTypes: true }).catch(
1297
+ (error) => {
1298
+ if (isMissingDirectoryError(error)) return void 0;
1299
+ throw error;
1300
+ }
1301
+ );
1302
+ if (!entries) return {};
1303
+ const inventory = {};
1304
+ for (const entry of entries) {
1305
+ const file = join(directory, entry.name);
1306
+ const relativePath = join(relativeDirectory, entry.name).replaceAll(
1307
+ "\\",
1308
+ "/"
1309
+ );
1310
+ if (entry.isDirectory()) {
1311
+ Object.assign(
1312
+ inventory,
1313
+ await fingerprintOutputDirectory(file, relativePath, metrics)
1314
+ );
1315
+ continue;
1316
+ }
1317
+ if (entry.isFile()) {
1318
+ const hashedAt = performance.now();
1319
+ const content = await readFile(file);
1320
+ inventory[relativePath] = createHash("sha256").update(content).digest("hex");
1321
+ if (metrics) {
1322
+ metrics.outputHashValidationDurationMs += performance.now() - hashedAt;
1323
+ metrics.outputFilesHashed += 1;
1324
+ metrics.outputBytesHashed += content.length;
1325
+ }
1326
+ }
1327
+ }
1328
+ return inventory;
1329
+ }
1330
+ function inventoriesEqual(left, right) {
1331
+ const leftPaths = Object.keys(left);
1332
+ const rightPaths = Object.keys(right);
1333
+ return leftPaths.length === rightPaths.length && leftPaths.every((path11) => left[path11] === right[path11]);
1334
+ }
978
1335
  async function collectAppFiles(directory, files) {
979
1336
  const entries = await readdir(directory, { withFileTypes: true }).catch(
980
1337
  () => void 0
@@ -990,14 +1347,21 @@ async function collectAppFiles(directory, files) {
990
1347
  }
991
1348
  }
992
1349
  }
993
- async function loadOutputCache(path10, timingsMs, log = () => {
1350
+ async function loadOutputCache(path11, timingsMs, log = () => {
994
1351
  }) {
1352
+ if (!await isSafeOutputCacheDirectory(dirname(path11))) {
1353
+ log("SSG output cache directory is unsafe and will be ignored.");
1354
+ return void 0;
1355
+ }
1356
+ if (!await isRegularFile(path11)) {
1357
+ return void 0;
1358
+ }
995
1359
  let raw;
996
1360
  try {
997
1361
  raw = await measure(
998
1362
  timingsMs,
999
1363
  "outputCacheRead",
1000
- () => readFile(path10, "utf8")
1364
+ () => readFile(path11, "utf8")
1001
1365
  );
1002
1366
  } catch (error) {
1003
1367
  if (isNotFoundError(error)) return void 0;
@@ -1019,32 +1383,65 @@ async function loadOutputCache(path10, timingsMs, log = () => {
1019
1383
  log("SSG output cache uses an unsupported format and will be rebuilt.");
1020
1384
  return void 0;
1021
1385
  }
1386
+ const entriesAreValid = await measure(
1387
+ timingsMs,
1388
+ "outputCacheEntriesValidate",
1389
+ () => validateOutputCacheEntries(path11, parsed.outputs, timingsMs)
1390
+ );
1391
+ if (!entriesAreValid) {
1392
+ log("SSG output cache entries are corrupted and will be rebuilt.");
1393
+ return void 0;
1394
+ }
1022
1395
  return timingsMs ? { ...parsed, fileSizeBytes: Buffer.byteLength(raw) } : parsed;
1023
1396
  }
1024
1397
  function isOutputCacheState(value) {
1025
1398
  if (typeof value !== "object" || value === null) return false;
1026
1399
  const candidate = value;
1027
- return candidate.version === 1 && (candidate.appFingerprint === void 0 || typeof candidate.appFingerprint === "string") && isOutputCacheEntries(candidate.outputs);
1400
+ return candidate.version === 4 && (candidate.appFingerprint === void 0 || typeof candidate.appFingerprint === "string") && (candidate.clientManifestFingerprint === void 0 || typeof candidate.clientManifestFingerprint === "string") && isOutputCacheEntries(candidate.outputs) && (candidate.finalOutputInventory === void 0 || isOutputInventory(candidate.finalOutputInventory)) && (candidate.publicDirectory === void 0 || typeof candidate.publicDirectory === "string") && (candidate.publicFingerprint === void 0 || typeof candidate.publicFingerprint === "string") && (candidate.configFile === void 0 || typeof candidate.configFile === "string");
1401
+ }
1402
+ function isOutputInventory(value) {
1403
+ return typeof value === "object" && value !== null && !Array.isArray(value) && Object.entries(value).every(
1404
+ ([outputPath, checksum]) => isSafeLogicalOutputPath(outputPath) && isChecksum(checksum)
1405
+ );
1028
1406
  }
1029
1407
  function isOutputCacheEntries(value) {
1030
1408
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
1031
1409
  return false;
1032
1410
  }
1033
- return Object.values(value).every(
1034
- (entry) => typeof entry === "object" && entry !== null && typeof entry.source === "string" && (entry.encoding === "utf8" || entry.encoding === "base64")
1411
+ return Object.entries(value).every(
1412
+ ([outputPath, entry]) => isSafeLogicalOutputPath(outputPath) && typeof entry === "object" && entry !== null && typeof entry.cachePath === "string" && entry.cachePath === outputCacheEntryName(outputPath) && isChecksum(entry.checksum)
1035
1413
  );
1036
1414
  }
1037
- async function saveOutputCache(path10, state, timingsMs) {
1038
- const directory = dirname(path10);
1415
+ async function saveOutputCache(path11, state, outputDirectory, timingsMs) {
1416
+ const directory = dirname(path11);
1039
1417
  await mkdir(directory, { recursive: true });
1040
- const temporaryPath = `${path10}.${randomUUID()}.tmp`;
1041
- const serialized = measureSync(
1042
- timingsMs,
1043
- "outputCacheSerialize",
1044
- () => `${JSON.stringify(state)}
1045
- `
1046
- );
1418
+ if (!await isSafeOutputCacheDirectory(directory)) {
1419
+ throw new Error(`Unsafe SSG output cache directory: ${directory}`);
1420
+ }
1421
+ if (!await isSafeCacheFileTarget(path11)) {
1422
+ throw new Error(`Unsafe SSG output cache file: ${path11}`);
1423
+ }
1424
+ const entriesPath = outputCacheEntriesPath(path11);
1425
+ const temporaryPath = `${path11}.${randomUUID()}.tmp`;
1047
1426
  try {
1427
+ await waitForFaultInjection("metadata-save-start");
1428
+ const outputs = await measure(
1429
+ timingsMs,
1430
+ "outputCacheEntriesWrite",
1431
+ () => saveOutputCacheEntries(
1432
+ entriesPath,
1433
+ outputDirectory,
1434
+ state.outputs,
1435
+ state.finalOutputInventory,
1436
+ timingsMs
1437
+ )
1438
+ );
1439
+ const serialized = measureSync(
1440
+ timingsMs,
1441
+ "outputCacheSerialize",
1442
+ () => `${JSON.stringify({ ...state, outputs })}
1443
+ `
1444
+ );
1048
1445
  await measure(
1049
1446
  timingsMs,
1050
1447
  "outputCacheWrite",
@@ -1053,13 +1450,157 @@ async function saveOutputCache(path10, state, timingsMs) {
1053
1450
  await measure(
1054
1451
  timingsMs,
1055
1452
  "outputCacheRename",
1056
- () => replaceFile(temporaryPath, path10)
1453
+ () => replaceFile(temporaryPath, path11)
1454
+ );
1455
+ await waitForFaultInjection("metadata-committed");
1456
+ const checksum = createHash("sha256").update(serialized).digest("hex");
1457
+ if (state.finalOutputInventory && state.configFile) {
1458
+ await saveFastPathCache(
1459
+ fastPathCachePath(path11),
1460
+ createFastPathCacheState(state, checksum),
1461
+ timingsMs
1462
+ );
1463
+ }
1464
+ await collectUnreferencedOutputCacheEntries(
1465
+ outputCacheEntriesPath(path11),
1466
+ outputs,
1467
+ timingsMs
1468
+ );
1469
+ return checksum;
1470
+ } catch (error) {
1471
+ await rm(temporaryPath, { force: true }).catch(() => void 0);
1472
+ throw error;
1473
+ }
1474
+ }
1475
+ function outputCacheEntriesPath(outputCachePath) {
1476
+ return outputCachePath.replace(/\.json$/, ".entries");
1477
+ }
1478
+ async function saveOutputCacheEntries(directory, outputDirectory, outputs, finalOutputInventory, timingsMs) {
1479
+ await ensureOutputCacheEntriesDirectory(directory);
1480
+ const entries = {};
1481
+ for (const outputPath of Object.keys(outputs).sort()) {
1482
+ assertSafeLogicalOutputPath(outputPath);
1483
+ const existing = outputs[outputPath];
1484
+ if (existing.cachePath && existing.checksum) {
1485
+ if (existing.cachePath !== outputCacheEntryName(outputPath) || !isChecksum(existing.checksum)) {
1486
+ throw new Error(`Invalid SSG output cache entry: ${outputPath}`);
1487
+ }
1488
+ entries[outputPath] = existing;
1489
+ continue;
1490
+ }
1491
+ const source = resolveOutputCacheFile(outputDirectory, outputPath);
1492
+ const cachePath = outputCacheEntryName(outputPath);
1493
+ const destination = join(directory, cachePath);
1494
+ await waitForFaultInjection("entry-create-start");
1495
+ await replaceOutputCacheEntry(source, destination, timingsMs);
1496
+ const checksum = finalOutputInventory?.[outputPath];
1497
+ if (!checksum) {
1498
+ const cachedChecksum = await checksumFile(destination);
1499
+ if (!cachedChecksum) {
1500
+ throw new Error(`Could not checksum cached output: ${outputPath}`);
1501
+ }
1502
+ entries[outputPath] = { cachePath, checksum: cachedChecksum };
1503
+ continue;
1504
+ }
1505
+ entries[outputPath] = { cachePath, checksum };
1506
+ }
1507
+ return entries;
1508
+ }
1509
+ async function validateOutputCacheEntries(outputCachePath, entries, timingsMs) {
1510
+ try {
1511
+ const directory = outputCacheEntriesPath(outputCachePath);
1512
+ if (!await isSafeOutputCacheEntriesDirectory(directory)) return false;
1513
+ for (const [outputPath, entry] of Object.entries(entries)) {
1514
+ if (!isSafeLogicalOutputPath(outputPath) || entry.cachePath !== outputCacheEntryName(outputPath) || !isChecksum(entry.checksum)) {
1515
+ return false;
1516
+ }
1517
+ const path11 = resolveOutputCacheFile(directory, entry.cachePath);
1518
+ if (!await isRegularFile(path11)) return false;
1519
+ if (await measure(
1520
+ timingsMs,
1521
+ "outputCacheEntryChecksum",
1522
+ () => checksumFile(path11)
1523
+ ) !== entry.checksum)
1524
+ return false;
1525
+ }
1526
+ return true;
1527
+ } catch {
1528
+ return false;
1529
+ }
1530
+ }
1531
+ function fastPathCachePath(outputCachePath) {
1532
+ return outputCachePath.replace(/\.json$/, ".fast-path.json");
1533
+ }
1534
+ function createFastPathCacheState(state, outputCacheChecksum) {
1535
+ if (!state.finalOutputInventory || !state.configFile) {
1536
+ throw new Error("SSG output cache is missing fast-path validation data.");
1537
+ }
1538
+ return {
1539
+ version: 1,
1540
+ outputCacheChecksum,
1541
+ appFingerprint: state.appFingerprint,
1542
+ finalOutputInventory: state.finalOutputInventory,
1543
+ publicDirectory: state.publicDirectory,
1544
+ publicFingerprint: state.publicFingerprint,
1545
+ configFile: state.configFile,
1546
+ outputPaths: Object.keys(state.outputs).sort()
1547
+ };
1548
+ }
1549
+ async function saveFastPathCache(path11, state, timingsMs) {
1550
+ const directory = dirname(path11);
1551
+ await mkdir(directory, { recursive: true });
1552
+ if (!await isSafeOutputCacheDirectory(directory)) {
1553
+ throw new Error(`Unsafe SSG fast-path cache directory: ${directory}`);
1554
+ }
1555
+ if (!await isSafeCacheFileTarget(path11)) {
1556
+ throw new Error(`Unsafe SSG fast-path cache file: ${path11}`);
1557
+ }
1558
+ const temporaryPath = `${path11}.${randomUUID()}.tmp`;
1559
+ try {
1560
+ await waitForFaultInjection("sidecar-save-start");
1561
+ await measure(
1562
+ timingsMs,
1563
+ "sidecarWrite",
1564
+ () => writeFile(temporaryPath, `${JSON.stringify(state)}
1565
+ `, "utf8")
1057
1566
  );
1567
+ await measure(
1568
+ timingsMs,
1569
+ "sidecarRename",
1570
+ () => replaceFile(temporaryPath, path11)
1571
+ );
1572
+ await waitForFaultInjection("sidecar-committed");
1058
1573
  } catch (error) {
1059
1574
  await rm(temporaryPath, { force: true }).catch(() => void 0);
1060
1575
  throw error;
1061
1576
  }
1062
1577
  }
1578
+ async function loadFastPathCache(path11) {
1579
+ try {
1580
+ if (!await isSafeOutputCacheDirectory(dirname(path11))) return void 0;
1581
+ if (!await isRegularFile(path11)) return void 0;
1582
+ const candidate = JSON.parse(await readFile(path11, "utf8"));
1583
+ return isFastPathCacheState(candidate) ? candidate : void 0;
1584
+ } catch {
1585
+ return void 0;
1586
+ }
1587
+ }
1588
+ function isFastPathCacheState(value) {
1589
+ if (typeof value !== "object" || value === null) return false;
1590
+ const candidate = value;
1591
+ return candidate.version === 1 && typeof candidate.outputCacheChecksum === "string" && (candidate.appFingerprint === void 0 || typeof candidate.appFingerprint === "string") && isOutputInventory(candidate.finalOutputInventory) && (candidate.publicDirectory === void 0 || typeof candidate.publicDirectory === "string") && (candidate.publicFingerprint === void 0 || typeof candidate.publicFingerprint === "string") && typeof candidate.configFile === "string" && Array.isArray(candidate.outputPaths) && candidate.outputPaths.every(isSafeLogicalOutputPath) && candidate.outputPaths.every(
1592
+ (path11, index, paths) => index === 0 || paths[index - 1] < path11
1593
+ );
1594
+ }
1595
+ async function checksumFile(path11) {
1596
+ const hash = createHash("sha256");
1597
+ try {
1598
+ for await (const chunk of createReadStream(path11)) hash.update(chunk);
1599
+ return hash.digest("hex");
1600
+ } catch {
1601
+ return void 0;
1602
+ }
1603
+ }
1063
1604
  async function replaceFile(temporaryPath, targetPath) {
1064
1605
  try {
1065
1606
  await rename(temporaryPath, targetPath);
@@ -1079,11 +1620,160 @@ function isReplacementFailure(error) {
1079
1620
  function isNotFoundError(error) {
1080
1621
  return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
1081
1622
  }
1082
- function encodeOutput(source) {
1083
- return typeof source === "string" ? { source, encoding: "utf8" } : { source: Buffer.from(source).toString("base64"), encoding: "base64" };
1623
+ async function decodeOutput(outputCachePath, entry) {
1624
+ const file = resolveOutputCacheFile(
1625
+ outputCacheEntriesPath(outputCachePath),
1626
+ entry.cachePath
1627
+ );
1628
+ const content = await readFile(file);
1629
+ const checksum = createHash("sha256").update(content).digest("hex");
1630
+ if (checksum !== entry.checksum) {
1631
+ throw new Error("SSG output cache entry checksum mismatch.");
1632
+ }
1633
+ return content;
1634
+ }
1635
+ function resolveOutputCacheFile(directory, relativePath) {
1636
+ if (!relativePath || relativePath.includes("\0") || isAbsolutePath(relativePath)) {
1637
+ throw new Error(`Invalid SSG output cache path: ${relativePath}`);
1638
+ }
1639
+ const file = resolve(directory, relativePath);
1640
+ const relativePathFromDirectory = relative(directory, file);
1641
+ if (relativePathFromDirectory.startsWith("..") || relativePathFromDirectory === "" || isAbsolutePath(relativePathFromDirectory)) {
1642
+ throw new Error(`Unsafe SSG output cache path: ${relativePath}`);
1643
+ }
1644
+ return file;
1645
+ }
1646
+ function isSafeLogicalOutputPath(path11) {
1647
+ if (typeof path11 !== "string" || !path11 || path11.includes("\\") || path11.includes("\0") || isAbsolutePath(path11)) {
1648
+ return false;
1649
+ }
1650
+ try {
1651
+ resolveOutputCacheFile("/riebeckite-output-root", path11);
1652
+ return true;
1653
+ } catch {
1654
+ return false;
1655
+ }
1656
+ }
1657
+ function assertSafeLogicalOutputPath(path11) {
1658
+ if (!isSafeLogicalOutputPath(path11)) {
1659
+ throw new Error(`Invalid SSG output path: ${path11}`);
1660
+ }
1661
+ }
1662
+ function outputCacheEntryName(outputPath) {
1663
+ return createHash("sha256").update(outputPath).digest("hex");
1664
+ }
1665
+ function isChecksum(value) {
1666
+ return typeof value === "string" && /^[a-f0-9]{64}$/u.test(value);
1667
+ }
1668
+ async function ensureOutputCacheEntriesDirectory(directory) {
1669
+ await mkdir(directory, { recursive: true });
1670
+ if (!await isSafeOutputCacheEntriesDirectory(directory)) {
1671
+ throw new Error(`Unsafe SSG output cache entries directory: ${directory}`);
1672
+ }
1673
+ }
1674
+ async function isSafeOutputCacheDirectory(directory) {
1675
+ try {
1676
+ const info = await lstat(directory);
1677
+ return info.isDirectory() && !info.isSymbolicLink();
1678
+ } catch {
1679
+ return false;
1680
+ }
1681
+ }
1682
+ async function isSafeCacheFileTarget(path11) {
1683
+ try {
1684
+ return !(await lstat(path11)).isSymbolicLink();
1685
+ } catch (error) {
1686
+ return isNotFoundError(error);
1687
+ }
1688
+ }
1689
+ async function isSafeOutputCacheEntriesDirectory(directory) {
1690
+ return isSafeOutputCacheDirectory(directory);
1691
+ }
1692
+ async function isRegularFile(path11) {
1693
+ try {
1694
+ const info = await lstat(path11);
1695
+ return info.isFile() && !info.isSymbolicLink();
1696
+ } catch {
1697
+ return false;
1698
+ }
1699
+ }
1700
+ async function replaceOutputCacheEntry(source, destination, timingsMs) {
1701
+ if ((await lstat(destination).catch(() => void 0))?.isSymbolicLink()) {
1702
+ throw new Error(`Unsafe SSG output cache entry: ${destination}`);
1703
+ }
1704
+ const temporaryPath = `${destination}.${randomUUID()}.tmp`;
1705
+ try {
1706
+ await measure(
1707
+ timingsMs,
1708
+ "outputCacheEntryWrite",
1709
+ () => writeFile(temporaryPath, new Uint8Array())
1710
+ );
1711
+ await waitForFaultInjection("entry-body-write");
1712
+ await measure(
1713
+ timingsMs,
1714
+ "outputCacheEntryCopy",
1715
+ () => copyFile(source, temporaryPath)
1716
+ );
1717
+ await waitForFaultInjection("entry-rename-before");
1718
+ await measure(
1719
+ timingsMs,
1720
+ "outputCacheEntryRename",
1721
+ () => replaceFile(temporaryPath, destination)
1722
+ );
1723
+ await waitForFaultInjection("entry-committed");
1724
+ } catch (error) {
1725
+ await rm(temporaryPath, { force: true }).catch(() => void 0);
1726
+ throw error;
1727
+ }
1728
+ }
1729
+ async function collectUnreferencedOutputCacheEntries(directory, outputs, timingsMs) {
1730
+ try {
1731
+ if (!await isSafeOutputCacheEntriesDirectory(directory)) return;
1732
+ await waitForFaultInjection("gc-start");
1733
+ const referenced = new Set(
1734
+ Object.values(outputs).map((entry) => entry.cachePath)
1735
+ );
1736
+ const entries = await measure(
1737
+ timingsMs,
1738
+ "outputCacheGcScan",
1739
+ () => readdir(directory, { withFileTypes: true })
1740
+ );
1741
+ for (const entry of entries) {
1742
+ if (!/^[a-f0-9]{64}$/u.test(entry.name) || referenced.has(entry.name)) {
1743
+ continue;
1744
+ }
1745
+ const path11 = join(directory, entry.name);
1746
+ if (await isRegularFile(path11)) {
1747
+ await waitForFaultInjection("gc-entry-delete");
1748
+ await measure(
1749
+ timingsMs,
1750
+ "outputCacheGcDelete",
1751
+ () => rm(path11, { force: true })
1752
+ );
1753
+ }
1754
+ }
1755
+ } catch {
1756
+ return;
1757
+ }
1084
1758
  }
1085
- function decodeOutput(entry) {
1086
- return entry.encoding === "utf8" ? entry.source : Buffer.from(entry.source, "base64");
1759
+ async function waitForFaultInjection(point) {
1760
+ if (process.env.NODE_ENV !== "test" || process.env.RIEBECKITE_SSG_FAULT_INJECTION !== "1" || process.env.RIEBECKITE_SSG_FAULT_POINT !== point) {
1761
+ return;
1762
+ }
1763
+ const occurrence = Number.parseInt(
1764
+ process.env.RIEBECKITE_SSG_FAULT_OCCURRENCE ?? "1",
1765
+ 10
1766
+ );
1767
+ const count = (faultInjectionCounts.get(point) ?? 0) + 1;
1768
+ faultInjectionCounts.set(point, count);
1769
+ if (!Number.isSafeInteger(occurrence) || occurrence < 1 || count !== occurrence)
1770
+ return;
1771
+ const signalFile = process.env.RIEBECKITE_SSG_FAULT_SIGNAL_FILE;
1772
+ if (!signalFile) return;
1773
+ await writeFile(signalFile, `${point}
1774
+ `, "utf8");
1775
+ await new Promise(() => {
1776
+ });
1087
1777
  }
1088
1778
  async function writeSsgOutputMetrics(metrics) {
1089
1779
  const file = process.env.RIEBECKITE_SSG_METRICS_FILE;
@@ -1092,6 +1782,7 @@ async function writeSsgOutputMetrics(metrics) {
1092
1782
  `, "utf8");
1093
1783
  }
1094
1784
  async function measure(timingsMs, name, fn) {
1785
+ if (!timingsMs) return await fn();
1095
1786
  const start = performance.now();
1096
1787
  try {
1097
1788
  return await fn();
@@ -1101,6 +1792,7 @@ async function measure(timingsMs, name, fn) {
1101
1792
  }
1102
1793
  }
1103
1794
  function measureSync(timingsMs, name, fn) {
1795
+ if (!timingsMs) return fn();
1104
1796
  const start = performance.now();
1105
1797
  try {
1106
1798
  return fn();
@@ -1156,11 +1848,11 @@ function createRiebeckiteSsg(options = {}) {
1156
1848
  }
1157
1849
 
1158
1850
  // src/vite_plugin.ts
1159
- import path9 from "node:path";
1851
+ import path10 from "node:path";
1160
1852
 
1161
1853
  // src/asset_entries.ts
1162
- import fs5 from "node:fs";
1163
- import path7 from "node:path";
1854
+ import fs6 from "node:fs";
1855
+ import path8 from "node:path";
1164
1856
  import { uniqueStrings } from "@riebeckite/core";
1165
1857
  function writeRiebeckiteAssetEntries(config, paths) {
1166
1858
  writeGeneratedFile(paths.frameworkStyles, createFrameworkStylesEntry());
@@ -1201,12 +1893,12 @@ ${imports}
1201
1893
  }
1202
1894
  function writeGeneratedFile(filePath, contents) {
1203
1895
  if (readGeneratedFile(filePath) === contents) return;
1204
- fs5.mkdirSync(path7.dirname(filePath), { recursive: true });
1205
- fs5.writeFileSync(filePath, contents);
1896
+ fs6.mkdirSync(path8.dirname(filePath), { recursive: true });
1897
+ fs6.writeFileSync(filePath, contents);
1206
1898
  }
1207
1899
  function readGeneratedFile(filePath) {
1208
1900
  try {
1209
- return fs5.readFileSync(filePath, "utf8");
1901
+ return fs6.readFileSync(filePath, "utf8");
1210
1902
  } catch (error) {
1211
1903
  if (isNotFoundError2(error)) return void 0;
1212
1904
  throw error;
@@ -1274,8 +1966,8 @@ ${initializers.map((initializer) => ` ${initializer};`).join("\n")}
1274
1966
  }
1275
1967
 
1276
1968
  // src/vite_runner.ts
1277
- import fs6 from "node:fs/promises";
1278
- import path8 from "node:path";
1969
+ import fs7 from "node:fs/promises";
1970
+ import path9 from "node:path";
1279
1971
  import { build, createServer as createServer2 } from "vite";
1280
1972
  var viteConfigFileNames = [
1281
1973
  "vite.config.ts",
@@ -1301,10 +1993,10 @@ var HonoxApplicationRootError = class extends Error {
1301
1993
  }
1302
1994
  };
1303
1995
  async function resolveHonoxApplication(options) {
1304
- const configRoot = path8.resolve(options.configRoot);
1305
- const appRoot = options.appRoot ? path8.resolve(options.appRoot) : await resolveHonoxApplicationRoot(configRoot, options.startDirectory);
1996
+ const configRoot = path9.resolve(options.configRoot);
1997
+ const appRoot = options.appRoot ? path9.resolve(options.appRoot) : await resolveHonoxApplicationRoot(configRoot, options.startDirectory);
1306
1998
  const workspaceRoot = options.workspaceRoot ?? await findWorkspaceRoot(configRoot);
1307
- const configFile = path8.resolve(
1999
+ const configFile = path9.resolve(
1308
2000
  configRoot,
1309
2001
  options.configFile ?? "riebeckite.config.ts"
1310
2002
  );
@@ -1324,14 +2016,14 @@ async function resolveHonoxApplication(options) {
1324
2016
  }
1325
2017
  async function findWorkspaceRoot(configRoot) {
1326
2018
  try {
1327
- const workspaceConfig = path8.join(configRoot, "pnpm-workspace.yaml");
1328
- return (await fs6.stat(workspaceConfig)).isFile() ? configRoot : void 0;
2019
+ const workspaceConfig = path9.join(configRoot, "pnpm-workspace.yaml");
2020
+ return (await fs7.stat(workspaceConfig)).isFile() ? configRoot : void 0;
1329
2021
  } catch {
1330
2022
  return void 0;
1331
2023
  }
1332
2024
  }
1333
2025
  async function startHonoxDevServer(options) {
1334
- const root = path8.resolve(options.root);
2026
+ const root = path9.resolve(options.root);
1335
2027
  await withWorkingDirectory(root, async () => {
1336
2028
  const server = await createServer2({ root });
1337
2029
  await server.listen();
@@ -1340,7 +2032,7 @@ async function startHonoxDevServer(options) {
1340
2032
  });
1341
2033
  }
1342
2034
  async function buildHonoxApplication(options) {
1343
- const root = path8.resolve(options.root);
2035
+ const root = path9.resolve(options.root);
1344
2036
  await withWorkingDirectory(root, async () => {
1345
2037
  await runTraced(
1346
2038
  options.tracer,
@@ -1385,34 +2077,34 @@ async function findViteApplicationRoots(root, startDirectory) {
1385
2077
  const directory = directories.pop();
1386
2078
  if (!directory) continue;
1387
2079
  if (await hasViteConfig(directory)) applicationRoots.push(directory);
1388
- const entries = await fs6.readdir(directory, { withFileTypes: true });
2080
+ const entries = await fs7.readdir(directory, { withFileTypes: true });
1389
2081
  for (const entry of entries) {
1390
2082
  if (!entry.isDirectory() || entry.name === "node_modules" || entry.name === ".git" || entry.name === "tests") {
1391
2083
  continue;
1392
2084
  }
1393
- directories.push(path8.join(directory, entry.name));
2085
+ directories.push(path9.join(directory, entry.name));
1394
2086
  }
1395
2087
  }
1396
2088
  return applicationRoots;
1397
2089
  }
1398
2090
  async function findViteApplicationRootFrom(startDirectory, configRoot) {
1399
- let directory = path8.resolve(startDirectory);
2091
+ let directory = path9.resolve(startDirectory);
1400
2092
  while (isWithin(configRoot, directory)) {
1401
2093
  if (await hasViteConfig(directory)) return directory;
1402
- const parent = path8.dirname(directory);
2094
+ const parent = path9.dirname(directory);
1403
2095
  if (parent === directory) return void 0;
1404
2096
  directory = parent;
1405
2097
  }
1406
2098
  return void 0;
1407
2099
  }
1408
2100
  function isWithin(parent, target) {
1409
- const relative2 = path8.relative(parent, target);
1410
- return relative2 === "" || !relative2.startsWith("..") && !path8.isAbsolute(relative2);
2101
+ const relative2 = path9.relative(parent, target);
2102
+ return relative2 === "" || !relative2.startsWith("..") && !path9.isAbsolute(relative2);
1411
2103
  }
1412
2104
  async function hasViteConfig(directory) {
1413
2105
  for (const fileName of viteConfigFileNames) {
1414
2106
  try {
1415
- if ((await fs6.stat(path8.join(directory, fileName))).isFile()) return true;
2107
+ if ((await fs7.stat(path9.join(directory, fileName))).isFile()) return true;
1416
2108
  } catch {
1417
2109
  }
1418
2110
  }
@@ -1430,7 +2122,7 @@ var defaultSsrExternals = [
1430
2122
  "vfile-matter",
1431
2123
  "yaml"
1432
2124
  ];
1433
- function riebeckite(options = {}) {
2125
+ function riebeckite(options = {}, onApplicationResolved) {
1434
2126
  let resolvedConfig;
1435
2127
  let application;
1436
2128
  const getConfig = () => {
@@ -1443,7 +2135,7 @@ function riebeckite(options = {}) {
1443
2135
  {
1444
2136
  name: "riebeckite-host-integration",
1445
2137
  async config(userConfig) {
1446
- const root = userConfig.root ? path9.resolve(userConfig.root) : process.cwd();
2138
+ const root = userConfig.root ? path10.resolve(userConfig.root) : process.cwd();
1447
2139
  const appRoot = options.appRoot ?? root;
1448
2140
  const resolvedApplication = await resolveHonoxApplication({
1449
2141
  configRoot: options.configRoot ?? appRoot,
@@ -1453,13 +2145,14 @@ function riebeckite(options = {}) {
1453
2145
  });
1454
2146
  resolvedConfig = resolvedApplication.config;
1455
2147
  application = resolvedApplication;
2148
+ onApplicationResolved?.(resolvedApplication);
1456
2149
  writeRiebeckiteAssetEntries(resolvedConfig, {
1457
- frameworkStyles: path9.join(
2150
+ frameworkStyles: path10.join(
1458
2151
  appRoot,
1459
2152
  "app/.riebeckite/framework-styles.css"
1460
2153
  ),
1461
- pluginStyles: path9.join(appRoot, "app/.riebeckite/plugin-styles.css"),
1462
- themeStyles: path9.join(appRoot, "app/.riebeckite/theme-styles.css")
2154
+ pluginStyles: path10.join(appRoot, "app/.riebeckite/plugin-styles.css"),
2155
+ themeStyles: path10.join(appRoot, "app/.riebeckite/theme-styles.css")
1463
2156
  });
1464
2157
  return {
1465
2158
  define: {
@@ -1497,13 +2190,24 @@ function requireApplication(application) {
1497
2190
  }
1498
2191
  function riebeckiteVite(options = {}) {
1499
2192
  const { ssg, ssrExternals, ...integration } = options;
2193
+ const fallbackSiteRoot = integration.configRoot ?? integration.appRoot;
2194
+ let resolvedSiteRoot;
1500
2195
  return [
1501
- ...riebeckite(integration),
1502
- riebeckiteSsrExternals([...defaultSsrExternals, ...ssrExternals ?? []]),
2196
+ ...riebeckite(integration, (application) => {
2197
+ resolvedSiteRoot = application.configRoot;
2198
+ }),
2199
+ riebeckiteSsrExternals(
2200
+ [...defaultSsrExternals, ...ssrExternals ?? []],
2201
+ () => {
2202
+ if (integration.workspaceRoot) return [];
2203
+ const siteRoot = resolvedSiteRoot ?? fallbackSiteRoot;
2204
+ return siteRoot ? siteRiebeckiteExternals(siteRoot) : [];
2205
+ }
2206
+ ),
1503
2207
  createRiebeckiteSsg(ssg)
1504
2208
  ];
1505
2209
  }
1506
- function riebeckiteSsrExternals(externals) {
2210
+ function riebeckiteSsrExternals(externals, siteExternals = () => []) {
1507
2211
  return {
1508
2212
  name: "riebeckite-ssr-externals",
1509
2213
  config() {
@@ -1511,7 +2215,7 @@ function riebeckiteSsrExternals(externals) {
1511
2215
  environments: {
1512
2216
  ssr: {
1513
2217
  resolve: {
1514
- external: [...externals]
2218
+ external: [...externals, ...siteExternals()]
1515
2219
  }
1516
2220
  }
1517
2221
  }
@@ -1521,6 +2225,7 @@ function riebeckiteSsrExternals(externals) {
1521
2225
  }
1522
2226
  export {
1523
2227
  buildHonoxApplication,
2228
+ createNoChangeFastPathOutputMetrics,
1524
2229
  createRiebeckiteSsg,
1525
2230
  defaultSsgEntry,
1526
2231
  defaultSsrExternals,
@@ -1533,5 +2238,6 @@ export {
1533
2238
  riebeckiteSsg,
1534
2239
  riebeckiteSsgExtensionMap,
1535
2240
  riebeckiteVite,
1536
- startHonoxDevServer
2241
+ startHonoxDevServer,
2242
+ verifyNoChangeFastPath
1537
2243
  };