alepha 0.15.2 → 0.15.3

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 (132) hide show
  1. package/README.md +68 -80
  2. package/dist/api/audits/index.d.ts +332 -332
  3. package/dist/api/audits/index.d.ts.map +1 -1
  4. package/dist/api/files/index.d.ts +170 -170
  5. package/dist/api/files/index.d.ts.map +1 -1
  6. package/dist/api/jobs/index.d.ts +151 -151
  7. package/dist/api/keys/index.d.ts +195 -195
  8. package/dist/api/keys/index.d.ts.map +1 -1
  9. package/dist/api/parameters/index.d.ts +260 -260
  10. package/dist/api/users/index.d.ts +22 -11
  11. package/dist/api/users/index.d.ts.map +1 -1
  12. package/dist/api/users/index.js +7 -2
  13. package/dist/api/users/index.js.map +1 -1
  14. package/dist/api/verifications/index.d.ts +128 -128
  15. package/dist/api/verifications/index.d.ts.map +1 -1
  16. package/dist/bucket/index.d.ts +8 -0
  17. package/dist/bucket/index.d.ts.map +1 -1
  18. package/dist/bucket/index.js +7 -2
  19. package/dist/bucket/index.js.map +1 -1
  20. package/dist/cli/index.d.ts +191 -74
  21. package/dist/cli/index.d.ts.map +1 -1
  22. package/dist/cli/index.js +215 -48
  23. package/dist/cli/index.js.map +1 -1
  24. package/dist/command/index.d.ts +10 -0
  25. package/dist/command/index.d.ts.map +1 -1
  26. package/dist/command/index.js +67 -13
  27. package/dist/command/index.js.map +1 -1
  28. package/dist/core/index.browser.js +28 -21
  29. package/dist/core/index.browser.js.map +1 -1
  30. package/dist/core/index.d.ts.map +1 -1
  31. package/dist/core/index.js +28 -21
  32. package/dist/core/index.js.map +1 -1
  33. package/dist/core/index.native.js +28 -21
  34. package/dist/core/index.native.js.map +1 -1
  35. package/dist/email/index.d.ts +8 -0
  36. package/dist/email/index.d.ts.map +1 -1
  37. package/dist/email/index.js +7 -2
  38. package/dist/email/index.js.map +1 -1
  39. package/dist/mcp/index.d.ts +5 -5
  40. package/dist/orm/index.bun.js +32 -16
  41. package/dist/orm/index.bun.js.map +1 -1
  42. package/dist/orm/index.d.ts +4 -1
  43. package/dist/orm/index.d.ts.map +1 -1
  44. package/dist/orm/index.js +34 -22
  45. package/dist/orm/index.js.map +1 -1
  46. package/dist/react/router/index.browser.js +9 -15
  47. package/dist/react/router/index.browser.js.map +1 -1
  48. package/dist/react/router/index.d.ts +295 -407
  49. package/dist/react/router/index.d.ts.map +1 -1
  50. package/dist/react/router/index.js +566 -776
  51. package/dist/react/router/index.js.map +1 -1
  52. package/dist/redis/index.d.ts +19 -19
  53. package/dist/security/index.d.ts +42 -42
  54. package/dist/security/index.d.ts.map +1 -1
  55. package/dist/security/index.js +8 -7
  56. package/dist/security/index.js.map +1 -1
  57. package/dist/server/auth/index.d.ts +167 -167
  58. package/dist/server/core/index.d.ts +9 -9
  59. package/dist/server/health/index.d.ts +17 -17
  60. package/dist/server/links/index.d.ts +39 -39
  61. package/dist/server/static/index.js +7 -2
  62. package/dist/server/static/index.js.map +1 -1
  63. package/dist/server/swagger/index.d.ts +8 -0
  64. package/dist/server/swagger/index.d.ts.map +1 -1
  65. package/dist/server/swagger/index.js +7 -2
  66. package/dist/server/swagger/index.js.map +1 -1
  67. package/dist/sms/index.d.ts +8 -0
  68. package/dist/sms/index.d.ts.map +1 -1
  69. package/dist/sms/index.js +7 -2
  70. package/dist/sms/index.js.map +1 -1
  71. package/dist/system/index.browser.js +734 -12
  72. package/dist/system/index.browser.js.map +1 -1
  73. package/dist/system/index.d.ts +8 -0
  74. package/dist/system/index.d.ts.map +1 -1
  75. package/dist/system/index.js +7 -2
  76. package/dist/system/index.js.map +1 -1
  77. package/dist/vite/index.d.ts +1 -1
  78. package/dist/vite/index.js +15 -7
  79. package/dist/vite/index.js.map +1 -1
  80. package/package.json +4 -2
  81. package/src/api/logs/TODO.md +13 -10
  82. package/src/cli/apps/AlephaPackageBuilderCli.ts +9 -0
  83. package/src/cli/atoms/buildOptions.ts +99 -9
  84. package/src/cli/commands/build.ts +149 -32
  85. package/src/cli/commands/db.ts +5 -7
  86. package/src/cli/commands/init.spec.ts +50 -6
  87. package/src/cli/commands/init.ts +28 -5
  88. package/src/cli/providers/ViteDevServerProvider.ts +1 -10
  89. package/src/cli/services/AlephaCliUtils.ts +16 -0
  90. package/src/cli/services/PackageManagerUtils.ts +2 -0
  91. package/src/cli/services/ProjectScaffolder.spec.ts +97 -0
  92. package/src/cli/services/ProjectScaffolder.ts +28 -6
  93. package/src/cli/templates/agentMd.ts +6 -1
  94. package/src/cli/templates/apiAppSecurityTs.ts +11 -0
  95. package/src/cli/templates/apiIndexTs.ts +18 -4
  96. package/src/cli/templates/webAppRouterTs.ts +25 -1
  97. package/src/cli/templates/webHelloComponentTsx.ts +15 -5
  98. package/src/command/helpers/Runner.spec.ts +135 -0
  99. package/src/command/helpers/Runner.ts +4 -1
  100. package/src/command/providers/CliProvider.spec.ts +325 -0
  101. package/src/command/providers/CliProvider.ts +117 -7
  102. package/src/core/Alepha.ts +32 -25
  103. package/src/orm/index.bun.ts +1 -1
  104. package/src/orm/index.ts +2 -6
  105. package/src/orm/providers/drivers/BunSqliteProvider.ts +4 -1
  106. package/src/orm/providers/drivers/CloudflareD1Provider.ts +57 -30
  107. package/src/orm/providers/drivers/DatabaseProvider.ts +9 -1
  108. package/src/orm/providers/drivers/NodeSqliteProvider.ts +4 -1
  109. package/src/react/router/hooks/useActive.ts +1 -1
  110. package/src/react/router/hooks/useRouter.ts +1 -1
  111. package/src/react/router/index.ts +4 -0
  112. package/src/react/router/primitives/$page.browser.spec.tsx +24 -24
  113. package/src/react/router/primitives/$page.spec.tsx +0 -32
  114. package/src/react/router/primitives/$page.ts +6 -14
  115. package/src/react/router/providers/ReactBrowserProvider.ts +6 -3
  116. package/src/react/router/providers/ReactPageProvider.ts +1 -1
  117. package/src/react/router/providers/ReactPreloadProvider.spec.ts +142 -0
  118. package/src/react/router/providers/ReactPreloadProvider.ts +85 -0
  119. package/src/react/router/providers/ReactServerProvider.ts +7 -78
  120. package/src/react/router/providers/ReactServerTemplateProvider.spec.ts +210 -0
  121. package/src/react/router/providers/ReactServerTemplateProvider.ts +228 -665
  122. package/src/react/router/services/ReactRouter.ts +13 -13
  123. package/src/security/__tests__/ServerSecurityProvider.spec.ts +77 -0
  124. package/src/security/providers/ServerSecurityProvider.ts +30 -22
  125. package/src/server/core/providers/NodeHttpServerProvider.spec.ts +9 -3
  126. package/src/system/index.browser.ts +25 -0
  127. package/src/system/index.workerd.ts +1 -0
  128. package/src/system/providers/FileSystemProvider.ts +8 -0
  129. package/src/system/providers/NodeFileSystemProvider.ts +11 -2
  130. package/src/vite/tasks/buildServer.ts +2 -12
  131. package/src/vite/tasks/generateCloudflare.ts +10 -7
  132. 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,162 +1440,452 @@ 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
