@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/dist/ssr.js CHANGED
@@ -1,201 +1,3 @@
1
- import { t as runWithIslandRequest } from "./request-scope-C4reU4v0.js";
2
- import { FrameError, getFrameAuth, getFrameGuard, getFrameLoaderRunner, getLoaderGuard, isTrustedOrigin, renderServerIsland } from "./server-island-registry.js";
1
+ import { C as setLoaderGuard, S as setFrameLoaderRunner, _ as readBodyBounded, a as __ilhaServerAction, b as setFrameAuth, c as frameEnvelope, d as getFrameLoaderRunner, f as getLoaderGuard, g as json, h as isTrustedOrigin, i as MAX_BODY, l as getFrameAuth, m as isSafeFramePath, n as FrameError, o as authorizeFrameRequest, p as getServerIslandEntry, r as LOADER_ENDPOINT, s as forwardIdentityHeaders, t as FRAME_ENDPOINT, u as getFrameGuard, v as registerServerIsland, w as ssr, x as setFrameGuard, y as renderServerIsland } from "./ssr-BxrcUYy5.js";
3
2
 
4
- //#region src/ssr.ts
5
- /**
6
- * Production SSR endpoints for server-owned islands and regular-page loads.
7
- *
8
- * Default export is an oxidejs-style fetch middleware:
9
- * `(request) => Response | undefined`. Returns `undefined` for any request it
10
- * does not own, so hosts can chain it ahead of their own handler:
11
- *
12
- * ```ts
13
- * oxide({ middleware: ["@ilha/router/ssr"] });
14
- * ```
15
- *
16
- * Serves:
17
- * - `POST /__ilha/frame` — re-renders a server island (JSON `{ id, path }` in,
18
- * `{ html }` out). Renderers come from the process-global registry
19
- * populated by self-registration code appended to `.server` modules.
20
- * - `GET /__ilha/loader?path=…` — regular-page server loads via the loader
21
- * runner (`setFrameLoaderRunner`, wired by the generated server module).
22
- */
23
- const FRAME_ENDPOINT = "/__ilha/frame";
24
- /** Regular-page server loads: served through the loader-runner slot. */
25
- const LOADER_ENDPOINT = "/__ilha/loader";
26
- /** Max request body size — matches the dev middleware cap. */
27
- const MAX_BODY = 16384;
28
- function json(status, body) {
29
- return new Response(JSON.stringify(body), {
30
- status,
31
- headers: {
32
- "cache-control": "no-store",
33
- "content-type": "application/json;charset=utf-8"
34
- }
35
- });
36
- }
37
- /**
38
- * Read a request body as UTF-8, streaming it with a hard byte cap. Returns
39
- * `null` when the body exceeds `maxBytes` (the reader is cancelled before the
40
- * cap is far exceeded) or when decoding fails.
41
- */
42
- async function readBodyBounded(request, maxBytes) {
43
- const contentLength = request.headers.get("content-length");
44
- if (contentLength !== null && Number(contentLength) > maxBytes) return null;
45
- const reader = request.body?.getReader();
46
- if (!reader) return "";
47
- const chunks = [];
48
- let size = 0;
49
- for (;;) {
50
- const { done, value } = await reader.read();
51
- if (done) break;
52
- size += value?.byteLength ?? 0;
53
- if (size > maxBytes) {
54
- await reader.cancel().catch(() => {});
55
- return null;
56
- }
57
- chunks.push(value);
58
- }
59
- const decoder = new TextDecoder();
60
- return chunks.map((c) => decoder.decode(c, { stream: true })).join("") + decoder.decode();
61
- }
62
- async function ssr(request) {
63
- let pathname;
64
- try {
65
- pathname = new URL(request.url).pathname;
66
- } catch {
67
- return json(400, { error: "frame failed" });
68
- }
69
- if (pathname !== "/__ilha/frame" && pathname !== "/__ilha/loader") return;
70
- const auth = getFrameAuth();
71
- if (!isTrustedOrigin(request, auth)) return json(403, { error: "frame failed" });
72
- if (pathname === "/__ilha/loader") {
73
- if (request.method !== "GET") return json(405, { error: "method not allowed" });
74
- const guard = getLoaderGuard() ?? getFrameGuard();
75
- if (!guard && (auth?.defaultAction ?? "deny") === "deny") return json(403, { error: "loader failed" });
76
- try {
77
- const denied = await guard?.(request);
78
- if (denied) return denied;
79
- } catch {
80
- return json(403, { error: "loader failed" });
81
- }
82
- const runner = getFrameLoaderRunner();
83
- if (!runner) return json(404, {
84
- kind: "error",
85
- status: 404,
86
- message: "not found"
87
- });
88
- const cl = request.headers.get("content-length");
89
- if (cl && Number(cl) > 16384) return json(413, { error: "frame failed" });
90
- let target = "/";
91
- try {
92
- target = new URL(request.url).searchParams.get("path") ?? "/";
93
- } catch {
94
- return json(400, {
95
- kind: "error",
96
- status: 400,
97
- message: "bad request"
98
- });
99
- }
100
- if (!target.startsWith("/") || target.includes("//") || target.includes("\\") || target.length > 2048) return json(400, {
101
- kind: "error",
102
- status: 400,
103
- message: "bad request"
104
- });
105
- try {
106
- const result = await runWithIslandRequest(request, () => runner(target, request));
107
- if (result.kind === "redirect") return json(result.status || 302, {
108
- kind: "redirect",
109
- to: result.to,
110
- status: result.status
111
- });
112
- if (result.kind !== "data") {
113
- const status = result.status || 500;
114
- return json(status, {
115
- kind: result.kind,
116
- status,
117
- message: result.message
118
- });
119
- }
120
- return json(200, result);
121
- } catch (error) {
122
- console.error("[ilha-router] loader endpoint failed:", error);
123
- return json(500, {
124
- kind: "error",
125
- status: 500,
126
- message: "loader failed"
127
- });
128
- }
129
- }
130
- if (request.method !== "POST") return json(405, { error: "frame failed" });
131
- if (!(request.headers.get("content-type") ?? "").startsWith("application/json")) return json(415, { error: "frame failed" });
132
- const guard = getFrameGuard();
133
- if (!guard && (auth?.defaultAction ?? "deny") === "deny") return json(403, { error: "frame failed" });
134
- try {
135
- const denied = await guard?.(request);
136
- if (denied) return denied;
137
- } catch (error) {
138
- console.error("[ilha-router] frame guard failed:", error);
139
- return json(403, { error: "frame failed" });
140
- }
141
- if (auth?.csrf) try {
142
- if (!await auth.csrf(request)) return json(403, { error: "frame failed" });
143
- } catch {
144
- return json(403, { error: "frame failed" });
145
- }
146
- let id;
147
- let path = "/";
148
- try {
149
- const text = await readBodyBounded(request, MAX_BODY);
150
- if (text === null) return json(413, { error: "frame failed" });
151
- const body = JSON.parse(text);
152
- id = String(body.id ?? "");
153
- if (typeof body.path === "string") {
154
- if (body.path.startsWith("/") && !body.path.includes("//") && !body.path.includes("\\") && body.path.length <= 2048) path = body.path;
155
- else return json(400, { error: "frame failed" });
156
- }
157
- } catch {
158
- return json(400, { error: "frame failed" });
159
- }
160
- try {
161
- let origin;
162
- try {
163
- origin = new URL(request.url).origin;
164
- } catch {
165
- return json(400, { error: "frame failed" });
166
- }
167
- const headers = new Headers();
168
- for (const name of [
169
- "cookie",
170
- "authorization",
171
- "user-agent"
172
- ]) {
173
- const value = request.headers.get(name);
174
- if (value !== null) headers.set(name, value);
175
- }
176
- const scoped = new Request(new URL(path, origin), {
177
- method: "POST",
178
- headers
179
- });
180
- for (const sym of Object.getOwnPropertySymbols(request)) {
181
- if (Symbol.keyFor(sym) === void 0) continue;
182
- try {
183
- scoped[sym] = request[sym];
184
- } catch {}
185
- }
186
- return json(200, { html: await renderServerIsland(id, scoped, (scopedRequest, fn) => Promise.resolve(runWithIslandRequest(scopedRequest, fn))) });
187
- } catch (error) {
188
- if (error instanceof FrameError) {
189
- if (error.redirect) return json(error.status, { redirect: error.redirect });
190
- if (error.status >= 500) console.error("[ilha-router] frame render failed:", error);
191
- return json(error.status, { error: "frame failed" });
192
- }
193
- console.error("[ilha-router] frame render failed:", error);
194
- return json(400, { error: "frame failed" });
195
- }
196
- }
197
- /** Side-effect imports required alongside this handler. */
198
- ssr.imports = ["ilha:pages/server", "ilha:loaders"];
199
-
200
- //#endregion
201
- export { FRAME_ENDPOINT, LOADER_ENDPOINT, MAX_BODY, ssr as default, json, readBodyBounded };
3
+ export { FRAME_ENDPOINT, FrameError, LOADER_ENDPOINT, MAX_BODY, __ilhaServerAction, authorizeFrameRequest, ssr as default, forwardIdentityHeaders, frameEnvelope, getFrameAuth, getFrameGuard, getFrameLoaderRunner, getLoaderGuard, getServerIslandEntry, isSafeFramePath, isTrustedOrigin, json, readBodyBounded, registerServerIsland, renderServerIsland, setFrameAuth, setFrameGuard, setFrameLoaderRunner, setLoaderGuard };
package/dist/vite.js CHANGED
@@ -1,4 +1,4 @@
1
- import { t as ilhaPages } from "./plugin-BHuojFhQ.js";
1
+ import { t as ilhaPages } from "./plugin-CmI3Brr2.js";
2
2
 
