@ilha/router 0.11.1 → 0.11.3

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.
@@ -1,4 +1,4 @@
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";
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-CV_vNpS3.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 * as Result from "effect/Result";
@@ -8,6 +8,22 @@ import { createHash } from "node:crypto";
8
8
 
9
9
  //#region src/server-islands.ts
10
10
  /**
11
+ * Build-time support for server-defined islands. The plugin scans
12
+ * `*.server.ts(x)` modules for island exports (`export const X = ilha…`),
13
+ * generates a client virtual module per file that re-creates those exports as
14
+ * proxies wired to tacho stubs, and rewrites client-graph import sites so
15
+ * island bindings resolve to the proxy while everything else keeps flowing
16
+ * through oxidejs's tacho stub replacement.
17
+ */
18
+ /** Oxide RPC client key — basename without `.server.*` (oxide indexes by module name). */
19
+ function serverModuleRpcKey(spec) {
20
+ return basename(spec).replace(/\.server\.(?:[jt]sx?)$/i, "");
21
+ }
22
+ /** Client repaint group key — full normalized path so same-basename files stay distinct. */
23
+ function serverModuleRepaintKey(spec) {
24
+ return spec.replace(/\\/g, "/").replace(/\.server\.(?:[jt]sx?)$/i, "");
25
+ }
26
+ /**
11
27
  * Replace identity `action(` wrappers of exported server actions with the
12
28
  * capture-aware shim on ALREADY-COMPILED module code. Must run inside the
13
29
  * SSR transform so upstream JSX/TS output is preserved.
@@ -165,11 +181,12 @@ function serverIslandPublicId(spec, name) {
165
181
  return createHash("sha256").update(`${spec}#${name}`).digest("base64url");
166
182
  }
167
183
  function generateServerIslandModule(spec, scan) {
168
- const moduleKey = basename(spec).replace(/\.server\.(?:[jt]sx?)$/i, "");
184
+ const rpcKey = serverModuleRpcKey(spec);
185
+ const repaintKey = serverModuleRepaintKey(spec);
169
186
  const lines = [
170
187
  `import { client as $$rpc } from "virtual:oxide/client";`,
171
188
  `import { __ilhaApplyHead, __ilhaServerIsland } from "@ilha/router/server-island";`,
172
- `const $$call = (method, args) => { const opts = args.at(-1); return opts && typeof opts === "object" && opts.signal instanceof AbortSignal && Object.keys(opts).length === 1 ? $$rpc[${JSON.stringify(moduleKey)}][method](args.slice(0, -1), opts) : $$rpc[${JSON.stringify(moduleKey)}][method](args); };`,
189
+ `const $$call = (method, args) => { const opts = args.at(-1); return opts && typeof opts === "object" && opts.signal instanceof AbortSignal && Object.keys(opts).length === 1 ? $$rpc[${JSON.stringify(rpcKey)}][method](...args.slice(0, -1), opts) : $$rpc[${JSON.stringify(rpcKey)}][method](...args); };`,
173
190
  ...scan.clientRefs.map((ref, index) => ref.imported === "default" ? `import $$child${index} from ${JSON.stringify(ref.spec)};` : `import { ${ref.imported} as $$child${index} } from ${JSON.stringify(ref.spec)};`)
174
191
  ];
175
192
  for (const name of scan.exports) if (!scan.islands.some((island) => island.name === name)) lines.push(`export const ${name} = (...args) => $$call(${JSON.stringify(name)}, args);`);
@@ -186,7 +203,7 @@ function generateServerIslandModule(spec, scan) {
186
203
  if (scan.clientRefs.length) wiring.push(`children: { ${scan.clientRefs.map((ref, index) => `${JSON.stringify(ref.id)}: $$child${index}`).join(", ")} }`);
187
204
  const id = serverIslandPublicId(spec, island.name);
188
205
  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; })`);
189
- const call = `__ilhaServerIsland(${JSON.stringify(id)}, ${JSON.stringify(island.as)}, { ${wiring.join(", ")} })`;
206
+ const call = `__ilhaServerIsland(${JSON.stringify(id)}, ${JSON.stringify(island.as)}, { ${wiring.join(", ")} }, ${JSON.stringify(repaintKey)})`;
190
207
  if (island.name === "default") lines.push(`export default ${call};`);
191
208
  else lines.push(`export const ${island.name} = ${call};`);
192
209
  }
package/dist/rsbuild.js CHANGED
@@ -1,4 +1,4 @@
1
- import { t as ilhaPages } from "./plugin-C3AAI3e_.js";
1
+ import { t as ilhaPages } from "./plugin-Bfnn44nu.js";
2
2
 
3
3
  //#region src/rsbuild.ts
4
4
  /** Rsbuild plugin — use via `@ilha/router/rsbuild`. */
@@ -15,6 +15,8 @@
15
15
  * `data-ilha-actions` manifest emitted by `hydratable()`.
16
16
  */
17
17
  declare const ISLAND_CALL: unique symbol;
