alepha 0.15.2 → 0.15.4

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 (180) hide show
  1. package/README.md +68 -80
  2. package/dist/api/audits/index.d.ts.map +1 -1
  3. package/dist/api/audits/index.js +8 -0
  4. package/dist/api/audits/index.js.map +1 -1
  5. package/dist/api/files/index.d.ts +170 -170
  6. package/dist/api/files/index.d.ts.map +1 -1
  7. package/dist/api/files/index.js +1 -0
  8. package/dist/api/files/index.js.map +1 -1
  9. package/dist/api/jobs/index.d.ts.map +1 -1
  10. package/dist/api/jobs/index.js +3 -0
  11. package/dist/api/jobs/index.js.map +1 -1
  12. package/dist/api/notifications/index.browser.js +1 -0
  13. package/dist/api/notifications/index.browser.js.map +1 -1
  14. package/dist/api/notifications/index.js +1 -0
  15. package/dist/api/notifications/index.js.map +1 -1
  16. package/dist/api/parameters/index.d.ts +260 -260
  17. package/dist/api/parameters/index.d.ts.map +1 -1
  18. package/dist/api/parameters/index.js +10 -0
  19. package/dist/api/parameters/index.js.map +1 -1
  20. package/dist/api/users/index.d.ts +12 -1
  21. package/dist/api/users/index.d.ts.map +1 -1
  22. package/dist/api/users/index.js +18 -2
  23. package/dist/api/users/index.js.map +1 -1
  24. package/dist/batch/index.d.ts +4 -4
  25. package/dist/bucket/index.d.ts +8 -0
  26. package/dist/bucket/index.d.ts.map +1 -1
  27. package/dist/bucket/index.js +7 -2
  28. package/dist/bucket/index.js.map +1 -1
  29. package/dist/cli/index.d.ts +196 -74
  30. package/dist/cli/index.d.ts.map +1 -1
  31. package/dist/cli/index.js +234 -50
  32. package/dist/cli/index.js.map +1 -1
  33. package/dist/command/index.d.ts +10 -0
  34. package/dist/command/index.d.ts.map +1 -1
  35. package/dist/command/index.js +67 -13
  36. package/dist/command/index.js.map +1 -1
  37. package/dist/core/index.browser.js +28 -21
  38. package/dist/core/index.browser.js.map +1 -1
  39. package/dist/core/index.d.ts.map +1 -1
  40. package/dist/core/index.js +28 -21
  41. package/dist/core/index.js.map +1 -1
  42. package/dist/core/index.native.js +28 -21
  43. package/dist/core/index.native.js.map +1 -1
  44. package/dist/email/index.d.ts +21 -13
  45. package/dist/email/index.d.ts.map +1 -1
  46. package/dist/email/index.js +10561 -4
  47. package/dist/email/index.js.map +1 -1
  48. package/dist/lock/core/index.d.ts +6 -1
  49. package/dist/lock/core/index.d.ts.map +1 -1
  50. package/dist/lock/core/index.js +9 -1
  51. package/dist/lock/core/index.js.map +1 -1
  52. package/dist/mcp/index.d.ts +5 -5
  53. package/dist/orm/index.bun.js +32 -16
  54. package/dist/orm/index.bun.js.map +1 -1
  55. package/dist/orm/index.d.ts +4 -1
  56. package/dist/orm/index.d.ts.map +1 -1
  57. package/dist/orm/index.js +34 -22
  58. package/dist/orm/index.js.map +1 -1
  59. package/dist/react/auth/index.browser.js +2 -1
  60. package/dist/react/auth/index.browser.js.map +1 -1
  61. package/dist/react/auth/index.js +2 -1
  62. package/dist/react/auth/index.js.map +1 -1
  63. package/dist/react/core/index.d.ts +3 -3
  64. package/dist/react/router/index.browser.js +9 -15
  65. package/dist/react/router/index.browser.js.map +1 -1
  66. package/dist/react/router/index.d.ts +305 -407
  67. package/dist/react/router/index.d.ts.map +1 -1
  68. package/dist/react/router/index.js +581 -781
  69. package/dist/react/router/index.js.map +1 -1
  70. package/dist/scheduler/index.d.ts +13 -1
  71. package/dist/scheduler/index.d.ts.map +1 -1
  72. package/dist/scheduler/index.js +42 -4
  73. package/dist/scheduler/index.js.map +1 -1
  74. package/dist/security/index.d.ts +42 -42
  75. package/dist/security/index.d.ts.map +1 -1
  76. package/dist/security/index.js +8 -7
  77. package/dist/security/index.js.map +1 -1
  78. package/dist/server/auth/index.d.ts +167 -167
  79. package/dist/server/compress/index.d.ts.map +1 -1
  80. package/dist/server/compress/index.js +1 -0
  81. package/dist/server/compress/index.js.map +1 -1
  82. package/dist/server/health/index.d.ts +17 -17
  83. package/dist/server/links/index.d.ts +39 -39
  84. package/dist/server/links/index.js +1 -1
  85. package/dist/server/links/index.js.map +1 -1
  86. package/dist/server/static/index.js +7 -2
  87. package/dist/server/static/index.js.map +1 -1
  88. package/dist/server/swagger/index.d.ts +8 -0
  89. package/dist/server/swagger/index.d.ts.map +1 -1
  90. package/dist/server/swagger/index.js +7 -2
  91. package/dist/server/swagger/index.js.map +1 -1
  92. package/dist/sms/index.d.ts +8 -0
  93. package/dist/sms/index.d.ts.map +1 -1
  94. package/dist/sms/index.js +7 -2
  95. package/dist/sms/index.js.map +1 -1
  96. package/dist/system/index.browser.js +734 -12
  97. package/dist/system/index.browser.js.map +1 -1
  98. package/dist/system/index.d.ts +8 -0
  99. package/dist/system/index.d.ts.map +1 -1
  100. package/dist/system/index.js +7 -2
  101. package/dist/system/index.js.map +1 -1
  102. package/dist/vite/index.d.ts +3 -2
  103. package/dist/vite/index.d.ts.map +1 -1
  104. package/dist/vite/index.js +42 -8
  105. package/dist/vite/index.js.map +1 -1
  106. package/dist/websocket/index.d.ts +34 -34
  107. package/dist/websocket/index.d.ts.map +1 -1
  108. package/package.json +9 -4
  109. package/src/api/audits/controllers/AdminAuditController.ts +8 -0
  110. package/src/api/files/controllers/AdminFileStatsController.ts +1 -0
  111. package/src/api/jobs/controllers/AdminJobController.ts +3 -0
  112. package/src/api/logs/TODO.md +13 -10
  113. package/src/api/notifications/controllers/AdminNotificationController.ts +1 -0
  114. package/src/api/parameters/controllers/AdminConfigController.ts +10 -0
  115. package/src/api/users/controllers/AdminIdentityController.ts +3 -0
  116. package/src/api/users/controllers/AdminSessionController.ts +3 -0
  117. package/src/api/users/controllers/AdminUserController.ts +5 -0
  118. package/src/cli/apps/AlephaPackageBuilderCli.ts +9 -0
  119. package/src/cli/atoms/buildOptions.ts +99 -9
  120. package/src/cli/commands/build.ts +150 -32
  121. package/src/cli/commands/db.ts +5 -7
  122. package/src/cli/commands/init.spec.ts +50 -6
  123. package/src/cli/commands/init.ts +28 -5
  124. package/src/cli/providers/ViteDevServerProvider.ts +31 -9
  125. package/src/cli/services/AlephaCliUtils.ts +16 -0
  126. package/src/cli/services/PackageManagerUtils.ts +2 -0
  127. package/src/cli/services/ProjectScaffolder.spec.ts +97 -0
  128. package/src/cli/services/ProjectScaffolder.ts +28 -6
  129. package/src/cli/templates/agentMd.ts +6 -1
  130. package/src/cli/templates/apiAppSecurityTs.ts +11 -0
  131. package/src/cli/templates/apiIndexTs.ts +18 -4
  132. package/src/cli/templates/webAppRouterTs.ts +25 -1
  133. package/src/cli/templates/webHelloComponentTsx.ts +15 -5
  134. package/src/command/helpers/Runner.spec.ts +135 -0
  135. package/src/command/helpers/Runner.ts +4 -1
  136. package/src/command/providers/CliProvider.spec.ts +325 -0
  137. package/src/command/providers/CliProvider.ts +117 -7
  138. package/src/core/Alepha.ts +32 -25
  139. package/src/email/index.workerd.ts +36 -0
  140. package/src/email/providers/WorkermailerEmailProvider.ts +221 -0
  141. package/src/lock/core/primitives/$lock.ts +13 -1
  142. package/src/orm/index.bun.ts +1 -1
  143. package/src/orm/index.ts +2 -6
  144. package/src/orm/providers/drivers/BunSqliteProvider.ts +4 -1
  145. package/src/orm/providers/drivers/CloudflareD1Provider.ts +57 -30
  146. package/src/orm/providers/drivers/DatabaseProvider.ts +9 -1
  147. package/src/orm/providers/drivers/NodeSqliteProvider.ts +4 -1
  148. package/src/react/auth/services/ReactAuth.ts +3 -1
  149. package/src/react/router/atoms/ssrManifestAtom.ts +7 -0
  150. package/src/react/router/hooks/useActive.ts +1 -1
  151. package/src/react/router/hooks/useRouter.ts +1 -1
  152. package/src/react/router/index.ts +4 -0
  153. package/src/react/router/primitives/$page.browser.spec.tsx +24 -24
  154. package/src/react/router/primitives/$page.spec.tsx +0 -32
  155. package/src/react/router/primitives/$page.ts +6 -14
  156. package/src/react/router/providers/ReactBrowserProvider.ts +6 -3
  157. package/src/react/router/providers/ReactPageProvider.ts +1 -1
  158. package/src/react/router/providers/ReactPreloadProvider.spec.ts +142 -0
  159. package/src/react/router/providers/ReactPreloadProvider.ts +85 -0
  160. package/src/react/router/providers/ReactServerProvider.ts +21 -82
  161. package/src/react/router/providers/ReactServerTemplateProvider.spec.ts +210 -0
  162. package/src/react/router/providers/ReactServerTemplateProvider.ts +228 -665
  163. package/src/react/router/providers/SSRManifestProvider.ts +7 -0
  164. package/src/react/router/services/ReactRouter.ts +13 -13
  165. package/src/scheduler/index.workerd.ts +43 -0
  166. package/src/scheduler/providers/CronProvider.ts +53 -6
  167. package/src/scheduler/providers/WorkerdCronProvider.ts +102 -0
  168. package/src/security/__tests__/ServerSecurityProvider.spec.ts +77 -0
  169. package/src/security/providers/ServerSecurityProvider.ts +30 -22
  170. package/src/server/compress/providers/ServerCompressProvider.ts +6 -0
  171. package/src/server/core/providers/NodeHttpServerProvider.spec.ts +9 -3
  172. package/src/server/links/providers/ServerLinksProvider.spec.ts +332 -0
  173. package/src/server/links/providers/ServerLinksProvider.ts +1 -1
  174. package/src/system/index.browser.ts +25 -0
  175. package/src/system/index.workerd.ts +1 -0
  176. package/src/system/providers/FileSystemProvider.ts +8 -0
  177. package/src/system/providers/NodeFileSystemProvider.ts +11 -2
  178. package/src/vite/tasks/buildServer.ts +2 -12
  179. package/src/vite/tasks/generateCloudflare.ts +47 -8
  180. package/src/vite/tasks/generateDocker.ts +4 -0
