@askrjs/askr 0.2.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/access-CEA2pOry.js +1046 -0
  3. package/dist/actions/index.d.ts +16 -2
  4. package/dist/actions/index.js +5 -5
  5. package/dist/{activity-ZxJXI9hF.js → activity-oy2YFTFF.js} +22 -16
  6. package/dist/benchmark.js +8 -9
  7. package/dist/boot/index.d.ts +21 -9
  8. package/dist/boot/index.js +1 -1
  9. package/dist/{boot-D4GaLA0d.js → boot-BMBMWUGI.js} +680 -440
  10. package/dist/capabilities-Bbf0mlp9.js +42 -0
  11. package/dist/components/index.d.ts +11 -8
  12. package/dist/components/index.js +1 -1
  13. package/dist/{compose-ref-Bfaf4kUC.js → compose-ref-v0qGlHoN.js} +7 -1
  14. package/dist/control/index.d.ts +20 -2
  15. package/dist/control/index.js +1 -1
  16. package/dist/{control-57x3wa4J.js → control-BR_mrX__.js} +4 -4
  17. package/dist/core.d.ts +163 -0
  18. package/dist/csp-nonce-BfFWcd0w.js +173 -0
  19. package/dist/data/index.d.ts +74 -2
  20. package/dist/data/index.js +3 -3
  21. package/dist/{data-B7XCDeC8.js → data-2edW0jhF.js} +133 -138
  22. package/dist/{data-runtime-7t_Y0iv3.js → data-runtime-CxoY98lX.js} +22 -15
  23. package/dist/{deferred-BELyvMsC.js → deferred-CbgNhf67.js} +5 -5
  24. package/dist/dom-renderer.d.ts +77 -0
  25. package/dist/domains/actions.d.ts +70 -0
  26. package/dist/domains/component.d.ts +119 -0
  27. package/dist/domains/context.d.ts +73 -0
  28. package/dist/domains/control.d.ts +328 -0
  29. package/dist/domains/data.d.ts +461 -0
  30. package/dist/domains/diagnostics.d.ts +21 -0
  31. package/dist/domains/lifecycle.d.ts +112 -0
  32. package/dist/domains/refs.d.ts +18 -0
  33. package/dist/domains/renderer.d.ts +127 -0
  34. package/dist/domains/routing.d.ts +501 -0
  35. package/dist/domains/scheduler.d.ts +79 -0
  36. package/dist/domains/server.d.ts +112 -0
  37. package/dist/domains/state.d.ts +129 -0
  38. package/dist/domains/telemetry.d.ts +29 -0
  39. package/dist/eager-control.d.ts +8 -0
  40. package/dist/{effect-4LwBwV58.js → effect-C3Zsc1k6.js} +14 -41
  41. package/dist/{types-DkVJhVs9.d.ts → elements.d.ts} +84 -42
  42. package/dist/{for-internal-DeoSg_AV.js → for-state-BDykeOJL.js} +623 -511
  43. package/dist/foundations/icon/index.d.ts +67 -16
  44. package/dist/foundations/icon/index.js +1 -1
  45. package/dist/foundations/index.d.ts +26 -3
  46. package/dist/foundations/index.js +1 -1
  47. package/dist/foundations/interactions/index.d.ts +73 -34
  48. package/dist/foundations/interactions/index.js +1 -1
  49. package/dist/foundations/state/index.d.ts +12 -5
  50. package/dist/foundations/state/index.js +1 -1
  51. package/dist/foundations/structures/index.d.ts +44 -3
  52. package/dist/foundations/structures/index.js +1 -1
  53. package/dist/foundations/utilities/index.d.ts +34 -3
  54. package/dist/foundations/utilities/index.js +1 -1
  55. package/dist/fx/index.d.ts +72 -25
  56. package/dist/fx/index.js +8 -8
  57. package/dist/index.d.ts +121 -4
  58. package/dist/index.js +316 -47
  59. package/dist/{component-internal-Dl02WQKS.js → instance-VAFYVJOa.js} +495 -283
  60. package/dist/{interactions-D102JxBR.js → interactions-CTeACdT_.js} +1 -1
  61. package/dist/jsx-dev-runtime.d.ts +51 -11
  62. package/dist/jsx-globals.d.ts +2 -0
  63. package/dist/jsx-runtime-CPJXAOBF.js +43 -0
  64. package/dist/jsx-runtime.d.ts +17 -3
  65. package/dist/jsx-runtime.js +2 -42
  66. package/dist/jsx.d.ts +108 -0
  67. package/dist/{manifest-D6rfj_jC.js → manifest-D1P3UNWV.js} +4 -9
  68. package/dist/{navigate-C46GtScG.js → navigate-prC5V8XV.js} +204 -476
  69. package/dist/{navigate-wel1e6cU.d.ts → navigation.d.ts} +10 -8
  70. package/dist/{readable-CEcteIDE.js → notify-DxxuDPeY.js} +96 -34
  71. package/dist/{query-registry-Bua3cDyb.js → query-registry-LrYpQsDJ.js} +1 -1
  72. package/dist/{compose-ref-DOJHoW5X.d.ts → refs.d.ts} +11 -7
  73. package/dist/{ssr-BaC5qHEw.js → render-resolved-BC0W7_5b.js} +165 -558
  74. package/dist/{renderer-BhXofszH.js → renderer-CT0HeY58.js} +6866 -5715
  75. package/dist/{resolution-t8J0yNaU.js → resolution-3DskUkjG.js} +189 -180
  76. package/dist/{resource-operation-B8q-QRUx.js → resource-18-sFRl9.js} +98 -58
  77. package/dist/resources/index.d.ts +64 -46
  78. package/dist/resources/index.js +215 -9
  79. package/dist/route-activity.d.ts +21 -0
  80. package/dist/{route-matching-Bo6UqG-5.js → route-matching-CUzZ31gT.js} +68 -82
  81. package/dist/router/index.d.ts +378 -79
  82. package/dist/router/index.js +9 -9
  83. package/dist/router-internal-7nuNEHfG.js +36 -0
  84. package/dist/scope-Cq_iYbTR.js +403 -0
  85. package/dist/{selector-C7rIpOnN.js → selector-BSPVkjKP.js} +41 -41
  86. package/dist/{shared-CGrx2AOL.js → shared-kU8yvryp.js} +3 -6
  87. package/dist/{snapshot-source-BB42xgwb.js → snapshot-source-An4iKajG.js} +1 -1
  88. package/dist/ssg/index.d.ts +51 -15
  89. package/dist/ssg/index.js +269 -209
  90. package/dist/ssr/index.d.ts +121 -68
  91. package/dist/ssr/index.js +2 -1
  92. package/dist/ssr-CiNm6Vzl.js +393 -0
  93. package/dist/{ssr-DSHd_Q9K.js → ssr-EPjwJH5R.js} +5 -1
  94. package/dist/{state-BzhOW2TX.js → state-BXJfudq1.js} +10 -15
  95. package/dist/{structures-BwfeO4Iz.js → structures-CjEzfj4h.js} +37 -57
  96. package/dist/{index-r5THqzPN.d.ts → structures.d.ts} +57 -36
  97. package/dist/testing/index.d.ts +125 -24
  98. package/dist/testing/index.js +26 -62
  99. package/dist/timer-C7uqnJVt.js +85 -0
  100. package/dist/{index-B1gu0_m0.d.ts → utilities.d.ts} +24 -13
  101. package/dist/{verify-hydration-_Q6avFp0.js → verify-hydration-ikaxotf7.js} +8 -5
  102. package/package.json +13 -15
  103. package/dist/access-Rr7CMa06.js +0 -507
  104. package/dist/activity-CAJ5eho-.d.ts +0 -14
  105. package/dist/benchmark.d.ts +0 -26
  106. package/dist/cleanup-DsrbV-rr.js +0 -535
  107. package/dist/component-cleanup-DjpwO9WX.js +0 -143
  108. package/dist/component-scope-4I3yEMkb.js +0 -195
  109. package/dist/control-DdFw-Caz.d.ts +0 -8
  110. package/dist/csp-nonce-C9OuML0U.js +0 -72
  111. package/dist/css-BuPaTOfe.js +0 -70
  112. package/dist/fastlane-Buz1Rvlr.js +0 -166
  113. package/dist/index-DSqXWc77.d.ts +0 -1402
  114. package/dist/jsx-runtime-BTzDdBJ_.d.ts +0 -2
  115. package/dist/jsx-runtime-Cdd6Gkb0.d.ts +0 -38
  116. package/dist/lifecycle-batch-Be-CLDjV.js +0 -253
  117. package/dist/lifecycle-operations-C5m03NQc.js +0 -195
