@ilha/router 0.11.3 → 0.11.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/README.md +75 -66
- package/dist/codegen.d.ts +3 -3
- package/dist/hash.d.ts +13 -9
- package/dist/head-BLtm0xtK.js +342 -0
- package/dist/head.d.ts +42 -28
- package/dist/http.d.ts +1 -1
- package/dist/index.d.ts +52 -55
- package/dist/index.js +339 -305
- package/dist/plugin-DCdQ9yO8.js +1140 -0
- package/dist/plugin.d.ts +15 -15
- package/dist/request-scope.d.ts +3 -5
- package/dist/route-error.d.ts +6 -0
- package/dist/route-match.d.ts +3 -3
- package/dist/rsbuild.d.ts +3 -3
- package/dist/rsbuild.js +2 -4
- package/dist/server-island.d.ts +26 -15
- package/dist/server-island.js +166 -136
- package/dist/server-islands.d.ts +15 -14
- package/dist/snapshot-XpAkSV8s.js +57 -0
- package/dist/snapshot.d.ts +15 -2
- package/dist/{ssr-CV_vNpS3.js → ssr-Dvq_qXs4.js} +170 -142
- package/dist/ssr.d.ts +38 -39
- package/dist/ssr.js +2 -2
- package/dist/vite.d.ts +3 -3
- package/dist/vite.js +2 -4
- package/oxlint.cjs +201 -122
- package/package.json +3 -3
- package/dist/head-DpYV3bHH.js +0 -285
- package/dist/plugin-Bfnn44nu.js +0 -1034
- package/dist/snapshot-C0E2OGwL.js +0 -55
package/dist/plugin.d.ts
CHANGED
|
@@ -33,7 +33,7 @@ export interface IlhaPagesOptions {
|
|
|
33
33
|
* when islands serve private data. Production equivalents register via
|
|
34
34
|
* `setFrameGuard()` from `@ilha/router/ssr`.
|
|
35
35
|
*/
|
|
36
|
-
frameGuard?: (request: Request) => Response |
|
|
36
|
+
frameGuard?: (request: Request) => Response | undefined | Promise<Response | undefined>;
|
|
37
37
|
/**
|
|
38
38
|
* Explicit trusted origins for frame/loader requests (e.g. a `.vercel.app`
|
|
39
39
|
* or custom domain). When unset, origin checks compare the `Origin` header
|
|
@@ -51,28 +51,28 @@ export interface IlhaPagesOptions {
|
|
|
51
51
|
*/
|
|
52
52
|
strict?: boolean;
|
|
53
53
|
}
|
|
54
|
-
export declare
|
|
55
|
-
|
|
54
|
+
export declare const resolvePluginPaths: (root: string, options: IlhaPagesOptions) => {
|
|
55
|
+
clientFile: string;
|
|
56
56
|
outDir: string;
|
|
57
|
+
pagesDir: string;
|
|
57
58
|
serverFile: string;
|
|
58
|
-
clientFile: string;
|
|
59
59
|
};
|
|
60
60
|
export interface PagesPluginState {
|
|
61
61
|
pagesDir: string;
|
|
62
62
|
outDir: string;
|
|
63
63
|
serverFile: string;
|
|
64
64
|
clientFile: string;
|
|
65
|
-
setPaths(root: string)
|
|
66
|
-
regen()
|
|
67
|
-
shouldRegenOnChange(file: string)
|
|
68
|
-
isUnderPagesDir(file: string)
|
|
65
|
+
setPaths: (root: string) => void;
|
|
66
|
+
regen: () => Promise<void>;
|
|
67
|
+
shouldRegenOnChange: (file: string) => boolean;
|
|
68
|
+
isUnderPagesDir: (file: string) => boolean;
|
|
69
69
|
}
|
|
70
|
-
export declare
|
|
71
|
-
export declare
|
|
72
|
-
export declare
|
|
73
|
-
export declare
|
|
74
|
-
type InvalidateModules = () =>
|
|
75
|
-
export declare
|
|
76
|
-
export declare
|
|
70
|
+
export declare const createPagesPluginState: (options: IlhaPagesOptions) => PagesPluginState;
|
|
71
|
+
export declare const regenFromPagesChange: (state: PagesPluginState, file: string, shouldRegen: (file: string) => boolean) => Promise<void>;
|
|
72
|
+
export declare const resolvePagesId: (state: PagesPluginState, id: string, importer?: string) => string | undefined;
|
|
73
|
+
export declare const loadPagesModule: (state: PagesPluginState, id: string) => string | undefined;
|
|
74
|
+
type InvalidateModules = () => Promise<void>;
|
|
75
|
+
export declare const createStructuralInvalidate: (state: PagesPluginState, invalidate: InvalidateModules) => (file: string) => Promise<void>;
|
|
76
|
+
export declare const setupRspackPagesWatcher: (state: PagesPluginState, structuralInvalidate: (file: string) => Promise<void>) => () => void;
|
|
77
77
|
export declare const ilhaPages: import("unplugin").UnpluginInstance<IlhaPagesOptions | undefined, boolean>;
|
|
78
78
|
export {};
|
package/dist/request-scope.d.ts
CHANGED
|
@@ -13,11 +13,9 @@
|
|
|
13
13
|
* storage without importing `node:async_hooks`; this node-only module is the
|
|
14
14
|
* sole place that constructs it.
|
|
15
15
|
*/
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
export { REQUEST_ALS_KEY };
|
|
16
|
+
export { REQUEST_ALS_KEY } from "./als-key";
|
|
17
|
+
export type { IslandContext } from "./index";
|
|
19
18
|
/** Run `fn` with `request` available to `useContext().request`. When oxidejs
|
|
20
19
|
* is loaded, its action scope is entered too, so `useRequest()` works in
|
|
21
20
|
* island renders and streamed frames. */
|
|
22
|
-
export declare
|
|
23
|
-
export type { IslandContext };
|
|
21
|
+
export declare const runWithIslandRequest: <T>(request: Request, fn: () => T) => T;
|
package/dist/route-match.d.ts
CHANGED
|
@@ -11,12 +11,12 @@ export interface ParsedPattern {
|
|
|
11
11
|
segments: string[];
|
|
12
12
|
kinds: number[];
|
|
13
13
|
}
|
|
14
|
-
export declare
|
|
14
|
+
export declare const parsePattern: (pattern: string) => ParsedPattern;
|
|
15
15
|
/**
|
|
16
16
|
* Match `segments` against a pathname. Returns raw (still-encoded) captured
|
|
17
17
|
* params, or `null` when the path doesn't match — the caller decides whether to
|
|
18
18
|
* decode via {@link safeDecode}.
|
|
19
19
|
*/
|
|
20
|
-
export declare
|
|
20
|
+
export declare const matchSegments: (segments: string[], pathname: string) => Record<string, string> | null;
|
|
21
21
|
/** Decode a route-param value, tolerating malformed percent-encoding. */
|
|
22
|
-
export declare
|
|
22
|
+
export declare const safeDecode: (value: string) => string;
|
package/dist/rsbuild.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
import type { IlhaPagesOptions } from "./plugin";
|
|
2
|
+
export type { LayoutHandler, ErrorHandler, RouteSnapshot, AppError, } from "./index";
|
|
2
3
|
export { ilhaPages, type IlhaPagesOptions } from "./plugin";
|
|
3
|
-
import { type IlhaPagesOptions } from "./plugin";
|
|
4
4
|
/** Rsbuild plugin — use via `@ilha/router/rsbuild`. */
|
|
5
|
-
export declare
|
|
5
|
+
export declare const pages: (options?: IlhaPagesOptions) => any;
|
|
6
6
|
export default pages;
|
package/dist/rsbuild.js
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import { t as ilhaPages } from "./plugin-
|
|
1
|
+
import { t as ilhaPages } from "./plugin-DCdQ9yO8.js";
|
|
2
2
|
|
|
3
3
|
//#region src/rsbuild.ts
|
|
4
4
|
/** Rsbuild plugin — use via `@ilha/router/rsbuild`. */
|
|
5
|
-
|
|
6
|
-
return ilhaPages.rsbuild(options);
|
|
7
|
-
}
|
|
5
|
+
const pages = (options = {}) => ilhaPages.rsbuild(options);
|
|
8
6
|
|
|
9
7
|
//#endregion
|
|
10
8
|
export { pages as default, pages, ilhaPages };
|
package/dist/server-island.d.ts
CHANGED
|
@@ -14,27 +14,35 @@
|
|
|
14
14
|
* `[data-ilha-on]` event sentinels to named actions using the
|
|
15
15
|
* `data-ilha-actions` manifest emitted by `hydratable()`.
|
|
16
16
|
*/
|
|
17
|
+
import type { SnapshotObject, SnapshotValue } from "./snapshot";
|
|
18
|
+
/** Symbol.for keeps brands stable across duplicate ilha copies in one realm. */
|
|
19
|
+
declare const ISLAND: unique symbol;
|
|
20
|
+
declare const ISLAND_SLOT_TAG: unique symbol;
|
|
21
|
+
export declare const ISLAND_MOUNT_INTERNAL: unique symbol;
|
|
17
22
|
declare const ISLAND_CALL: unique symbol;
|
|
18
|
-
/** @internal
|
|
19
|
-
export declare
|
|
20
|
-
export type ServerStreamFn = (signal: AbortSignal) => AsyncGenerator<
|
|
23
|
+
/** @internal */
|
|
24
|
+
export declare const __ilhaRepaintServerModule: (moduleKey: string) => void;
|
|
25
|
+
export type ServerStreamFn = (signal: AbortSignal) => AsyncGenerator<SnapshotValue> | Generator<SnapshotValue>;
|
|
26
|
+
/** JSON-serializable value — the wire format for server-action payloads. */
|
|
27
|
+
export type ServerActionPayload = SnapshotValue;
|
|
28
|
+
export type ServerIslandProps = SnapshotObject;
|
|
21
29
|
export interface ServerIslandWiring {
|
|
22
30
|
/** Stream key → client transport. The plugin wires these to tacho stubs. */
|
|
23
31
|
streams?: Record<string, ServerStreamFn>;
|
|
24
32
|
/** Action key → client transport. Event payloads are not serializable;
|
|
25
33
|
* handlers receive `undefined` and should read island state instead. */
|
|
26
|
-
actions?: Record<string, (payload?:
|
|
34
|
+
actions?: Record<string, (payload?: ServerActionPayload) => SnapshotValue | Promise<SnapshotValue>>;
|
|
27
35
|
/** Frame transport: re-renders the island with the latest parent props. */
|
|
28
|
-
frame?: (props?:
|
|
36
|
+
frame?: (props?: ServerIslandProps) => string | Promise<string> | SnapshotValue;
|
|
29
37
|
/** Client-capable islands nested in the server render, keyed by opaque ref. */
|
|
30
|
-
children?: Record<string,
|
|
38
|
+
children?: Record<string, ServerIslandCallable>;
|
|
31
39
|
}
|
|
32
40
|
export interface ServerIslandHandle {
|
|
33
41
|
unmount: () => void;
|
|
34
|
-
updateProps: (props?:
|
|
42
|
+
updateProps: (props?: ServerIslandProps) => void;
|
|
35
43
|
}
|
|
36
|
-
/** @internal
|
|
37
|
-
export declare
|
|
44
|
+
/** @internal */
|
|
45
|
+
export declare const __ilhaApplyHead: <T>(entries: T) => void;
|
|
38
46
|
/**
|
|
39
47
|
* Create a client proxy island for a server-defined island. Called by
|
|
40
48
|
* generated virtual modules — not by application code.
|
|
@@ -43,16 +51,19 @@ export declare function __ilhaApplyHead(entries: unknown): void;
|
|
|
43
51
|
* @param as - Slot tag declared by the server island's `{ as }` option (default div).
|
|
44
52
|
* @param wiring - Stream/action transports wired to tacho stubs by codegen.
|
|
45
53
|
*/
|
|
46
|
-
interface
|
|
54
|
+
interface IslandCallPayload {
|
|
47
55
|
[ISLAND_CALL]: true;
|
|
48
56
|
island: ServerIslandCallable;
|
|
49
|
-
props?:
|
|
57
|
+
props?: ServerIslandProps;
|
|
50
58
|
key: string;
|
|
51
59
|
}
|
|
52
|
-
export type ServerIslandCallable =
|
|
53
|
-
|
|
60
|
+
export type ServerIslandCallable = ((props?: ServerIslandProps) => string) & {
|
|
61
|
+
[ISLAND]: true;
|
|
62
|
+
[ISLAND_SLOT_TAG]: string;
|
|
63
|
+
mount: (host: Element, props?: ServerIslandProps) => () => void;
|
|
54
64
|
toString: () => string;
|
|
55
|
-
key: (slotKey: string) => (props?:
|
|
65
|
+
key: (slotKey: string) => (props?: ServerIslandProps) => IslandCallPayload;
|
|
66
|
+
[ISLAND_MOUNT_INTERNAL]: (host: Element, props?: ServerIslandProps) => ServerIslandHandle;
|
|
56
67
|
};
|
|
57
|
-
export declare
|
|
68
|
+
export declare const __ilhaServerIsland: (id: string, as: string, wiring?: ServerIslandWiring, moduleKey?: string) => ServerIslandCallable;
|
|
58
69
|
export {};
|
package/dist/server-island.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { t as applyHeadEntriesToDocument } from "./head-
|
|
2
|
-
import { t as parseSnapshotAttr } from "./snapshot-
|
|
1
|
+
import { t as applyHeadEntriesToDocument } from "./head-BLtm0xtK.js";
|
|
2
|
+
import { t as parseSnapshotAttr } from "./snapshot-XpAkSV8s.js";
|
|
3
3
|
|
|
4
4
|
//#region src/server-island.ts
|
|
5
5
|
/**
|
|
@@ -18,21 +18,25 @@ import { t as parseSnapshotAttr } from "./snapshot-C0E2OGwL.js";
|
|
|
18
18
|
* `[data-ilha-on]` event sentinels to named actions using the
|
|
19
19
|
* `data-ilha-actions` manifest emitted by `hydratable()`.
|
|
20
20
|
*/
|
|
21
|
+
const objectTag = (value) => Object.prototype.toString.call(value);
|
|
22
|
+
const isString = (value) => objectTag(value) === "[object String]";
|
|
23
|
+
const isObject = (value) => value !== null && objectTag(value) === "[object Object]";
|
|
21
24
|
/** Symbol.for keeps brands stable across duplicate ilha copies in one realm. */
|
|
22
25
|
const ISLAND = Symbol.for("ilha.island");
|
|
23
26
|
const ISLAND_SLOT_TAG = Symbol.for("ilha.islandSlotTag");
|
|
24
27
|
const ISLAND_MOUNT_INTERNAL = Symbol.for("ilha.islandMountInternal");
|
|
28
|
+
const ISLAND_CALL = Symbol.for("ilha.islandCall");
|
|
25
29
|
const STATE_ATTR = "data-ilha-state";
|
|
26
30
|
const EVENT_SENTINEL_ATTR = "data-ilha-on";
|
|
27
31
|
const ACTIONS_ATTR = "data-ilha-actions";
|
|
28
32
|
const PROPS_ATTR = "data-ilha-props";
|
|
29
33
|
const CLIENT_REF_ATTR = "data-ilha-client-ref";
|
|
30
34
|
const moduleRepaints = /* @__PURE__ */ new Map();
|
|
31
|
-
/** @internal
|
|
32
|
-
|
|
35
|
+
/** @internal */
|
|
36
|
+
const __ilhaRepaintServerModule = (moduleKey) => {
|
|
33
37
|
for (const repaint of moduleRepaints.get(moduleKey) ?? []) repaint();
|
|
34
|
-
}
|
|
35
|
-
|
|
38
|
+
};
|
|
39
|
+
const linkServerModuleRepaints = (moduleKey, repaint) => {
|
|
36
40
|
let set = moduleRepaints.get(moduleKey);
|
|
37
41
|
if (!set) {
|
|
38
42
|
set = /* @__PURE__ */ new Set();
|
|
@@ -40,72 +44,97 @@ function linkServerModuleRepaints(moduleKey, repaint) {
|
|
|
40
44
|
}
|
|
41
45
|
set.add(repaint);
|
|
42
46
|
return () => {
|
|
43
|
-
|
|
44
|
-
if (
|
|
47
|
+
const live = moduleRepaints.get(moduleKey);
|
|
48
|
+
if (!live) return;
|
|
49
|
+
live.delete(repaint);
|
|
50
|
+
if (live.size === 0) moduleRepaints.delete(moduleKey);
|
|
45
51
|
};
|
|
46
|
-
}
|
|
47
|
-
/** @internal
|
|
48
|
-
|
|
52
|
+
};
|
|
53
|
+
/** @internal */
|
|
54
|
+
const __ilhaApplyHead = (entries) => {
|
|
49
55
|
if (Array.isArray(entries)) applyHeadEntriesToDocument(entries);
|
|
50
|
-
}
|
|
56
|
+
};
|
|
51
57
|
/** Defensive snapshot parse — reuses the shared guarded parser (size cap,
|
|
52
58
|
* plain-object check, depth cap, prototype-key stripping). */
|
|
53
|
-
|
|
59
|
+
const assertValidTag = (tag) => {
|
|
54
60
|
const trimmed = tag.trim();
|
|
55
|
-
if (/^[a-z][a-z0-9-]*$/
|
|
61
|
+
if (/^[a-z][a-z0-9-]*$/iu.test(trimmed)) return trimmed.toLowerCase();
|
|
56
62
|
return "div";
|
|
57
|
-
}
|
|
63
|
+
};
|
|
58
64
|
/** True when `candidate` is owned by `host`: walking up must not cross another
|
|
59
65
|
* island or slot boundary before reaching it. */
|
|
60
|
-
|
|
66
|
+
const belongsToHost = (host, candidate) => {
|
|
61
67
|
let el = candidate.parentElement;
|
|
62
68
|
while (el && el !== host) {
|
|
63
|
-
if (el.
|
|
64
|
-
if (el.
|
|
69
|
+
if (el.matches("[data-ilha]")) return false;
|
|
70
|
+
if (el.matches("[data-ilha-slot]") && el instanceof HTMLElement && el.style.display !== "contents") return false;
|
|
65
71
|
el = el.parentElement;
|
|
66
72
|
}
|
|
67
73
|
return el === host;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
74
|
+
};
|
|
75
|
+
const sleepMs = (ms) => {
|
|
76
|
+
const { promise, resolve } = Promise.withResolvers();
|
|
77
|
+
setTimeout(() => {
|
|
78
|
+
resolve(null);
|
|
79
|
+
}, ms);
|
|
80
|
+
return promise;
|
|
81
|
+
};
|
|
82
|
+
const abortAwareDelay = async (ms, signal, abortError) => {
|
|
83
|
+
if (signal.aborted) throw abortError;
|
|
84
|
+
await sleepMs(ms);
|
|
85
|
+
if (signal.aborted) throw abortError;
|
|
86
|
+
};
|
|
87
|
+
const isRetryableRpcFailure = (error) => {
|
|
88
|
+
const msg = String(error);
|
|
89
|
+
return msg.includes("404") || msg.includes("503") || msg.includes("empty HTTP response") || msg.includes("HttpError") || msg.includes("RpcClientDefect") || msg.includes("RpcClientError") || msg.includes("TransportError") || msg.includes("InvalidUrlError");
|
|
90
|
+
};
|
|
91
|
+
const isAbortLike = (error, signal) => {
|
|
92
|
+
if (signal.aborted) return true;
|
|
93
|
+
return error.name === "AbortError";
|
|
94
|
+
};
|
|
95
|
+
const closeStream = async (gen) => {
|
|
96
|
+
if (!gen) return;
|
|
97
|
+
try {
|
|
98
|
+
await gen.return?.(void 0);
|
|
99
|
+
} catch {}
|
|
100
|
+
};
|
|
101
|
+
const pullStream = async (gen, step, key, state, signal, onValue) => {
|
|
102
|
+
if (signal.aborted || step.done) return;
|
|
103
|
+
state[key] = step.value;
|
|
104
|
+
onValue();
|
|
105
|
+
const next = await gen.next();
|
|
106
|
+
await pullStream(gen, next, key, state, signal, onValue);
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* Pull a wired server stream until it ends or the island unmounts.
|
|
110
|
+
* Dev cold-start and flaky Oxide RPC transports (empty bodies, aborted
|
|
111
|
+
* fetches during Vite dep reloads) are retried with the same generator
|
|
112
|
+
* factory — including failures after the first chunk.
|
|
113
|
+
*/
|
|
114
|
+
const resumeServerStream = async (fn, key, state, controller, onValue, attempt = 0) => {
|
|
115
|
+
if (controller.signal.aborted) return;
|
|
116
|
+
let gen;
|
|
117
|
+
try {
|
|
118
|
+
gen = await fn(controller.signal);
|
|
119
|
+
const first = await gen.next();
|
|
120
|
+
await pullStream(gen, first, key, state, controller.signal, onValue);
|
|
121
|
+
await closeStream(gen);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
await closeStream(gen);
|
|
124
|
+
if (isAbortLike(error, controller.signal)) return;
|
|
125
|
+
if (!isRetryableRpcFailure(error) || attempt >= 3) {
|
|
126
|
+
console.error(`[ilha-router] stream "${key}" failed:`, error);
|
|
127
|
+
return;
|
|
101
128
|
}
|
|
129
|
+
await abortAwareDelay(50 * (attempt + 1), controller.signal, error);
|
|
130
|
+
return resumeServerStream(fn, key, state, controller, onValue, attempt + 1);
|
|
102
131
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
function hydrateServerIsland(host, id, wiring, props, moduleKey) {
|
|
132
|
+
};
|
|
133
|
+
const hydrateServerIsland = (host, id, wiring, props, moduleKey) => {
|
|
106
134
|
const controller = new AbortController();
|
|
107
|
-
const cleanups = [
|
|
108
|
-
|
|
135
|
+
const cleanups = [() => {
|
|
136
|
+
controller.abort();
|
|
137
|
+
}];
|
|
109
138
|
const state = {};
|
|
110
139
|
const rawState = host.getAttribute(STATE_ATTR);
|
|
111
140
|
if (rawState) {
|
|
@@ -114,30 +143,12 @@ function hydrateServerIsland(host, id, wiring, props, moduleKey) {
|
|
|
114
143
|
for (const [key, value] of Object.entries(parsed)) if (!key.startsWith("_")) state[key] = value;
|
|
115
144
|
}
|
|
116
145
|
}
|
|
117
|
-
const frame = wiring
|
|
146
|
+
const { frame } = wiring;
|
|
118
147
|
let currentProps = props;
|
|
119
148
|
let repaintChain = Promise.resolve();
|
|
120
|
-
const scheduleRepaint = () => {
|
|
121
|
-
if (!frame || controller.signal.aborted) return;
|
|
122
|
-
const sent = currentProps;
|
|
123
|
-
repaintChain = repaintChain.then(async () => {
|
|
124
|
-
if (controller.signal.aborted || !host.isConnected) return;
|
|
125
|
-
const html = await frame(sent);
|
|
126
|
-
if (controller.signal.aborted || typeof html !== "string") return;
|
|
127
|
-
const parsed = new DOMParser().parseFromString(`<div>${html}</div>`, "text/html");
|
|
128
|
-
host.replaceChildren(...Array.from(parsed.body.firstElementChild?.childNodes ?? []));
|
|
129
|
-
syncChildren();
|
|
130
|
-
wireEvents();
|
|
131
|
-
}).catch((err) => {
|
|
132
|
-
if (!controller.signal.aborted) console.error(`[ilha-router] frame render failed for "${id}":`, err);
|
|
133
|
-
});
|
|
134
|
-
};
|
|
135
|
-
const repaintModule = () => {
|
|
136
|
-
if (moduleKey) __ilhaRepaintServerModule(moduleKey);
|
|
137
|
-
else scheduleRepaint();
|
|
138
|
-
};
|
|
139
|
-
if (moduleKey) cleanups.push(linkServerModuleRepaints(moduleKey, scheduleRepaint));
|
|
140
149
|
const attached = [];
|
|
150
|
+
const mountedChildren = /* @__PURE__ */ new Map();
|
|
151
|
+
const hooks = { repaintModule: () => {} };
|
|
141
152
|
const readManifest = () => {
|
|
142
153
|
const raw = host.querySelector(`:scope > template[${ACTIONS_ATTR}], template[${ACTIONS_ATTR}]`)?.getAttribute(ACTIONS_ATTR) ?? host.getAttribute(ACTIONS_ATTR) ?? null;
|
|
143
154
|
return raw ? parseSnapshotAttr(raw) : void 0;
|
|
@@ -147,7 +158,7 @@ function hydrateServerIsland(host, id, wiring, props, moduleKey) {
|
|
|
147
158
|
attached.length = 0;
|
|
148
159
|
const manifest = readManifest();
|
|
149
160
|
if (!manifest) return;
|
|
150
|
-
const sentinels = [host, ...
|
|
161
|
+
const sentinels = [host, ...host.querySelectorAll(`[${EVENT_SENTINEL_ATTR}]`)];
|
|
151
162
|
for (const el of sentinels) {
|
|
152
163
|
if (!el.hasAttribute(EVENT_SENTINEL_ATTR)) continue;
|
|
153
164
|
if (el !== host && !belongsToHost(host, el)) continue;
|
|
@@ -155,48 +166,60 @@ function hydrateServerIsland(host, id, wiring, props, moduleKey) {
|
|
|
155
166
|
for (const part of spec.split(",")) {
|
|
156
167
|
const sep = part.lastIndexOf(":");
|
|
157
168
|
if (sep < 1) continue;
|
|
158
|
-
const
|
|
169
|
+
const eventType = part.slice(0, sep);
|
|
159
170
|
const entry = manifest[part];
|
|
160
171
|
let actionKey;
|
|
161
172
|
let callArgs = [];
|
|
162
|
-
if (
|
|
163
|
-
else if (entry &&
|
|
173
|
+
if (isString(entry)) actionKey = entry;
|
|
174
|
+
else if (entry && isObject(entry)) {
|
|
164
175
|
actionKey = String(entry.k);
|
|
165
176
|
if (Array.isArray(entry.a)) callArgs = entry.a;
|
|
166
177
|
}
|
|
167
|
-
const
|
|
168
|
-
if (!
|
|
178
|
+
const actionFn = actionKey === null || actionKey === void 0 ? void 0 : wiring.actions?.[actionKey];
|
|
179
|
+
if (!actionFn) continue;
|
|
180
|
+
const boundKey = actionKey;
|
|
169
181
|
const listener = () => {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
182
|
+
const run = async () => {
|
|
183
|
+
try {
|
|
184
|
+
await Promise.resolve(actionFn(...callArgs));
|
|
185
|
+
hooks.repaintModule();
|
|
186
|
+
} catch (error) {
|
|
187
|
+
console.error(`[ilha-router] action "${String(boundKey)}" failed:`, error);
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
run();
|
|
173
191
|
};
|
|
174
|
-
el.addEventListener(
|
|
192
|
+
el.addEventListener(eventType, listener);
|
|
175
193
|
attached.push({
|
|
176
194
|
el,
|
|
177
|
-
|
|
178
|
-
|
|
195
|
+
listener,
|
|
196
|
+
type: eventType
|
|
179
197
|
});
|
|
180
198
|
}
|
|
181
199
|
}
|
|
182
200
|
};
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
if (!value || typeof value !== "object") continue;
|
|
201
|
+
const reviveChildProps = (childProps) => {
|
|
202
|
+
if (!childProps) return;
|
|
203
|
+
const next = { ...childProps };
|
|
204
|
+
for (const [key, value] of Object.entries(childProps)) {
|
|
205
|
+
if (!value || !isObject(value)) continue;
|
|
189
206
|
const marker = value;
|
|
190
|
-
if (marker.__ilha !== "action" ||
|
|
191
|
-
const
|
|
192
|
-
if (!
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
207
|
+
if (marker.__ilha !== "action" || !isString(marker.k)) continue;
|
|
208
|
+
const actionFn = wiring.actions?.[marker.k];
|
|
209
|
+
if (!actionFn) continue;
|
|
210
|
+
let args = [];
|
|
211
|
+
if (Array.isArray(marker.a)) args = marker.a;
|
|
212
|
+
const revived = (..._runtimeArgs) => {
|
|
213
|
+
const run = async () => {
|
|
214
|
+
const result = await Promise.resolve(actionFn(...args));
|
|
215
|
+
hooks.repaintModule();
|
|
216
|
+
return result;
|
|
217
|
+
};
|
|
218
|
+
return run();
|
|
219
|
+
};
|
|
220
|
+
Object.assign(next, { [key]: revived });
|
|
198
221
|
}
|
|
199
|
-
return
|
|
222
|
+
return next;
|
|
200
223
|
};
|
|
201
224
|
const syncChildren = () => {
|
|
202
225
|
for (const [el, handle] of mountedChildren) {
|
|
@@ -206,38 +229,46 @@ function hydrateServerIsland(host, id, wiring, props, moduleKey) {
|
|
|
206
229
|
}
|
|
207
230
|
for (const el of host.querySelectorAll(`[${CLIENT_REF_ATTR}]`)) {
|
|
208
231
|
if (!belongsToHost(host, el)) continue;
|
|
209
|
-
const
|
|
232
|
+
const childProps = reviveChildProps(parseSnapshotAttr(el.getAttribute(PROPS_ATTR) ?? "") ?? void 0);
|
|
210
233
|
const mounted = mountedChildren.get(el);
|
|
211
234
|
if (mounted) {
|
|
212
|
-
mounted.updateProps(
|
|
235
|
+
mounted.updateProps(childProps);
|
|
213
236
|
continue;
|
|
214
237
|
}
|
|
215
238
|
const mount = (wiring.children?.[el.getAttribute(CLIENT_REF_ATTR) ?? ""])?.[ISLAND_MOUNT_INTERNAL];
|
|
216
|
-
if (mount) mountedChildren.set(el, mount(el,
|
|
239
|
+
if (mount) mountedChildren.set(el, mount(el, childProps));
|
|
217
240
|
}
|
|
218
241
|
};
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
242
|
+
const applyFrameHtml = async (sent) => {
|
|
243
|
+
if (!frame || controller.signal.aborted || !host.isConnected) return;
|
|
244
|
+
const html = await frame(sent);
|
|
245
|
+
if (controller.signal.aborted || !isString(html)) return;
|
|
246
|
+
const parsed = new DOMParser().parseFromString(`<div>${html}</div>`, "text/html");
|
|
247
|
+
host.replaceChildren(...parsed.body.firstElementChild?.childNodes ?? []);
|
|
248
|
+
syncChildren();
|
|
249
|
+
wireEvents();
|
|
250
|
+
};
|
|
251
|
+
const scheduleRepaint = () => {
|
|
252
|
+
if (!frame || controller.signal.aborted) return;
|
|
253
|
+
const sent = currentProps;
|
|
254
|
+
const previous = repaintChain;
|
|
255
|
+
repaintChain = (async () => {
|
|
256
|
+
await previous;
|
|
223
257
|
try {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
if (controller.signal.aborted || done) break;
|
|
228
|
-
state[key] = value;
|
|
229
|
-
repaintModule();
|
|
230
|
-
step = await gen.next();
|
|
231
|
-
}
|
|
232
|
-
} catch (err) {
|
|
233
|
-
if (!controller.signal.aborted && err?.name !== "AbortError") console.error(`[ilha-router] stream "${key}" failed:`, err);
|
|
234
|
-
} finally {
|
|
235
|
-
Promise.resolve(gen.return?.(void 0)).catch(() => {});
|
|
258
|
+
await applyFrameHtml(sent);
|
|
259
|
+
} catch (error) {
|
|
260
|
+
if (!controller.signal.aborted) console.error(`[ilha-router] frame render failed for "${id}":`, error);
|
|
236
261
|
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
262
|
+
})();
|
|
263
|
+
};
|
|
264
|
+
hooks.repaintModule = () => {
|
|
265
|
+
if (moduleKey) __ilhaRepaintServerModule(moduleKey);
|
|
266
|
+
else scheduleRepaint();
|
|
267
|
+
};
|
|
268
|
+
if (moduleKey) cleanups.push(linkServerModuleRepaints(moduleKey, scheduleRepaint));
|
|
269
|
+
wireEvents();
|
|
270
|
+
syncChildren();
|
|
271
|
+
for (const [key, fn] of Object.entries(wiring.streams ?? {})) resumeServerStream(fn, key, state, controller, hooks.repaintModule);
|
|
241
272
|
if (frame && !host.hasAttribute(STATE_ATTR) && host.childNodes.length === 0) scheduleRepaint();
|
|
242
273
|
return {
|
|
243
274
|
unmount: () => {
|
|
@@ -253,8 +284,8 @@ function hydrateServerIsland(host, id, wiring, props, moduleKey) {
|
|
|
253
284
|
scheduleRepaint();
|
|
254
285
|
}
|
|
255
286
|
};
|
|
256
|
-
}
|
|
257
|
-
|
|
287
|
+
};
|
|
288
|
+
const __ilhaServerIsland = (id, as, wiring = {}, moduleKey) => {
|
|
258
289
|
const slotTag = assertValidTag(as);
|
|
259
290
|
const island = ((props) => {
|
|
260
291
|
return "";
|
|
@@ -262,20 +293,19 @@ function __ilhaServerIsland(id, as, wiring = {}, moduleKey) {
|
|
|
262
293
|
island[ISLAND] = true;
|
|
263
294
|
island[ISLAND_SLOT_TAG] = slotTag;
|
|
264
295
|
island.toString = () => "";
|
|
265
|
-
const ISLAND_CALL = Symbol.for("ilha.islandCall");
|
|
266
296
|
island.key = (slotKey) => {
|
|
267
|
-
if (
|
|
268
|
-
return (
|
|
297
|
+
if (!isString(slotKey) || slotKey.trim().length === 0 || slotKey.includes(":")) throw new Error("server island key() requires a non-empty key without \":\".");
|
|
298
|
+
return (callProps) => ({
|
|
269
299
|
[ISLAND_CALL]: true,
|
|
270
300
|
island,
|
|
271
|
-
|
|
272
|
-
|
|
301
|
+
key: slotKey,
|
|
302
|
+
props: callProps
|
|
273
303
|
});
|
|
274
304
|
};
|
|
275
305
|
island[ISLAND_MOUNT_INTERNAL] = (host, mountProps) => hydrateServerIsland(host, id, wiring, mountProps, moduleKey);
|
|
276
306
|
island.mount = (host, mountProps) => hydrateServerIsland(host, id, wiring, mountProps, moduleKey).unmount;
|
|
277
307
|
return island;
|
|
278
|
-
}
|
|
308
|
+
};
|
|
279
309
|
|
|
280
310
|
//#endregion
|
|
281
|
-
export { __ilhaApplyHead, __ilhaRepaintServerModule, __ilhaServerIsland };
|
|
311
|
+
export { ISLAND_MOUNT_INTERNAL, __ilhaApplyHead, __ilhaRepaintServerModule, __ilhaServerIsland };
|