@ilha/router 0.10.4 → 0.11.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 +233 -1010
- package/dist/als-key-CVxbuM3z.js +5 -0
- package/dist/als-key.d.ts +1 -0
- package/dist/codegen.d.ts +0 -2
- package/dist/{snapshot-CsEaY6h_.js → head-DpYV3bHH.js} +5 -57
- package/dist/index.d.ts +58 -340
- package/dist/index.js +700 -3
- package/dist/{plugin-BQil-7B_.js → plugin-C3AAI3e_.js} +67 -193
- package/dist/plugin.d.ts +0 -10
- package/dist/request-scope.d.ts +2 -1
- package/dist/rsbuild.js +1 -1
- package/dist/server-island.d.ts +0 -3
- package/dist/server-island.js +7 -8
- package/dist/server-islands.d.ts +0 -3
- package/dist/snapshot-C0E2OGwL.js +55 -0
- package/dist/snapshot.d.ts +1 -0
- package/dist/{ssr-BTmqfAp9.js → ssr-WA3bSsCo.js} +146 -215
- package/dist/ssr.d.ts +29 -64
- package/dist/ssr.js +2 -2
- package/dist/vite.js +1 -1
- package/oxlint.cjs +63 -241
- package/package.json +5 -7
- package/dist/server.d.ts +0 -8
- package/dist/server.js +0 -14
- package/dist/src-PCXNZfql.js +0 -2161
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { C as runWithIslandRequest, a as authorizeFrameRequest, b as setFrameAuth, c as frameScopedUrl, f as isSafeFramePath, h as parseFrameProps, n as FrameError, s as frameEnvelope, u as getFrameGuard, x as setFrameGuard, y as renderServerIslandResult } from "./ssr-WA3bSsCo.js";
|
|
2
2
|
import { existsSync, readFileSync, statSync, watch } from "node:fs";
|
|
3
3
|
import { basename, dirname, extname, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
import * as Result from "effect/Result";
|
|
4
5
|
import { createUnplugin } from "unplugin";
|
|
5
6
|
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
6
7
|
import { createHash } from "node:crypto";
|
|
@@ -21,9 +22,8 @@ function rewriteServerActions(code, rpcActions) {
|
|
|
21
22
|
});
|
|
22
23
|
}
|
|
23
24
|
const EXPORT_RE = /(?:^|\n)\s*export\s+(?:declare\s+)?(?:async\s+)?(?:function\s*\*?|const|let|var|class)\s+([A-Za-z_$][\w$]*)/g;
|
|
24
|
-
const ISLAND_EXPORT_RE = /(?:^|\n)\s*export\s+(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*
|
|
25
|
-
const DEFAULT_ISLAND_RE = /export\s+default\s+
|
|
26
|
-
const AS_RES = [/\{\s*as:\s*["'`]([a-z][a-z0-9-]*)["'`]\s*[,}]?/];
|
|
25
|
+
const ISLAND_EXPORT_RE = /(?:^|\n)\s*export\s+(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s+function|function\s*\*)/g;
|
|
26
|
+
const DEFAULT_ISLAND_RE = /export\s+default\s+(?:async\s+function|function\s*\*|function\b)/;
|
|
27
27
|
function clientRefPublicId(spec, imported) {
|
|
28
28
|
return createHash("sha256").update(`${spec}#${imported}`).digest("base64url");
|
|
29
29
|
}
|
|
@@ -102,32 +102,20 @@ function scanServerIslands(source) {
|
|
|
102
102
|
const islands = [];
|
|
103
103
|
const collect = (name, start, sliceEnd) => {
|
|
104
104
|
const slice = source.slice(start, sliceEnd);
|
|
105
|
-
|
|
106
|
-
for (const re of AS_RES) {
|
|
107
|
-
const hit = slice.match(re)?.[1];
|
|
108
|
-
if (hit) {
|
|
109
|
-
as = hit;
|
|
110
|
-
break;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
105
|
+
const as = "div";
|
|
113
106
|
const streams = {};
|
|
114
107
|
const actions = {};
|
|
115
108
|
let actionOrder = 0;
|
|
116
109
|
let streamOrder = 0;
|
|
117
|
-
for (const
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
128
|
-
const target = referencedExports(args, candidates);
|
|
129
|
-
actions[`a${actionOrder++}`] = target ?? "";
|
|
130
|
-
}
|
|
110
|
+
for (const match of slice.matchAll(/[A-Za-z0-9_$]*[Aa]ction\s*\(/g)) {
|
|
111
|
+
const args = extractCallArgs(slice, match.index + match[0].indexOf("("));
|
|
112
|
+
if (!args) continue;
|
|
113
|
+
const target = referencedExports(args, candidates);
|
|
114
|
+
actions[`a${actionOrder++}`] = target ?? "";
|
|
115
|
+
}
|
|
116
|
+
for (const match of slice.matchAll(/fromAsyncIterable\(\s*([A-Za-z_$][\w$]*)\s*\(/g)) {
|
|
117
|
+
const target = match[1];
|
|
118
|
+
if (candidates.has(target) && !Object.values(streams).includes(target)) streams[`d${streamOrder++}`] = target;
|
|
131
119
|
}
|
|
132
120
|
islands.push({
|
|
133
121
|
name,
|
|
@@ -152,8 +140,7 @@ function scanServerIslands(source) {
|
|
|
152
140
|
islands,
|
|
153
141
|
exports,
|
|
154
142
|
rpcActions,
|
|
155
|
-
clientRefs: scanClientRefs(source)
|
|
156
|
-
clientLoader: /(^|\n)\s*export\s+(?:const|let|var)\s+load\b\s*=\s*loader\.client\b/.test(source)
|
|
143
|
+
clientRefs: scanClientRefs(source)
|
|
157
144
|
};
|
|
158
145
|
}
|
|
159
146
|
function loadServerModuleScan(path) {
|
|
@@ -198,8 +185,7 @@ function generateServerIslandModule(spec, scan) {
|
|
|
198
185
|
if (actions.length) wiring.push(`actions: { ${actions.join(", ")} }`);
|
|
199
186
|
if (scan.clientRefs.length) wiring.push(`children: { ${scan.clientRefs.map((ref, index) => `${JSON.stringify(ref.id)}: $$child${index}`).join(", ")} }`);
|
|
200
187
|
const id = serverIslandPublicId(spec, island.name);
|
|
201
|
-
wiring.push(`frame: (props) => fetch("/__ilha/frame", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ id: ${JSON.stringify(id)}, path: location.pathname + location.search, props }) }).then((r) => { if (!r.ok) throw new Error("frame failed"); return r.json(); }).then((j) => { if (j.redirect) { location.assign(
|
|
202
|
-
if (scan.clientLoader) wiring.push(`clientLoader: () => $$call("load", [])`);
|
|
188
|
+
wiring.push(`frame: (props) => fetch("/__ilha/frame", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ id: ${JSON.stringify(id)}, path: location.pathname + location.search, props }) }).then((r) => { if (!r.ok) throw new Error("frame failed"); return r.json(); }).then((j) => { if (j.redirect) { try { const u = new URL(j.redirect, location.href); if (u.origin === location.origin) location.assign(u.pathname + u.search + u.hash); } catch {} throw new Error("frame redirected"); } __ilhaApplyHead(j.head); return j.html; })`);
|
|
203
189
|
const call = `__ilhaServerIsland(${JSON.stringify(id)}, ${JSON.stringify(island.as)}, { ${wiring.join(", ")} })`;
|
|
204
190
|
if (island.name === "default") lines.push(`export default ${call};`);
|
|
205
191
|
else lines.push(`export const ${island.name} = ${call};`);
|
|
@@ -274,32 +260,6 @@ function toPosix(p) {
|
|
|
274
260
|
const EXCLUDED_RE = /\.(test|spec|d)\.(ts|tsx)$/;
|
|
275
261
|
/** Server pages: `foo.server.tsx` routes `/foo`, rendered through the frame protocol. */
|
|
276
262
|
const SERVER_PAGE_RE = /\.server\.(ts|tsx)$/;
|
|
277
|
-
/**
|
|
278
|
-
* Match a top-of-statement `export const load`, `export let load`,
|
|
279
|
-
* `export function load`, or `export async function load`. Intentionally
|
|
280
|
-
* conservative: `export { load } from "./x"` re-exports are NOT detected
|
|
281
|
-
* in v1. Declare `load` directly in the file to be picked up.
|
|
282
|
-
*/
|
|
283
|
-
const LOADER_EXPORT_RE = /^\s*export\s+(?:const|let|var|async\s+function|function)\s+load\b/m;
|
|
284
|
-
/** `export const load = loader.client(…)` — the only client-loader form. */
|
|
285
|
-
/** `export const load = loader.client(…)` — client loader under the server name. */
|
|
286
|
-
const LOAD_CLIENT_EXPORT_RE = /(^|\n)\s*export\s+(?:const|let|var)\s+load\b\s*=\s*loader\.client\b/m;
|
|
287
|
-
async function detectLoaderExports(file) {
|
|
288
|
-
try {
|
|
289
|
-
const stripped = (await readFile(file, "utf8")).replace(/^\s*\/\/.*$/gm, "");
|
|
290
|
-
const isClientLoader = LOAD_CLIENT_EXPORT_RE.test(stripped);
|
|
291
|
-
return {
|
|
292
|
-
load: LOADER_EXPORT_RE.test(stripped) && !isClientLoader,
|
|
293
|
-
isClientLoader
|
|
294
|
-
};
|
|
295
|
-
} catch (err) {
|
|
296
|
-
if (err?.code !== "ENOENT") console.warn(`[ilha-router] failed to read ${file} while detecting loader exports:`, err);
|
|
297
|
-
return {
|
|
298
|
-
load: false,
|
|
299
|
-
isClientLoader: false
|
|
300
|
-
};
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
263
|
function fileToSegment(name) {
|
|
304
264
|
if (name.startsWith("[...") && name.endsWith("]")) return `**:${name.slice(4, -1)}`;
|
|
305
265
|
if (name.startsWith("[") && name.endsWith("]")) return `:${name.slice(1, -1)}`;
|
|
@@ -374,37 +334,17 @@ async function collectFiles(dir, depth = 0) {
|
|
|
374
334
|
async function scanPages(pagesDir) {
|
|
375
335
|
const all = await collectFiles(pagesDir);
|
|
376
336
|
const allSet = new Set(all);
|
|
377
|
-
|
|
378
|
-
const layoutLoaderCache = /* @__PURE__ */ new Map();
|
|
379
|
-
const getLayoutExports = (file) => {
|
|
380
|
-
let cached = layoutLoaderCache.get(file);
|
|
381
|
-
if (!cached) {
|
|
382
|
-
cached = detectLoaderExports(file);
|
|
383
|
-
layoutLoaderCache.set(file, cached);
|
|
384
|
-
}
|
|
385
|
-
return cached;
|
|
386
|
-
};
|
|
387
|
-
return Promise.all(pages.map(async (file) => {
|
|
388
|
-
const server = SERVER_PAGE_RE.test(basename(file));
|
|
337
|
+
return all.filter((f) => !basename(f).startsWith("+")).map((file) => {
|
|
389
338
|
const pattern = fileToPattern(pagesDir, file);
|
|
390
|
-
const layouts = chainForFile(pagesDir, file, allSet, "+layout");
|
|
391
|
-
const errors = chainForFile(pagesDir, file, allSet, "+error");
|
|
392
|
-
const [pageExports, ...layoutExports] = await Promise.all([detectLoaderExports(file), ...layouts.map(getLayoutExports)]);
|
|
393
|
-
const loaderLayouts = layouts.filter((_, i) => layoutExports[i].load);
|
|
394
|
-
const clientLoaderLayouts = layouts.filter((_, i) => layoutExports[i].isClientLoader);
|
|
395
339
|
return {
|
|
396
340
|
file,
|
|
397
341
|
pattern,
|
|
398
342
|
name: patternToName(pattern),
|
|
399
|
-
layouts,
|
|
400
|
-
errors,
|
|
401
|
-
|
|
402
|
-
loaderLayouts,
|
|
403
|
-
hasClientLoader: pageExports.isClientLoader,
|
|
404
|
-
clientLoaderLayouts,
|
|
405
|
-
server
|
|
343
|
+
layouts: chainForFile(pagesDir, file, allSet, "+layout"),
|
|
344
|
+
errors: chainForFile(pagesDir, file, allSet, "+error"),
|
|
345
|
+
server: SERVER_PAGE_RE.test(basename(file))
|
|
406
346
|
};
|
|
407
|
-
})
|
|
347
|
+
});
|
|
408
348
|
}
|
|
409
349
|
function validateEntries(entries, pagesDir, strict) {
|
|
410
350
|
if (entries.length === 0) {
|
|
@@ -416,7 +356,8 @@ function validateEntries(entries, pagesDir, strict) {
|
|
|
416
356
|
const problems = [];
|
|
417
357
|
for (const entry of entries) {
|
|
418
358
|
if (entry.server) try {
|
|
419
|
-
|
|
359
|
+
const scan = loadServerModuleScan(entry.file);
|
|
360
|
+
if (!scan.islands.some((island) => island.name === "default") && !scan.exports.includes("default")) throw new Error(`[ilha:pages] Server page ${entry.file} has no default export.`);
|
|
420
361
|
} catch (err) {
|
|
421
362
|
if (err instanceof Error && err.message.includes("[ilha:pages]")) throw err;
|
|
422
363
|
throw new Error(`[ilha:pages] Server page ${entry.file} could not be scanned for island exports.`);
|
|
@@ -435,8 +376,7 @@ function validateEntries(entries, pagesDir, strict) {
|
|
|
435
376
|
function resolveGeneratedPaths(outDir) {
|
|
436
377
|
return {
|
|
437
378
|
serverFile: join(outDir, "pages.server.ts"),
|
|
438
|
-
clientFile: join(outDir, "pages.client.ts")
|
|
439
|
-
loadersFile: join(outDir, "loaders.ts")
|
|
379
|
+
clientFile: join(outDir, "pages.client.ts")
|
|
440
380
|
};
|
|
441
381
|
}
|
|
442
382
|
async function generate(pagesDir, outDir, options = {}) {
|
|
@@ -446,13 +386,13 @@ async function generate(pagesDir, outDir, options = {}) {
|
|
|
446
386
|
const entries = sortEntries(await scanPages(pagesDir));
|
|
447
387
|
validateEntries(entries, pagesDir, options.strict === true);
|
|
448
388
|
await mkdir(outDir, { recursive: true });
|
|
449
|
-
const { serverFile, clientFile
|
|
389
|
+
const { serverFile, clientFile } = resolveGeneratedPaths(outDir);
|
|
450
390
|
const serverChanged = await writeIfChanged(serverFile, buildServerFile(entries, serverFile));
|
|
451
391
|
const clientChanged = await writeIfChanged(clientFile, buildClientFile(entries, clientFile, {
|
|
452
392
|
isStatic,
|
|
453
393
|
interceptLinks
|
|
454
394
|
}));
|
|
455
|
-
if (!isStatic)
|
|
395
|
+
if (!isStatic) {}
|
|
456
396
|
if (serverChanged || clientChanged) await generateTypes(outDir);
|
|
457
397
|
}
|
|
458
398
|
function buildServerFile(entries, serverFile) {
|
|
@@ -474,7 +414,7 @@ function buildServerFile(entries, serverFile) {
|
|
|
474
414
|
const wrappedId = `_wrapped${i}`;
|
|
475
415
|
wrappedIslandLines.push(`const ${wrappedId} = ${expr};`);
|
|
476
416
|
registryLines.push(` ${JSON.stringify(entry.name)}: ${wrappedId}` + (i < entries.length - 1 ? "," : ""));
|
|
477
|
-
routeLines.push(` .route(${JSON.stringify(entry.pattern)}, ${wrappedId})`
|
|
417
|
+
routeLines.push(` .route(${JSON.stringify(entry.pattern)}, ${wrappedId})`);
|
|
478
418
|
if (entry.errors.length > 0) routeLines.push(` .errorBoundary(${JSON.stringify(entry.pattern)}, _error${i}_${entry.errors.length - 1})`);
|
|
479
419
|
}
|
|
480
420
|
return [
|
|
@@ -483,7 +423,6 @@ function buildServerFile(entries, serverFile) {
|
|
|
483
423
|
`// Import via: import { pageRouter, registry } from "ilha:pages/server";`,
|
|
484
424
|
``,
|
|
485
425
|
...imports,
|
|
486
|
-
...entries.some((e) => e.hasLoader || e.loaderLayouts.length > 0) ? [`import { setFrameLoaderRunner } from "@ilha/router/ssr";`, `setFrameLoaderRunner((path, request) => pageRouter.runLoader(path, request));`] : [],
|
|
487
426
|
``,
|
|
488
427
|
...wrappedIslandLines,
|
|
489
428
|
``,
|
|
@@ -503,8 +442,6 @@ function buildClientFile(entries, clientFile, opts) {
|
|
|
503
442
|
return r.startsWith(".") ? r : `./${r}`;
|
|
504
443
|
};
|
|
505
444
|
const clientImport = (abs) => `${rel(abs)}?client`;
|
|
506
|
-
const clientLoaderImport = (abs) => `${rel(abs)}?client-loader`;
|
|
507
|
-
let needsComposeLoaders = false;
|
|
508
445
|
const imports = isStatic ? [`import { router as _router, wrapLayout, wrapError } from "@ilha/router";`, `import type { Island } from "ilha";`] : [`import { router, wrapLayout, wrapError } from "@ilha/router";`, `import type { Island } from "ilha";`];
|
|
509
446
|
const wrappedIslandLines = [];
|
|
510
447
|
const registryLines = [];
|
|
@@ -536,27 +473,10 @@ function buildClientFile(entries, clientFile, opts) {
|
|
|
536
473
|
wrappedIslandLines.push(`const ${wrappedId} = ${expr};`);
|
|
537
474
|
registryLines.push(` ${JSON.stringify(entry.name)}: ${wrappedId}` + (i < entries.length - 1 ? "," : ""));
|
|
538
475
|
if (!isStatic) {
|
|
539
|
-
routeLines.push(` .route(${JSON.stringify(entry.pattern)}, ${wrappedId})`
|
|
540
|
-
const clientLoaderIds = [];
|
|
541
|
-
for (const [j, layout] of entry.clientLoaderLayouts.entries()) {
|
|
542
|
-
const id = `_cl${i}_l${j}`;
|
|
543
|
-
imports.push(`import { load as ${id} } from ${JSON.stringify(clientLoaderImport(layout))};`);
|
|
544
|
-
clientLoaderIds.push(id);
|
|
545
|
-
}
|
|
546
|
-
if (entry.hasClientLoader) {
|
|
547
|
-
const id = `_cl${i}`;
|
|
548
|
-
imports.push(`import { load as ${id} } from ${JSON.stringify(clientLoaderImport(entry.file))};`);
|
|
549
|
-
clientLoaderIds.push(id);
|
|
550
|
-
}
|
|
551
|
-
if (clientLoaderIds.length > 0) {
|
|
552
|
-
const expr = clientLoaderIds.length === 1 ? clientLoaderIds[0] : `composeLoaders([${clientLoaderIds.join(", ")}])`;
|
|
553
|
-
if (clientLoaderIds.length > 1) needsComposeLoaders = true;
|
|
554
|
-
routeLines.push(` .clientLoader(${JSON.stringify(entry.pattern)}, ${expr})`);
|
|
555
|
-
}
|
|
476
|
+
routeLines.push(` .route(${JSON.stringify(entry.pattern)}, ${wrappedId})`);
|
|
556
477
|
if (entry.errors.length > 0) routeLines.push(` .errorBoundary(${JSON.stringify(entry.pattern)}, _error${i}_${entry.errors.length - 1})`);
|
|
557
478
|
}
|
|
558
479
|
}
|
|
559
|
-
if (needsComposeLoaders) imports[0] = imports[0].replace("{ router", "{ composeLoaders, router");
|
|
560
480
|
const routerExpr = isStatic ? `_router({ mode: "static" })` : `router(${interceptLinks === false ? `{ interceptLinks: false }` : ""})`;
|
|
561
481
|
const lines = [
|
|
562
482
|
`// @generated by @ilha/router — do not edit`,
|
|
@@ -576,52 +496,6 @@ function buildClientFile(entries, clientFile, opts) {
|
|
|
576
496
|
else lines.push(`export const pageRouter = ${routerExpr}`, ...routeLines, ` ;`);
|
|
577
497
|
return lines.join("\n");
|
|
578
498
|
}
|
|
579
|
-
function buildLoadersFile(entries, loadersFile, serverFile) {
|
|
580
|
-
const relFromLoaders = (abs) => {
|
|
581
|
-
const r = toPosix(relative(dirname(loadersFile), abs));
|
|
582
|
-
return r.startsWith(".") ? r : `./${r}`;
|
|
583
|
-
};
|
|
584
|
-
const withLoaders = entries.filter((e) => e.hasLoader || e.loaderLayouts.length > 0);
|
|
585
|
-
if (withLoaders.length === 0) return [
|
|
586
|
-
`// @generated by @ilha/router — do not edit`,
|
|
587
|
-
`// This project has no loader exports; this file is intentionally empty.`,
|
|
588
|
-
``,
|
|
589
|
-
`export {};`,
|
|
590
|
-
``
|
|
591
|
-
].join("\n");
|
|
592
|
-
const serverRel = relFromLoaders(serverFile).replace(/\.tsx?$/, "");
|
|
593
|
-
const imports = [`import { pageRouter } from ${JSON.stringify(serverRel)};`];
|
|
594
|
-
let needsComposeLoaders = false;
|
|
595
|
-
const attachLines = [];
|
|
596
|
-
for (const [i, entry] of withLoaders.entries()) {
|
|
597
|
-
const loaderIds = [];
|
|
598
|
-
for (const [j, layout] of entry.loaderLayouts.entries()) {
|
|
599
|
-
const id = `_p${i}_l${j}`;
|
|
600
|
-
imports.push(`import { load as ${id} } from ${JSON.stringify(relFromLoaders(layout))};`);
|
|
601
|
-
loaderIds.push(id);
|
|
602
|
-
}
|
|
603
|
-
if (entry.hasLoader) {
|
|
604
|
-
const id = `_p${i}`;
|
|
605
|
-
imports.push(`import { load as ${id} } from ${JSON.stringify(relFromLoaders(entry.file))};`);
|
|
606
|
-
loaderIds.push(id);
|
|
607
|
-
}
|
|
608
|
-
const loadersExpr = loaderIds.length === 1 ? loaderIds[0] : `composeLoaders([${loaderIds.join(", ")}])`;
|
|
609
|
-
if (loaderIds.length > 1) needsComposeLoaders = true;
|
|
610
|
-
attachLines.push(`pageRouter.attachLoader(${JSON.stringify(entry.pattern)}, ${loadersExpr});`);
|
|
611
|
-
}
|
|
612
|
-
if (needsComposeLoaders) imports.unshift(`import { composeLoaders } from "@ilha/router";`);
|
|
613
|
-
return [
|
|
614
|
-
`// @generated by @ilha/router — do not edit`,
|
|
615
|
-
`// Server-only. Import this module from your SSR entry to wire loaders`,
|
|
616
|
-
`// onto pageRouter. Importing it from the client is a no-op but wastes`,
|
|
617
|
-
`// bundle size — rely on the default build pipeline to keep it out.`,
|
|
618
|
-
``,
|
|
619
|
-
...imports,
|
|
620
|
-
``,
|
|
621
|
-
...attachLines,
|
|
622
|
-
``
|
|
623
|
-
].join("\n");
|
|
624
|
-
}
|
|
625
499
|
async function writeIfChanged(file, content) {
|
|
626
500
|
try {
|
|
627
501
|
if (await readFile(file, "utf8") === content) return false;
|
|
@@ -648,7 +522,7 @@ async function generateTypes(outDir) {
|
|
|
648
522
|
`}`,
|
|
649
523
|
``,
|
|
650
524
|
`declare module "ilha:loaders" {`,
|
|
651
|
-
` // Side-effect-only module.
|
|
525
|
+
` // Side-effect-only module. oxidejs imports it alongside @ilha/router/ssr.`,
|
|
652
526
|
`}`,
|
|
653
527
|
``
|
|
654
528
|
].join("\n"));
|
|
@@ -656,6 +530,14 @@ async function generateTypes(outDir) {
|
|
|
656
530
|
|
|
657
531
|
//#endregion
|
|
658
532
|
//#region src/plugin.ts
|
|
533
|
+
function devServerOrigin(server) {
|
|
534
|
+
const configured = server.config.server.origin;
|
|
535
|
+
if (configured) return new URL(typeof configured === "string" ? configured : String(configured)).origin;
|
|
536
|
+
const local = server.resolvedUrls?.local?.[0];
|
|
537
|
+
if (local) return new URL(local).origin;
|
|
538
|
+
const { host, port, https } = server.config.server;
|
|
539
|
+
return `${https ? "https" : "http"}://${host === true || host === void 0 || host === "" ? "localhost" : host === false ? "localhost" : String(host)}:${typeof port === "number" ? port : port ? Number(port) : 5173}`;
|
|
540
|
+
}
|
|
659
541
|
const VIRTUAL_PAGES_SERVER = "ilha:pages/server";
|
|
660
542
|
const VIRTUAL_PAGES_CLIENT = "ilha:pages/client";
|
|
661
543
|
const VIRTUAL_LOADERS = "ilha:loaders";
|
|
@@ -669,8 +551,6 @@ const RESOLVED_VIRTUAL_IDS = [
|
|
|
669
551
|
];
|
|
670
552
|
/** Query suffix used on page/layout imports in the client file. */
|
|
671
553
|
const CLIENT_QUERY = "?client";
|
|
672
|
-
/** Query suffix that re-exports a page/layout's `load` (loader.client) for the browser bundle. */
|
|
673
|
-
const CLIENT_LOADER_QUERY = "?client-loader";
|
|
674
554
|
function decodeServerIslandId(id) {
|
|
675
555
|
if (!id.startsWith("\0ilha:server-island:")) return null;
|
|
676
556
|
try {
|
|
@@ -750,13 +630,12 @@ function detectIlhaConsumers(root) {
|
|
|
750
630
|
function resolvePluginPaths(root, options) {
|
|
751
631
|
const pagesDir = resolve(root, options.dir ?? "src/pages");
|
|
752
632
|
const outDir = resolve(root, options.outDir ?? ".ilha");
|
|
753
|
-
const { serverFile, clientFile
|
|
633
|
+
const { serverFile, clientFile } = resolveGeneratedPaths(outDir);
|
|
754
634
|
return {
|
|
755
635
|
pagesDir,
|
|
756
636
|
outDir,
|
|
757
637
|
serverFile,
|
|
758
|
-
clientFile
|
|
759
|
-
loadersFile
|
|
638
|
+
clientFile
|
|
760
639
|
};
|
|
761
640
|
}
|
|
762
641
|
function createPagesPluginState(options) {
|
|
@@ -764,9 +643,8 @@ function createPagesPluginState(options) {
|
|
|
764
643
|
let outDir;
|
|
765
644
|
let serverFile;
|
|
766
645
|
let clientFile;
|
|
767
|
-
let loadersFile;
|
|
768
646
|
const setPaths = (root) => {
|
|
769
|
-
({pagesDir, outDir, serverFile, clientFile
|
|
647
|
+
({pagesDir, outDir, serverFile, clientFile} = resolvePluginPaths(root, options));
|
|
770
648
|
};
|
|
771
649
|
const regen = async () => {
|
|
772
650
|
try {
|
|
@@ -799,9 +677,6 @@ function createPagesPluginState(options) {
|
|
|
799
677
|
get clientFile() {
|
|
800
678
|
return clientFile;
|
|
801
679
|
},
|
|
802
|
-
get loadersFile() {
|
|
803
|
-
return loadersFile;
|
|
804
|
-
},
|
|
805
680
|
setPaths,
|
|
806
681
|
regen,
|
|
807
682
|
shouldRegenOnChange,
|
|
@@ -816,7 +691,7 @@ function resolvePagesId(state, id, importer) {
|
|
|
816
691
|
if (id === "ilha:pages/server") return RESOLVED_PAGES_SERVER;
|
|
817
692
|
if (id === "ilha:pages/client") return RESOLVED_PAGES_CLIENT;
|
|
818
693
|
if (id === "ilha:loaders") return RESOLVED_LOADERS;
|
|
819
|
-
for (const query of [
|
|
694
|
+
for (const query of [CLIENT_QUERY]) {
|
|
820
695
|
if (!id.endsWith(query)) continue;
|
|
821
696
|
const bare = id.slice(0, -query.length);
|
|
822
697
|
const resolved = importer ? resolve(importer.replace(/\?.*$/, ""), "..", bare) : resolve(bare);
|
|
@@ -833,14 +708,7 @@ function loadPagesModule(state, id) {
|
|
|
833
708
|
const spec = state.clientFile.replace(/\.tsx?$/, "");
|
|
834
709
|
return `export { pageRouter, registry } from ${JSON.stringify(spec)};`;
|
|
835
710
|
}
|
|
836
|
-
if (id === "\0ilha:loaders") {
|
|
837
|
-
const spec = state.loadersFile.replace(/\.tsx?$/, "");
|
|
838
|
-
return `import ${JSON.stringify(spec)};`;
|
|
839
|
-
}
|
|
840
|
-
if (id.endsWith("?client-loader")) {
|
|
841
|
-
const bare = id.slice(0, -14);
|
|
842
|
-
return `export { load } from ${JSON.stringify(bare)};`;
|
|
843
|
-
}
|
|
711
|
+
if (id === "\0ilha:loaders") return `// @generated by @ilha/router — do not edit\nexport {};\n`;
|
|
844
712
|
if (id.endsWith("?client")) {
|
|
845
713
|
const bare = id.slice(0, -7);
|
|
846
714
|
return `export { default } from ${JSON.stringify(bare)};`;
|
|
@@ -914,7 +782,6 @@ const pagesFactory = (options = {}) => {
|
|
|
914
782
|
const singletonPeers = [
|
|
915
783
|
"ilha",
|
|
916
784
|
"@ilha/router",
|
|
917
|
-
"alien-signals",
|
|
918
785
|
...detectIlhaConsumers(userConfig.root ? resolve(userConfig.root) : process.cwd())
|
|
919
786
|
];
|
|
920
787
|
const existingNoExternal = userConfig.ssr?.noExternal;
|
|
@@ -928,8 +795,7 @@ const pagesFactory = (options = {}) => {
|
|
|
928
795
|
...userConfig.optimizeDeps?.include ?? [],
|
|
929
796
|
"ilha",
|
|
930
797
|
"ilha/jsx-runtime",
|
|
931
|
-
"ilha/jsx-dev-runtime"
|
|
932
|
-
"alien-signals"
|
|
798
|
+
"ilha/jsx-dev-runtime"
|
|
933
799
|
])]
|
|
934
800
|
}
|
|
935
801
|
};
|
|
@@ -949,11 +815,7 @@ const pagesFactory = (options = {}) => {
|
|
|
949
815
|
if (Object.keys(scan.rpcActions).length > 0) lines.unshift(`import { __ilhaServerAction } from "@ilha/router/ssr";`);
|
|
950
816
|
lines.unshift(`import * as __ilhaSelf from ${JSON.stringify(file)};`);
|
|
951
817
|
lines.unshift(`import { registerServerIsland } from "@ilha/router/ssr";`);
|
|
952
|
-
for (const island of scan.islands) {
|
|
953
|
-
const id2 = serverIslandPublicId(file, island.name);
|
|
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))} }` : "";
|
|
955
|
-
lines.push(`registerServerIsland(${JSON.stringify(id2)}, () => __ilhaSelf[${JSON.stringify(island.name)}]?.[Symbol.for("ilha.renderState")]${isServerPage});`);
|
|
956
|
-
}
|
|
818
|
+
for (const island of scan.islands) lines.push(`registerServerIsland(${JSON.stringify(serverIslandPublicId(file, island.name))}, () => __ilhaSelf[${JSON.stringify(island.name)}]);`);
|
|
957
819
|
}
|
|
958
820
|
const base = rewriteServerActions(code, scan?.rpcActions ?? {});
|
|
959
821
|
if (base === code && lines.length === 0) return null;
|
|
@@ -1001,7 +863,6 @@ const pagesFactory = (options = {}) => {
|
|
|
1001
863
|
configureServer(server) {
|
|
1002
864
|
server.watcher.add(state.pagesDir);
|
|
1003
865
|
if (options.frameGuard) setFrameGuard(options.frameGuard);
|
|
1004
|
-
if (options.loaderGuard) setLoaderGuard(options.loaderGuard);
|
|
1005
866
|
setFrameAuth({
|
|
1006
867
|
trustedOrigins: options.trustedOrigins,
|
|
1007
868
|
csrf: options.csrf,
|
|
@@ -1019,7 +880,8 @@ const pagesFactory = (options = {}) => {
|
|
|
1019
880
|
res.end();
|
|
1020
881
|
return;
|
|
1021
882
|
}
|
|
1022
|
-
const
|
|
883
|
+
const serverOrigin = devServerOrigin(server);
|
|
884
|
+
const frameUrl = `${serverOrigin}${req.url ?? "/"}`;
|
|
1023
885
|
const authorized = await authorizeFrameRequest(new Request(frameUrl, {
|
|
1024
886
|
method: req.method,
|
|
1025
887
|
headers: new Headers(req.headers)
|
|
@@ -1061,19 +923,31 @@ const pagesFactory = (options = {}) => {
|
|
|
1061
923
|
framePath = body.path;
|
|
1062
924
|
}
|
|
1063
925
|
await server.ssrLoadModule(VIRTUAL_PAGES_SERVER);
|
|
1064
|
-
|
|
926
|
+
await server.ssrLoadModule(target.file);
|
|
1065
927
|
const headers = authorized.identityHeaders;
|
|
1066
|
-
const
|
|
1067
|
-
const request = new Request(new URL(framePath, requestOrigin), {
|
|
928
|
+
const request = new Request(frameScopedUrl(req.url ?? "/", framePath, serverOrigin), {
|
|
1068
929
|
method: "POST",
|
|
1069
930
|
headers
|
|
1070
931
|
});
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
932
|
+
const result = await renderServerIslandResult(body.id ?? "", request, (r, fn) => runWithIslandRequest(r, fn), incomingProps);
|
|
933
|
+
if (Result.isFailure(result)) {
|
|
934
|
+
const err = result.failure;
|
|
935
|
+
if (err.redirect) {
|
|
936
|
+
const env = frameEnvelope(err.status, { redirect: err.redirect });
|
|
937
|
+
res.statusCode = env.status;
|
|
938
|
+
for (const [k, v] of Object.entries(env.headers)) res.setHeader(k, v);
|
|
939
|
+
res.end(env.body);
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
const status = err.status;
|
|
943
|
+
if (status >= 500) console.error("[ilha-router] frame render failed:", err);
|
|
944
|
+
const env = frameEnvelope(status, { error: "frame failed" });
|
|
945
|
+
res.statusCode = env.status;
|
|
946
|
+
for (const [k, v] of Object.entries(env.headers)) res.setHeader(k, v);
|
|
947
|
+
res.end(env.body);
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
const env = frameEnvelope(200, { html: result.success });
|
|
1077
951
|
res.statusCode = env.status;
|
|
1078
952
|
for (const [k, v] of Object.entries(env.headers)) res.setHeader(k, v);
|
|
1079
953
|
res.end(env.body);
|
package/dist/plugin.d.ts
CHANGED
|
@@ -8,8 +8,6 @@ export declare const RESOLVED_LOADERS = "\0ilha:loaders";
|
|
|
8
8
|
export declare const RESOLVED_VIRTUAL_IDS: readonly ["\0ilha:pages/server", "\0ilha:pages/client", "\0ilha:loaders"];
|
|
9
9
|
/** Query suffix used on page/layout imports in the client file. */
|
|
10
10
|
export declare const CLIENT_QUERY = "?client";
|
|
11
|
-
/** Query suffix that re-exports a page/layout's `load` (loader.client) for the browser bundle. */
|
|
12
|
-
export declare const CLIENT_LOADER_QUERY = "?client-loader";
|
|
13
11
|
export interface IlhaPagesOptions {
|
|
14
12
|
/** Directory containing page files. Default: `src/pages` */
|
|
15
13
|
dir?: string;
|
|
@@ -36,12 +34,6 @@ export interface IlhaPagesOptions {
|
|
|
36
34
|
* `setFrameGuard()` from `@ilha/router/ssr`.
|
|
37
35
|
*/
|
|
38
36
|
frameGuard?: (request: Request) => Response | void | Promise<Response | void>;
|
|
39
|
-
/**
|
|
40
|
-
* Guard consulted only by `GET /__ilha/loader` in production. When absent,
|
|
41
|
-
* the loader endpoint falls back to the frame guard for backwards
|
|
42
|
-
* compatibility. Mirrors `setLoaderGuard()`.
|
|
43
|
-
*/
|
|
44
|
-
loaderGuard?: (request: Request) => Response | void | Promise<Response | void>;
|
|
45
37
|
/**
|
|
46
38
|
* Explicit trusted origins for frame/loader requests (e.g. a `.vercel.app`
|
|
47
39
|
* or custom domain). When unset, origin checks compare the `Origin` header
|
|
@@ -64,14 +56,12 @@ export declare function resolvePluginPaths(root: string, options: IlhaPagesOptio
|
|
|
64
56
|
outDir: string;
|
|
65
57
|
serverFile: string;
|
|
66
58
|
clientFile: string;
|
|
67
|
-
loadersFile: string;
|
|
68
59
|
};
|
|
69
60
|
export interface PagesPluginState {
|
|
70
61
|
pagesDir: string;
|
|
71
62
|
outDir: string;
|
|
72
63
|
serverFile: string;
|
|
73
64
|
clientFile: string;
|
|
74
|
-
loadersFile: string;
|
|
75
65
|
setPaths(root: string): void;
|
|
76
66
|
regen(): Promise<void>;
|
|
77
67
|
shouldRegenOnChange(file: string): boolean;
|
package/dist/request-scope.d.ts
CHANGED
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
* storage without importing `node:async_hooks`; this node-only module is the
|
|
14
14
|
* sole place that constructs it.
|
|
15
15
|
*/
|
|
16
|
+
import { REQUEST_ALS_KEY } from "./als-key";
|
|
16
17
|
import type { IslandContext } from "./index";
|
|
17
|
-
export
|
|
18
|
+
export { REQUEST_ALS_KEY };
|
|
18
19
|
/** Run `fn` with `request` available to `useContext().request`. When oxidejs
|
|
19
20
|
* is loaded, its action scope is entered too, so `useRequest()` works in
|
|
20
21
|
* island renders and streamed frames. */
|
package/dist/rsbuild.js
CHANGED
package/dist/server-island.d.ts
CHANGED
|
@@ -24,9 +24,6 @@ export interface ServerIslandWiring {
|
|
|
24
24
|
actions?: Record<string, (payload?: unknown) => unknown>;
|
|
25
25
|
/** Frame transport: re-renders the island with the latest parent props. */
|
|
26
26
|
frame?: (props?: Record<string, unknown>) => unknown;
|
|
27
|
-
/** RPC transport for the module's `loader.client` export — invoked once
|
|
28
|
-
* when the view hydrates. Side-effect loader on server pages. */
|
|
29
|
-
clientLoader?: () => unknown;
|
|
30
27
|
/** Client-capable islands nested in the server render, keyed by opaque ref. */
|
|
31
28
|
children?: Record<string, unknown>;
|
|
32
29
|
}
|
package/dist/server-island.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { t as applyHeadEntriesToDocument } from "./head-DpYV3bHH.js";
|
|
2
|
+
import { t as parseSnapshotAttr } from "./snapshot-C0E2OGwL.js";
|
|
3
3
|
|
|
4
4
|
//#region src/server-island.ts
|
|
5
5
|
/**
|
|
@@ -43,7 +43,8 @@ function assertValidTag(tag) {
|
|
|
43
43
|
function belongsToHost(host, candidate) {
|
|
44
44
|
let el = candidate.parentElement;
|
|
45
45
|
while (el && el !== host) {
|
|
46
|
-
if (el.hasAttribute("data-ilha")
|
|
46
|
+
if (el.hasAttribute("data-ilha")) return false;
|
|
47
|
+
if (el.hasAttribute("data-ilha-slot") && el.style.display !== "contents") return false;
|
|
47
48
|
el = el.parentElement;
|
|
48
49
|
}
|
|
49
50
|
return el === host;
|
|
@@ -106,7 +107,8 @@ function hydrateServerIsland(host, id, wiring, props) {
|
|
|
106
107
|
if (controller.signal.aborted || !host.isConnected) return;
|
|
107
108
|
const html = await frame(sent);
|
|
108
109
|
if (controller.signal.aborted || typeof html !== "string") return;
|
|
109
|
-
|
|
110
|
+
const parsed = new DOMParser().parseFromString(`<div>${html}</div>`, "text/html");
|
|
111
|
+
host.replaceChildren(...Array.from(parsed.body.firstElementChild?.childNodes ?? []));
|
|
110
112
|
syncChildren();
|
|
111
113
|
wireEvents();
|
|
112
114
|
}).catch((err) => {
|
|
@@ -115,7 +117,7 @@ function hydrateServerIsland(host, id, wiring, props) {
|
|
|
115
117
|
};
|
|
116
118
|
const attached = [];
|
|
117
119
|
const readManifest = () => {
|
|
118
|
-
const raw =
|
|
120
|
+
const raw = host.querySelector(`:scope > template[${ACTIONS_ATTR}], template[${ACTIONS_ATTR}]`)?.getAttribute(ACTIONS_ATTR) ?? host.getAttribute(ACTIONS_ATTR) ?? null;
|
|
119
121
|
return raw ? parseSnapshotAttr(raw) : void 0;
|
|
120
122
|
};
|
|
121
123
|
const wireEvents = () => {
|
|
@@ -215,9 +217,6 @@ function hydrateServerIsland(host, id, wiring, props) {
|
|
|
215
217
|
}
|
|
216
218
|
})();
|
|
217
219
|
if (frame && !host.hasAttribute(STATE_ATTR) && host.childNodes.length === 0) scheduleRepaint();
|
|
218
|
-
if (wiring.clientLoader) Promise.resolve(wiring.clientLoader()).catch((err) => {
|
|
219
|
-
if (!controller.signal.aborted) console.error(`[ilha-router] client loader failed for "${id}":`, err);
|
|
220
|
-
});
|
|
221
220
|
return {
|
|
222
221
|
unmount: () => {
|
|
223
222
|
for (const { el, type, listener } of attached) el.removeEventListener(type, listener);
|
package/dist/server-islands.d.ts
CHANGED
|
@@ -42,9 +42,6 @@ export interface ServerModuleScan {
|
|
|
42
42
|
rpcActions: Record<string, string>;
|
|
43
43
|
/** Imported JSX components that must hydrate inside the server island. */
|
|
44
44
|
clientRefs: ClientIslandRef[];
|
|
45
|
-
/** True when the module declares `export const load = loader.client(…)` —
|
|
46
|
-
* the proxy wires it as an RPC call invoked when the view hydrates. */
|
|
47
|
-
clientLoader?: boolean;
|
|
48
45
|
}
|
|
49
46
|
export declare function clientRefPublicId(spec: string, imported: string): string;
|
|
50
47
|
/** Scan a `*.server.ts(x)` module source for island exports and their
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
//#region src/snapshot.ts
|
|
2
|
+
/**
|
|
3
|
+
* Defensive parser for `data-ilha-state` / `data-ilha-props` snapshot
|
|
4
|
+
* attributes on the router side. Mirrors the core ilha guards: size cap,
|
|
5
|
+
* plain-object check, depth cap, and prototype-key stripping. Returns
|
|
6
|
+
* undefined (degrade gracefully) on any failure.
|
|
7
|
+
*/
|
|
8
|
+
const MAX_SNAPSHOT_CHARS = 262144;
|
|
9
|
+
const MAX_SNAPSHOT_DEPTH = 32;
|
|
10
|
+
const UNSAFE_SNAPSHOT_KEYS = /* @__PURE__ */ new Set([
|
|
11
|
+
"__proto__",
|
|
12
|
+
"constructor",
|
|
13
|
+
"prototype"
|
|
14
|
+
]);
|
|
15
|
+
function stripUnsafeKeys(value) {
|
|
16
|
+
if (value === null || typeof value !== "object") return;
|
|
17
|
+
if (Array.isArray(value)) {
|
|
18
|
+
for (const item of value) stripUnsafeKeys(item);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
for (const key of Object.getOwnPropertyNames(value)) if (UNSAFE_SNAPSHOT_KEYS.has(key)) delete value[key];
|
|
22
|
+
else stripUnsafeKeys(value[key]);
|
|
23
|
+
}
|
|
24
|
+
function exceedsDepth(value, depth) {
|
|
25
|
+
if (depth > MAX_SNAPSHOT_DEPTH) return true;
|
|
26
|
+
if (value === null || typeof value !== "object") return false;
|
|
27
|
+
if (Array.isArray(value)) {
|
|
28
|
+
for (const item of value) if (exceedsDepth(item, depth + 1)) return true;
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
for (const key in value) {
|
|
32
|
+
if (!Object.hasOwn(value, key)) continue;
|
|
33
|
+
if (exceedsDepth(value[key], depth + 1)) return true;
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
function sanitizeSnapshotObject(value) {
|
|
38
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
39
|
+
if (exceedsDepth(value, 1)) return void 0;
|
|
40
|
+
stripUnsafeKeys(value);
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
function parseSnapshotAttr(raw) {
|
|
44
|
+
if (raw.length > MAX_SNAPSHOT_CHARS) return void 0;
|
|
45
|
+
let parsed;
|
|
46
|
+
try {
|
|
47
|
+
parsed = JSON.parse(raw);
|
|
48
|
+
} catch {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
return sanitizeSnapshotObject(parsed);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
//#endregion
|
|
55
|
+
export { sanitizeSnapshotObject as n, parseSnapshotAttr as t };
|