package/dist/ssg/index.js CHANGED
@@ -1,14 +1,109 @@
1
- import "../access-Rr7CMa06.js";
2
- import { n as createRouteRegistry } from "../manifest-D6rfj_jC.js";
3
- import { o as addRouteBasePath } from "../route-matching-Bo6UqG-5.js";
4
- import { c as route, n as resolveDeferredValues } from "../deferred-BELyvMsC.js";
5
- import { S as RouteDataLoadError, c as prepareRouteHydrationData, t as bindResolvedRouteData, v as getRenderHandler, y as _preloadRouteHandler } from "../resolution-t8J0yNaU.js";
6
- import { n as renderDocument } from "../ssr-DSHd_Q9K.js";
7
- import { n as renderToString } from "../ssr-BaC5qHEw.js";
8
- import * as fs from "node:fs/promises";
9
- import * as pathModule from "node:path";
1
+ import "../access-CEA2pOry.js";
2
+ import { n as createRouteRegistry } from "../manifest-D1P3UNWV.js";
3
+ import { _ as prepareRouteHydrationData, d as _preloadRouteHandler, m as RouteDataLoadError, t as bindResolvedRouteData, u as getRenderHandler } from "../resolution-3DskUkjG.js";
4
+ import { o as addRouteBasePath } from "../route-matching-CUzZ31gT.js";
5
+ import { c as route, n as resolveDeferredValues } from "../deferred-CbgNhf67.js";
6
+ import { n as renderDocument } from "../ssr-EPjwJH5R.js";
7
+ import { n as renderToString } from "../ssr-CiNm6Vzl.js";
8
+ import * as fs$1 from "node:fs/promises";
10
9
  import * as os from "node:os";
