@takazudo/zfb 2.22.1 → 3.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 +13 -16
- package/dist/config.d.ts +77 -25
- package/dist/config.js +27 -6
- package/dist/config.js.map +1 -1
- package/dist/content.d.ts +8 -22
- package/dist/content.js +6 -28
- package/dist/content.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/island-boundary.d.ts +4 -0
- package/dist/island-boundary.js +42 -0
- package/dist/island-boundary.js.map +1 -0
- package/dist/island.d.ts +2 -118
- package/dist/island.js +4 -284
- package/dist/island.js.map +1 -1
- package/dist/jsx-types.d.ts +2 -38
- package/dist/jsx-types.js +3 -10
- package/dist/jsx-types.js.map +1 -1
- package/dist/plugins.d.ts +40 -0
- package/dist/plugins.js.map +1 -1
- package/dist/runtime.d.ts +26 -77
- package/dist/runtime.js +307 -340
- package/dist/runtime.js.map +1 -1
- package/dist/zudo-react/client.d.ts +3 -0
- package/dist/zudo-react/client.js +3 -0
- package/dist/zudo-react/client.js.map +1 -0
- package/dist/zudo-react/description.d.ts +16 -0
- package/dist/zudo-react/description.js +64 -0
- package/dist/zudo-react/description.js.map +1 -0
- package/dist/zudo-react/dom-bindings.d.ts +13 -0
- package/dist/zudo-react/dom-bindings.js +58 -0
- package/dist/zudo-react/dom-bindings.js.map +1 -0
- package/dist/zudo-react/escape.d.ts +2 -0
- package/dist/zudo-react/escape.js +7 -0
- package/dist/zudo-react/escape.js.map +1 -0
- package/dist/zudo-react/forms.d.ts +29 -0
- package/dist/zudo-react/forms.js +371 -0
- package/dist/zudo-react/forms.js.map +1 -0
- package/dist/zudo-react/hydrate.d.ts +4 -0
- package/dist/zudo-react/hydrate.js +912 -0
- package/dist/zudo-react/hydrate.js.map +1 -0
- package/dist/zudo-react/index.d.ts +47 -0
- package/dist/zudo-react/index.js +13 -0
- package/dist/zudo-react/index.js.map +1 -0
- package/dist/zudo-react/island-root-type.d.ts +1 -0
- package/dist/zudo-react/island-root-type.js +2 -0
- package/dist/zudo-react/island-root-type.js.map +1 -0
- package/dist/zudo-react/jsx-dev-runtime.d.ts +8 -0
- package/dist/zudo-react/jsx-dev-runtime.js +6 -0
- package/dist/zudo-react/jsx-dev-runtime.js.map +1 -0
- package/dist/zudo-react/jsx-runtime.d.ts +5 -0
- package/dist/zudo-react/jsx-runtime.js +7 -0
- package/dist/zudo-react/jsx-runtime.js.map +1 -0
- package/dist/zudo-react/jsx-types.d.ts +216 -0
- package/dist/zudo-react/jsx-types.js +2 -0
- package/dist/zudo-react/jsx-types.js.map +1 -0
- package/dist/zudo-react/props-transport.d.ts +2 -0
- package/dist/zudo-react/props-transport.js +95 -0
- package/dist/zudo-react/props-transport.js.map +1 -0
- package/dist/zudo-react/reactive-types.d.ts +8 -0
- package/dist/zudo-react/reactive-types.js +2 -0
- package/dist/zudo-react/reactive-types.js.map +1 -0
- package/dist/zudo-react/reactive.d.ts +20 -0
- package/dist/zudo-react/reactive.js +176 -0
- package/dist/zudo-react/reactive.js.map +1 -0
- package/dist/zudo-react/render-html.d.ts +3 -0
- package/dist/zudo-react/render-html.js +535 -0
- package/dist/zudo-react/render-html.js.map +1 -0
- package/dist/zudo-react/root.d.ts +20 -0
- package/dist/zudo-react/root.js +74 -0
- package/dist/zudo-react/root.js.map +1 -0
- package/dist/zudo-react/scheduler.d.ts +12 -0
- package/dist/zudo-react/scheduler.js +113 -0
- package/dist/zudo-react/scheduler.js.map +1 -0
- package/dist/zudo-react/scope.d.ts +42 -0
- package/dist/zudo-react/scope.js +220 -0
- package/dist/zudo-react/scope.js.map +1 -0
- package/dist/zudo-react/server.d.ts +15 -0
- package/dist/zudo-react/server.js +11 -0
- package/dist/zudo-react/server.js.map +1 -0
- package/dist/zudo-react/structure.d.ts +16 -0
- package/dist/zudo-react/structure.js +49 -0
- package/dist/zudo-react/structure.js.map +1 -0
- package/dist/zudo-react/vocabulary.d.ts +12 -0
- package/dist/zudo-react/vocabulary.js +96 -0
- package/dist/zudo-react/vocabulary.js.map +1 -0
- package/package.json +28 -24
package/dist/runtime.js
CHANGED
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
// happy-dom or bare Node, and the absence of `IntersectionObserver` /
|
|
21
21
|
// `requestIdleCallback` / `matchMedia` is handled gracefully.
|
|
22
22
|
import { resolveWhen } from "./types.js";
|
|
23
|
+
import { ROOT_KEY } from "./zudo-react/root.js";
|
|
24
|
+
import { parseProps as parseOwnedProps } from "./zudo-react/props-transport.js";
|
|
23
25
|
const g = globalThis;
|
|
24
26
|
/**
|
|
25
27
|
* Internal variant of `scheduleHydrate` that also reports whether the fire
|
|
@@ -195,53 +197,89 @@ const PERSIST_ATTR = "data-zfb-transition-persist";
|
|
|
195
197
|
// string). Consumed by clearMountedForRemount(). See #1389.
|
|
196
198
|
const ISLAND_REMOUNT_ATTR = "data-zfb-island-remount";
|
|
197
199
|
/**
|
|
198
|
-
* Public
|
|
200
|
+
* Public observation marker; the symbol handle is the live-root guard.
|
|
199
201
|
*
|
|
200
|
-
* State table
|
|
201
|
-
*
|
|
202
|
-
* -
|
|
203
|
-
*
|
|
204
|
-
* -
|
|
205
|
-
* -
|
|
206
|
-
* -
|
|
207
|
-
*
|
|
208
|
-
* -
|
|
209
|
-
* `fn(props, element, mode)` returns, alongside the `mounted` entry.
|
|
210
|
-
* - missing manifest entry: absent; `scheduleMount` returns without writing.
|
|
211
|
-
* - no `mount` export: absent; both manifest paths return without writing.
|
|
212
|
-
* - synchronous mount throw: absent; the `mounted` entry is not written, so a
|
|
213
|
-
* later walk can retry the element.
|
|
214
|
-
* - rejected import: absent; the URL rejection handler clears `pending` and
|
|
215
|
-
* any defensive `mounted` entry.
|
|
216
|
-
* - detached during import: absent; the URL success handler clears `pending`
|
|
217
|
-
* and returns before calling mount.
|
|
218
|
-
* - unmounted (discarded): `unmountIslands` clears the marker and `mounted`
|
|
219
|
-
* entry in `finally`, even when the unmount thunk throws.
|
|
220
|
-
* - unmounted (persisted-lifted): retained together with the `mounted` entry;
|
|
221
|
-
* `unmountIslands` skips elements whose persist id exists in the incoming body.
|
|
222
|
-
* - props-changed remount: `clearMountedForRemount` clears the marker and map
|
|
223
|
-
* entry in `finally`, then the forced mount writes it again after mount returns.
|
|
224
|
-
* - dev hot-swap over a marked DOM: a fresh module's `mountIslands` strips the
|
|
225
|
-
* stale marker before scheduling, then writes it after its own mount returns.
|
|
202
|
+
* State table:
|
|
203
|
+
* - initial/deferred/missing entry/failed mount: marker and handle absent;
|
|
204
|
+
* - successful mount: both present;
|
|
205
|
+
* - discarded root: disposal leaves DOM for the body swap, then clears both;
|
|
206
|
+
* - unchanged island inside a retained persist boundary: handle and marker survive;
|
|
207
|
+
* - changed island inside a retained boundary: metadata and remount flag are set,
|
|
208
|
+
* then the post-swap scan disposes once and mounts in render mode;
|
|
209
|
+
* - removed island inside a retained boundary: dispose and detach before swap;
|
|
210
|
+
* - bundle re-import: dispose the old symbol handle before render mode replaces it.
|
|
226
211
|
*/
|
|
227
212
|
export const ISLAND_MOUNTED_ATTR = "data-zfb-island-mounted";
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
const
|
|
213
|
+
const COMPOSITION_KEY = Symbol.for("@takazudo/zfb/zudo-react/composition-v1");
|
|
214
|
+
const TRACKER_KEY = Symbol.for("@takazudo/zfb/zudo-react/composition-tracker-v1");
|
|
215
|
+
// A fresh bundle instance replaces roots installed by a previous instance.
|
|
216
|
+
// Ordinary repeat scans in the same instance keep their live handles.
|
|
217
|
+
const owned = new WeakSet();
|
|
218
|
+
function rootHandle(element) {
|
|
219
|
+
return element[ROOT_KEY];
|
|
220
|
+
}
|
|
221
|
+
function setRootHandle(element, handle) {
|
|
222
|
+
const target = element;
|
|
223
|
+
if (handle)
|
|
224
|
+
target[ROOT_KEY] = handle;
|
|
225
|
+
else
|
|
226
|
+
delete target[ROOT_KEY];
|
|
227
|
+
}
|
|
228
|
+
function reportIslandError(element, phase, error) {
|
|
229
|
+
const name = element.getAttribute("data-zfb-island") ??
|
|
230
|
+
element.getAttribute("data-zfb-island-skip-ssr") ??
|
|
231
|
+
"unknown";
|
|
232
|
+
try {
|
|
233
|
+
console.error(`[zfb] island "${name}" ${phase} failed`, error);
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
// A broken reporter must not stop other roots.
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
function disposeIsland(element, phase) {
|
|
240
|
+
const handle = rootHandle(element);
|
|
241
|
+
try {
|
|
242
|
+
handle?.dispose();
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
reportIslandError(element, phase, error);
|
|
246
|
+
}
|
|
247
|
+
finally {
|
|
248
|
+
if (rootHandle(element) === handle)
|
|
249
|
+
setRootHandle(element, undefined);
|
|
250
|
+
element.removeAttribute(ISLAND_MOUNTED_ATTR);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function installCompositionTracker(doc) {
|
|
254
|
+
const target = doc;
|
|
255
|
+
if (target[TRACKER_KEY])
|
|
256
|
+
return;
|
|
257
|
+
const set = (event, value) => {
|
|
258
|
+
if (event.target instanceof Element) {
|
|
259
|
+
event.target[COMPOSITION_KEY] = value;
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
const start = (event) => set(event, "active");
|
|
263
|
+
const end = (event) => set(event, "idle");
|
|
264
|
+
const input = (event) => {
|
|
265
|
+
if (event.isComposing)
|
|
266
|
+
set(event, "active");
|
|
267
|
+
};
|
|
268
|
+
doc.addEventListener("compositionstart", start, true);
|
|
269
|
+
doc.addEventListener("compositionend", end, true);
|
|
270
|
+
doc.addEventListener("input", input, true);
|
|
271
|
+
doc.addEventListener("blur", end, true);
|
|
272
|
+
target[TRACKER_KEY] = () => {
|
|
273
|
+
doc.removeEventListener("compositionstart", start, true);
|
|
274
|
+
doc.removeEventListener("compositionend", end, true);
|
|
275
|
+
doc.removeEventListener("input", input, true);
|
|
276
|
+
doc.removeEventListener("blur", end, true);
|
|
277
|
+
delete target[TRACKER_KEY];
|
|
278
|
+
};
|
|
279
|
+
}
|
|
233
280
|
// Elements for which the nested-island self-wrap warning has already been
|
|
234
281
|
// emitted. Guards against repeated warn spam across re-walks (e.g. SPA swaps).
|
|
235
282
|
const warnedNested = new WeakSet();
|
|
236
|
-
// Elements with an in-flight dynamic import that has not yet resolved.
|
|
237
|
-
// Two concurrent `mountIslands` invocations (or two `scheduleMount`
|
|
238
|
-
// calls hitting the same element through different code paths) could
|
|
239
|
-
// otherwise both pass the `mounted` guard and both spawn an
|
|
240
|
-
// `importIsland(url)` -> `fn()` chain, double-mounting the component.
|
|
241
|
-
// Adding the element to `pending` synchronously, before the import is
|
|
242
|
-
// fired, closes that window; the entry is removed in both the success
|
|
243
|
-
// (after `mounted.set`) and failure branches.
|
|
244
|
-
const pending = new WeakSet();
|
|
245
283
|
// Module-level captured manifest — set by the first `mountIslands` call and reused by
|
|
246
284
|
// `mountNewIslands()` so the client-router does not need to know the manifest directly.
|
|
247
285
|
// Named technical cause (W1B §12.1): the router lives in @takazudo/zfb-runtime; the
|
|
@@ -260,7 +298,7 @@ const pendingCancels = new Map();
|
|
|
260
298
|
*
|
|
261
299
|
* No-op when `document` is undefined (SSR, edge runtime). Safe to call
|
|
262
300
|
* multiple times: each element is mounted at most once thanks to the
|
|
263
|
-
*
|
|
301
|
+
* symbol-handle guard.
|
|
264
302
|
*
|
|
265
303
|
* The manifest is captured at module level so `mountNewIslands()` can re-use
|
|
266
304
|
* it after an SPA body swap without needing the caller to re-supply it.
|
|
@@ -270,9 +308,10 @@ export function mountIslands(manifest) {
|
|
|
270
308
|
return;
|
|
271
309
|
// Capture the manifest for post-swap re-walks via mountNewIslands().
|
|
272
310
|
capturedManifest = manifest;
|
|
311
|
+
installCompositionTracker(document);
|
|
273
312
|
const ssrIslands = document.querySelectorAll("[data-zfb-island]");
|
|
274
313
|
for (const el of Array.from(ssrIslands)) {
|
|
275
|
-
|
|
314
|
+
const replacing = prepareMount(el);
|
|
276
315
|
// Skip the empty-skeleton case left behind when the server-side
|
|
277
316
|
// rewriter has not run yet (data-zfb-island="" with no component
|
|
278
317
|
// name). The hydration emit step is expected to fill this in
|
|
@@ -282,16 +321,16 @@ export function mountIslands(manifest) {
|
|
|
282
321
|
if (!name)
|
|
283
322
|
continue;
|
|
284
323
|
warnIfNestedIsland(el, name);
|
|
285
|
-
scheduleMount(manifest, el, name, "hydrate");
|
|
324
|
+
scheduleMount(manifest, el, name, replacing ? "render" : "hydrate", { force: replacing });
|
|
286
325
|
}
|
|
287
326
|
const skipSsrIslands = document.querySelectorAll("[data-zfb-island-skip-ssr]");
|
|
288
327
|
for (const el of Array.from(skipSsrIslands)) {
|
|
289
|
-
|
|
328
|
+
const replacing = prepareMount(el);
|
|
290
329
|
const name = el.getAttribute("data-zfb-island-skip-ssr");
|
|
291
330
|
if (!name)
|
|
292
331
|
continue;
|
|
293
332
|
warnIfNestedIsland(el, name);
|
|
294
|
-
scheduleMount(manifest, el, name, "render");
|
|
333
|
+
scheduleMount(manifest, el, name, "render", { force: replacing });
|
|
295
334
|
}
|
|
296
335
|
}
|
|
297
336
|
/**
|
|
@@ -313,81 +352,46 @@ export function mountNewIslands() {
|
|
|
313
352
|
const manifest = capturedManifest;
|
|
314
353
|
const ssrIslands = document.querySelectorAll("[data-zfb-island]");
|
|
315
354
|
for (const el of Array.from(ssrIslands)) {
|
|
316
|
-
|
|
355
|
+
const replacing = prepareMount(el);
|
|
317
356
|
const name = el.getAttribute("data-zfb-island");
|
|
318
357
|
if (!name)
|
|
319
358
|
continue;
|
|
320
|
-
//
|
|
321
|
-
|
|
322
|
-
// entry BEFORE scheduleMount's already-mounted guard so it re-mounts fresh
|
|
323
|
-
// with the refreshed data-props. No-op for every other element.
|
|
324
|
-
const forceRemount = clearMountedForRemount(el);
|
|
359
|
+
// The router marks changed persisted identity/props on the surviving node.
|
|
360
|
+
const forceRemount = clearMountedForRemount(el) || replacing;
|
|
325
361
|
warnIfNestedIsland(el, name);
|
|
326
|
-
scheduleMount(manifest, el, name, "hydrate", { force: forceRemount });
|
|
362
|
+
scheduleMount(manifest, el, name, forceRemount ? "render" : "hydrate", { force: forceRemount });
|
|
327
363
|
}
|
|
328
364
|
const skipSsrIslands = document.querySelectorAll("[data-zfb-island-skip-ssr]");
|
|
329
365
|
for (const el of Array.from(skipSsrIslands)) {
|
|
330
|
-
|
|
366
|
+
const replacing = prepareMount(el);
|
|
331
367
|
const name = el.getAttribute("data-zfb-island-skip-ssr");
|
|
332
368
|
if (!name)
|
|
333
369
|
continue;
|
|
334
370
|
warnIfNestedIsland(el, name);
|
|
335
|
-
|
|
371
|
+
const forceRemount = clearMountedForRemount(el) || replacing;
|
|
372
|
+
scheduleMount(manifest, el, name, "render", { force: forceRemount });
|
|
336
373
|
}
|
|
337
374
|
}
|
|
338
|
-
/**
|
|
339
|
-
* Consume the cross-package "needs-remount" signal for the persist-props hybrid
|
|
340
|
-
* path (port-spec §12.3.1 hybrid case / §12.3.2). When a persisted island's
|
|
341
|
-
* props differ from the incoming markup, `swapBodyElement` refreshes the
|
|
342
|
-
* surviving element's `data-props` and marks it with `ISLAND_REMOUNT_ATTR`.
|
|
343
|
-
* That attribute is the ONLY channel that crosses the zfb-runtime → zfb package
|
|
344
|
-
* boundary — the `mounted` map is module-private to this file, so a shared
|
|
345
|
-
* in-memory "needs-remount" queue between the two packages is impossible; the
|
|
346
|
-
* live DOM node carrying the flag IS the queue.
|
|
347
|
-
*
|
|
348
|
-
* On a flagged mounted element: fire the old instance's unmount thunk (so its
|
|
349
|
-
* useEffect/framework cleanups run against the still-connected node), drop the
|
|
350
|
-
* `mounted` entry so `scheduleMount`'s guard no longer short-circuits, strip the
|
|
351
|
-
* flag, and ask the caller to force the replacement mount through immediately
|
|
352
|
-
* instead of re-entering any deferred scheduler. This keeps a deferred persisted
|
|
353
|
-
* island from blanking while it waits for idle/visible/media to fire again.
|
|
354
|
-
*
|
|
355
|
-
* On a flagged element whose URL import is still pending, leave the flag in
|
|
356
|
-
* place. The already-running import's success handler consumes it after the
|
|
357
|
-
* module resolves and re-reads `data-props` at that point, so a props refresh
|
|
358
|
-
* that happened during the import wins without starting a duplicate import.
|
|
359
|
-
*
|
|
360
|
-
* A no-op for elements without the flag (the common case: fresh markers and
|
|
361
|
-
* props-unchanged persisted islands).
|
|
362
|
-
*
|
|
363
|
-
* Scope: only the `[data-zfb-island]` (hydrated) loop calls this, mirroring the
|
|
364
|
-
* writer side — swapBodyElement sets the flag only for `newTarget.matches(
|
|
365
|
-
* "[data-zfb-island]")`, never for skip-ssr islands.
|
|
366
|
-
*/
|
|
375
|
+
/** Consume a persisted root's remount flag and dispose its previous resources. */
|
|
367
376
|
function clearMountedForRemount(el) {
|
|
368
377
|
if (!el.hasAttribute(ISLAND_REMOUNT_ATTR))
|
|
369
378
|
return false;
|
|
370
|
-
if (pending.has(el))
|
|
371
|
-
return false;
|
|
372
|
-
const thunk = mounted.get(el);
|
|
373
|
-
if (thunk) {
|
|
374
|
-
try {
|
|
375
|
-
thunk();
|
|
376
|
-
}
|
|
377
|
-
finally {
|
|
378
|
-
mounted.delete(el);
|
|
379
|
-
el.removeAttribute(ISLAND_MOUNTED_ATTR);
|
|
380
|
-
el.removeAttribute(ISLAND_REMOUNT_ATTR);
|
|
381
|
-
}
|
|
382
|
-
return true;
|
|
383
|
-
}
|
|
384
|
-
el.removeAttribute(ISLAND_MOUNTED_ATTR);
|
|
385
379
|
el.removeAttribute(ISLAND_REMOUNT_ATTR);
|
|
386
|
-
|
|
380
|
+
if (rootHandle(el))
|
|
381
|
+
disposeIsland(el, "remount disposal");
|
|
382
|
+
else
|
|
383
|
+
el.removeAttribute(ISLAND_MOUNTED_ATTR);
|
|
384
|
+
return true;
|
|
387
385
|
}
|
|
388
|
-
function
|
|
389
|
-
|
|
386
|
+
function prepareMount(el) {
|
|
387
|
+
let replacing = false;
|
|
388
|
+
if (rootHandle(el) && !owned.has(el)) {
|
|
389
|
+
disposeIsland(el, "dev replacement disposal");
|
|
390
|
+
replacing = true;
|
|
391
|
+
}
|
|
392
|
+
if (!rootHandle(el))
|
|
390
393
|
el.removeAttribute(ISLAND_MOUNTED_ATTR);
|
|
394
|
+
return replacing;
|
|
391
395
|
}
|
|
392
396
|
/**
|
|
393
397
|
* Cancel deferred-hydration callbacks for all islands in the old body before a
|
|
@@ -438,10 +442,7 @@ function warnIfNestedIsland(el, componentName) {
|
|
|
438
442
|
`and apply <Island when="..."> at the call site instead.`);
|
|
439
443
|
}
|
|
440
444
|
function scheduleMount(manifest, element, componentName, mode, options = {}) {
|
|
441
|
-
|
|
442
|
-
// prevents two concurrent `mountIslands` calls from each firing a
|
|
443
|
-
// separate dynamic import for the same element.
|
|
444
|
-
if (mounted.has(element) || pending.has(element))
|
|
445
|
+
if (rootHandle(element))
|
|
445
446
|
return;
|
|
446
447
|
const entry = manifest[componentName];
|
|
447
448
|
if (entry == null) {
|
|
@@ -452,140 +453,21 @@ function scheduleMount(manifest, element, componentName, mode, options = {}) {
|
|
|
452
453
|
}
|
|
453
454
|
return;
|
|
454
455
|
}
|
|
455
|
-
|
|
456
|
-
// Two manifest shapes:
|
|
457
|
-
//
|
|
458
|
-
// - `string` (per-island bundle URL): fetch via dynamic `import()`
|
|
459
|
-
// and call `mount` / `default` on the resolved module.
|
|
460
|
-
// - `IslandModule` (inline descriptor): the shared-bundle path has
|
|
461
|
-
// already imported every island's source into the same bundle and
|
|
462
|
-
// constructed a mount function for it. Skip the dynamic import
|
|
463
|
-
// and call the supplied function directly.
|
|
464
|
-
if (typeof entry !== "string") {
|
|
465
|
-
fireInlineMount(element, entry, mode, options);
|
|
466
|
-
return;
|
|
467
|
-
}
|
|
468
|
-
const url = entry;
|
|
469
|
-
const fire = () => {
|
|
470
|
-
// Re-check both guards in case `fire` is invoked from a deferred
|
|
471
|
-
// scheduler (rIC/rAF/visibility) after a sibling caller already
|
|
472
|
-
// mounted or started importing for this element.
|
|
473
|
-
if (mounted.has(element) || pending.has(element))
|
|
474
|
-
return;
|
|
475
|
-
// When the deferred fire actually runs, the cancel handle is no longer
|
|
476
|
-
// needed — remove it so pendingCancels doesn't hold stale entries.
|
|
477
|
-
pendingCancels.delete(element);
|
|
478
|
-
// Lazy props parse: read and parse data-props only now that we know we
|
|
479
|
-
// are actually going to mount this island. For deferred strategies
|
|
480
|
-
// (media, visible, idle) this avoids JSON.parse work at boot time for
|
|
481
|
-
// islands that may never hydrate (e.g. media query never matches).
|
|
482
|
-
const props = readProps(element);
|
|
483
|
-
// Mark as pending BEFORE firing the import so any concurrent
|
|
484
|
-
// `mountIslands` invocation that arrives during the await window
|
|
485
|
-
// is short-circuited by `scheduleMount`'s guard.
|
|
486
|
-
pending.add(element);
|
|
487
|
-
// Dynamic-import is cached by the JS runtime, so repeat hits for
|
|
488
|
-
// the same URL share the resolved module — module-level
|
|
489
|
-
// singletons are fine.
|
|
490
|
-
//
|
|
491
|
-
// We move the element from `pending` to `mounted` only on the
|
|
492
|
-
// success path so a failed import (e.g. transient network blip
|
|
493
|
-
// in dev) doesn't permanently block a retry of the same element.
|
|
494
|
-
let started;
|
|
495
|
-
try {
|
|
496
|
-
started = importIsland(url);
|
|
497
|
-
}
|
|
498
|
-
catch (err) {
|
|
499
|
-
// Some implementations of dynamic-import wrappers can throw
|
|
500
|
-
// synchronously (e.g. URL parsing errors). Treat the same as
|
|
501
|
-
// an async rejection.
|
|
502
|
-
pending.delete(element);
|
|
503
|
-
// eslint-disable-next-line no-console
|
|
504
|
-
console.error(`[zfb] failed to start dynamic import for ${url}`, err);
|
|
505
|
-
return;
|
|
506
|
-
}
|
|
507
|
-
started.then((mod) => {
|
|
508
|
-
const fn = mod.mount ?? mod.default;
|
|
509
|
-
if (typeof fn !== "function") {
|
|
510
|
-
pending.delete(element);
|
|
511
|
-
if (typeof process !== "undefined" &&
|
|
512
|
-
process.env &&
|
|
513
|
-
process.env["NODE_ENV"] !== "production") {
|
|
514
|
-
// eslint-disable-next-line no-console
|
|
515
|
-
console.warn(`[zfb] island bundle at ${url} did not export mount() or default()`);
|
|
516
|
-
}
|
|
517
|
-
return;
|
|
518
|
-
}
|
|
519
|
-
// Stale-mount race guard: if the element was detached while the
|
|
520
|
-
// dynamic import was in-flight (e.g. a body swap happened), skip
|
|
521
|
-
// mounting — the element is no longer in the live document and
|
|
522
|
-
// its useEffect listeners would never receive a cleanup call.
|
|
523
|
-
if (!element.isConnected) {
|
|
524
|
-
pending.delete(element);
|
|
525
|
-
return;
|
|
526
|
-
}
|
|
527
|
-
const shouldRefreshProps = element.hasAttribute(ISLAND_REMOUNT_ATTR);
|
|
528
|
-
const propsForMount = shouldRefreshProps ? readProps(element) : props;
|
|
529
|
-
if (shouldRefreshProps)
|
|
530
|
-
element.removeAttribute(ISLAND_REMOUNT_ATTR);
|
|
531
|
-
const unmountThunk = mod.unmount
|
|
532
|
-
? () => mod.unmount(element)
|
|
533
|
-
: () => {
|
|
534
|
-
// noop — bundle does not expose unmount
|
|
535
|
-
};
|
|
536
|
-
try {
|
|
537
|
-
fn(propsForMount, element, mode);
|
|
538
|
-
mounted.set(element, unmountThunk);
|
|
539
|
-
element.setAttribute(ISLAND_MOUNTED_ATTR, "");
|
|
540
|
-
}
|
|
541
|
-
finally {
|
|
542
|
-
pending.delete(element);
|
|
543
|
-
}
|
|
544
|
-
}, (err) => {
|
|
545
|
-
// Surface the error in dev so the user notices, then clear
|
|
546
|
-
// both guards so a later retry (e.g. another scheduleHydrate
|
|
547
|
-
// fire) can attempt the import again.
|
|
548
|
-
pending.delete(element);
|
|
549
|
-
mounted.delete(element);
|
|
550
|
-
// eslint-disable-next-line no-console
|
|
551
|
-
console.error(`[zfb] failed to load island bundle ${url}`, err);
|
|
552
|
-
});
|
|
553
|
-
};
|
|
554
|
-
if (mode === "render") {
|
|
555
|
-
// SSR-skip islands ignore data-when: there is nothing to defer
|
|
556
|
-
// hydration of, just an empty container we paint into. Mount
|
|
557
|
-
// immediately so the user sees output.
|
|
558
|
-
fire();
|
|
559
|
-
return;
|
|
560
|
-
}
|
|
561
|
-
if (options.force) {
|
|
562
|
-
fire();
|
|
563
|
-
return;
|
|
564
|
-
}
|
|
565
|
-
const { fired, cancel } = scheduleHydrateInternal(element, when, fire);
|
|
566
|
-
// Track deferred-hydration cancel handle so cancelPendingIslands() can abort
|
|
567
|
-
// idle / visibility callbacks before a body swap. (W1B §12.5)
|
|
568
|
-
// Only register when the scheduler did NOT fire synchronously — a synchronous
|
|
569
|
-
// fire means the island is already handling its import and there is no
|
|
570
|
-
// deferred callback to cancel. Registering noop after a sync fire would leave
|
|
571
|
-
// a stale pendingCancels entry for an already-handled element. (#743)
|
|
572
|
-
if (when && when !== "load" && !fired) {
|
|
573
|
-
pendingCancels.set(element, cancel);
|
|
574
|
-
}
|
|
456
|
+
fireInlineMount(element, entry, mode, options);
|
|
575
457
|
}
|
|
576
458
|
/**
|
|
577
459
|
* Run the mount step for the inline-module manifest shape used by the
|
|
578
460
|
* shared-bundle path. The module is already in memory (it was imported
|
|
579
461
|
* into the bundle at build time), so there is no async window to
|
|
580
462
|
* coordinate around — we just call `mount` / `default` directly,
|
|
581
|
-
* gated by
|
|
463
|
+
* gated by `data-when` semantics.
|
|
582
464
|
*/
|
|
583
465
|
function fireInlineMount(element, mod, mode, options = {}) {
|
|
584
|
-
const fn = mod.mount
|
|
466
|
+
const fn = mod.mount;
|
|
585
467
|
if (typeof fn !== "function") {
|
|
586
468
|
if (typeof process !== "undefined" && process.env && process.env["NODE_ENV"] !== "production") {
|
|
587
469
|
// eslint-disable-next-line no-console
|
|
588
|
-
console.warn("[zfb]
|
|
470
|
+
console.warn("[zfb] owned island manifest entry did not export mount()");
|
|
589
471
|
}
|
|
590
472
|
return;
|
|
591
473
|
}
|
|
@@ -593,7 +475,7 @@ function fireInlineMount(element, mod, mode, options = {}) {
|
|
|
593
475
|
// Re-check the guard in case `fire` is invoked from a deferred
|
|
594
476
|
// scheduler (rIC/rAF/visibility) after a sibling caller already
|
|
595
477
|
// mounted this element.
|
|
596
|
-
if (
|
|
478
|
+
if (rootHandle(element))
|
|
597
479
|
return;
|
|
598
480
|
// When the deferred fire actually runs, the cancel handle is no longer
|
|
599
481
|
// needed — remove it so pendingCancels doesn't hold stale entries.
|
|
@@ -605,20 +487,24 @@ function fireInlineMount(element, mod, mode, options = {}) {
|
|
|
605
487
|
// Lazy props parse: read and parse data-props only at mount time.
|
|
606
488
|
// For deferred strategies (media, visible, idle) this avoids JSON.parse
|
|
607
489
|
// work at boot time for islands that may never hydrate.
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
490
|
+
try {
|
|
491
|
+
const props = readProps(element, mod.identity);
|
|
492
|
+
const result = fn(props, element, mode);
|
|
493
|
+
if (result === null)
|
|
494
|
+
return;
|
|
495
|
+
if (!result || typeof result.dispose !== "function" || typeof result.unmount !== "function") {
|
|
496
|
+
throw new TypeError(`ZR_ROOT_HANDLE: island ${mod.identity.component} returned no root handle`);
|
|
497
|
+
}
|
|
498
|
+
const handle = result;
|
|
499
|
+
setRootHandle(element, handle);
|
|
500
|
+
owned.add(element);
|
|
501
|
+
element.setAttribute(ISLAND_MOUNTED_ATTR, "");
|
|
502
|
+
}
|
|
503
|
+
catch (error) {
|
|
504
|
+
element.removeAttribute(ISLAND_MOUNTED_ATTR);
|
|
505
|
+
reportIslandError(element, "mount", error);
|
|
506
|
+
}
|
|
617
507
|
};
|
|
618
|
-
if (mode === "render") {
|
|
619
|
-
fire();
|
|
620
|
-
return;
|
|
621
|
-
}
|
|
622
508
|
if (options.force) {
|
|
623
509
|
fire();
|
|
624
510
|
return;
|
|
@@ -636,115 +522,196 @@ function fireInlineMount(element, mod, mode, options = {}) {
|
|
|
636
522
|
}
|
|
637
523
|
}
|
|
638
524
|
/**
|
|
639
|
-
*
|
|
640
|
-
*
|
|
641
|
-
*
|
|
642
|
-
* Walks `root` for `[data-zfb-island]` and `[data-zfb-island-skip-ssr]` elements,
|
|
643
|
-
* looks up each element's unmount thunk in the `mounted` WeakMap, calls it (which
|
|
644
|
-
* triggers `render(null, element)` for Preact or `root.unmount()` for React), and
|
|
645
|
-
* removes the entry from the map so `mountNewIslands()` can re-mount later.
|
|
646
|
-
*
|
|
647
|
-
* Call this before `swapBodyElement(...)` so the OLD body's islands receive proper
|
|
648
|
-
* framework lifecycle cleanup (useEffect teardowns, etc.) before being discarded.
|
|
649
|
-
*
|
|
650
|
-
* When `incomingBody` is supplied (the client-router passes the parsed incoming
|
|
651
|
-
* document body), any island whose `data-zfb-transition-persist` id matches a
|
|
652
|
-
* marker in that body is DELIBERATELY SKIPPED: swapBodyElement will physically
|
|
653
|
-
* lift the node into the new body, so its component instance and internal state
|
|
654
|
-
* must survive — unmounting it here would empty the container before the lift and
|
|
655
|
-
* defeat the persist contract (issue #1389). Omit `incomingBody` (or pass null)
|
|
656
|
-
* to unmount everything, the pre-#1389 behavior.
|
|
657
|
-
*
|
|
658
|
-
* No-op for elements not in the `mounted` map (e.g. never-mounted or already cleaned up).
|
|
525
|
+
* Snapshot persistence and island pairings before changing any old-body node.
|
|
526
|
+
* The router lifts only matched persist nodes whose incoming targets are not
|
|
527
|
+
* nested inside another matched target. Their descendants survive that lift.
|
|
659
528
|
*/
|
|
660
529
|
export function unmountIslands(root = document.body, incomingBody) {
|
|
661
|
-
const
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
530
|
+
const islandSelector = "[data-zfb-island],[data-zfb-island-skip-ssr]";
|
|
531
|
+
const oldIslands = Array.from(root.querySelectorAll(islandSelector));
|
|
532
|
+
if (!incomingBody) {
|
|
533
|
+
for (const island of oldIslands)
|
|
534
|
+
disposeIsland(island, "disposal");
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
const persistSelector = `[${PERSIST_ATTR}]`;
|
|
538
|
+
const oldPersist = Array.from(root.querySelectorAll(persistSelector));
|
|
539
|
+
const incomingPersist = Array.from(incomingBody.querySelectorAll(persistSelector));
|
|
540
|
+
const incomingById = new Map();
|
|
541
|
+
for (const element of incomingPersist) {
|
|
542
|
+
const id = element.getAttribute(PERSIST_ATTR);
|
|
543
|
+
if (id !== null && !incomingById.has(id))
|
|
544
|
+
incomingById.set(id, element);
|
|
545
|
+
}
|
|
546
|
+
const targets = new Map();
|
|
547
|
+
for (const element of oldPersist) {
|
|
548
|
+
const id = element.getAttribute(PERSIST_ATTR);
|
|
549
|
+
const target = id === null ? undefined : incomingById.get(id);
|
|
550
|
+
if (target)
|
|
551
|
+
targets.set(element, target);
|
|
552
|
+
}
|
|
553
|
+
const matchedTargets = new Set(targets.values());
|
|
554
|
+
const lifted = new Set();
|
|
555
|
+
for (const [element, target] of targets) {
|
|
556
|
+
let ancestor = target.parentElement;
|
|
557
|
+
while (ancestor && !matchedTargets.has(ancestor))
|
|
558
|
+
ancestor = ancestor.parentElement;
|
|
559
|
+
if (!ancestor)
|
|
560
|
+
lifted.add(element);
|
|
561
|
+
}
|
|
562
|
+
const retained = new Set(lifted);
|
|
563
|
+
for (const element of oldPersist) {
|
|
564
|
+
if (!targets.has(element))
|
|
565
|
+
continue;
|
|
566
|
+
let ancestor = element.parentElement;
|
|
567
|
+
while (ancestor && !lifted.has(ancestor))
|
|
568
|
+
ancestor = ancestor.parentElement;
|
|
569
|
+
if (ancestor)
|
|
570
|
+
retained.add(element);
|
|
571
|
+
}
|
|
572
|
+
const boundaryById = new Map();
|
|
573
|
+
for (const element of oldPersist) {
|
|
574
|
+
if (!retained.has(element))
|
|
575
|
+
continue;
|
|
576
|
+
const id = element.getAttribute(PERSIST_ATTR);
|
|
577
|
+
if (id !== null && !boundaryById.has(id))
|
|
578
|
+
boundaryById.set(id, element);
|
|
579
|
+
}
|
|
580
|
+
const oldByBoundary = new Map();
|
|
581
|
+
const incomingByBoundary = new Map();
|
|
582
|
+
const withoutBoundary = [];
|
|
583
|
+
for (const island of oldIslands) {
|
|
584
|
+
let ancestor = island;
|
|
585
|
+
while (ancestor && !retained.has(ancestor))
|
|
586
|
+
ancestor = ancestor.parentElement;
|
|
587
|
+
if (!ancestor) {
|
|
588
|
+
withoutBoundary.push(island);
|
|
678
589
|
continue;
|
|
679
|
-
const thunk = mounted.get(el);
|
|
680
|
-
try {
|
|
681
|
-
thunk?.();
|
|
682
590
|
}
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
591
|
+
const group = oldByBoundary.get(ancestor) ?? [];
|
|
592
|
+
group.push(island);
|
|
593
|
+
oldByBoundary.set(ancestor, group);
|
|
594
|
+
}
|
|
595
|
+
for (const island of incomingBody.querySelectorAll(islandSelector)) {
|
|
596
|
+
let ancestor = island;
|
|
597
|
+
let boundary;
|
|
598
|
+
while (ancestor && !boundary) {
|
|
599
|
+
const id = ancestor.getAttribute(PERSIST_ATTR);
|
|
600
|
+
if (id !== null)
|
|
601
|
+
boundary = boundaryById.get(id);
|
|
602
|
+
ancestor = ancestor.parentElement;
|
|
686
603
|
}
|
|
604
|
+
if (!boundary)
|
|
605
|
+
continue;
|
|
606
|
+
const group = incomingByBoundary.get(boundary) ?? [];
|
|
607
|
+
group.push(island);
|
|
608
|
+
incomingByBoundary.set(boundary, group);
|
|
609
|
+
}
|
|
610
|
+
const pairs = [];
|
|
611
|
+
const removed = [];
|
|
612
|
+
for (const [boundary, oldGroup] of oldByBoundary) {
|
|
613
|
+
const incomingGroup = incomingByBoundary.get(boundary) ?? [];
|
|
614
|
+
const pairedOld = new Set();
|
|
615
|
+
const pairedIncoming = new Set();
|
|
616
|
+
// A persisted island root always maps to its own target, even when the
|
|
617
|
+
// target changes component or ceases to be an island.
|
|
618
|
+
if (oldGroup.includes(boundary)) {
|
|
619
|
+
pairedOld.add(boundary);
|
|
620
|
+
const target = targets.get(boundary);
|
|
621
|
+
if (target && incomingGroup.includes(target)) {
|
|
622
|
+
pairs.push({ old: boundary, incoming: target });
|
|
623
|
+
pairedIncoming.add(target);
|
|
624
|
+
}
|
|
625
|
+
else {
|
|
626
|
+
removed.push(boundary);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
for (const old of oldGroup) {
|
|
630
|
+
if (pairedOld.has(old))
|
|
631
|
+
continue;
|
|
632
|
+
const name = islandName(old);
|
|
633
|
+
const incoming = incomingGroup.find((candidate) => !pairedIncoming.has(candidate) && islandName(candidate) === name);
|
|
634
|
+
if (!incoming)
|
|
635
|
+
continue;
|
|
636
|
+
pairs.push({ old, incoming });
|
|
637
|
+
pairedOld.add(old);
|
|
638
|
+
pairedIncoming.add(incoming);
|
|
639
|
+
}
|
|
640
|
+
const remainingIncoming = incomingGroup.filter((island) => !pairedIncoming.has(island));
|
|
641
|
+
let next = 0;
|
|
642
|
+
for (const old of oldGroup) {
|
|
643
|
+
if (pairedOld.has(old))
|
|
644
|
+
continue;
|
|
645
|
+
const incoming = remainingIncoming[next++];
|
|
646
|
+
if (incoming)
|
|
647
|
+
pairs.push({ old, incoming });
|
|
648
|
+
else
|
|
649
|
+
removed.push(old);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
// Apply only after all boundaries and pairings have been computed. An old
|
|
653
|
+
// descendant removed from a lifted wrapper must be detached as well as
|
|
654
|
+
// disposed, or the post-swap walk could hydrate its mutated DOM.
|
|
655
|
+
for (const island of withoutBoundary)
|
|
656
|
+
disposeIsland(island, "disposal");
|
|
657
|
+
for (const { old, incoming } of pairs) {
|
|
658
|
+
const copyProps = !old.hasAttribute("data-zfb-transition-persist-props") ||
|
|
659
|
+
old.getAttribute("data-zfb-transition-persist-props") === "false";
|
|
660
|
+
const identityChanged = ISLAND_IDENTITY_ATTRS.some((attribute) => old.getAttribute(attribute) !== incoming.getAttribute(attribute));
|
|
661
|
+
if (!old.hasAttribute(ISLAND_REMOUNT_ATTR) &&
|
|
662
|
+
!identityChanged &&
|
|
663
|
+
(!copyProps || old.getAttribute("data-props") === incoming.getAttribute("data-props")))
|
|
664
|
+
continue;
|
|
665
|
+
for (const attribute of ISLAND_IDENTITY_ATTRS)
|
|
666
|
+
copyAttribute(incoming, old, attribute);
|
|
667
|
+
if (copyProps)
|
|
668
|
+
copyAttribute(incoming, old, "data-props");
|
|
669
|
+
old.setAttribute(ISLAND_REMOUNT_ATTR, "");
|
|
670
|
+
}
|
|
671
|
+
for (const island of removed) {
|
|
672
|
+
disposeIsland(island, "disposal");
|
|
673
|
+
island.remove();
|
|
687
674
|
}
|
|
688
675
|
}
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
676
|
+
const ISLAND_IDENTITY_ATTRS = [
|
|
677
|
+
"data-zfb-island",
|
|
678
|
+
"data-zfb-island-skip-ssr",
|
|
679
|
+
"data-zfb-transport",
|
|
680
|
+
"data-zfb-protocol",
|
|
681
|
+
"data-zfb-build",
|
|
682
|
+
];
|
|
683
|
+
function islandName(element) {
|
|
684
|
+
return (element.getAttribute("data-zfb-island") ?? element.getAttribute("data-zfb-island-skip-ssr"));
|
|
685
|
+
}
|
|
686
|
+
function copyAttribute(from, to, attribute) {
|
|
687
|
+
const value = from.getAttribute(attribute);
|
|
688
|
+
if (value === null)
|
|
689
|
+
to.removeAttribute(attribute);
|
|
690
|
+
else
|
|
691
|
+
to.setAttribute(attribute, value);
|
|
705
692
|
}
|
|
706
|
-
function readProps(element) {
|
|
693
|
+
function readProps(element, identity) {
|
|
707
694
|
const raw = element.getAttribute("data-props");
|
|
708
|
-
|
|
709
|
-
|
|
695
|
+
const hasHydrate = element.hasAttribute("data-zfb-island");
|
|
696
|
+
const hasSkipSsr = element.hasAttribute("data-zfb-island-skip-ssr");
|
|
697
|
+
const component = element.getAttribute("data-zfb-island") ?? element.getAttribute("data-zfb-island-skip-ssr");
|
|
698
|
+
if (hasHydrate === hasSkipSsr ||
|
|
699
|
+
component !== identity.component ||
|
|
700
|
+
element.getAttribute("data-zfb-transport") !== "json/1" ||
|
|
701
|
+
element.getAttribute("data-zfb-protocol") !== "zudo-react/1" ||
|
|
702
|
+
element.getAttribute("data-zfb-build") !== identity.build ||
|
|
703
|
+
raw === null) {
|
|
704
|
+
throw new TypeError(`ZR_IDENTITY: island ${identity.component} has invalid transport identity`);
|
|
705
|
+
}
|
|
706
|
+
if (element.querySelector("[data-zfb-island],[data-zfb-island-skip-ssr]")) {
|
|
707
|
+
throw new TypeError(`ZR_NESTED_ISLAND: island ${identity.component} contains another island`);
|
|
708
|
+
}
|
|
710
709
|
try {
|
|
711
|
-
|
|
712
|
-
// Reject arrays explicitly: `typeof [] === "object"` is true but
|
|
713
|
-
// an array is not a valid props bag, and passing it through would
|
|
714
|
-
// mean the component receives index-keyed values where it
|
|
715
|
-
// expected a record. Fall through to the empty-object default
|
|
716
|
-
// instead of forwarding a malformed shape.
|
|
717
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
718
|
-
return parsed;
|
|
719
|
-
}
|
|
710
|
+
return parseOwnedProps(raw);
|
|
720
711
|
}
|
|
721
|
-
catch {
|
|
722
|
-
|
|
712
|
+
catch (error) {
|
|
713
|
+
throw new TypeError(`ZR_PROPS: island ${identity.component}: ${String(error)}`);
|
|
723
714
|
}
|
|
724
|
-
return {};
|
|
725
|
-
}
|
|
726
|
-
/**
|
|
727
|
-
* Indirection so tests can stub the dynamic import without intercepting
|
|
728
|
-
* the global `import()`. In production this is a thin wrapper over
|
|
729
|
-
* native `import(url)`.
|
|
730
|
-
*/
|
|
731
|
-
let importImpl = (url) =>
|
|
732
|
-
// Modern bundlers (esbuild, Vite, Rollup, webpack) preserve a plain
|
|
733
|
-
// `import(<dynamic>)` call when the argument isn't a static literal,
|
|
734
|
-
// so we no longer need the `new Function(...)` indirection — which
|
|
735
|
-
// also failed under strict CSPs that disallow `unsafe-eval`.
|
|
736
|
-
import(/* @vite-ignore */ /* webpackIgnore: true */ url);
|
|
737
|
-
function importIsland(url) {
|
|
738
|
-
return importImpl(url);
|
|
739
|
-
}
|
|
740
|
-
/**
|
|
741
|
-
* Test-only seam. Replace the module dynamic-import with a fake.
|
|
742
|
-
* Returns the previous implementation so tests can restore it.
|
|
743
|
-
*/
|
|
744
|
-
export function __setIslandImporterForTests(impl) {
|
|
745
|
-
const prev = importImpl;
|
|
746
|
-
importImpl = impl;
|
|
747
|
-
return prev;
|
|
748
715
|
}
|
|
749
716
|
/**
|
|
750
717
|
* Test-only seam. Returns whether the given element has an entry in the
|