@ilha/router 0.9.2 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -27
- package/dist/head.d.ts +67 -0
- package/dist/http.d.ts +28 -0
- package/dist/index.d.ts +43 -97
- package/dist/index.js +2 -1
- package/dist/{plugin-BHuojFhQ.js → plugin-CmI3Brr2.js} +102 -115
- package/dist/plugin.d.ts +1 -1
- package/dist/route-match.d.ts +1 -1
- package/dist/{rspack.d.ts → rsbuild.d.ts} +2 -2
- package/dist/rsbuild.js +10 -0
- package/dist/server-island.d.ts +17 -2
- package/dist/server-island.js +13 -25
- package/dist/server-islands.d.ts +16 -2
- package/dist/snapshot-CsEaY6h_.js +337 -0
- package/dist/snapshot.d.ts +1 -0
- package/dist/{src-BBsbD5vU.js → src-B5dHU24f.js} +157 -361
- package/dist/ssr-BxrcUYy5.js +498 -0
- package/dist/ssr.d.ts +173 -0
- package/dist/ssr.js +2 -200
- package/dist/vite.js +1 -1
- package/package.json +7 -14
- package/dist/public-types.d.ts +0 -7
- package/dist/request-scope-C4reU4v0.js +0 -34
- package/dist/rolldown.d.ts +0 -6
- package/dist/rolldown.js +0 -10
- package/dist/rspack.js +0 -10
- package/dist/server-island-registry.d.ts +0 -122
- package/dist/server-island-registry.js +0 -189
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { FrameError, getFrameAuth, getFrameGuard, isTrustedOrigin, renderServerIsland, setFrameAuth, setFrameGuard, setLoaderGuard } from "./server-island-registry.js";
|
|
1
|
+
import { C as setLoaderGuard, T as runWithIslandRequest, b as setFrameAuth, c as frameEnvelope, m as isSafeFramePath, n as FrameError, o as authorizeFrameRequest, u as getFrameGuard, x as setFrameGuard, y as renderServerIsland } from "./ssr-BxrcUYy5.js";
|
|
3
2
|
import { existsSync, readFileSync, statSync, watch } from "node:fs";
|
|
4
3
|
import { basename, dirname, extname, join, relative, resolve, sep } from "node:path";
|
|
5
4
|
import { createUnplugin } from "unplugin";
|
|
@@ -7,10 +6,24 @@ import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
|
7
6
|
import { createHash } from "node:crypto";
|
|
8
7
|
|
|
9
8
|
//#region src/server-islands.ts
|
|
9
|
+
/**
|
|
10
|
+
* Replace identity `action(` wrappers of exported server actions with the
|
|
11
|
+
* capture-aware shim on ALREADY-COMPILED module code. Must run inside the
|
|
12
|
+
* SSR transform so upstream JSX/TS output is preserved.
|
|
13
|
+
*/
|
|
14
|
+
function rewriteServerActions(code, rpcActions) {
|
|
15
|
+
const names = Object.keys(rpcActions);
|
|
16
|
+
if (names.length === 0) return code;
|
|
17
|
+
const re = new RegExp(`(export\\s+(?:const|let|var)\\s+(${names.join("|")})\\b\\s*=\\s*)action\\s*\\(`, "g");
|
|
18
|
+
return code.replace(re, (_match, head, name) => {
|
|
19
|
+
const key = rpcActions[name];
|
|
20
|
+
return `${head}__ilhaServerAction(${JSON.stringify(key)}, `;
|
|
21
|
+
});
|
|
22
|
+
}
|
|
10
23
|
const EXPORT_RE = /(?:^|\n)\s*export\s+(?:declare\s+)?(?:async\s+)?(?:function\s*\*?|const|let|var|class)\s+([A-Za-z_$][\w$]*)/g;
|
|
11
24
|
const ISLAND_EXPORT_RE = /(?:^|\n)\s*export\s+(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*ilha\b/g;
|
|
12
25
|
const DEFAULT_ISLAND_RE = /export\s+default\s+ilha\b/;
|
|
13
|
-
const
|
|
26
|
+
const AS_RES = [/\{\s*as:\s*["'`]([a-z][a-z0-9-]*)["'`]\s*[,}]?/];
|
|
14
27
|
function clientRefPublicId(spec, imported) {
|
|
15
28
|
return createHash("sha256").update(`${spec}#${imported}`).digest("base64url");
|
|
16
29
|
}
|
|
@@ -69,35 +82,12 @@ function extractCallArgs(source, openParen, limit = 4e3) {
|
|
|
69
82
|
console.warn("[ilha-router] scanServerIslands: argument list exceeded the scan limit — call skipped.");
|
|
70
83
|
return null;
|
|
71
84
|
}
|
|
72
|
-
/** First callback body inside an args list: everything after the first top-level
|
|
73
|
-
* comma. Used to scan which module exports a stream/action closure references. */
|
|
74
|
-
function callbackBody(args) {
|
|
75
|
-
let depth = 0;
|
|
76
|
-
let quote = null;
|
|
77
|
-
for (let i = 0; i < args.length; i++) {
|
|
78
|
-
const ch = args[i];
|
|
79
|
-
if (quote !== null) {
|
|
80
|
-
if (ch === "\\") i++;
|
|
81
|
-
else if (ch === quote) quote = null;
|
|
82
|
-
continue;
|
|
83
|
-
}
|
|
84
|
-
if (ch === "\"" || ch === "'" || ch === "`") {
|
|
85
|
-
quote = ch;
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
if (ch === "(" || ch === "{" || ch === "[") depth++;
|
|
89
|
-
else if (ch === ")" || ch === "}" || ch === "]") depth--;
|
|
90
|
-
else if (ch === "," && depth === 0) return args.slice(i + 1);
|
|
91
|
-
}
|
|
92
|
-
return "";
|
|
93
|
-
}
|
|
94
|
-
/** Identifiers in `body` that are members of `candidates`, excluding keywords. */
|
|
95
85
|
function referencedExports(body, candidates) {
|
|
96
86
|
for (const match of body.matchAll(/([A-Za-z_$][\w$]*)\s*\(/g)) if (candidates.has(match[1])) return match[1];
|
|
97
87
|
}
|
|
98
88
|
/** Scan a `*.server.ts(x)` module source for island exports and their
|
|
99
89
|
* declarative wiring. Convention: islands start with `ilha` — both builder
|
|
100
|
-
*
|
|
90
|
+
* function components (`ilha(() => …)`) and `ilha(schema, component)`. */
|
|
101
91
|
function scanServerIslands(source) {
|
|
102
92
|
const exports = [];
|
|
103
93
|
for (const match of source.matchAll(EXPORT_RE)) exports.push(match[1]);
|
|
@@ -107,20 +97,36 @@ function scanServerIslands(source) {
|
|
|
107
97
|
if (name && /^[A-Za-z_$][\w$]*$/.test(name) && !exports.includes(name)) exports.push(name);
|
|
108
98
|
}
|
|
109
99
|
const candidates = new Set(exports);
|
|
100
|
+
const rpcActions = {};
|
|
101
|
+
for (const match of source.matchAll(/export\s+(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*action\s*\(/g)) rpcActions[match[1]] = `x:${match[1]}`;
|
|
110
102
|
const islands = [];
|
|
111
103
|
const collect = (name, start, sliceEnd) => {
|
|
112
104
|
const slice = source.slice(start, sliceEnd);
|
|
113
|
-
|
|
105
|
+
let as = "div";
|
|
106
|
+
for (const re of AS_RES) {
|
|
107
|
+
const hit = slice.match(re)?.[1];
|
|
108
|
+
if (hit) {
|
|
109
|
+
as = hit;
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
114
113
|
const streams = {};
|
|
115
114
|
const actions = {};
|
|
116
|
-
|
|
117
|
-
|
|
115
|
+
let actionOrder = 0;
|
|
116
|
+
let streamOrder = 0;
|
|
117
|
+
for (const kind of ["action", "derived"]) {
|
|
118
|
+
const re = new RegExp(kind === "action" ? "[A-Za-z0-9_$]*[Aa]ction\\s*\\(" : "\\bderived\\s*\\(", "g");
|
|
118
119
|
for (const match of slice.matchAll(re)) {
|
|
119
|
-
const
|
|
120
|
-
const args = extractCallArgs(slice, (match.index ?? 0) + match[0].indexOf("("));
|
|
120
|
+
const args = extractCallArgs(slice, match.index + match[0].indexOf("("));
|
|
121
121
|
if (!args) continue;
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
if (kind === "derived") {
|
|
123
|
+
if (!/(async\s+function\s*\*|yield\b|for\s+await)/.test(args)) continue;
|
|
124
|
+
const target = referencedExports(args, candidates);
|
|
125
|
+
if (target) streams[`d${streamOrder++}`] = target;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const target = referencedExports(args, candidates);
|
|
129
|
+
actions[`a${actionOrder++}`] = target ?? "";
|
|
124
130
|
}
|
|
125
131
|
}
|
|
126
132
|
islands.push({
|
|
@@ -145,6 +151,7 @@ function scanServerIslands(source) {
|
|
|
145
151
|
return {
|
|
146
152
|
islands,
|
|
147
153
|
exports,
|
|
154
|
+
rpcActions,
|
|
148
155
|
clientRefs: scanClientRefs(source),
|
|
149
156
|
clientLoader: /(^|\n)\s*export\s+(?:const|let|var)\s+load\b\s*=\s*loader\.client\b/.test(source)
|
|
150
157
|
};
|
|
@@ -174,7 +181,7 @@ function generateServerIslandModule(spec, scan) {
|
|
|
174
181
|
const moduleKey = basename(spec).replace(/\.server\.(?:[jt]sx?)$/i, "");
|
|
175
182
|
const lines = [
|
|
176
183
|
`import { client as $$rpc } from "virtual:oxide/client";`,
|
|
177
|
-
`import { __ilhaServerIsland } from "@ilha/router/server-island";`,
|
|
184
|
+
`import { __ilhaApplyHead, __ilhaServerIsland } from "@ilha/router/server-island";`,
|
|
178
185
|
`const $$call = (method, args) => { const opts = args.at(-1); return opts && typeof opts === "object" && opts.signal instanceof AbortSignal && Object.keys(opts).length === 1 ? $$rpc[${JSON.stringify(moduleKey)}][method](args.slice(0, -1), opts) : $$rpc[${JSON.stringify(moduleKey)}][method](args); };`,
|
|
179
186
|
...scan.clientRefs.map((ref, index) => ref.imported === "default" ? `import $$child${index} from ${JSON.stringify(ref.spec)};` : `import { ${ref.imported} as $$child${index} } from ${JSON.stringify(ref.spec)};`)
|
|
180
187
|
];
|
|
@@ -182,12 +189,16 @@ function generateServerIslandModule(spec, scan) {
|
|
|
182
189
|
for (const island of scan.islands) {
|
|
183
190
|
const wiring = [];
|
|
184
191
|
const streams = Object.entries(island.streams).map(([key, target]) => `${JSON.stringify(key)}: (signal) => $$call(${JSON.stringify(target)}, [{ signal }])`);
|
|
185
|
-
const
|
|
192
|
+
const rpcEntries = Object.entries(scan.rpcActions).map(([name, key]) => [key, name]);
|
|
193
|
+
const actions = Object.entries({
|
|
194
|
+
...island.actions,
|
|
195
|
+
...Object.fromEntries(rpcEntries)
|
|
196
|
+
}).map(([key, target]) => `${JSON.stringify(key)}: (...args) => $$call(${JSON.stringify(target)}, args)`);
|
|
186
197
|
if (streams.length) wiring.push(`streams: { ${streams.join(", ")} }`);
|
|
187
198
|
if (actions.length) wiring.push(`actions: { ${actions.join(", ")} }`);
|
|
188
199
|
if (scan.clientRefs.length) wiring.push(`children: { ${scan.clientRefs.map((ref, index) => `${JSON.stringify(ref.id)}: $$child${index}`).join(", ")} }`);
|
|
189
200
|
const id = serverIslandPublicId(spec, island.name);
|
|
190
|
-
wiring.push(`frame: () => fetch("/__ilha/frame", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ id: ${JSON.stringify(id)}, path: location.pathname + location.search }) }).then((r) => { if (!r.ok) throw new Error("frame failed"); return r.json(); }).then((j) => { if (j.redirect) { location.assign(j.redirect); throw new Error("frame redirected"); } return j.html; })`);
|
|
201
|
+
wiring.push(`frame: () => fetch("/__ilha/frame", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ id: ${JSON.stringify(id)}, path: location.pathname + location.search }) }).then((r) => { if (!r.ok) throw new Error("frame failed"); return r.json(); }).then((j) => { if (j.redirect) { location.assign(j.redirect); throw new Error("frame redirected"); } __ilhaApplyHead(j.head); return j.html; })`);
|
|
191
202
|
if (scan.clientLoader) wiring.push(`clientLoader: () => $$call("load", [])`);
|
|
192
203
|
const call = `__ilhaServerIsland(${JSON.stringify(id)}, ${JSON.stringify(island.as)}, { ${wiring.join(", ")} })`;
|
|
193
204
|
if (island.name === "default") lines.push(`export default ${call};`);
|
|
@@ -472,11 +483,11 @@ function buildServerFile(entries, serverFile) {
|
|
|
472
483
|
`// Import via: import { pageRouter, registry } from "ilha:pages/server";`,
|
|
473
484
|
``,
|
|
474
485
|
...imports,
|
|
475
|
-
...entries.some((e) => e.hasLoader || e.loaderLayouts.length > 0) ? [`import { setFrameLoaderRunner } from "@ilha/router/
|
|
486
|
+
...entries.some((e) => e.hasLoader || e.loaderLayouts.length > 0) ? [`import { setFrameLoaderRunner } from "@ilha/router/ssr";`, `setFrameLoaderRunner((path, request) => pageRouter.runLoader(path, request));`] : [],
|
|
476
487
|
``,
|
|
477
488
|
...wrappedIslandLines,
|
|
478
489
|
``,
|
|
479
|
-
`export const registry: Record<string, Island<any
|
|
490
|
+
`export const registry: Record<string, Island<any>> = {`,
|
|
480
491
|
...registryLines,
|
|
481
492
|
`};`,
|
|
482
493
|
``,
|
|
@@ -556,7 +567,7 @@ function buildClientFile(entries, clientFile, opts) {
|
|
|
556
567
|
``,
|
|
557
568
|
...wrappedIslandLines,
|
|
558
569
|
``,
|
|
559
|
-
`export const registry: Record<string, Island<any
|
|
570
|
+
`export const registry: Record<string, Island<any>> = {`,
|
|
560
571
|
...registryLines,
|
|
561
572
|
`};`,
|
|
562
573
|
``
|
|
@@ -626,14 +637,14 @@ async function generateTypes(outDir) {
|
|
|
626
637
|
` import type { RouterBuilder } from "@ilha/router";`,
|
|
627
638
|
` import type { Island } from "ilha";`,
|
|
628
639
|
` export const pageRouter: RouterBuilder;`,
|
|
629
|
-
` export const registry: Record<string, Island<any
|
|
640
|
+
` export const registry: Record<string, Island<any>>;`,
|
|
630
641
|
`}`,
|
|
631
642
|
``,
|
|
632
643
|
`declare module "ilha:pages/client" {`,
|
|
633
644
|
` import type { RouterBuilder } from "@ilha/router";`,
|
|
634
645
|
` import type { Island } from "ilha";`,
|
|
635
646
|
` export const pageRouter: RouterBuilder;`,
|
|
636
|
-
` export const registry: Record<string, Island<any
|
|
647
|
+
` export const registry: Record<string, Island<any>>;`,
|
|
637
648
|
`}`,
|
|
638
649
|
``,
|
|
639
650
|
`declare module "ilha:loaders" {`,
|
|
@@ -722,8 +733,8 @@ function detectIlhaConsumers(root) {
|
|
|
722
733
|
const appPkg = readJson(join(root, "package.json"));
|
|
723
734
|
if (!appPkg) return [];
|
|
724
735
|
const deps = {
|
|
725
|
-
...appPkg.dependencies
|
|
726
|
-
...appPkg.devDependencies
|
|
736
|
+
...appPkg.dependencies,
|
|
737
|
+
...appPkg.devDependencies
|
|
727
738
|
};
|
|
728
739
|
const found = [];
|
|
729
740
|
for (const name of Object.keys(deps)) {
|
|
@@ -902,7 +913,6 @@ const pagesFactory = (options = {}) => {
|
|
|
902
913
|
config(userConfig) {
|
|
903
914
|
const singletonPeers = [
|
|
904
915
|
"ilha",
|
|
905
|
-
"@ilha/store",
|
|
906
916
|
"@ilha/router",
|
|
907
917
|
"alien-signals",
|
|
908
918
|
...detectIlhaConsumers(userConfig.root ? resolve(userConfig.root) : process.cwd())
|
|
@@ -919,7 +929,6 @@ const pagesFactory = (options = {}) => {
|
|
|
919
929
|
"ilha",
|
|
920
930
|
"ilha/jsx-runtime",
|
|
921
931
|
"ilha/jsx-dev-runtime",
|
|
922
|
-
"@ilha/store",
|
|
923
932
|
"alien-signals"
|
|
924
933
|
])]
|
|
925
934
|
}
|
|
@@ -937,16 +946,19 @@ const pagesFactory = (options = {}) => {
|
|
|
937
946
|
const lines = [];
|
|
938
947
|
for (const ref of scan?.clientRefs ?? []) lines.push(`if (${ref.local}?.[Symbol.for("ilha.island")]) ${ref.local}[Symbol.for("ilha.clientRef")] = ${JSON.stringify(ref.id)};`);
|
|
939
948
|
if (scan && scan.islands.length > 0 && !code.startsWith("// oxidejs:client-stub")) {
|
|
949
|
+
if (Object.keys(scan.rpcActions).length > 0) lines.unshift(`import { __ilhaServerAction } from "@ilha/router/ssr";`);
|
|
940
950
|
lines.unshift(`import * as __ilhaSelf from ${JSON.stringify(file)};`);
|
|
941
|
-
lines.unshift(`import { registerServerIsland } from "@ilha/router/
|
|
951
|
+
lines.unshift(`import { registerServerIsland } from "@ilha/router/ssr";`);
|
|
942
952
|
for (const island of scan.islands) {
|
|
943
953
|
const id2 = serverIslandPublicId(file, island.name);
|
|
944
954
|
const isServerPage = SERVER_PAGE_RE.test(file) && state.isUnderPagesDir(file) && scan.exports.includes("load") ? `, { load: __ilhaSelf.load, pattern: ${JSON.stringify(fileToPattern(state.pagesDir, file))} }` : "";
|
|
945
955
|
lines.push(`registerServerIsland(${JSON.stringify(id2)}, () => __ilhaSelf[${JSON.stringify(island.name)}]?.[Symbol.for("ilha.renderState")]${isServerPage});`);
|
|
946
956
|
}
|
|
947
957
|
}
|
|
948
|
-
|
|
949
|
-
|
|
958
|
+
const base = rewriteServerActions(code, scan?.rpcActions ?? {});
|
|
959
|
+
if (base === code && lines.length === 0) return null;
|
|
960
|
+
if (lines.length === 0) return base;
|
|
961
|
+
return `${base}\n${lines.join("\n")}`;
|
|
950
962
|
}
|
|
951
963
|
if (id.startsWith("\0") || id.includes("node_modules")) return null;
|
|
952
964
|
if (serverFile) return null;
|
|
@@ -990,7 +1002,7 @@ const pagesFactory = (options = {}) => {
|
|
|
990
1002
|
server.watcher.add(state.pagesDir);
|
|
991
1003
|
if (options.frameGuard) setFrameGuard(options.frameGuard);
|
|
992
1004
|
if (options.loaderGuard) setLoaderGuard(options.loaderGuard);
|
|
993
|
-
|
|
1005
|
+
setFrameAuth({
|
|
994
1006
|
trustedOrigins: options.trustedOrigins,
|
|
995
1007
|
csrf: options.csrf,
|
|
996
1008
|
defaultAction: "open"
|
|
@@ -1007,48 +1019,17 @@ const pagesFactory = (options = {}) => {
|
|
|
1007
1019
|
res.end();
|
|
1008
1020
|
return;
|
|
1009
1021
|
}
|
|
1010
|
-
const
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
const v = req.headers[name];
|
|
1022
|
-
if (typeof v === "string") identityHeaders.set(name, v);
|
|
1023
|
-
}
|
|
1024
|
-
try {
|
|
1025
|
-
const denied = await getFrameGuard()?.(new Request(`http://${req.headers.host ?? "localhost"}${req.url ?? "/"}`, {
|
|
1026
|
-
method: req.method,
|
|
1027
|
-
headers: identityHeaders
|
|
1028
|
-
}));
|
|
1029
|
-
if (denied) {
|
|
1030
|
-
res.statusCode = denied.status;
|
|
1031
|
-
res.setHeader("cache-control", "no-store");
|
|
1032
|
-
res.end();
|
|
1033
|
-
return;
|
|
1034
|
-
}
|
|
1035
|
-
} catch {
|
|
1036
|
-
res.statusCode = 403;
|
|
1037
|
-
res.end();
|
|
1038
|
-
return;
|
|
1039
|
-
}
|
|
1040
|
-
const csrf = getFrameAuth()?.csrf;
|
|
1041
|
-
if (csrf) try {
|
|
1042
|
-
if (!await csrf(new Request(`http://${req.headers.host ?? "localhost"}${req.url ?? "/"}`, {
|
|
1043
|
-
method: req.method,
|
|
1044
|
-
headers: identityHeaders
|
|
1045
|
-
}))) {
|
|
1046
|
-
res.statusCode = 403;
|
|
1047
|
-
res.end();
|
|
1048
|
-
return;
|
|
1049
|
-
}
|
|
1050
|
-
} catch {
|
|
1051
|
-
res.statusCode = 403;
|
|
1022
|
+
const frameUrl = `http://${req.headers.host ?? "localhost"}${req.url ?? "/"}`;
|
|
1023
|
+
const authorized = await authorizeFrameRequest(new Request(frameUrl, {
|
|
1024
|
+
method: req.method,
|
|
1025
|
+
headers: new Headers(req.headers)
|
|
1026
|
+
}), {
|
|
1027
|
+
defaultAction: "open",
|
|
1028
|
+
onGuardError: () => {}
|
|
1029
|
+
});
|
|
1030
|
+
if (!authorized.ok) {
|
|
1031
|
+
if (authorized.status === 403 && !getFrameGuard()) console.warn(`[ilha-router] dev frame request rejected: Origin ${String(req.headers.origin)} is not trusted (host: ${String(req.headers.host ?? "localhost")}). Configure trustedOrigins via IlhaPagesOptions if this origin is expected.`);
|
|
1032
|
+
res.statusCode = authorized.status;
|
|
1052
1033
|
res.end();
|
|
1053
1034
|
return;
|
|
1054
1035
|
}
|
|
@@ -1067,42 +1048,48 @@ const pagesFactory = (options = {}) => {
|
|
|
1067
1048
|
const body = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
1068
1049
|
const target = serverIslands.get(body.id ?? "");
|
|
1069
1050
|
if (!target) throw new Error("unknown island");
|
|
1070
|
-
let framePath = "/
|
|
1071
|
-
if (typeof body.path === "string"
|
|
1051
|
+
let framePath = "/";
|
|
1052
|
+
if (typeof body.path === "string") {
|
|
1053
|
+
if (!isSafeFramePath(body.path)) {
|
|
1054
|
+
const env = frameEnvelope(400, { error: "frame failed" });
|
|
1055
|
+
res.statusCode = env.status;
|
|
1056
|
+
for (const [k, v] of Object.entries(env.headers)) res.setHeader(k, v);
|
|
1057
|
+
res.end(env.body);
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
framePath = body.path;
|
|
1061
|
+
}
|
|
1072
1062
|
await server.ssrLoadModule(VIRTUAL_PAGES_SERVER);
|
|
1073
1063
|
if (typeof (await server.ssrLoadModule(target.file))[target.name]?.[Symbol.for("ilha.renderState")] !== "function") throw new Error("unknown island");
|
|
1074
|
-
const headers =
|
|
1075
|
-
for (const name of [
|
|
1076
|
-
"cookie",
|
|
1077
|
-
"authorization",
|
|
1078
|
-
"user-agent"
|
|
1079
|
-
]) {
|
|
1080
|
-
const value = req.headers[name];
|
|
1081
|
-
if (typeof value === "string") headers.set(name, value);
|
|
1082
|
-
}
|
|
1064
|
+
const headers = authorized.identityHeaders;
|
|
1083
1065
|
const requestOrigin = `http://${req.headers.host ?? "localhost"}`;
|
|
1084
1066
|
const request = new Request(new URL(framePath, requestOrigin), {
|
|
1085
1067
|
method: "POST",
|
|
1086
1068
|
headers
|
|
1087
1069
|
});
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1070
|
+
let head;
|
|
1071
|
+
const html = await renderServerIsland(body.id ?? "", request, (r, fn) => runWithIslandRequest(r, fn), (entries) => head = entries);
|
|
1072
|
+
const env = frameEnvelope(200, {
|
|
1073
|
+
html: String(html),
|
|
1074
|
+
head
|
|
1075
|
+
});
|
|
1076
|
+
res.statusCode = env.status;
|
|
1077
|
+
for (const [k, v] of Object.entries(env.headers)) res.setHeader(k, v);
|
|
1078
|
+
res.end(env.body);
|
|
1092
1079
|
} catch (err) {
|
|
1093
1080
|
if (err instanceof FrameError && err.redirect) {
|
|
1094
|
-
|
|
1095
|
-
res.
|
|
1096
|
-
res.setHeader(
|
|
1097
|
-
res.end(
|
|
1081
|
+
const env = frameEnvelope(err.status, { redirect: err.redirect });
|
|
1082
|
+
res.statusCode = env.status;
|
|
1083
|
+
for (const [k, v] of Object.entries(env.headers)) res.setHeader(k, v);
|
|
1084
|
+
res.end(env.body);
|
|
1098
1085
|
return;
|
|
1099
1086
|
}
|
|
1100
1087
|
const status = err instanceof FrameError ? err.status : 400;
|
|
1101
1088
|
if (!(err instanceof FrameError) || err.status >= 500) console.error("[ilha-router] frame render failed:", err);
|
|
1102
|
-
|
|
1103
|
-
res.
|
|
1104
|
-
res.setHeader(
|
|
1105
|
-
res.end(
|
|
1089
|
+
const env = frameEnvelope(status, { error: "frame failed" });
|
|
1090
|
+
res.statusCode = env.status;
|
|
1091
|
+
for (const [k, v] of Object.entries(env.headers)) res.setHeader(k, v);
|
|
1092
|
+
res.end(env.body);
|
|
1106
1093
|
}
|
|
1107
1094
|
});
|
|
1108
1095
|
const structuralInvalidate = createStructuralInvalidate(state, async () => {
|
package/dist/plugin.d.ts
CHANGED
|
@@ -33,7 +33,7 @@ export interface IlhaPagesOptions {
|
|
|
33
33
|
* Return a `Response` to reject; return nothing to allow. Island state is
|
|
34
34
|
* world-readable through frames unless gated — install a session check here
|
|
35
35
|
* when islands serve private data. Production equivalents register via
|
|
36
|
-
* `setFrameGuard()` from `@ilha/router/
|
|
36
|
+
* `setFrameGuard()` from `@ilha/router/ssr`.
|
|
37
37
|
*/
|
|
38
38
|
frameGuard?: (request: Request) => Response | void | Promise<Response | void>;
|
|
39
39
|
/**
|
package/dist/route-match.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared, decoded-aware route pattern matching used by both the router
|
|
3
|
-
* (`index.ts`) and the server-frame path (`
|
|
3
|
+
* (`index.ts`) and the server-frame path (`ssr.ts`) so the
|
|
4
4
|
* two never drift on segment semantics or parameter decoding.
|
|
5
5
|
*
|
|
6
6
|
* Patterns support `:name` segments, a bare mid-pattern `*` (one segment), and
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type { LayoutHandler, ErrorHandler, RouteSnapshot, AppError } from "./index";
|
|
2
2
|
export { ilhaPages, type IlhaPagesOptions } from "./plugin";
|
|
3
3
|
import { type IlhaPagesOptions } from "./plugin";
|
|
4
|
-
/**
|
|
5
|
-
export declare function pages(options?: IlhaPagesOptions):
|
|
4
|
+
/** Rsbuild plugin — use via `@ilha/router/rsbuild`. */
|
|
5
|
+
export declare function pages(options?: IlhaPagesOptions): any;
|
|
6
6
|
export default pages;
|
package/dist/rsbuild.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { t as ilhaPages } from "./plugin-CmI3Brr2.js";
|
|
2
|
+
|
|
3
|
+
//#region src/rsbuild.ts
|
|
4
|
+
/** Rsbuild plugin — use via `@ilha/router/rsbuild`. */
|
|
5
|
+
function pages(options = {}) {
|
|
6
|
+
return ilhaPages.rsbuild(options);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
//#endregion
|
|
10
|
+
export { pages as default, pages, ilhaPages };
|
package/dist/server-island.d.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* `[data-ilha-on]` event sentinels to named actions using the
|
|
15
15
|
* `data-ilha-actions` manifest emitted by `hydratable()`.
|
|
16
16
|
*/
|
|
17
|
+
declare const ISLAND_CALL: unique symbol;
|
|
17
18
|
export type ServerStreamFn = (signal: AbortSignal) => AsyncGenerator<unknown> | Generator<unknown>;
|
|
18
19
|
export interface ServerIslandWiring {
|
|
19
20
|
/** Stream key → client transport. The plugin wires these to tacho stubs. */
|
|
@@ -33,12 +34,26 @@ export interface ServerIslandHandle {
|
|
|
33
34
|
unmount: () => void;
|
|
34
35
|
updateProps: (props?: Record<string, unknown>) => void;
|
|
35
36
|
}
|
|
37
|
+
/** @internal Apply head entries returned with a server-page frame. */
|
|
38
|
+
export declare function __ilhaApplyHead(entries: unknown): void;
|
|
36
39
|
/**
|
|
37
40
|
* Create a client proxy island for a server-defined island. Called by
|
|
38
41
|
* generated virtual modules — not by application code.
|
|
39
42
|
*
|
|
40
43
|
* @param id - Stable identity (`<relative-path>#<export>`), for diagnostics.
|
|
41
|
-
* @param as - Slot tag declared by the server island's
|
|
44
|
+
* @param as - Slot tag declared by the server island's `{ as }` option (default div).
|
|
42
45
|
* @param wiring - Stream/action transports wired to tacho stubs by codegen.
|
|
43
46
|
*/
|
|
44
|
-
|
|
47
|
+
interface IslandCallShape {
|
|
48
|
+
[ISLAND_CALL]: true;
|
|
49
|
+
island: ServerIslandCallable;
|
|
50
|
+
props?: Record<string, unknown>;
|
|
51
|
+
key: string;
|
|
52
|
+
}
|
|
53
|
+
export type ServerIslandCallable = Record<symbol, unknown> & ((props?: Record<string, unknown>) => string) & {
|
|
54
|
+
mount: (host: Element) => () => void;
|
|
55
|
+
toString: () => string;
|
|
56
|
+
key: (slotKey: string) => (props?: Record<string, unknown>) => IslandCallShape;
|
|
57
|
+
};
|
|
58
|
+
export declare function __ilhaServerIsland(id: string, as: string, wiring?: ServerIslandWiring): ServerIslandCallable;
|
|
59
|
+
export {};
|
package/dist/server-island.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { n as applyHeadEntriesToDocument, t as parseSnapshotAttr } from "./snapshot-CsEaY6h_.js";
|
|
1
2
|
import { morph } from "ilha";
|
|
2
3
|
|
|
3
4
|
//#region src/server-island.ts
|
|
@@ -20,23 +21,18 @@ import { morph } from "ilha";
|
|
|
20
21
|
/** Symbol.for keeps brands stable across duplicate ilha copies in one realm. */
|
|
21
22
|
const ISLAND = Symbol.for("ilha.island");
|
|
22
23
|
const ISLAND_SLOT_TAG = Symbol.for("ilha.islandSlotTag");
|
|
23
|
-
const ISLAND_MOUNT_INTERNAL
|
|
24
|
+
const ISLAND_MOUNT_INTERNAL = Symbol.for("ilha.islandMountInternal");
|
|
24
25
|
const STATE_ATTR = "data-ilha-state";
|
|
25
26
|
const EVENT_SENTINEL_ATTR = "data-ilha-on";
|
|
26
27
|
const ACTIONS_ATTR = "data-ilha-actions";
|
|
27
28
|
const PROPS_ATTR = "data-ilha-props";
|
|
28
29
|
const CLIENT_REF_ATTR = "data-ilha-client-ref";
|
|
29
|
-
/**
|
|
30
|
-
function
|
|
31
|
-
if (
|
|
32
|
-
try {
|
|
33
|
-
const parsed = JSON.parse(raw);
|
|
34
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
|
|
35
|
-
return parsed;
|
|
36
|
-
} catch {
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
30
|
+
/** @internal Apply head entries returned with a server-page frame. */
|
|
31
|
+
function __ilhaApplyHead(entries) {
|
|
32
|
+
if (Array.isArray(entries)) applyHeadEntriesToDocument(entries);
|
|
39
33
|
}
|
|
34
|
+
/** Defensive snapshot parse — reuses the shared guarded parser (size cap,
|
|
35
|
+
* plain-object check, depth cap, prototype-key stripping). */
|
|
40
36
|
function assertValidTag(tag) {
|
|
41
37
|
const trimmed = tag.trim();
|
|
42
38
|
if (/^[a-z][a-z0-9-]*$/i.test(trimmed)) return trimmed.toLowerCase();
|
|
@@ -59,7 +55,7 @@ function hydrateServerIsland(host, id, wiring) {
|
|
|
59
55
|
const state = {};
|
|
60
56
|
const rawState = host.getAttribute(STATE_ATTR);
|
|
61
57
|
if (rawState) {
|
|
62
|
-
const parsed =
|
|
58
|
+
const parsed = parseSnapshotAttr(rawState);
|
|
63
59
|
if (parsed) {
|
|
64
60
|
for (const [key, value] of Object.entries(parsed)) if (!key.startsWith("_")) state[key] = value;
|
|
65
61
|
}
|
|
@@ -82,7 +78,7 @@ function hydrateServerIsland(host, id, wiring) {
|
|
|
82
78
|
const attached = [];
|
|
83
79
|
const readManifest = () => {
|
|
84
80
|
const raw = Array.from(host.children).find((c) => c.matches(`template[${ACTIONS_ATTR}]`))?.getAttribute(ACTIONS_ATTR) ?? host.getAttribute(ACTIONS_ATTR) ?? null;
|
|
85
|
-
return raw ?
|
|
81
|
+
return raw ? parseSnapshotAttr(raw) : void 0;
|
|
86
82
|
};
|
|
87
83
|
const wireEvents = () => {
|
|
88
84
|
for (const { el, type, listener } of attached) el.removeEventListener(type, listener);
|
|
@@ -148,13 +144,13 @@ function hydrateServerIsland(host, id, wiring) {
|
|
|
148
144
|
}
|
|
149
145
|
for (const el of host.querySelectorAll(`[${CLIENT_REF_ATTR}]`)) {
|
|
150
146
|
if (!belongsToHost(host, el)) continue;
|
|
151
|
-
const props = reviveChildProps(
|
|
147
|
+
const props = reviveChildProps(parseSnapshotAttr(el.getAttribute(PROPS_ATTR) ?? "") ?? void 0);
|
|
152
148
|
const mounted = mountedChildren.get(el);
|
|
153
149
|
if (mounted) {
|
|
154
150
|
mounted.updateProps(props);
|
|
155
151
|
continue;
|
|
156
152
|
}
|
|
157
|
-
const mount = (wiring.children?.[el.getAttribute(CLIENT_REF_ATTR) ?? ""])?.[ISLAND_MOUNT_INTERNAL
|
|
153
|
+
const mount = (wiring.children?.[el.getAttribute(CLIENT_REF_ATTR) ?? ""])?.[ISLAND_MOUNT_INTERNAL];
|
|
158
154
|
if (mount) mountedChildren.set(el, mount(el, props));
|
|
159
155
|
}
|
|
160
156
|
};
|
|
@@ -194,14 +190,6 @@ function hydrateServerIsland(host, id, wiring) {
|
|
|
194
190
|
updateProps: () => {}
|
|
195
191
|
};
|
|
196
192
|
}
|
|
197
|
-
/**
|
|
198
|
-
* Create a client proxy island for a server-defined island. Called by
|
|
199
|
-
* generated virtual modules — not by application code.
|
|
200
|
-
*
|
|
201
|
-
* @param id - Stable identity (`<relative-path>#<export>`), for diagnostics.
|
|
202
|
-
* @param as - Slot tag declared by the server island's `.as()` (default div).
|
|
203
|
-
* @param wiring - Stream/action transports wired to tacho stubs by codegen.
|
|
204
|
-
*/
|
|
205
193
|
function __ilhaServerIsland(id, as, wiring = {}) {
|
|
206
194
|
const slotTag = assertValidTag(as);
|
|
207
195
|
const island = ((props) => {
|
|
@@ -220,10 +208,10 @@ function __ilhaServerIsland(id, as, wiring = {}) {
|
|
|
220
208
|
key: slotKey
|
|
221
209
|
});
|
|
222
210
|
};
|
|
223
|
-
island[ISLAND_MOUNT_INTERNAL
|
|
211
|
+
island[ISLAND_MOUNT_INTERNAL] = (host) => hydrateServerIsland(host, id, wiring);
|
|
224
212
|
island.mount = (host) => hydrateServerIsland(host, id, wiring).unmount;
|
|
225
213
|
return island;
|
|
226
214
|
}
|
|
227
215
|
|
|
228
216
|
//#endregion
|
|
229
|
-
export { __ilhaServerIsland };
|
|
217
|
+
export { __ilhaApplyHead, __ilhaServerIsland };
|
package/dist/server-islands.d.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
export interface ScannedServerIsland {
|
|
10
10
|
/** Export binding name, or `"default"` for `export default ilha…`. */
|
|
11
11
|
name: string;
|
|
12
|
-
/** Slot tag from
|
|
12
|
+
/** Slot tag from the `{ as }` option — must match what SSR emits. */
|
|
13
13
|
as: string;
|
|
14
14
|
/** Stream key → referenced module export used as its transport. */
|
|
15
15
|
streams: Record<string, string>;
|
|
@@ -22,10 +22,24 @@ export interface ClientIslandRef {
|
|
|
22
22
|
imported: string;
|
|
23
23
|
spec: string;
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Replace identity `action(` wrappers of exported server actions with the
|
|
27
|
+
* capture-aware shim on ALREADY-COMPILED module code. Must run inside the
|
|
28
|
+
* SSR transform so upstream JSX/TS output is preserved.
|
|
29
|
+
*/
|
|
30
|
+
export declare function rewriteServerActions(code: string, rpcActions: Record<string, string>): string;
|
|
25
31
|
export interface ServerModuleScan {
|
|
26
32
|
islands: ScannedServerIsland[];
|
|
27
33
|
/** All value-export names of the module (transport candidates). */
|
|
28
34
|
exports: string[];
|
|
35
|
+
/**
|
|
36
|
+
* Exported server actions rewritten to capture-aware shims: name → the
|
|
37
|
+
* `x:<name>` manifest key the client proxy wires an RPC transport for.
|
|
38
|
+
* Event closures may call these directly without wrapping in ilha's
|
|
39
|
+
* action() — during hydration-manifest rendering the call is recorded,
|
|
40
|
+
* not executed.
|
|
41
|
+
*/
|
|
42
|
+
rpcActions: Record<string, string>;
|
|
29
43
|
/** Imported JSX components that must hydrate inside the server island. */
|
|
30
44
|
clientRefs: ClientIslandRef[];
|
|
31
45
|
/** True when the module declares `export const load = loader.client(…)` —
|
|
@@ -35,7 +49,7 @@ export interface ServerModuleScan {
|
|
|
35
49
|
export declare function clientRefPublicId(spec: string, imported: string): string;
|
|
36
50
|
/** Scan a `*.server.ts(x)` module source for island exports and their
|
|
37
51
|
* declarative wiring. Convention: islands start with `ilha` — both builder
|
|
38
|
-
*
|
|
52
|
+
* function components (`ilha(() => …)`) and `ilha(schema, component)`. */
|
|
39
53
|
export declare function scanServerIslands(source: string): ServerModuleScan;
|
|
40
54
|
export declare function loadServerModuleScan(path: string): ServerModuleScan;
|
|
41
55
|
/** Virtual-module id prefix for generated client proxies of server islands.
|