11
- import * as fsSync from "node:fs";
10
+ import * as pathModule from "node:path";
11
+ import * as fs from "node:fs";
12
+ //#region src/ssg/parallelism.ts
13
+ function resolveParallelism(requested) {
14
+ if (requested !== "auto") return Math.max(1, requested ?? 1);
15
+ const maybeNavigator = globalThis;
16
+ const envWorkers = Number(maybeNavigator.process?.env?.ASKR_SSG_WORKERS ?? maybeNavigator.process?.env?.NUMBER_OF_PROCESSORS ?? maybeNavigator.process?.env?.UV_THREADPOOL_SIZE);
17
+ if (Number.isFinite(envWorkers) && envWorkers > 0) return Math.max(1, Math.trunc(envWorkers));
18
+ if (typeof maybeNavigator.navigator?.hardwareConcurrency === "number") return Math.max(1, maybeNavigator.navigator.hardwareConcurrency);
19
+ const availableParallelism = os.availableParallelism;
20
+ return Math.max(1, availableParallelism?.() ?? os.cpus().length ?? 1);
21
+ }
22
+ //#endregion
23
+ //#region src/ssg/output-publication.ts
24
+ const outputDirectoryQueues = /* @__PURE__ */ new Map();
25
+ async function withOutputDirectoryLock(outputDir, operation) {
26
+ const key = pathModule.resolve(outputDir).normalize("NFC").toLowerCase();
27
+ const previous = outputDirectoryQueues.get(key) ?? Promise.resolve();
28
+ let release;
29
+ const current = new Promise((resolve) => {
30
+ release = resolve;
31
+ });
32
+ const tail = previous.then(() => current, () => current);
33
+ outputDirectoryQueues.set(key, tail);
34
+ await previous.catch(() => void 0);
35
+ try {
36
+ return await operation();
37
+ } finally {
38
+ release();
39
+ if (outputDirectoryQueues.get(key) === tail) outputDirectoryQueues.delete(key);
40
+ }
41
+ }
42
+ async function createStagingDirectory(outputDir) {
43
+ const parent = pathModule.dirname(outputDir);
44
+ const base = pathModule.basename(outputDir);
45
+ await fs$1.mkdir(parent, { recursive: true });
46
+ return fs$1.mkdtemp(pathModule.join(parent, `.${base}.askr-staging-`));
47
+ }
48
+ function resolveAssetDestination(outputDir, source) {
49
+ const relativeDestination = source.to ?? pathModule.basename(source.from);
50
+ const destination = pathModule.resolve(outputDir, relativeDestination);
51
+ const relative = pathModule.relative(pathModule.resolve(outputDir), destination);
52
+ if (relative.startsWith("..") || pathModule.isAbsolute(relative)) throw new Error(`SSG asset destination must stay inside outputDir: ${relativeDestination}`);
53
+ return destination;
54
+ }
55
+ async function copyStaticAssets(assets, outputDir) {
56
+ for (const source of assets ?? []) {
57
+ if (!source.from || source.from.trim().length === 0) throw new Error("SSG asset source requires a non-empty from path");
58
+ const from = pathModule.resolve(source.from);
59
+ const to = resolveAssetDestination(outputDir, source);
60
+ await fs$1.mkdir(pathModule.dirname(to), { recursive: true });
61
+ await fs$1.cp(from, to, {
62
+ recursive: true,
63
+ force: true
64
+ });
65
+ }
66
+ }
67
+ async function replaceOutputDirectory(stagingDir, outputDir, fileOperations = fs$1) {
68
+ const parent = pathModule.dirname(outputDir);
69
+ const backupDir = pathModule.join(parent, `.${pathModule.basename(outputDir)}.askr-backup-${Date.now()}`);
70
+ let backedUp = false;
71
+ let failed = false;
72
+ let failure;
73
+ try {
74
+ try {
75
+ await fileOperations.rename(outputDir, backupDir);
76
+ backedUp = true;
77
+ } catch (error) {
78
+ if (error.code !== "ENOENT") throw error;
79
+ }
80
+ await fileOperations.rename(stagingDir, outputDir);
81
+ await fileOperations.rm(backupDir, {
82
+ recursive: true,
83
+ force: true
84
+ });
85
+ } catch (error) {
86
+ failed = true;
87
+ failure = error;
88
+ if (backedUp) try {
89
+ await fileOperations.rm(outputDir, {
90
+ recursive: true,
91
+ force: true
92
+ });
93
+ await fileOperations.rename(backupDir, outputDir);
94
+ } catch {}
95
+ }
96
+ try {
97
+ await fileOperations.rm(stagingDir, {
98
+ recursive: true,
99
+ force: true
100
+ });
101
+ } catch (error) {
102
+ if (!failed) throw error;
103
+ }
104
+ if (failed) throw failure;
105
+ }
106
+ //#endregion
12
107
  //#region src/ssg/route-utils.ts
