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.
- package/README.md +68 -80
- package/dist/api/audits/index.d.ts.map +1 -1
- package/dist/api/audits/index.js +8 -0
- package/dist/api/audits/index.js.map +1 -1
- package/dist/api/files/index.d.ts +170 -170
- package/dist/api/files/index.d.ts.map +1 -1
- package/dist/api/files/index.js +1 -0
- package/dist/api/files/index.js.map +1 -1
- package/dist/api/jobs/index.d.ts.map +1 -1
- package/dist/api/jobs/index.js +3 -0
- package/dist/api/jobs/index.js.map +1 -1
- package/dist/api/notifications/index.browser.js +1 -0
- package/dist/api/notifications/index.browser.js.map +1 -1
- package/dist/api/notifications/index.js +1 -0
- package/dist/api/notifications/index.js.map +1 -1
- package/dist/api/parameters/index.d.ts +260 -260
- package/dist/api/parameters/index.d.ts.map +1 -1
- package/dist/api/parameters/index.js +10 -0
- package/dist/api/parameters/index.js.map +1 -1
- package/dist/api/users/index.d.ts +12 -1
- package/dist/api/users/index.d.ts.map +1 -1
- package/dist/api/users/index.js +18 -2
- package/dist/api/users/index.js.map +1 -1
- package/dist/batch/index.d.ts +4 -4
- package/dist/bucket/index.d.ts +8 -0
- package/dist/bucket/index.d.ts.map +1 -1
- package/dist/bucket/index.js +7 -2
- package/dist/bucket/index.js.map +1 -1
- package/dist/cli/index.d.ts +196 -74
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +234 -50
- package/dist/cli/index.js.map +1 -1
- package/dist/command/index.d.ts +10 -0
- package/dist/command/index.d.ts.map +1 -1
- package/dist/command/index.js +67 -13
- package/dist/command/index.js.map +1 -1
- package/dist/core/index.browser.js +28 -21
- package/dist/core/index.browser.js.map +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +28 -21
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.native.js +28 -21
- package/dist/core/index.native.js.map +1 -1
- package/dist/email/index.d.ts +21 -13
- package/dist/email/index.d.ts.map +1 -1
- package/dist/email/index.js +10561 -4
- package/dist/email/index.js.map +1 -1
- package/dist/lock/core/index.d.ts +6 -1
- package/dist/lock/core/index.d.ts.map +1 -1
- package/dist/lock/core/index.js +9 -1
- package/dist/lock/core/index.js.map +1 -1
- package/dist/mcp/index.d.ts +5 -5
- package/dist/orm/index.bun.js +32 -16
- package/dist/orm/index.bun.js.map +1 -1
- package/dist/orm/index.d.ts +4 -1
- package/dist/orm/index.d.ts.map +1 -1
- package/dist/orm/index.js +34 -22
- package/dist/orm/index.js.map +1 -1
- package/dist/react/auth/index.browser.js +2 -1
- package/dist/react/auth/index.browser.js.map +1 -1
- package/dist/react/auth/index.js +2 -1
- package/dist/react/auth/index.js.map +1 -1
- package/dist/react/core/index.d.ts +3 -3
- package/dist/react/router/index.browser.js +9 -15
- package/dist/react/router/index.browser.js.map +1 -1
- package/dist/react/router/index.d.ts +305 -407
- package/dist/react/router/index.d.ts.map +1 -1
- package/dist/react/router/index.js +581 -781
- package/dist/react/router/index.js.map +1 -1
- package/dist/scheduler/index.d.ts +13 -1
- package/dist/scheduler/index.d.ts.map +1 -1
- package/dist/scheduler/index.js +42 -4
- package/dist/scheduler/index.js.map +1 -1
- package/dist/security/index.d.ts +42 -42
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/index.js +8 -7
- package/dist/security/index.js.map +1 -1
- package/dist/server/auth/index.d.ts +167 -167
- package/dist/server/compress/index.d.ts.map +1 -1
- package/dist/server/compress/index.js +1 -0
- package/dist/server/compress/index.js.map +1 -1
- package/dist/server/health/index.d.ts +17 -17
- package/dist/server/links/index.d.ts +39 -39
- package/dist/server/links/index.js +1 -1
- package/dist/server/links/index.js.map +1 -1
- package/dist/server/static/index.js +7 -2
- package/dist/server/static/index.js.map +1 -1
- package/dist/server/swagger/index.d.ts +8 -0
- package/dist/server/swagger/index.d.ts.map +1 -1
- package/dist/server/swagger/index.js +7 -2
- package/dist/server/swagger/index.js.map +1 -1
- package/dist/sms/index.d.ts +8 -0
- package/dist/sms/index.d.ts.map +1 -1
- package/dist/sms/index.js +7 -2
- package/dist/sms/index.js.map +1 -1
- package/dist/system/index.browser.js +734 -12
- package/dist/system/index.browser.js.map +1 -1
- package/dist/system/index.d.ts +8 -0
- package/dist/system/index.d.ts.map +1 -1
- package/dist/system/index.js +7 -2
- package/dist/system/index.js.map +1 -1
- package/dist/vite/index.d.ts +3 -2
- package/dist/vite/index.d.ts.map +1 -1
- package/dist/vite/index.js +42 -8
- package/dist/vite/index.js.map +1 -1
- package/dist/websocket/index.d.ts +34 -34
- package/dist/websocket/index.d.ts.map +1 -1
- package/package.json +9 -4
- package/src/api/audits/controllers/AdminAuditController.ts +8 -0
- package/src/api/files/controllers/AdminFileStatsController.ts +1 -0
- package/src/api/jobs/controllers/AdminJobController.ts +3 -0
- package/src/api/logs/TODO.md +13 -10
- package/src/api/notifications/controllers/AdminNotificationController.ts +1 -0
- package/src/api/parameters/controllers/AdminConfigController.ts +10 -0
- package/src/api/users/controllers/AdminIdentityController.ts +3 -0
- package/src/api/users/controllers/AdminSessionController.ts +3 -0
- package/src/api/users/controllers/AdminUserController.ts +5 -0
- package/src/cli/apps/AlephaPackageBuilderCli.ts +9 -0
- package/src/cli/atoms/buildOptions.ts +99 -9
- package/src/cli/commands/build.ts +150 -32
- package/src/cli/commands/db.ts +5 -7
- package/src/cli/commands/init.spec.ts +50 -6
- package/src/cli/commands/init.ts +28 -5
- package/src/cli/providers/ViteDevServerProvider.ts +31 -9
- package/src/cli/services/AlephaCliUtils.ts +16 -0
- package/src/cli/services/PackageManagerUtils.ts +2 -0
- package/src/cli/services/ProjectScaffolder.spec.ts +97 -0
- package/src/cli/services/ProjectScaffolder.ts +28 -6
- package/src/cli/templates/agentMd.ts +6 -1
- package/src/cli/templates/apiAppSecurityTs.ts +11 -0
- package/src/cli/templates/apiIndexTs.ts +18 -4
- package/src/cli/templates/webAppRouterTs.ts +25 -1
- package/src/cli/templates/webHelloComponentTsx.ts +15 -5
- package/src/command/helpers/Runner.spec.ts +135 -0
- package/src/command/helpers/Runner.ts +4 -1
- package/src/command/providers/CliProvider.spec.ts +325 -0
- package/src/command/providers/CliProvider.ts +117 -7
- package/src/core/Alepha.ts +32 -25
- package/src/email/index.workerd.ts +36 -0
- package/src/email/providers/WorkermailerEmailProvider.ts +221 -0
- package/src/lock/core/primitives/$lock.ts +13 -1
- package/src/orm/index.bun.ts +1 -1
- package/src/orm/index.ts +2 -6
- package/src/orm/providers/drivers/BunSqliteProvider.ts +4 -1
- package/src/orm/providers/drivers/CloudflareD1Provider.ts +57 -30
- package/src/orm/providers/drivers/DatabaseProvider.ts +9 -1
- package/src/orm/providers/drivers/NodeSqliteProvider.ts +4 -1
- package/src/react/auth/services/ReactAuth.ts +3 -1
- package/src/react/router/atoms/ssrManifestAtom.ts +7 -0
- package/src/react/router/hooks/useActive.ts +1 -1
- package/src/react/router/hooks/useRouter.ts +1 -1
- package/src/react/router/index.ts +4 -0
- package/src/react/router/primitives/$page.browser.spec.tsx +24 -24
- package/src/react/router/primitives/$page.spec.tsx +0 -32
- package/src/react/router/primitives/$page.ts +6 -14
- package/src/react/router/providers/ReactBrowserProvider.ts +6 -3
- package/src/react/router/providers/ReactPageProvider.ts +1 -1
- package/src/react/router/providers/ReactPreloadProvider.spec.ts +142 -0
- package/src/react/router/providers/ReactPreloadProvider.ts +85 -0
- package/src/react/router/providers/ReactServerProvider.ts +21 -82
- package/src/react/router/providers/ReactServerTemplateProvider.spec.ts +210 -0
- package/src/react/router/providers/ReactServerTemplateProvider.ts +228 -665
- package/src/react/router/providers/SSRManifestProvider.ts +7 -0
- package/src/react/router/services/ReactRouter.ts +13 -13
- package/src/scheduler/index.workerd.ts +43 -0
- package/src/scheduler/providers/CronProvider.ts +53 -6
- package/src/scheduler/providers/WorkerdCronProvider.ts +102 -0
- package/src/security/__tests__/ServerSecurityProvider.spec.ts +77 -0
- package/src/security/providers/ServerSecurityProvider.ts +30 -22
- package/src/server/compress/providers/ServerCompressProvider.ts +6 -0
- package/src/server/core/providers/NodeHttpServerProvider.spec.ts +9 -3
- package/src/server/links/providers/ServerLinksProvider.spec.ts +332 -0
- package/src/server/links/providers/ServerLinksProvider.ts +1 -1
- package/src/system/index.browser.ts +25 -0
- package/src/system/index.workerd.ts +1 -0
- package/src/system/providers/FileSystemProvider.ts +8 -0
- package/src/system/providers/NodeFileSystemProvider.ts +11 -2
- package/src/vite/tasks/buildServer.ts +2 -12
- package/src/vite/tasks/generateCloudflare.ts +47 -8
- 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
|
|
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/
|
|
1443
|
+
//#region ../../src/react/router/atoms/ssrManifestAtom.ts
|
|
1450
1444
|
/**
|
|
1451
|
-
*
|
|
1445
|
+
* Schema for the SSR manifest atom.
|
|
1452
1446
|
*/
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
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
|
-
*
|
|
1459
|
+
* SSR Manifest atom containing all manifest data for SSR module preloading.
|
|
1459
1460
|
*
|
|
1460
|
-
* This
|
|
1461
|
-
*
|
|
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
|
-
*
|
|
1464
|
-
*
|
|
1465
|
-
*
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
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
|
-
*
|
|
1472
|
-
*
|
|
1473
|
-
*
|
|
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
|
-
*
|
|
1476
|
-
*
|
|
1477
|
-
*
|
|
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
|
|
1481
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
1495
|
+
get manifest() {
|
|
1496
|
+
return this.alepha.store.get(ssrManifestAtom) ?? {};
|
|
1497
|
+
}
|
|
1494
1498
|
/**
|
|
1495
|
-
*
|
|
1499
|
+
* Get the full manifest object.
|
|
1496
1500
|
*/
|
|
1497
|
-
|
|
1501
|
+
getManifest() {
|
|
1502
|
+
return this.manifest;
|
|
1503
|
+
}
|
|
1498
1504
|
/**
|
|
1499
|
-
*
|
|
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
|
-
|
|
1508
|
+
get base() {
|
|
1509
|
+
return this.manifest.base ?? "";
|
|
1510
|
+
}
|
|
1502
1511
|
/**
|
|
1503
|
-
*
|
|
1512
|
+
* Get the preload manifest.
|
|
1504
1513
|
*/
|
|
1505
|
-
|
|
1514
|
+
get preloadManifest() {
|
|
1515
|
+
return this.manifest.preload;
|
|
1516
|
+
}
|
|
1506
1517
|
/**
|
|
1507
|
-
*
|
|
1518
|
+
* Get the client manifest.
|
|
1508
1519
|
*/
|
|
1509
|
-
|
|
1520
|
+
get clientManifest() {
|
|
1521
|
+
return this.manifest.client;
|
|
1522
|
+
}
|
|
1510
1523
|
/**
|
|
1511
|
-
*
|
|
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
|
-
|
|
1532
|
+
resolvePreloadKey(key) {
|
|
1533
|
+
return this.preloadManifest?.[key];
|
|
1534
|
+
}
|
|
1514
1535
|
/**
|
|
1515
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
1552
|
+
* Find manifest entry for a source path, trying different extensions.
|
|
1520
1553
|
*/
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
this.
|
|
1524
|
-
|
|
1525
|
-
|
|
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
|
-
*
|
|
1529
|
-
* Uses Node's path.join for proper normalization (handles .. and .)
|
|
1569
|
+
* Recursively collect all chunk URLs for a manifest entry.
|
|
1530
1570
|
*/
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
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
|
-
*
|
|
1586
|
+
* Collect modulepreload links for a route and its parent chain.
|
|
1537
1587
|
*/
|
|
1538
|
-
|
|
1539
|
-
if (
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
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 (
|
|
1556
|
-
|
|
1557
|
-
return {
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
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
|
-
|
|
1573
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
3295
|
-
*
|
|
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
|
|
3591
|
+
* Shared TextEncoder - reused across all requests.
|
|
3303
3592
|
*/
|
|
3304
3593
|
encoder = new TextEncoder();
|
|
3305
3594
|
/**
|
|
3306
|
-
* Pre-encoded
|
|
3307
|
-
*/
|
|
3308
|
-
|
|
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
|
-
*
|
|
3612
|
+
* Early head content (charset, viewport, entry assets).
|
|
3613
|
+
* Set once during configuration, reused for all requests.
|
|
3315
3614
|
*/
|
|
3316
|
-
|
|
3615
|
+
earlyHeadContent = "";
|
|
3317
3616
|
/**
|
|
3318
3617
|
* Root element ID for React mounting.
|
|
3319
3618
|
*/
|
|
3320
|
-
|
|
3321
|
-
return "root";
|
|
3322
|
-
}
|
|
3619
|
+
rootId = "root";
|
|
3323
3620
|
/**
|
|
3324
|
-
* Regex
|
|
3621
|
+
* Regex for extracting root div content from HTML.
|
|
3325
3622
|
*/
|
|
3326
|
-
|
|
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
|
|
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)?.[
|
|
3628
|
+
return html.match(this.rootDivRegex)?.[1];
|
|
3337
3629
|
}
|
|
3338
3630
|
/**
|
|
3339
|
-
*
|
|
3631
|
+
* Set early head content (charset, viewport, entry assets).
|
|
3632
|
+
* Called once during server configuration.
|
|
3340
3633
|
*/
|
|
3341
|
-
|
|
3342
|
-
|
|
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
|
-
*
|
|
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
|
|
3471
|
-
|
|
3651
|
+
renderHeadContent(head) {
|
|
3652
|
+
if (!head) return "";
|
|
3472
3653
|
let content = "";
|
|
3473
|
-
if (
|
|
3474
|
-
if (
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
if (head.link) for (const link of head.link)
|
|
3479
|
-
|
|
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
|
-
*
|
|
3552
|
-
*
|
|
3553
|
-
|
|
3554
|
-
|
|
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
|
-
|
|
3575
|
-
this.
|
|
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
|
-
*
|
|
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
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
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
|
-
*
|
|
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 (
|
|
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
|
|
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.
|
|
3702
|
-
controller.enqueue(slots.
|
|
3703
|
-
controller.enqueue(encoder.encode(this.
|
|
3704
|
-
controller.enqueue(slots.
|
|
3705
|
-
controller.enqueue(slots.
|
|
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
|
|
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.
|
|
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.
|
|
3783
|
+
controller.enqueue(slots.HEAD_CLOSE);
|
|
3722
3784
|
headClosed = true;
|
|
3723
3785
|
bodyStarted = true;
|
|
3724
|
-
await this.
|
|
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,
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
3758
|
-
|
|
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.
|
|
3763
|
-
controller.enqueue(encoder.encode(this.
|
|
3764
|
-
controller.enqueue(slots.
|
|
3765
|
-
|
|
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
|
-
|
|
3769
|
-
controller.enqueue(
|
|
3770
|
-
controller.enqueue(slots.
|
|
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
|
|
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
|
|
3788
|
-
else if (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
|
|
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
|
-
|
|
3928
|
+
this.registerPages();
|
|
4095
3929
|
this.log.info("SSR OK");
|
|
4096
3930
|
return;
|
|
4097
3931
|
}
|
|
4098
|
-
this.log.info("SSR is disabled
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
4545
|
+
async push(path, options) {
|
|
4747
4546
|
for (const page of this.pages) if (page.name === path) {
|
|
4748
|
-
await this.browser?.
|
|
4547
|
+
await this.browser?.push(this.path(path, options), options);
|
|
4749
4548
|
return;
|
|
4750
4549
|
}
|
|
4751
|
-
await this.browser?.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|