@ilha/router 0.11.5 → 0.11.6
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/{head-BLtm0xtK.js → head-WG_AtbdX.js} +41 -2
- package/dist/head.d.ts +4 -0
- package/dist/index.js +1 -1
- package/dist/{plugin-DCdQ9yO8.js → plugin-Ci-W2rMt.js} +2 -2
- package/dist/request-scope.d.ts +17 -3
- package/dist/rsbuild.js +1 -1
- package/dist/server-island.js +1 -1
- package/dist/{ssr-Dvq_qXs4.js → ssr-D9q5C-OV.js} +93 -13
- package/dist/ssr.js +1 -1
- package/dist/vite.js +1 -1
- package/dist/webcontainer.d.ts +9 -0
- package/package.json +3 -3
|
@@ -35,8 +35,36 @@ const ILHA_ROUTER_HTML_ATTR = "data-ilha-router-html";
|
|
|
35
35
|
const ILHA_ROUTER_BODY_ATTR = "data-ilha-router-body";
|
|
36
36
|
/** Browser-only fallback; SSR uses AsyncLocalStorage (see `withHeadStore`). */
|
|
37
37
|
let _browserHeadStore = null;
|
|
38
|
+
/** WebContainer fallback while a serialized `withHeadStore` entry is in flight. */
|
|
39
|
+
let _headSyncStore = null;
|
|
40
|
+
/** Serialize WebContainer head entries so overlapping renders cannot share sync state. */
|
|
41
|
+
let _headFallbackTail = Promise.resolve(null);
|
|
38
42
|
let _headAls = null;
|
|
39
43
|
let _headAlsInit = null;
|
|
44
|
+
let _headWcOverride = null;
|
|
45
|
+
/** When true, activeHeadStore skips ALS so tests exercise the sync fallback. */
|
|
46
|
+
let _headAlsBypassForTests = false;
|
|
47
|
+
const headInWebcontainer = () => {
|
|
48
|
+
if (_headWcOverride !== null) return _headWcOverride;
|
|
49
|
+
if (isBrowser) return false;
|
|
50
|
+
try {
|
|
51
|
+
const versions = globalThis.process?.versions;
|
|
52
|
+
return Boolean(versions?.webcontainer);
|
|
53
|
+
} catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const withHeadFallbackEntry = async (fn) => {
|
|
58
|
+
const previous = _headFallbackTail;
|
|
59
|
+
const next = Promise.withResolvers();
|
|
60
|
+
_headFallbackTail = next.promise;
|
|
61
|
+
await previous;
|
|
62
|
+
try {
|
|
63
|
+
return await fn();
|
|
64
|
+
} finally {
|
|
65
|
+
next.resolve(null);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
40
68
|
/** ESM dynamic import — Nitro/Vite SSR workers have no `require`. */
|
|
41
69
|
const getHeadAlsAsync = async () => {
|
|
42
70
|
if (_headAls) return _headAls;
|
|
@@ -52,7 +80,8 @@ const getHeadAlsAsync = async () => {
|
|
|
52
80
|
};
|
|
53
81
|
const activeHeadStore = () => {
|
|
54
82
|
if (isBrowser) return _browserHeadStore;
|
|
55
|
-
|
|
83
|
+
if (_headAlsBypassForTests) return _headSyncStore;
|
|
84
|
+
return _headAls?.getStore() ?? _headSyncStore ?? null;
|
|
56
85
|
};
|
|
57
86
|
const cssEscapeAttr = (value) => {
|
|
58
87
|
const cssApi = globalThis.CSS;
|
|
@@ -231,7 +260,17 @@ const withHeadStore = async (store, fn) => {
|
|
|
231
260
|
_browserHeadStore = prev;
|
|
232
261
|
}
|
|
233
262
|
}
|
|
234
|
-
|
|
263
|
+
const als = await getHeadAlsAsync();
|
|
264
|
+
if (!headInWebcontainer()) return await als.run(store, () => Promise.resolve(fn()));
|
|
265
|
+
return withHeadFallbackEntry(async () => {
|
|
266
|
+
const previous = _headSyncStore;
|
|
267
|
+
_headSyncStore = store;
|
|
268
|
+
try {
|
|
269
|
+
return await als.run(store, () => Promise.resolve(fn()));
|
|
270
|
+
} finally {
|
|
271
|
+
_headSyncStore = previous;
|
|
272
|
+
}
|
|
273
|
+
});
|
|
235
274
|
};
|
|
236
275
|
let _flushScheduled = false;
|
|
237
276
|
let _flushTarget = null;
|
package/dist/head.d.ts
CHANGED
|
@@ -42,6 +42,10 @@ export interface SerializedHead {
|
|
|
42
42
|
export interface HeadStore {
|
|
43
43
|
entries: HeadInput[];
|
|
44
44
|
}
|
|
45
|
+
/** Test-only: force WebContainer head fallback (mirrors request-scope). */
|
|
46
|
+
export declare const __setHeadInWebcontainerForTests: (value: boolean | null) => void;
|
|
47
|
+
/** Test-only: force activeHeadStore to skip ALS (simulates WebContainer ALS loss). */
|
|
48
|
+
export declare const __setHeadAlsBypassForTests: (value: boolean) => void;
|
|
45
49
|
export declare const cssEscapeAttr: (value: string) => string;
|
|
46
50
|
export declare const escapeHeadAttr: <T>(value: T) => string;
|
|
47
51
|
/** Escape text content for inline HTML (loader error messages etc.). */
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as REQUEST_ALS_KEY } from "./als-key-CVxbuM3z.js";
|
|
2
|
-
import { a as withHeadStore, i as serializeHead, n as head, r as openBrowserHead, t as applyHeadEntriesToDocument } from "./head-
|
|
2
|
+
import { a as withHeadStore, i as serializeHead, n as head, r as openBrowserHead, t as applyHeadEntriesToDocument } from "./head-WG_AtbdX.js";
|
|
3
3
|
import { h, mount, renderToString } from "ilha";
|
|
4
4
|
|
|
5
5
|
//#region src/hash.ts
|
|
@@ -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-
|
|
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-D9q5C-OV.js";
|
|
2
2
|
import { existsSync, readFileSync, statSync, watch } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import * as Result from "effect/Result";
|
|
@@ -892,7 +892,7 @@ const renderAuthorizedFrame = async (req, res, ctx, raw, serverOrigin, identityH
|
|
|
892
892
|
headers: identityHeaders,
|
|
893
893
|
method: "POST"
|
|
894
894
|
});
|
|
895
|
-
const result = await renderServerIslandResult(body.id ?? "", request, (r, fn) => runWithIslandRequest(r, fn), incomingProps);
|
|
895
|
+
const result = await renderServerIslandResult(body.id ?? "", request, (r, fn) => Promise.resolve(runWithIslandRequest(r, fn)), incomingProps);
|
|
896
896
|
if (Result.isFailure(result)) {
|
|
897
897
|
const err = result.failure;
|
|
898
898
|
if (err.redirect) {
|
package/dist/request-scope.d.ts
CHANGED
|
@@ -12,10 +12,24 @@
|
|
|
12
12
|
* is `useContext()` from the main `@ilha/router` entry, which reads the
|
|
13
13
|
* storage without importing `node:async_hooks`; this node-only module is the
|
|
14
14
|
* sole place that constructs it.
|
|
15
|
+
*
|
|
16
|
+
* StackBlitz WebContainers lose AsyncLocalStorage across `async/await`. A
|
|
17
|
+
* module sync fallback keeps `useContext().request` readable, and unrelated
|
|
18
|
+
* fallback entries are serialized so concurrent renders cannot stomp each
|
|
19
|
+
* other. Nested calls reenter only while owned by the active entry (sync nest
|
|
20
|
+
* or ALS owner) so a suspended outer cannot admit unrelated concurrent work.
|
|
15
21
|
*/
|
|
16
22
|
export { REQUEST_ALS_KEY } from "./als-key";
|
|
23
|
+
export { __setInWebcontainerForTests } from "./webcontainer";
|
|
17
24
|
export type { IslandContext } from "./index";
|
|
18
|
-
/**
|
|
25
|
+
/** Test-only: force getStore() to skip ALS (simulates WebContainer ALS loss). */
|
|
26
|
+
export declare const __setAlsBypassForTests: (value: boolean) => void;
|
|
27
|
+
/**
|
|
28
|
+
* Run `fn` with `request` available to `useContext().request`. When oxidejs
|
|
19
29
|
* is loaded, its action scope is entered too, so `useRequest()` works in
|
|
20
|
-
* island renders and streamed frames.
|
|
21
|
-
|
|
30
|
+
* island renders and streamed frames.
|
|
31
|
+
*
|
|
32
|
+
* On WebContainer the return is always a `Promise` (fallback lock). Elsewhere
|
|
33
|
+
* the return matches `fn` (sync or async).
|
|
34
|
+
*/
|
|
35
|
+
export declare const runWithIslandRequest: <T>(request: Request, fn: () => T) => T | Promise<Awaited<T>>;
|
package/dist/rsbuild.js
CHANGED
package/dist/server-island.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as REQUEST_ALS_KEY } from "./als-key-CVxbuM3z.js";
|
|
2
|
-
import { a as withHeadStore } from "./head-
|
|
2
|
+
import { a as withHeadStore } from "./head-WG_AtbdX.js";
|
|
3
3
|
import { n as sanitizeSnapshotObject } from "./snapshot-XpAkSV8s.js";
|
|
4
4
|
import { renderToString } from "ilha";
|
|
5
5
|
import * as Result from "effect/Result";
|
|
@@ -8,6 +8,25 @@ import * as Data from "effect/Data";
|
|
|
8
8
|
import * as Effect from "effect/Effect";
|
|
9
9
|
import { action, brandServerAction } from "oxidejs";
|
|
10
10
|
|
|
11
|
+
//#region src/webcontainer.ts
|
|
12
|
+
/**
|
|
13
|
+
* StackBlitz WebContainer detection for AsyncLocalStorage fallbacks.
|
|
14
|
+
* Safe to import from browser-shared modules: on the client this always
|
|
15
|
+
* returns false (no `process.versions.webcontainer`).
|
|
16
|
+
*/
|
|
17
|
+
let webcontainerOverride = null;
|
|
18
|
+
/** StackBlitz sets `process.versions.webcontainer` inside WebContainers. */
|
|
19
|
+
const inWebcontainer = () => {
|
|
20
|
+
if (webcontainerOverride !== null) return webcontainerOverride;
|
|
21
|
+
try {
|
|
22
|
+
const versions = globalThis.process?.versions;
|
|
23
|
+
return Boolean(versions?.webcontainer);
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
//#endregion
|
|
11
30
|
//#region src/request-scope.ts
|
|
12
31
|
/**
|
|
13
32
|
* Request scope for server-owned island rendering.
|
|
@@ -23,25 +42,86 @@ import { action, brandServerAction } from "oxidejs";
|
|
|
23
42
|
* is `useContext()` from the main `@ilha/router` entry, which reads the
|
|
24
43
|
* storage without importing `node:async_hooks`; this node-only module is the
|
|
25
44
|
* sole place that constructs it.
|
|
45
|
+
*
|
|
46
|
+
* StackBlitz WebContainers lose AsyncLocalStorage across `async/await`. A
|
|
47
|
+
* module sync fallback keeps `useContext().request` readable, and unrelated
|
|
48
|
+
* fallback entries are serialized so concurrent renders cannot stomp each
|
|
49
|
+
* other. Nested calls reenter only while owned by the active entry (sync nest
|
|
50
|
+
* or ALS owner) so a suspended outer cannot admit unrelated concurrent work.
|
|
26
51
|
*/
|
|
27
52
|
/** Installed by oxidejs when its module loads. Lets `useRequest()` resolve
|
|
28
53
|
* inside island renders and frames, not just `/__oxide/action`. */
|
|
29
54
|
const OXIDE_RUN_WITH_REQUEST = Symbol.for("oxidejs.runWithRequest");
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
55
|
+
const innerAls = new AsyncLocalStorage();
|
|
56
|
+
/** Tracks the active fallback lock owner across awaits where ALS survives. */
|
|
57
|
+
const entryOwnerAls = new AsyncLocalStorage();
|
|
58
|
+
let syncStore = null;
|
|
59
|
+
/** When true, getStore ignores ALS so tests exercise the sync fallback path. */
|
|
60
|
+
let alsBypassForTests = false;
|
|
61
|
+
/** Serialize unrelated WebContainer fallback entries (set syncStore → run → clear). */
|
|
62
|
+
let fallbackTail = Promise.resolve(null);
|
|
63
|
+
/** Lock holder identity — nested reentry requires matching ALS owner (or sync nest). */
|
|
64
|
+
let activeOwner = null;
|
|
65
|
+
/**
|
|
66
|
+
* Sync-only nest depth while `runScoped` is invoking `fn`. Lets nested
|
|
67
|
+
* `runWithIslandRequest` reenter before `fn` awaits; cleared before awaiting
|
|
68
|
+
* so a suspended entry does not admit unrelated concurrent requests.
|
|
69
|
+
*/
|
|
70
|
+
let syncNestDepth = 0;
|
|
71
|
+
const withFallbackEntry = async (fn) => {
|
|
72
|
+
const owner = alsBypassForTests ? void 0 : entryOwnerAls.getStore();
|
|
73
|
+
if (syncNestDepth > 0 || owner !== void 0 && owner === activeOwner) return await fn();
|
|
74
|
+
const previous = fallbackTail;
|
|
75
|
+
const next = Promise.withResolvers();
|
|
76
|
+
fallbackTail = next.promise;
|
|
77
|
+
await previous;
|
|
78
|
+
const myOwner = {};
|
|
79
|
+
activeOwner = myOwner;
|
|
80
|
+
try {
|
|
81
|
+
return await entryOwnerAls.run(myOwner, fn);
|
|
82
|
+
} finally {
|
|
83
|
+
activeOwner = null;
|
|
84
|
+
next.resolve(null);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
const ensureScope = () => {
|
|
34
88
|
const g = globalThis;
|
|
35
89
|
const existing = g[REQUEST_ALS_KEY];
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
90
|
+
if (existing && "getStore" in existing) return existing;
|
|
91
|
+
const scope = { getStore: () => (alsBypassForTests ? void 0 : innerAls.getStore()) ?? syncStore ?? void 0 };
|
|
92
|
+
g[REQUEST_ALS_KEY] = scope;
|
|
93
|
+
return scope;
|
|
94
|
+
};
|
|
95
|
+
const runScoped = (request, oxide, fn) => innerAls.run(request, () => oxide ? oxide(request, fn) : fn());
|
|
96
|
+
/**
|
|
97
|
+
* Run `fn` with `request` available to `useContext().request`. When oxidejs
|
|
98
|
+
* is loaded, its action scope is entered too, so `useRequest()` works in
|
|
99
|
+
* island renders and streamed frames.
|
|
100
|
+
*
|
|
101
|
+
* On WebContainer the return is always a `Promise` (fallback lock). Elsewhere
|
|
102
|
+
* the return matches `fn` (sync or async).
|
|
103
|
+
*/
|
|
104
|
+
const runWithIslandRequest = (request, fn) => {
|
|
105
|
+
ensureScope();
|
|
106
|
+
const oxideSlot = globalThis[OXIDE_RUN_WITH_REQUEST];
|
|
43
107
|
const oxide = oxideSlot === void 0 ? void 0 : oxideSlot;
|
|
44
|
-
|
|
108
|
+
if (!inWebcontainer()) return runScoped(request, oxide, fn);
|
|
109
|
+
return withFallbackEntry(async () => {
|
|
110
|
+
const previous = syncStore;
|
|
111
|
+
syncStore = request;
|
|
112
|
+
try {
|
|
113
|
+
syncNestDepth += 1;
|
|
114
|
+
let result;
|
|
115
|
+
try {
|
|
116
|
+
result = runScoped(request, oxide, fn);
|
|
117
|
+
} finally {
|
|
118
|
+
syncNestDepth -= 1;
|
|
119
|
+
}
|
|
120
|
+
return await Promise.resolve(result);
|
|
121
|
+
} finally {
|
|
122
|
+
syncStore = previous;
|
|
123
|
+
}
|
|
124
|
+
});
|
|
45
125
|
};
|
|
46
126
|
|
|
47
127
|
//#endregion
|
package/dist/ssr.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { S as ssrWithImports, _ 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-
|
|
1
|
+
import { S as ssrWithImports, _ 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-D9q5C-OV.js";
|
|
2
2
|
|
|
3
3
|
export { FRAME_ENDPOINT, FrameError, MAX_BODY, __ilhaServerAction, authorizeFrameRequest, ssrWithImports as default, forwardIdentityHeaders, frameEnvelope, frameScopedUrl, getFrameAuth, getFrameGuard, getServerIslandEntry, isSafeFramePath, isTrustedOrigin, json, parseFrameProps, readBodyBounded, registerServerIsland, renderServerIsland, renderServerIslandResult, setFrameAuth, setFrameGuard };
|
package/dist/vite.js
CHANGED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StackBlitz WebContainer detection for AsyncLocalStorage fallbacks.
|
|
3
|
+
* Safe to import from browser-shared modules: on the client this always
|
|
4
|
+
* returns false (no `process.versions.webcontainer`).
|
|
5
|
+
*/
|
|
6
|
+
/** StackBlitz sets `process.versions.webcontainer` inside WebContainers. */
|
|
7
|
+
export declare const inWebcontainer: () => boolean;
|
|
8
|
+
/** Test-only: force or clear the WebContainer detection path. */
|
|
9
|
+
export declare const __setInWebcontainerForTests: (value: boolean | null) => void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ilha/router",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.6",
|
|
4
4
|
"description": "A tiny SPA router for Ilha",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"frontend",
|
|
@@ -74,12 +74,12 @@
|
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
76
|
"ilha": "^0.14.4",
|
|
77
|
-
"oxidejs": "^0.3.
|
|
77
|
+
"oxidejs": "^0.3.2",
|
|
78
78
|
"vite": "^8.2.2"
|
|
79
79
|
},
|
|
80
80
|
"peerDependencies": {
|
|
81
81
|
"ilha": ">=0.14.4",
|
|
82
|
-
"oxidejs": ">=0.3.
|
|
82
|
+
"oxidejs": ">=0.3.2"
|
|
83
83
|
},
|
|
84
84
|
"peerDependenciesMeta": {
|
|
85
85
|
"oxidejs": {
|