13
108
  function getPortableOutputPathKey(filePath) {
14
109
  return filePath.normalize("NFC").replaceAll("\\", "/").toLowerCase();
@@ -312,15 +407,13 @@ function resolveSsgOutputPath(outputDir, filePath) {
312
407
  * Write rendered routes to disk
313
408
  * Creates outputDir/{route-path}/index.html structure
314
409
  */
315
- async function writeStaticFiles(results, outputDir, options = {}, fileOperations = fs) {
410
+ async function writeStaticFiles(results, outputDir, options = {}, fileOperations = fs$1) {
316
411
  await fileOperations.mkdir(outputDir, { recursive: true });
317
412
  for (const result of results) {
318
413
  if (result.status !== "removed") continue;
319
414
  const fullPath = resolveSsgOutputPath(outputDir, result.filePath);
320
- if (fsSync.existsSync(fullPath)) {
321
- await fileOperations.rm(fullPath, { force: true });
322
- await pruneEmptyDirs(pathModule.dirname(fullPath), outputDir, fileOperations);
323
- }
415
+ await fileOperations.rm(fullPath, { force: true });
416
+ await pruneEmptyDirs(pathModule.dirname(fullPath), outputDir, fileOperations);
324
417
  }
325
418
  const pendingWrites = [];
326
419
  const reportedErrors = /* @__PURE__ */ new Set();
@@ -348,23 +441,42 @@ async function writeStaticFiles(results, outputDir, options = {}, fileOperations
348
441
  for (const dir of directories) await fileOperations.mkdir(dir, { recursive: true });
349
442
  const concurrency = Math.max(1, Math.min(options.concurrency ?? 8, pendingWrites.length || 1));
350
443
  let nextIndex = 0;
444
+ let failed = false;
445
+ let failure;
446
+ const recordFailure = (error) => {
447
+ if (!failed) {
448
+ failed = true;
449
+ failure = error;
450
+ }
451
+ };
351
452
  const worker = async () => {
352
- while (true) {
353
- const current = nextIndex;
354
- nextIndex += 1;
355
- if (current >= pendingWrites.length) return;
356
- const result = pendingWrites[current];
357
- const fullPath = resolveSsgOutputPath(outputDir, result.filePath);
358
- const tempPath = pathModule.join(pathModule.dirname(fullPath), `.${pathModule.basename(fullPath)}.askr-${process.pid}-${current}.tmp`);
359
- try {
360
- await fileOperations.writeFile(tempPath, result.html, "utf8");
361
- await fileOperations.rename(tempPath, fullPath);
362
- } finally {
363
- await fileOperations.rm(tempPath, { force: true });
453
+ try {
454
+ while (!failed) {
455
+ const current = nextIndex;
456
+ nextIndex += 1;
457
+ if (current >= pendingWrites.length) return;
458
+ const result = pendingWrites[current];
459
+ const fullPath = resolveSsgOutputPath(outputDir, result.filePath);
460
+ const tempPath = pathModule.join(pathModule.dirname(fullPath), `.${pathModule.basename(fullPath)}.askr-${process.pid}-${current}.tmp`);
461
+ try {
462
+ await fileOperations.writeFile(tempPath, result.html, "utf8");
463
+ await fileOperations.rename(tempPath, fullPath);
464
+ } catch (error) {
465
+ recordFailure(error);
466
+ } finally {
467
+ try {
468
+ await fileOperations.rm(tempPath, { force: true });
469
+ } catch (error) {
470
+ recordFailure(error);
471
+ }
472
+ }
364
473
  }
474
+ } catch (error) {
475
+ recordFailure(error);
365
476
  }
366
477
  };
367
478
  await Promise.all(Array.from({ length: concurrency }, () => worker()));
479
+ if (failed) throw failure;
368
480
  }
369
481
  async function pruneEmptyDirs(startDir, rootDir, fileOperations) {
370
482
  let current = startDir;
@@ -372,8 +484,12 @@ async function pruneEmptyDirs(startDir, rootDir, fileOperations) {
372
484
  while (pathModule.resolve(current) !== normalizedRoot) {
373
485
  const relative = pathModule.relative(normalizedRoot, current);
374
486
  if (relative === ".." || relative.startsWith(`..${pathModule.sep}`) || pathModule.isAbsolute(relative)) break;
375
- if (!fsSync.existsSync(current)) break;
376
- if ((await fileOperations.readdir(current)).length > 0) break;
487
+ try {
488
+ if ((await fileOperations.readdir(current)).length > 0) break;
489
+ } catch (error) {
490
+ if (error.code === "ENOENT") break;
491
+ throw error;
492
+ }
377
493
  await fileOperations.rmdir(current);
378
494
  current = pathModule.dirname(current);
379
495
  }
@@ -464,8 +580,8 @@ function resultToMetadata(result) {
464
580
  */
465
581
  async function writeMetadata(metadata, outputDir) {
466
582
  const filePath = pathModule.join(outputDir, "metadata.json");
467
- await fs.mkdir(outputDir, { recursive: true });
468
- await fs.writeFile(filePath, JSON.stringify(metadata, null, 2), "utf8");
583
+ await fs$1.mkdir(outputDir, { recursive: true });
584
+ await fs$1.writeFile(filePath, JSON.stringify(metadata, null, 2), "utf8");
469
585
  }
470
586
  const MANIFEST_DIR = ".askr";
471
587
  const MANIFEST_FILE = "ssg-manifest.json";
@@ -482,9 +598,9 @@ function getIncrementalManifestPath(outputDir) {
482
598
  }
483
599
  function readIncrementalManifest(outputDir, seed) {
484
600
  const manifestPath = getIncrementalManifestPath(outputDir);
485
- if (!fsSync.existsSync(manifestPath)) return null;
601
+ if (!fs.existsSync(manifestPath)) return null;
486
602
  try {
487
- const raw = fsSync.readFileSync(manifestPath, "utf8");
603
+ const raw = fs.readFileSync(manifestPath, "utf8");
488
604
  const parsed = JSON.parse(raw);
489
605
  if (parsed.schemaVersion !== 1 || parsed.seed !== seed || !Array.isArray(parsed.routes)) return null;
490
606
  const routes = parsed.routes.filter((entry) => !!entry && typeof entry.routeId === "string" && typeof entry.path === "string" && typeof entry.filePath === "string" && isContainedManifestFilePath(outputDir, entry.filePath) && Array.isArray(entry.invalidationKeys) && (entry.htmlHash === null || typeof entry.htmlHash === "string") && (entry.lastStatus === "success" || entry.lastStatus === "error"));
@@ -510,20 +626,20 @@ function isContainedManifestFilePath(outputDir, filePath) {
510
626
  async function writeIncrementalManifest(manifest, outputDir) {
511
627
  if (!manifest.routes.every((entry) => isContainedManifestFilePath(outputDir, entry.filePath))) throw new Error("SSG manifest contains a path outside outputDir.");
512
628
  const manifestPath = getIncrementalManifestPath(outputDir);
513
- await fs.mkdir(pathModule.dirname(manifestPath), { recursive: true });
514
- await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2), "utf8");
629
+ await fs$1.mkdir(pathModule.dirname(manifestPath), { recursive: true });
630
+ await fs$1.writeFile(manifestPath, JSON.stringify(manifest, null, 2), "utf8");
515
631
  }
516
632
  function getExistingOutputFileSize(outputDir, filePath) {
517
633
  const fullPath = resolveSsgOutputPath(outputDir, filePath);
518
- if (!fsSync.existsSync(fullPath)) return 0;
634
+ if (!fs.existsSync(fullPath)) return 0;
519
635
  try {
520
- return fsSync.statSync(fullPath).size;
636
+ return fs.statSync(fullPath).size;
521
637
  } catch {
522
638
  return 0;
523
639
  }
524
640
  }
525
641
  function outputFileExists(outputDir, filePath) {
526
- return fsSync.existsSync(resolveSsgOutputPath(outputDir, filePath));
642
+ return fs.existsSync(resolveSsgOutputPath(outputDir, filePath));
527
643
  }
528
644
  //#endregion
529
645
  //#region src/ssg/generation-plan.ts
@@ -671,107 +787,114 @@ function routeRegistryToRouteConfigs(registry) {
671
787
  }
672
788
  //#endregion
673
789
  //#region src/ssg/create-static-gen.ts
674
- function resolveParallelism(requested) {
675
- if (requested !== "auto") return Math.max(1, requested ?? 1);
676
- const maybeNavigator = globalThis;
677
- const envWorkers = Number(maybeNavigator.process?.env?.ASKR_SSG_WORKERS ?? maybeNavigator.process?.env?.NUMBER_OF_PROCESSORS ?? maybeNavigator.process?.env?.UV_THREADPOOL_SIZE);
678
- if (Number.isFinite(envWorkers) && envWorkers > 0) return Math.max(1, Math.trunc(envWorkers));
679
- if (typeof maybeNavigator.navigator?.hardwareConcurrency === "number") return Math.max(1, maybeNavigator.navigator.hardwareConcurrency);
680
- const availableParallelism = os.availableParallelism;
681
- return Math.max(1, availableParallelism?.() ?? os.cpus().length ?? 1);
682
- }
683
- const outputDirectoryQueues = /* @__PURE__ */ new Map();
684
- async function withOutputDirectoryLock(outputDir, operation) {
685
- const key = pathModule.resolve(outputDir).normalize("NFC").toLowerCase();
686
- const previous = outputDirectoryQueues.get(key) ?? Promise.resolve();
687
- let release;
688
- const current = new Promise((resolve) => {
689
- release = resolve;
690
- });
691
- const tail = previous.then(() => current, () => current);
692
- outputDirectoryQueues.set(key, tail);
693
- await previous.catch(() => void 0);
694
- try {
695
- return await operation();
696
- } finally {
697
- release();
698
- if (outputDirectoryQueues.get(key) === tail) outputDirectoryQueues.delete(key);
699
- }
700
- }
701
- async function createStagingDirectory(outputDir) {
702
- const parent = pathModule.dirname(outputDir);
703
- const base = pathModule.basename(outputDir);
704
- await fs.mkdir(parent, { recursive: true });
705
- return fs.mkdtemp(pathModule.join(parent, `.${base}.askr-staging-`));
706
- }
707
- function resolveAssetDestination(outputDir, source) {
708
- const relativeDestination = source.to ?? pathModule.basename(source.from);
709
- const destination = pathModule.resolve(outputDir, relativeDestination);
710
- const relative = pathModule.relative(pathModule.resolve(outputDir), destination);
711
- if (relative.startsWith("..") || pathModule.isAbsolute(relative)) throw new Error(`SSG asset destination must stay inside outputDir: ${relativeDestination}`);
712
- return destination;
713
- }
714
- async function copyStaticAssets(assets, outputDir) {
715
- for (const source of assets ?? []) {
716
- if (!source.from || source.from.trim().length === 0) throw new Error("SSG asset source requires a non-empty from path");
717
- const from = pathModule.resolve(source.from);
718
- const to = resolveAssetDestination(outputDir, source);
719
- await fs.mkdir(pathModule.dirname(to), { recursive: true });
720
- await fs.cp(from, to, {
721
- recursive: true,
722
- force: true
790
+ /**
791
+ * Turn selected routes and their renders into results and manifest entries.
792
+ *
793
+ * A route arrives in one of three states: unchanged and therefore skipped,
794
+ * rendered successfully, or rendered with an error. Each produces a different
795
+ * result shape and a different manifest entry, and a successful render is only
796
+ * written when its HTML actually differs from what the previous build left.
797
+ */
798
+ function assembleRouteResults({ selected, renderedByRouteId, routeResultsById, dataMap, effectiveMode, outputDir }) {
799
+ let cacheHits = 0;
800
+ const nextManifestRoutes = [];
801
+ for (const entry of selected) {
802
+ const { descriptor, previous, reason } = entry;
803
+ const resolvedData = resolveSsgRouteData(dataMap, descriptor.route.path, descriptor.path);
804
+ const baseData = resolvedData.hasData ? resolvedData.data : void 0;
805
+ const resourceCount = resolvedData.hasData && baseData ? Object.keys(baseData).length : 0;
806
+ if (reason === "unchanged") {
807
+ routeResultsById.set(descriptor.routeId, {
808
+ path: descriptor.path,
809
+ filePath: descriptor.filePath,
810
+ html: "",
811
+ fileSize: previous?.htmlHash !== null && previous !== null ? getExistingOutputFileSize(outputDir, descriptor.filePath) : 0,
812
+ renderDuration: 0,
813
+ resourceCount,
814
+ status: "skipped",
815
+ reason: "unchanged",
816
+ written: false
817
+ });
818
+ if (previous) nextManifestRoutes.push({
819
+ ...previous,
820
+ path: descriptor.path,
821
+ filePath: descriptor.filePath,
822
+ invalidationKeys: descriptor.invalidationKeys.slice()
823
+ });
824
+ continue;
825
+ }
826
+ const rendered = renderedByRouteId.get(descriptor.routeId);
827
+ if (!rendered) throw new Error(`Missing rendered result for route "${descriptor.path}"`);
828
+ const nextResult = {
829
+ ...rendered,
830
+ path: descriptor.path,
831
+ filePath: descriptor.filePath,
832
+ resourceCount,
833
+ reason,
834
+ written: false
835
+ };
836
+ if (nextResult.status === "success") {
837
+ const htmlDigest = hashHtml(nextResult.html);
838
+ const shouldWrite = effectiveMode === "full" || !previous || previous.htmlHash !== htmlDigest || !outputFileExists(outputDir, descriptor.filePath);
839
+ nextResult.written = shouldWrite;
840
+ if (!shouldWrite) cacheHits += 1;
841
+ nextManifestRoutes.push({
842
+ routeId: descriptor.routeId,
843
+ path: descriptor.path,
844
+ filePath: descriptor.filePath,
845
+ invalidationKeys: descriptor.invalidationKeys.slice(),
846
+ htmlHash: htmlDigest,
847
+ lastStatus: "success"
848
+ });
849
+ } else nextManifestRoutes.push({
850
+ routeId: descriptor.routeId,
851
+ path: descriptor.path,
852
+ filePath: descriptor.filePath,
853
+ invalidationKeys: descriptor.invalidationKeys.slice(),
854
+ htmlHash: previous?.htmlHash ?? null,
855
+ lastStatus: "error"
723
856
  });
857
+ routeResultsById.set(descriptor.routeId, nextResult);
724
858
  }
859
+ return {
860
+ cacheHits,
861
+ nextManifestRoutes
862
+ };
725
863
  }
726
- async function replaceOutputDirectory(stagingDir, outputDir, fileOperations = fs) {
727
- const parent = pathModule.dirname(outputDir);
728
- const backupDir = pathModule.join(parent, `.${pathModule.basename(outputDir)}.askr-backup-${Date.now()}`);
729
- let backedUp = false;
864
+ /**
865
+ * Write a completed generation to disk.
866
+ *
867
+ * A full build is all-or-nothing, so it writes into a sibling staging directory
868
+ * and swaps it into place only once every file, the metadata and the manifest
869
+ * have landed; a failure removes the staging directory and rethrows. Incremental
870
+ * builds write in place, because they are already only touching routes that
871
+ * changed.
872
+ */
873
+ async function publishGeneration({ routeResults, nextManifestRoutes, effectiveMode, seed, concurrency, outputDir, assets, metadata }) {
874
+ const targetOutputDir = effectiveMode === "full" ? await createStagingDirectory(outputDir) : outputDir;
730
875
  try {
731
- try {
732
- await fileOperations.rename(outputDir, backupDir);
733
- backedUp = true;
734
- } catch (error) {
735
- if (error.code !== "ENOENT") throw error;
736
- }
737
- await fileOperations.rename(stagingDir, outputDir);
738
- await fileOperations.rm(backupDir, {
739
- recursive: true,
740
- force: true
741
- });
876
+ const writeStartTime = performance.now();
877
+ await writeStaticFiles(routeResults, targetOutputDir, { concurrency });
878
+ performance.now() - writeStartTime;
879
+ await writeMetadata(metadata, targetOutputDir);
880
+ await writeIncrementalManifest({
881
+ schemaVersion: 1,
882
+ seed,
883
+ mode: effectiveMode,
884
+ routes: nextManifestRoutes
885
+ }, targetOutputDir);
886
+ await copyStaticAssets(assets, targetOutputDir);
887
+ if (effectiveMode === "full") await replaceOutputDirectory(targetOutputDir, outputDir);
742
888
  } catch (error) {
743
- if (backedUp) {
744
- await fileOperations.rm(outputDir, {
889
+ if (effectiveMode === "full") try {
890
+ await fs$1.rm(targetOutputDir, {
745
891
  recursive: true,
746
892
  force: true
747
893
  });
748
- await fileOperations.rename(backupDir, outputDir);
749
- }
894
+ } catch {}
750
895
  throw error;
751
- } finally {
752
- await fileOperations.rm(stagingDir, {
753
- recursive: true,
754
- force: true
755
- });
756
896
  }
757
897
  }
758
- /**
759
- * Create a Static Site Generator
760
- *
761
- * Usage:
762
- * ```ts
763
- * const ssg = createStaticGen({
764
- * registry,
765
- * outputDir: './dist',
766
- * dataOverrides: {
767
- * '/api/posts': { posts: [...] }
768
- * }
769
- * });
770
- *
771
- * const result = await ssg.generate();
772
- * console.log(`Generated ${result.successful}/${result.totalRoutes} routes`);
773
- * ```
774
- */
775
898
  function createStaticGen(options) {
776
899
  let result = null;
777
900
  if (!options || !("registry" in options) || !options.registry) throw new Error("route registry is required");
@@ -824,66 +947,14 @@ function createStaticGen(options) {
824
947
  performance.now() - renderStartTime;
825
948
  const renderedByRouteId = /* @__PURE__ */ new Map();
826
949
  for (let index = 0; index < renderedResults.length; index += 1) renderedByRouteId.set(routesToRender[index].descriptor.routeId, renderedResults[index]);
827
- let cacheHits = 0;
828
- const nextManifestRoutes = [];
829
- for (const entry of selected) {
830
- const { descriptor, previous, reason } = entry;
831
- const resolvedData = resolveSsgRouteData(dataMap, descriptor.route.path, descriptor.path);
832
- const baseData = resolvedData.hasData ? resolvedData.data : void 0;
833
- const resourceCount = resolvedData.hasData && baseData ? Object.keys(baseData).length : 0;
834
- if (reason === "unchanged") {
835
- routeResultsById.set(descriptor.routeId, {
836
- path: descriptor.path,
837
- filePath: descriptor.filePath,
838
- html: "",
839
- fileSize: previous?.htmlHash !== null && previous !== null ? getExistingOutputFileSize(options.outputDir, descriptor.filePath) : 0,
840
- renderDuration: 0,
841
- resourceCount,
842
- status: "skipped",
843
- reason: "unchanged",
844
- written: false
845
- });
846
- if (previous) nextManifestRoutes.push({
847
- ...previous,
848
- path: descriptor.path,
849
- filePath: descriptor.filePath,
850
- invalidationKeys: descriptor.invalidationKeys.slice()
851
- });
852
- continue;
853
- }
854
- const rendered = renderedByRouteId.get(descriptor.routeId);
855
- if (!rendered) throw new Error(`Missing rendered result for route "${descriptor.path}"`);
856
- const nextResult = {
857
- ...rendered,
858
- path: descriptor.path,
859
- filePath: descriptor.filePath,
860
- resourceCount,
861
- reason,
862
- written: false
863
- };
864
- if (nextResult.status === "success") {
865
- const htmlDigest = hashHtml(nextResult.html);
866
- const shouldWrite = effectiveMode === "full" || !previous || previous.htmlHash !== htmlDigest || !outputFileExists(options.outputDir, descriptor.filePath);
867
- nextResult.written = shouldWrite;
868
- if (!shouldWrite) cacheHits += 1;
869
- nextManifestRoutes.push({
870
- routeId: descriptor.routeId,
871
- path: descriptor.path,
872
- filePath: descriptor.filePath,
873
- invalidationKeys: descriptor.invalidationKeys.slice(),
874
- htmlHash: htmlDigest,
875
- lastStatus: "success"
876
- });
877
- } else nextManifestRoutes.push({
878
- routeId: descriptor.routeId,
879
- path: descriptor.path,
880
- filePath: descriptor.filePath,
881
- invalidationKeys: descriptor.invalidationKeys.slice(),
882
- htmlHash: previous?.htmlHash ?? null,
883
- lastStatus: "error"
884
- });
885
- routeResultsById.set(descriptor.routeId, nextResult);
886
- }
950
+ const { cacheHits, nextManifestRoutes } = assembleRouteResults({
951
+ selected,
952
+ renderedByRouteId,
953
+ routeResultsById,
954
+ dataMap,
955
+ effectiveMode,
956
+ outputDir: options.outputDir
957
+ });
887
958
  const removedResults = collectRemovedRouteResults(previousManifest, currentRouteIds);
888
959
  const routeResults = [];
889
960
  for (const routeId of splitRoutes.outputRouteIds) {
@@ -901,27 +972,16 @@ function createStaticGen(options) {
901
972
  for (const routeResult of result.routes) if (routeResult.status === "success") routeResult.written = false;
902
973
  return result;
903
974
  }
904
- const targetOutputDir = effectiveMode === "full" ? await createStagingDirectory(options.outputDir) : options.outputDir;
905
- try {
906
- const writeStartTime = performance.now();
907
- await writeStaticFiles(routeResults, targetOutputDir, { concurrency: resolvedConcurrency });
908
- performance.now() - writeStartTime;
909
- await writeMetadata(resultToMetadata(result), targetOutputDir);
910
- await writeIncrementalManifest({
911
- schemaVersion: 1,
912
- seed,
913
- mode: effectiveMode,
914
- routes: nextManifestRoutes
915
- }, targetOutputDir);
916
- await copyStaticAssets(options.assets, targetOutputDir);
917
- if (effectiveMode === "full") await replaceOutputDirectory(targetOutputDir, options.outputDir);
918
- } catch (error) {
919
- if (effectiveMode === "full") await fs.rm(targetOutputDir, {
920
- recursive: true,
921
- force: true
922
- });
923
- throw error;
924
- }
975
+ await publishGeneration({
976
+ routeResults,
977
+ nextManifestRoutes,
978
+ effectiveMode,
979
+ seed,
980
+ concurrency: resolvedConcurrency,
981
+ outputDir: options.outputDir,
982
+ assets: options.assets,
983
+ metadata: resultToMetadata(result)
984
+ });
925
985
  return result;
926
986
  });
927
987
  },