@@ -156,12 +156,6 @@ var PagePrimitive = class extends Primitive {
156
156
  async fetch(options) {
157
157
  return this.reactPageService.fetch(this.options.path || "", options);
158
158
  }
159
- match(url) {
160
- return false;
161
- }
162
- pathname(config) {
163
- return this.options.path || "";
164
- }
165
159
  };
166
160
  $page[KIND] = PagePrimitive;
167
161
 
@@ -1165,7 +1159,7 @@ var ReactPageProvider = class {
1165
1159
  }
1166
1160
  pathname(name, options = {}) {
1167
1161
  const page = this.page(name);
1168
- if (!page) throw new Error(`Page ${name} not found`);
1162
+ if (!page) throw new AlephaError(`Page ${name} not found`);
1169
1163
  let url = page.path ?? "";
1170
1164
  let parent = page.parent;
1171
1165
  while (parent) {
@@ -1446,164 +1440,461 @@ const isPageRoute = (it) => {
1446
1440
  };
1447
1441
 
1448
1442
  //#endregion
1449
- //#region ../../src/system/providers/FileSystemProvider.ts
1443
+ //#region ../../src/react/router/atoms/ssrManifestAtom.ts
1450
1444
  /**
1451
- * FileSystem interface providing utilities for working with files.
1445
+ * Schema for the SSR manifest atom.
1452
1446
  */
1453
- var FileSystemProvider = class {};
1454
-
1455
- //#endregion
1456
- //#region ../../src/system/providers/MemoryFileSystemProvider.ts
1447
+ const ssrManifestAtomSchema = t.object({
1448
+ base: t.optional(t.string()),
1449
+ preload: t.optional(t.record(t.string(), t.string())),
1450
+ client: t.optional(t.record(t.string(), t.object({
1451
+ file: t.string(),
1452
+ isEntry: t.optional(t.boolean()),
1453
+ imports: t.optional(t.array(t.string())),
1454
+ css: t.optional(t.array(t.string()))
1455
+ }))),
1456
+ devHead: t.optional(t.string())
1457
+ });
1457
1458
  /**
1458
- * In-memory implementation of FileSystemProvider for testing.
1459
+ * SSR Manifest atom containing all manifest data for SSR module preloading.
1459
1460
  *
1460
- * This provider stores all files and directories in memory, making it ideal for
1461
- * unit tests that need to verify file operations without touching the real file system.
1461
+ * This atom is populated at build time by embedding manifest data into the
1462
+ * generated index.js. This approach is optimal for serverless deployments
1463
+ * as it eliminates filesystem reads at runtime.
1462
1464
  *
1463
- * @example
1464
- * ```typescript
1465
- * // In tests, substitute the real FileSystemProvider with MemoryFileSystemProvider
1466
- * const alepha = Alepha.create().with({
1467
- * provide: FileSystemProvider,
1468
- * use: MemoryFileSystemProvider,
1469
- * });
1465
+ * The manifest includes:
1466
+ * - preload: Maps short hash keys to source paths (from viteAlephaSsrPreload)
1467
+ * - client: Maps source files to their output info (file, imports, css)
1468
+ */
1469
+ const ssrManifestAtom = $atom({
1470
+ name: "alepha.react.ssr.manifest",
1471
+ description: "SSR manifest for module preloading",
1472
+ schema: ssrManifestAtomSchema,
1473
+ default: {}
1474
+ });
1475
+
1476
+ //#endregion
1477
+ //#region ../../src/react/router/providers/SSRManifestProvider.ts
1478
+ /**
1479
+ * Provider for SSR manifest data used for module preloading.
1470
1480
  *
1471
- * // Run code that uses FileSystemProvider
1472
- * const service = alepha.inject(MyService);
1473
- * await service.saveFile("test.txt", "Hello World");
1481
+ * The manifest is populated at build time by embedding data into the
1482
+ * generated index.js via the ssrManifestAtom. This eliminates filesystem
1483
+ * reads at runtime, making it optimal for serverless deployments.
1474
1484
  *
1475
- * // Verify the file was written
1476
- * const memoryFs = alepha.inject(MemoryFileSystemProvider);
1477
- * expect(memoryFs.files.get("test.txt")?.toString()).toBe("Hello World");
1478
- * ```
1485
+ * Manifest files are generated during `vite build`:
1486
+ * - manifest.json (client manifest)
1487
+ * - preload-manifest.json (from viteAlephaSsrPreload plugin)
1479
1488
  */
1480
- var MemoryFileSystemProvider = class {
1481
- json = $inject(Json);
1482
- /**
1483
- * In-memory storage for files (path -> content)
1484
- */
1485
- files = /* @__PURE__ */ new Map();
1486
- /**
1487
- * In-memory storage for directories
1488
- */
1489
- directories = /* @__PURE__ */ new Set();
1489
+ var SSRManifestProvider = class {
1490
+ alepha = $inject(Alepha);
1490
1491
  /**
1491
- * Track mkdir calls for test assertions
1492
+ * Get the manifest from the store at runtime.
1493
+ * This ensures the manifest is available even when set after module load.
1492
1494
  */
1493
- mkdirCalls = [];
1495
+ get manifest() {
1496
+ return this.alepha.store.get(ssrManifestAtom) ?? {};
1497
+ }
1494
1498
  /**
1495
- * Track writeFile calls for test assertions
1499
+ * Get the full manifest object.
1496
1500
  */
1497
- writeFileCalls = [];
1501
+ getManifest() {
1502
+ return this.manifest;
1503
+ }
1498
1504
  /**
1499
- * Track readFile calls for test assertions
1505
+ * Get the base path for assets (from Vite's base config).
1506
+ * Returns empty string if base is "/" (default), otherwise returns the base path.
1500
1507
  */
1501
- readFileCalls = [];
1508
+ get base() {
1509
+ return this.manifest.base ?? "";
1510
+ }
1502
1511
  /**
1503
- * Track rm calls for test assertions
1512
+ * Get the preload manifest.
1504
1513
  */
1505
- rmCalls = [];
1514
+ get preloadManifest() {
1515
+ return this.manifest.preload;
1516
+ }
1506
1517
  /**
1507
- * Track join calls for test assertions
1518
+ * Get the client manifest.
1508
1519
  */
1509
- joinCalls = [];
1520
+ get clientManifest() {
1521
+ return this.manifest.client;
1522
+ }
1510
1523
  /**
1511
- * Error to throw on mkdir (for testing error handling)
1524
+ * Resolve a preload key to its source path.
1525
+ *
1526
+ * The key is a short hash injected by viteAlephaSsrPreload plugin,
1527
+ * which maps to the full source path in the preload manifest.
1528
+ *
1529
+ * @param key - Short hash key (e.g., "a1b2c3d4")
1530
+ * @returns Source path (e.g., "src/pages/UserDetail.tsx") or undefined
1512
1531
  */
1513
- mkdirError = null;
1532
+ resolvePreloadKey(key) {
1533
+ return this.preloadManifest?.[key];
1534
+ }
1514
1535
  /**
1515
- * Error to throw on writeFile (for testing error handling)
1536
+ * Get all chunks required for a source file, including transitive dependencies.
1537
+ *
1538
+ * Uses the client manifest to recursively resolve all imported chunks.
1539
+ *
1540
+ * @param sourcePath - Source file path (e.g., "src/pages/Home.tsx")
1541
+ * @returns Array of chunk URLs to preload, or empty array if not found
1516
1542
  */
1517
- writeFileError = null;
1543
+ getChunks(sourcePath) {
1544
+ if (!this.clientManifest) return [];
1545
+ if (!this.findManifestEntry(sourcePath)) return [];
1546
+ const chunks = /* @__PURE__ */ new Set();
1547
+ const visited = /* @__PURE__ */ new Set();
1548
+ this.collectChunksRecursive(sourcePath, chunks, visited);
1549
+ return Array.from(chunks);
1550
+ }
1518
1551
  /**
1519
- * Error to throw on readFile (for testing error handling)
1552
+ * Find manifest entry for a source path, trying different extensions.
1520
1553
  */
1521
- readFileError = null;
1522
- constructor(options = {}) {
1523
- this.mkdirError = options.mkdirError ?? null;
1524
- this.writeFileError = options.writeFileError ?? null;
1525
- this.readFileError = options.readFileError ?? null;
1554
+ findManifestEntry(sourcePath) {
1555
+ if (!this.clientManifest) return void 0;
1556
+ if (this.clientManifest[sourcePath]) return this.clientManifest[sourcePath];
1557
+ const basePath = sourcePath.replace(/\.[^.]+$/, "");
1558
+ for (const ext of [
1559
+ ".tsx",
1560
+ ".ts",
1561
+ ".jsx",
1562
+ ".js"
1563
+ ]) {
1564
+ const pathWithExt = basePath + ext;
1565
+ if (this.clientManifest[pathWithExt]) return this.clientManifest[pathWithExt];
1566
+ }
1526
1567
  }
1527
1568
  /**
1528
- * Join path segments using forward slashes.
1529
- * Uses Node's path.join for proper normalization (handles .. and .)
1569
+ * Recursively collect all chunk URLs for a manifest entry.
1530
1570
  */
1531
- join(...paths) {
1532
- this.joinCalls.push(paths);
1533
- return join(...paths);
1571
+ collectChunksRecursive(key, chunks, visited) {
1572
+ if (visited.has(key)) return;
1573
+ visited.add(key);
1574
+ if (!this.clientManifest) return;
1575
+ const entry = this.clientManifest[key];
1576
+ if (!entry) return;
1577
+ const base = this.base;
1578
+ if (entry.file) chunks.add(`${base}/${entry.file}`);
1579
+ if (entry.css) for (const css of entry.css) chunks.add(`${base}/${css}`);
1580
+ if (entry.imports) for (const imp of entry.imports) {
1581
+ if (imp === "index.html" || imp.endsWith(".html")) continue;
1582
+ this.collectChunksRecursive(imp, chunks, visited);
1583
+ }
1534
1584
  }
1535
1585
  /**
1536
- * Create a FileLike object from various sources.
1586
+ * Collect modulepreload links for a route and its parent chain.
1537
1587
  */
1538
- createFile(options) {
1539
- if ("path" in options) {
1540
- const filePath = options.path;
1541
- const buffer = this.files.get(filePath);
1542
- if (buffer === void 0) throw new Error(`ENOENT: no such file or directory, open '${filePath}'`);
1543
- return {
1544
- name: options.name ?? filePath.split("/").pop() ?? "file",
1545
- type: options.type ?? "application/octet-stream",
1546
- size: buffer.byteLength,
1547
- lastModified: Date.now(),
1548
- stream: () => {
1549
- throw new Error("Stream not implemented in MemoryFileSystemProvider");
1550
- },
1551
- arrayBuffer: async () => buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength),
1552
- text: async () => buffer.toString("utf-8")
1553
- };
1588
+ collectPreloadLinks(route) {
1589
+ if (!this.isAvailable()) return [];
1590
+ const preloadPaths = [];
1591
+ let current = route;
1592
+ while (current) {
1593
+ const preloadKey = current[PAGE_PRELOAD_KEY];
1594
+ if (preloadKey) {
1595
+ const sourcePath = this.resolvePreloadKey(preloadKey);
1596
+ if (sourcePath) preloadPaths.push(sourcePath);
1597
+ }
1598
+ current = current.parent;
1554
1599
  }
1555
- if ("buffer" in options) {
1556
- const buffer = options.buffer;
1557
- return {
1558
- name: options.name ?? "file",
1559
- type: options.type ?? "application/octet-stream",
1560
- size: buffer.byteLength,
1561
- lastModified: Date.now(),
1562
- stream: () => {
1563
- throw new Error("Stream not implemented in MemoryFileSystemProvider");
1564
- },
1565
- arrayBuffer: async () => buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength),
1566
- text: async () => buffer.toString("utf-8")
1600
+ if (preloadPaths.length === 0) return [];
1601
+ return this.getChunksForMultiple(preloadPaths).map((href) => {
1602
+ if (href.endsWith(".css")) return {
1603
+ rel: "preload",
1604
+ href,
1605
+ as: "style",
1606
+ crossorigin: ""
1567
1607
  };
1568
- }
1569
- if ("text" in options) {
1570
- const buffer = Buffer.from(options.text, "utf-8");
1571
1608
  return {
1572
- name: options.name ?? "file.txt",
1573
- type: options.type ?? "text/plain",
1574
- size: buffer.byteLength,
1575
- lastModified: Date.now(),
1576
- stream: () => {
1577
- throw new Error("Stream not implemented in MemoryFileSystemProvider");
1578
- },
1579
- arrayBuffer: async () => buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength),
1580
- text: async () => options.text
1609
+ rel: "modulepreload",
1610
+ href
1581
1611
  };
1582
- }
1583
- throw new Error("MemoryFileSystemProvider.createFile: unsupported options. Only buffer and text are supported.");
1584
- }
1585
- /**
1586
- * Remove a file or directory from memory.
1587
- */
1588
- async rm(path, options) {
1589
- this.rmCalls.push({
1590
- path,
1591
- options
1592
1612
  });
1593
- if (!(this.files.has(path) || this.directories.has(path)) && !options?.force) throw new Error(`ENOENT: no such file or directory, rm '${path}'`);
1594
- if (this.directories.has(path)) if (options?.recursive) {
1595
- this.directories.delete(path);
1596
- for (const filePath of this.files.keys()) if (filePath.startsWith(`${path}/`)) this.files.delete(filePath);
1597
- for (const dirPath of this.directories) if (dirPath.startsWith(`${path}/`)) this.directories.delete(dirPath);
1598
- } else throw new Error(`EISDIR: illegal operation on a directory, rm '${path}'`);
1599
- else this.files.delete(path);
1600
1613
  }
1601
1614
  /**
1602
- * Copy a file or directory in memory.
1615
+ * Get all chunks for multiple source files.
1616
+ *
1617
+ * @param sourcePaths - Array of source file paths
1618
+ * @returns Deduplicated array of chunk URLs
1603
1619
  */
1604
- async cp(src, dest, options) {
1605
- if (this.directories.has(src)) {
1606
- if (!options?.recursive) throw new Error(`Cannot copy directory without recursive option: ${src}`);
1620
+ getChunksForMultiple(sourcePaths) {
1621
+ const allChunks = /* @__PURE__ */ new Set();
1622
+ for (const path of sourcePaths) {
1623
+ const chunks = this.getChunks(path);
1624
+ for (const chunk of chunks) allChunks.add(chunk);
1625
+ }
1626
+ return Array.from(allChunks);
1627
+ }
1628
+ /**
1629
+ * Check if manifest is loaded and available.
1630
+ */
1631
+ isAvailable() {
1632
+ return this.clientManifest !== void 0;
1633
+ }
1634
+ /**
1635
+ * Cached entry assets - computed once at first access.
1636
+ */
1637
+ cachedEntryAssets = null;
1638
+ /**
1639
+ * Get the entry point assets (main entry.js and associated CSS files).
1640
+ *
1641
+ * These assets are always required for all pages and can be preloaded
1642
+ * before page-specific loaders run.
1643
+ *
1644
+ * @returns Entry assets with js and css paths, or null if manifest unavailable
1645
+ */
1646
+ getEntryAssets() {
1647
+ if (this.cachedEntryAssets) return this.cachedEntryAssets;
1648
+ if (!this.clientManifest) return null;
1649
+ const base = this.base;
1650
+ for (const [key, entry] of Object.entries(this.clientManifest)) if (entry.isEntry) {
1651
+ this.cachedEntryAssets = {
1652
+ js: `${base}/${entry.file}`,
1653
+ css: entry.css?.map((css) => `${base}/${css}`) ?? []
1654
+ };
1655
+ return this.cachedEntryAssets;
1656
+ }
1657
+ return null;
1658
+ }
1659
+ /**
1660
+ * Build preload link tags for entry assets.
1661
+ *
1662
+ * @returns Array of link objects ready to be rendered
1663
+ */
1664
+ getEntryPreloadLinks() {
1665
+ const assets = this.getEntryAssets();
1666
+ if (!assets) return [];
1667
+ const links = [];
1668
+ for (const css of assets.css) links.push({
1669
+ rel: "stylesheet",
1670
+ href: css,
1671
+ crossorigin: ""
1672
+ });
1673
+ if (assets.js) links.push({
1674
+ rel: "modulepreload",
1675
+ href: assets.js
1676
+ });
1677
+ return links;
1678
+ }
1679
+ };
1680
+
1681
+ //#endregion
1682
+ //#region ../../src/react/router/providers/ReactPreloadProvider.ts
1683
+ /**
1684
+ * Adds HTTP Link headers for preloading entry assets.
1685
+ *
1686
+ * Benefits:
1687
+ * - Early Hints (103): Servers can send preload hints before the full response
1688
+ * - CDN optimization: Many CDNs use Link headers to optimize asset delivery
1689
+ * - Browser prefetching: Browsers can start fetching resources earlier
1690
+ *
1691
+ * The Link header is computed once at first request and cached for reuse.
1692
+ */
1693
+ var ReactPreloadProvider = class {
1694
+ alepha = $inject(Alepha);
1695
+ ssrManifest = $inject(SSRManifestProvider);
1696
+ /**
1697
+ * Cached Link header value - computed once, reused for all requests.
1698
+ */
1699
+ cachedLinkHeader;
1700
+ /**
1701
+ * Build the Link header string from entry assets.
1702
+ *
1703
+ * Format: <url>; rel=preload; as=type, <url>; rel=modulepreload
1704
+ *
1705
+ * @returns Link header string or null if no assets
1706
+ */
1707
+ buildLinkHeader() {
1708
+ const assets = this.ssrManifest.getEntryAssets();
1709
+ if (!assets) return null;
1710
+ const links = [];
1711
+ for (const css of assets.css) links.push(`<${css}>; rel=preload; as=style`);
1712
+ if (assets.js) links.push(`<${assets.js}>; rel=modulepreload`);
1713
+ return links.length > 0 ? links.join(", ") : null;
1714
+ }
1715
+ /**
1716
+ * Get the cached Link header, computing it on first access.
1717
+ */
1718
+ getLinkHeader() {
1719
+ if (this.cachedLinkHeader === void 0) this.cachedLinkHeader = this.buildLinkHeader();
1720
+ return this.cachedLinkHeader;
1721
+ }
1722
+ /**
1723
+ * Add Link header to HTML responses for asset preloading.
1724
+ */
1725
+ onResponse = $hook({
1726
+ on: "server:onResponse",
1727
+ priority: "first",
1728
+ handler: ({ response }) => {
1729
+ const contentType = response.headers["content-type"];
1730
+ if (!contentType || !contentType.includes("text/html")) return;
1731
+ const linkHeader = this.getLinkHeader();
1732
+ if (!linkHeader) return;
1733
+ if (response.headers.link) response.headers.link = `${response.headers.link}, ${linkHeader}`;
1734
+ else response.headers.link = linkHeader;
1735
+ }
1736
+ });
1737
+ };
1738
+
1739
+ //#endregion
1740
+ //#region ../../src/system/providers/FileSystemProvider.ts
1741
+ /**
1742
+ * FileSystem interface providing utilities for working with files.
1743
+ */
1744
+ var FileSystemProvider = class {};
1745
+
1746
+ //#endregion
1747
+ //#region ../../src/system/providers/MemoryFileSystemProvider.ts
1748
+ /**
1749
+ * In-memory implementation of FileSystemProvider for testing.
1750
+ *
1751
+ * This provider stores all files and directories in memory, making it ideal for
1752
+ * unit tests that need to verify file operations without touching the real file system.
1753
+ *
1754
+ * @example
1755
+ * ```typescript
1756
+ * // In tests, substitute the real FileSystemProvider with MemoryFileSystemProvider
1757
+ * const alepha = Alepha.create().with({
1758
+ * provide: FileSystemProvider,
1759
+ * use: MemoryFileSystemProvider,
1760
+ * });
1761
+ *
1762
+ * // Run code that uses FileSystemProvider
1763
+ * const service = alepha.inject(MyService);
1764
+ * await service.saveFile("test.txt", "Hello World");
1765
+ *
1766
+ * // Verify the file was written
1767
+ * const memoryFs = alepha.inject(MemoryFileSystemProvider);
1768
+ * expect(memoryFs.files.get("test.txt")?.toString()).toBe("Hello World");
1769
+ * ```
1770
+ */
1771
+ var MemoryFileSystemProvider = class {
1772
+ json = $inject(Json);
1773
+ /**
1774
+ * In-memory storage for files (path -> content)
1775
+ */
1776
+ files = /* @__PURE__ */ new Map();
1777
+ /**
1778
+ * In-memory storage for directories
1779
+ */
1780
+ directories = /* @__PURE__ */ new Set();
1781
+ /**
1782
+ * Track mkdir calls for test assertions
1783
+ */
1784
+ mkdirCalls = [];
1785
+ /**
1786
+ * Track writeFile calls for test assertions
1787
+ */
1788
+ writeFileCalls = [];
1789
+ /**
1790
+ * Track readFile calls for test assertions
1791
+ */
1792
+ readFileCalls = [];
1793
+ /**
1794
+ * Track rm calls for test assertions
1795
+ */
1796
+ rmCalls = [];
1797
+ /**
1798
+ * Track join calls for test assertions
1799
+ */
1800
+ joinCalls = [];
1801
+ /**
1802
+ * Error to throw on mkdir (for testing error handling)
1803
+ */
1804
+ mkdirError = null;
1805
+ /**
1806
+ * Error to throw on writeFile (for testing error handling)
1807
+ */
1808
+ writeFileError = null;
1809
+ /**
1810
+ * Error to throw on readFile (for testing error handling)
1811
+ */
1812
+ readFileError = null;
1813
+ constructor(options = {}) {
1814
+ this.mkdirError = options.mkdirError ?? null;
1815
+ this.writeFileError = options.writeFileError ?? null;
1816
+ this.readFileError = options.readFileError ?? null;
1817
+ }
1818
+ /**
1819
+ * Join path segments using forward slashes.
1820
+ * Uses Node's path.join for proper normalization (handles .. and .)
1821
+ */
1822
+ join(...paths) {
1823
+ this.joinCalls.push(paths);
1824
+ return join(...paths);
1825
+ }
1826
+ /**
1827
+ * Create a FileLike object from various sources.
1828
+ */
1829
+ createFile(options) {
1830
+ if ("path" in options) {
1831
+ const filePath = options.path;
1832
+ const buffer = this.files.get(filePath);
1833
+ if (buffer === void 0) throw new Error(`ENOENT: no such file or directory, open '${filePath}'`);
1834
+ return {
1835
+ name: options.name ?? filePath.split("/").pop() ?? "file",
1836
+ type: options.type ?? "application/octet-stream",
1837
+ size: buffer.byteLength,
1838
+ lastModified: Date.now(),
1839
+ stream: () => {
1840
+ throw new Error("Stream not implemented in MemoryFileSystemProvider");
1841
+ },
1842
+ arrayBuffer: async () => buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength),
1843
+ text: async () => buffer.toString("utf-8")
1844
+ };
1845
+ }
1846
+ if ("buffer" in options) {
1847
+ const buffer = options.buffer;
1848
+ return {
1849
+ name: options.name ?? "file",
1850
+ type: options.type ?? "application/octet-stream",
1851
+ size: buffer.byteLength,
1852
+ lastModified: Date.now(),
1853
+ stream: () => {
1854
+ throw new Error("Stream not implemented in MemoryFileSystemProvider");
1855
+ },
1856
+ arrayBuffer: async () => buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength),
1857
+ text: async () => buffer.toString("utf-8")
1858
+ };
1859
+ }
1860
+ if ("text" in options) {
1861
+ const buffer = Buffer.from(options.text, "utf-8");
1862
+ return {
1863
+ name: options.name ?? "file.txt",
1864
+ type: options.type ?? "text/plain",
1865
+ size: buffer.byteLength,
1866
+ lastModified: Date.now(),
1867
+ stream: () => {
1868
+ throw new Error("Stream not implemented in MemoryFileSystemProvider");
1869
+ },
1870
+ arrayBuffer: async () => buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength),
1871
+ text: async () => options.text
1872
+ };
1873
+ }
1874
+ throw new Error("MemoryFileSystemProvider.createFile: unsupported options. Only buffer and text are supported.");
1875
+ }
1876
+ /**
1877
+ * Remove a file or directory from memory.
1878
+ */
1879
+ async rm(path, options) {
1880
+ this.rmCalls.push({
1881
+ path,
1882
+ options
1883
+ });
1884
+ if (!(this.files.has(path) || this.directories.has(path)) && !options?.force) throw new Error(`ENOENT: no such file or directory, rm '${path}'`);
1885
+ if (this.directories.has(path)) if (options?.recursive) {
1886
+ this.directories.delete(path);
1887
+ for (const filePath of this.files.keys()) if (filePath.startsWith(`${path}/`)) this.files.delete(filePath);
1888
+ for (const dirPath of this.directories) if (dirPath.startsWith(`${path}/`)) this.directories.delete(dirPath);
1889
+ } else throw new Error(`EISDIR: illegal operation on a directory, rm '${path}'`);
1890
+ else this.files.delete(path);
1891
+ }
1892
+ /**
1893
+ * Copy a file or directory in memory.
1894
+ */
1895
+ async cp(src, dest, options) {
1896
+ if (this.directories.has(src)) {
1897
+ if (!options?.recursive) throw new Error(`Cannot copy directory without recursive option: ${src}`);
1607
1898
  this.directories.add(dest);
1608
1899
  for (const [filePath, content] of this.files) if (filePath.startsWith(`${src}/`)) {
1609
1900
  const newPath = filePath.replace(src, dest);
@@ -2804,8 +3095,13 @@ var NodeFileSystemProvider = class {
2804
3095
  * await fs.mkdir("/tmp/mydir", { mode: 0o755 });
2805
3096
  * ```
2806
3097
  */
2807
- async mkdir(path, options) {
2808
- await mkdir(path, options);
3098
+ async mkdir(path, options = {}) {
3099
+ const p = mkdir(path, {
3100
+ recursive: options.recursive ?? true,
3101
+ mode: options.mode
3102
+ });
3103
+ if (options.force === false) await p;
3104
+ else await p.catch(() => {});
2809
3105
  }
2810
3106
  /**
2811
3107
  * Lists files in a directory.
@@ -3283,235 +3579,99 @@ const AlephaSystem = $module({
3283
3579
  //#endregion
3284
3580
  //#region ../../src/react/router/providers/ReactServerTemplateProvider.ts
3285
3581
  /**
3286
- * Handles HTML template parsing, preprocessing, and streaming for SSR.
3287
- *
3288
- * Responsibilities:
3289
- * - Parse template once at startup into logical slots
3290
- * - Pre-encode static parts as Uint8Array for zero-copy streaming
3291
- * - Render dynamic parts (attributes, head content) efficiently
3292
- * - Build hydration data for client-side rehydration
3582
+ * Handles HTML streaming for SSR.
3293
3583
  *
3294
- * This provider is injected into ReactServerProvider to handle all
3295
- * template-related operations, keeping ReactServerProvider focused
3296
- * on request handling and React rendering coordination.
3584
+ * Uses hardcoded HTML structure - all customization via $head primitive.
3585
+ * Pre-encodes static parts as Uint8Array for zero-copy streaming.
3297
3586
  */
3298
3587
  var ReactServerTemplateProvider = class {
3299
3588
  log = $logger();
3300
3589
  alepha = $inject(Alepha);
3301
3590
  /**
3302
- * Shared TextEncoder instance - reused across all requests.
3591
+ * Shared TextEncoder - reused across all requests.
3303
3592
  */
3304
3593
  encoder = new TextEncoder();
3305
3594
  /**
3306
- * Pre-encoded common strings for streaming.
3307
- */
3308
- ENCODED = {
3595
+ * Pre-encoded static HTML parts for zero-copy streaming.
3596
+ */
3597
+ SLOTS = {
3598
+ DOCTYPE: this.encoder.encode("<!DOCTYPE html>\n"),
3599
+ HTML_OPEN: this.encoder.encode("<html"),
3600
+ HTML_CLOSE: this.encoder.encode(">\n"),
3601
+ HEAD_OPEN: this.encoder.encode("<head>"),
3602
+ HEAD_CLOSE: this.encoder.encode("</head>\n"),
3603
+ BODY_OPEN: this.encoder.encode("<body"),
3604
+ BODY_CLOSE: this.encoder.encode(">\n"),
3605
+ ROOT_OPEN: this.encoder.encode("<div id=\"root\">"),
3606
+ ROOT_CLOSE: this.encoder.encode("</div>\n"),
3607
+ BODY_HTML_CLOSE: this.encoder.encode("</body>\n</html>"),
3309
3608
  HYDRATION_PREFIX: this.encoder.encode("<script>window.__ssr="),
3310
- HYDRATION_SUFFIX: this.encoder.encode("<\/script>"),
3311
- EMPTY: this.encoder.encode("")
3609
+ HYDRATION_SUFFIX: this.encoder.encode("<\/script>")
3312
3610
  };
3313
3611
  /**
3314
- * Cached template slots - parsed once, reused for all requests.
3612
+ * Early head content (charset, viewport, entry assets).
3613
+ * Set once during configuration, reused for all requests.
3315
3614
  */
3316
- slots = null;
3615
+ earlyHeadContent = "";
3317
3616
  /**
3318
3617
  * Root element ID for React mounting.
3319
3618
  */
3320
- get rootId() {
3321
- return "root";
3322
- }
3619
+ rootId = "root";
3323
3620
  /**
3324
- * Regex pattern for matching the root div and extracting its content.
3621
+ * Regex for extracting root div content from HTML.
3325
3622
  */
3326
- get rootDivRegex() {
3327
- return new RegExp(`<div([^>]*)\\s+id=["']${this.rootId}["']([^>]*)>([\\s\\S]*?)<\\/div>`, "i");
3328
- }
3623
+ rootDivRegex = new RegExp(`<div[^>]*\\s+id=["']${this.rootId}["'][^>]*>([\\s\\S]*?)<\\/div>`, "i");
3329
3624
  /**
3330
- * Extract the content inside the root div from HTML.
3331
- *
3332
- * @param html - Full HTML string
3333
- * @returns The content inside the root div, or undefined if not found
3625
+ * Extract content inside the root div from HTML.
3334
3626
  */
3335
3627
  extractRootContent(html) {
3336
- return html.match(this.rootDivRegex)?.[3];
3628
+ return html.match(this.rootDivRegex)?.[1];
3337
3629
  }
3338
3630
  /**
3339
- * Check if template has been parsed and slots are available.
3631
+ * Set early head content (charset, viewport, entry assets).
3632
+ * Called once during server configuration.
3340
3633
  */
3341
- isReady() {
3342
- return this.slots !== null;
3634
+ setEarlyHeadContent(entryAssets, globalHead) {
3635
+ const charset = globalHead?.charset ?? "UTF-8";
3636
+ const viewport = globalHead?.viewport ?? "width=device-width, initial-scale=1";
3637
+ this.earlyHeadContent = `<meta charset="${this.escapeHtml(charset)}">\n<meta name="viewport" content="${this.escapeHtml(viewport)}">\n` + entryAssets;
3343
3638
  }
3344
3639
  /**
3345
- * Get the parsed template slots.
3346
- * Throws if template hasn't been parsed yet.
3347
- */
3348
- getSlots() {
3349
- if (!this.slots) throw new AlephaError("Template not parsed. Call parseTemplate() during configuration.");
3350
- return this.slots;
3351
- }
3352
- /**
3353
- * Parse an HTML template into logical slots for efficient streaming.
3354
- *
3355
- * This should be called once during server startup/configuration.
3356
- * The parsed slots are cached and reused for all requests.
3357
- */
3358
- parseTemplate(template) {
3359
- this.log.debug("Parsing template into slots");
3360
- const rootId = this.rootId;
3361
- const doctypeMatch = template.match(/<!DOCTYPE[^>]*>/i);
3362
- const doctype = doctypeMatch?.[0] ?? "<!DOCTYPE html>";
3363
- let remaining = doctypeMatch ? template.slice(doctypeMatch.index + doctypeMatch[0].length) : template;
3364
- const htmlMatch = remaining.match(/<html([^>]*)>/i);
3365
- const htmlAttrsStr = htmlMatch?.[1]?.trim() ?? "";
3366
- const htmlOriginalAttrs = this.parseAttributes(htmlAttrsStr);
3367
- remaining = htmlMatch ? remaining.slice(htmlMatch.index + htmlMatch[0].length) : remaining;
3368
- const headMatch = remaining.match(/<head([^>]*)>([\s\S]*?)<\/head>/i);
3369
- const headOriginalContent = headMatch?.[2]?.trim() ?? "";
3370
- remaining = headMatch ? remaining.slice(headMatch.index + headMatch[0].length) : remaining;
3371
- const bodyMatch = remaining.match(/<body([^>]*)>/i);
3372
- const bodyAttrsStr = bodyMatch?.[1]?.trim() ?? "";
3373
- const bodyOriginalAttrs = this.parseAttributes(bodyAttrsStr);
3374
- const bodyStartIndex = bodyMatch ? bodyMatch.index + bodyMatch[0].length : 0;
3375
- remaining = remaining.slice(bodyStartIndex);
3376
- const rootDivRegex = new RegExp(`<div([^>]*)\\s+id=["']${rootId}["']([^>]*)>([\\s\\S]*?)<\\/div>`, "i");
3377
- const rootMatch = remaining.match(rootDivRegex);
3378
- let beforeRoot = "";
3379
- let afterRoot = "";
3380
- let rootAttrs = "";
3381
- if (rootMatch) {
3382
- beforeRoot = remaining.slice(0, rootMatch.index).trim();
3383
- const rootEndIndex = rootMatch.index + rootMatch[0].length;
3384
- const bodyCloseIndex = remaining.indexOf("</body>");
3385
- afterRoot = bodyCloseIndex > rootEndIndex ? remaining.slice(rootEndIndex, bodyCloseIndex).trim() : "";
3386
- rootAttrs = `${rootMatch[1] ?? ""}${rootMatch[2] ?? ""}`.trim();
3387
- } else {
3388
- const bodyCloseIndex = remaining.indexOf("</body>");
3389
- if (bodyCloseIndex > 0) beforeRoot = remaining.slice(0, bodyCloseIndex).trim();
3390
- }
3391
- const rootOpenTag = rootAttrs ? `<div ${rootAttrs} id="${rootId}">` : `<div id="${rootId}">`;
3392
- this.slots = {
3393
- doctype: this.encoder.encode(`${doctype}\n`),
3394
- htmlOpen: this.encoder.encode("<html"),
3395
- htmlClose: this.encoder.encode(">\n"),
3396
- headOpen: this.encoder.encode("<head>"),
3397
- headClose: this.encoder.encode("</head>\n"),
3398
- bodyOpen: this.encoder.encode("<body"),
3399
- bodyClose: this.encoder.encode(">\n"),
3400
- rootOpen: this.encoder.encode(rootOpenTag),
3401
- rootClose: this.encoder.encode("</div>\n"),
3402
- scriptClose: this.encoder.encode("</body>\n</html>"),
3403
- htmlOriginalAttrs,
3404
- bodyOriginalAttrs,
3405
- headOriginalContent,
3406
- beforeRoot,
3407
- afterRoot
3408
- };
3409
- this.log.debug("Template parsed successfully", {
3410
- hasHtmlAttrs: Object.keys(htmlOriginalAttrs).length > 0,
3411
- hasBodyAttrs: Object.keys(bodyOriginalAttrs).length > 0,
3412
- hasHeadContent: headOriginalContent.length > 0,
3413
- hasBeforeRoot: beforeRoot.length > 0,
3414
- hasAfterRoot: afterRoot.length > 0
3415
- });
3416
- return this.slots;
3417
- }
3418
- /**
3419
- * Parse HTML attributes string into a record.
3420
- *
3421
- * Handles: key="value", key='value', key=value, and boolean key
3422
- */
3423
- parseAttributes(attrStr) {
3424
- const attrs = {};
3425
- if (!attrStr) return attrs;
3426
- for (const match of attrStr.matchAll(/([^\s=]+)(?:=(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g)) {
3427
- const key = match[1];
3428
- attrs[key] = match[2] ?? match[3] ?? match[4] ?? "";
3429
- }
3430
- return attrs;
3431
- }
3432
- /**
3433
- * Render attributes record to HTML string.
3434
- *
3435
- * @param attrs - Attributes to render
3436
- * @returns HTML attribute string like ` lang="en" class="dark"`
3640
+ * Render attributes record to HTML string.
3437
3641
  */
3438
3642
  renderAttributes(attrs) {
3643
+ if (!attrs) return "";
3439
3644
  const entries = Object.entries(attrs);
3440
3645
  if (entries.length === 0) return "";
3441
3646
  return entries.map(([key, value]) => ` ${key}="${this.escapeHtml(value)}"`).join("");
3442
3647
  }
3443
3648
  /**
3444
- * Render merged HTML attributes (original + dynamic).
3445
- */
3446
- renderMergedHtmlAttrs(dynamicAttrs) {
3447
- const merged = {
3448
- ...this.getSlots().htmlOriginalAttrs,
3449
- ...dynamicAttrs
3450
- };
3451
- return this.renderAttributes(merged);
3452
- }
3453
- /**
3454
- * Render merged body attributes (original + dynamic).
3455
- */
3456
- renderMergedBodyAttrs(dynamicAttrs) {
3457
- const merged = {
3458
- ...this.getSlots().bodyOriginalAttrs,
3459
- ...dynamicAttrs
3460
- };
3461
- return this.renderAttributes(merged);
3462
- }
3463
- /**
3464
3649
  * Render head content (title, meta, link, script tags).
3465
- *
3466
- * @param head - Head data to render
3467
- * @param includeOriginal - Whether to include original head content
3468
- * @returns HTML string with head content
3469
3650
  */
3470
- renderHeadContent(head, includeOriginal = true) {
3471
- const slots = this.getSlots();
3651
+ renderHeadContent(head) {
3652
+ if (!head) return "";
3472
3653
  let content = "";
3473
- if (includeOriginal && slots.headOriginalContent) content += slots.headOriginalContent;
3474
- if (!head) return content;
3475
- if (head.title) if (content.includes("<title>")) content = content.replace(/<title>.*?<\/title>/i, `<title>${this.escapeHtml(head.title)}</title>`);
3476
- else content += `<title>${this.escapeHtml(head.title)}</title>\n`;
3477
- if (head.meta) for (const meta of head.meta) content += this.renderMetaTag(meta);
3478
- if (head.link) for (const link of head.link) content += this.renderLinkTag(link);
3479
- if (head.script) for (const script of head.script) content += this.renderScriptTag(script);
3654
+ if (head.title) content += `<title>${this.escapeHtml(head.title)}</title>\n`;
3655
+ if (head.meta) {
3656
+ for (const meta of head.meta) if (meta.property) content += `<meta property="${this.escapeHtml(meta.property)}" content="${this.escapeHtml(meta.content)}">\n`;
3657
+ else if (meta.name) content += `<meta name="${this.escapeHtml(meta.name)}" content="${this.escapeHtml(meta.content)}">\n`;
3658
+ }
3659
+ if (head.link) for (const link of head.link) {
3660
+ content += `<link rel="${this.escapeHtml(link.rel)}" href="${this.escapeHtml(link.href)}"`;
3661
+ if (link.type) content += ` type="${this.escapeHtml(link.type)}"`;
3662
+ if (link.as) content += ` as="${this.escapeHtml(link.as)}"`;
3663
+ if (link.crossorigin != null) content += " crossorigin=\"\"";
3664
+ content += ">\n";
3665
+ }
3666
+ if (head.script) for (const script of head.script) if (typeof script === "string") content += `<script>${script}<\/script>\n`;
3667
+ else {
3668
+ const { content: scriptContent, ...rest } = script;
3669
+ const attrs = Object.entries(rest).filter(([, v]) => v !== false && v !== void 0).map(([k, v]) => v === true ? k : `${k}="${this.escapeHtml(String(v))}"`).join(" ");
3670
+ content += scriptContent ? `<script ${attrs}>${scriptContent}<\/script>\n` : `<script ${attrs}><\/script>\n`;
3671
+ }
3480
3672
  return content;
3481
3673
  }
3482
3674
  /**
3483
- * Render a meta tag.
3484
- */
3485
- renderMetaTag(meta) {
3486
- if (meta.property) return `<meta property="${this.escapeHtml(meta.property)}" content="${this.escapeHtml(meta.content)}">\n`;
3487
- if (meta.name) return `<meta name="${this.escapeHtml(meta.name)}" content="${this.escapeHtml(meta.content)}">\n`;
3488
- return "";
3489
- }
3490
- /**
3491
- * Render a link tag.
3492
- */
3493
- renderLinkTag(link) {
3494
- let tag = `<link rel="${this.escapeHtml(link.rel)}" href="${this.escapeHtml(link.href)}"`;
3495
- if (link.type) tag += ` type="${this.escapeHtml(link.type)}"`;
3496
- if (link.as) tag += ` as="${this.escapeHtml(link.as)}"`;
3497
- if (link.crossorigin != null) tag += " crossorigin=\"\"";
3498
- tag += ">\n";
3499
- return tag;
3500
- }
3501
- /**
3502
- * Render a script tag.
3503
- */
3504
- renderScriptTag(script) {
3505
- if (typeof script === "string") return `<script>${script}<\/script>\n`;
3506
- const { content, ...rest } = script;
3507
- const attrs = Object.entries(rest).filter(([, value]) => value !== false && value !== void 0).map(([key, value]) => {
3508
- if (value === true) return key;
3509
- return `${key}="${this.escapeHtml(String(value))}"`;
3510
- }).join(" ");
3511
- if (content) return attrs ? `<script ${attrs}>${content}<\/script>\n` : `<script>${content}<\/script>\n`;
3512
- return `<script ${attrs}><\/script>\n`;
3513
- }
3514
- /**
3515
3675
  * Escape HTML special characters.
3516
3676
  */
3517
3677
  escapeHtml(str) {
@@ -3519,16 +3679,12 @@ var ReactServerTemplateProvider = class {
3519
3679
  }
3520
3680
  /**
3521
3681
  * Safely serialize data to JSON for embedding in HTML.
3522
- * Escapes characters that could break out of script tags.
3523
3682
  */
3524
3683
  safeJsonSerialize(data) {
3525
3684
  return JSON.stringify(data).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
3526
3685
  }
3527
3686
  /**
3528
3687
  * Build hydration data from router state.
3529
- *
3530
- * This creates the data structure that will be serialized to window.__ssr
3531
- * for client-side rehydration.
3532
3688
  */
3533
3689
  buildHydrationData(state) {
3534
3690
  const { request, context, ...store } = this.alepha.context.als?.getStore() ?? {};
@@ -3548,169 +3704,75 @@ var ReactServerTemplateProvider = class {
3548
3704
  return hydrationData;
3549
3705
  }
3550
3706
  /**
3551
- * Stream the body content: body tag, root div, React content, hydration, and closing tags.
3552
- *
3553
- * If an error occurs during React streaming, it injects error HTML instead of aborting,
3554
- * ensuring users see an error message rather than a white screen.
3555
- */
3556
- async streamBodyContent(controller, reactStream, state, hydration) {
3557
- const slots = this.getSlots();
3558
- const encoder = this.encoder;
3559
- const head = state.head;
3560
- controller.enqueue(slots.bodyOpen);
3561
- controller.enqueue(encoder.encode(this.renderMergedBodyAttrs(head?.bodyAttributes)));
3562
- controller.enqueue(slots.bodyClose);
3563
- if (slots.beforeRoot) controller.enqueue(encoder.encode(slots.beforeRoot));
3564
- controller.enqueue(slots.rootOpen);
3707
+ * Pipe React stream to controller with backpressure handling.
3708
+ * Returns true if stream completed successfully, false if error occurred.
3709
+ */
3710
+ async pipeReactStream(controller, reactStream, state) {
3565
3711
  const reader = reactStream.getReader();
3566
- let streamError = null;
3567
3712
  try {
3568
3713
  while (true) {
3714
+ if (controller.desiredSize !== null && controller.desiredSize <= 0) await new Promise((resolve) => queueMicrotask(resolve));
3569
3715
  const { done, value } = await reader.read();
3570
3716
  if (done) break;
3571
3717
  controller.enqueue(value);
3572
3718
  }
3719
+ return true;
3573
3720
  } catch (error) {
3574
- streamError = error;
3575
- this.log.error("Error during React stream reading", error);
3721
+ this.log.error("React stream error", error);
3722
+ controller.enqueue(this.encoder.encode(this.renderErrorToString(error instanceof Error ? error : new Error(String(error)), state)));
3723
+ return false;
3576
3724
  } finally {
3577
3725
  reader.releaseLock();
3578
3726
  }
3579
- if (streamError) {
3580
- this.injectErrorHtml(controller, encoder, slots, streamError, state, {
3581
- headClosed: true,
3582
- bodyStarted: true
3583
- });
3584
- return;
3585
- }
3586
- controller.enqueue(slots.rootClose);
3587
- if (slots.afterRoot) controller.enqueue(encoder.encode(slots.afterRoot));
3588
- if (hydration) {
3589
- const hydrationData = this.buildHydrationData(state);
3590
- controller.enqueue(this.ENCODED.HYDRATION_PREFIX);
3591
- controller.enqueue(encoder.encode(this.safeJsonSerialize(hydrationData)));
3592
- controller.enqueue(this.ENCODED.HYDRATION_SUFFIX);
3593
- }
3594
- controller.enqueue(slots.scriptClose);
3595
- }
3596
- /**
3597
- * Create a ReadableStream that streams the HTML template with React content.
3598
- *
3599
- * This is the main entry point for SSR streaming. It:
3600
- * 1. Sends <head> immediately (browser starts downloading assets)
3601
- * 2. Streams React content as it renders
3602
- * 3. Appends hydration script and closing tags
3603
- *
3604
- * @param reactStream - ReadableStream from renderToReadableStream
3605
- * @param state - Router state with head data
3606
- * @param options - Streaming options
3607
- */
3608
- createHtmlStream(reactStream, state, options = {}) {
3609
- const { hydration = true, onError } = options;
3610
- const slots = this.getSlots();
3611
- const head = state.head;
3612
- const encoder = this.encoder;
3613
- return new ReadableStream({ start: async (controller) => {
3614
- try {
3615
- controller.enqueue(slots.doctype);
3616
- controller.enqueue(slots.htmlOpen);
3617
- controller.enqueue(encoder.encode(this.renderMergedHtmlAttrs(head?.htmlAttributes)));
3618
- controller.enqueue(slots.htmlClose);
3619
- controller.enqueue(slots.headOpen);
3620
- if (this.earlyHeadContent) controller.enqueue(encoder.encode(this.earlyHeadContent));
3621
- controller.enqueue(encoder.encode(this.renderHeadContent(head)));
3622
- controller.enqueue(slots.headClose);
3623
- await this.streamBodyContent(controller, reactStream, state, hydration);
3624
- controller.close();
3625
- } catch (error) {
3626
- onError?.(error);
3627
- controller.error(error);
3628
- }
3629
- } });
3630
3727
  }
3631
3728
  /**
3632
- * Early head content for preloading.
3633
- *
3634
- * Contains entry assets (JS + CSS) that are always required and can be
3635
- * sent before page loaders run.
3729
+ * Stream complete HTML document (head already closed).
3730
+ * Used by both createHtmlStream and late phase of createEarlyHtmlStream.
3636
3731
  */
3637
- earlyHeadContent = "";
3638
- /**
3639
- * Set the early head content (entry script + CSS).
3640
- *
3641
- * Also strips these assets from the original head content to avoid duplicates,
3642
- * since we're moving them to the early phase.
3643
- *
3644
- * Automatically prepends critical meta tags (charset, viewport) if not present
3645
- * in $head configuration, ensuring they're sent as early as possible.
3646
- *
3647
- * @param content - HTML string with entry assets
3648
- * @param globalHead - Global head configuration from $head primitives
3649
- * @param entryAssets - Entry asset paths to strip from original head
3650
- */
3651
- setEarlyHeadContent(content, globalHead, entryAssets) {
3652
- const criticalMeta = [];
3653
- const charset = globalHead?.charset ?? "UTF-8";
3654
- criticalMeta.push(`<meta charset="${this.escapeHtml(charset)}">`);
3655
- const viewport = globalHead?.viewport ?? "width=device-width, initial-scale=1";
3656
- criticalMeta.push(`<meta name="viewport" content="${this.escapeHtml(viewport)}">`);
3657
- this.earlyHeadContent = criticalMeta.length > 0 ? `${criticalMeta.join("\n")}\n${content}` : content;
3658
- if (entryAssets && this.slots) {
3659
- let headContent = this.slots.headOriginalContent;
3660
- if (entryAssets.js) {
3661
- const scriptPattern = new RegExp(`<script[^>]*\\ssrc=["']${this.escapeRegExp(entryAssets.js)}["'][^>]*>\\s*<\/script>\\s*`, "gi");
3662
- headContent = headContent.replace(scriptPattern, "");
3663
- }
3664
- for (const css of entryAssets.css) {
3665
- const linkPattern = new RegExp(`<link[^>]*\\shref=["']${this.escapeRegExp(css)}["'][^>]*>\\s*`, "gi");
3666
- headContent = headContent.replace(linkPattern, "");
3667
- }
3668
- this.slots.headOriginalContent = headContent.trim();
3732
+ async streamBodyAndClose(controller, reactStream, state, hydration) {
3733
+ const { encoder, SLOTS: slots } = this;
3734
+ controller.enqueue(slots.BODY_OPEN);
3735
+ controller.enqueue(encoder.encode(this.renderAttributes(state.head?.bodyAttributes)));
3736
+ controller.enqueue(slots.BODY_CLOSE);
3737
+ controller.enqueue(slots.ROOT_OPEN);
3738
+ await this.pipeReactStream(controller, reactStream, state);
3739
+ controller.enqueue(slots.ROOT_CLOSE);
3740
+ if (hydration) {
3741
+ controller.enqueue(slots.HYDRATION_PREFIX);
3742
+ controller.enqueue(encoder.encode(this.safeJsonSerialize(this.buildHydrationData(state))));
3743
+ controller.enqueue(slots.HYDRATION_SUFFIX);
3669
3744
  }
3745
+ controller.enqueue(slots.BODY_HTML_CLOSE);
3670
3746
  }
3671
3747
  /**
3672
- * Escape special regex characters in a string.
3673
- */
3674
- escapeRegExp(str) {
3675
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3676
- }
3677
- /**
3678
- * Create an optimized HTML stream with early head streaming.
3679
- *
3680
- * This version sends critical assets (entry.js, CSS) BEFORE page loaders run,
3681
- * allowing the browser to start downloading them immediately.
3748
+ * Create HTML stream with early head optimization.
3682
3749
  *
3683
3750
  * Flow:
3684
3751
  * 1. Send DOCTYPE, <html>, <head> open, entry preloads (IMMEDIATE)
3685
- * 2. Run async work (createLayers, etc.)
3752
+ * 2. Run async work (page loaders)
3686
3753
  * 3. Send rest of head, body, React content, hydration
3687
- *
3688
- * @param globalHead - Global head with htmlAttributes (from $head primitives)
3689
- * @param asyncWork - Async function to run between early head and rest of stream
3690
- * @param options - Streaming options
3691
3754
  */
3692
3755
  createEarlyHtmlStream(globalHead, asyncWork, options = {}) {
3693
3756
  const { hydration = true, onError } = options;
3694
- const slots = this.getSlots();
3695
- const encoder = this.encoder;
3757
+ const { encoder, SLOTS: slots } = this;
3696
3758
  let headClosed = false;
3697
3759
  let bodyStarted = false;
3698
3760
  let routerState;
3699
3761
  return new ReadableStream({ start: async (controller) => {
3700
3762
  try {
3701
- controller.enqueue(slots.doctype);
3702
- controller.enqueue(slots.htmlOpen);
3703
- controller.enqueue(encoder.encode(this.renderMergedHtmlAttrs(globalHead?.htmlAttributes)));
3704
- controller.enqueue(slots.htmlClose);
3705
- controller.enqueue(slots.headOpen);
3763
+ controller.enqueue(slots.DOCTYPE);
3764
+ controller.enqueue(slots.HTML_OPEN);
3765
+ controller.enqueue(encoder.encode(this.renderAttributes(globalHead?.htmlAttributes)));
3766
+ controller.enqueue(slots.HTML_CLOSE);
3767
+ controller.enqueue(slots.HEAD_OPEN);
3706
3768
  if (this.earlyHeadContent) controller.enqueue(encoder.encode(this.earlyHeadContent));
3707
3769
  const result = await asyncWork();
3708
3770
  if (!result || "redirect" in result) {
3709
3771
  if (result && "redirect" in result) {
3710
- this.log.debug("Loader redirect detected after streaming started, using meta refresh", { redirect: result.redirect });
3772
+ this.log.debug("Loader redirect, using meta refresh", { redirect: result.redirect });
3711
3773
  controller.enqueue(encoder.encode(`<meta http-equiv="refresh" content="0; url=${this.escapeHtml(result.redirect)}">\n`));
3712
3774
  }
3713
- controller.enqueue(slots.headClose);
3775
+ controller.enqueue(slots.HEAD_CLOSE);
3714
3776
  controller.enqueue(encoder.encode("<body></body></html>"));
3715
3777
  controller.close();
3716
3778
  return;
@@ -3718,15 +3780,15 @@ var ReactServerTemplateProvider = class {
3718
3780
  const { state, reactStream } = result;
3719
3781
  routerState = state;
3720
3782
  controller.enqueue(encoder.encode(this.renderHeadContent(state.head)));
3721
- controller.enqueue(slots.headClose);
3783
+ controller.enqueue(slots.HEAD_CLOSE);
3722
3784
  headClosed = true;
3723
3785
  bodyStarted = true;
3724
- await this.streamBodyContent(controller, reactStream, state, hydration);
3786
+ await this.streamBodyAndClose(controller, reactStream, state, hydration);
3725
3787
  controller.close();
3726
3788
  } catch (error) {
3727
3789
  onError?.(error);
3728
3790
  try {
3729
- this.injectErrorHtml(controller, encoder, slots, error, routerState, {
3791
+ this.injectErrorHtml(controller, error, routerState, {
3730
3792
  headClosed,
3731
3793
  bodyStarted
3732
3794
  });
@@ -3738,56 +3800,60 @@ var ReactServerTemplateProvider = class {
3738
3800
  } });
3739
3801
  }
3740
3802
  /**
3741
- * Inject error HTML into the stream when an error occurs during streaming.
3742
- *
3743
- * Uses the router state's onError handler to render the error component,
3744
- * falling back to ErrorViewer if no custom handler is defined.
3745
- * Renders using renderToString to produce static HTML.
3746
- *
3747
- * Since we may have already sent partial HTML (DOCTYPE, <html>, <head>),
3748
- * we need to complete the document with an error message instead of aborting.
3749
- *
3750
- * Handles different states:
3751
- * - headClosed=false, bodyStarted=false: Need to add head content, close head, open body, add error, close all
3752
- * - headClosed=true, bodyStarted=false: Need to open body, add error, close all
3753
- * - headClosed=true, bodyStarted=true: Already inside root div, add error, close all
3803
+ * Create HTML stream (non-early version, for testing/prerender).
3754
3804
  */
3755
- injectErrorHtml(controller, encoder, slots, error, routerState, streamState) {
3805
+ createHtmlStream(reactStream, state, options = {}) {
3806
+ const { hydration = true, onError } = options;
3807
+ const { encoder, SLOTS: slots } = this;
3808
+ return new ReadableStream({ start: async (controller) => {
3809
+ try {
3810
+ controller.enqueue(slots.DOCTYPE);
3811
+ controller.enqueue(slots.HTML_OPEN);
3812
+ controller.enqueue(encoder.encode(this.renderAttributes(state.head?.htmlAttributes)));
3813
+ controller.enqueue(slots.HTML_CLOSE);
3814
+ controller.enqueue(slots.HEAD_OPEN);
3815
+ if (this.earlyHeadContent) controller.enqueue(encoder.encode(this.earlyHeadContent));
3816
+ controller.enqueue(encoder.encode(this.renderHeadContent(state.head)));
3817
+ controller.enqueue(slots.HEAD_CLOSE);
3818
+ await this.streamBodyAndClose(controller, reactStream, state, hydration);
3819
+ controller.close();
3820
+ } catch (error) {
3821
+ onError?.(error);
3822
+ controller.error(error);
3823
+ }
3824
+ } });
3825
+ }
3826
+ /**
3827
+ * Inject error HTML when streaming fails.
3828
+ */
3829
+ injectErrorHtml(controller, error, routerState, streamState) {
3830
+ const { encoder, SLOTS: slots } = this;
3756
3831
  if (!streamState.headClosed) {
3757
- const headContent = this.renderHeadContent(routerState?.head);
3758
- if (headContent) controller.enqueue(encoder.encode(headContent));
3759
- controller.enqueue(slots.headClose);
3832
+ controller.enqueue(encoder.encode(this.renderHeadContent(routerState?.head)));
3833
+ controller.enqueue(slots.HEAD_CLOSE);
3760
3834
  }
3761
3835
  if (!streamState.bodyStarted) {
3762
- controller.enqueue(slots.bodyOpen);
3763
- controller.enqueue(encoder.encode(this.renderMergedBodyAttrs(routerState?.head?.bodyAttributes)));
3764
- controller.enqueue(slots.bodyClose);
3765
- if (slots.beforeRoot) controller.enqueue(encoder.encode(slots.beforeRoot));
3766
- controller.enqueue(slots.rootOpen);
3836
+ controller.enqueue(slots.BODY_OPEN);
3837
+ controller.enqueue(encoder.encode(this.renderAttributes(routerState?.head?.bodyAttributes)));
3838
+ controller.enqueue(slots.BODY_CLOSE);
3839
+ controller.enqueue(slots.ROOT_OPEN);
3767
3840
  }
3768
- const errorHtml = this.renderErrorToString(error instanceof Error ? error : new Error(String(error)), routerState);
3769
- controller.enqueue(encoder.encode(errorHtml));
3770
- controller.enqueue(slots.rootClose);
3771
- if (!streamState.bodyStarted && slots.afterRoot) controller.enqueue(encoder.encode(slots.afterRoot));
3772
- controller.enqueue(slots.scriptClose);
3841
+ controller.enqueue(encoder.encode(this.renderErrorToString(error instanceof Error ? error : new Error(String(error)), routerState)));
3842
+ controller.enqueue(slots.ROOT_CLOSE);
3843
+ controller.enqueue(slots.BODY_HTML_CLOSE);
3773
3844
  }
3774
3845
  /**
3775
- * Render an error to HTML string using the router's error handler.
3776
- *
3777
- * Falls back to ErrorViewer if:
3778
- * - No router state is available
3779
- * - The error handler returns null/undefined
3780
- * - The error handler itself throws
3846
+ * Render error to HTML string.
3781
3847
  */
3782
3848
  renderErrorToString(error, routerState) {
3783
3849
  this.log.error("SSR rendering error", error);
3784
3850
  let errorElement;
3785
3851
  if (routerState?.onError) try {
3786
3852
  const result = routerState.onError(error, routerState);
3787
- if (result instanceof Redirection) this.log.warn("Error handler returned Redirection but headers already sent", { redirect: result.redirect });
3788
- else if (result !== null && result !== void 0) errorElement = result;
3853
+ if (result instanceof Redirection) this.log.warn("Error handler returned Redirection but headers sent", { redirect: result.redirect });
3854
+ else if (result != null) errorElement = result;
3789
3855
  } catch (handlerError) {
3790
- this.log.error("Error handler threw an exception", handlerError);
3856
+ this.log.error("Error handler threw", handlerError);
3791
3857
  }
3792
3858
  if (!errorElement) errorElement = createElement(ErrorViewer_default, {
3793
3859
  error,
@@ -3803,238 +3869,6 @@ var ReactServerTemplateProvider = class {
3803
3869
  }
3804
3870
  };
3805
3871
 
3806
- //#endregion
3807
- //#region ../../src/react/router/atoms/ssrManifestAtom.ts
3808
- /**
3809
- * Schema for the SSR manifest atom.
3810
- */
3811
- const ssrManifestAtomSchema = t.object({
3812
- base: t.optional(t.string()),
3813
- preload: t.optional(t.record(t.string(), t.string())),
3814
- client: t.optional(t.record(t.string(), t.object({
3815
- file: t.string(),
3816
- isEntry: t.optional(t.boolean()),
3817
- imports: t.optional(t.array(t.string())),
3818
- css: t.optional(t.array(t.string()))
3819
- })))
3820
- });
3821
- /**
3822
- * SSR Manifest atom containing all manifest data for SSR module preloading.
3823
- *
3824
- * This atom is populated at build time by embedding manifest data into the
3825
- * generated index.js. This approach is optimal for serverless deployments
3826
- * as it eliminates filesystem reads at runtime.
3827
- *
3828
- * The manifest includes:
3829
- * - preload: Maps short hash keys to source paths (from viteAlephaSsrPreload)
3830
- * - client: Maps source files to their output info (file, imports, css)
3831
- */
3832
- const ssrManifestAtom = $atom({
3833
- name: "alepha.react.ssr.manifest",
3834
- description: "SSR manifest for module preloading",
3835
- schema: ssrManifestAtomSchema,
3836
- default: {}
3837
- });
3838
-
3839
- //#endregion
3840
- //#region ../../src/react/router/providers/SSRManifestProvider.ts
3841
- /**
3842
- * Provider for SSR manifest data used for module preloading.
3843
- *
3844
- * The manifest is populated at build time by embedding data into the
3845
- * generated index.js via the ssrManifestAtom. This eliminates filesystem
3846
- * reads at runtime, making it optimal for serverless deployments.
3847
- *
3848
- * Manifest files are generated during `vite build`:
3849
- * - manifest.json (client manifest)
3850
- * - preload-manifest.json (from viteAlephaSsrPreload plugin)
3851
- */
3852
- var SSRManifestProvider = class {
3853
- alepha = $inject(Alepha);
3854
- /**
3855
- * Get the manifest from the store at runtime.
3856
- * This ensures the manifest is available even when set after module load.
3857
- */
3858
- get manifest() {
3859
- return this.alepha.store.get(ssrManifestAtom) ?? {};
3860
- }
3861
- /**
3862
- * Get the base path for assets (from Vite's base config).
3863
- * Returns empty string if base is "/" (default), otherwise returns the base path.
3864
- */
3865
- get base() {
3866
- return this.manifest.base ?? "";
3867
- }
3868
- /**
3869
- * Get the preload manifest.
3870
- */
3871
- get preloadManifest() {
3872
- return this.manifest.preload;
3873
- }
3874
- /**
3875
- * Get the client manifest.
3876
- */
3877
- get clientManifest() {
3878
- return this.manifest.client;
3879
- }
3880
- /**
3881
- * Resolve a preload key to its source path.
3882
- *
3883
- * The key is a short hash injected by viteAlephaSsrPreload plugin,
3884
- * which maps to the full source path in the preload manifest.
3885
- *
3886
- * @param key - Short hash key (e.g., "a1b2c3d4")
3887
- * @returns Source path (e.g., "src/pages/UserDetail.tsx") or undefined
3888
- */
3889
- resolvePreloadKey(key) {
3890
- return this.preloadManifest?.[key];
3891
- }
3892
- /**
3893
- * Get all chunks required for a source file, including transitive dependencies.
3894
- *
3895
- * Uses the client manifest to recursively resolve all imported chunks.
3896
- *
3897
- * @param sourcePath - Source file path (e.g., "src/pages/Home.tsx")
3898
- * @returns Array of chunk URLs to preload, or empty array if not found
3899
- */
3900
- getChunks(sourcePath) {
3901
- if (!this.clientManifest) return [];
3902
- if (!this.findManifestEntry(sourcePath)) return [];
3903
- const chunks = /* @__PURE__ */ new Set();
3904
- const visited = /* @__PURE__ */ new Set();
3905
- this.collectChunksRecursive(sourcePath, chunks, visited);
3906
- return Array.from(chunks);
3907
- }
3908
- /**
3909
- * Find manifest entry for a source path, trying different extensions.
3910
- */
3911
- findManifestEntry(sourcePath) {
3912
- if (!this.clientManifest) return void 0;
3913
- if (this.clientManifest[sourcePath]) return this.clientManifest[sourcePath];
3914
- const basePath = sourcePath.replace(/\.[^.]+$/, "");
3915
- for (const ext of [
3916
- ".tsx",
3917
- ".ts",
3918
- ".jsx",
3919
- ".js"
3920
- ]) {
3921
- const pathWithExt = basePath + ext;
3922
- if (this.clientManifest[pathWithExt]) return this.clientManifest[pathWithExt];
3923
- }
3924
- }
3925
- /**
3926
- * Recursively collect all chunk URLs for a manifest entry.
3927
- */
3928
- collectChunksRecursive(key, chunks, visited) {
3929
- if (visited.has(key)) return;
3930
- visited.add(key);
3931
- if (!this.clientManifest) return;
3932
- const entry = this.clientManifest[key];
3933
- if (!entry) return;
3934
- const base = this.base;
3935
- if (entry.file) chunks.add(`${base}/${entry.file}`);
3936
- if (entry.css) for (const css of entry.css) chunks.add(`${base}/${css}`);
3937
- if (entry.imports) for (const imp of entry.imports) {
3938
- if (imp === "index.html" || imp.endsWith(".html")) continue;
3939
- this.collectChunksRecursive(imp, chunks, visited);
3940
- }
3941
- }
3942
- /**
3943
- * Collect modulepreload links for a route and its parent chain.
3944
- */
3945
- collectPreloadLinks(route) {
3946
- if (!this.isAvailable()) return [];
3947
- const preloadPaths = [];
3948
- let current = route;
3949
- while (current) {
3950
- const preloadKey = current[PAGE_PRELOAD_KEY];
3951
- if (preloadKey) {
3952
- const sourcePath = this.resolvePreloadKey(preloadKey);
3953
- if (sourcePath) preloadPaths.push(sourcePath);
3954
- }
3955
- current = current.parent;
3956
- }
3957
- if (preloadPaths.length === 0) return [];
3958
- return this.getChunksForMultiple(preloadPaths).map((href) => {
3959
- if (href.endsWith(".css")) return {
3960
- rel: "preload",
3961
- href,
3962
- as: "style",
3963
- crossorigin: ""
3964
- };
3965
- return {
3966
- rel: "modulepreload",
3967
- href
3968
- };
3969
- });
3970
- }
3971
- /**
3972
- * Get all chunks for multiple source files.
3973
- *
3974
- * @param sourcePaths - Array of source file paths
3975
- * @returns Deduplicated array of chunk URLs
3976
- */
3977
- getChunksForMultiple(sourcePaths) {
3978
- const allChunks = /* @__PURE__ */ new Set();
3979
- for (const path of sourcePaths) {
3980
- const chunks = this.getChunks(path);
3981
- for (const chunk of chunks) allChunks.add(chunk);
3982
- }
3983
- return Array.from(allChunks);
3984
- }
3985
- /**
3986
- * Check if manifest is loaded and available.
3987
- */
3988
- isAvailable() {
3989
- return this.clientManifest !== void 0;
3990
- }
3991
- /**
3992
- * Cached entry assets - computed once at first access.
3993
- */
3994
- cachedEntryAssets = null;
3995
- /**
3996
- * Get the entry point assets (main entry.js and associated CSS files).
3997
- *
3998
- * These assets are always required for all pages and can be preloaded
3999
- * before page-specific loaders run.
4000
- *
4001
- * @returns Entry assets with js and css paths, or null if manifest unavailable
4002
- */
4003
- getEntryAssets() {
4004
- if (this.cachedEntryAssets) return this.cachedEntryAssets;
4005
- if (!this.clientManifest) return null;
4006
- const base = this.base;
4007
- for (const [key, entry] of Object.entries(this.clientManifest)) if (entry.isEntry) {
4008
- this.cachedEntryAssets = {
4009
- js: `${base}/${entry.file}`,
4010
- css: entry.css?.map((css) => `${base}/${css}`) ?? []
4011
- };
4012
- return this.cachedEntryAssets;
4013
- }
4014
- return null;
4015
- }
4016
- /**
4017
- * Build preload link tags for entry assets.
4018
- *
4019
- * @returns Array of link objects ready to be rendered
4020
- */
4021
- getEntryPreloadLinks() {
4022
- const assets = this.getEntryAssets();
4023
- if (!assets) return [];
4024
- const links = [];
4025
- for (const css of assets.css) links.push({
4026
- rel: "stylesheet",
4027
- href: css,
4028
- crossorigin: ""
4029
- });
4030
- if (assets.js) links.push({
4031
- rel: "modulepreload",
4032
- href: assets.js
4033
- });
4034
- return links;
4035
- }
4036
- };
4037
-
4038
3872
  //#endregion
4039
3873
  //#region ../../src/react/router/providers/ReactServerProvider.ts
4040
3874
  /**
@@ -4091,38 +3925,17 @@ var ReactServerProvider = class {
4091
3925
  }
4092
3926
  }
4093
3927
  if (ssrEnabled) {
4094
- await this.registerPages(async () => this.template);
3928
+ this.registerPages();
4095
3929
  this.log.info("SSR OK");
4096
3930
  return;
4097
3931
  }
4098
- this.log.info("SSR is disabled, use History API fallback");
4099
- this.serverRouterProvider.createRoute({
4100
- path: "*",
4101
- handler: async ({ url, reply }) => {
4102
- if (url.pathname.includes(".")) {
4103
- reply.headers["content-type"] = "text/plain";
4104
- reply.body = "Not Found";
4105
- reply.status = 404;
4106
- return;
4107
- }
4108
- reply.headers["content-type"] = "text/html";
4109
- return this.template;
4110
- }
4111
- });
3932
+ this.log.info("SSR is disabled");
4112
3933
  }
4113
3934
  });
4114
3935
  /**
4115
- * Get the current HTML template.
4116
- */
4117
- get template() {
4118
- return this.alepha.store.get("alepha.react.server.template") ?? "<!DOCTYPE html><html lang='en'><head></head><body><div id='root'></div></body></html>";
4119
- }
4120
- /**
4121
3936
  * Register all pages as server routes.
4122
3937
  */
4123
- async registerPages(templateLoader) {
4124
- const template = await templateLoader();
4125
- if (template) this.templateProvider.parseTemplate(template);
3938
+ registerPages() {
4126
3939
  this.setupEarlyHeadContent();
4127
3940
  this.hasServerLinksProvider = this.alepha.has(ServerLinksProvider);
4128
3941
  for (const page of this.pageApi.getPages()) if (page.component || page.lazy) {
@@ -4132,7 +3945,7 @@ var ReactServerProvider = class {
4132
3945
  schema: void 0,
4133
3946
  method: "GET",
4134
3947
  path: page.match,
4135
- handler: this.createHandler(page, templateLoader)
3948
+ handler: this.createHandler(page)
4136
3949
  });
4137
3950
  }
4138
3951
  }
@@ -4141,27 +3954,23 @@ var ReactServerProvider = class {
4141
3954
  *
4142
3955
  * This content is sent immediately when streaming starts, before page loaders run,
4143
3956
  * allowing the browser to start downloading entry.js and CSS files early.
4144
- *
4145
- * Uses <script type="module"> instead of <link rel="modulepreload"> for JS
4146
- * because the script needs to execute anyway - this way the browser starts
4147
- * downloading, parsing, AND will execute as soon as ready.
4148
- *
4149
- * Also injects critical meta tags (charset, viewport) if not specified in $head,
4150
- * and strips these assets from the original template head to avoid duplicates.
4151
3957
  */
4152
3958
  setupEarlyHeadContent() {
4153
- const assets = this.ssrManifestProvider.getEntryAssets();
4154
3959
  const globalHead = this.serverHeadProvider.resolveGlobalHead();
3960
+ const manifest = this.ssrManifestProvider.getManifest();
3961
+ if (manifest.devHead) {
3962
+ this.templateProvider.setEarlyHeadContent(`${manifest.devHead}\n`, globalHead);
3963
+ this.log.debug("Early head content set (dev mode)");
3964
+ return;
3965
+ }
4155
3966
  const parts = [];
3967
+ const assets = this.ssrManifestProvider.getEntryAssets();
4156
3968
  if (assets) {
4157
3969
  for (const css of assets.css) parts.push(`<link rel="stylesheet" href="${css}" crossorigin="">`);
4158
3970
  if (assets.js) parts.push(`<script type="module" crossorigin="" src="${assets.js}"><\/script>`);
4159
3971
  }
4160
- this.templateProvider.setEarlyHeadContent(parts.length > 0 ? `${parts.join("\n")}\n` : "", globalHead, assets ?? void 0);
4161
- this.log.debug("Early head content set", {
4162
- css: assets?.css.length ?? 0,
4163
- js: assets?.js ? 1 : 0
4164
- });
3972
+ this.templateProvider.setEarlyHeadContent(parts.length > 0 ? `${parts.join("\n")}\n` : "", globalHead);
3973
+ this.log.debug("Early head content set", { parts: parts.length });
4165
3974
  }
4166
3975
  /**
4167
3976
  * Get the public directory path where static files are located.
@@ -4187,15 +3996,9 @@ var ReactServerProvider = class {
4187
3996
  /**
4188
3997
  * Create the request handler for a page route.
4189
3998
  */
4190
- createHandler(route, templateLoader) {
3999
+ createHandler(route) {
4191
4000
  return async (serverRequest) => {
4192
4001
  const { url, reply, query, params } = serverRequest;
4193
- if (!this.templateProvider.isReady()) {
4194
- const template = await templateLoader();
4195
- if (!template) throw new AlephaError("Missing template for SSR rendering");
4196
- this.templateProvider.parseTemplate(template);
4197
- this.setupEarlyHeadContent();
4198
- }
4199
4002
  this.log.trace("Rendering page", { name: route.name });
4200
4003
  const state = {
4201
4004
  url,
@@ -4298,10 +4101,6 @@ var ReactServerProvider = class {
4298
4101
  };
4299
4102
  this.log.trace("Rendering", { url });
4300
4103
  await this.alepha.events.emit("react:server:render:begin", { state });
4301
- if (!this.templateProvider.isReady()) {
4302
- this.templateProvider.parseTemplate(this.template);
4303
- this.setupEarlyHeadContent();
4304
- }
4305
4104
  const result = await this.renderPage(page, state);
4306
4105
  if (result.redirect) return {
4307
4106
  state,
@@ -4573,7 +4372,7 @@ var ReactBrowserProvider = class {
4573
4372
  }
4574
4373
  await this.render({ previous });
4575
4374
  }
4576
- async go(url, options = {}) {
4375
+ async push(url, options = {}) {
4577
4376
  this.log.trace(`Going to ${url}`, {
4578
4377
  url,
4579
4378
  options
@@ -4710,7 +4509,7 @@ var ReactRouter = class {
4710
4509
  */
4711
4510
  async reload() {
4712
4511
  if (!this.browser) return;
4713
- await this.go(this.location.pathname + this.location.search, {
4512
+ await this.push(this.location.pathname + this.location.search, {
4714
4513
  replace: true,
4715
4514
  force: true
4716
4515
  });
@@ -4743,12 +4542,12 @@ var ReactRouter = class {
4743
4542
  async invalidate(props) {
4744
4543
  await this.browser?.invalidate(props);
4745
4544
  }
4746
- async go(path, options) {
4545
+ async push(path, options) {
4747
4546
  for (const page of this.pages) if (page.name === path) {
4748
- await this.browser?.go(this.path(path, options), options);
4547
+ await this.browser?.push(this.path(path, options), options);
4749
4548
  return;
4750
4549
  }
4751
- await this.browser?.go(path, options);
4550
+ await this.browser?.push(path, options);
4752
4551
  }
4753
4552
  anchor(path, options = {}) {
4754
4553
  let href = path;
@@ -4761,7 +4560,7 @@ var ReactRouter = class {
4761
4560
  onClick: (ev) => {
4762
4561
  ev.stopPropagation();
4763
4562
  ev.preventDefault();
4764
- this.go(href, options).catch(console.error);
4563
+ this.push(href, options).catch(console.error);
4765
4564
  }
4766
4565
  };
4767
4566
  }
@@ -4799,7 +4598,7 @@ var ReactRouter = class {
4799
4598
  * }
4800
4599
  *
4801
4600
  * const router = useRouter<App>();
4802
- * router.go("home"); // typesafe
4601
+ * router.push("home"); // typesafe
4803
4602
  */
4804
4603
  const useRouter = () => {
4805
4604
  return useInject(ReactRouter);
@@ -4849,7 +4648,7 @@ const useActive = (args) => {
4849
4648
  if (isPending) return;
4850
4649
  setPending(true);
4851
4650
  try {
4852
- await router.go(href);
4651
+ await router.push(href);
4853
4652
  } finally {
4854
4653
  setPending(false);
4855
4654
  }
@@ -4915,6 +4714,7 @@ const AlephaReactRouter = $module({
4915
4714
  services: [
4916
4715
  ReactPageProvider,
4917
4716
  ReactPageService,
4717
+ ReactPreloadProvider,
4918
4718
  ReactRouter,
4919
4719
  ReactServerProvider,
4920
4720
  ReactServerTemplateProvider,
@@ -4924,9 +4724,9 @@ const AlephaReactRouter = $module({
4924
4724
  register: (alepha) => alepha.with(AlephaReact).with(AlephaDateTime).with(AlephaServer).with(AlephaServerCache).with(AlephaServerLinks).with({
4925
4725
  provide: ReactPageService,
4926
4726
  use: ReactPageServerService
4927
- }).with(SSRManifestProvider).with(ReactServerTemplateProvider).with(ReactServerProvider).with(ReactPageProvider).with(ReactRouter)
4727
+ }).with(SSRManifestProvider).with(ReactServerTemplateProvider).with(ReactPreloadProvider).with(ReactServerProvider).with(ReactPageProvider).with(ReactRouter)
4928
4728
  });
4929
4729
 
4930
4730
  //#endregion
4931
- export { $page, AlephaReactRouter, ErrorViewer_default as ErrorViewer, Link_default as Link, NestedView_default as NestedView, NotFound_default as NotFound, PAGE_PRELOAD_KEY, PagePrimitive, ReactBrowserProvider, ReactPageProvider, ReactPageService, ReactRouter, ReactServerProvider, ReactServerTemplateProvider, Redirection, RouterLayerContext, SSRManifestProvider, isPageRoute, reactBrowserOptions, reactServerOptions, useActive, useQueryParams, useRouter, useRouterState };
4731
+ export { $page, AlephaReactRouter, ErrorViewer_default as ErrorViewer, Link_default as Link, NestedView_default as NestedView, NotFound_default as NotFound, PAGE_PRELOAD_KEY, PagePrimitive, ReactBrowserProvider, ReactPageProvider, ReactPageService, ReactPreloadProvider, ReactRouter, ReactServerProvider, ReactServerTemplateProvider, Redirection, RouterLayerContext, SSRManifestProvider, isPageRoute, reactBrowserOptions, reactServerOptions, useActive, useQueryParams, useRouter, useRouterState };
4932
4732
  //# sourceMappingURL=index.js.map