@ilha/router 0.10.4 → 0.11.1
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 +106 -240
- 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
package/dist/snapshot.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import "
|
|
3
|
-
import {
|
|
1
|
+
import { t as REQUEST_ALS_KEY } from "./als-key-CVxbuM3z.js";
|
|
2
|
+
import { n as sanitizeSnapshotObject } from "./snapshot-C0E2OGwL.js";
|
|
3
|
+
import { renderToString } from "ilha";
|
|
4
|
+
import * as Result from "effect/Result";
|
|
4
5
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
6
|
+
import * as Data from "effect/Data";
|
|
7
|
+
import * as Effect from "effect/Effect";
|
|
5
8
|
|
|
6
9
|
//#region src/request-scope.ts
|
|
7
10
|
/**
|
|
@@ -19,7 +22,6 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
19
22
|
* storage without importing `node:async_hooks`; this node-only module is the
|
|
20
23
|
* sole place that constructs it.
|
|
21
24
|
*/
|
|
22
|
-
const REQUEST_ALS_KEY = Symbol.for("ilha.requestAls");
|
|
23
25
|
/** Installed by oxidejs when its module loads. Lets `useRequest()` resolve
|
|
24
26
|
* inside island renders and frames, not just `/__oxide/action`. */
|
|
25
27
|
const OXIDE_RUN_WITH_REQUEST = Symbol.for("oxidejs.runWithRequest");
|
|
@@ -35,6 +37,21 @@ function runWithIslandRequest(request, fn) {
|
|
|
35
37
|
|
|
36
38
|
//#endregion
|
|
37
39
|
//#region src/ssr.ts
|
|
40
|
+
/**
|
|
41
|
+
* Production SSR endpoint for server-owned islands.
|
|
42
|
+
*
|
|
43
|
+
* Default export is an oxidejs-style fetch middleware:
|
|
44
|
+
* `(request) => Response | undefined`. Returns `undefined` for any request it
|
|
45
|
+
* does not own, so hosts can chain it ahead of their own handler:
|
|
46
|
+
*
|
|
47
|
+
* ```ts
|
|
48
|
+
* oxide({ middleware: ["@ilha/router/ssr"] });
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* Serves `POST /__ilha/frame` — re-renders a server island (JSON `{ id, path }`
|
|
52
|
+
* in, `{ html }` out). Renderers come from the process-global registry
|
|
53
|
+
* populated by self-registration code appended to `.server` modules.
|
|
54
|
+
*/
|
|
38
55
|
const REGISTRY_KEY = Symbol.for("ilha.serverIslandRenderers");
|
|
39
56
|
function registry() {
|
|
40
57
|
const g = globalThis;
|
|
@@ -60,20 +77,6 @@ function setFrameGuard(guard) {
|
|
|
60
77
|
function getFrameGuard() {
|
|
61
78
|
return globalThis[GUARD_KEY];
|
|
62
79
|
}
|
|
63
|
-
const LOADER_GUARD_KEY = Symbol.for("ilha.loaderGuard");
|
|
64
|
-
/**
|
|
65
|
-
* Install a guard consulted only by `GET /__ilha/loader`. When absent, the
|
|
66
|
-
* loader endpoint falls back to `getFrameGuard()` for backwards compatibility.
|
|
67
|
-
* Prefer a dedicated loader guard so gating the loader endpoint is independent
|
|
68
|
-
* of frame rendering.
|
|
69
|
-
*/
|
|
70
|
-
function setLoaderGuard(guard) {
|
|
71
|
-
const g = globalThis;
|
|
72
|
-
g[LOADER_GUARD_KEY] = guard;
|
|
73
|
-
}
|
|
74
|
-
function getLoaderGuard() {
|
|
75
|
-
return globalThis[LOADER_GUARD_KEY];
|
|
76
|
-
}
|
|
77
80
|
const AUTH_KEY = Symbol.for("ilha.frameAuth");
|
|
78
81
|
/**
|
|
79
82
|
* Install the frame-authorization policy consumed by the production
|
|
@@ -121,6 +124,30 @@ function isTrustedOrigin(request, policy) {
|
|
|
121
124
|
function isSafeFramePath(path) {
|
|
122
125
|
return path.startsWith("/") && !path.includes("//") && !path.includes("\\") && path.length <= 2048;
|
|
123
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* Build the scoped frame render URL from the incoming request URL and frame
|
|
129
|
+
* path. Absolute `incomingUrl` values supply the origin. Relative values (for
|
|
130
|
+
* example Vite's `req.url`) require an explicit trusted `serverOrigin` — never
|
|
131
|
+
* a client `Host` header.
|
|
132
|
+
*/
|
|
133
|
+
function frameScopedUrl(incomingUrl, framePath, serverOrigin) {
|
|
134
|
+
let origin;
|
|
135
|
+
try {
|
|
136
|
+
const incoming = new URL(incomingUrl);
|
|
137
|
+
if (incoming.protocol !== "http:" && incoming.protocol !== "https:") throw new TypeError("unsupported protocol");
|
|
138
|
+
origin = incoming.origin;
|
|
139
|
+
} catch {
|
|
140
|
+
if (serverOrigin == null) throw new Error("frameScopedUrl: relative incomingUrl requires serverOrigin");
|
|
141
|
+
origin = parseServerOrigin(serverOrigin);
|
|
142
|
+
}
|
|
143
|
+
return new URL(framePath, origin).href;
|
|
144
|
+
}
|
|
145
|
+
function parseServerOrigin(value) {
|
|
146
|
+
if (!value.includes("://")) throw new Error("frameScopedUrl: serverOrigin must be an absolute URL");
|
|
147
|
+
const parsed = new URL(value);
|
|
148
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error("frameScopedUrl: serverOrigin must be http(s)");
|
|
149
|
+
return parsed.origin;
|
|
150
|
+
}
|
|
124
151
|
/** Identity headers forwarded onto scoped render/loader requests. */
|
|
125
152
|
const FORWARD_IDENTITY_HEADERS = [
|
|
126
153
|
"cookie",
|
|
@@ -157,118 +184,83 @@ function frameEnvelope(status, body) {
|
|
|
157
184
|
body: JSON.stringify(body)
|
|
158
185
|
};
|
|
159
186
|
}
|
|
160
|
-
const
|
|
161
|
-
/**
|
|
162
|
-
* Install the handler backing `GET /__ilha/loader` in production. The
|
|
163
|
-
* generated `pages.server.ts` wires this to `pageRouter.runLoader`, so
|
|
164
|
-
* regular-page server loads get full route matching, layout chains, and
|
|
165
|
-
* redirect/error semantics. Dev and prod handlers share the slot.
|
|
166
|
-
*/
|
|
167
|
-
function setFrameLoaderRunner(runner) {
|
|
168
|
-
const g = globalThis;
|
|
169
|
-
g[LOADER_RUNNER_KEY] = runner;
|
|
170
|
-
}
|
|
171
|
-
function getFrameLoaderRunner() {
|
|
172
|
-
return globalThis[LOADER_RUNNER_KEY];
|
|
173
|
-
}
|
|
174
|
-
setServerManifestSerializer({ template(manifest) {
|
|
175
|
-
return `<template data-ilha-actions='${JSON.stringify(Object.fromEntries(manifest)).replace(/&/g, "&").replace(/'/g, "'").replace(/</g, "<")}'></template>`;
|
|
176
|
-
} });
|
|
187
|
+
const ACTION_CALL = Symbol.for("ilha.actionCall");
|
|
177
188
|
/** @internal Brand an exported server action with its generated RPC transport key. */
|
|
178
189
|
function __ilhaServerAction(key, fn) {
|
|
179
|
-
|
|
190
|
+
const wrapped = ((...args) => fn(...args));
|
|
191
|
+
wrapped.with = (...args) => {
|
|
192
|
+
const handler = ((..._ev) => wrapped(...args));
|
|
193
|
+
handler[ACTION_CALL] = {
|
|
194
|
+
k: key,
|
|
195
|
+
a: args
|
|
196
|
+
};
|
|
197
|
+
return handler;
|
|
198
|
+
};
|
|
199
|
+
return wrapped;
|
|
180
200
|
}
|
|
181
|
-
function registerServerIsland(id, render
|
|
182
|
-
registry().set(id, {
|
|
183
|
-
render,
|
|
184
|
-
load: options?.load,
|
|
185
|
-
pattern: options?.pattern
|
|
186
|
-
});
|
|
201
|
+
function registerServerIsland(id, render) {
|
|
202
|
+
registry().set(id, { render });
|
|
187
203
|
}
|
|
188
204
|
function getServerIslandEntry(id) {
|
|
189
205
|
return registry().get(id);
|
|
190
206
|
}
|
|
191
|
-
/** Client-facing frame failure. `redirect` carries a
|
|
192
|
-
var FrameError = class extends
|
|
193
|
-
status;
|
|
194
|
-
redirect;
|
|
195
|
-
constructor(status, message, redirect) {
|
|
196
|
-
super(message);
|
|
197
|
-
this.status = status;
|
|
198
|
-
this.redirect = redirect;
|
|
199
|
-
}
|
|
200
|
-
};
|
|
201
|
-
/** Match a route pattern (`/user/:id`, `/docs/**:slug`) against a pathname.
|
|
202
|
-
* Returns decoded params, or null when the path doesn't match. Shares the
|
|
203
|
-
* router's matcher semantics via `route-match.ts`. */
|
|
204
|
-
function matchPatternParams(pattern, pathname) {
|
|
205
|
-
const raw = matchSegments(parsePattern(pattern).segments, pathname);
|
|
206
|
-
if (!raw) return null;
|
|
207
|
-
const params = {};
|
|
208
|
-
for (const [k, v] of Object.entries(raw)) params[k] = safeDecode(v);
|
|
209
|
-
return params;
|
|
210
|
-
}
|
|
207
|
+
/** Client-facing frame failure. `redirect` carries a same-origin redirect target. */
|
|
208
|
+
var FrameError = class extends Data.TaggedError("FrameError") {};
|
|
211
209
|
/**
|
|
212
210
|
* Shared tail of every frame request: run the page's `load` when registered
|
|
213
211
|
* (params matched from the frame path), then invoke the renderer inside the
|
|
214
212
|
* caller's scope. Throws `FrameError` with an HTTP status for client-facing
|
|
215
213
|
* failures; loader redirects surface via `FrameError.redirect`.
|
|
216
214
|
*/
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
const safe = resolveRedirectTarget(r.to, url, false);
|
|
250
|
-
if (!safe.ok) throw new FrameError(500, "unsafe redirect target");
|
|
251
|
-
throw new FrameError(r.status || 302, "frame failed", safe.to);
|
|
252
|
-
}
|
|
253
|
-
if (marker.__ilhaLoaderError === true) throw new FrameError(error.status || 500, "frame failed");
|
|
254
|
-
throw error;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
const render = entry.render();
|
|
258
|
-
if (typeof render !== "function") throw new FrameError(400, "unknown island");
|
|
259
|
-
const html = await runWithScope(request, () => render(props));
|
|
260
|
-
return String(html);
|
|
215
|
+
const frameFail = (status, message) => new FrameError({
|
|
216
|
+
status,
|
|
217
|
+
message: message ?? "frame failed"
|
|
218
|
+
});
|
|
219
|
+
/**
|
|
220
|
+
* Render a registered server island. Typed error channel: the only failure is
|
|
221
|
+
* `FrameError`; everything else is a defect surfaced as a 400 to the client.
|
|
222
|
+
*/
|
|
223
|
+
function renderServerIsland(id, request, runWithScope, incomingProps) {
|
|
224
|
+
return Effect.gen(function* () {
|
|
225
|
+
const entry = registry().get(id);
|
|
226
|
+
if (!entry) return yield* Effect.fail(frameFail(400, "unknown island"));
|
|
227
|
+
const render = entry.render();
|
|
228
|
+
if (typeof render !== "function") return yield* Effect.fail(frameFail(400, "unknown island"));
|
|
229
|
+
return yield* Effect.tryPromise({
|
|
230
|
+
try: async () => {
|
|
231
|
+
return await runWithScope(request, () => renderToStringOf(render, incomingProps));
|
|
232
|
+
},
|
|
233
|
+
catch: (error) => error instanceof FrameError ? error : frameFail(400, error instanceof Error ? error.message : void 0)
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
/** Convenience for non-Effect callers: run the render and resolve to a Result. */
|
|
238
|
+
function renderServerIslandResult(id, request, runWithScope, incomingProps) {
|
|
239
|
+
return Effect.runPromise(Effect.result(renderServerIsland(id, request, runWithScope, incomingProps)));
|
|
240
|
+
}
|
|
241
|
+
function renderToStringOf(render, props) {
|
|
242
|
+
const out = render(props);
|
|
243
|
+
return Promise.resolve(out).then((view) => typeof view === "string" ? view : renderToString(() => view, {
|
|
244
|
+
captureActions: true,
|
|
245
|
+
markers: false
|
|
246
|
+
}));
|
|
261
247
|
}
|
|
262
248
|
const FRAME_ENDPOINT = "/__ilha/frame";
|
|
263
|
-
/** Regular-page server loads: served through the loader-runner slot. */
|
|
264
|
-
const LOADER_ENDPOINT = "/__ilha/loader";
|
|
265
249
|
/** Max request body size — matches the dev middleware cap. */
|
|
266
250
|
const MAX_BODY = 16384;
|
|
267
251
|
/** Parent-island props on a frame POST. Missing is fine; anything else is 400. */
|
|
268
252
|
function parseFrameProps(value) {
|
|
269
253
|
if (value == null) return void 0;
|
|
270
|
-
if (typeof value !== "object" || Array.isArray(value)) throw new FrameError(
|
|
271
|
-
|
|
254
|
+
if (typeof value !== "object" || Array.isArray(value)) throw new FrameError({
|
|
255
|
+
status: 400,
|
|
256
|
+
message: "frame failed"
|
|
257
|
+
});
|
|
258
|
+
const sanitized = sanitizeSnapshotObject(value);
|
|
259
|
+
if (!sanitized) throw new FrameError({
|
|
260
|
+
status: 400,
|
|
261
|
+
message: "frame failed"
|
|
262
|
+
});
|
|
263
|
+
return sanitized;
|
|
272
264
|
}
|
|
273
265
|
function json(status, body) {
|
|
274
266
|
const env = frameEnvelope(status, body);
|
|
@@ -353,107 +345,50 @@ async function authorizeFrameRequest(request, options) {
|
|
|
353
345
|
};
|
|
354
346
|
}
|
|
355
347
|
async function ssr(request) {
|
|
356
|
-
let
|
|
348
|
+
let url;
|
|
357
349
|
try {
|
|
358
|
-
|
|
350
|
+
url = new URL(request.url);
|
|
359
351
|
} catch {
|
|
360
352
|
return json(400, { error: "frame failed" });
|
|
361
353
|
}
|
|
362
|
-
if (pathname !== "/__ilha/frame"
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
if (request.method !== "
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
}
|
|
375
|
-
const runner = getFrameLoaderRunner();
|
|
376
|
-
if (!runner) return json(404, {
|
|
377
|
-
kind: "error",
|
|
378
|
-
status: 404,
|
|
379
|
-
message: "not found"
|
|
380
|
-
});
|
|
381
|
-
const cl = request.headers.get("content-length");
|
|
382
|
-
if (cl && Number(cl) > 16384) return json(413, { error: "frame failed" });
|
|
383
|
-
let target = "/";
|
|
384
|
-
try {
|
|
385
|
-
target = new URL(request.url).searchParams.get("path") ?? "/";
|
|
386
|
-
} catch {
|
|
387
|
-
return json(400, {
|
|
388
|
-
kind: "error",
|
|
389
|
-
status: 400,
|
|
390
|
-
message: "bad request"
|
|
391
|
-
});
|
|
392
|
-
}
|
|
393
|
-
if (!isSafeFramePath(target)) return json(400, {
|
|
394
|
-
kind: "error",
|
|
395
|
-
status: 400,
|
|
396
|
-
message: "bad request"
|
|
354
|
+
if (url.pathname !== "/__ilha/frame") return void 0;
|
|
355
|
+
const program = Effect.gen(function* () {
|
|
356
|
+
const auth = getFrameAuth();
|
|
357
|
+
if (!isTrustedOrigin(request, auth)) return yield* Effect.fail(frameFail(403));
|
|
358
|
+
if (request.method !== "POST") return yield* Effect.fail(frameFail(405));
|
|
359
|
+
if (!(request.headers.get("content-type") ?? "").startsWith("application/json")) return yield* Effect.fail(frameFail(415));
|
|
360
|
+
const authorized = yield* Effect.tryPromise({
|
|
361
|
+
try: () => authorizeFrameRequest(request, {
|
|
362
|
+
defaultAction: auth?.defaultAction ?? "deny",
|
|
363
|
+
onGuardError: (error) => console.error("[ilha-router] frame guard failed:", error)
|
|
364
|
+
}),
|
|
365
|
+
catch: () => frameFail(403)
|
|
397
366
|
});
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
367
|
+
if (!authorized.ok) return yield* Effect.fail(frameFail(authorized.status));
|
|
368
|
+
let id;
|
|
369
|
+
let path = "/";
|
|
370
|
+
let incomingProps;
|
|
371
|
+
{
|
|
372
|
+
const text = yield* Effect.tryPromise({
|
|
373
|
+
try: () => readBodyBounded(request, MAX_BODY),
|
|
374
|
+
catch: () => frameFail(400)
|
|
404
375
|
});
|
|
405
|
-
if (
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
376
|
+
if (text === null) return yield* Effect.fail(frameFail(413));
|
|
377
|
+
try {
|
|
378
|
+
const body = JSON.parse(text);
|
|
379
|
+
id = String(body.id ?? "");
|
|
380
|
+
incomingProps = parseFrameProps(body.props);
|
|
381
|
+
if (typeof body.path === "string") {
|
|
382
|
+
if (!isSafeFramePath(body.path)) return yield* Effect.fail(frameFail(400));
|
|
383
|
+
path = body.path;
|
|
384
|
+
}
|
|
385
|
+
} catch {
|
|
386
|
+
return yield* Effect.fail(frameFail(400));
|
|
412
387
|
}
|
|
413
|
-
return json(200, result);
|
|
414
|
-
} catch (error) {
|
|
415
|
-
console.error("[ilha-router] loader endpoint failed:", error);
|
|
416
|
-
return json(500, {
|
|
417
|
-
kind: "error",
|
|
418
|
-
status: 500,
|
|
419
|
-
message: "loader failed"
|
|
420
|
-
});
|
|
421
388
|
}
|
|
422
|
-
|
|
423
|
-
if (request.method !== "POST") return json(405, { error: "frame failed" });
|
|
424
|
-
if (!(request.headers.get("content-type") ?? "").startsWith("application/json")) return json(415, { error: "frame failed" });
|
|
425
|
-
const authorized = await authorizeFrameRequest(request, {
|
|
426
|
-
defaultAction: auth?.defaultAction ?? "deny",
|
|
427
|
-
onGuardError: (error) => console.error("[ilha-router] frame guard failed:", error)
|
|
428
|
-
});
|
|
429
|
-
if (!authorized.ok) return json(authorized.status, { error: "frame failed" });
|
|
430
|
-
let id;
|
|
431
|
-
let path = "/";
|
|
432
|
-
let incomingProps;
|
|
433
|
-
try {
|
|
434
|
-
const text = await readBodyBounded(request, MAX_BODY);
|
|
435
|
-
if (text === null) return json(413, { error: "frame failed" });
|
|
436
|
-
const body = JSON.parse(text);
|
|
437
|
-
id = String(body.id ?? "");
|
|
438
|
-
incomingProps = parseFrameProps(body.props);
|
|
439
|
-
if (typeof body.path === "string") {
|
|
440
|
-
if (!isSafeFramePath(body.path)) return json(400, { error: "frame failed" });
|
|
441
|
-
path = body.path;
|
|
442
|
-
}
|
|
443
|
-
} catch {
|
|
444
|
-
return json(400, { error: "frame failed" });
|
|
445
|
-
}
|
|
446
|
-
try {
|
|
447
|
-
let origin;
|
|
448
|
-
try {
|
|
449
|
-
origin = new URL(request.url).origin;
|
|
450
|
-
} catch {
|
|
451
|
-
return json(400, { error: "frame failed" });
|
|
452
|
-
}
|
|
453
|
-
const headers = forwardIdentityHeaders(request.headers);
|
|
454
|
-
const scoped = new Request(new URL(path, origin), {
|
|
389
|
+
const scoped = new Request(frameScopedUrl(url.href, path), {
|
|
455
390
|
method: "POST",
|
|
456
|
-
headers
|
|
391
|
+
headers: forwardIdentityHeaders(request.headers)
|
|
457
392
|
});
|
|
458
393
|
for (const sym of Object.getOwnPropertySymbols(request)) {
|
|
459
394
|
if (Symbol.keyFor(sym) === void 0) continue;
|
|
@@ -461,23 +396,19 @@ async function ssr(request) {
|
|
|
461
396
|
scoped[sym] = request[sym];
|
|
462
397
|
} catch {}
|
|
463
398
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
});
|
|
469
|
-
} catch (error) {
|
|
470
|
-
if (error instanceof FrameError) {
|
|
399
|
+
return json(200, { html: yield* renderServerIsland(id, scoped, (scopedRequest, fn) => Promise.resolve(runWithIslandRequest(scopedRequest, fn)), incomingProps) });
|
|
400
|
+
});
|
|
401
|
+
return Effect.runPromise(Effect.map(Effect.result(program), Result.match({
|
|
402
|
+
onFailure: (error) => {
|
|
471
403
|
if (error.redirect) return json(error.status, { redirect: error.redirect });
|
|
472
404
|
if (error.status >= 500) console.error("[ilha-router] frame render failed:", error);
|
|
473
405
|
return json(error.status, { error: "frame failed" });
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
}
|
|
406
|
+
},
|
|
407
|
+
onSuccess: (response) => response
|
|
408
|
+
})));
|
|
478
409
|
}
|
|
479
410
|
/** Side-effect imports required alongside this handler. */
|
|
480
|
-
ssr.imports = ["ilha:pages/server"
|
|
411
|
+
ssr.imports = ["ilha:pages/server"];
|
|
481
412
|
|
|
482
413
|
//#endregion
|
|
483
|
-
export {
|
|
414
|
+
export { runWithIslandRequest as C, ssr as S, registerServerIsland as _, authorizeFrameRequest as a, setFrameAuth as b, frameScopedUrl as c, getServerIslandEntry as d, isSafeFramePath as f, readBodyBounded as g, parseFrameProps as h, __ilhaServerAction as i, getFrameAuth as l, json as m, FrameError as n, forwardIdentityHeaders as o, isTrustedOrigin as p, MAX_BODY as r, frameEnvelope as s, FRAME_ENDPOINT as t, getFrameGuard as u, renderServerIsland as v, setFrameGuard as x, renderServerIslandResult as y };
|
package/dist/ssr.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Production SSR
|
|
2
|
+
* Production SSR endpoint for server-owned islands.
|
|
3
3
|
*
|
|
4
4
|
* Default export is an oxidejs-style fetch middleware:
|
|
5
5
|
* `(request) => Response | undefined`. Returns `undefined` for any request it
|
|
@@ -9,16 +9,12 @@
|
|
|
9
9
|
* oxide({ middleware: ["@ilha/router/ssr"] });
|
|
10
10
|
* ```
|
|
11
11
|
*
|
|
12
|
-
* Serves
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* populated by self-registration code appended to `.server` modules.
|
|
16
|
-
* - `GET /__ilha/loader?path=…` — regular-page server loads via the loader
|
|
17
|
-
* runner (`setFrameLoaderRunner`, wired by the generated server module).
|
|
12
|
+
* Serves `POST /__ilha/frame` — re-renders a server island (JSON `{ id, path }`
|
|
13
|
+
* in, `{ html }` out). Renderers come from the process-global registry
|
|
14
|
+
* populated by self-registration code appended to `.server` modules.
|
|
18
15
|
*/
|
|
19
|
-
import "
|
|
20
|
-
import
|
|
21
|
-
import type { HeadInput } from "./head";
|
|
16
|
+
import * as Effect from "effect/Effect";
|
|
17
|
+
import * as Result from "effect/Result";
|
|
22
18
|
/**
|
|
23
19
|
* Server-frame state shared by the dev middleware and the production
|
|
24
20
|
* `@ilha/router/ssr` handler: the renderers registry (keyed by the public
|
|
@@ -33,25 +29,10 @@ import type { HeadInput } from "./head";
|
|
|
33
29
|
* additionally register their `load` and route pattern so frame handlers can
|
|
34
30
|
* run the loader with matched params.
|
|
35
31
|
*/
|
|
36
|
-
/** Loader context for server-page `load` — mirrors the router's shape. */
|
|
37
|
-
export interface FrameLoaderContext {
|
|
38
|
-
params: Record<string, string>;
|
|
39
|
-
request: Request;
|
|
40
|
-
url: URL;
|
|
41
|
-
signal: AbortSignal;
|
|
42
|
-
/** Contribute `<head>` data for this route. Safe to call multiple times. */
|
|
43
|
-
head: (input: HeadInput) => void;
|
|
44
|
-
}
|
|
45
|
-
export type ServerPageLoader = (ctx: FrameLoaderContext) => unknown;
|
|
46
32
|
/** A frame render: optionally preceded by running the page's `load`. */
|
|
47
33
|
export interface ServerIslandEntry {
|
|
48
34
|
/** Returns the renderState fn (`Symbol.for("ilha.renderState")` getter). */
|
|
49
35
|
render: () => unknown;
|
|
50
|
-
/** The module's `load` export — runs at frame time; its return value
|
|
51
|
-
* becomes the island's render props. */
|
|
52
|
-
load?: ServerPageLoader;
|
|
53
|
-
/** Route pattern for the page (`/user/:id`) — matches params for `load`. */
|
|
54
|
-
pattern?: string;
|
|
55
36
|
}
|
|
56
37
|
export type FrameGuard = (request: Request) => Response | void | Promise<Response | void>;
|
|
57
38
|
/**
|
|
@@ -63,14 +44,6 @@ export type FrameGuard = (request: Request) => Response | void | Promise<Respons
|
|
|
63
44
|
*/
|
|
64
45
|
export declare function setFrameGuard(guard: FrameGuard): void;
|
|
65
46
|
export declare function getFrameGuard(): FrameGuard | undefined;
|
|
66
|
-
/**
|
|
67
|
-
* Install a guard consulted only by `GET /__ilha/loader`. When absent, the
|
|
68
|
-
* loader endpoint falls back to `getFrameGuard()` for backwards compatibility.
|
|
69
|
-
* Prefer a dedicated loader guard so gating the loader endpoint is independent
|
|
70
|
-
* of frame rendering.
|
|
71
|
-
*/
|
|
72
|
-
export declare function setLoaderGuard(guard: FrameGuard): void;
|
|
73
|
-
export declare function getLoaderGuard(): FrameGuard | undefined;
|
|
74
47
|
/** Frame-authorization policy, installed via {@link setFrameAuth}. */
|
|
75
48
|
export interface FrameAuthPolicy {
|
|
76
49
|
/**
|
|
@@ -115,6 +88,13 @@ export declare function isTrustedOrigin(request: Request, policy: FrameAuthPolic
|
|
|
115
88
|
* check), bounded length. `false` for anything else.
|
|
116
89
|
*/
|
|
117
90
|
export declare function isSafeFramePath(path: string): boolean;
|
|
91
|
+
/**
|
|
92
|
+
* Build the scoped frame render URL from the incoming request URL and frame
|
|
93
|
+
* path. Absolute `incomingUrl` values supply the origin. Relative values (for
|
|
94
|
+
* example Vite's `req.url`) require an explicit trusted `serverOrigin` — never
|
|
95
|
+
* a client `Host` header.
|
|
96
|
+
*/
|
|
97
|
+
export declare function frameScopedUrl(incomingUrl: string, framePath: string, serverOrigin?: string): string;
|
|
118
98
|
/**
|
|
119
99
|
* Copy identity headers (cookie, authorization, user-agent) onto a fresh
|
|
120
100
|
* `Headers`. Accepts a `Headers` or a Node `IncomingHttpHeaders`-style plain
|
|
@@ -129,45 +109,30 @@ export interface FrameEnvelope {
|
|
|
129
109
|
body: string;
|
|
130
110
|
}
|
|
131
111
|
export declare function frameEnvelope(status: number, body: Record<string, unknown>): FrameEnvelope;
|
|
132
|
-
export type FrameLoaderRunner = (path: string, request?: Request) => Promise<{
|
|
133
|
-
kind: string;
|
|
134
|
-
data?: unknown;
|
|
135
|
-
headEntries?: unknown;
|
|
136
|
-
status?: number;
|
|
137
|
-
to?: string;
|
|
138
|
-
message?: string;
|
|
139
|
-
}>;
|
|
140
|
-
/**
|
|
141
|
-
* Install the handler backing `GET /__ilha/loader` in production. The
|
|
142
|
-
* generated `pages.server.ts` wires this to `pageRouter.runLoader`, so
|
|
143
|
-
* regular-page server loads get full route matching, layout chains, and
|
|
144
|
-
* redirect/error semantics. Dev and prod handlers share the slot.
|
|
145
|
-
*/
|
|
146
|
-
export declare function setFrameLoaderRunner(runner: FrameLoaderRunner): void;
|
|
147
|
-
export declare function getFrameLoaderRunner(): FrameLoaderRunner | undefined;
|
|
148
112
|
/** @internal Brand an exported server action with its generated RPC transport key. */
|
|
149
|
-
export declare function __ilhaServerAction<A extends unknown[], R>(key: string, fn: (...args: A) => R):
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}): void;
|
|
113
|
+
export declare function __ilhaServerAction<A extends unknown[], R>(key: string, fn: (...args: A) => R): ((...args: A) => R) & {
|
|
114
|
+
with: (...args: A) => (...ev: unknown[]) => R;
|
|
115
|
+
};
|
|
116
|
+
export declare function registerServerIsland(id: string, render: () => unknown): void;
|
|
154
117
|
export declare function getServerIslandEntry(id: string): ServerIslandEntry | undefined;
|
|
155
|
-
|
|
156
|
-
|
|
118
|
+
declare const FrameError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
119
|
+
readonly _tag: "FrameError";
|
|
120
|
+
} & Readonly<A>;
|
|
121
|
+
/** Client-facing frame failure. `redirect` carries a same-origin redirect target. */
|
|
122
|
+
export declare class FrameError extends FrameError_base<{
|
|
157
123
|
status: number;
|
|
124
|
+
message?: string;
|
|
158
125
|
redirect?: string;
|
|
159
|
-
|
|
126
|
+
}> {
|
|
160
127
|
}
|
|
161
128
|
/**
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
* caller's scope. Throws `FrameError` with an HTTP status for client-facing
|
|
165
|
-
* failures; loader redirects surface via `FrameError.redirect`.
|
|
129
|
+
* Render a registered server island. Typed error channel: the only failure is
|
|
130
|
+
* `FrameError`; everything else is a defect surfaced as a 400 to the client.
|
|
166
131
|
*/
|
|
167
|
-
export declare function renderServerIsland(id: string, request: Request, runWithScope: <T>(request: Request, fn: () => T) => T | Promise<T>,
|
|
132
|
+
export declare function renderServerIsland(id: string, request: Request, runWithScope: <T>(request: Request, fn: () => T) => T | Promise<T>, incomingProps?: Record<string, unknown>): Effect.Effect<string, FrameError>;
|
|
133
|
+
/** Convenience for non-Effect callers: run the render and resolve to a Result. */
|
|
134
|
+
export declare function renderServerIslandResult(id: string, request: Request, runWithScope: <T>(request: Request, fn: () => T) => T | Promise<T>, incomingProps?: Record<string, unknown>): Promise<Result.Result<string, FrameError>>;
|
|
168
135
|
export declare const FRAME_ENDPOINT = "/__ilha/frame";
|
|
169
|
-
/** Regular-page server loads: served through the loader-runner slot. */
|
|
170
|
-
export declare const LOADER_ENDPOINT = "/__ilha/loader";
|
|
171
136
|
/** Max request body size — matches the dev middleware cap. */
|
|
172
137
|
export declare const MAX_BODY: number;
|
|
173
138
|
/** Parent-island props on a frame POST. Missing is fine; anything else is 400. */
|
package/dist/ssr.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { S as ssr, _ as registerServerIsland, a as authorizeFrameRequest, b as setFrameAuth, c as frameScopedUrl, d as getServerIslandEntry, f as isSafeFramePath, g as readBodyBounded, h as parseFrameProps, i as __ilhaServerAction, l as getFrameAuth, m as json, n as FrameError, o as forwardIdentityHeaders, p as isTrustedOrigin, r as MAX_BODY, s as frameEnvelope, t as FRAME_ENDPOINT, u as getFrameGuard, v as renderServerIsland, x as setFrameGuard, y as renderServerIslandResult } from "./ssr-WA3bSsCo.js";
|
|
2
2
|
|
|
3
|
-
export { FRAME_ENDPOINT, FrameError,
|
|
3
|
+
export { FRAME_ENDPOINT, FrameError, MAX_BODY, __ilhaServerAction, authorizeFrameRequest, ssr as default, forwardIdentityHeaders, frameEnvelope, frameScopedUrl, getFrameAuth, getFrameGuard, getServerIslandEntry, isSafeFramePath, isTrustedOrigin, json, parseFrameProps, readBodyBounded, registerServerIsland, renderServerIsland, renderServerIslandResult, setFrameAuth, setFrameGuard };
|
package/dist/vite.js
CHANGED