@riebeckite/honox 0.0.22 → 0.0.24
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.d.ts +2 -2
- package/dist/index.js +848 -142
- package/dist/server.js +6 -5
- package/dist/src/site_externals.d.ts +1 -0
- package/dist/src/ssg_plugin.d.ts +26 -4
- package/dist/src/ui/primitives.d.ts +6 -18
- package/dist/src/ui/primitives.js +4 -18
- package/dist/src/vite_plugin.d.ts +2 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,18 +1,42 @@
|
|
|
1
1
|
// src/config_loader.ts
|
|
2
|
-
import
|
|
3
|
-
import
|
|
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/
|
|
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 =
|
|
11
|
-
if (!
|
|
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 =
|
|
15
|
-
const packageJson = JSON.parse(
|
|
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 =
|
|
41
|
-
if (
|
|
42
|
-
return
|
|
43
|
-
(entry) => findWorkspacePackageDirectories(
|
|
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:
|
|
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:
|
|
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
|
|
135
|
+
import path3 from "node:path";
|
|
112
136
|
function resolveHonoxConfig(config, appRoot) {
|
|
113
137
|
return {
|
|
114
138
|
...config,
|
|
115
|
-
buildDirectory:
|
|
116
|
-
outputDirectory:
|
|
139
|
+
buildDirectory: path3.join(appRoot, ".riebeckite"),
|
|
140
|
+
outputDirectory: path3.join(appRoot, "dist"),
|
|
117
141
|
content: {
|
|
118
142
|
...config.content,
|
|
119
|
-
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
|
|
128
|
-
|
|
151
|
+
const temporaryDirectory = await fs3.mkdtemp(
|
|
152
|
+
path4.join(options.configRoot, ".riebeckite-config-")
|
|
129
153
|
);
|
|
130
|
-
const outputFile =
|
|
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(
|
|
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
|
|
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
|
|
163
|
-
import
|
|
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 =
|
|
191
|
-
islandDirectory =
|
|
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
|
|
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 =
|
|
255
|
-
return relativePath !== "" && !relativePath.startsWith(`..${
|
|
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
|
|
281
|
-
import
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
580
|
+
const root = path7.resolve(publicDir);
|
|
553
581
|
const walk = async (directory) => {
|
|
554
|
-
const entries = await
|
|
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 =
|
|
585
|
+
const full = path7.join(directory, entry.name);
|
|
558
586
|
if (entry.isDirectory()) {
|
|
559
587
|
await walk(full);
|
|
560
588
|
} else {
|
|
561
|
-
paths.add(
|
|
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 =
|
|
624
|
+
const file = path7.join(publicDir, logicalPath);
|
|
597
625
|
try {
|
|
598
|
-
return (await
|
|
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:
|
|
786
|
+
version: 4,
|
|
716
787
|
appFingerprint,
|
|
717
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
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] =
|
|
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
|
-
|
|
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
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
asset
|
|
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
|
-
(
|
|
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
|
|
814
|
-
if (siteOwnedOutputPaths.has(
|
|
815
|
-
shadowedOutputs.add(
|
|
916
|
+
const path11 = output.path;
|
|
917
|
+
if (siteOwnedOutputPaths.has(path11)) {
|
|
918
|
+
shadowedOutputs.add(path11);
|
|
816
919
|
continue;
|
|
817
920
|
}
|
|
818
|
-
const cached = previousOutputCache.outputs[
|
|
921
|
+
const cached = previousOutputCache.outputs[path11];
|
|
819
922
|
if (!cached) continue;
|
|
820
923
|
this.emitFile({
|
|
821
924
|
type: "asset",
|
|
822
|
-
fileName:
|
|
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[
|
|
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
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
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
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
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
|
|
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(
|
|
1041
|
+
return findOversizedAssets(path11, relativePath);
|
|
898
1042
|
}
|
|
899
1043
|
if (!entry.isFile()) return [];
|
|
900
|
-
const size = (await stat(
|
|
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
|
|
918
|
-
if (siteOwnedOutputPaths.has(
|
|
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(
|
|
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(
|
|
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 ===
|
|
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.
|
|
1034
|
-
(entry) => typeof entry === "object" && entry !== null && typeof entry.
|
|
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(
|
|
1038
|
-
const directory = dirname(
|
|
1415
|
+
async function saveOutputCache(path11, state, outputDirectory, timingsMs) {
|
|
1416
|
+
const directory = dirname(path11);
|
|
1039
1417
|
await mkdir(directory, { recursive: true });
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
(
|
|
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,
|
|
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
|
|
1083
|
-
|
|
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
|
|
1086
|
-
|
|
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
|
|
1851
|
+
import path10 from "node:path";
|
|
1160
1852
|
|
|
1161
1853
|
// src/asset_entries.ts
|
|
1162
|
-
import
|
|
1163
|
-
import
|
|
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
|
-
|
|
1205
|
-
|
|
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
|
|
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
|
|
1278
|
-
import
|
|
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 =
|
|
1305
|
-
const appRoot = options.appRoot ?
|
|
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 =
|
|
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 =
|
|
1328
|
-
return (await
|
|
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 =
|
|
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 =
|
|
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
|
|
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(
|
|
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 =
|
|
2091
|
+
let directory = path9.resolve(startDirectory);
|
|
1400
2092
|
while (isWithin(configRoot, directory)) {
|
|
1401
2093
|
if (await hasViteConfig(directory)) return directory;
|
|
1402
|
-
const parent =
|
|
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 =
|
|
1410
|
-
return relative2 === "" || !relative2.startsWith("..") && !
|
|
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
|
|
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 ?
|
|
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:
|
|
2150
|
+
frameworkStyles: path10.join(
|
|
1458
2151
|
appRoot,
|
|
1459
2152
|
"app/.riebeckite/framework-styles.css"
|
|
1460
2153
|
),
|
|
1461
|
-
pluginStyles:
|
|
1462
|
-
themeStyles:
|
|
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
|
-
|
|
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
|
};
|