@atolljs/islands 0.1.0
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 +346 -0
- package/package.json +29 -0
- package/src/app.ts +81 -0
- package/src/callbackProps.ts +75 -0
- package/src/index.ts +40 -0
- package/src/island.ts +905 -0
- package/src/memory.ts +30 -0
- package/src/ops.ts +215 -0
- package/src/react-reconciler.d.ts +18 -0
- package/src/worker/defineWorkers.ts +666 -0
- package/src/worker/dom/css.ts +152 -0
- package/src/worker/dom/document.ts +589 -0
- package/src/worker/dom/element.ts +656 -0
- package/src/worker/dom/html.ts +107 -0
- package/src/worker/dom/node.ts +481 -0
- package/src/worker/dom/selectors.ts +108 -0
- package/src/worker/dom/window.ts +528 -0
- package/src/worker/hostConfig.ts +446 -0
- package/src/worker/index.ts +78 -0
- package/src/worker/instance.ts +317 -0
- package/src/worker/proxyDom.ts +28 -0
- package/src/worker/reactInstance.ts +74 -0
- package/src/worker/slot.tsx +18 -0
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
import { parseDocument, ElementType } from 'htmlparser2';
|
|
2
|
+
import {
|
|
3
|
+
allocId,
|
|
4
|
+
bumpOpsVersion,
|
|
5
|
+
getActiveInstance,
|
|
6
|
+
getLastActiveInstance,
|
|
7
|
+
getInstanceSize,
|
|
8
|
+
instances,
|
|
9
|
+
markInstanceActive,
|
|
10
|
+
pushOp,
|
|
11
|
+
registerHandler,
|
|
12
|
+
unregisterHandler,
|
|
13
|
+
} from '../instance';
|
|
14
|
+
import type { ElementInstance, HostInstance, TextInstance } from '../instance';
|
|
15
|
+
import type { EventPayload, Op } from '../../ops';
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
activeInstanceDoc,
|
|
19
|
+
ambientDoc,
|
|
20
|
+
createProxyDocument,
|
|
21
|
+
docForInstance,
|
|
22
|
+
type InternalDocument,
|
|
23
|
+
type ProxyDocument,
|
|
24
|
+
} from './document';
|
|
25
|
+
import { ProxyElement } from './element';
|
|
26
|
+
import {
|
|
27
|
+
ProxyNode,
|
|
28
|
+
ProxyText,
|
|
29
|
+
ProxyComment,
|
|
30
|
+
listenerOptsForWire,
|
|
31
|
+
normalizeListenerOptions,
|
|
32
|
+
type ProxyEventHandler,
|
|
33
|
+
} from './node';
|
|
34
|
+
|
|
35
|
+
/* ── The global shim ───────────────────────────────────────────────────── */
|
|
36
|
+
|
|
37
|
+
/** What installDomShim puts on globalThis.window — a facade OBJECT, not
|
|
38
|
+
* globalThis itself (the pool's message channel lives on self). */
|
|
39
|
+
export interface WindowShim {
|
|
40
|
+
document: ProxyDocument;
|
|
41
|
+
navigator: { userAgent: string };
|
|
42
|
+
location: { href: string; reload(): void; assign(url: string): void; replace(url: string): void };
|
|
43
|
+
innerWidth: number;
|
|
44
|
+
innerHeight: number;
|
|
45
|
+
devicePixelRatio: number;
|
|
46
|
+
requestAnimationFrame(cb: (time: number) => void): number;
|
|
47
|
+
cancelAnimationFrame(id?: number): void;
|
|
48
|
+
setTimeout(
|
|
49
|
+
callback: (...args: unknown[]) => void,
|
|
50
|
+
delay?: number,
|
|
51
|
+
...args: unknown[]
|
|
52
|
+
): number;
|
|
53
|
+
clearTimeout(id?: number): void;
|
|
54
|
+
setInterval(
|
|
55
|
+
callback: (...args: unknown[]) => void,
|
|
56
|
+
delay?: number,
|
|
57
|
+
...args: unknown[]
|
|
58
|
+
): number;
|
|
59
|
+
clearInterval(id?: number): void;
|
|
60
|
+
getComputedStyle(el: unknown): Record<string, never>;
|
|
61
|
+
/** Media queries can't be answered worker-side — always `matches: false`. */
|
|
62
|
+
matchMedia(query: string): MediaQueryList;
|
|
63
|
+
addEventListener(
|
|
64
|
+
type: string,
|
|
65
|
+
fn: ProxyEventHandler,
|
|
66
|
+
options?: boolean | AddEventListenerOptions,
|
|
67
|
+
): void;
|
|
68
|
+
removeEventListener(
|
|
69
|
+
type: string,
|
|
70
|
+
fn: ProxyEventHandler,
|
|
71
|
+
options?: boolean | EventListenerOptions,
|
|
72
|
+
): void;
|
|
73
|
+
/** Scrolling is main-thread business — no-ops so window.scrollTo(x,y)
|
|
74
|
+
* calls in libraries don't throw. */
|
|
75
|
+
scrollTo(x?: number | ScrollToOptions, y?: number): void;
|
|
76
|
+
scrollBy(x?: number | ScrollToOptions, y?: number): void;
|
|
77
|
+
scroll(x?: number | ScrollToOptions, y?: number): void;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The uninstall currently returned by the latest installDomShim — chained
|
|
82
|
+
* so installing a fresh document's shim first unwinds the previous one.
|
|
83
|
+
*/
|
|
84
|
+
let activeShimUninstall: (() => void) | null = null;
|
|
85
|
+
|
|
86
|
+
export interface WindowFacadeBundle {
|
|
87
|
+
facade: WindowShim;
|
|
88
|
+
/** Detach every surviving window listener — ops and handler-table entries. */
|
|
89
|
+
teardown(): void;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Build the `window` facade for one instance's document — shared by
|
|
94
|
+
* installDomShim (imperative apps) and the instance dispatcher (React mounts
|
|
95
|
+
* get a window the first time library code reads one). The bundle caches
|
|
96
|
+
* on the document so both paths hand out the SAME facade — a listener
|
|
97
|
+
* registered through `window` must land in the same table whichever global
|
|
98
|
+
* routed the call.
|
|
99
|
+
*/
|
|
100
|
+
const buildWindowFacade = (internal: InternalDocument): WindowFacadeBundle => {
|
|
101
|
+
const g = globalThis as Record<string, unknown>;
|
|
102
|
+
|
|
103
|
+
/** type → registered listener entries. Window listeners get their own
|
|
104
|
+
* table (a lib removing a document listener must not detach its window
|
|
105
|
+
* twin), but land on the same id-0 container as document listeners. */
|
|
106
|
+
const windowListeners = new Map<
|
|
107
|
+
string,
|
|
108
|
+
Array<{ fn: ProxyEventHandler; hid: number; capture: boolean }>
|
|
109
|
+
>();
|
|
110
|
+
|
|
111
|
+
const pushDocOp = (op: Op): void => {
|
|
112
|
+
pushOp(internal.instance, op);
|
|
113
|
+
if (getActiveInstance() !== internal.instance) bumpOpsVersion();
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const windowRemoveEventListener = (
|
|
117
|
+
type: string,
|
|
118
|
+
fn: ProxyEventHandler,
|
|
119
|
+
options?: boolean | EventListenerOptions,
|
|
120
|
+
): void => {
|
|
121
|
+
internal._assertAlive();
|
|
122
|
+
const capture = normalizeListenerOptions(options).capture;
|
|
123
|
+
const table = windowListeners.get(type);
|
|
124
|
+
const index = table?.findIndex((l) => l.fn === fn && l.capture === capture) ?? -1;
|
|
125
|
+
if (index === -1 || table === undefined) return;
|
|
126
|
+
const [entry] = table.splice(index, 1);
|
|
127
|
+
unregisterHandler(entry.hid);
|
|
128
|
+
internal._handlerIds.delete(entry.hid);
|
|
129
|
+
pushDocOp({
|
|
130
|
+
t: 'unlisten',
|
|
131
|
+
id: 0,
|
|
132
|
+
type,
|
|
133
|
+
handler: entry.hid,
|
|
134
|
+
opts: entry.capture ? { capture: true } : undefined,
|
|
135
|
+
});
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const windowAddEventListener = (
|
|
139
|
+
type: string,
|
|
140
|
+
fn: ProxyEventHandler,
|
|
141
|
+
options?: boolean | AddEventListenerOptions,
|
|
142
|
+
): void => {
|
|
143
|
+
internal._assertAlive();
|
|
144
|
+
const opts = normalizeListenerOptions(options);
|
|
145
|
+
let table = windowListeners.get(type);
|
|
146
|
+
if (table === undefined) windowListeners.set(type, (table = []));
|
|
147
|
+
// real DOM dedupes identical (type, listener, capture) triples
|
|
148
|
+
if (table.some((l) => l.fn === fn && l.capture === opts.capture)) return;
|
|
149
|
+
const hid = registerHandler(
|
|
150
|
+
(p) => {
|
|
151
|
+
try {
|
|
152
|
+
fn.call(facade, internal._enrichEvent(p as EventPayload, facade));
|
|
153
|
+
} finally {
|
|
154
|
+
if (opts.once) windowRemoveEventListener(type, fn, { capture: opts.capture });
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
internal.instance,
|
|
158
|
+
0, // window-level listener → target id 0 is the island root
|
|
159
|
+
);
|
|
160
|
+
table.push({ fn, hid, capture: opts.capture });
|
|
161
|
+
internal._handlerIds.add(hid);
|
|
162
|
+
pushDocOp({ t: 'listen', id: 0, type, handler: hid, opts: listenerOptsForWire(options) });
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const raf = g.requestAnimationFrame as ((cb: (time: number) => void) => number) | undefined;
|
|
166
|
+
const caf = g.cancelAnimationFrame as ((id: number) => void) | undefined;
|
|
167
|
+
const globalSetTimeout = g.setTimeout as typeof setTimeout;
|
|
168
|
+
const globalSetInterval = g.setInterval as typeof setInterval;
|
|
169
|
+
const globalClearTimeout = g.clearTimeout as typeof clearTimeout;
|
|
170
|
+
const globalClearInterval = g.clearInterval as typeof clearInterval;
|
|
171
|
+
|
|
172
|
+
// A instance owns the timers/rAFs it schedules through this facade; teardown
|
|
173
|
+
// cancels them so deferred library work (Leaflet drag inertia, scroll-zoom
|
|
174
|
+
// debounces, etc.) doesn't outlive the document and mutate a dead or
|
|
175
|
+
// foreign ambient DOM. We wrap callbacks so completed ones drop their id.
|
|
176
|
+
const timeouts = new Set<number>();
|
|
177
|
+
const intervals = new Set<number>();
|
|
178
|
+
const rafs = new Set<number>();
|
|
179
|
+
|
|
180
|
+
const facade: WindowShim = {
|
|
181
|
+
document: internal,
|
|
182
|
+
navigator: { userAgent: 'atoll-islands' },
|
|
183
|
+
location: {
|
|
184
|
+
href: 'about:blank',
|
|
185
|
+
reload() {},
|
|
186
|
+
assign(_url: string) {},
|
|
187
|
+
replace(_url: string) {},
|
|
188
|
+
},
|
|
189
|
+
get innerWidth(): number {
|
|
190
|
+
// Fed by the pushed container size — the viewport the island lives in.
|
|
191
|
+
return getInstanceSize(internal.instance)?.w ?? 0;
|
|
192
|
+
},
|
|
193
|
+
get innerHeight(): number {
|
|
194
|
+
return getInstanceSize(internal.instance)?.h ?? 0;
|
|
195
|
+
},
|
|
196
|
+
devicePixelRatio: 1,
|
|
197
|
+
// Workers normally have no rAF — pass through when one exists (in-process
|
|
198
|
+
// test), degrade to a 16ms timer otherwise. Schedule through the host
|
|
199
|
+
// timers but keep track of the handles so teardown can cancel pending
|
|
200
|
+
// callbacks (Leaflet's drag/zoom animations, etc.).
|
|
201
|
+
requestAnimationFrame(cb: (time: number) => void): number {
|
|
202
|
+
let id: number;
|
|
203
|
+
const wrapped = (time: number): void => {
|
|
204
|
+
rafs.delete(id);
|
|
205
|
+
cb(time);
|
|
206
|
+
};
|
|
207
|
+
id = typeof raf === 'function'
|
|
208
|
+
? raf.call(globalThis, wrapped)
|
|
209
|
+
: (globalSetTimeout((stamp: number) => wrapped(stamp), 16, Date.now()) as unknown as number);
|
|
210
|
+
rafs.add(id);
|
|
211
|
+
return id;
|
|
212
|
+
},
|
|
213
|
+
cancelAnimationFrame(id?: number): void {
|
|
214
|
+
if (id === undefined) return;
|
|
215
|
+
rafs.delete(id);
|
|
216
|
+
if (typeof caf === 'function') caf.call(globalThis, id);
|
|
217
|
+
else globalClearTimeout(id);
|
|
218
|
+
},
|
|
219
|
+
setTimeout(...args: Parameters<typeof setTimeout>): number {
|
|
220
|
+
const [cb, delay, ...rest] = args as [
|
|
221
|
+
cb: (...a: unknown[]) => void,
|
|
222
|
+
delay?: number,
|
|
223
|
+
...rest: unknown[],
|
|
224
|
+
];
|
|
225
|
+
let id: number;
|
|
226
|
+
const wrapped = (...a: unknown[]): void => {
|
|
227
|
+
timeouts.delete(id);
|
|
228
|
+
cb(...a);
|
|
229
|
+
};
|
|
230
|
+
id = globalSetTimeout(wrapped, delay, ...rest) as unknown as number;
|
|
231
|
+
timeouts.add(id);
|
|
232
|
+
return id;
|
|
233
|
+
},
|
|
234
|
+
clearTimeout(id?: number): void {
|
|
235
|
+
if (id === undefined) return;
|
|
236
|
+
timeouts.delete(id);
|
|
237
|
+
globalClearTimeout(id);
|
|
238
|
+
},
|
|
239
|
+
setInterval(...args: Parameters<typeof setInterval>): number {
|
|
240
|
+
const [cb, delay, ...rest] = args as [
|
|
241
|
+
cb: (...a: unknown[]) => void,
|
|
242
|
+
delay?: number,
|
|
243
|
+
...rest: unknown[],
|
|
244
|
+
];
|
|
245
|
+
const id = globalSetInterval(cb, delay, ...rest) as unknown as number;
|
|
246
|
+
intervals.add(id);
|
|
247
|
+
return id;
|
|
248
|
+
},
|
|
249
|
+
clearInterval(id?: number): void {
|
|
250
|
+
if (id === undefined) return;
|
|
251
|
+
intervals.delete(id);
|
|
252
|
+
globalClearInterval(id);
|
|
253
|
+
},
|
|
254
|
+
// No measurement channel — an empty declaration, same honesty rule as
|
|
255
|
+
// doc.getComputedStyle (which warns once); the facade stays silent.
|
|
256
|
+
getComputedStyle: () => ({}),
|
|
257
|
+
// No preference/quiz surface exists worker-side — media queries never
|
|
258
|
+
// match. The shape is complete enough for feature-detection code.
|
|
259
|
+
matchMedia: (query: string) =>
|
|
260
|
+
({
|
|
261
|
+
matches: false,
|
|
262
|
+
media: query,
|
|
263
|
+
onchange: null,
|
|
264
|
+
addListener: () => {},
|
|
265
|
+
removeListener: () => {},
|
|
266
|
+
addEventListener: () => {},
|
|
267
|
+
removeEventListener: () => {},
|
|
268
|
+
dispatchEvent: () => false,
|
|
269
|
+
}) as unknown as MediaQueryList,
|
|
270
|
+
addEventListener: windowAddEventListener,
|
|
271
|
+
removeEventListener: windowRemoveEventListener,
|
|
272
|
+
// No scroll surface exists worker-side — libraries call these during
|
|
273
|
+
// gesture handlers (Leaflet's Keyboard._onMouseDown scrolls the page
|
|
274
|
+
// back); honest no-ops.
|
|
275
|
+
scrollTo: () => {},
|
|
276
|
+
scrollBy: () => {},
|
|
277
|
+
scroll: () => {},
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const teardown = (): void => {
|
|
281
|
+
for (const id of rafs) {
|
|
282
|
+
if (typeof caf === 'function') caf.call(globalThis, id);
|
|
283
|
+
else globalClearTimeout(id);
|
|
284
|
+
}
|
|
285
|
+
rafs.clear();
|
|
286
|
+
for (const id of timeouts) globalClearTimeout(id);
|
|
287
|
+
timeouts.clear();
|
|
288
|
+
for (const id of intervals) globalClearInterval(id);
|
|
289
|
+
intervals.clear();
|
|
290
|
+
for (const [type, table] of windowListeners) {
|
|
291
|
+
for (const entry of table) {
|
|
292
|
+
unregisterHandler(entry.hid);
|
|
293
|
+
internal._handlerIds.delete(entry.hid);
|
|
294
|
+
if (!internal._disposed) {
|
|
295
|
+
pushOp(internal.instance, { t: 'unlisten', id: 0, type, handler: entry.hid });
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
table.length = 0;
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
return { facade, teardown };
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
/** The document's window facade, built once and shared by every path. */
|
|
306
|
+
const windowFacadeFor = (internal: InternalDocument): WindowFacadeBundle => {
|
|
307
|
+
if (internal._windowBundle === undefined) {
|
|
308
|
+
internal._windowBundle = buildWindowFacade(internal);
|
|
309
|
+
}
|
|
310
|
+
return internal._windowBundle;
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
let instanceDispatcherInstalled = false;
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Define `document`/`window`/`Element` as GLOBAL GETTERS that resolve to
|
|
317
|
+
* the current instance's proxy document — the mechanism that lets React-island
|
|
318
|
+
* libraries (recharts reading `window.getComputedStyle`, `document.body`)
|
|
319
|
+
* work without their app ever touching a doc, and that routes imperative
|
|
320
|
+
* library calls correctly when several mounts share one module (the
|
|
321
|
+
* in-process test layout).
|
|
322
|
+
*
|
|
323
|
+
* Resolution (docForCurrentContext): the active instance's doc inside tasks;
|
|
324
|
+
* the single registered doc when only one instance exists — every real island
|
|
325
|
+
* worker, so timer/promise callbacks from libraries still hit their own
|
|
326
|
+
* document; the most recent instance's doc as a final multi-instance fallback;
|
|
327
|
+
* the pre-install global otherwise (a real document in happy-dom tests —
|
|
328
|
+
* transparent to shell-side code, which never runs inside a instance task).
|
|
329
|
+
*/
|
|
330
|
+
/**
|
|
331
|
+
* Stand-ins for bare-global feature probes framework code runs inside a
|
|
332
|
+
* instance — a real worker has no `HTMLMediaElement` constructor or
|
|
333
|
+
* `customElements` registry, so `x instanceof HTMLMediaElement` /
|
|
334
|
+
* `customElements.get(...)` would ReferenceError. The class never matches
|
|
335
|
+
* an instanceof; the registry reports nothing defined. A host-provided
|
|
336
|
+
* global (happy-dom) wins over the stub whenever one exists.
|
|
337
|
+
*/
|
|
338
|
+
const HTML_MEDIA_STUB = class HTMLMediaElement {};
|
|
339
|
+
const CUSTOM_ELEMENTS_STUB = {
|
|
340
|
+
get: () => undefined,
|
|
341
|
+
define: () => {},
|
|
342
|
+
upgrade: () => {},
|
|
343
|
+
// Honest answer for "will this element ever upgrade" — it won't.
|
|
344
|
+
whenDefined: () => new Promise<never>(() => {}),
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
export function installInstanceDispatcher(): void {
|
|
348
|
+
if (instanceDispatcherInstalled) return;
|
|
349
|
+
instanceDispatcherInstalled = true;
|
|
350
|
+
const g = globalThis as Record<string, unknown>;
|
|
351
|
+
const prevDocument = g.document;
|
|
352
|
+
const prevWindow = g.window;
|
|
353
|
+
const prevElement = g.Element;
|
|
354
|
+
const prevNode = g.Node;
|
|
355
|
+
const prevText = g.Text;
|
|
356
|
+
const prevComment = g.Comment;
|
|
357
|
+
const prevMedia = g.HTMLMediaElement;
|
|
358
|
+
const prevCustomElements = g.customElements;
|
|
359
|
+
// Explicit assignments override the fallback (never the in-instance doc) —
|
|
360
|
+
// keeps pre-dispatcher semantics for out-of-instance code, and lets test
|
|
361
|
+
// harnesses/suites restore globals by plain assignment.
|
|
362
|
+
let docOverride: unknown;
|
|
363
|
+
let winOverride: unknown;
|
|
364
|
+
let elOverride: unknown;
|
|
365
|
+
|
|
366
|
+
Object.defineProperty(g, 'document', {
|
|
367
|
+
configurable: true,
|
|
368
|
+
enumerable: true,
|
|
369
|
+
get: () => activeInstanceDoc() ?? docOverride ?? ambientDoc() ?? prevDocument,
|
|
370
|
+
set: (v) => {
|
|
371
|
+
docOverride = v;
|
|
372
|
+
},
|
|
373
|
+
});
|
|
374
|
+
Object.defineProperty(g, 'window', {
|
|
375
|
+
configurable: true,
|
|
376
|
+
enumerable: true,
|
|
377
|
+
get: () => {
|
|
378
|
+
const doc = activeInstanceDoc();
|
|
379
|
+
if (doc !== undefined) return windowFacadeFor(doc).facade;
|
|
380
|
+
if (winOverride !== undefined) return winOverride;
|
|
381
|
+
const ambient = ambientDoc();
|
|
382
|
+
return ambient !== undefined ? windowFacadeFor(ambient).facade : prevWindow;
|
|
383
|
+
},
|
|
384
|
+
set: (v) => {
|
|
385
|
+
winOverride = v;
|
|
386
|
+
},
|
|
387
|
+
});
|
|
388
|
+
// Workers lack the Element constructor — `x instanceof Element` inside a
|
|
389
|
+
// library would ReferenceError. Point it at ProxyElement while a instance
|
|
390
|
+
// doc is resolvable; real HTMLElement checks never run worker-side.
|
|
391
|
+
Object.defineProperty(g, 'Element', {
|
|
392
|
+
configurable: true,
|
|
393
|
+
enumerable: true,
|
|
394
|
+
get: () => {
|
|
395
|
+
if (activeInstanceDoc() !== undefined) return ProxyElement;
|
|
396
|
+
if (elOverride !== undefined) return elOverride;
|
|
397
|
+
return ambientDoc() !== undefined ? ProxyElement : prevElement;
|
|
398
|
+
},
|
|
399
|
+
set: (v) => {
|
|
400
|
+
elOverride = v;
|
|
401
|
+
},
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Class globals — same resolution shape as `Element` above (in-instance →
|
|
406
|
+
* proxy class/stub; else explicit override; else ambient → proxy/stub;
|
|
407
|
+
* else the captured pre-dispatcher value). Frameworks cache prototype
|
|
408
|
+
* getters (`Node.prototype.firstChild`) and do `x instanceof Comment`
|
|
409
|
+
* before touching the document, so these must BE the proxy classes —
|
|
410
|
+
* not dispatchers that return different objects per instance.
|
|
411
|
+
*/
|
|
412
|
+
const installClassGlobal = (name: string, instanceValue: unknown, prev: unknown): void => {
|
|
413
|
+
let override: unknown;
|
|
414
|
+
Object.defineProperty(g, name, {
|
|
415
|
+
configurable: true,
|
|
416
|
+
enumerable: true,
|
|
417
|
+
get: () => {
|
|
418
|
+
if (activeInstanceDoc() !== undefined) return instanceValue;
|
|
419
|
+
if (override !== undefined) return override;
|
|
420
|
+
return ambientDoc() !== undefined ? instanceValue : prev;
|
|
421
|
+
},
|
|
422
|
+
set: (v) => {
|
|
423
|
+
override = v;
|
|
424
|
+
},
|
|
425
|
+
});
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
installClassGlobal('Node', ProxyNode, prevNode);
|
|
429
|
+
installClassGlobal('Text', ProxyText, prevText);
|
|
430
|
+
installClassGlobal('Comment', ProxyComment, prevComment);
|
|
431
|
+
installClassGlobal(
|
|
432
|
+
'HTMLMediaElement',
|
|
433
|
+
typeof prevMedia === 'function' ? prevMedia : HTML_MEDIA_STUB,
|
|
434
|
+
prevMedia,
|
|
435
|
+
);
|
|
436
|
+
installClassGlobal(
|
|
437
|
+
'customElements',
|
|
438
|
+
prevCustomElements !== undefined ? prevCustomElements : CUSTOM_ELEMENTS_STUB,
|
|
439
|
+
prevCustomElements,
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Install this document as the instance's DOM surface — the entry point for
|
|
445
|
+
* running real DOM-dependent libraries unmodified inside an island instance:
|
|
446
|
+
*
|
|
447
|
+
* const doc = createProxyDocument(instance);
|
|
448
|
+
* installDomShim(doc);
|
|
449
|
+
* SomeVendorLib.mount(doc.body); // uses document./window./innerHTML…
|
|
450
|
+
*
|
|
451
|
+
* Under the instance dispatcher (installed here automatically, and by
|
|
452
|
+
* definePolyWorker for every island worker) `globalThis.document` and
|
|
453
|
+
* `globalThis.window` already resolve to this document while its instance is
|
|
454
|
+
* active — so this call's real work is building the window facade
|
|
455
|
+
* (navigator/location stubs, timers + rAF passthrough, innerWidth/Height
|
|
456
|
+
* from the pushed container size, empty getComputedStyle, never-matching
|
|
457
|
+
* matchMedia, addEventListener wired to `listen` ops on id 0) and exposing
|
|
458
|
+
* it as `document.defaultView`.
|
|
459
|
+
*
|
|
460
|
+
* It also pins the CLASS globals (`Node`/`Text`/`Comment` → the proxy
|
|
461
|
+
* classes; `HTMLMediaElement`/`customElements` → never-match stubs) for
|
|
462
|
+
* framework feature probes — the instance dispatcher resolves them
|
|
463
|
+
* ambiently anyway, so the assignment makes this document's install
|
|
464
|
+
* explicit and restores the previous values on uninstall.
|
|
465
|
+
*
|
|
466
|
+
* What does NOT get touched — deliberately:
|
|
467
|
+
* - `globalThis.addEventListener`/`removeEventListener` and `self` — the
|
|
468
|
+
* pool's `self.onmessage` channel lives there; `window` is a facade
|
|
469
|
+
* object, not the global.
|
|
470
|
+
* - Any other global (history, fetch, localStorage…) — libraries touching
|
|
471
|
+
* them fail loudly, which is the honest answer.
|
|
472
|
+
*
|
|
473
|
+
* Returns `uninstall()` tearing down the facade's listeners. Also runs
|
|
474
|
+
* automatically on `doc.dispose()` (the updateProps rebuild path) and is
|
|
475
|
+
* superseded by a later installDomShim call — the last install wins.
|
|
476
|
+
*/
|
|
477
|
+
export function installDomShim(doc: ProxyDocument): () => void {
|
|
478
|
+
const internal = doc as InternalDocument;
|
|
479
|
+
installInstanceDispatcher();
|
|
480
|
+
const g = globalThis as Record<string, unknown>;
|
|
481
|
+
activeShimUninstall?.();
|
|
482
|
+
activeShimUninstall = null;
|
|
483
|
+
|
|
484
|
+
// Capture the ambient state BEFORE this doc claims it — uninstall puts
|
|
485
|
+
// it all back, so chained installs restore true originals.
|
|
486
|
+
const prevInstance = getLastActiveInstance();
|
|
487
|
+
const prevDocument = g.document;
|
|
488
|
+
const prevWindow = g.window;
|
|
489
|
+
const prevElement = g.Element;
|
|
490
|
+
const prevNode = g.Node;
|
|
491
|
+
const prevText = g.Text;
|
|
492
|
+
const prevComment = g.Comment;
|
|
493
|
+
const prevMedia = g.HTMLMediaElement;
|
|
494
|
+
const prevCustomElements = g.customElements;
|
|
495
|
+
|
|
496
|
+
// The installed document becomes the ambient one for out-of-instance
|
|
497
|
+
// readers: the explicit assignment wins the dispatcher's resolution,
|
|
498
|
+
// and the instance is marked for implicit resolution paths.
|
|
499
|
+
markInstanceActive(internal.instance);
|
|
500
|
+
const bundle = windowFacadeFor(internal);
|
|
501
|
+
g.document = doc;
|
|
502
|
+
g.window = bundle.facade;
|
|
503
|
+
g.Element = ProxyElement;
|
|
504
|
+
g.Node = ProxyNode;
|
|
505
|
+
g.Text = ProxyText;
|
|
506
|
+
g.Comment = ProxyComment;
|
|
507
|
+
if (typeof g.HTMLMediaElement !== 'function') g.HTMLMediaElement = HTML_MEDIA_STUB;
|
|
508
|
+
if (g.customElements === undefined) g.customElements = CUSTOM_ELEMENTS_STUB;
|
|
509
|
+
|
|
510
|
+
const uninstall = (): void => {
|
|
511
|
+
bundle.teardown();
|
|
512
|
+
internal._windowBundle = undefined;
|
|
513
|
+
g.document = prevDocument;
|
|
514
|
+
g.window = prevWindow;
|
|
515
|
+
g.Element = prevElement;
|
|
516
|
+
g.Node = prevNode;
|
|
517
|
+
g.Text = prevText;
|
|
518
|
+
g.Comment = prevComment;
|
|
519
|
+
g.HTMLMediaElement = prevMedia;
|
|
520
|
+
g.customElements = prevCustomElements;
|
|
521
|
+
markInstanceActive(prevInstance);
|
|
522
|
+
if (activeShimUninstall === uninstall) activeShimUninstall = null;
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
internal._uninstallShim = uninstall;
|
|
526
|
+
activeShimUninstall = uninstall;
|
|
527
|
+
return uninstall;
|
|
528
|
+
}
|