+ });
1457
1457
  /**
1458
- * In-memory implementation of FileSystemProvider for testing.
1458
+ * SSR Manifest atom containing all manifest data for SSR module preloading.
1459
1459
  *
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.
1460
+ * This atom is populated at build time by embedding manifest data into the
1461
+ * generated index.js. This approach is optimal for serverless deployments
1462
+ * as it eliminates filesystem reads at runtime.
1462
1463
  *
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
- * });
1464
+ * The manifest includes:
1465
+ * - preload: Maps short hash keys to source paths (from viteAlephaSsrPreload)
1466
+ * - client: Maps source files to their output info (file, imports, css)
1467
+ */
1468
+ const ssrManifestAtom = $atom({
1469
+ name: "alepha.react.ssr.manifest",
1470
+ description: "SSR manifest for module preloading",
1471
+ schema: ssrManifestAtomSchema,
1472
+ default: {}
1473
+ });
1474
+
1475
+ //#endregion
1476
+ //#region ../../src/react/router/providers/SSRManifestProvider.ts
1477
+ /**
1478
+ * Provider for SSR manifest data used for module preloading.
1470
1479
  *
1471
- * // Run code that uses FileSystemProvider
1472
- * const service = alepha.inject(MyService);
1473
- * await service.saveFile("test.txt", "Hello World");
1480
+ * The manifest is populated at build time by embedding data into the
1481
+ * generated index.js via the ssrManifestAtom. This eliminates filesystem
1482
+ * reads at runtime, making it optimal for serverless deployments.
1474
1483
  *
1475
- * // Verify the file was written
1476
- * const memoryFs = alepha.inject(MemoryFileSystemProvider);
1477
- * expect(memoryFs.files.get("test.txt")?.toString()).toBe("Hello World");
1478
- * ```
1484
+ * Manifest files are generated during `vite build`:
1485
+ * - manifest.json (client manifest)
1486
+ * - preload-manifest.json (from viteAlephaSsrPreload plugin)
1479
1487
  */
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();
1490
- /**
1491
- * Track mkdir calls for test assertions
1492
- */
1493
- mkdirCalls = [];
1488
+ var SSRManifestProvider = class {
1489
+ alepha = $inject(Alepha);
1494
1490
  /**
1495
- * Track writeFile calls for test assertions
1491
+ * Get the manifest from the store at runtime.
1492
+ * This ensures the manifest is available even when set after module load.
1496
1493
  */
1497
- writeFileCalls = [];
1494
+ get manifest() {
1495
+ return this.alepha.store.get(ssrManifestAtom) ?? {};
1496
+ }
1498
1497
  /**
1499
- * Track readFile calls for test assertions
1498
+ * Get the base path for assets (from Vite's base config).
1499
+ * Returns empty string if base is "/" (default), otherwise returns the base path.
1500
1500
  */
1501
- readFileCalls = [];
1501
+ get base() {
1502
+ return this.manifest.base ?? "";
1503
+ }
1502
1504
  /**
1503
- * Track rm calls for test assertions
1505
+ * Get the preload manifest.
1504
1506
  */
1505
- rmCalls = [];
1507
+ get preloadManifest() {
1508
+ return this.manifest.preload;
1509
+ }
1506
1510
  /**
1507
- * Track join calls for test assertions
1511
+ * Get the client manifest.
1508
1512
  */
1509
- joinCalls = [];
1513
+ get clientManifest() {
1514
+ return this.manifest.client;
1515
+ }
1510
1516
  /**
1511
- * Error to throw on mkdir (for testing error handling)
1517
+ * Resolve a preload key to its source path.
1518
+ *
1519
+ * The key is a short hash injected by viteAlephaSsrPreload plugin,
1520
+ * which maps to the full source path in the preload manifest.
1521
+ *
1522
+ * @param key - Short hash key (e.g., "a1b2c3d4")
1523
+ * @returns Source path (e.g., "src/pages/UserDetail.tsx") or undefined
1512
1524
  */
1513
- mkdirError = null;
1525
+ resolvePreloadKey(key) {
1526
+ return this.preloadManifest?.[key];
1527
+ }
1514
1528
  /**
1515
- * Error to throw on writeFile (for testing error handling)
1529
+ * Get all chunks required for a source file, including transitive dependencies.
1530
+ *
1531
+ * Uses the client manifest to recursively resolve all imported chunks.
1532
+ *
1533
+ * @param sourcePath - Source file path (e.g., "src/pages/Home.tsx")
1534
+ * @returns Array of chunk URLs to preload, or empty array if not found
1516
1535
  */
1517
- writeFileError = null;
1536
+ getChunks(sourcePath) {
1537
+ if (!this.clientManifest) return [];
1538
+ if (!this.findManifestEntry(sourcePath)) return [];
1539
+ const chunks = /* @__PURE__ */ new Set();
1540
+ const visited = /* @__PURE__ */ new Set();
1541
+ this.collectChunksRecursive(sourcePath, chunks, visited);
1542
+ return Array.from(chunks);
1543
+ }
1518
1544
  /**
1519
- * Error to throw on readFile (for testing error handling)
1545
+ * Find manifest entry for a source path, trying different extensions.
1520
1546
  */
1521
- readFileError = null;
1522
- constructor(options = {}) {
1523
- this.mkdirError = options.mkdirError ?? null;
1524
- this.writeFileError = options.writeFileError ?? null;
1525
- this.readFileError = options.readFileError ?? null;
1547
+ findManifestEntry(sourcePath) {
1548
+ if (!this.clientManifest) return void 0;
1549
+ if (this.clientManifest[sourcePath]) return this.clientManifest[sourcePath];
1550
+ const basePath = sourcePath.replace(/\.[^.]+$/, "");
1551
+ for (const ext of [
1552
+ ".tsx",
1553
+ ".ts",
1554
+ ".jsx",
1555
+ ".js"
1556
+ ]) {
1557
+ const pathWithExt = basePath + ext;
1558
+ if (this.clientManifest[pathWithExt]) return this.clientManifest[pathWithExt];
1559
+ }
1526
1560
  }
1527
1561
  /**
1528
- * Join path segments using forward slashes.
1529
- * Uses Node's path.join for proper normalization (handles .. and .)
1562
+ * Recursively collect all chunk URLs for a manifest entry.
1530
1563
  */
1531
- join(...paths) {
1532
- this.joinCalls.push(paths);
1533
- return join(...paths);
1564
+ collectChunksRecursive(key, chunks, visited) {
1565
+ if (visited.has(key)) return;
1566
+ visited.add(key);
1567
+ if (!this.clientManifest) return;
1568
+ const entry = this.clientManifest[key];
1569
+ if (!entry) return;
1570
+ const base = this.base;
1571
+ if (entry.file) chunks.add(`${base}/${entry.file}`);
1572
+ if (entry.css) for (const css of entry.css) chunks.add(`${base}/${css}`);
1573
+ if (entry.imports) for (const imp of entry.imports) {
1574
+ if (imp === "index.html" || imp.endsWith(".html")) continue;
1575
+ this.collectChunksRecursive(imp, chunks, visited);
1576
+ }
1534
1577
  }
1535
1578
  /**
1536
- * Create a FileLike object from various sources.
1579
+ * Collect modulepreload links for a route and its parent chain.
1537
1580
  */
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
- };
1581
+ collectPreloadLinks(route) {
1582
+ if (!this.isAvailable()) return [];
1583
+ const preloadPaths = [];
1584
+ let current = route;
1585
+ while (current) {
1586
+ const preloadKey = current[PAGE_PRELOAD_KEY];
1587
+ if (preloadKey) {
1588
+ const sourcePath = this.resolvePreloadKey(preloadKey);
1589
+ if (sourcePath) preloadPaths.push(sourcePath);
1590
+ }
1591
+ current = current.parent;
1554
1592
  }
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")
1593
+ if (preloadPaths.length === 0) return [];
1594
+ return this.getChunksForMultiple(preloadPaths).map((href) => {
1595
+ if (href.endsWith(".css")) return {
1596
+ rel: "preload",
1597
+ href,
1598
+ as: "style",
1599
+ crossorigin: ""
1567
1600
  };
1568
- }
1569
- if ("text" in options) {
1570
- const buffer = Buffer.from(options.text, "utf-8");
1571
1601
  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
1602
+ rel: "modulepreload",
1603
+ href
1581
1604
  };
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
1605
  });
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
1606
  }
1601
1607
  /**
1602
- * Copy a file or directory in memory.
1608
+ * Get all chunks for multiple source files.
1609
+ *
1610
+ * @param sourcePaths - Array of source file paths
1611
+ * @returns Deduplicated array of chunk URLs
1603
1612
  */
1604
- async cp(src, dest, options) {
1613
+ getChunksForMultiple(sourcePaths) {
1614
+ const allChunks = /* @__PURE__ */ new Set();
1615
+ for (const path of sourcePaths) {
1616
+ const chunks = this.getChunks(path);
1617
+ for (const chunk of chunks) allChunks.add(chunk);
1618
+ }
1619
+ return Array.from(allChunks);
1620
+ }
1621
+ /**
1622
+ * Check if manifest is loaded and available.
1623
+ */
1624
+ isAvailable() {
1625
+ return this.clientManifest !== void 0;
1626
+ }
1627
+ /**
1628
+ * Cached entry assets - computed once at first access.
1629
+ */
1630
+ cachedEntryAssets = null;
1631
+ /**
1632
+ * Get the entry point assets (main entry.js and associated CSS files).
1633
+ *
1634
+ * These assets are always required for all pages and can be preloaded
1635
+ * before page-specific loaders run.
1636
+ *
1637
+ * @returns Entry assets with js and css paths, or null if manifest unavailable
1638
+ */
1639
+ getEntryAssets() {
1640
+ if (this.cachedEntryAssets) return this.cachedEntryAssets;
1641
+ if (!this.clientManifest) return null;
1642
+ const base = this.base;
1643
+ for (const [key, entry] of Object.entries(this.clientManifest)) if (entry.isEntry) {
1644
+ this.cachedEntryAssets = {
1645
+ js: `${base}/${entry.file}`,
1646
+ css: entry.css?.map((css) => `${base}/${css}`) ?? []
1647
+ };
1648
+ return this.cachedEntryAssets;
1649
+ }
1650
+ return null;
1651
+ }
1652
+ /**
1653
+ * Build preload link tags for entry assets.
1654
+ *
1655
+ * @returns Array of link objects ready to be rendered
1656
+ */
1657
+ getEntryPreloadLinks() {
1658
+ const assets = this.getEntryAssets();
1659
+ if (!assets) return [];
1660
+ const links = [];
1661
+ for (const css of assets.css) links.push({
1662
+ rel: "stylesheet",
1663
+ href: css,
1664
+ crossorigin: ""
1665
+ });
1666
+ if (assets.js) links.push({
1667
+ rel: "modulepreload",
1668
+ href: assets.js
1669
+ });
1670
+ return links;
1671
+ }
1672
+ };
1673
+
1674
+ //#endregion
1675
+ //#region ../../src/react/router/providers/ReactPreloadProvider.ts
1676
+ /**
1677
+ * Adds HTTP Link headers for preloading entry assets.
1678
+ *
1679
+ * Benefits:
1680
+ * - Early Hints (103): Servers can send preload hints before the full response
1681
+ * - CDN optimization: Many CDNs use Link headers to optimize asset delivery
1682
+ * - Browser prefetching: Browsers can start fetching resources earlier
1683
+ *
1684
+ * The Link header is computed once at first request and cached for reuse.
1685
+ */
1686
+ var ReactPreloadProvider = class {
1687
+ alepha = $inject(Alepha);
1688
+ ssrManifest = $inject(SSRManifestProvider);
1689
+ /**
1690
+ * Cached Link header value - computed once, reused for all requests.
1691
+ */
1692
+ cachedLinkHeader;
1693
+ /**
1694
+ * Build the Link header string from entry assets.
1695
+ *
1696
+ * Format: <url>; rel=preload; as=type, <url>; rel=modulepreload
1697
+ *
1698
+ * @returns Link header string or null if no assets
1699
+ */
1700
+ buildLinkHeader() {
1701
+ const assets = this.ssrManifest.getEntryAssets();
1702
+ if (!assets) return null;
1703
+ const links = [];
1704
+ for (const css of assets.css) links.push(`<${css}>; rel=preload; as=style`);
1705
+ if (assets.js) links.push(`<${assets.js}>; rel=modulepreload`);
1706
+ return links.length > 0 ? links.join(", ") : null;
1707
+ }
1708
+ /**
1709
+ * Get the cached Link header, computing it on first access.
1710
+ */
1711
+ getLinkHeader() {
1712
+ if (this.cachedLinkHeader === void 0) this.cachedLinkHeader = this.buildLinkHeader();
1713
+ return this.cachedLinkHeader;
1714
+ }
1715
+ /**
1716
+ * Add Link header to HTML responses for asset preloading.
1717
+ */
1718
+ onResponse = $hook({
1719
+ on: "server:onResponse",
1720
+ priority: "first",
1721
+ handler: ({ response }) => {
1722
+ const contentType = response.headers["content-type"];
1723
+ if (!contentType || !contentType.includes("text/html")) return;
1724
+ const linkHeader = this.getLinkHeader();
1725
+ if (!linkHeader) return;
1726
+ if (response.headers.link) response.headers.link = `${response.headers.link}, ${linkHeader}`;
1727
+ else response.headers.link = linkHeader;
1728
+ }
1729
+ });
1730
+ };
1731
+
1732
+ //#endregion
1733
+ //#region ../../src/system/providers/FileSystemProvider.ts
1734
+ /**
1735
+ * FileSystem interface providing utilities for working with files.
1736
+ */
1737
+ var FileSystemProvider = class {};
1738
+
1739
+ //#endregion
1740
+ //#region ../../src/system/providers/MemoryFileSystemProvider.ts
1741
+ /**
1742
+ * In-memory implementation of FileSystemProvider for testing.
1743
+ *
1744
+ * This provider stores all files and directories in memory, making it ideal for
1745
+ * unit tests that need to verify file operations without touching the real file system.
1746
+ *
1747
+ * @example
1748
+ * ```typescript
1749
+ * // In tests, substitute the real FileSystemProvider with MemoryFileSystemProvider
1750
+ * const alepha = Alepha.create().with({
1751
+ * provide: FileSystemProvider,
1752
+ * use: MemoryFileSystemProvider,
1753
+ * });
1754
+ *
1755
+ * // Run code that uses FileSystemProvider
1756
+ * const service = alepha.inject(MyService);
1757
+ * await service.saveFile("test.txt", "Hello World");
1758
+ *
1759
+ * // Verify the file was written
1760
+ * const memoryFs = alepha.inject(MemoryFileSystemProvider);
1761
+ * expect(memoryFs.files.get("test.txt")?.toString()).toBe("Hello World");
1762
+ * ```
1763
+ */
1764
+ var MemoryFileSystemProvider = class {
1765
+ json = $inject(Json);
1766
+ /**
1767
+ * In-memory storage for files (path -> content)
1768
+ */
1769
+ files = /* @__PURE__ */ new Map();
1770
+ /**
1771
+ * In-memory storage for directories
1772
+ */
1773
+ directories = /* @__PURE__ */ new Set();
1774
+ /**
1775
+ * Track mkdir calls for test assertions
1776
+ */
1777
+ mkdirCalls = [];
1778
+ /**
1779
+ * Track writeFile calls for test assertions
1780
+ */
1781
+ writeFileCalls = [];
1782
+ /**
1783
+ * Track readFile calls for test assertions
1784
+ */
1785
+ readFileCalls = [];
1786
+ /**
1787
+ * Track rm calls for test assertions
1788
+ */
1789
+ rmCalls = [];
1790
+ /**
1791
+ * Track join calls for test assertions
1792
+ */
1793
+ joinCalls = [];
1794
+ /**
1795
+ * Error to throw on mkdir (for testing error handling)
1796
+ */
1797
+ mkdirError = null;
1798
+ /**
1799
+ * Error to throw on writeFile (for testing error handling)
1800
+ */
1801
+ writeFileError = null;
1802
+ /**
1803
+ * Error to throw on readFile (for testing error handling)
1804
+ */
1805
+ readFileError = null;
1806
+ constructor(options = {}) {
1807
+ this.mkdirError = options.mkdirError ?? null;
1808
+ this.writeFileError = options.writeFileError ?? null;
1809
+ this.readFileError = options.readFileError ?? null;
1810
+ }
1811
+ /**
1812
+ * Join path segments using forward slashes.
1813
+ * Uses Node's path.join for proper normalization (handles .. and .)
1814
+ */
1815
+ join(...paths) {
1816
+ this.joinCalls.push(paths);
1817
+ return join(...paths);
1818
+ }
1819
+ /**
1820
+ * Create a FileLike object from various sources.
1821
+ */
1822
+ createFile(options) {
1823
+ if ("path" in options) {
1824
+ const filePath = options.path;
1825
+ const buffer = this.files.get(filePath);
1826
+ if (buffer === void 0) throw new Error(`ENOENT: no such file or directory, open '${filePath}'`);
1827
+ return {
1828
+ name: options.name ?? filePath.split("/").pop() ?? "file",
1829
+ type: options.type ?? "application/octet-stream",
1830
+ size: buffer.byteLength,
1831
+ lastModified: Date.now(),
1832
+ stream: () => {
1833
+ throw new Error("Stream not implemented in MemoryFileSystemProvider");
1834
+ },
1835
+ arrayBuffer: async () => buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength),
1836
+ text: async () => buffer.toString("utf-8")
1837
+ };
1838
+ }
1839
+ if ("buffer" in options) {
1840
+ const buffer = options.buffer;
1841
+ return {
1842
+ name: options.name ?? "file",
1843
+ type: options.type ?? "application/octet-stream",
1844
+ size: buffer.byteLength,
1845
+ lastModified: Date.now(),
1846
+ stream: () => {
1847
+ throw new Error("Stream not implemented in MemoryFileSystemProvider");
1848
+ },
1849
+ arrayBuffer: async () => buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength),
1850
+ text: async () => buffer.toString("utf-8")
1851
+ };
1852
+ }
1853
+ if ("text" in options) {
1854
+ const buffer = Buffer.from(options.text, "utf-8");
1855
+ return {
1856
+ name: options.name ?? "file.txt",
1857
+ type: options.type ?? "text/plain",
1858
+ size: buffer.byteLength,
1859
+ lastModified: Date.now(),
1860
+ stream: () => {
1861
+ throw new Error("Stream not implemented in MemoryFileSystemProvider");
1862
+ },
1863
+ arrayBuffer: async () => buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength),
1864
+ text: async () => options.text
1865
+ };
1866
+ }
1867
+ throw new Error("MemoryFileSystemProvider.createFile: unsupported options. Only buffer and text are supported.");
1868
+ }
1869
+ /**
1870
+ * Remove a file or directory from memory.
1871
+ */
1872
+ async rm(path, options) {
1873
+ this.rmCalls.push({
1874
+ path,
1875
+ options
1876
+ });
1877
+ if (!(this.files.has(path) || this.directories.has(path)) && !options?.force) throw new Error(`ENOENT: no such file or directory, rm '${path}'`);
1878
+ if (this.directories.has(path)) if (options?.recursive) {
1879
+ this.directories.delete(path);
1880
+ for (const filePath of this.files.keys()) if (filePath.startsWith(`${path}/`)) this.files.delete(filePath);
1881
+ for (const dirPath of this.directories) if (dirPath.startsWith(`${path}/`)) this.directories.delete(dirPath);
1882
+ } else throw new Error(`EISDIR: illegal operation on a directory, rm '${path}'`);
1883
+ else this.files.delete(path);
1884
+ }
1885
+ /**
1886
+ * Copy a file or directory in memory.
1887
+ */
1888
+ async cp(src, dest, options) {
1605
1889
  if (this.directories.has(src)) {
1606
1890
  if (!options?.recursive) throw new Error(`Cannot copy directory without recursive option: ${src}`);
1607
1891
  this.directories.add(dest);
@@ -2804,8 +3088,13 @@ var NodeFileSystemProvider = class {
2804
3088
  * await fs.mkdir("/tmp/mydir", { mode: 0o755 });
2805
3089
  * ```
2806
3090
  */
2807
- async mkdir(path, options) {
2808
- await mkdir(path, options);
3091
+ async mkdir(path, options = {}) {
3092
+ const p = mkdir(path, {
3093
+ recursive: options.recursive ?? true,
3094
+ mode: options.mode
3095
+ });
3096
+ if (options.force === false) await p;
3097
+ else await p.catch(() => {});
2809
3098
  }
2810
3099
  /**
2811
3100
  * Lists files in a directory.
@@ -3283,235 +3572,99 @@ const AlephaSystem = $module({
3283
3572
  //#endregion
3284
3573
  //#region ../../src/react/router/providers/ReactServerTemplateProvider.ts
3285
3574
  /**
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
3575
+ * Handles HTML streaming for SSR.
3293
3576
  *
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.
3577
+ * Uses hardcoded HTML structure - all customization via $head primitive.
3578
+ * Pre-encodes static parts as Uint8Array for zero-copy streaming.
3297
3579
  */
3298
3580
  var ReactServerTemplateProvider = class {
3299
3581
  log = $logger();
3300
3582
  alepha = $inject(Alepha);
3301
3583
  /**
3302
- * Shared TextEncoder instance - reused across all requests.
3584
+ * Shared TextEncoder - reused across all requests.
3303
3585
  */
3304
3586
  encoder = new TextEncoder();
3305
3587
  /**
3306
- * Pre-encoded common strings for streaming.
3307
- */
3308
- ENCODED = {
3588
+ * Pre-encoded static HTML parts for zero-copy streaming.
3589
+ */
3590
+ SLOTS = {
3591
+ DOCTYPE: this.encoder.encode("<!DOCTYPE html>\n"),
3592
+ HTML_OPEN: this.encoder.encode("<html"),
3593
+ HTML_CLOSE: this.encoder.encode(">\n"),
3594
+ HEAD_OPEN: this.encoder.encode("<head>"),
3595
+ HEAD_CLOSE: this.encoder.encode("</head>\n"),
3596
+ BODY_OPEN: this.encoder.encode("<body"),
3597
+ BODY_CLOSE: this.encoder.encode(">\n"),
3598
+ ROOT_OPEN: this.encoder.encode("<div id=\"root\">"),
3599
+ ROOT_CLOSE: this.encoder.encode("</div>\n"),
3600
+ BODY_HTML_CLOSE: this.encoder.encode("</body>\n</html>"),
3309
3601
  HYDRATION_PREFIX: this.encoder.encode("<script>window.__ssr="),
3310
- HYDRATION_SUFFIX: this.encoder.encode("<\/script>"),
3311
- EMPTY: this.encoder.encode("")
3602
+ HYDRATION_SUFFIX: this.encoder.encode("<\/script>")
3312
3603
  };
3313
3604
  /**
3314
- * Cached template slots - parsed once, reused for all requests.
3605
+ * Early head content (charset, viewport, entry assets).
3606
+ * Set once during configuration, reused for all requests.
3315
3607
  */
3316
- slots = null;
3608
+ earlyHeadContent = "";
3317
3609
  /**
3318
3610
  * Root element ID for React mounting.
3319
3611
  */
3320
- get rootId() {
3321
- return "root";
3322
- }
3612
+ rootId = "root";
3323
3613
  /**
3324
- * Regex pattern for matching the root div and extracting its content.
3614
+ * Regex for extracting root div content from HTML.
3325
3615
  */
3326
- get rootDivRegex() {
3327
- return new RegExp(`<div([^>]*)\\s+id=["']${this.rootId}["']([^>]*)>([\\s\\S]*?)<\\/div>`, "i");
3328
- }
3616
+ rootDivRegex = new RegExp(`<div[^>]*\\s+id=["']${this.rootId}["'][^>]*>([\\s\\S]*?)<\\/div>`, "i");
3329
3617
  /**
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
3618
+ * Extract content inside the root div from HTML.
3334
3619
  */
3335
3620
  extractRootContent(html) {
3336
- return html.match(this.rootDivRegex)?.[3];
3621
+ return html.match(this.rootDivRegex)?.[1];
3337
3622
  }
3338
3623
  /**
3339
- * Check if template has been parsed and slots are available.
3624
+ * Set early head content (charset, viewport, entry assets).
3625
+ * Called once during server configuration.
3340
3626
  */
3341
- isReady() {
3342
- return this.slots !== null;
3627
+ setEarlyHeadContent(entryAssets, globalHead) {
3628
+ const charset = globalHead?.charset ?? "UTF-8";
3629
+ const viewport = globalHead?.viewport ?? "width=device-width, initial-scale=1";
3630
+ this.earlyHeadContent = `<meta charset="${this.escapeHtml(charset)}">\n<meta name="viewport" content="${this.escapeHtml(viewport)}">\n` + entryAssets;
3343
3631
  }
3344
3632
  /**
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"`
3633
+ * Render attributes record to HTML string.
3437
3634
  */
3438
3635
  renderAttributes(attrs) {
3636
+ if (!attrs) return "";
3439
3637
  const entries = Object.entries(attrs);
3440
3638
  if (entries.length === 0) return "";
3441
3639
  return entries.map(([key, value]) => ` ${key}="${this.escapeHtml(value)}"`).join("");
3442
3640
  }
3443
3641
  /**
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
3642
  * 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
3643
  */
3470
- renderHeadContent(head, includeOriginal = true) {
3471
- const slots = this.getSlots();
3644
+ renderHeadContent(head) {
3645
+ if (!head) return "";
3472
3646
  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);
3647
+ if (head.title) content += `<title>${this.escapeHtml(head.title)}</title>\n`;
3648
+ if (head.meta) {
3649
+ for (const meta of head.meta) if (meta.property) content += `<meta property="${this.escapeHtml(meta.property)}" content="${this.escapeHtml(meta.content)}">\n`;
3650
+ else if (meta.name) content += `<meta name="${this.escapeHtml(meta.name)}" content="${this.escapeHtml(meta.content)}">\n`;
3651
+ }
3652
+ if (head.link) for (const link of head.link) {
3653
+ content += `<link rel="${this.escapeHtml(link.rel)}" href="${this.escapeHtml(link.href)}"`;
3654
+ if (link.type) content += ` type="${this.escapeHtml(link.type)}"`;
3655
+ if (link.as) content += ` as="${this.escapeHtml(link.as)}"`;
3656
+ if (link.crossorigin != null) content += " crossorigin=\"\"";
3657
+ content += ">\n";
3658
+ }
3659
+ if (head.script) for (const script of head.script) if (typeof script === "string") content += `<script>${script}<\/script>\n`;
3660
+ else {
3661
+ const { content: scriptContent, ...rest } = script;
3662
+ const attrs = Object.entries(rest).filter(([, v]) => v !== false && v !== void 0).map(([k, v]) => v === true ? k : `${k}="${this.escapeHtml(String(v))}"`).join(" ");
3663
+ content += scriptContent ? `<script ${attrs}>${scriptContent}<\/script>\n` : `<script ${attrs}><\/script>\n`;
3664
+ }
3480
3665
  return content;
3481
3666
  }
3482
3667
  /**
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
3668
  * Escape HTML special characters.
3516
3669
  */
3517
3670
  escapeHtml(str) {
@@ -3519,16 +3672,12 @@ var ReactServerTemplateProvider = class {
3519
3672
  }
3520
3673
  /**
3521
3674
  * Safely serialize data to JSON for embedding in HTML.
3522
- * Escapes characters that could break out of script tags.
3523
3675
  */
3524
3676
  safeJsonSerialize(data) {
3525
3677
  return JSON.stringify(data).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
3526
3678
  }
3527
3679
  /**
3528
3680
  * 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
3681
  */
3533
3682
  buildHydrationData(state) {
3534
3683
  const { request, context, ...store } = this.alepha.context.als?.getStore() ?? {};
@@ -3548,169 +3697,75 @@ var ReactServerTemplateProvider = class {
3548
3697
  return hydrationData;
3549
3698
  }
3550
3699
  /**
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);
3700
+ * Pipe React stream to controller with backpressure handling.
3701
+ * Returns true if stream completed successfully, false if error occurred.
3702
+ */
3703
+ async pipeReactStream(controller, reactStream, state) {
3565
3704
  const reader = reactStream.getReader();
3566
- let streamError = null;
3567
3705
  try {
3568
3706
  while (true) {
3707
+ if (controller.desiredSize !== null && controller.desiredSize <= 0) await new Promise((resolve) => queueMicrotask(resolve));
3569
3708
  const { done, value } = await reader.read();
3570
3709
  if (done) break;
3571
3710
  controller.enqueue(value);
3572
3711
  }
3712
+ return true;
3573
3713
  } catch (error) {
3574
- streamError = error;
3575
- this.log.error("Error during React stream reading", error);
3714
+ this.log.error("React stream error", error);
3715
+ controller.enqueue(this.encoder.encode(this.renderErrorToString(error instanceof Error ? error : new Error(String(error)), state)));
3716
+ return false;
3576
3717
  } finally {
3577
3718
  reader.releaseLock();
3578
3719
  }
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
3720
  }
3596
3721
  /**
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
- }
3631
- /**
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.
3636
- */
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
3722
+ * Stream complete HTML document (head already closed).
3723
+ * Used by both createHtmlStream and late phase of createEarlyHtmlStream.
3650
3724
  */
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();
3725
+ async streamBodyAndClose(controller, reactStream, state, hydration) {
3726
+ const { encoder, SLOTS: slots } = this;
3727
+ controller.enqueue(slots.BODY_OPEN);
3728
+ controller.enqueue(encoder.encode(this.renderAttributes(state.head?.bodyAttributes)));
3729
+ controller.enqueue(slots.BODY_CLOSE);
3730
+ controller.enqueue(slots.ROOT_OPEN);
3731
+ await this.pipeReactStream(controller, reactStream, state);
3732
+ controller.enqueue(slots.ROOT_CLOSE);
3733
+ if (hydration) {
3734
+ controller.enqueue(slots.HYDRATION_PREFIX);
3735
+ controller.enqueue(encoder.encode(this.safeJsonSerialize(this.buildHydrationData(state))));
3736
+ controller.enqueue(slots.HYDRATION_SUFFIX);
3669
3737
  }
3738
+ controller.enqueue(slots.BODY_HTML_CLOSE);
3670
3739
  }
3671
3740
  /**
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.
3741
+ * Create HTML stream with early head optimization.
3682
3742
  *
3683
3743
  * Flow:
3684
3744
  * 1. Send DOCTYPE, <html>, <head> open, entry preloads (IMMEDIATE)
3685
- * 2. Run async work (createLayers, etc.)
3745
+ * 2. Run async work (page loaders)
3686
3746
  * 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
3747
  */
3692
3748
  createEarlyHtmlStream(globalHead, asyncWork, options = {}) {
3693
3749
  const { hydration = true, onError } = options;
3694
- const slots = this.getSlots();
3695
- const encoder = this.encoder;
3750
+ const { encoder, SLOTS: slots } = this;
3696
3751
  let headClosed = false;
3697
3752
  let bodyStarted = false;
3698
3753
  let routerState;
3699
3754
  return new ReadableStream({ start: async (controller) => {
3700
3755
  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);
3756
+ controller.enqueue(slots.DOCTYPE);
3757
+ controller.enqueue(slots.HTML_OPEN);
3758
+ controller.enqueue(encoder.encode(this.renderAttributes(globalHead?.htmlAttributes)));
3759
+ controller.enqueue(slots.HTML_CLOSE);
3760
+ controller.enqueue(slots.HEAD_OPEN);
3706
3761
  if (this.earlyHeadContent) controller.enqueue(encoder.encode(this.earlyHeadContent));
3707
3762
  const result = await asyncWork();
3708
3763
  if (!result || "redirect" in result) {
3709
3764
  if (result && "redirect" in result) {
3710
- this.log.debug("Loader redirect detected after streaming started, using meta refresh", { redirect: result.redirect });
3765
+ this.log.debug("Loader redirect, using meta refresh", { redirect: result.redirect });
3711
3766
  controller.enqueue(encoder.encode(`<meta http-equiv="refresh" content="0; url=${this.escapeHtml(result.redirect)}">\n`));
3712
3767
  }
3713
- controller.enqueue(slots.headClose);
3768
+ controller.enqueue(slots.HEAD_CLOSE);
3714
3769
  controller.enqueue(encoder.encode("<body></body></html>"));
3715
3770
  controller.close();
3716
3771
  return;
@@ -3718,15 +3773,15 @@ var ReactServerTemplateProvider = class {
3718
3773
  const { state, reactStream } = result;
3719
3774
  routerState = state;
3720
3775
  controller.enqueue(encoder.encode(this.renderHeadContent(state.head)));
3721
- controller.enqueue(slots.headClose);
3776
+ controller.enqueue(slots.HEAD_CLOSE);
3722
3777
  headClosed = true;
3723
3778
  bodyStarted = true;
3724
- await this.streamBodyContent(controller, reactStream, state, hydration);
3779
+ await this.streamBodyAndClose(controller, reactStream, state, hydration);
3725
3780
  controller.close();
3726
3781
  } catch (error) {
3727
3782
  onError?.(error);
3728
3783
  try {
3729
- this.injectErrorHtml(controller, encoder, slots, error, routerState, {
3784
+ this.injectErrorHtml(controller, error, routerState, {
3730
3785
  headClosed,
3731
3786
  bodyStarted
3732
3787
  });
@@ -3738,56 +3793,60 @@ var ReactServerTemplateProvider = class {
3738
3793
  } });
3739
3794
  }
3740
3795
  /**
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
3796
+ * Create HTML stream (non-early version, for testing/prerender).
3797
+ */
3798
+ createHtmlStream(reactStream, state, options = {}) {
3799
+ const { hydration = true, onError } = options;
3800
+ const { encoder, SLOTS: slots } = this;
3801
+ return new ReadableStream({ start: async (controller) => {
3802
+ try {
3803
+ controller.enqueue(slots.DOCTYPE);
3804
+ controller.enqueue(slots.HTML_OPEN);
3805
+ controller.enqueue(encoder.encode(this.renderAttributes(state.head?.htmlAttributes)));
3806
+ controller.enqueue(slots.HTML_CLOSE);
3807
+ controller.enqueue(slots.HEAD_OPEN);
3808
+ if (this.earlyHeadContent) controller.enqueue(encoder.encode(this.earlyHeadContent));
3809
+ controller.enqueue(encoder.encode(this.renderHeadContent(state.head)));
3810
+ controller.enqueue(slots.HEAD_CLOSE);
3811
+ await this.streamBodyAndClose(controller, reactStream, state, hydration);
3812
+ controller.close();
3813
+ } catch (error) {
3814
+ onError?.(error);
3815
+ controller.error(error);
3816
+ }
3817
+ } });
3818
+ }
3819
+ /**
3820
+ * Inject error HTML when streaming fails.
3754
3821
  */
3755
- injectErrorHtml(controller, encoder, slots, error, routerState, streamState) {
3822
+ injectErrorHtml(controller, error, routerState, streamState) {
3823
+ const { encoder, SLOTS: slots } = this;
3756
3824
  if (!streamState.headClosed) {
3757
- const headContent = this.renderHeadContent(routerState?.head);
3758
- if (headContent) controller.enqueue(encoder.encode(headContent));
3759
- controller.enqueue(slots.headClose);
3825
+ controller.enqueue(encoder.encode(this.renderHeadContent(routerState?.head)));
3826
+ controller.enqueue(slots.HEAD_CLOSE);
3760
3827
  }
3761
3828
  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);
3829
+ controller.enqueue(slots.BODY_OPEN);
3830
+ controller.enqueue(encoder.encode(this.renderAttributes(routerState?.head?.bodyAttributes)));
3831
+ controller.enqueue(slots.BODY_CLOSE);
3832
+ controller.enqueue(slots.ROOT_OPEN);
3767
3833
  }
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);
3834
+ controller.enqueue(encoder.encode(this.renderErrorToString(error instanceof Error ? error : new Error(String(error)), routerState)));
3835
+ controller.enqueue(slots.ROOT_CLOSE);
3836
+ controller.enqueue(slots.BODY_HTML_CLOSE);
3773
3837
  }
3774
3838
  /**
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
3839
+ * Render error to HTML string.
3781
3840
  */
3782
3841
  renderErrorToString(error, routerState) {
3783
3842
  this.log.error("SSR rendering error", error);
3784
3843
  let errorElement;
3785
3844
  if (routerState?.onError) try {
3786
3845
  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;
3846
+ if (result instanceof Redirection) this.log.warn("Error handler returned Redirection but headers sent", { redirect: result.redirect });
3847
+ else if (result != null) errorElement = result;
3789
3848
  } catch (handlerError) {
3790
- this.log.error("Error handler threw an exception", handlerError);
3849
+ this.log.error("Error handler threw", handlerError);
3791
3850
  }
3792
3851
  if (!errorElement) errorElement = createElement(ErrorViewer_default, {
3793
3852
  error,
@@ -3803,238 +3862,6 @@ var ReactServerTemplateProvider = class {
3803
3862
  }
3804
3863
  };
3805
3864
 
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
3865
  //#endregion
4039
3866
  //#region ../../src/react/router/providers/ReactServerProvider.ts
4040
3867
  /**
@@ -4091,38 +3918,17 @@ var ReactServerProvider = class {
4091
3918
  }
4092
3919
  }
4093
3920
  if (ssrEnabled) {
4094
- await this.registerPages(async () => this.template);
3921
+ this.registerPages();
4095
3922
  this.log.info("SSR OK");
4096
3923
  return;
4097
3924
  }
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
- });
3925
+ this.log.info("SSR is disabled");
4112
3926
  }
4113
3927
  });
4114
3928
  /**
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
3929
  * Register all pages as server routes.
4122
3930
  */
4123
- async registerPages(templateLoader) {
4124
- const template = await templateLoader();
4125
- if (template) this.templateProvider.parseTemplate(template);
3931
+ registerPages() {
4126
3932
  this.setupEarlyHeadContent();
4127
3933
  this.hasServerLinksProvider = this.alepha.has(ServerLinksProvider);
4128
3934
  for (const page of this.pageApi.getPages()) if (page.component || page.lazy) {
@@ -4132,7 +3938,7 @@ var ReactServerProvider = class {
4132
3938
  schema: void 0,
4133
3939
  method: "GET",
4134
3940
  path: page.match,
4135
- handler: this.createHandler(page, templateLoader)
3941
+ handler: this.createHandler(page)
4136
3942
  });
4137
3943
  }
4138
3944
  }
@@ -4141,13 +3947,6 @@ var ReactServerProvider = class {
4141
3947
  *
4142
3948
  * This content is sent immediately when streaming starts, before page loaders run,
4143
3949
  * 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
3950
  */
4152
3951
  setupEarlyHeadContent() {
4153
3952
  const assets = this.ssrManifestProvider.getEntryAssets();
@@ -4157,7 +3956,7 @@ var ReactServerProvider = class {
4157
3956
  for (const css of assets.css) parts.push(`<link rel="stylesheet" href="${css}" crossorigin="">`);
4158
3957
  if (assets.js) parts.push(`<script type="module" crossorigin="" src="${assets.js}"><\/script>`);
4159
3958
  }
4160
- this.templateProvider.setEarlyHeadContent(parts.length > 0 ? `${parts.join("\n")}\n` : "", globalHead, assets ?? void 0);
3959
+ this.templateProvider.setEarlyHeadContent(parts.length > 0 ? `${parts.join("\n")}\n` : "", globalHead);
4161
3960
  this.log.debug("Early head content set", {
4162
3961
  css: assets?.css.length ?? 0,
4163
3962
  js: assets?.js ? 1 : 0
@@ -4187,15 +3986,9 @@ var ReactServerProvider = class {
4187
3986
  /**
4188
3987
  * Create the request handler for a page route.
4189
3988
  */
4190
- createHandler(route, templateLoader) {
3989
+ createHandler(route) {
4191
3990
  return async (serverRequest) => {
4192
3991
  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
3992
  this.log.trace("Rendering page", { name: route.name });
4200
3993
  const state = {
4201
3994
  url,
@@ -4298,10 +4091,6 @@ var ReactServerProvider = class {
4298
4091
  };
4299
4092
  this.log.trace("Rendering", { url });
4300
4093
  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
4094
  const result = await this.renderPage(page, state);
4306
4095
  if (result.redirect) return {
4307
4096
  state,
@@ -4573,7 +4362,7 @@ var ReactBrowserProvider = class {
4573
4362
  }
4574
4363
  await this.render({ previous });
4575
4364
  }
4576
- async go(url, options = {}) {
4365
+ async push(url, options = {}) {
4577
4366
  this.log.trace(`Going to ${url}`, {
4578
4367
  url,
4579
4368
  options
@@ -4710,7 +4499,7 @@ var ReactRouter = class {
4710
4499
  */
4711
4500
  async reload() {
4712
4501
  if (!this.browser) return;
4713
- await this.go(this.location.pathname + this.location.search, {
4502
+ await this.push(this.location.pathname + this.location.search, {
4714
4503
  replace: true,
4715
4504
  force: true
4716
4505
  });
@@ -4743,12 +4532,12 @@ var ReactRouter = class {
4743
4532
  async invalidate(props) {
4744
4533
  await this.browser?.invalidate(props);
4745
4534
  }
4746
- async go(path, options) {
4535
+ async push(path, options) {
4747
4536
  for (const page of this.pages) if (page.name === path) {
4748
- await this.browser?.go(this.path(path, options), options);
4537
+ await this.browser?.push(this.path(path, options), options);
4749
4538
  return;
4750
4539
  }
4751
- await this.browser?.go(path, options);
4540
+ await this.browser?.push(path, options);
4752
4541
  }
4753
4542
  anchor(path, options = {}) {
4754
4543
  let href = path;
@@ -4761,7 +4550,7 @@ var ReactRouter = class {
4761
4550
  onClick: (ev) => {
4762
4551
  ev.stopPropagation();
4763
4552
  ev.preventDefault();
4764
- this.go(href, options).catch(console.error);
4553
+ this.push(href, options).catch(console.error);
4765
4554
  }
4766
4555
  };
4767
4556
  }
@@ -4799,7 +4588,7 @@ var ReactRouter = class {
4799
4588
  * }
4800
4589
  *
4801
4590
  * const router = useRouter<App>();
4802
- * router.go("home"); // typesafe
4591
+ * router.push("home"); // typesafe
4803
4592
  */
4804
4593
  const useRouter = () => {
4805
4594
  return useInject(ReactRouter);
@@ -4849,7 +4638,7 @@ const useActive = (args) => {
4849
4638
  if (isPending) return;
4850
4639
  setPending(true);
4851
4640
  try {
4852
- await router.go(href);
4641
+ await router.push(href);
4853
4642
  } finally {
4854
4643
  setPending(false);
4855
4644
  }
@@ -4915,6 +4704,7 @@ const AlephaReactRouter = $module({
4915
4704
  services: [
4916
4705
  ReactPageProvider,
4917
4706
  ReactPageService,
4707
+ ReactPreloadProvider,
4918
4708
  ReactRouter,
4919
4709
  ReactServerProvider,
4920
4710
  ReactServerTemplateProvider,
@@ -4924,9 +4714,9 @@ const AlephaReactRouter = $module({
4924
4714
  register: (alepha) => alepha.with(AlephaReact).with(AlephaDateTime).with(AlephaServer).with(AlephaServerCache).with(AlephaServerLinks).with({
4925
4715
  provide: ReactPageService,
4926
4716
  use: ReactPageServerService
4927
- }).with(SSRManifestProvider).with(ReactServerTemplateProvider).with(ReactServerProvider).with(ReactPageProvider).with(ReactRouter)
4717
+ }).with(SSRManifestProvider).with(ReactServerTemplateProvider).with(ReactPreloadProvider).with(ReactServerProvider).with(ReactPageProvider).with(ReactRouter)
4928
4718
  });
4929
4719
 
4930
4720
  //#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 };
4721
+ 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
4722
  //# sourceMappingURL=index.js.map