@ilha/router 0.9.0 → 0.9.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/dist/index.js CHANGED
@@ -601,7 +601,7 @@ async function withViewSwap(mutate) {
601
601
  * shape, so all client mount paths handle both sources identically. Loader
602
602
  * `head` contributions are dropped, matching the endpoint fetch path.
603
603
  */
604
- async function runLocalLoader(loader, matchParams, pathWithSearch, signal, isClientLoader = false) {
604
+ async function runLocalLoader(loader, matchParams, pathWithSearch, signal) {
605
605
  const url = new URL(pathWithSearch, location.origin);
606
606
  const params = extractParams(matchParams);
607
607
  const headEntries = [];
@@ -624,14 +624,7 @@ async function runLocalLoader(loader, matchParams, pathWithSearch, signal, isCli
624
624
  };
625
625
  }
626
626
  if (result.kind === "data") {
627
- const data = isClientLoader ? {
628
- ...result.data,
629
- load: {
630
- loading: false,
631
- value: result.data ?? {},
632
- error: void 0
633
- }
634
- } : result.data;
627
+ const data = envelopLoad(result.data);
635
628
  return headEntries.length > 0 ? {
636
629
  kind: "data",
637
630
  data,
@@ -659,7 +652,7 @@ async function fetchLoaderData(pathWithSearch, signal) {
659
652
  const pathOnly = pathWithSearch.split("?")[0] ?? "";
660
653
  const localMatch = matchRoute(_routes, pathOnly);
661
654
  const localLoader = localMatch?.data?.clientLoader ?? localMatch?.data?.loader;
662
- if (localLoader) return runLocalLoader(localLoader, localMatch?.params, pathWithSearch, signal, !!localMatch?.data?.clientLoader);
655
+ if (localLoader) return runLocalLoader(localLoader, localMatch?.params, pathWithSearch, signal);
663
656
  const url = `${LOADER_ENDPOINT}?path=${encodeURIComponent(pathWithSearch)}`;
664
657
  try {
665
658
  const res = await fetch(url, {
@@ -677,7 +670,12 @@ async function fetchLoaderData(pathWithSearch, signal) {
677
670
  message: res.statusText
678
671
  };
679
672
  }
680
- return await res.json();
673
+ const parsed = await res.json();
674
+ if (parsed.kind === "data" && !localMatch?.data?.clientLoader && parsed.data && typeof parsed.data === "object" && !("load" in parsed.data)) return {
675
+ ...parsed,
676
+ data: envelopLoad(parsed.data)
677
+ };
678
+ return parsed;
681
679
  } catch (e) {
682
680
  if (e?.name === "AbortError") throw e;
683
681
  return {
@@ -983,6 +981,14 @@ function activeIsland(value) {
983
981
  return store ? store.island : _activeIslandSig();
984
982
  }
985
983
  let _routes = createRouteRegistry();
984
+ /** Wrap loader data in the load envelope (plain, serializable). */
985
+ function envelopLoad(data) {
986
+ return { load: {
987
+ loading: false,
988
+ value: data ?? {},
989
+ error: void 0
990
+ } };
991
+ }
986
992
  function createRouteRegistry() {
987
993
  return [];
988
994
  }
@@ -1553,8 +1559,10 @@ function loaderAbort(request, timeout) {
1553
1559
  const ctrl = new AbortController();
1554
1560
  const abort = () => ctrl.abort();
1555
1561
  const reqSignal = request?.signal;
1556
- if (reqSignal) if (reqSignal.aborted) abort();
1557
- else reqSignal.addEventListener("abort", abort, { once: true });
1562
+ if (reqSignal) {
1563
+ if (reqSignal.aborted) abort();
1564
+ else reqSignal.addEventListener("abort", abort, { once: true });
1565
+ }
1558
1566
  let timer;
1559
1567
  if (timeout && timeout > 0) timer = setTimeout(abort, timeout);
1560
1568
  return {
@@ -2068,9 +2076,17 @@ function router(options = {}) {
2068
2076
  };
2069
2077
  }
2070
2078
  if (result.kind !== "data") return result;
2071
- if (headStore.entries.length === 0) return result;
2072
- return {
2079
+ const enveloped = {
2073
2080
  ...result,
2081
+ data: { load: {
2082
+ loading: false,
2083
+ value: result.data ?? {},
2084
+ error: void 0
2085
+ } }
2086
+ };
2087
+ if (headStore.entries.length === 0) return enveloped;
2088
+ return {
2089
+ ...enveloped,
2074
2090
  head: serializeHead(headStore.entries),
2075
2091
  headEntries: headStore.entries
2076
2092
  };
@@ -2179,7 +2195,7 @@ function router(options = {}) {
2179
2195
  head: serializeHead(headStore.entries)
2180
2196
  };
2181
2197
  }
2182
- props = result.data;
2198
+ props = envelopLoad(result.data);
2183
2199
  }
2184
2200
  const name = buildReverseRegistry(registry).get(island);
2185
2201
  if (!name) {
@@ -1,4 +1,4 @@
1
- import { t as runWithIslandRequest } from "./request-scope-D6_4rqMb.js";
1
+ import { t as runWithIslandRequest } from "./request-scope-C4reU4v0.js";
2
2
  import { FrameError, getFrameGuard, renderServerIsland, setFrameGuard } from "./server-island-registry.js";
3
3
  import { existsSync, readFileSync, statSync, watch } from "node:fs";
4
4
  import { basename, dirname, extname, join, relative, resolve, sep } from "node:path";
@@ -8,7 +8,7 @@ import { AsyncLocalStorage } from "node:async_hooks";
8
8
  * page SSR through the router, or streamed frames through the plugin's
9
9
  * `/__ilha/frame` endpoint. Both seed this scope with the originating
10
10
  * `Request`, so render functions can read request data (URL, headers,
11
- * cookies) without depending on Oxide's action-only `useRequest()`.
11
+ * cookies) through `useContext().request` or a host integration such as Oxide's `useRequest()`.
12
12
  *
13
13
  * The storage lives on `globalThis` under `ilha.requestAls` so every module
14
14
  * copy (plugin bundle, SSR graph) shares one instance. The public accessor
@@ -17,10 +17,17 @@ import { AsyncLocalStorage } from "node:async_hooks";
17
17
  * sole place that constructs it.
18
18
  */
19
19
  const REQUEST_ALS_KEY = Symbol.for("ilha.requestAls");
20
- /** Run `fn` with `request` available to `useContext().request`. */
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. */
21
26
  function runWithIslandRequest(request, fn) {
22
27
  const g = globalThis;
23
- return (g[REQUEST_ALS_KEY] ??= new AsyncLocalStorage()).run(request, fn);
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());
24
31
  }
25
32
 
26
33
  //#endregion
@@ -5,7 +5,7 @@
5
5
  * page SSR through the router, or streamed frames through the plugin's
6
6
  * `/__ilha/frame` endpoint. Both seed this scope with the originating
7
7
  * `Request`, so render functions can read request data (URL, headers,
8
- * cookies) without depending on Oxide's action-only `useRequest()`.
8
+ * cookies) through `useContext().request` or a host integration such as Oxide's `useRequest()`.
9
9
  *
10
10
  * The storage lives on `globalThis` under `ilha.requestAls` so every module
11
11
  * copy (plugin bundle, SSR graph) shares one instance. The public accessor
@@ -15,6 +15,8 @@
15
15
  */
16
16
  import type { IslandContext } from "./index";
17
17
  export declare const REQUEST_ALS_KEY: unique symbol;
18
- /** Run `fn` with `request` available to `useContext().request`. */
18
+ /** Run `fn` with `request` available to `useContext().request`. When oxidejs
19
+ * is loaded, its action scope is entered too, so `useRequest()` works in
20
+ * island renders and streamed frames. */
19
21
  export declare function runWithIslandRequest<T>(request: Request, fn: () => T): T;
20
22
  export type { IslandContext };
@@ -2,5 +2,5 @@ export type { LayoutHandler, ErrorHandler, RouteSnapshot, AppError } from "./ind
2
2
  export { ilhaPages, type IlhaPagesOptions } from "./plugin";
3
3
  import { type IlhaPagesOptions } from "./plugin";
4
4
  /** Rolldown plugin — use via `@ilha/router/rolldown`. */
5
- export declare function pages(options?: IlhaPagesOptions): import("unplugin").RolldownPlugin<any> | import("unplugin").RolldownPlugin<any>[];
5
+ export declare function pages(options?: IlhaPagesOptions): import("rolldown").Plugin<any> | import("rolldown").Plugin<any>[];
6
6
  export default pages;
package/dist/rolldown.js CHANGED
@@ -1,4 +1,4 @@
1
- import { t as ilhaPages } from "./plugin-DPGzrIVj.js";
1
+ import { t as ilhaPages } from "./plugin-DogkskcY.js";
2
2
 
3
3
  //#region src/rolldown.ts
4
4
  /** Rolldown plugin — use via `@ilha/router/rolldown`. */
package/dist/rspack.js CHANGED
@@ -1,4 +1,4 @@
1
- import { t as ilhaPages } from "./plugin-DPGzrIVj.js";
1
+ import { t as ilhaPages } from "./plugin-DogkskcY.js";
2
2
 
3
3
  //#region src/rspack.ts
4
4
  /** Rspack plugin — use via `@ilha/router/rspack`. */
@@ -104,12 +104,16 @@ async function renderServerIsland(id, request, runWithScope) {
104
104
  const params = entry.pattern ? matchPatternParams(entry.pattern, url.pathname) : {};
105
105
  if (!params) throw new FrameError(400, "frame failed");
106
106
  try {
107
- props = await entry.load({
108
- params,
109
- request,
110
- url,
111
- signal: request.signal
112
- });
107
+ props = { load: {
108
+ loading: false,
109
+ value: await entry.load({
110
+ params,
111
+ request,
112
+ url,
113
+ signal: request.signal
114
+ }) ?? {},
115
+ error: void 0
116
+ } };
113
117
  } catch (error) {
114
118
  const marker = error;
115
119
  if (marker.__ilhaRedirect === true) {
package/dist/ssr.js CHANGED
@@ -1,4 +1,4 @@
1
- import { t as runWithIslandRequest } from "./request-scope-D6_4rqMb.js";
1
+ import { t as runWithIslandRequest } from "./request-scope-C4reU4v0.js";
2
2
  import { FrameError, getFrameGuard, getFrameLoaderRunner, renderServerIsland } from "./server-island-registry.js";
3
3
 
4
4
  //#region src/ssr.ts
@@ -136,6 +136,12 @@ async function ssr(request) {
136
136
  method: "POST",
137
137
  headers
138
138
  });
139
+ for (const sym of Object.getOwnPropertySymbols(request)) {
140
+ if (Symbol.keyFor(sym) === void 0) continue;
141
+ try {
142
+ scoped[sym] = request[sym];
143
+ } catch {}
144
+ }
139
145
  return json(200, { html: await renderServerIsland(id, scoped, (scopedRequest, fn) => Promise.resolve(runWithIslandRequest(scopedRequest, fn))) });
140
146
  } catch (error) {
141
147
  if (error instanceof FrameError) {
package/dist/vite.js CHANGED
@@ -1,4 +1,4 @@
1
- import { t as ilhaPages } from "./plugin-DPGzrIVj.js";
1
+ import { t as ilhaPages } from "./plugin-DogkskcY.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.0",
3
+ "version": "0.9.1",
4
4
  "description": "A tiny SPA router for Ilha",
5
5
  "keywords": [
6
6
  "frontend",