@moku-labs/web 1.17.0 → 2.0.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 +1 -1
- package/dist/browser.d.mts +111 -108
- package/dist/browser.mjs +189 -189
- package/dist/index.cjs +198 -198
- package/dist/index.d.cts +111 -108
- package/dist/index.d.mts +111 -108
- package/dist/index.mjs +198 -198
- package/dist/{render-KdufA3_b.cjs → render-DHUcHCYs.cjs} +4 -4
- package/dist/{render-UO4nimWr.mjs → render-yXHc9BWI.mjs} +4 -4
- package/dist/testing.d.mts +59 -56
- package/dist/testing.mjs +50 -50
- package/package.json +1 -1
package/dist/browser.mjs
CHANGED
|
@@ -2189,15 +2189,15 @@ const headPlugin = createPlugin$1("head", {
|
|
|
2189
2189
|
function createApi(ctx) {
|
|
2190
2190
|
return {
|
|
2191
2191
|
/**
|
|
2192
|
-
* Register a
|
|
2192
|
+
* Register a island definition (last-registered-wins); warns on collision.
|
|
2193
2193
|
*
|
|
2194
|
-
* @param
|
|
2194
|
+
* @param island - The island definition created via `createIsland`.
|
|
2195
2195
|
* @example
|
|
2196
2196
|
* app.spa.register(counter);
|
|
2197
2197
|
*/
|
|
2198
|
-
register(
|
|
2199
|
-
if (ctx.state.
|
|
2200
|
-
ctx.state.kernel?.register(
|
|
2198
|
+
register(island) {
|
|
2199
|
+
if (ctx.state.registeredIslands.has(island.name)) ctx.log.warn("spa:island-collision", { name: island.name });
|
|
2200
|
+
ctx.state.kernel?.register(island);
|
|
2201
2201
|
},
|
|
2202
2202
|
/**
|
|
2203
2203
|
* Programmatically navigate to a path (client runtime; no-op without a DOM).
|
|
@@ -2223,13 +2223,13 @@ function createApi(ctx) {
|
|
|
2223
2223
|
* Resolve a registered island's api by name (the cross-island seam). Returns
|
|
2224
2224
|
* `undefined` when no provider with that name is currently registered.
|
|
2225
2225
|
*
|
|
2226
|
-
* @param name - The provider island's
|
|
2226
|
+
* @param name - The provider island's island name.
|
|
2227
2227
|
* @returns The provider's api, or `undefined`.
|
|
2228
2228
|
* @example
|
|
2229
|
-
* app.spa.
|
|
2229
|
+
* app.spa.island("lightbox");
|
|
2230
2230
|
*/
|
|
2231
|
-
|
|
2232
|
-
return ctx.state.
|
|
2231
|
+
island(name) {
|
|
2232
|
+
return ctx.state.islandApis.get(name);
|
|
2233
2233
|
}
|
|
2234
2234
|
};
|
|
2235
2235
|
}
|
|
@@ -2248,8 +2248,8 @@ function spaEvents(register) {
|
|
|
2248
2248
|
return {
|
|
2249
2249
|
"spa:navigate": register("A navigation has been intercepted and is starting."),
|
|
2250
2250
|
"spa:navigated": register("The swap completed and the new URL is active."),
|
|
2251
|
-
"spa:
|
|
2252
|
-
"spa:
|
|
2251
|
+
"spa:island-mount": register("A island instance attached to an element."),
|
|
2252
|
+
"spa:island-unmount": register("A island instance detached from an element.")
|
|
2253
2253
|
};
|
|
2254
2254
|
}
|
|
2255
2255
|
//#endregion
|
|
@@ -2505,10 +2505,84 @@ const dataPlugin = createPlugin$1("data", {
|
|
|
2505
2505
|
api: dataApi
|
|
2506
2506
|
});
|
|
2507
2507
|
//#endregion
|
|
2508
|
+
//#region src/plugins/spa/head.ts
|
|
2509
|
+
/** Single-element head selectors synced by replace/append/remove on navigation. */
|
|
2510
|
+
const META_SELECTORS = [
|
|
2511
|
+
"meta[name=\"description\"]",
|
|
2512
|
+
"meta[property=\"og:title\"]",
|
|
2513
|
+
"meta[property=\"og:description\"]",
|
|
2514
|
+
"meta[property=\"og:url\"]",
|
|
2515
|
+
"meta[property=\"og:image\"]",
|
|
2516
|
+
"meta[property=\"og:type\"]",
|
|
2517
|
+
"meta[property=\"og:locale\"]",
|
|
2518
|
+
"meta[name=\"twitter:card\"]",
|
|
2519
|
+
"meta[name=\"twitter:title\"]",
|
|
2520
|
+
"meta[name=\"twitter:description\"]",
|
|
2521
|
+
"meta[name=\"twitter:image\"]",
|
|
2522
|
+
"meta[name=\"twitter:site\"]",
|
|
2523
|
+
"link[rel=\"canonical\"]"
|
|
2524
|
+
];
|
|
2525
|
+
/** Head element groups fully replaced (remove-all-then-clone) on navigation. */
|
|
2526
|
+
const REPLACE_ALL_SELECTORS = [
|
|
2527
|
+
"script[type=\"application/ld+json\"]",
|
|
2528
|
+
"link[rel=\"alternate\"][hreflang]",
|
|
2529
|
+
"meta[property^=\"article:\"]"
|
|
2530
|
+
];
|
|
2531
|
+
/**
|
|
2532
|
+
* Sync a single head element by selector between the fetched and live document:
|
|
2533
|
+
* replace when both exist, append when only the new doc has it, remove when only
|
|
2534
|
+
* the live doc has it.
|
|
2535
|
+
*
|
|
2536
|
+
* @param selector - CSS selector for the head element to sync.
|
|
2537
|
+
* @param doc - The fetched document (DOMParser-parsed).
|
|
2538
|
+
* @example
|
|
2539
|
+
* syncElement('link[rel="canonical"]', doc);
|
|
2540
|
+
*/
|
|
2541
|
+
function syncElement(selector, doc) {
|
|
2542
|
+
const newElement = doc.querySelector(selector);
|
|
2543
|
+
const oldElement = document.querySelector(selector);
|
|
2544
|
+
if (newElement && oldElement) oldElement.replaceWith(newElement.cloneNode(true));
|
|
2545
|
+
else if (newElement) document.head.append(newElement.cloneNode(true));
|
|
2546
|
+
else if (oldElement) oldElement.remove();
|
|
2547
|
+
}
|
|
2548
|
+
/**
|
|
2549
|
+
* Remove all live matches for a selector and re-clone the fetched document's
|
|
2550
|
+
* matches into the live `<head>`.
|
|
2551
|
+
*
|
|
2552
|
+
* @param selector - CSS selector for the element group to replace wholesale.
|
|
2553
|
+
* @param doc - The fetched document (DOMParser-parsed).
|
|
2554
|
+
* @example
|
|
2555
|
+
* replaceAllBySelector('script[type="application/ld+json"]', doc);
|
|
2556
|
+
*/
|
|
2557
|
+
function replaceAllBySelector(selector, doc) {
|
|
2558
|
+
for (const element of document.querySelectorAll(selector)) element.remove();
|
|
2559
|
+
for (const element of doc.querySelectorAll(selector)) document.head.append(element.cloneNode(true));
|
|
2560
|
+
}
|
|
2561
|
+
/**
|
|
2562
|
+
* Syncs the live document `<head>` after a navigation from the fetched document
|
|
2563
|
+
* (whose head was composed by the `head` plugin). Recomputes
|
|
2564
|
+
* title/meta/canonical/JSON-LD/hreflang/`<html lang>` once and applies them.
|
|
2565
|
+
* The `head` API is accepted to bind the structural dependency (spec/09 deps).
|
|
2566
|
+
*
|
|
2567
|
+
* @param _head - The head plugin API (dependency binding; composition reused via the fetched doc).
|
|
2568
|
+
* @param doc - The fetched document parsed from the navigated page's HTML.
|
|
2569
|
+
* @example
|
|
2570
|
+
* syncHead(headApi, parsedDoc);
|
|
2571
|
+
*/
|
|
2572
|
+
function syncHead(_head, doc) {
|
|
2573
|
+
if (typeof document === "undefined") return;
|
|
2574
|
+
const newTitle = doc.querySelector("title")?.textContent;
|
|
2575
|
+
if (newTitle) document.title = newTitle;
|
|
2576
|
+
const newLang = doc.documentElement.lang;
|
|
2577
|
+
if (newLang) document.documentElement.lang = newLang;
|
|
2578
|
+
for (const selector of META_SELECTORS) syncElement(selector, doc);
|
|
2579
|
+
for (const selector of REPLACE_ALL_SELECTORS) replaceAllBySelector(selector, doc);
|
|
2580
|
+
}
|
|
2581
|
+
//#endregion
|
|
2508
2582
|
//#region src/plugins/spa/types.ts
|
|
2509
|
-
var types_exports$4 = /* @__PURE__ */ __exportAll({
|
|
2583
|
+
var types_exports$4 = /* @__PURE__ */ __exportAll({ ISLAND_HOOK_NAMES: () => ISLAND_HOOK_NAMES });
|
|
2510
2584
|
/** Allowed hook names — single source of truth for fail-fast validation. */
|
|
2511
|
-
const
|
|
2585
|
+
const ISLAND_HOOK_NAMES = [
|
|
2512
2586
|
"onCreate",
|
|
2513
2587
|
"onMount",
|
|
2514
2588
|
"onNavStart",
|
|
@@ -2517,10 +2591,10 @@ const COMPONENT_HOOK_NAMES = [
|
|
|
2517
2591
|
"onDestroy"
|
|
2518
2592
|
];
|
|
2519
2593
|
//#endregion
|
|
2520
|
-
//#region src/plugins/spa/
|
|
2594
|
+
//#region src/plugins/spa/islands.ts
|
|
2521
2595
|
/**
|
|
2522
|
-
* @file spa plugin —
|
|
2523
|
-
* surface (`
|
|
2596
|
+
* @file spa plugin — island lifecycle, mounting, the plugin-mirror authoring
|
|
2597
|
+
* surface (`createIsland` with a typed `{ state, render, events, api }` spec),
|
|
2524
2598
|
* the per-instance state + microtask-batched render scheduler, declarative
|
|
2525
2599
|
* delegated events, and the cross-island api registry.
|
|
2526
2600
|
* @see README.md
|
|
@@ -2528,8 +2602,8 @@ const COMPONENT_HOOK_NAMES = [
|
|
|
2528
2602
|
/** Error prefix for spa fail-fast failures (spec/11 Part-3). */
|
|
2529
2603
|
const ERROR_PREFIX$2 = "[web]";
|
|
2530
2604
|
/** The set of legal hook names, frozen for O(1) membership checks. */
|
|
2531
|
-
const HOOK_NAME_SET = new Set(
|
|
2532
|
-
/** The spec-only keys that select the plugin-mirror form of {@link
|
|
2605
|
+
const HOOK_NAME_SET = new Set(ISLAND_HOOK_NAMES);
|
|
2606
|
+
/** The spec-only keys that select the plugin-mirror form of {@link createIsland}. */
|
|
2533
2607
|
const SPEC_KEYS = new Set([
|
|
2534
2608
|
"state",
|
|
2535
2609
|
"render",
|
|
@@ -2568,7 +2642,7 @@ let renderChunk;
|
|
|
2568
2642
|
let commitVNodeFunction;
|
|
2569
2643
|
/**
|
|
2570
2644
|
* Load the lazy `./render` chunk (once) and cache its `commitVNode` for synchronous
|
|
2571
|
-
* use by later renders. Awaited by a
|
|
2645
|
+
* use by later renders. Awaited by a island's `mountPromise` so the test harness's
|
|
2572
2646
|
* `settle()` can deterministically flush a VNode render.
|
|
2573
2647
|
*
|
|
2574
2648
|
* @returns A promise that resolves once `commitVNode` is available.
|
|
@@ -2576,7 +2650,7 @@ let commitVNodeFunction;
|
|
|
2576
2650
|
* await loadRenderChunk();
|
|
2577
2651
|
*/
|
|
2578
2652
|
async function loadRenderChunk() {
|
|
2579
|
-
renderChunk ??= import("./render-
|
|
2653
|
+
renderChunk ??= import("./render-yXHc9BWI.mjs");
|
|
2580
2654
|
commitVNodeFunction = (await renderChunk).commitVNode;
|
|
2581
2655
|
}
|
|
2582
2656
|
/**
|
|
@@ -2585,7 +2659,7 @@ async function loadRenderChunk() {
|
|
|
2585
2659
|
* a Preact `VNode` → committed through the lazy gate (loading it on demand if needed).
|
|
2586
2660
|
*
|
|
2587
2661
|
* @param host - The island host element to render into.
|
|
2588
|
-
* @param result - The value returned by the
|
|
2662
|
+
* @param result - The value returned by the island's `render`.
|
|
2589
2663
|
* @example
|
|
2590
2664
|
* commitResult(host, h(View, { items }));
|
|
2591
2665
|
*/
|
|
@@ -2607,7 +2681,7 @@ function commitResult(host, result) {
|
|
|
2607
2681
|
loadRenderChunk().then(() => commitVNodeFunction?.(vnode, host)).catch(() => {});
|
|
2608
2682
|
}
|
|
2609
2683
|
/**
|
|
2610
|
-
* Run a
|
|
2684
|
+
* Run a island's `render(state, ctx)` and commit the result now. Guards against
|
|
2611
2685
|
* synchronous re-entrancy (a render that calls `ctx.flush`) with a depth cap.
|
|
2612
2686
|
*
|
|
2613
2687
|
* @param instance - The instance to render.
|
|
@@ -2618,7 +2692,7 @@ function commitResult(host, result) {
|
|
|
2618
2692
|
function runRender(instance) {
|
|
2619
2693
|
const render = instance.def.spec?.render;
|
|
2620
2694
|
if (!render) return;
|
|
2621
|
-
if (instance.renderDepth > MAX_RENDER_DEPTH) throw new Error(`${ERROR_PREFIX$2}
|
|
2695
|
+
if (instance.renderDepth > MAX_RENDER_DEPTH) throw new Error(`${ERROR_PREFIX$2} island "${instance.def.name}" render re-entered ${MAX_RENDER_DEPTH}+ times\n → a render must not synchronously trigger its own render (avoid ctx.flush() inside render)`);
|
|
2622
2696
|
instance.renderDepth += 1;
|
|
2623
2697
|
try {
|
|
2624
2698
|
commitResult(instance.el, render(instance.state ?? {}, instance.ctx));
|
|
@@ -2644,12 +2718,12 @@ function scheduleRender(instance) {
|
|
|
2644
2718
|
});
|
|
2645
2719
|
}
|
|
2646
2720
|
/**
|
|
2647
|
-
* Build the single per-instance {@link
|
|
2721
|
+
* Build the single per-instance {@link IslandContext} reused by every hook, event
|
|
2648
2722
|
* handler, and render. Route fields (`params`/`meta`/`locale`/`url`) and `data` read
|
|
2649
2723
|
* through the instance so a navigation update is reflected without rebuilding the ctx;
|
|
2650
|
-
* `state`/`set`/`flush`/`cleanup`/`
|
|
2724
|
+
* `state`/`set`/`flush`/`cleanup`/`island` are bound to the instance + plugin state.
|
|
2651
2725
|
*
|
|
2652
|
-
* @param state - The plugin state (for the cross-island `
|
|
2726
|
+
* @param state - The plugin state (for the cross-island `island` resolver).
|
|
2653
2727
|
* @param instance - The instance the context is bound to.
|
|
2654
2728
|
* @returns The instance-bound context.
|
|
2655
2729
|
* @example
|
|
@@ -2753,13 +2827,13 @@ function buildContext(state, instance) {
|
|
|
2753
2827
|
/**
|
|
2754
2828
|
* Resolve another island's registered api by name (`undefined` when absent).
|
|
2755
2829
|
*
|
|
2756
|
-
* @param name - The provider island's
|
|
2830
|
+
* @param name - The provider island's island name.
|
|
2757
2831
|
* @returns The provider's api, or `undefined`.
|
|
2758
2832
|
* @example
|
|
2759
|
-
* ctx.
|
|
2833
|
+
* ctx.island("lightbox");
|
|
2760
2834
|
*/
|
|
2761
|
-
|
|
2762
|
-
return state.
|
|
2835
|
+
island(name) {
|
|
2836
|
+
return state.islandApis.get(name);
|
|
2763
2837
|
}
|
|
2764
2838
|
};
|
|
2765
2839
|
}
|
|
@@ -2783,7 +2857,7 @@ function matchTarget(host, event, selector) {
|
|
|
2783
2857
|
return matched && host.contains(matched) ? matched : void 0;
|
|
2784
2858
|
}
|
|
2785
2859
|
/**
|
|
2786
|
-
* Attach a
|
|
2860
|
+
* Attach a island's declarative `events` map: one real listener per event TYPE on
|
|
2787
2861
|
* the host (dispatch walks `closest(selector)` for each registered selector), each
|
|
2788
2862
|
* removed via the instance's cleanup registry on destroy.
|
|
2789
2863
|
*
|
|
@@ -2800,7 +2874,7 @@ function attachEvents(instance, events) {
|
|
|
2800
2874
|
const space = key.indexOf(" ");
|
|
2801
2875
|
const type = (space === -1 ? key : key.slice(0, space)).trim();
|
|
2802
2876
|
const selector = space === -1 ? "" : key.slice(space + 1).trim();
|
|
2803
|
-
if (type === "") throw new Error(`${ERROR_PREFIX$2}
|
|
2877
|
+
if (type === "") throw new Error(`${ERROR_PREFIX$2} island "${instance.def.name}" event key must start with an event type: "${key}"\n → use "<type>" or "<type> <selector>" (e.g. "click [data-action]")`);
|
|
2804
2878
|
const list = byType.get(type) ?? [];
|
|
2805
2879
|
list.push({
|
|
2806
2880
|
selector,
|
|
@@ -2823,62 +2897,62 @@ function attachEvents(instance, events) {
|
|
|
2823
2897
|
* Validate a single hook entry: its key must be a known hook name and its value
|
|
2824
2898
|
* must be a function. Throws fail-fast on the first violation.
|
|
2825
2899
|
*
|
|
2826
|
-
* @param
|
|
2900
|
+
* @param islandName - The owning island name (for error messages).
|
|
2827
2901
|
* @param source - The raw authoring object being validated.
|
|
2828
2902
|
* @param key - The hook key to validate.
|
|
2829
|
-
* @throws {Error} If `key` is not in `
|
|
2903
|
+
* @throws {Error} If `key` is not in `ISLAND_HOOK_NAMES`.
|
|
2830
2904
|
* @throws {TypeError} If the hook value is not a function.
|
|
2831
2905
|
* @example
|
|
2832
2906
|
* validateHookEntry("counter", source, "onMount");
|
|
2833
2907
|
*/
|
|
2834
|
-
function validateHookEntry(
|
|
2835
|
-
if (!HOOK_NAME_SET.has(key)) throw new Error(`${ERROR_PREFIX$2} unknown
|
|
2836
|
-
if (typeof source[key] !== "function") throw new TypeError(`${ERROR_PREFIX$2}
|
|
2908
|
+
function validateHookEntry(islandName, source, key) {
|
|
2909
|
+
if (!HOOK_NAME_SET.has(key)) throw new Error(`${ERROR_PREFIX$2} unknown island hook "${key}" on "${islandName}"\n → valid hooks: ${ISLAND_HOOK_NAMES.join(", ")}\n → spec keys: state, render, events, api`);
|
|
2910
|
+
if (typeof source[key] !== "function") throw new TypeError(`${ERROR_PREFIX$2} island hook "${key}" on "${islandName}" must be a function\n → provide a function or omit the hook`);
|
|
2837
2911
|
}
|
|
2838
2912
|
/**
|
|
2839
2913
|
* Validate the spec extras (`state`/`render`/`api` must be functions; `events` must be
|
|
2840
2914
|
* a plain object of functions). Throws fail-fast on the first violation.
|
|
2841
2915
|
*
|
|
2842
|
-
* @param
|
|
2916
|
+
* @param islandName - The owning island name (for error messages).
|
|
2843
2917
|
* @param extras - The partitioned spec extras to validate.
|
|
2844
2918
|
* @throws {TypeError} If a present extra has the wrong shape.
|
|
2845
2919
|
* @example
|
|
2846
2920
|
* validateSpecExtras("board", { state: () => ({}) });
|
|
2847
2921
|
*/
|
|
2848
|
-
function validateSpecExtras(
|
|
2922
|
+
function validateSpecExtras(islandName, extras) {
|
|
2849
2923
|
for (const key of [
|
|
2850
2924
|
"state",
|
|
2851
2925
|
"render",
|
|
2852
2926
|
"api"
|
|
2853
|
-
]) if (extras[key] !== void 0 && typeof extras[key] !== "function") throw new TypeError(`${ERROR_PREFIX$2}
|
|
2927
|
+
]) if (extras[key] !== void 0 && typeof extras[key] !== "function") throw new TypeError(`${ERROR_PREFIX$2} island "${key}" on "${islandName}" must be a function\n → provide a function or omit it`);
|
|
2854
2928
|
if (extras.events !== void 0) {
|
|
2855
2929
|
const events = extras.events;
|
|
2856
|
-
if (!(typeof events === "object")) throw new TypeError(`${ERROR_PREFIX$2}
|
|
2857
|
-
for (const [key, handler] of Object.entries(events)) if (typeof handler !== "function") throw new TypeError(`${ERROR_PREFIX$2}
|
|
2930
|
+
if (!(typeof events === "object")) throw new TypeError(`${ERROR_PREFIX$2} island "events" on "${islandName}" must be an object of handlers`);
|
|
2931
|
+
for (const [key, handler] of Object.entries(events)) if (typeof handler !== "function") throw new TypeError(`${ERROR_PREFIX$2} island event "${key}" on "${islandName}" must be a function`);
|
|
2858
2932
|
}
|
|
2859
2933
|
}
|
|
2860
2934
|
/**
|
|
2861
|
-
* Create a validated
|
|
2862
|
-
* (`
|
|
2863
|
-
* (`
|
|
2935
|
+
* Create a validated island definition. Accepts either the legacy hooks-only form
|
|
2936
|
+
* (`createIsland("counter", { onMount() {} })`) or the plugin-mirror spec form
|
|
2937
|
+
* (`createIsland("board", { state, render, events, api, ...hooks })`). Spec-only
|
|
2864
2938
|
* keys (`state`/`render`/`events`/`api`) are partitioned out before hook-name
|
|
2865
2939
|
* validation, so a real typo (e.g. `onMout`) still throws immediately while the spec
|
|
2866
2940
|
* keys are accepted.
|
|
2867
2941
|
*
|
|
2868
|
-
* @param name - Unique
|
|
2942
|
+
* @param name - Unique island name.
|
|
2869
2943
|
* @param spec - Lifecycle hooks, or the `{ state, render, events, api, ...hooks }` spec.
|
|
2870
|
-
* @returns A `
|
|
2944
|
+
* @returns A `IslandDef` ready to `register`.
|
|
2871
2945
|
* @throws {Error} If `name` is empty, a hook key is unknown, or an extra/hook value has the wrong shape.
|
|
2872
2946
|
* @example
|
|
2873
|
-
* const counter =
|
|
2947
|
+
* const counter = createIsland("counter", { onMount({ el }) { el.textContent = "0"; } });
|
|
2874
2948
|
* @example
|
|
2875
|
-
* const list =
|
|
2949
|
+
* const list = createIsland<{ items: string[] }>("list", {
|
|
2876
2950
|
* state: () => ({ items: [] }),
|
|
2877
2951
|
* render: (s) => h(List, { items: s.items })
|
|
2878
2952
|
* });
|
|
2879
2953
|
*/
|
|
2880
|
-
function
|
|
2881
|
-
if (name.trim() === "") throw new Error(`${ERROR_PREFIX$2}
|
|
2954
|
+
function createIsland(name, spec) {
|
|
2955
|
+
if (name.trim() === "") throw new Error(`${ERROR_PREFIX$2} island name must be a non-empty string\n → pass a unique name to createIsland("name", hooks)`);
|
|
2882
2956
|
const source = spec;
|
|
2883
2957
|
const hooks = {};
|
|
2884
2958
|
const extras = {};
|
|
@@ -2955,30 +3029,30 @@ function disposeInstance(state, instance) {
|
|
|
2955
3029
|
} catch {}
|
|
2956
3030
|
instance.cleanups.length = 0;
|
|
2957
3031
|
instance.renderScheduled = false;
|
|
2958
|
-
if (instance.api !== void 0 && state.
|
|
3032
|
+
if (instance.api !== void 0 && state.islandApis.get(instance.def.name) === instance.api) state.islandApis.delete(instance.def.name);
|
|
2959
3033
|
}
|
|
2960
3034
|
/**
|
|
2961
|
-
* Mounts a single `data-
|
|
3035
|
+
* Mounts a single `data-island` element: classifies persistent vs page-specific,
|
|
2962
3036
|
* builds the instance + its bound context, initializes per-instance `state`, registers
|
|
2963
3037
|
* its `api`, attaches declarative `events`, fires `onCreate` then `onMount` (capturing
|
|
2964
3038
|
* an async `onMount` + render-chunk load as `mountPromise`), schedules the initial
|
|
2965
|
-
* render, records it, and emits `spa:
|
|
2966
|
-
* mounted, has no
|
|
3039
|
+
* render, records it, and emits `spa:island-mount`. No-ops if the element is already
|
|
3040
|
+
* mounted, has no island name, or names an unregistered island.
|
|
2967
3041
|
*
|
|
2968
|
-
* @param state - The plugin state (
|
|
2969
|
-
* @param emit - The event emitter for spa:
|
|
3042
|
+
* @param state - The plugin state (registeredIslands + instances + islandApis).
|
|
3043
|
+
* @param emit - The event emitter for spa:island-mount.
|
|
2970
3044
|
* @param swapArea - The swap-region element, or null when none was found.
|
|
2971
3045
|
* @param data - The current page data payload.
|
|
2972
|
-
* @param element - The candidate element carrying a `data-
|
|
3046
|
+
* @param element - The candidate element carrying a `data-island` attribute.
|
|
2973
3047
|
* @param route - The matched-route slice for the current URL (params/meta/locale/url).
|
|
2974
3048
|
* @example
|
|
2975
3049
|
* mountElement(state, emit, swapArea, data, element, route);
|
|
2976
3050
|
*/
|
|
2977
3051
|
function mountElement(state, emit, swapArea, data, element, route = EMPTY_ROUTE) {
|
|
2978
3052
|
if (state.instances.has(element)) return;
|
|
2979
|
-
const name = element.dataset.
|
|
3053
|
+
const name = element.dataset.island;
|
|
2980
3054
|
if (!name) return;
|
|
2981
|
-
const definition = state.
|
|
3055
|
+
const definition = state.registeredIslands.get(name);
|
|
2982
3056
|
if (!definition) return;
|
|
2983
3057
|
const instance = {
|
|
2984
3058
|
def: definition,
|
|
@@ -3004,7 +3078,7 @@ function mountElement(state, emit, swapArea, data, element, route = EMPTY_ROUTE)
|
|
|
3004
3078
|
if (spec?.state) instance.state = spec.state(instance.ctx);
|
|
3005
3079
|
if (spec?.api) {
|
|
3006
3080
|
instance.api = spec.api(instance.ctx);
|
|
3007
|
-
state.
|
|
3081
|
+
state.islandApis.set(definition.name, instance.api);
|
|
3008
3082
|
}
|
|
3009
3083
|
if (spec?.events) attachEvents(instance, spec.events);
|
|
3010
3084
|
runHook(instance, "onCreate");
|
|
@@ -3015,20 +3089,20 @@ function mountElement(state, emit, swapArea, data, element, route = EMPTY_ROUTE)
|
|
|
3015
3089
|
if (onMountResult && typeof onMountResult.then === "function") pending.push(onMountResult);
|
|
3016
3090
|
instance.mountPromise = pending.length > 0 ? Promise.all(pending).then(() => {}) : void 0;
|
|
3017
3091
|
state.instances.set(element, instance);
|
|
3018
|
-
emit("spa:
|
|
3092
|
+
emit("spa:island-mount", {
|
|
3019
3093
|
name: definition.name,
|
|
3020
3094
|
el: element
|
|
3021
3095
|
});
|
|
3022
3096
|
}
|
|
3023
3097
|
/**
|
|
3024
|
-
* Scans the swap region, mounts
|
|
3098
|
+
* Scans the swap region, mounts islands for matching `data-island` elements,
|
|
3025
3099
|
* classifies persistent (outside swap area) vs page-specific (inside), runs
|
|
3026
|
-
* `onCreate`/`onMount` + initial render, and emits `spa:
|
|
3100
|
+
* `onCreate`/`onMount` + initial render, and emits `spa:island-mount` per instance.
|
|
3027
3101
|
* Already-mounted elements are skipped.
|
|
3028
3102
|
*
|
|
3029
|
-
* @param state - The plugin state (
|
|
3030
|
-
* @param emit - The event emitter for spa:
|
|
3031
|
-
* @param swapSelector - CSS selector bounding page-specific
|
|
3103
|
+
* @param state - The plugin state (registeredIslands + instances + islandApis).
|
|
3104
|
+
* @param emit - The event emitter for spa:island-mount.
|
|
3105
|
+
* @param swapSelector - CSS selector bounding page-specific islands.
|
|
3032
3106
|
* @param route - The matched-route slice for the current URL (params/meta/locale/url).
|
|
3033
3107
|
* @example
|
|
3034
3108
|
* scanAndMount(state, emit, "main > section", route);
|
|
@@ -3037,16 +3111,16 @@ function scanAndMount(state, emit, swapSelector, route = EMPTY_ROUTE) {
|
|
|
3037
3111
|
if (typeof document === "undefined") return;
|
|
3038
3112
|
const swapArea = document.querySelector(swapSelector);
|
|
3039
3113
|
const data = extractPageData(document);
|
|
3040
|
-
for (const element of document.querySelectorAll("[data-
|
|
3114
|
+
for (const element of document.querySelectorAll("[data-island]")) mountElement(state, emit, swapArea, data, element, route);
|
|
3041
3115
|
}
|
|
3042
3116
|
/**
|
|
3043
3117
|
* Unmounts page-specific instances inside the swap region (runs `onUnMount` then
|
|
3044
3118
|
* `onDestroy`, then their cleanup disposers + api unregister), removes them from state,
|
|
3045
|
-
* and emits `spa:
|
|
3119
|
+
* and emits `spa:island-unmount`. Persistent instances (outside the swap area) are
|
|
3046
3120
|
* left in place.
|
|
3047
3121
|
*
|
|
3048
3122
|
* @param state - The plugin state holding live instances.
|
|
3049
|
-
* @param emit - The event emitter for spa:
|
|
3123
|
+
* @param emit - The event emitter for spa:island-unmount.
|
|
3050
3124
|
* @example
|
|
3051
3125
|
* unmountPageSpecific(state, emit);
|
|
3052
3126
|
*/
|
|
@@ -3059,7 +3133,7 @@ function unmountPageSpecific(state, emit) {
|
|
|
3059
3133
|
runHook(instance, "onDestroy");
|
|
3060
3134
|
disposeInstance(state, instance);
|
|
3061
3135
|
state.instances.delete(element);
|
|
3062
|
-
emit("spa:
|
|
3136
|
+
emit("spa:island-unmount", {
|
|
3063
3137
|
name: instance.def.name,
|
|
3064
3138
|
el: element
|
|
3065
3139
|
});
|
|
@@ -3068,11 +3142,11 @@ function unmountPageSpecific(state, emit) {
|
|
|
3068
3142
|
/**
|
|
3069
3143
|
* Disposes ALL live instances (persistent and page-specific) on teardown: runs
|
|
3070
3144
|
* `onUnMount` then `onDestroy`, then their cleanup disposers + api unregister, emits
|
|
3071
|
-
* `spa:
|
|
3145
|
+
* `spa:island-unmount`, and clears the instance + api maps. Used by the kernel's
|
|
3072
3146
|
* `dispose` on plugin stop.
|
|
3073
3147
|
*
|
|
3074
3148
|
* @param state - The plugin state holding live instances.
|
|
3075
|
-
* @param emit - The event emitter for spa:
|
|
3149
|
+
* @param emit - The event emitter for spa:island-unmount.
|
|
3076
3150
|
* @example
|
|
3077
3151
|
* unmountAll(state, emit);
|
|
3078
3152
|
*/
|
|
@@ -3083,13 +3157,13 @@ function unmountAll(state, emit) {
|
|
|
3083
3157
|
runHook(instance, "onUnMount");
|
|
3084
3158
|
runHook(instance, "onDestroy");
|
|
3085
3159
|
disposeInstance(state, instance);
|
|
3086
|
-
emit("spa:
|
|
3160
|
+
emit("spa:island-unmount", {
|
|
3087
3161
|
name: instance.def.name,
|
|
3088
3162
|
el: element
|
|
3089
3163
|
});
|
|
3090
3164
|
}
|
|
3091
3165
|
state.instances.clear();
|
|
3092
|
-
state.
|
|
3166
|
+
state.islandApis.clear();
|
|
3093
3167
|
}
|
|
3094
3168
|
/**
|
|
3095
3169
|
* Fires `onNavStart` on every currently-mounted instance (persistent instances
|
|
@@ -3126,80 +3200,6 @@ function notifyNavEnd(state, route = EMPTY_ROUTE) {
|
|
|
3126
3200
|
}
|
|
3127
3201
|
}
|
|
3128
3202
|
//#endregion
|
|
3129
|
-
//#region src/plugins/spa/head.ts
|
|
3130
|
-
/** Single-element head selectors synced by replace/append/remove on navigation. */
|
|
3131
|
-
const META_SELECTORS = [
|
|
3132
|
-
"meta[name=\"description\"]",
|
|
3133
|
-
"meta[property=\"og:title\"]",
|
|
3134
|
-
"meta[property=\"og:description\"]",
|
|
3135
|
-
"meta[property=\"og:url\"]",
|
|
3136
|
-
"meta[property=\"og:image\"]",
|
|
3137
|
-
"meta[property=\"og:type\"]",
|
|
3138
|
-
"meta[property=\"og:locale\"]",
|
|
3139
|
-
"meta[name=\"twitter:card\"]",
|
|
3140
|
-
"meta[name=\"twitter:title\"]",
|
|
3141
|
-
"meta[name=\"twitter:description\"]",
|
|
3142
|
-
"meta[name=\"twitter:image\"]",
|
|
3143
|
-
"meta[name=\"twitter:site\"]",
|
|
3144
|
-
"link[rel=\"canonical\"]"
|
|
3145
|
-
];
|
|
3146
|
-
/** Head element groups fully replaced (remove-all-then-clone) on navigation. */
|
|
3147
|
-
const REPLACE_ALL_SELECTORS = [
|
|
3148
|
-
"script[type=\"application/ld+json\"]",
|
|
3149
|
-
"link[rel=\"alternate\"][hreflang]",
|
|
3150
|
-
"meta[property^=\"article:\"]"
|
|
3151
|
-
];
|
|
3152
|
-
/**
|
|
3153
|
-
* Sync a single head element by selector between the fetched and live document:
|
|
3154
|
-
* replace when both exist, append when only the new doc has it, remove when only
|
|
3155
|
-
* the live doc has it.
|
|
3156
|
-
*
|
|
3157
|
-
* @param selector - CSS selector for the head element to sync.
|
|
3158
|
-
* @param doc - The fetched document (DOMParser-parsed).
|
|
3159
|
-
* @example
|
|
3160
|
-
* syncElement('link[rel="canonical"]', doc);
|
|
3161
|
-
*/
|
|
3162
|
-
function syncElement(selector, doc) {
|
|
3163
|
-
const newElement = doc.querySelector(selector);
|
|
3164
|
-
const oldElement = document.querySelector(selector);
|
|
3165
|
-
if (newElement && oldElement) oldElement.replaceWith(newElement.cloneNode(true));
|
|
3166
|
-
else if (newElement) document.head.append(newElement.cloneNode(true));
|
|
3167
|
-
else if (oldElement) oldElement.remove();
|
|
3168
|
-
}
|
|
3169
|
-
/**
|
|
3170
|
-
* Remove all live matches for a selector and re-clone the fetched document's
|
|
3171
|
-
* matches into the live `<head>`.
|
|
3172
|
-
*
|
|
3173
|
-
* @param selector - CSS selector for the element group to replace wholesale.
|
|
3174
|
-
* @param doc - The fetched document (DOMParser-parsed).
|
|
3175
|
-
* @example
|
|
3176
|
-
* replaceAllBySelector('script[type="application/ld+json"]', doc);
|
|
3177
|
-
*/
|
|
3178
|
-
function replaceAllBySelector(selector, doc) {
|
|
3179
|
-
for (const element of document.querySelectorAll(selector)) element.remove();
|
|
3180
|
-
for (const element of doc.querySelectorAll(selector)) document.head.append(element.cloneNode(true));
|
|
3181
|
-
}
|
|
3182
|
-
/**
|
|
3183
|
-
* Syncs the live document `<head>` after a navigation from the fetched document
|
|
3184
|
-
* (whose head was composed by the `head` plugin). Recomputes
|
|
3185
|
-
* title/meta/canonical/JSON-LD/hreflang/`<html lang>` once and applies them.
|
|
3186
|
-
* The `head` API is accepted to bind the structural dependency (spec/09 deps).
|
|
3187
|
-
*
|
|
3188
|
-
* @param _head - The head plugin API (dependency binding; composition reused via the fetched doc).
|
|
3189
|
-
* @param doc - The fetched document parsed from the navigated page's HTML.
|
|
3190
|
-
* @example
|
|
3191
|
-
* syncHead(headApi, parsedDoc);
|
|
3192
|
-
*/
|
|
3193
|
-
function syncHead(_head, doc) {
|
|
3194
|
-
if (typeof document === "undefined") return;
|
|
3195
|
-
const newTitle = doc.querySelector("title")?.textContent;
|
|
3196
|
-
if (newTitle) document.title = newTitle;
|
|
3197
|
-
const newLang = doc.documentElement.lang;
|
|
3198
|
-
if (newLang) document.documentElement.lang = newLang;
|
|
3199
|
-
for (const selector of META_SELECTORS) syncElement(selector, doc);
|
|
3200
|
-
for (const selector of REPLACE_ALL_SELECTORS) replaceAllBySelector(selector, doc);
|
|
3201
|
-
}
|
|
3202
|
-
//#endregion
|
|
3203
3203
|
//#region src/plugins/spa/progress.ts
|
|
3204
3204
|
/** Delay before the bar appears, so fast navigations show no indicator. */
|
|
3205
3205
|
const START_DELAY_MS = 150;
|
|
@@ -3606,7 +3606,7 @@ const defaultSpaConfig = {
|
|
|
3606
3606
|
swapSelector: "main > section",
|
|
3607
3607
|
viewTransitions: false,
|
|
3608
3608
|
progressBar: true,
|
|
3609
|
-
|
|
3609
|
+
islands: []
|
|
3610
3610
|
};
|
|
3611
3611
|
/**
|
|
3612
3612
|
* Whether a selector is syntactically valid (parseable by the DOM). Falls back
|
|
@@ -3628,8 +3628,8 @@ function isValidSelector(selector) {
|
|
|
3628
3628
|
}
|
|
3629
3629
|
/**
|
|
3630
3630
|
* Validates the spa config and applies defaults (Part-3 errors on an empty or
|
|
3631
|
-
* syntactically-invalid `swapSelector`).
|
|
3632
|
-
* `
|
|
3631
|
+
* syntactically-invalid `swapSelector`). Island-hook validation runs later in
|
|
3632
|
+
* `createIsland` when the islands are registered.
|
|
3633
3633
|
*
|
|
3634
3634
|
* @param config - The raw spa config to validate.
|
|
3635
3635
|
* @returns The fully-resolved config with defaults applied.
|
|
@@ -3645,7 +3645,7 @@ function resolveSpaConfig(config) {
|
|
|
3645
3645
|
swapSelector,
|
|
3646
3646
|
viewTransitions: config.viewTransitions ?? false,
|
|
3647
3647
|
progressBar: config.progressBar ?? true,
|
|
3648
|
-
|
|
3648
|
+
islands: config.islands ?? []
|
|
3649
3649
|
};
|
|
3650
3650
|
}
|
|
3651
3651
|
/**
|
|
@@ -3662,9 +3662,9 @@ function resolveSpaConfig(config) {
|
|
|
3662
3662
|
*/
|
|
3663
3663
|
function createState(_ctx) {
|
|
3664
3664
|
return {
|
|
3665
|
-
|
|
3665
|
+
registeredIslands: /* @__PURE__ */ new Map(),
|
|
3666
3666
|
instances: /* @__PURE__ */ new Map(),
|
|
3667
|
-
|
|
3667
|
+
islandApis: /* @__PURE__ */ new Map(),
|
|
3668
3668
|
currentUrl: "",
|
|
3669
3669
|
destroyRouter: null,
|
|
3670
3670
|
started: false,
|
|
@@ -3684,15 +3684,15 @@ function createState(_ctx) {
|
|
|
3684
3684
|
/** Error prefix for spa kernel failures (spec/11 Part-3). */
|
|
3685
3685
|
const ERROR_PREFIX = "[web]";
|
|
3686
3686
|
/**
|
|
3687
|
-
* Registers a
|
|
3687
|
+
* Registers a island definition into state (last-registered-wins).
|
|
3688
3688
|
*
|
|
3689
|
-
* @param state - The plugin state holding
|
|
3690
|
-
* @param
|
|
3689
|
+
* @param state - The plugin state holding registeredIslands.
|
|
3690
|
+
* @param island - The island definition to register.
|
|
3691
3691
|
* @example
|
|
3692
|
-
*
|
|
3692
|
+
* registerIsland(state, counter);
|
|
3693
3693
|
*/
|
|
3694
|
-
function
|
|
3695
|
-
state.
|
|
3694
|
+
function registerIsland(state, island) {
|
|
3695
|
+
state.registeredIslands.set(island.name, island);
|
|
3696
3696
|
}
|
|
3697
3697
|
/**
|
|
3698
3698
|
* Resolve the current document URL (pathname + search), or `""` when headless.
|
|
@@ -3767,15 +3767,15 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
3767
3767
|
});
|
|
3768
3768
|
};
|
|
3769
3769
|
/**
|
|
3770
|
-
* Build the matched-route slice (params/meta/locale/url) for the
|
|
3770
|
+
* Build the matched-route slice (params/meta/locale/url) for the island context at `path`,
|
|
3771
3771
|
* so islands read their route's params/meta directly. An unmatched path yields an empty slice.
|
|
3772
3772
|
*
|
|
3773
3773
|
* @param path - The URL (pathname + search) to match.
|
|
3774
3774
|
* @returns The route slice for the matched route.
|
|
3775
3775
|
* @example
|
|
3776
|
-
* scanAndMount(state, emit, resolved.swapSelector,
|
|
3776
|
+
* scanAndMount(state, emit, resolved.swapSelector, islandRouteContext(pathname));
|
|
3777
3777
|
*/
|
|
3778
|
-
const
|
|
3778
|
+
const islandRouteContext = (path) => {
|
|
3779
3779
|
const matchPath = path.split("?")[0] ?? path;
|
|
3780
3780
|
const hit = deps.router.match(matchPath);
|
|
3781
3781
|
const locale = hit?.params.lang ?? (typeof document === "undefined" ? "" : document.documentElement.lang) ?? "";
|
|
@@ -3804,7 +3804,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
3804
3804
|
syncHead(deps.head, doc);
|
|
3805
3805
|
unmountPageSpecific(state, emit);
|
|
3806
3806
|
if (!swapRegion(doc, resolved.swapSelector, resolved.viewTransitions, () => {
|
|
3807
|
-
const routeSlice =
|
|
3807
|
+
const routeSlice = islandRouteContext(pathname);
|
|
3808
3808
|
scanAndMount(state, emit, resolved.swapSelector, routeSlice);
|
|
3809
3809
|
notifyNavEnd(state, routeSlice);
|
|
3810
3810
|
}, applyPendingScroll)) {
|
|
@@ -3817,7 +3817,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
3817
3817
|
emit("spa:navigated", { url: pathname });
|
|
3818
3818
|
};
|
|
3819
3819
|
/**
|
|
3820
|
-
* Begin a navigation: start progress, notify
|
|
3820
|
+
* Begin a navigation: start progress, notify islands, emit navigate.
|
|
3821
3821
|
*
|
|
3822
3822
|
* @param pathname - The destination pathname.
|
|
3823
3823
|
* @example
|
|
@@ -3903,11 +3903,11 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
3903
3903
|
const commitDataRender = async (pathname, resolvedRender, signal) => {
|
|
3904
3904
|
if (signal?.aborted) return;
|
|
3905
3905
|
const { route, vnode, routeContext, region } = resolvedRender;
|
|
3906
|
-
const { renderVNode } = await import("./render-
|
|
3906
|
+
const { renderVNode } = await import("./render-yXHc9BWI.mjs");
|
|
3907
3907
|
if (signal?.aborted) return;
|
|
3908
3908
|
syncDataHead(deps.head, route, routeContext);
|
|
3909
3909
|
unmountPageSpecific(state, emit);
|
|
3910
|
-
const routeSlice =
|
|
3910
|
+
const routeSlice = islandRouteContext(pathname);
|
|
3911
3911
|
/**
|
|
3912
3912
|
* Render the VNode into the region and re-mount its islands in one paint — the
|
|
3913
3913
|
* swap body handed to `runSwap` (optionally wrapped in a View Transition).
|
|
@@ -3965,14 +3965,14 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
3965
3965
|
* await bootRender("/b/abc123");
|
|
3966
3966
|
*/
|
|
3967
3967
|
const bootRender = async (pathname) => {
|
|
3968
|
-
const routeSlice =
|
|
3968
|
+
const routeSlice = islandRouteContext(pathname);
|
|
3969
3969
|
const resolvedRender = await resolveDataRender(pathname);
|
|
3970
3970
|
if (resolvedRender === false) {
|
|
3971
3971
|
scanAndMount(state, emit, resolved.swapSelector, routeSlice);
|
|
3972
3972
|
return;
|
|
3973
3973
|
}
|
|
3974
3974
|
const { vnode, region } = resolvedRender;
|
|
3975
|
-
const { renderVNode } = await import("./render-
|
|
3975
|
+
const { renderVNode } = await import("./render-yXHc9BWI.mjs");
|
|
3976
3976
|
renderVNode(vnode, region);
|
|
3977
3977
|
scanAndMount(state, emit, resolved.swapSelector, routeSlice);
|
|
3978
3978
|
};
|
|
@@ -4000,13 +4000,13 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
4000
4000
|
};
|
|
4001
4001
|
return {
|
|
4002
4002
|
/**
|
|
4003
|
-
* Register config
|
|
4003
|
+
* Register config islands and seed currentUrl from the document.
|
|
4004
4004
|
*
|
|
4005
4005
|
* @example
|
|
4006
4006
|
* kernel.init();
|
|
4007
4007
|
*/
|
|
4008
4008
|
init() {
|
|
4009
|
-
for (const
|
|
4009
|
+
for (const island of resolved.islands) registerIsland(state, island);
|
|
4010
4010
|
state.currentUrl = currentLocationUrl();
|
|
4011
4011
|
},
|
|
4012
4012
|
/**
|
|
@@ -4024,18 +4024,18 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
4024
4024
|
const matchPath = state.currentUrl.split("?")[0] ?? state.currentUrl;
|
|
4025
4025
|
const hit = deps.router.match(matchPath);
|
|
4026
4026
|
if (hit?.route._handlers.render && isClientOnlyRoute(deps.router.mode(), hit.route)) bootRender(state.currentUrl);
|
|
4027
|
-
else scanAndMount(state, emit, resolved.swapSelector,
|
|
4027
|
+
else scanAndMount(state, emit, resolved.swapSelector, islandRouteContext(state.currentUrl));
|
|
4028
4028
|
state.started = true;
|
|
4029
4029
|
},
|
|
4030
4030
|
/**
|
|
4031
|
-
* Register a
|
|
4031
|
+
* Register a island definition (last-registered-wins).
|
|
4032
4032
|
*
|
|
4033
|
-
* @param
|
|
4033
|
+
* @param island - The island definition to register.
|
|
4034
4034
|
* @example
|
|
4035
4035
|
* kernel.register(counter);
|
|
4036
4036
|
*/
|
|
4037
|
-
register(
|
|
4038
|
-
|
|
4037
|
+
register(island) {
|
|
4038
|
+
registerIsland(state, island);
|
|
4039
4039
|
},
|
|
4040
4040
|
/**
|
|
4041
4041
|
* Process a navigation to `path` (fetch then swap; full reload on error).
|
|
@@ -4049,13 +4049,13 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
4049
4049
|
navigate(path).catch(() => {});
|
|
4050
4050
|
},
|
|
4051
4051
|
/**
|
|
4052
|
-
* Scan the swap region and mount
|
|
4052
|
+
* Scan the swap region and mount islands for matching elements.
|
|
4053
4053
|
*
|
|
4054
4054
|
* @example
|
|
4055
4055
|
* kernel.scan();
|
|
4056
4056
|
*/
|
|
4057
4057
|
scan() {
|
|
4058
|
-
scanAndMount(state, emit, resolved.swapSelector,
|
|
4058
|
+
scanAndMount(state, emit, resolved.swapSelector, islandRouteContext(state.currentUrl));
|
|
4059
4059
|
},
|
|
4060
4060
|
/**
|
|
4061
4061
|
* Tear down router listeners, dispose all instances, reset boot state.
|
|
@@ -4074,7 +4074,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
4074
4074
|
}
|
|
4075
4075
|
/**
|
|
4076
4076
|
* Builds the shared kernel from the plugin context, stores it on `ctx.state`,
|
|
4077
|
-
* and runs its init step (validate config, register config.
|
|
4077
|
+
* and runs its init step (validate config, register config.islands, seed
|
|
4078
4078
|
* currentUrl). Captures the OPTIONAL `data` reader when the `data` plugin is
|
|
4079
4079
|
* composed (enabling client DATA navigation) — resolved by instance via
|
|
4080
4080
|
* `ctx.require(dataPlugin)`, guarded by `ctx.has("data")` so `data` stays optional
|
|
@@ -4142,10 +4142,10 @@ function disposeSpa() {
|
|
|
4142
4142
|
/**
|
|
4143
4143
|
* @file `lazyEmbed` island — activates the static embed facades emitted by the
|
|
4144
4144
|
* content pipeline's `::embed` directive (pipeline/embed.ts). Mounts on every
|
|
4145
|
-
* `[data-
|
|
4145
|
+
* `[data-island="lazy-embed"]` figure; a click on the facade's button swaps
|
|
4146
4146
|
* it for the real `<iframe loading="lazy">`. Until that click the embedded
|
|
4147
4147
|
* document costs the page nothing — no request, no third-party JS, no
|
|
4148
|
-
* scroll-jacking. Register it in `pluginConfigs.spa.
|
|
4148
|
+
* scroll-jacking. Register it in `pluginConfigs.spa.islands`; all visual
|
|
4149
4149
|
* chrome (`.lazy-embed*` classes) is consumer CSS.
|
|
4150
4150
|
*/
|
|
4151
4151
|
/** CSS class on the injected `<iframe>` (consumer CSS sizes it). */
|
|
@@ -4198,7 +4198,7 @@ function onFacadeClick(event) {
|
|
|
4198
4198
|
* Lazy-embed island: facade button click → real `<iframe loading="lazy">`.
|
|
4199
4199
|
* The companion of the content pipeline's `::embed` directive.
|
|
4200
4200
|
*/
|
|
4201
|
-
const lazyEmbed =
|
|
4201
|
+
const lazyEmbed = createIsland("lazy-embed", {
|
|
4202
4202
|
/**
|
|
4203
4203
|
* Bind the activation click handler when a facade mounts.
|
|
4204
4204
|
*
|
|
@@ -4224,18 +4224,18 @@ const lazyEmbed = createComponent("lazy-embed", {
|
|
|
4224
4224
|
//#region src/plugins/spa/index.ts
|
|
4225
4225
|
/**
|
|
4226
4226
|
* @file spa — Complex Plugin (WIRING ONLY, ≤30 lines). All logic lives in the
|
|
4227
|
-
* domain files (kernel/router/head/progress/
|
|
4227
|
+
* domain files (kernel/router/head/progress/islands/lifecycle); index wires.
|
|
4228
4228
|
*
|
|
4229
4229
|
* Depends: router, head.
|
|
4230
|
-
* Emits: spa:navigate, spa:navigated, spa:
|
|
4230
|
+
* Emits: spa:navigate, spa:navigated, spa:island-mount, spa:island-unmount.
|
|
4231
4231
|
* @see README.md
|
|
4232
4232
|
*/
|
|
4233
4233
|
/**
|
|
4234
4234
|
* SPA plugin — progressive client-side navigation layered over the static site:
|
|
4235
4235
|
* swaps a page region on navigation, with an optional progress bar and View
|
|
4236
|
-
* Transitions. Register interactive islands with {@link
|
|
4237
|
-
* on router and head; emits `spa:navigate`, `spa:navigated`, `spa:
|
|
4238
|
-
* and `spa:
|
|
4236
|
+
* Transitions. Register interactive islands with {@link createIsland}. Depends
|
|
4237
|
+
* on router and head; emits `spa:navigate`, `spa:navigated`, `spa:island-mount`,
|
|
4238
|
+
* and `spa:island-unmount`.
|
|
4239
4239
|
*
|
|
4240
4240
|
* @example Enable view transitions and a custom swap region
|
|
4241
4241
|
* ```ts
|
|
@@ -4903,4 +4903,4 @@ const createApp = core.createApp;
|
|
|
4903
4903
|
*/
|
|
4904
4904
|
const createPlugin = core.createPlugin;
|
|
4905
4905
|
//#endregion
|
|
4906
|
-
export { types_exports as Content, types_exports$1 as Data, types_exports$2 as Head, types_exports$3 as Router, types_exports$4 as Spa, browserEnv, buildArticleHead, canonical, contentPlugin, createApp,
|
|
4906
|
+
export { types_exports as Content, types_exports$1 as Data, types_exports$2 as Head, types_exports$3 as Router, types_exports$4 as Spa, browserEnv, buildArticleHead, canonical, contentPlugin, createApp, createIsland, createPlugin, createUrls, dataPlugin, defineRoutes, envPlugin, feedLink, headPlugin, hreflang, i18nPlugin, jsonLd, lazyEmbed, logPlugin, meta, og, route, routerPlugin, sitePlugin, spaPlugin, twitter };
|