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.
- package/README.md +68 -80
- package/dist/api/audits/index.d.ts +332 -332
- package/dist/api/audits/index.d.ts.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/jobs/index.d.ts +151 -151
- package/dist/api/keys/index.d.ts +195 -195
- package/dist/api/keys/index.d.ts.map +1 -1
- package/dist/api/parameters/index.d.ts +260 -260
- package/dist/api/users/index.d.ts +22 -11
- package/dist/api/users/index.d.ts.map +1 -1
- package/dist/api/users/index.js +7 -2
- package/dist/api/users/index.js.map +1 -1
- package/dist/api/verifications/index.d.ts +128 -128
- package/dist/api/verifications/index.d.ts.map +1 -1
- 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 +191 -74
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +215 -48
- 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 +8 -0
- package/dist/email/index.d.ts.map +1 -1
- package/dist/email/index.js +7 -2
- package/dist/email/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/router/index.browser.js +9 -15
- package/dist/react/router/index.browser.js.map +1 -1
- package/dist/react/router/index.d.ts +295 -407
- package/dist/react/router/index.d.ts.map +1 -1
- package/dist/react/router/index.js +566 -776
- package/dist/react/router/index.js.map +1 -1
- package/dist/redis/index.d.ts +19 -19
- 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/core/index.d.ts +9 -9
- package/dist/server/health/index.d.ts +17 -17
- package/dist/server/links/index.d.ts +39 -39
- 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 +1 -1
- package/dist/vite/index.js +15 -7
- package/dist/vite/index.js.map +1 -1
- package/package.json +4 -2
- package/src/api/logs/TODO.md +13 -10
- package/src/cli/apps/AlephaPackageBuilderCli.ts +9 -0
- package/src/cli/atoms/buildOptions.ts +99 -9
- package/src/cli/commands/build.ts +149 -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 +1 -10
- 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/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/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 +7 -78
- package/src/react/router/providers/ReactServerTemplateProvider.spec.ts +210 -0
- package/src/react/router/providers/ReactServerTemplateProvider.ts +228 -665
- package/src/react/router/services/ReactRouter.ts +13 -13
- package/src/security/__tests__/ServerSecurityProvider.spec.ts +77 -0
- package/src/security/providers/ServerSecurityProvider.ts +30 -22
- package/src/server/core/providers/NodeHttpServerProvider.spec.ts +9 -3
- 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 +10 -7
- 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,162 +1440,452 @@ 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
|
+
});
|
|
1457
1457
|
/**
|
|
1458
|
-
*
|
|
1458
|
+
* SSR Manifest atom containing all manifest data for SSR module preloading.
|
|
1459
1459
|
*
|
|
1460
|
-
* This
|
|
1461
|
-
*
|
|
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
|
-
*
|
|
1464
|
-
*
|
|
1465
|
-
*
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
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
|
-
*
|
|
1472
|
-
*
|
|
1473
|
-
*
|
|
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
|
-
*
|
|
1476
|
-
*
|
|
1477
|
-
*
|
|
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
|
|
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();
|
|
1490
|
-
/**
|
|
1491
|
-
* Track mkdir calls for test assertions
|
|
1492
|
-
*/
|
|
1493
|
-
mkdirCalls = [];
|
|
1488
|
+
var SSRManifestProvider = class {
|
|
1489
|
+
alepha = $inject(Alepha);
|
|
1494
1490
|
/**
|
|
1495
|
-
*
|
|
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
|
-
|
|
1494
|
+
get manifest() {
|
|
1495
|
+
return this.alepha.store.get(ssrManifestAtom) ?? {};
|
|
1496
|
+
}
|
|
1498
1497
|
/**
|
|
1499
|
-
*
|
|
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
|
-
|
|
1501
|
+
get base() {
|
|
1502
|
+
return this.manifest.base ?? "";
|
|
1503
|
+
}
|
|
1502
1504
|
/**
|
|
1503
|
-
*
|
|
1505
|
+
* Get the preload manifest.
|
|
1504
1506
|
*/
|
|
1505
|
-
|
|
1507
|
+
get preloadManifest() {
|
|
1508
|
+
return this.manifest.preload;
|
|
1509
|
+
}
|
|
1506
1510
|
/**
|
|
1507
|
-
*
|
|
1511
|
+
* Get the client manifest.
|
|
1508
1512
|
*/
|
|
1509
|
-
|
|
1513
|
+
get clientManifest() {
|
|
1514
|
+
return this.manifest.client;
|
|
1515
|
+
}
|
|
1510
1516
|
/**
|
|
1511
|
-
*
|
|
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
|
-
|
|
1525
|
+
resolvePreloadKey(key) {
|
|
1526
|
+
return this.preloadManifest?.[key];
|
|
1527
|
+
}
|
|
1514
1528
|
/**
|
|
1515
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
1545
|
+
* Find manifest entry for a source path, trying different extensions.
|
|
1520
1546
|
*/
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
this.
|
|
1524
|
-
|
|
1525
|
-
|
|
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
|
-
*
|
|
1529
|
-
* Uses Node's path.join for proper normalization (handles .. and .)
|
|
1562
|
+
* Recursively collect all chunk URLs for a manifest entry.
|
|
1530
1563
|
*/
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
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
|
-
*
|
|
1579
|
+
* Collect modulepreload links for a route and its parent chain.
|
|
1537
1580
|
*/
|
|
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
|
-
};
|
|
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 (
|
|
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")
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
3295
|
-
*
|
|
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
|
|
3584
|
+
* Shared TextEncoder - reused across all requests.
|
|
3303
3585
|
*/
|
|
3304
3586
|
encoder = new TextEncoder();
|
|
3305
3587
|
/**
|
|
3306
|
-
* Pre-encoded
|
|
3307
|
-
*/
|
|
3308
|
-
|
|
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
|
-
*
|
|
3605
|
+
* Early head content (charset, viewport, entry assets).
|
|
3606
|
+
* Set once during configuration, reused for all requests.
|
|
3315
3607
|
*/
|
|
3316
|
-
|
|
3608
|
+
earlyHeadContent = "";
|
|
3317
3609
|
/**
|
|
3318
3610
|
* Root element ID for React mounting.
|
|
3319
3611
|
*/
|
|
3320
|
-
|
|
3321
|
-
return "root";
|
|
3322
|
-
}
|
|
3612
|
+
rootId = "root";
|
|
3323
3613
|
/**
|
|
3324
|
-
* Regex
|
|
3614
|
+
* Regex for extracting root div content from HTML.
|
|
3325
3615
|
*/
|
|
3326
|
-
|
|
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
|
|
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)?.[
|
|
3621
|
+
return html.match(this.rootDivRegex)?.[1];
|
|
3337
3622
|
}
|
|
3338
3623
|
/**
|
|
3339
|
-
*
|
|
3624
|
+
* Set early head content (charset, viewport, entry assets).
|
|
3625
|
+
* Called once during server configuration.
|
|
3340
3626
|
*/
|
|
3341
|
-
|
|
3342
|
-
|
|
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
|
-
*
|
|
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
|
|
3471
|
-
|
|
3644
|
+
renderHeadContent(head) {
|
|
3645
|
+
if (!head) return "";
|
|
3472
3646
|
let content = "";
|
|
3473
|
-
if (
|
|
3474
|
-
if (
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
if (head.link) for (const link of head.link)
|
|
3479
|
-
|
|
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
|
-
*
|
|
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);
|
|
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
|
-
|
|
3575
|
-
this.
|
|
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
|
-
*
|
|
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
|
-
|
|
3652
|
-
const
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
this.
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
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
|
-
*
|
|
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 (
|
|
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
|
|
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.
|
|
3702
|
-
controller.enqueue(slots.
|
|
3703
|
-
controller.enqueue(encoder.encode(this.
|
|
3704
|
-
controller.enqueue(slots.
|
|
3705
|
-
controller.enqueue(slots.
|
|
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
|
|
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.
|
|
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.
|
|
3776
|
+
controller.enqueue(slots.HEAD_CLOSE);
|
|
3722
3777
|
headClosed = true;
|
|
3723
3778
|
bodyStarted = true;
|
|
3724
|
-
await this.
|
|
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,
|
|
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
|
-
*
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
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,
|
|
3822
|
+
injectErrorHtml(controller, error, routerState, streamState) {
|
|
3823
|
+
const { encoder, SLOTS: slots } = this;
|
|
3756
3824
|
if (!streamState.headClosed) {
|
|
3757
|
-
|
|
3758
|
-
|
|
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.
|
|
3763
|
-
controller.enqueue(encoder.encode(this.
|
|
3764
|
-
controller.enqueue(slots.
|
|
3765
|
-
|
|
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
|
-
|
|
3769
|
-
controller.enqueue(
|
|
3770
|
-
controller.enqueue(slots.
|
|
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
|
|
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
|
|
3788
|
-
else if (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
|
|
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
|
-
|
|
3921
|
+
this.registerPages();
|
|
4095
3922
|
this.log.info("SSR OK");
|
|
4096
3923
|
return;
|
|
4097
3924
|
}
|
|
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
|
-
});
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
4535
|
+
async push(path, options) {
|
|
4747
4536
|
for (const page of this.pages) if (page.name === path) {
|
|
4748
|
-
await this.browser?.
|
|
4537
|
+
await this.browser?.push(this.path(path, options), options);
|
|
4749
4538
|
return;
|
|
4750
4539
|
}
|
|
4751
|
-
await this.browser?.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|