3
3
  //#region src/vite.ts
4
4
  /** Vite plugin — use via `@ilha/router/vite`. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ilha/router",
3
- "version": "0.9.2",
3
+ "version": "0.10.0",
4
4
  "description": "A tiny SPA router for Ilha",
5
5
  "keywords": [
6
6
  "frontend",
@@ -9,6 +9,7 @@
9
9
  "islands",
10
10
  "router",
11
11
  "routing",
12
+ "rsbuild",
12
13
  "spa",
13
14
  "ssr",
14
15
  "unplugin",
@@ -47,21 +48,13 @@
47
48
  "types": "./dist/ssr.d.ts",
48
49
  "import": "./dist/ssr.js"
49
50
  },
50
- "./server-island-registry": {
51
- "types": "./dist/server-island-registry.d.ts",
52
- "import": "./dist/server-island-registry.js"
53
- },
54
51
  "./vite": {
55
52
  "types": "./dist/vite.d.ts",
56
53
  "import": "./dist/vite.js"
57
54
  },
58
- "./rspack": {
59
- "types": "./dist/rspack.d.ts",
60
- "import": "./dist/rspack.js"
61
- },
62
- "./rolldown": {
63
- "types": "./dist/rolldown.d.ts",
64
- "import": "./dist/rolldown.js"
55
+ "./rsbuild": {
56
+ "types": "./dist/rsbuild.d.ts",
57
+ "import": "./dist/rsbuild.js"
65
58
  }
66
59
  },
67
60
  "publishConfig": {
@@ -76,10 +69,10 @@
76
69
  "unplugin": "3.3.0"
77
70
  },
78
71
  "devDependencies": {
79
- "ilha": "0.11.1",
72
+ "ilha": "0.12.0",
80
73
  "vite": "^8.2.2"
81
74
  },
82
75
  "peerDependencies": {
83
- "ilha": ">=0.11.1"
76
+ "ilha": ">=0.12.0"
84
77
  }
85
78
  }
@@ -1,7 +0,0 @@
1
- /**
2
- * Compile-time type anchors for the router API. Every imperative call lives
3
- * inside this never-invoked function so module scope has NO side effects:
4
- * `router()` resets the module-global route registry, which must not happen
5
- * at import time (tsconfig.build emits declarations for these anchors only).
6
- */
7
- export declare function typecheckRouterApi(): void;
@@ -1,34 +0,0 @@
1
- import { AsyncLocalStorage } from "node:async_hooks";
2
-
3
- //#region src/request-scope.ts
4
- /**
5
- * Request scope for server-owned island rendering.
6
- *
7
- * A `.server.tsx` island's render function always executes on the server —
8
- * page SSR through the router, or streamed frames through the plugin's
9
- * `/__ilha/frame` endpoint. Both seed this scope with the originating
10
- * `Request`, so render functions can read request data (URL, headers,
11
- * cookies) through `useContext().request` or a host integration such as Oxide's `useRequest()`.
12
- *
13
- * The storage lives on `globalThis` under `ilha.requestAls` so every module
14
- * copy (plugin bundle, SSR graph) shares one instance. The public accessor
15
- * is `useContext()` from the main `@ilha/router` entry, which reads the
16
- * storage without importing `node:async_hooks`; this node-only module is the
17
- * sole place that constructs it.
18
- */
19
- const REQUEST_ALS_KEY = Symbol.for("ilha.requestAls");
20
- /** Installed by oxidejs when its module loads. Lets `useRequest()` resolve
21
- * inside island renders and frames, not just `/__oxide/action`. */
22
- const OXIDE_RUN_WITH_REQUEST = Symbol.for("oxidejs.runWithRequest");
23
- /** Run `fn` with `request` available to `useContext().request`. When oxidejs
24
- * is loaded, its action scope is entered too, so `useRequest()` works in
25
- * island renders and streamed frames. */
26
- function runWithIslandRequest(request, fn) {
27
- const g = globalThis;
28
- const als = g[REQUEST_ALS_KEY] ??= new AsyncLocalStorage();
29
- const oxide = g[OXIDE_RUN_WITH_REQUEST];
30
- return als.run(request, () => oxide ? oxide(request, fn) : fn());
31
- }
32
-
33
- //#endregion
34
- export { runWithIslandRequest as t };
@@ -1,6 +0,0 @@
1
- export type { LayoutHandler, ErrorHandler, RouteSnapshot, AppError } from "./index";
2
- export { ilhaPages, type IlhaPagesOptions } from "./plugin";
3
- import { type IlhaPagesOptions } from "./plugin";
4
- /** Rolldown plugin — use via `@ilha/router/rolldown`. */
5
- export declare function pages(options?: IlhaPagesOptions): import("rolldown").Plugin<any> | import("rolldown").Plugin<any>[];
6
- export default pages;
package/dist/rolldown.js DELETED
@@ -1,10 +0,0 @@
1
- import { t as ilhaPages } from "./plugin-BHuojFhQ.js";
2
-
3
- //#region src/rolldown.ts
4
- /** Rolldown plugin — use via `@ilha/router/rolldown`. */
5
- function pages(options = {}) {
6
- return ilhaPages.rolldown(options);
7
- }
8
-
9
- //#endregion
10
- export { pages as default, pages, ilhaPages };
package/dist/rspack.js DELETED
@@ -1,10 +0,0 @@
1
- import { t as ilhaPages } from "./plugin-BHuojFhQ.js";
2
-
3
- //#region src/rspack.ts
4
- /** Rspack plugin — use via `@ilha/router/rspack`. */
5
- function pages(options = {}) {
6
- return ilhaPages.rspack(options);
7
- }
8
-
9
- //#endregion
10
- export { pages as default, pages, ilhaPages };
@@ -1,122 +0,0 @@
1
- /**
2
- * Process-global registry of server-island renderers, keyed by the public
3
- * island id (`sha256(file#name)`, see `serverIslandPublicId`). Lives on
4
- * `globalThis` so every module copy (plugin bundle, SSR graph, frame entry)
5
- * shares one instance — same pattern as `request-scope.ts`.
6
- *
7
- * `.server` modules self-register when the plugin appends registration code
8
- * to their server-graph copy; the production `/__ilha/frame` handler (see
9
- * `@ilha/router/frame`) consumes the registry to re-render an island from a
10
- * client state snapshot. Server pages additionally register their `load` and
11
- * route pattern so frame handlers can run the loader with matched params.
12
- */
13
- /** Loader context for server-page `load` — mirrors the router's shape. */
14
- export interface FrameLoaderContext {
15
- params: Record<string, string>;
16
- request: Request;
17
- url: URL;
18
- signal: AbortSignal;
19
- }
20
- export type ServerPageLoader = (ctx: FrameLoaderContext) => unknown;
21
- /** A frame render: optionally preceded by running the page's `load`. */
22
- export interface ServerIslandEntry {
23
- /** Returns the renderState fn (`Symbol.for("ilha.renderState")` getter). */
24
- render: () => unknown;
25
- /** The module's `load` export — runs at frame time; its return value
26
- * becomes the island's render props. */
27
- load?: ServerPageLoader;
28
- /** Route pattern for the page (`/user/:id`) — matches params for `load`. */
29
- pattern?: string;
30
- }
31
- export type FrameGuard = (request: Request) => Response | void | Promise<Response | void>;
32
- /**
33
- * Install a guard consulted by every `/__ilha/frame` request (dev middleware
34
- * and the production `@ilha/router/frame` handler share this slot — both read
35
- * it from `globalThis`). Return a `Response` to reject; return nothing to
36
- * allow. Island state is world-readable through frames unless you gate them,
37
- * so apps serving private data should install a session check here.
38
- */
39
- export declare function setFrameGuard(guard: FrameGuard): void;
40
- export declare function getFrameGuard(): FrameGuard | undefined;
41
- /**
42
- * Install a guard consulted only by `GET /__ilha/loader`. When absent, the
43
- * loader endpoint falls back to `getFrameGuard()` for backwards compatibility.
44
- * Prefer a dedicated loader guard so gating the loader endpoint is independent
45
- * of frame rendering.
46
- */
47
- export declare function setLoaderGuard(guard: FrameGuard): void;
48
- export declare function getLoaderGuard(): FrameGuard | undefined;
49
- /** Frame-authorization policy, installed via {@link setFrameAuth}. */
50
- export interface FrameAuthPolicy {
51
- /**
52
- * Action taken when no frame guard is registered. `"deny"` (default in the
53
- * production handler) rejects every `/__ilha/frame` request with 403;
54
- * `"open"` preserves the legacy unauthenticated behavior. The dev
55
- * middleware stays permissive unless a guard is registered.
56
- */
57
- defaultAction?: "open" | "deny";
58
- /**
59
- * Explicit trusted origins (e.g. `"https://app.example.com"`). When set,
60
- * origin checks accept only these; otherwise the check compares the `Origin`
61
- * header against `https://{host}` / `http://{host}`.
62
- */
63
- trustedOrigins?: string[];
64
- /**
65
- * Optional CSRF verifier for the state-changing frame POST. Receives the
66
- * original `Request`; returning falsy rejects the request. Use this for
67
- * server-to-server frame callers that have no browser `Origin`.
68
- */
69
- csrf?: (request: Request) => boolean | Promise<boolean>;
70
- }
71
- /**
72
- * Install the frame-authorization policy consumed by the production
73
- * `@ilha/router/ssr` handler. `trustedOrigins` and `csrf` are also applied by
74
- * the dev middleware (via `IlhaPagesOptions`).
75
- */
76
- export declare function setFrameAuth(policy: FrameAuthPolicy): void;
77
- export declare function getFrameAuth(): FrameAuthPolicy | undefined;
78
- /**
79
- * Same-origin check for frame/loader requests. Browsers always send `Origin`
80
- * on cross-origin and same-origin `POST`; its absence implies a non-browser
81
- * caller (allowed — gate those via a guard or `csrf`). When `Origin` is
82
- * present it must match the configured trusted origins, else the request's
83
- * own `Host`.
84
- */
85
- export declare function isTrustedOrigin(request: Request, policy: FrameAuthPolicy | undefined): boolean;
86
- export type FrameLoaderRunner = (path: string, request?: Request) => Promise<{
87
- kind: string;
88
- data?: unknown;
89
- headEntries?: unknown;
90
- status?: number;
91
- to?: string;
92
- message?: string;
93
- }>;
94
- /**
95
- * Install the handler backing `GET /__ilha/loader` in production. The
96
- * generated `pages.server.ts` wires this to `pageRouter.runLoader`, so
97
- * regular-page server loads get full route matching, layout chains, and
98
- * redirect/error semantics. Dev and prod handlers share the slot.
99
- */
100
- export declare function setFrameLoaderRunner(runner: FrameLoaderRunner): void;
101
- export declare function getFrameLoaderRunner(): FrameLoaderRunner | undefined;
102
- /** Register `id` → entry. Later registrations win (id encodes file + name). */
103
- export declare function registerServerIsland(id: string, render: () => unknown, options?: {
104
- load?: ServerPageLoader;
105
- pattern?: string;
106
- }): void;
107
- export declare function getServerIslandEntry(id: string): ServerIslandEntry | undefined;
108
- /** Back-compat alias used by tests. */
109
- export declare const getServerIslandRenderer: typeof getServerIslandEntry;
110
- /** Client-facing frame failure. `redirect` carries a loader redirect target. */
111
- export declare class FrameError extends Error {
112
- status: number;
113
- redirect?: string;
114
- constructor(status: number, message: string, redirect?: string);
115
- }
116
- /**
117
- * Shared tail of every frame request: run the page's `load` when registered
118
- * (params matched from the frame path), then invoke the renderer inside the
119
- * caller's scope. Throws `FrameError` with an HTTP status for client-facing
120
- * failures; loader redirects surface via `FrameError.redirect`.
121
- */
122
- export declare function renderServerIsland(id: string, request: Request, runWithScope: <T>(request: Request, fn: () => T) => T | Promise<T>): Promise<string>;
@@ -1,189 +0,0 @@
1
- import { F as parsePattern, I as safeDecode, P as matchSegments, S as resolveRedirectTarget } from "./src-BBsbD5vU.js";
2
-
3
- //#region src/server-island-registry.ts
4
- /**
5
- * Process-global registry of server-island renderers, keyed by the public
6
- * island id (`sha256(file#name)`, see `serverIslandPublicId`). Lives on
7
- * `globalThis` so every module copy (plugin bundle, SSR graph, frame entry)
8
- * shares one instance — same pattern as `request-scope.ts`.
9
- *
10
- * `.server` modules self-register when the plugin appends registration code
11
- * to their server-graph copy; the production `/__ilha/frame` handler (see
12
- * `@ilha/router/frame`) consumes the registry to re-render an island from a
13
- * client state snapshot. Server pages additionally register their `load` and
14
- * route pattern so frame handlers can run the loader with matched params.
15
- */
16
- const REGISTRY_KEY = Symbol.for("ilha.serverIslandRenderers");
17
- function registry() {
18
- const g = globalThis;
19
- let map = g[REGISTRY_KEY];
20
- if (!map) {
21
- map = /* @__PURE__ */ new Map();
22
- g[REGISTRY_KEY] = map;
23
- }
24
- return map;
25
- }
26
- const GUARD_KEY = Symbol.for("ilha.frameGuard");
27
- /**
28
- * Install a guard consulted by every `/__ilha/frame` request (dev middleware
29
- * and the production `@ilha/router/frame` handler share this slot — both read
30
- * it from `globalThis`). Return a `Response` to reject; return nothing to
31
- * allow. Island state is world-readable through frames unless you gate them,
32
- * so apps serving private data should install a session check here.
33
- */
34
- function setFrameGuard(guard) {
35
- const g = globalThis;
36
- g[GUARD_KEY] = guard;
37
- }
38
- function getFrameGuard() {
39
- return globalThis[GUARD_KEY];
40
- }
41
- const LOADER_GUARD_KEY = Symbol.for("ilha.loaderGuard");
42
- /**
43
- * Install a guard consulted only by `GET /__ilha/loader`. When absent, the
44
- * loader endpoint falls back to `getFrameGuard()` for backwards compatibility.
45
- * Prefer a dedicated loader guard so gating the loader endpoint is independent
46
- * of frame rendering.
47
- */
48
- function setLoaderGuard(guard) {
49
- const g = globalThis;
50
- g[LOADER_GUARD_KEY] = guard;
51
- }
52
- function getLoaderGuard() {
53
- return globalThis[LOADER_GUARD_KEY];
54
- }
55
- const AUTH_KEY = Symbol.for("ilha.frameAuth");
56
- /**
57
- * Install the frame-authorization policy consumed by the production
58
- * `@ilha/router/ssr` handler. `trustedOrigins` and `csrf` are also applied by
59
- * the dev middleware (via `IlhaPagesOptions`).
60
- */
61
- function setFrameAuth(policy) {
62
- const g = globalThis;
63
- g[AUTH_KEY] = policy;
64
- }
65
- function getFrameAuth() {
66
- return globalThis[AUTH_KEY];
67
- }
68
- function normalizeOrigin(value) {
69
- try {
70
- return new URL(value).origin;
71
- } catch {
72
- return null;
73
- }
74
- }
75
- /**
76
- * Same-origin check for frame/loader requests. Browsers always send `Origin`
77
- * on cross-origin and same-origin `POST`; its absence implies a non-browser
78
- * caller (allowed — gate those via a guard or `csrf`). When `Origin` is
79
- * present it must match the configured trusted origins, else the request's
80
- * own `Host`.
81
- */
82
- function isTrustedOrigin(request, policy) {
83
- const originHeader = request.headers.get("origin");
84
- if (originHeader === null) return true;
85
- const origin = normalizeOrigin(originHeader);
86
- if (origin === null) return false;
87
- const trusted = policy?.trustedOrigins ?? [];
88
- if (trusted.length > 0) return trusted.some((o) => normalizeOrigin(o) === origin);
89
- const host = request.headers.get("host");
90
- if (!host) return false;
91
- return origin === `https://${host}` || origin === `http://${host}`;
92
- }
93
- const LOADER_RUNNER_KEY = Symbol.for("ilha.frameLoaderRunner");
94
- /**
95
- * Install the handler backing `GET /__ilha/loader` in production. The
96
- * generated `pages.server.ts` wires this to `pageRouter.runLoader`, so
97
- * regular-page server loads get full route matching, layout chains, and
98
- * redirect/error semantics. Dev and prod handlers share the slot.
99
- */
100
- function setFrameLoaderRunner(runner) {
101
- const g = globalThis;
102
- g[LOADER_RUNNER_KEY] = runner;
103
- }
104
- function getFrameLoaderRunner() {
105
- return globalThis[LOADER_RUNNER_KEY];
106
- }
107
- /** Register `id` → entry. Later registrations win (id encodes file + name). */
108
- function registerServerIsland(id, render, options) {
109
- registry().set(id, {
110
- render,
111
- load: options?.load,
112
- pattern: options?.pattern
113
- });
114
- }
115
- function getServerIslandEntry(id) {
116
- return registry().get(id);
117
- }
118
- /** Back-compat alias used by tests. */
119
- const getServerIslandRenderer = getServerIslandEntry;
120
- /** Client-facing frame failure. `redirect` carries a loader redirect target. */
121
- var FrameError = class extends Error {
122
- status;
123
- redirect;
124
- constructor(status, message, redirect) {
125
- super(message);
126
- this.status = status;
127
- this.redirect = redirect;
128
- }
129
- };
130
- /** Match a route pattern (`/user/:id`, `/docs/**:slug`) against a pathname.
131
- * Returns decoded params, or null when the path doesn't match. Shares the
132
- * router's matcher semantics via `route-match.ts`. */
133
- function matchPatternParams(pattern, pathname) {
134
- const raw = matchSegments(parsePattern(pattern).segments, pathname);
135
- if (!raw) return null;
136
- const params = {};
137
- for (const [k, v] of Object.entries(raw)) params[k] = safeDecode(v);
138
- return params;
139
- }
140
- /**
141
- * Shared tail of every frame request: run the page's `load` when registered
142
- * (params matched from the frame path), then invoke the renderer inside the
143
- * caller's scope. Throws `FrameError` with an HTTP status for client-facing
144
- * failures; loader redirects surface via `FrameError.redirect`.
145
- */
146
- async function renderServerIsland(id, request, runWithScope) {
147
- const entry = registry().get(id);
148
- if (!entry) throw new FrameError(400, "unknown island");
149
- let props;
150
- if (entry.load) {
151
- let url;
152
- try {
153
- url = new URL(request.url);
154
- } catch {
155
- throw new FrameError(400, "frame failed");
156
- }
157
- const params = entry.pattern ? matchPatternParams(entry.pattern, url.pathname) : {};
158
- if (!params) throw new FrameError(400, "frame failed");
159
- try {
160
- props = { load: {
161
- loading: false,
162
- value: await entry.load({
163
- params,
164
- request,
165
- url,
166
- signal: request.signal
167
- }) ?? {},
168
- error: void 0
169
- } };
170
- } catch (error) {
171
- const marker = error;
172
- if (marker.__ilhaRedirect === true) {
173
- const r = error;
174
- const safe = resolveRedirectTarget(r.to, url, false);
175
- if (!safe.ok) throw new FrameError(500, "unsafe redirect target");
176
- throw new FrameError(r.status || 302, "frame failed", safe.to);
177
- }
178
- if (marker.__ilhaLoaderError === true) throw new FrameError(error.status || 500, "frame failed");
179
- throw error;
180
- }
181
- }
182
- const render = entry.render();
183
- if (typeof render !== "function") throw new FrameError(400, "unknown island");
184
- const html = await runWithScope(request, () => render(props));
185
- return String(html);
186
- }
187
-
188
- //#endregion
189
- export { FrameError, getFrameAuth, getFrameGuard, getFrameLoaderRunner, getLoaderGuard, getServerIslandEntry, getServerIslandRenderer, isTrustedOrigin, registerServerIsland, renderServerIsland, setFrameAuth, setFrameGuard, setFrameLoaderRunner, setLoaderGuard };