@ilha/router 0.10.2 → 0.10.4

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/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { A as wrapLayout, C as routePath, D as useContext, E as src_default, F as getHistoryMode, I as setHistoryMode, O as useRoute, P as httpResponse, S as routeParams, T as router, _ as prefetch, a as RouterView, b as resolveRedirectTarget, c as composeLoaders, d as error, f as invalidate, g as navigating, h as navigate, i as RouterLink, k as wrapError, l as defineLayout, m as loader, n as LoaderError, o as afterNavigate, p as isActive, r as Redirect, s as beforeNavigate, t as LOADER_ENDPOINT, u as enableLinkInterception, v as prime, w as routeSearch, x as routeHash, y as redirect } from "./src-B5dHU24f.js";
1
+ import { A as wrapLayout, C as routePath, D as useContext, E as src_default, F as getHistoryMode, I as setHistoryMode, O as useRoute, P as httpResponse, S as routeParams, T as router, _ as prefetch, a as RouterView, b as resolveRedirectTarget, c as composeLoaders, d as error, f as invalidate, g as navigating, h as navigate, i as RouterLink, k as wrapError, l as defineLayout, m as loader, n as LoaderError, o as afterNavigate, p as isActive, r as Redirect, s as beforeNavigate, t as LOADER_ENDPOINT, u as enableLinkInterception, v as prime, w as routeSearch, x as routeHash, y as redirect } from "./src-PCXNZfql.js";
2
2
  import { o as head, s as serializeHead } from "./snapshot-CsEaY6h_.js";
3
3
 
4
4
  export { LOADER_ENDPOINT, LoaderError, Redirect, RouterLink, RouterView, afterNavigate, beforeNavigate, composeLoaders, src_default as default, defineLayout, enableLinkInterception, error, getHistoryMode, head, httpResponse, invalidate, isActive, loader, navigate, navigating, prefetch, prime, redirect, resolveRedirectTarget, routeHash, routeParams, routePath, routeSearch, router, serializeHead, setHistoryMode, useContext, useRoute, wrapError, wrapLayout };
@@ -1,4 +1,4 @@
1
- import { C as setLoaderGuard, T as runWithIslandRequest, b as setFrameAuth, c as frameEnvelope, m as isSafeFramePath, n as FrameError, o as authorizeFrameRequest, u as getFrameGuard, x as setFrameGuard, y as renderServerIsland } from "./ssr-CGcMUP1G.js";
1
+ import { E as runWithIslandRequest, S as setFrameGuard, _ as parseFrameProps, b as renderServerIsland, c as frameEnvelope, m as isSafeFramePath, n as FrameError, o as authorizeFrameRequest, u as getFrameGuard, w as setLoaderGuard, x as setFrameAuth } from "./ssr-BTmqfAp9.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
4
  import { createUnplugin } from "unplugin";