18
+ /** @internal Repaint every mounted island from the same `*.server` module. */
19
+ export declare function __ilhaRepaintServerModule(moduleKey: string): void;
18
20
  export type ServerStreamFn = (signal: AbortSignal) => AsyncGenerator<unknown> | Generator<unknown>;
19
21
  export interface ServerIslandWiring {
20
22
  /** Stream key → client transport. The plugin wires these to tacho stubs. */
@@ -52,5 +54,5 @@ export type ServerIslandCallable = Record<symbol, unknown> & ((props?: Record<st
52
54
  toString: () => string;
53
55
  key: (slotKey: string) => (props?: Record<string, unknown>) => IslandCallShape;
54
56
  };
55
- export declare function __ilhaServerIsland(id: string, as: string, wiring?: ServerIslandWiring): ServerIslandCallable;
57
+ export declare function __ilhaServerIsland(id: string, as: string, wiring?: ServerIslandWiring, moduleKey?: string): ServerIslandCallable;
56
58
  export {};
@@ -27,6 +27,23 @@ const EVENT_SENTINEL_ATTR = "data-ilha-on";
27
27
  const ACTIONS_ATTR = "data-ilha-actions";
28
28
  const PROPS_ATTR = "data-ilha-props";
29
29
  const CLIENT_REF_ATTR = "data-ilha-client-ref";
30
+ const moduleRepaints = /* @__PURE__ */ new Map();
31
+ /** @internal Repaint every mounted island from the same `*.server` module. */
32
+ function __ilhaRepaintServerModule(moduleKey) {
33
+ for (const repaint of moduleRepaints.get(moduleKey) ?? []) repaint();
34
+ }
35
+ function linkServerModuleRepaints(moduleKey, repaint) {
36
+ let set = moduleRepaints.get(moduleKey);
37
+ if (!set) {
38
+ set = /* @__PURE__ */ new Set();
39
+ moduleRepaints.set(moduleKey, set);
40
+ }
41
+ set.add(repaint);
42
+ return () => {
43
+ set.delete(repaint);
44
+ if (set.size === 0) moduleRepaints.delete(moduleKey);
45
+ };
46
+ }
30
47
  /** @internal Apply head entries returned with a server-page frame. */
31
48
  function __ilhaApplyHead(entries) {
32
49
  if (Array.isArray(entries)) applyHeadEntriesToDocument(entries);
@@ -85,7 +102,7 @@ async function startServerStream(fn, signal) {
85
102
  }
86
103
  throw last;
87
104
  }
88
- function hydrateServerIsland(host, id, wiring, props) {
105
+ function hydrateServerIsland(host, id, wiring, props, moduleKey) {
89
106
  const controller = new AbortController();
90
107
  const cleanups = [];
91
108
  cleanups.push(() => controller.abort());
@@ -115,6 +132,11 @@ function hydrateServerIsland(host, id, wiring, props) {
115
132
  if (!controller.signal.aborted) console.error(`[ilha-router] frame render failed for "${id}":`, err);
116
133
  });
117
134
  };
135
+ const repaintModule = () => {
136
+ if (moduleKey) __ilhaRepaintServerModule(moduleKey);
137
+ else scheduleRepaint();
138
+ };
139
+ if (moduleKey) cleanups.push(linkServerModuleRepaints(moduleKey, scheduleRepaint));
118
140
  const attached = [];
119
141
  const readManifest = () => {
120
142
  const raw = host.querySelector(`:scope > template[${ACTIONS_ATTR}], template[${ACTIONS_ATTR}]`)?.getAttribute(ACTIONS_ATTR) ?? host.getAttribute(ACTIONS_ATTR) ?? null;
@@ -145,7 +167,7 @@ function hydrateServerIsland(host, id, wiring, props) {
145
167
  const action = actionKey == null ? void 0 : wiring.actions?.[actionKey];
146
168
  if (!action) continue;
147
169
  const listener = () => {
148
- Promise.resolve(action(...callArgs)).then(() => scheduleRepaint()).catch((err) => {
170
+ Promise.resolve(action(...callArgs)).then(() => repaintModule()).catch((err) => {
149
171
  console.error(`[ilha-router] action "${String(actionKey)}" failed:`, err);
150
172
  });
151
173
  };
@@ -170,7 +192,7 @@ function hydrateServerIsland(host, id, wiring, props) {
170
192
  if (!action) continue;
171
193
  const args = Array.isArray(marker.a) ? marker.a : [];
172
194
  props[key] = (..._runtimeArgs) => Promise.resolve(action(...args)).then((result) => {
173
- scheduleRepaint();
195
+ repaintModule();
174
196
  return result;
175
197
  });
176
198
  }
@@ -204,7 +226,7 @@ function hydrateServerIsland(host, id, wiring, props) {
204
226
  const { done, value } = step;
205
227
  if (controller.signal.aborted || done) break;
206
228
  state[key] = value;
207
- scheduleRepaint();
229
+ repaintModule();
208
230
  step = await gen.next();
209
231
  }
210
232
  } catch (err) {
@@ -232,7 +254,7 @@ function hydrateServerIsland(host, id, wiring, props) {
232
254
  }
233
255
  };
234
256
  }
235
- function __ilhaServerIsland(id, as, wiring = {}) {
257
+ function __ilhaServerIsland(id, as, wiring = {}, moduleKey) {
236
258
  const slotTag = assertValidTag(as);
237
259
  const island = ((props) => {
238
260
  return "";
@@ -250,10 +272,10 @@ function __ilhaServerIsland(id, as, wiring = {}) {
250
272
  key: slotKey
251
273
  });
252
274
  };
253
- island[ISLAND_MOUNT_INTERNAL] = (host, mountProps) => hydrateServerIsland(host, id, wiring, mountProps);
254
- island.mount = (host, mountProps) => hydrateServerIsland(host, id, wiring, mountProps).unmount;
275
+ island[ISLAND_MOUNT_INTERNAL] = (host, mountProps) => hydrateServerIsland(host, id, wiring, mountProps, moduleKey);
276
+ island.mount = (host, mountProps) => hydrateServerIsland(host, id, wiring, mountProps, moduleKey).unmount;
255
277
  return island;
256
278
  }
257
279
 
258
280
  //#endregion
259
- export { __ilhaApplyHead, __ilhaServerIsland };
281
+ export { __ilhaApplyHead, __ilhaRepaintServerModule, __ilhaServerIsland };
@@ -6,6 +6,10 @@
6
6
  * island bindings resolve to the proxy while everything else keeps flowing
7
7
  * through oxidejs's tacho stub replacement.
8
8
  */
9
+ /** Oxide RPC client key — basename without `.server.*` (oxide indexes by module name). */
10
+ export declare function serverModuleRpcKey(spec: string): string;
11
+ /** Client repaint group key — full normalized path so same-basename files stay distinct. */
12
+ export declare function serverModuleRepaintKey(spec: string): string;
9
13
  export interface ScannedServerIsland {
10
14
  /** Export binding name, or `"default"` for `export default ilha…`. */
11
15
  name: string;
@@ -5,6 +5,7 @@ import * as Result from "effect/Result";
5
5
  import { AsyncLocalStorage } from "node:async_hooks";
6
6
  import * as Data from "effect/Data";
7
7
  import * as Effect from "effect/Effect";
8
+ import { action, brandServerAction } from "oxidejs";
8
9
 
9
10
  //#region src/request-scope.ts
10
11
  /**
@@ -184,19 +185,10 @@ function frameEnvelope(status, body) {
184
185
  body: JSON.stringify(body)
185
186
  };
186
187
  }
187
- const ACTION_CALL = Symbol.for("ilha.actionCall");
188
188
  /** @internal Brand an exported server action with its generated RPC transport key. */
189
189
  function __ilhaServerAction(key, fn) {
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;
190
+ const handle = typeof fn === "function" && fn.$$atom === 1 ? fn : action(fn);
191
+ return brandServerAction(key, handle);
200
192
  }
201
193
  function registerServerIsland(id, render) {
202
194
  registry().set(id, { render });
package/dist/ssr.d.ts CHANGED
@@ -110,9 +110,7 @@ export interface FrameEnvelope {
110
110
  }
111
111
  export declare function frameEnvelope(status: number, body: Record<string, unknown>): FrameEnvelope;
112
112
  /** @internal Brand an exported server action with its generated RPC transport key. */
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
- };
113
+ export declare function __ilhaServerAction<A extends unknown[], R>(key: string, fn: (...args: A) => R | Promise<R>): import("oxidejs").ServerActionHandle<A, Awaited<Awaited<R>>>;
116
114
  export declare function registerServerIsland(id: string, render: () => unknown): void;
117
115
  export declare function getServerIslandEntry(id: string): ServerIslandEntry | undefined;
118
116
  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 & {
package/dist/ssr.js CHANGED
@@ -1,3 +1,3 @@
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";
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-CV_vNpS3.js";
2
2
 
3
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
@@ -1,4 +1,4 @@
1
- import { t as ilhaPages } from "./plugin-C3AAI3e_.js";
1
+ import { t as ilhaPages } from "./plugin-Bfnn44nu.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.11.1",
3
+ "version": "0.11.3",
4
4
  "description": "A tiny SPA router for Ilha",
5
5
  "keywords": [
6
6
  "frontend",
@@ -69,14 +69,21 @@
69
69
  "test": "bun test"
70
70
  },
71
71
  "dependencies": {
72
- "effect": "^4.0.0-rc.112",
72
+ "effect": "4.0.0-rc.112",
73
73
  "unplugin": "3.3.0"
74
74
  },
75
75
  "devDependencies": {
76
- "ilha": "0.14.1",
76
+ "ilha": "^0.14.3",
77
+ "oxidejs": "^0.3.0",
77
78
  "vite": "^8.2.2"
78
79
  },
79
80
  "peerDependencies": {
80
- "ilha": ">=0.14.1"
81
+ "ilha": ">=0.14.3",
82
+ "oxidejs": ">=0.3.0"
83
+ },
84
+ "peerDependenciesMeta": {
85
+ "oxidejs": {
86
+ "optional": true
87
+ }
81
88
  }
82
89
  }