@@ -198,7 +198,7 @@ function generateServerIslandModule(spec, scan) {
198
198
  if (actions.length) wiring.push(`actions: { ${actions.join(", ")} }`);
199
199
  if (scan.clientRefs.length) wiring.push(`children: { ${scan.clientRefs.map((ref, index) => `${JSON.stringify(ref.id)}: $$child${index}`).join(", ")} }`);
200
200
  const id = serverIslandPublicId(spec, island.name);
201
- wiring.push(`frame: () => fetch("/__ilha/frame", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ id: ${JSON.stringify(id)}, path: location.pathname + location.search }) }).then((r) => { if (!r.ok) throw new Error("frame failed"); return r.json(); }).then((j) => { if (j.redirect) { location.assign(j.redirect); throw new Error("frame redirected"); } __ilhaApplyHead(j.head); return j.html; })`);
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(j.redirect); throw new Error("frame redirected"); } __ilhaApplyHead(j.head); return j.html; })`);
202
202
  if (scan.clientLoader) wiring.push(`clientLoader: () => $$call("load", [])`);
203
203
  const call = `__ilhaServerIsland(${JSON.stringify(id)}, ${JSON.stringify(island.as)}, { ${wiring.join(", ")} })`;
204
204
  if (island.name === "default") lines.push(`export default ${call};`);
@@ -1046,6 +1046,7 @@ const pagesFactory = (options = {}) => {
1046
1046
  }
1047
1047
  try {
1048
1048
  const body = JSON.parse(Buffer.concat(chunks).toString("utf8"));
1049
+ const incomingProps = parseFrameProps(body.props);
1049
1050
  const target = serverIslands.get(body.id ?? "");
1050
1051
  if (!target) throw new Error("unknown island");
1051
1052
  let framePath = "/";
@@ -1068,7 +1069,7 @@ const pagesFactory = (options = {}) => {
1068
1069
  headers
1069
1070
  });
1070
1071
  let head;
1071
- const html = await renderServerIsland(body.id ?? "", request, (r, fn) => runWithIslandRequest(r, fn), (entries) => head = entries);
1072
+ const html = await renderServerIsland(body.id ?? "", request, (r, fn) => runWithIslandRequest(r, fn), (entries) => head = entries, incomingProps);
1072
1073
  const env = frameEnvelope(200, {
1073
1074
  html: String(html),
1074
1075
  head
package/dist/rsbuild.js CHANGED
@@ -1,4 +1,4 @@
1
- import { t as ilhaPages } from "./plugin-Bh0y5kkI.js";
1
+ import { t as ilhaPages } from "./plugin-BQil-7B_.js";
2
2
 
3
3
  //#region src/rsbuild.ts
4
4
  /** Rsbuild plugin — use via `@ilha/router/rsbuild`. */
@@ -22,8 +22,8 @@ export interface ServerIslandWiring {
22
22
  /** Action key → client transport. Event payloads are not serializable;
23
23
  * handlers receive `undefined` and should read island state instead. */
24
24
  actions?: Record<string, (payload?: unknown) => unknown>;
25
- /** Frame transport: re-renders the island from server-owned state. */
26
- frame?: () => unknown;
25
+ /** Frame transport: re-renders the island with the latest parent props. */
26
+ frame?: (props?: Record<string, unknown>) => unknown;
27
27
  /** RPC transport for the module's `loader.client` export — invoked once
28
28
  * when the view hydrates. Side-effect loader on server pages. */
29
29
  clientLoader?: () => unknown;
@@ -51,7 +51,7 @@ interface IslandCallShape {
51
51
  key: string;
52
52
  }
53
53
  export type ServerIslandCallable = Record<symbol, unknown> & ((props?: Record<string, unknown>) => string) & {
54
- mount: (host: Element) => () => void;
54
+ mount: (host: Element, props?: Record<string, unknown>) => () => void;
55
55
  toString: () => string;
56
56
  key: (slotKey: string) => (props?: Record<string, unknown>) => IslandCallShape;
57
57
  };
@@ -48,7 +48,43 @@ function belongsToHost(host, candidate) {
48
48
  }
49
49
  return el === host;
50
50
  }
51
- function hydrateServerIsland(host, id, wiring) {
51
+ /** Dev cold-start can race a warming `virtual:oxide/actions` graph — retry briefly. */
52
+ async function startServerStream(fn, signal) {
53
+ let last;
54
+ for (let attempt = 0; attempt < 4; attempt++) {
55
+ if (signal.aborted) throw last ?? new DOMException("The operation was aborted.", "AbortError");
56
+ let gen;
57
+ try {
58
+ gen = await fn(signal);
59
+ const first = await gen.next();
60
+ return {
61
+ gen,
62
+ first
63
+ };
64
+ } catch (err) {
65
+ last = err;
66
+ if (gen) Promise.resolve(gen.return?.(void 0)).catch(() => {});
67
+ if (signal.aborted) throw err;
68
+ const msg = String(err);
69
+ if (!msg.includes("404") && !msg.includes("503")) throw err;
70
+ if (attempt === 3) throw err;
71
+ await new Promise((resolve, reject) => {
72
+ const timer = setTimeout(() => {
73
+ signal.removeEventListener("abort", onAbort);
74
+ resolve();
75
+ }, 50 * (attempt + 1));
76
+ const onAbort = () => {
77
+ clearTimeout(timer);
78
+ reject(err);
79
+ };
80
+ signal.addEventListener("abort", onAbort, { once: true });
81
+ if (signal.aborted) onAbort();
82
+ });
83
+ }
84
+ }
85
+ throw last;
86
+ }
87
+ function hydrateServerIsland(host, id, wiring, props) {
52
88
  const controller = new AbortController();
53
89
  const cleanups = [];
54
90
  cleanups.push(() => controller.abort());
@@ -61,12 +97,14 @@ function hydrateServerIsland(host, id, wiring) {
61
97
  }
62
98
  }
63
99
  const frame = wiring.frame;
100
+ let currentProps = props;
64
101
  let repaintChain = Promise.resolve();
65
102
  const scheduleRepaint = () => {
66
103
  if (!frame || controller.signal.aborted) return;
104
+ const sent = currentProps;
67
105
  repaintChain = repaintChain.then(async () => {
68
106
  if (controller.signal.aborted || !host.isConnected) return;
69
- const html = await frame();
107
+ const html = await frame(sent);
70
108
  if (controller.signal.aborted || typeof html !== "string") return;
71
109
  morph(host, html);
72
110
  syncChildren();
@@ -157,13 +195,15 @@ function hydrateServerIsland(host, id, wiring) {
157
195
  syncChildren();
158
196
  for (const [key, fn] of Object.entries(wiring.streams ?? {})) (async () => {
159
197
  try {
160
- const gen = await fn(controller.signal);
198
+ const { gen, first } = await startServerStream(fn, controller.signal);
161
199
  try {
200
+ let step = first;
162
201
  for (;;) {
163
- const { done, value } = await gen.next();
202
+ const { done, value } = step;
164
203
  if (controller.signal.aborted || done) break;
165
204
  state[key] = value;
166
205
  scheduleRepaint();
206
+ step = await gen.next();
167
207
  }
168
208
  } catch (err) {
169
209
  if (!controller.signal.aborted && err?.name !== "AbortError") console.error(`[ilha-router] stream "${key}" failed:`, err);
@@ -187,7 +227,10 @@ function hydrateServerIsland(host, id, wiring) {
187
227
  for (const cleanup of cleanups) cleanup();
188
228
  cleanups.length = 0;
189
229
  },
190
- updateProps: () => {}
230
+ updateProps: (next) => {
231
+ currentProps = next;
232
+ scheduleRepaint();
233
+ }
191
234
  };
192
235
  }
193
236
  function __ilhaServerIsland(id, as, wiring = {}) {
@@ -208,8 +251,8 @@ function __ilhaServerIsland(id, as, wiring = {}) {
208
251
  key: slotKey
209
252
  });
210
253
  };
211
- island[ISLAND_MOUNT_INTERNAL] = (host) => hydrateServerIsland(host, id, wiring);
212
- island.mount = (host) => hydrateServerIsland(host, id, wiring).unmount;
254
+ island[ISLAND_MOUNT_INTERNAL] = (host, mountProps) => hydrateServerIsland(host, id, wiring, mountProps);
255
+ island.mount = (host, mountProps) => hydrateServerIsland(host, id, wiring, mountProps).unmount;
213
256
  return island;
214
257
  }
215
258
 
@@ -1017,7 +1017,7 @@ function routePath(value) {
1017
1017
  const store = activeRouteStore();
1018
1018
  if (arguments.length > 0) {
1019
1019
  if (store) return store.path = value;
1020
- return _routePathSig(value), value;
1020
+ return _routePathSig.set(value), value;
1021
1021
  }
1022
1022
  return store ? store.path : _routePathSig();
1023
1023
  }
@@ -1026,7 +1026,7 @@ function routeParams(value) {
1026
1026
  const store = activeRouteStore();
1027
1027
  if (arguments.length > 0) {
1028
1028
  if (store) return store.params = value;
1029
- return _routeParamsSig(value), value;
1029
+ return _routeParamsSig.set(value), value;
1030
1030
  }
1031
1031
  return store ? store.params : _routeParamsSig();
1032
1032
  }
@@ -1035,7 +1035,7 @@ function routeSearch(value) {
1035
1035
  const store = activeRouteStore();
1036
1036
  if (arguments.length > 0) {
1037
1037
  if (store) return store.search = value;
1038
- return _routeSearchSig(value), value;
1038
+ return _routeSearchSig.set(value), value;
1039
1039
  }
1040
1040
  return store ? store.search : _routeSearchSig();
1041
1041
  }
@@ -1044,7 +1044,7 @@ function routeHash(value) {
1044
1044
  const store = activeRouteStore();
1045
1045
  if (arguments.length > 0) {
1046
1046
  if (store) return store.hash = value;
1047
- return _routeHashSig(value), value;
1047
+ return _routeHashSig.set(value), value;
1048
1048
  }
1049
1049
  return store ? store.hash : _routeHashSig();
1050
1050
  }
@@ -1066,13 +1066,13 @@ function invalidate() {
1066
1066
  /** Mark a navigation as started; returns an idempotent settle callback. */
1067
1067
  function beginNavigation() {
1068
1068
  if (!isBrowser) return () => {};
1069
- _navigatingSig(_navigatingSig() + 1);
1069
+ _navigatingSig.set(_navigatingSig() + 1);
1070
1070
  document.documentElement.setAttribute("data-router-pending", "");
1071
1071
  let done = false;
1072
1072
  return () => {
1073
1073
  if (done) return;
1074
1074
  done = true;
1075
- _navigatingSig(Math.max(0, _navigatingSig() - 1));
1075
+ _navigatingSig.set(Math.max(0, _navigatingSig() - 1));
1076
1076
  if (_navigatingSig() === 0) document.documentElement.removeAttribute("data-router-pending");
1077
1077
  };
1078
1078
  }
@@ -1103,8 +1103,8 @@ function activeIsland(value) {
1103
1103
  if (arguments.length > 0) {
1104
1104
  const next = value ?? null;
1105
1105
  if (store) return store.island = next;
1106
- if (next === null) _activeIslandSig(null);
1107
- else _activeIslandSig(() => next);
1106
+ if (next === null) _activeIslandSig.set(null);
1107
+ else _activeIslandSig.set(next);
1108
1108
  return next;
1109
1109
  }
1110
1110
  return store ? store.island : _activeIslandSig();
@@ -1,4 +1,4 @@
1
- import { M as parsePattern, N as safeDecode, b as resolveRedirectTarget, j as matchSegments } from "./src-B5dHU24f.js";
1
+ import { M as parsePattern, N as safeDecode, b as resolveRedirectTarget, j as matchSegments } from "./src-PCXNZfql.js";
2
2
  import "ilha";
3
3
  import { bindServerAction, setServerManifestSerializer } from "ilha/internal";
4
4
  import { AsyncLocalStorage } from "node:async_hooks";
@@ -214,10 +214,10 @@ function matchPatternParams(pattern, pathname) {
214
214
  * caller's scope. Throws `FrameError` with an HTTP status for client-facing
215
215
  * failures; loader redirects surface via `FrameError.redirect`.
216
216
  */
217
- async function renderServerIsland(id, request, runWithScope, onHead) {
217
+ async function renderServerIsland(id, request, runWithScope, onHead, incomingProps) {
218
218
  const entry = registry().get(id);
219
219
  if (!entry) throw new FrameError(400, "unknown island");
220
- let props;
220
+ let props = incomingProps;
221
221
  if (entry.load) {
222
222
  let url;
223
223
  try {
@@ -264,6 +264,12 @@ const FRAME_ENDPOINT = "/__ilha/frame";
264
264
  const LOADER_ENDPOINT = "/__ilha/loader";
265
265
  /** Max request body size — matches the dev middleware cap. */
266
266
  const MAX_BODY = 16384;
267
+ /** Parent-island props on a frame POST. Missing is fine; anything else is 400. */
268
+ function parseFrameProps(value) {
269
+ if (value == null) return void 0;
270
+ if (typeof value !== "object" || Array.isArray(value)) throw new FrameError(400, "frame failed");
271
+ return value;
272
+ }
267
273
  function json(status, body) {
268
274
  const env = frameEnvelope(status, body);
269
275
  return new Response(env.body, {
@@ -423,11 +429,13 @@ async function ssr(request) {
423
429
  if (!authorized.ok) return json(authorized.status, { error: "frame failed" });
424
430
  let id;
425
431
  let path = "/";
432
+ let incomingProps;
426
433
  try {
427
434
  const text = await readBodyBounded(request, MAX_BODY);
428
435
  if (text === null) return json(413, { error: "frame failed" });
429
436
  const body = JSON.parse(text);
430
437
  id = String(body.id ?? "");
438
+ incomingProps = parseFrameProps(body.props);
431
439
  if (typeof body.path === "string") {
432
440
  if (!isSafeFramePath(body.path)) return json(400, { error: "frame failed" });
433
441
  path = body.path;
@@ -455,7 +463,7 @@ async function ssr(request) {
455
463
  }
456
464
  let head;
457
465
  return json(200, {
458
- html: await renderServerIsland(id, scoped, (scopedRequest, fn) => Promise.resolve(runWithIslandRequest(scopedRequest, fn)), (entries) => head = entries),
466
+ html: await renderServerIsland(id, scoped, (scopedRequest, fn) => Promise.resolve(runWithIslandRequest(scopedRequest, fn)), (entries) => head = entries, incomingProps),
459
467
  head
460
468
  });
461
469
  } catch (error) {
@@ -472,4 +480,4 @@ async function ssr(request) {
472
480
  ssr.imports = ["ilha:pages/server", "ilha:loaders"];
473
481
 
474
482
  //#endregion
475
- export { setLoaderGuard as C, setFrameLoaderRunner as S, runWithIslandRequest as T, readBodyBounded as _, __ilhaServerAction as a, setFrameAuth as b, frameEnvelope as c, getFrameLoaderRunner as d, getLoaderGuard as f, json as g, isTrustedOrigin as h, MAX_BODY as i, getFrameAuth as l, isSafeFramePath as m, FrameError as n, authorizeFrameRequest as o, getServerIslandEntry as p, LOADER_ENDPOINT as r, forwardIdentityHeaders as s, FRAME_ENDPOINT as t, getFrameGuard as u, registerServerIsland as v, ssr as w, setFrameGuard as x, renderServerIsland as y };
483
+ export { setFrameLoaderRunner as C, runWithIslandRequest as E, setFrameGuard as S, ssr as T, parseFrameProps as _, __ilhaServerAction as a, renderServerIsland as b, frameEnvelope as c, getFrameLoaderRunner as d, getLoaderGuard as f, json as g, isTrustedOrigin as h, MAX_BODY as i, getFrameAuth as l, isSafeFramePath as m, FrameError as n, authorizeFrameRequest as o, getServerIslandEntry as p, LOADER_ENDPOINT as r, forwardIdentityHeaders as s, FRAME_ENDPOINT as t, getFrameGuard as u, readBodyBounded as v, setLoaderGuard as w, setFrameAuth as x, registerServerIsland as y };
package/dist/ssr.d.ts CHANGED
@@ -164,12 +164,14 @@ export declare class FrameError extends Error {
164
164
  * caller's scope. Throws `FrameError` with an HTTP status for client-facing
165
165
  * failures; loader redirects surface via `FrameError.redirect`.
166
166
  */
167
- export declare function renderServerIsland(id: string, request: Request, runWithScope: <T>(request: Request, fn: () => T) => T | Promise<T>, onHead?: (entries: HeadInput[]) => void): Promise<string>;
167
+ export declare function renderServerIsland(id: string, request: Request, runWithScope: <T>(request: Request, fn: () => T) => T | Promise<T>, onHead?: (entries: HeadInput[]) => void, incomingProps?: Record<string, unknown>): Promise<string>;
168
168
  export declare const FRAME_ENDPOINT = "/__ilha/frame";
169
169
  /** Regular-page server loads: served through the loader-runner slot. */
170
170
  export declare const LOADER_ENDPOINT = "/__ilha/loader";
171
171
  /** Max request body size — matches the dev middleware cap. */
172
172
  export declare const MAX_BODY: number;
173
+ /** Parent-island props on a frame POST. Missing is fine; anything else is 400. */
174
+ export declare function parseFrameProps(value: unknown): Record<string, unknown> | undefined;
173
175
  export declare function json(status: number, body: Record<string, unknown>): Response;
174
176
  /**
175
177
  * Read a request body as UTF-8, streaming it with a hard byte cap. Returns
package/dist/ssr.js CHANGED
@@ -1,3 +1,3 @@
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-CGcMUP1G.js";
1
+ import { C as setFrameLoaderRunner, S as setFrameGuard, T as ssr, _ as parseFrameProps, a as __ilhaServerAction, b as renderServerIsland, 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 readBodyBounded, w as setLoaderGuard, x as setFrameAuth, y as registerServerIsland } from "./ssr-BTmqfAp9.js";
2
2
 
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 };
3
+ export { FRAME_ENDPOINT, FrameError, LOADER_ENDPOINT, MAX_BODY, __ilhaServerAction, authorizeFrameRequest, ssr as default, forwardIdentityHeaders, frameEnvelope, getFrameAuth, getFrameGuard, getFrameLoaderRunner, getLoaderGuard, getServerIslandEntry, isSafeFramePath, isTrustedOrigin, json, parseFrameProps, readBodyBounded, registerServerIsland, renderServerIsland, setFrameAuth, setFrameGuard, setFrameLoaderRunner, setLoaderGuard };
package/dist/vite.js CHANGED
@@ -1,4 +1,4 @@
1
- import { t as ilhaPages } from "./plugin-Bh0y5kkI.js";
1
+ import { t as ilhaPages } from "./plugin-BQil-7B_.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.10.2",
3
+ "version": "0.10.4",
4
4
  "description": "A tiny SPA router for Ilha",
5
5
  "keywords": [
6
6
  "frontend",
@@ -75,10 +75,10 @@
75
75
  "unplugin": "3.3.0"
76
76
  },
77
77
  "devDependencies": {
78
- "ilha": "0.12.1",
78
+ "ilha": "0.13.0",
79
79
  "vite": "^8.2.2"
80
80
  },
81
81
  "peerDependencies": {
82
- "ilha": ">=0.12.1"
82
+ "ilha": ">=0.13.0"
83
83
  }
84
84
  }