@moku-labs/web 1.17.0 → 2.0.1
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 +3 -3
package/dist/index.cjs
CHANGED
|
@@ -9147,15 +9147,15 @@ const cliPlugin = createPlugin$1("cli", {
|
|
|
9147
9147
|
function createApi(ctx) {
|
|
9148
9148
|
return {
|
|
9149
9149
|
/**
|
|
9150
|
-
* Register a
|
|
9150
|
+
* Register a island definition (last-registered-wins); warns on collision.
|
|
9151
9151
|
*
|
|
9152
|
-
* @param
|
|
9152
|
+
* @param island - The island definition created via `createIsland`.
|
|
9153
9153
|
* @example
|
|
9154
9154
|
* app.spa.register(counter);
|
|
9155
9155
|
*/
|
|
9156
|
-
register(
|
|
9157
|
-
if (ctx.state.
|
|
9158
|
-
ctx.state.kernel?.register(
|
|
9156
|
+
register(island) {
|
|
9157
|
+
if (ctx.state.registeredIslands.has(island.name)) ctx.log.warn("spa:island-collision", { name: island.name });
|
|
9158
|
+
ctx.state.kernel?.register(island);
|
|
9159
9159
|
},
|
|
9160
9160
|
/**
|
|
9161
9161
|
* Programmatically navigate to a path (client runtime; no-op without a DOM).
|
|
@@ -9181,13 +9181,13 @@ function createApi(ctx) {
|
|
|
9181
9181
|
* Resolve a registered island's api by name (the cross-island seam). Returns
|
|
9182
9182
|
* `undefined` when no provider with that name is currently registered.
|
|
9183
9183
|
*
|
|
9184
|
-
* @param name - The provider island's
|
|
9184
|
+
* @param name - The provider island's island name.
|
|
9185
9185
|
* @returns The provider's api, or `undefined`.
|
|
9186
9186
|
* @example
|
|
9187
|
-
* app.spa.
|
|
9187
|
+
* app.spa.island("lightbox");
|
|
9188
9188
|
*/
|
|
9189
|
-
|
|
9190
|
-
return ctx.state.
|
|
9189
|
+
island(name) {
|
|
9190
|
+
return ctx.state.islandApis.get(name);
|
|
9191
9191
|
}
|
|
9192
9192
|
};
|
|
9193
9193
|
}
|
|
@@ -9206,15 +9206,89 @@ function spaEvents(register) {
|
|
|
9206
9206
|
return {
|
|
9207
9207
|
"spa:navigate": register("A navigation has been intercepted and is starting."),
|
|
9208
9208
|
"spa:navigated": register("The swap completed and the new URL is active."),
|
|
9209
|
-
"spa:
|
|
9210
|
-
"spa:
|
|
9209
|
+
"spa:island-mount": register("A island instance attached to an element."),
|
|
9210
|
+
"spa:island-unmount": register("A island instance detached from an element.")
|
|
9211
9211
|
};
|
|
9212
9212
|
}
|
|
9213
9213
|
//#endregion
|
|
9214
|
+
//#region src/plugins/spa/head.ts
|
|
9215
|
+
/** Single-element head selectors synced by replace/append/remove on navigation. */
|
|
9216
|
+
const META_SELECTORS = [
|
|
9217
|
+
"meta[name=\"description\"]",
|
|
9218
|
+
"meta[property=\"og:title\"]",
|
|
9219
|
+
"meta[property=\"og:description\"]",
|
|
9220
|
+
"meta[property=\"og:url\"]",
|
|
9221
|
+
"meta[property=\"og:image\"]",
|
|
9222
|
+
"meta[property=\"og:type\"]",
|
|
9223
|
+
"meta[property=\"og:locale\"]",
|
|
9224
|
+
"meta[name=\"twitter:card\"]",
|
|
9225
|
+
"meta[name=\"twitter:title\"]",
|
|
9226
|
+
"meta[name=\"twitter:description\"]",
|
|
9227
|
+
"meta[name=\"twitter:image\"]",
|
|
9228
|
+
"meta[name=\"twitter:site\"]",
|
|
9229
|
+
"link[rel=\"canonical\"]"
|
|
9230
|
+
];
|
|
9231
|
+
/** Head element groups fully replaced (remove-all-then-clone) on navigation. */
|
|
9232
|
+
const REPLACE_ALL_SELECTORS = [
|
|
9233
|
+
"script[type=\"application/ld+json\"]",
|
|
9234
|
+
"link[rel=\"alternate\"][hreflang]",
|
|
9235
|
+
"meta[property^=\"article:\"]"
|
|
9236
|
+
];
|
|
9237
|
+
/**
|
|
9238
|
+
* Sync a single head element by selector between the fetched and live document:
|
|
9239
|
+
* replace when both exist, append when only the new doc has it, remove when only
|
|
9240
|
+
* the live doc has it.
|
|
9241
|
+
*
|
|
9242
|
+
* @param selector - CSS selector for the head element to sync.
|
|
9243
|
+
* @param doc - The fetched document (DOMParser-parsed).
|
|
9244
|
+
* @example
|
|
9245
|
+
* syncElement('link[rel="canonical"]', doc);
|
|
9246
|
+
*/
|
|
9247
|
+
function syncElement(selector, doc) {
|
|
9248
|
+
const newElement = doc.querySelector(selector);
|
|
9249
|
+
const oldElement = document.querySelector(selector);
|
|
9250
|
+
if (newElement && oldElement) oldElement.replaceWith(newElement.cloneNode(true));
|
|
9251
|
+
else if (newElement) document.head.append(newElement.cloneNode(true));
|
|
9252
|
+
else if (oldElement) oldElement.remove();
|
|
9253
|
+
}
|
|
9254
|
+
/**
|
|
9255
|
+
* Remove all live matches for a selector and re-clone the fetched document's
|
|
9256
|
+
* matches into the live `<head>`.
|
|
9257
|
+
*
|
|
9258
|
+
* @param selector - CSS selector for the element group to replace wholesale.
|
|
9259
|
+
* @param doc - The fetched document (DOMParser-parsed).
|
|
9260
|
+
* @example
|
|
9261
|
+
* replaceAllBySelector('script[type="application/ld+json"]', doc);
|
|
9262
|
+
*/
|
|
9263
|
+
function replaceAllBySelector(selector, doc) {
|
|
9264
|
+
for (const element of document.querySelectorAll(selector)) element.remove();
|
|
9265
|
+
for (const element of doc.querySelectorAll(selector)) document.head.append(element.cloneNode(true));
|
|
9266
|
+
}
|
|
9267
|
+
/**
|
|
9268
|
+
* Syncs the live document `<head>` after a navigation from the fetched document
|
|
9269
|
+
* (whose head was composed by the `head` plugin). Recomputes
|
|
9270
|
+
* title/meta/canonical/JSON-LD/hreflang/`<html lang>` once and applies them.
|
|
9271
|
+
* The `head` API is accepted to bind the structural dependency (spec/09 deps).
|
|
9272
|
+
*
|
|
9273
|
+
* @param _head - The head plugin API (dependency binding; composition reused via the fetched doc).
|
|
9274
|
+
* @param doc - The fetched document parsed from the navigated page's HTML.
|
|
9275
|
+
* @example
|
|
9276
|
+
* syncHead(headApi, parsedDoc);
|
|
9277
|
+
*/
|
|
9278
|
+
function syncHead(_head, doc) {
|
|
9279
|
+
if (typeof document === "undefined") return;
|
|
9280
|
+
const newTitle = doc.querySelector("title")?.textContent;
|
|
9281
|
+
if (newTitle) document.title = newTitle;
|
|
9282
|
+
const newLang = doc.documentElement.lang;
|
|
9283
|
+
if (newLang) document.documentElement.lang = newLang;
|
|
9284
|
+
for (const selector of META_SELECTORS) syncElement(selector, doc);
|
|
9285
|
+
for (const selector of REPLACE_ALL_SELECTORS) replaceAllBySelector(selector, doc);
|
|
9286
|
+
}
|
|
9287
|
+
//#endregion
|
|
9214
9288
|
//#region src/plugins/spa/types.ts
|
|
9215
|
-
var types_exports$7 = /* @__PURE__ */ require_convention.__exportAll({
|
|
9289
|
+
var types_exports$7 = /* @__PURE__ */ require_convention.__exportAll({ ISLAND_HOOK_NAMES: () => ISLAND_HOOK_NAMES });
|
|
9216
9290
|
/** Allowed hook names — single source of truth for fail-fast validation. */
|
|
9217
|
-
const
|
|
9291
|
+
const ISLAND_HOOK_NAMES = [
|
|
9218
9292
|
"onCreate",
|
|
9219
9293
|
"onMount",
|
|
9220
9294
|
"onNavStart",
|
|
@@ -9223,10 +9297,10 @@ const COMPONENT_HOOK_NAMES = [
|
|
|
9223
9297
|
"onDestroy"
|
|
9224
9298
|
];
|
|
9225
9299
|
//#endregion
|
|
9226
|
-
//#region src/plugins/spa/
|
|
9300
|
+
//#region src/plugins/spa/islands.ts
|
|
9227
9301
|
/**
|
|
9228
|
-
* @file spa plugin —
|
|
9229
|
-
* surface (`
|
|
9302
|
+
* @file spa plugin — island lifecycle, mounting, the plugin-mirror authoring
|
|
9303
|
+
* surface (`createIsland` with a typed `{ state, render, events, api }` spec),
|
|
9230
9304
|
* the per-instance state + microtask-batched render scheduler, declarative
|
|
9231
9305
|
* delegated events, and the cross-island api registry.
|
|
9232
9306
|
* @see README.md
|
|
@@ -9234,8 +9308,8 @@ const COMPONENT_HOOK_NAMES = [
|
|
|
9234
9308
|
/** Error prefix for spa fail-fast failures (spec/11 Part-3). */
|
|
9235
9309
|
const ERROR_PREFIX$2 = "[web]";
|
|
9236
9310
|
/** The set of legal hook names, frozen for O(1) membership checks. */
|
|
9237
|
-
const HOOK_NAME_SET = new Set(
|
|
9238
|
-
/** The spec-only keys that select the plugin-mirror form of {@link
|
|
9311
|
+
const HOOK_NAME_SET = new Set(ISLAND_HOOK_NAMES);
|
|
9312
|
+
/** The spec-only keys that select the plugin-mirror form of {@link createIsland}. */
|
|
9239
9313
|
const SPEC_KEYS = new Set([
|
|
9240
9314
|
"state",
|
|
9241
9315
|
"render",
|
|
@@ -9274,7 +9348,7 @@ let renderChunk;
|
|
|
9274
9348
|
let commitVNodeFunction;
|
|
9275
9349
|
/**
|
|
9276
9350
|
* Load the lazy `./render` chunk (once) and cache its `commitVNode` for synchronous
|
|
9277
|
-
* use by later renders. Awaited by a
|
|
9351
|
+
* use by later renders. Awaited by a island's `mountPromise` so the test harness's
|
|
9278
9352
|
* `settle()` can deterministically flush a VNode render.
|
|
9279
9353
|
*
|
|
9280
9354
|
* @returns A promise that resolves once `commitVNode` is available.
|
|
@@ -9282,7 +9356,7 @@ let commitVNodeFunction;
|
|
|
9282
9356
|
* await loadRenderChunk();
|
|
9283
9357
|
*/
|
|
9284
9358
|
async function loadRenderChunk() {
|
|
9285
|
-
renderChunk ??= Promise.resolve().then(() => require("./render-
|
|
9359
|
+
renderChunk ??= Promise.resolve().then(() => require("./render-DHUcHCYs.cjs"));
|
|
9286
9360
|
commitVNodeFunction = (await renderChunk).commitVNode;
|
|
9287
9361
|
}
|
|
9288
9362
|
/**
|
|
@@ -9291,7 +9365,7 @@ async function loadRenderChunk() {
|
|
|
9291
9365
|
* a Preact `VNode` → committed through the lazy gate (loading it on demand if needed).
|
|
9292
9366
|
*
|
|
9293
9367
|
* @param host - The island host element to render into.
|
|
9294
|
-
* @param result - The value returned by the
|
|
9368
|
+
* @param result - The value returned by the island's `render`.
|
|
9295
9369
|
* @example
|
|
9296
9370
|
* commitResult(host, h(View, { items }));
|
|
9297
9371
|
*/
|
|
@@ -9313,7 +9387,7 @@ function commitResult(host, result) {
|
|
|
9313
9387
|
loadRenderChunk().then(() => commitVNodeFunction?.(vnode, host)).catch(() => {});
|
|
9314
9388
|
}
|
|
9315
9389
|
/**
|
|
9316
|
-
* Run a
|
|
9390
|
+
* Run a island's `render(state, ctx)` and commit the result now. Guards against
|
|
9317
9391
|
* synchronous re-entrancy (a render that calls `ctx.flush`) with a depth cap.
|
|
9318
9392
|
*
|
|
9319
9393
|
* @param instance - The instance to render.
|
|
@@ -9324,7 +9398,7 @@ function commitResult(host, result) {
|
|
|
9324
9398
|
function runRender(instance) {
|
|
9325
9399
|
const render = instance.def.spec?.render;
|
|
9326
9400
|
if (!render) return;
|
|
9327
|
-
if (instance.renderDepth > MAX_RENDER_DEPTH) throw new Error(`${ERROR_PREFIX$2}
|
|
9401
|
+
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)`);
|
|
9328
9402
|
instance.renderDepth += 1;
|
|
9329
9403
|
try {
|
|
9330
9404
|
commitResult(instance.el, render(instance.state ?? {}, instance.ctx));
|
|
@@ -9350,12 +9424,12 @@ function scheduleRender(instance) {
|
|
|
9350
9424
|
});
|
|
9351
9425
|
}
|
|
9352
9426
|
/**
|
|
9353
|
-
* Build the single per-instance {@link
|
|
9427
|
+
* Build the single per-instance {@link IslandContext} reused by every hook, event
|
|
9354
9428
|
* handler, and render. Route fields (`params`/`meta`/`locale`/`url`) and `data` read
|
|
9355
9429
|
* through the instance so a navigation update is reflected without rebuilding the ctx;
|
|
9356
|
-
* `state`/`set`/`flush`/`cleanup`/`
|
|
9430
|
+
* `state`/`set`/`flush`/`cleanup`/`island` are bound to the instance + plugin state.
|
|
9357
9431
|
*
|
|
9358
|
-
* @param state - The plugin state (for the cross-island `
|
|
9432
|
+
* @param state - The plugin state (for the cross-island `island` resolver).
|
|
9359
9433
|
* @param instance - The instance the context is bound to.
|
|
9360
9434
|
* @returns The instance-bound context.
|
|
9361
9435
|
* @example
|
|
@@ -9459,13 +9533,13 @@ function buildContext(state, instance) {
|
|
|
9459
9533
|
/**
|
|
9460
9534
|
* Resolve another island's registered api by name (`undefined` when absent).
|
|
9461
9535
|
*
|
|
9462
|
-
* @param name - The provider island's
|
|
9536
|
+
* @param name - The provider island's island name.
|
|
9463
9537
|
* @returns The provider's api, or `undefined`.
|
|
9464
9538
|
* @example
|
|
9465
|
-
* ctx.
|
|
9539
|
+
* ctx.island("lightbox");
|
|
9466
9540
|
*/
|
|
9467
|
-
|
|
9468
|
-
return state.
|
|
9541
|
+
island(name) {
|
|
9542
|
+
return state.islandApis.get(name);
|
|
9469
9543
|
}
|
|
9470
9544
|
};
|
|
9471
9545
|
}
|
|
@@ -9489,7 +9563,7 @@ function matchTarget(host, event, selector) {
|
|
|
9489
9563
|
return matched && host.contains(matched) ? matched : void 0;
|
|
9490
9564
|
}
|
|
9491
9565
|
/**
|
|
9492
|
-
* Attach a
|
|
9566
|
+
* Attach a island's declarative `events` map: one real listener per event TYPE on
|
|
9493
9567
|
* the host (dispatch walks `closest(selector)` for each registered selector), each
|
|
9494
9568
|
* removed via the instance's cleanup registry on destroy.
|
|
9495
9569
|
*
|
|
@@ -9506,7 +9580,7 @@ function attachEvents(instance, events) {
|
|
|
9506
9580
|
const space = key.indexOf(" ");
|
|
9507
9581
|
const type = (space === -1 ? key : key.slice(0, space)).trim();
|
|
9508
9582
|
const selector = space === -1 ? "" : key.slice(space + 1).trim();
|
|
9509
|
-
if (type === "") throw new Error(`${ERROR_PREFIX$2}
|
|
9583
|
+
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]")`);
|
|
9510
9584
|
const list = byType.get(type) ?? [];
|
|
9511
9585
|
list.push({
|
|
9512
9586
|
selector,
|
|
@@ -9529,62 +9603,62 @@ function attachEvents(instance, events) {
|
|
|
9529
9603
|
* Validate a single hook entry: its key must be a known hook name and its value
|
|
9530
9604
|
* must be a function. Throws fail-fast on the first violation.
|
|
9531
9605
|
*
|
|
9532
|
-
* @param
|
|
9606
|
+
* @param islandName - The owning island name (for error messages).
|
|
9533
9607
|
* @param source - The raw authoring object being validated.
|
|
9534
9608
|
* @param key - The hook key to validate.
|
|
9535
|
-
* @throws {Error} If `key` is not in `
|
|
9609
|
+
* @throws {Error} If `key` is not in `ISLAND_HOOK_NAMES`.
|
|
9536
9610
|
* @throws {TypeError} If the hook value is not a function.
|
|
9537
9611
|
* @example
|
|
9538
9612
|
* validateHookEntry("counter", source, "onMount");
|
|
9539
9613
|
*/
|
|
9540
|
-
function validateHookEntry(
|
|
9541
|
-
if (!HOOK_NAME_SET.has(key)) throw new Error(`${ERROR_PREFIX$2} unknown
|
|
9542
|
-
if (typeof source[key] !== "function") throw new TypeError(`${ERROR_PREFIX$2}
|
|
9614
|
+
function validateHookEntry(islandName, source, key) {
|
|
9615
|
+
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`);
|
|
9616
|
+
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`);
|
|
9543
9617
|
}
|
|
9544
9618
|
/**
|
|
9545
9619
|
* Validate the spec extras (`state`/`render`/`api` must be functions; `events` must be
|
|
9546
9620
|
* a plain object of functions). Throws fail-fast on the first violation.
|
|
9547
9621
|
*
|
|
9548
|
-
* @param
|
|
9622
|
+
* @param islandName - The owning island name (for error messages).
|
|
9549
9623
|
* @param extras - The partitioned spec extras to validate.
|
|
9550
9624
|
* @throws {TypeError} If a present extra has the wrong shape.
|
|
9551
9625
|
* @example
|
|
9552
9626
|
* validateSpecExtras("board", { state: () => ({}) });
|
|
9553
9627
|
*/
|
|
9554
|
-
function validateSpecExtras(
|
|
9628
|
+
function validateSpecExtras(islandName, extras) {
|
|
9555
9629
|
for (const key of [
|
|
9556
9630
|
"state",
|
|
9557
9631
|
"render",
|
|
9558
9632
|
"api"
|
|
9559
|
-
]) if (extras[key] !== void 0 && typeof extras[key] !== "function") throw new TypeError(`${ERROR_PREFIX$2}
|
|
9633
|
+
]) 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`);
|
|
9560
9634
|
if (extras.events !== void 0) {
|
|
9561
9635
|
const events = extras.events;
|
|
9562
|
-
if (!(typeof events === "object")) throw new TypeError(`${ERROR_PREFIX$2}
|
|
9563
|
-
for (const [key, handler] of Object.entries(events)) if (typeof handler !== "function") throw new TypeError(`${ERROR_PREFIX$2}
|
|
9636
|
+
if (!(typeof events === "object")) throw new TypeError(`${ERROR_PREFIX$2} island "events" on "${islandName}" must be an object of handlers`);
|
|
9637
|
+
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`);
|
|
9564
9638
|
}
|
|
9565
9639
|
}
|
|
9566
9640
|
/**
|
|
9567
|
-
* Create a validated
|
|
9568
|
-
* (`
|
|
9569
|
-
* (`
|
|
9641
|
+
* Create a validated island definition. Accepts either the legacy hooks-only form
|
|
9642
|
+
* (`createIsland("counter", { onMount() {} })`) or the plugin-mirror spec form
|
|
9643
|
+
* (`createIsland("board", { state, render, events, api, ...hooks })`). Spec-only
|
|
9570
9644
|
* keys (`state`/`render`/`events`/`api`) are partitioned out before hook-name
|
|
9571
9645
|
* validation, so a real typo (e.g. `onMout`) still throws immediately while the spec
|
|
9572
9646
|
* keys are accepted.
|
|
9573
9647
|
*
|
|
9574
|
-
* @param name - Unique
|
|
9648
|
+
* @param name - Unique island name.
|
|
9575
9649
|
* @param spec - Lifecycle hooks, or the `{ state, render, events, api, ...hooks }` spec.
|
|
9576
|
-
* @returns A `
|
|
9650
|
+
* @returns A `IslandDef` ready to `register`.
|
|
9577
9651
|
* @throws {Error} If `name` is empty, a hook key is unknown, or an extra/hook value has the wrong shape.
|
|
9578
9652
|
* @example
|
|
9579
|
-
* const counter =
|
|
9653
|
+
* const counter = createIsland("counter", { onMount({ el }) { el.textContent = "0"; } });
|
|
9580
9654
|
* @example
|
|
9581
|
-
* const list =
|
|
9655
|
+
* const list = createIsland<{ items: string[] }>("list", {
|
|
9582
9656
|
* state: () => ({ items: [] }),
|
|
9583
9657
|
* render: (s) => h(List, { items: s.items })
|
|
9584
9658
|
* });
|
|
9585
9659
|
*/
|
|
9586
|
-
function
|
|
9587
|
-
if (name.trim() === "") throw new Error(`${ERROR_PREFIX$2}
|
|
9660
|
+
function createIsland(name, spec) {
|
|
9661
|
+
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)`);
|
|
9588
9662
|
const source = spec;
|
|
9589
9663
|
const hooks = {};
|
|
9590
9664
|
const extras = {};
|
|
@@ -9661,30 +9735,30 @@ function disposeInstance(state, instance) {
|
|
|
9661
9735
|
} catch {}
|
|
9662
9736
|
instance.cleanups.length = 0;
|
|
9663
9737
|
instance.renderScheduled = false;
|
|
9664
|
-
if (instance.api !== void 0 && state.
|
|
9738
|
+
if (instance.api !== void 0 && state.islandApis.get(instance.def.name) === instance.api) state.islandApis.delete(instance.def.name);
|
|
9665
9739
|
}
|
|
9666
9740
|
/**
|
|
9667
|
-
* Mounts a single `data-
|
|
9741
|
+
* Mounts a single `data-island` element: classifies persistent vs page-specific,
|
|
9668
9742
|
* builds the instance + its bound context, initializes per-instance `state`, registers
|
|
9669
9743
|
* its `api`, attaches declarative `events`, fires `onCreate` then `onMount` (capturing
|
|
9670
9744
|
* an async `onMount` + render-chunk load as `mountPromise`), schedules the initial
|
|
9671
|
-
* render, records it, and emits `spa:
|
|
9672
|
-
* mounted, has no
|
|
9745
|
+
* render, records it, and emits `spa:island-mount`. No-ops if the element is already
|
|
9746
|
+
* mounted, has no island name, or names an unregistered island.
|
|
9673
9747
|
*
|
|
9674
|
-
* @param state - The plugin state (
|
|
9675
|
-
* @param emit - The event emitter for spa:
|
|
9748
|
+
* @param state - The plugin state (registeredIslands + instances + islandApis).
|
|
9749
|
+
* @param emit - The event emitter for spa:island-mount.
|
|
9676
9750
|
* @param swapArea - The swap-region element, or null when none was found.
|
|
9677
9751
|
* @param data - The current page data payload.
|
|
9678
|
-
* @param element - The candidate element carrying a `data-
|
|
9752
|
+
* @param element - The candidate element carrying a `data-island` attribute.
|
|
9679
9753
|
* @param route - The matched-route slice for the current URL (params/meta/locale/url).
|
|
9680
9754
|
* @example
|
|
9681
9755
|
* mountElement(state, emit, swapArea, data, element, route);
|
|
9682
9756
|
*/
|
|
9683
9757
|
function mountElement(state, emit, swapArea, data, element, route = EMPTY_ROUTE) {
|
|
9684
9758
|
if (state.instances.has(element)) return;
|
|
9685
|
-
const name = element.dataset.
|
|
9759
|
+
const name = element.dataset.island;
|
|
9686
9760
|
if (!name) return;
|
|
9687
|
-
const definition = state.
|
|
9761
|
+
const definition = state.registeredIslands.get(name);
|
|
9688
9762
|
if (!definition) return;
|
|
9689
9763
|
const instance = {
|
|
9690
9764
|
def: definition,
|
|
@@ -9710,7 +9784,7 @@ function mountElement(state, emit, swapArea, data, element, route = EMPTY_ROUTE)
|
|
|
9710
9784
|
if (spec?.state) instance.state = spec.state(instance.ctx);
|
|
9711
9785
|
if (spec?.api) {
|
|
9712
9786
|
instance.api = spec.api(instance.ctx);
|
|
9713
|
-
state.
|
|
9787
|
+
state.islandApis.set(definition.name, instance.api);
|
|
9714
9788
|
}
|
|
9715
9789
|
if (spec?.events) attachEvents(instance, spec.events);
|
|
9716
9790
|
runHook(instance, "onCreate");
|
|
@@ -9721,20 +9795,20 @@ function mountElement(state, emit, swapArea, data, element, route = EMPTY_ROUTE)
|
|
|
9721
9795
|
if (onMountResult && typeof onMountResult.then === "function") pending.push(onMountResult);
|
|
9722
9796
|
instance.mountPromise = pending.length > 0 ? Promise.all(pending).then(() => {}) : void 0;
|
|
9723
9797
|
state.instances.set(element, instance);
|
|
9724
|
-
emit("spa:
|
|
9798
|
+
emit("spa:island-mount", {
|
|
9725
9799
|
name: definition.name,
|
|
9726
9800
|
el: element
|
|
9727
9801
|
});
|
|
9728
9802
|
}
|
|
9729
9803
|
/**
|
|
9730
|
-
* Scans the swap region, mounts
|
|
9804
|
+
* Scans the swap region, mounts islands for matching `data-island` elements,
|
|
9731
9805
|
* classifies persistent (outside swap area) vs page-specific (inside), runs
|
|
9732
|
-
* `onCreate`/`onMount` + initial render, and emits `spa:
|
|
9806
|
+
* `onCreate`/`onMount` + initial render, and emits `spa:island-mount` per instance.
|
|
9733
9807
|
* Already-mounted elements are skipped.
|
|
9734
9808
|
*
|
|
9735
|
-
* @param state - The plugin state (
|
|
9736
|
-
* @param emit - The event emitter for spa:
|
|
9737
|
-
* @param swapSelector - CSS selector bounding page-specific
|
|
9809
|
+
* @param state - The plugin state (registeredIslands + instances + islandApis).
|
|
9810
|
+
* @param emit - The event emitter for spa:island-mount.
|
|
9811
|
+
* @param swapSelector - CSS selector bounding page-specific islands.
|
|
9738
9812
|
* @param route - The matched-route slice for the current URL (params/meta/locale/url).
|
|
9739
9813
|
* @example
|
|
9740
9814
|
* scanAndMount(state, emit, "main > section", route);
|
|
@@ -9743,16 +9817,16 @@ function scanAndMount(state, emit, swapSelector, route = EMPTY_ROUTE) {
|
|
|
9743
9817
|
if (typeof document === "undefined") return;
|
|
9744
9818
|
const swapArea = document.querySelector(swapSelector);
|
|
9745
9819
|
const data = extractPageData(document);
|
|
9746
|
-
for (const element of document.querySelectorAll("[data-
|
|
9820
|
+
for (const element of document.querySelectorAll("[data-island]")) mountElement(state, emit, swapArea, data, element, route);
|
|
9747
9821
|
}
|
|
9748
9822
|
/**
|
|
9749
9823
|
* Unmounts page-specific instances inside the swap region (runs `onUnMount` then
|
|
9750
9824
|
* `onDestroy`, then their cleanup disposers + api unregister), removes them from state,
|
|
9751
|
-
* and emits `spa:
|
|
9825
|
+
* and emits `spa:island-unmount`. Persistent instances (outside the swap area) are
|
|
9752
9826
|
* left in place.
|
|
9753
9827
|
*
|
|
9754
9828
|
* @param state - The plugin state holding live instances.
|
|
9755
|
-
* @param emit - The event emitter for spa:
|
|
9829
|
+
* @param emit - The event emitter for spa:island-unmount.
|
|
9756
9830
|
* @example
|
|
9757
9831
|
* unmountPageSpecific(state, emit);
|
|
9758
9832
|
*/
|
|
@@ -9765,7 +9839,7 @@ function unmountPageSpecific(state, emit) {
|
|
|
9765
9839
|
runHook(instance, "onDestroy");
|
|
9766
9840
|
disposeInstance(state, instance);
|
|
9767
9841
|
state.instances.delete(element);
|
|
9768
|
-
emit("spa:
|
|
9842
|
+
emit("spa:island-unmount", {
|
|
9769
9843
|
name: instance.def.name,
|
|
9770
9844
|
el: element
|
|
9771
9845
|
});
|
|
@@ -9774,11 +9848,11 @@ function unmountPageSpecific(state, emit) {
|
|
|
9774
9848
|
/**
|
|
9775
9849
|
* Disposes ALL live instances (persistent and page-specific) on teardown: runs
|
|
9776
9850
|
* `onUnMount` then `onDestroy`, then their cleanup disposers + api unregister, emits
|
|
9777
|
-
* `spa:
|
|
9851
|
+
* `spa:island-unmount`, and clears the instance + api maps. Used by the kernel's
|
|
9778
9852
|
* `dispose` on plugin stop.
|
|
9779
9853
|
*
|
|
9780
9854
|
* @param state - The plugin state holding live instances.
|
|
9781
|
-
* @param emit - The event emitter for spa:
|
|
9855
|
+
* @param emit - The event emitter for spa:island-unmount.
|
|
9782
9856
|
* @example
|
|
9783
9857
|
* unmountAll(state, emit);
|
|
9784
9858
|
*/
|
|
@@ -9789,13 +9863,13 @@ function unmountAll(state, emit) {
|
|
|
9789
9863
|
runHook(instance, "onUnMount");
|
|
9790
9864
|
runHook(instance, "onDestroy");
|
|
9791
9865
|
disposeInstance(state, instance);
|
|
9792
|
-
emit("spa:
|
|
9866
|
+
emit("spa:island-unmount", {
|
|
9793
9867
|
name: instance.def.name,
|
|
9794
9868
|
el: element
|
|
9795
9869
|
});
|
|
9796
9870
|
}
|
|
9797
9871
|
state.instances.clear();
|
|
9798
|
-
state.
|
|
9872
|
+
state.islandApis.clear();
|
|
9799
9873
|
}
|
|
9800
9874
|
/**
|
|
9801
9875
|
* Fires `onNavStart` on every currently-mounted instance (persistent instances
|
|
@@ -9832,80 +9906,6 @@ function notifyNavEnd(state, route = EMPTY_ROUTE) {
|
|
|
9832
9906
|
}
|
|
9833
9907
|
}
|
|
9834
9908
|
//#endregion
|
|
9835
|
-
//#region src/plugins/spa/head.ts
|
|
9836
|
-
/** Single-element head selectors synced by replace/append/remove on navigation. */
|
|
9837
|
-
const META_SELECTORS = [
|
|
9838
|
-
"meta[name=\"description\"]",
|
|
9839
|
-
"meta[property=\"og:title\"]",
|
|
9840
|
-
"meta[property=\"og:description\"]",
|
|
9841
|
-
"meta[property=\"og:url\"]",
|
|
9842
|
-
"meta[property=\"og:image\"]",
|
|
9843
|
-
"meta[property=\"og:type\"]",
|
|
9844
|
-
"meta[property=\"og:locale\"]",
|
|
9845
|
-
"meta[name=\"twitter:card\"]",
|
|
9846
|
-
"meta[name=\"twitter:title\"]",
|
|
9847
|
-
"meta[name=\"twitter:description\"]",
|
|
9848
|
-
"meta[name=\"twitter:image\"]",
|
|
9849
|
-
"meta[name=\"twitter:site\"]",
|
|
9850
|
-
"link[rel=\"canonical\"]"
|
|
9851
|
-
];
|
|
9852
|
-
/** Head element groups fully replaced (remove-all-then-clone) on navigation. */
|
|
9853
|
-
const REPLACE_ALL_SELECTORS = [
|
|
9854
|
-
"script[type=\"application/ld+json\"]",
|
|
9855
|
-
"link[rel=\"alternate\"][hreflang]",
|
|
9856
|
-
"meta[property^=\"article:\"]"
|
|
9857
|
-
];
|
|
9858
|
-
/**
|
|
9859
|
-
* Sync a single head element by selector between the fetched and live document:
|
|
9860
|
-
* replace when both exist, append when only the new doc has it, remove when only
|
|
9861
|
-
* the live doc has it.
|
|
9862
|
-
*
|
|
9863
|
-
* @param selector - CSS selector for the head element to sync.
|
|
9864
|
-
* @param doc - The fetched document (DOMParser-parsed).
|
|
9865
|
-
* @example
|
|
9866
|
-
* syncElement('link[rel="canonical"]', doc);
|
|
9867
|
-
*/
|
|
9868
|
-
function syncElement(selector, doc) {
|
|
9869
|
-
const newElement = doc.querySelector(selector);
|
|
9870
|
-
const oldElement = document.querySelector(selector);
|
|
9871
|
-
if (newElement && oldElement) oldElement.replaceWith(newElement.cloneNode(true));
|
|
9872
|
-
else if (newElement) document.head.append(newElement.cloneNode(true));
|
|
9873
|
-
else if (oldElement) oldElement.remove();
|
|
9874
|
-
}
|
|
9875
|
-
/**
|
|
9876
|
-
* Remove all live matches for a selector and re-clone the fetched document's
|
|
9877
|
-
* matches into the live `<head>`.
|
|
9878
|
-
*
|
|
9879
|
-
* @param selector - CSS selector for the element group to replace wholesale.
|
|
9880
|
-
* @param doc - The fetched document (DOMParser-parsed).
|
|
9881
|
-
* @example
|
|
9882
|
-
* replaceAllBySelector('script[type="application/ld+json"]', doc);
|
|
9883
|
-
*/
|
|
9884
|
-
function replaceAllBySelector(selector, doc) {
|
|
9885
|
-
for (const element of document.querySelectorAll(selector)) element.remove();
|
|
9886
|
-
for (const element of doc.querySelectorAll(selector)) document.head.append(element.cloneNode(true));
|
|
9887
|
-
}
|
|
9888
|
-
/**
|
|
9889
|
-
* Syncs the live document `<head>` after a navigation from the fetched document
|
|
9890
|
-
* (whose head was composed by the `head` plugin). Recomputes
|
|
9891
|
-
* title/meta/canonical/JSON-LD/hreflang/`<html lang>` once and applies them.
|
|
9892
|
-
* The `head` API is accepted to bind the structural dependency (spec/09 deps).
|
|
9893
|
-
*
|
|
9894
|
-
* @param _head - The head plugin API (dependency binding; composition reused via the fetched doc).
|
|
9895
|
-
* @param doc - The fetched document parsed from the navigated page's HTML.
|
|
9896
|
-
* @example
|
|
9897
|
-
* syncHead(headApi, parsedDoc);
|
|
9898
|
-
*/
|
|
9899
|
-
function syncHead(_head, doc) {
|
|
9900
|
-
if (typeof document === "undefined") return;
|
|
9901
|
-
const newTitle = doc.querySelector("title")?.textContent;
|
|
9902
|
-
if (newTitle) document.title = newTitle;
|
|
9903
|
-
const newLang = doc.documentElement.lang;
|
|
9904
|
-
if (newLang) document.documentElement.lang = newLang;
|
|
9905
|
-
for (const selector of META_SELECTORS) syncElement(selector, doc);
|
|
9906
|
-
for (const selector of REPLACE_ALL_SELECTORS) replaceAllBySelector(selector, doc);
|
|
9907
|
-
}
|
|
9908
|
-
//#endregion
|
|
9909
9909
|
//#region src/plugins/spa/progress.ts
|
|
9910
9910
|
/** Delay before the bar appears, so fast navigations show no indicator. */
|
|
9911
9911
|
const START_DELAY_MS = 150;
|
|
@@ -10312,7 +10312,7 @@ const defaultSpaConfig = {
|
|
|
10312
10312
|
swapSelector: "main > section",
|
|
10313
10313
|
viewTransitions: false,
|
|
10314
10314
|
progressBar: true,
|
|
10315
|
-
|
|
10315
|
+
islands: []
|
|
10316
10316
|
};
|
|
10317
10317
|
/**
|
|
10318
10318
|
* Whether a selector is syntactically valid (parseable by the DOM). Falls back
|
|
@@ -10334,8 +10334,8 @@ function isValidSelector(selector) {
|
|
|
10334
10334
|
}
|
|
10335
10335
|
/**
|
|
10336
10336
|
* Validates the spa config and applies defaults (Part-3 errors on an empty or
|
|
10337
|
-
* syntactically-invalid `swapSelector`).
|
|
10338
|
-
* `
|
|
10337
|
+
* syntactically-invalid `swapSelector`). Island-hook validation runs later in
|
|
10338
|
+
* `createIsland` when the islands are registered.
|
|
10339
10339
|
*
|
|
10340
10340
|
* @param config - The raw spa config to validate.
|
|
10341
10341
|
* @returns The fully-resolved config with defaults applied.
|
|
@@ -10351,7 +10351,7 @@ function resolveSpaConfig(config) {
|
|
|
10351
10351
|
swapSelector,
|
|
10352
10352
|
viewTransitions: config.viewTransitions ?? false,
|
|
10353
10353
|
progressBar: config.progressBar ?? true,
|
|
10354
|
-
|
|
10354
|
+
islands: config.islands ?? []
|
|
10355
10355
|
};
|
|
10356
10356
|
}
|
|
10357
10357
|
/**
|
|
@@ -10368,9 +10368,9 @@ function resolveSpaConfig(config) {
|
|
|
10368
10368
|
*/
|
|
10369
10369
|
function createState(_ctx) {
|
|
10370
10370
|
return {
|
|
10371
|
-
|
|
10371
|
+
registeredIslands: /* @__PURE__ */ new Map(),
|
|
10372
10372
|
instances: /* @__PURE__ */ new Map(),
|
|
10373
|
-
|
|
10373
|
+
islandApis: /* @__PURE__ */ new Map(),
|
|
10374
10374
|
currentUrl: "",
|
|
10375
10375
|
destroyRouter: null,
|
|
10376
10376
|
started: false,
|
|
@@ -10390,15 +10390,15 @@ function createState(_ctx) {
|
|
|
10390
10390
|
/** Error prefix for spa kernel failures (spec/11 Part-3). */
|
|
10391
10391
|
const ERROR_PREFIX = "[web]";
|
|
10392
10392
|
/**
|
|
10393
|
-
* Registers a
|
|
10393
|
+
* Registers a island definition into state (last-registered-wins).
|
|
10394
10394
|
*
|
|
10395
|
-
* @param state - The plugin state holding
|
|
10396
|
-
* @param
|
|
10395
|
+
* @param state - The plugin state holding registeredIslands.
|
|
10396
|
+
* @param island - The island definition to register.
|
|
10397
10397
|
* @example
|
|
10398
|
-
*
|
|
10398
|
+
* registerIsland(state, counter);
|
|
10399
10399
|
*/
|
|
10400
|
-
function
|
|
10401
|
-
state.
|
|
10400
|
+
function registerIsland(state, island) {
|
|
10401
|
+
state.registeredIslands.set(island.name, island);
|
|
10402
10402
|
}
|
|
10403
10403
|
/**
|
|
10404
10404
|
* Resolve the current document URL (pathname + search), or `""` when headless.
|
|
@@ -10473,15 +10473,15 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10473
10473
|
});
|
|
10474
10474
|
};
|
|
10475
10475
|
/**
|
|
10476
|
-
* Build the matched-route slice (params/meta/locale/url) for the
|
|
10476
|
+
* Build the matched-route slice (params/meta/locale/url) for the island context at `path`,
|
|
10477
10477
|
* so islands read their route's params/meta directly. An unmatched path yields an empty slice.
|
|
10478
10478
|
*
|
|
10479
10479
|
* @param path - The URL (pathname + search) to match.
|
|
10480
10480
|
* @returns The route slice for the matched route.
|
|
10481
10481
|
* @example
|
|
10482
|
-
* scanAndMount(state, emit, resolved.swapSelector,
|
|
10482
|
+
* scanAndMount(state, emit, resolved.swapSelector, islandRouteContext(pathname));
|
|
10483
10483
|
*/
|
|
10484
|
-
const
|
|
10484
|
+
const islandRouteContext = (path) => {
|
|
10485
10485
|
const matchPath = path.split("?")[0] ?? path;
|
|
10486
10486
|
const hit = deps.router.match(matchPath);
|
|
10487
10487
|
const locale = hit?.params.lang ?? (typeof document === "undefined" ? "" : document.documentElement.lang) ?? "";
|
|
@@ -10510,7 +10510,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10510
10510
|
syncHead(deps.head, doc);
|
|
10511
10511
|
unmountPageSpecific(state, emit);
|
|
10512
10512
|
if (!swapRegion(doc, resolved.swapSelector, resolved.viewTransitions, () => {
|
|
10513
|
-
const routeSlice =
|
|
10513
|
+
const routeSlice = islandRouteContext(pathname);
|
|
10514
10514
|
scanAndMount(state, emit, resolved.swapSelector, routeSlice);
|
|
10515
10515
|
notifyNavEnd(state, routeSlice);
|
|
10516
10516
|
}, applyPendingScroll)) {
|
|
@@ -10523,7 +10523,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10523
10523
|
emit("spa:navigated", { url: pathname });
|
|
10524
10524
|
};
|
|
10525
10525
|
/**
|
|
10526
|
-
* Begin a navigation: start progress, notify
|
|
10526
|
+
* Begin a navigation: start progress, notify islands, emit navigate.
|
|
10527
10527
|
*
|
|
10528
10528
|
* @param pathname - The destination pathname.
|
|
10529
10529
|
* @example
|
|
@@ -10609,11 +10609,11 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10609
10609
|
const commitDataRender = async (pathname, resolvedRender, signal) => {
|
|
10610
10610
|
if (signal?.aborted) return;
|
|
10611
10611
|
const { route, vnode, routeContext, region } = resolvedRender;
|
|
10612
|
-
const { renderVNode } = await Promise.resolve().then(() => require("./render-
|
|
10612
|
+
const { renderVNode } = await Promise.resolve().then(() => require("./render-DHUcHCYs.cjs"));
|
|
10613
10613
|
if (signal?.aborted) return;
|
|
10614
10614
|
syncDataHead(deps.head, route, routeContext);
|
|
10615
10615
|
unmountPageSpecific(state, emit);
|
|
10616
|
-
const routeSlice =
|
|
10616
|
+
const routeSlice = islandRouteContext(pathname);
|
|
10617
10617
|
/**
|
|
10618
10618
|
* Render the VNode into the region and re-mount its islands in one paint — the
|
|
10619
10619
|
* swap body handed to `runSwap` (optionally wrapped in a View Transition).
|
|
@@ -10671,14 +10671,14 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10671
10671
|
* await bootRender("/b/abc123");
|
|
10672
10672
|
*/
|
|
10673
10673
|
const bootRender = async (pathname) => {
|
|
10674
|
-
const routeSlice =
|
|
10674
|
+
const routeSlice = islandRouteContext(pathname);
|
|
10675
10675
|
const resolvedRender = await resolveDataRender(pathname);
|
|
10676
10676
|
if (resolvedRender === false) {
|
|
10677
10677
|
scanAndMount(state, emit, resolved.swapSelector, routeSlice);
|
|
10678
10678
|
return;
|
|
10679
10679
|
}
|
|
10680
10680
|
const { vnode, region } = resolvedRender;
|
|
10681
|
-
const { renderVNode } = await Promise.resolve().then(() => require("./render-
|
|
10681
|
+
const { renderVNode } = await Promise.resolve().then(() => require("./render-DHUcHCYs.cjs"));
|
|
10682
10682
|
renderVNode(vnode, region);
|
|
10683
10683
|
scanAndMount(state, emit, resolved.swapSelector, routeSlice);
|
|
10684
10684
|
};
|
|
@@ -10706,13 +10706,13 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10706
10706
|
};
|
|
10707
10707
|
return {
|
|
10708
10708
|
/**
|
|
10709
|
-
* Register config
|
|
10709
|
+
* Register config islands and seed currentUrl from the document.
|
|
10710
10710
|
*
|
|
10711
10711
|
* @example
|
|
10712
10712
|
* kernel.init();
|
|
10713
10713
|
*/
|
|
10714
10714
|
init() {
|
|
10715
|
-
for (const
|
|
10715
|
+
for (const island of resolved.islands) registerIsland(state, island);
|
|
10716
10716
|
state.currentUrl = currentLocationUrl();
|
|
10717
10717
|
},
|
|
10718
10718
|
/**
|
|
@@ -10730,18 +10730,18 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10730
10730
|
const matchPath = state.currentUrl.split("?")[0] ?? state.currentUrl;
|
|
10731
10731
|
const hit = deps.router.match(matchPath);
|
|
10732
10732
|
if (hit?.route._handlers.render && isClientOnlyRoute(deps.router.mode(), hit.route)) bootRender(state.currentUrl);
|
|
10733
|
-
else scanAndMount(state, emit, resolved.swapSelector,
|
|
10733
|
+
else scanAndMount(state, emit, resolved.swapSelector, islandRouteContext(state.currentUrl));
|
|
10734
10734
|
state.started = true;
|
|
10735
10735
|
},
|
|
10736
10736
|
/**
|
|
10737
|
-
* Register a
|
|
10737
|
+
* Register a island definition (last-registered-wins).
|
|
10738
10738
|
*
|
|
10739
|
-
* @param
|
|
10739
|
+
* @param island - The island definition to register.
|
|
10740
10740
|
* @example
|
|
10741
10741
|
* kernel.register(counter);
|
|
10742
10742
|
*/
|
|
10743
|
-
register(
|
|
10744
|
-
|
|
10743
|
+
register(island) {
|
|
10744
|
+
registerIsland(state, island);
|
|
10745
10745
|
},
|
|
10746
10746
|
/**
|
|
10747
10747
|
* Process a navigation to `path` (fetch then swap; full reload on error).
|
|
@@ -10755,13 +10755,13 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10755
10755
|
navigate(path).catch(() => {});
|
|
10756
10756
|
},
|
|
10757
10757
|
/**
|
|
10758
|
-
* Scan the swap region and mount
|
|
10758
|
+
* Scan the swap region and mount islands for matching elements.
|
|
10759
10759
|
*
|
|
10760
10760
|
* @example
|
|
10761
10761
|
* kernel.scan();
|
|
10762
10762
|
*/
|
|
10763
10763
|
scan() {
|
|
10764
|
-
scanAndMount(state, emit, resolved.swapSelector,
|
|
10764
|
+
scanAndMount(state, emit, resolved.swapSelector, islandRouteContext(state.currentUrl));
|
|
10765
10765
|
},
|
|
10766
10766
|
/**
|
|
10767
10767
|
* Tear down router listeners, dispose all instances, reset boot state.
|
|
@@ -10780,7 +10780,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10780
10780
|
}
|
|
10781
10781
|
/**
|
|
10782
10782
|
* Builds the shared kernel from the plugin context, stores it on `ctx.state`,
|
|
10783
|
-
* and runs its init step (validate config, register config.
|
|
10783
|
+
* and runs its init step (validate config, register config.islands, seed
|
|
10784
10784
|
* currentUrl). Captures the OPTIONAL `data` reader when the `data` plugin is
|
|
10785
10785
|
* composed (enabling client DATA navigation) — resolved by instance via
|
|
10786
10786
|
* `ctx.require(dataPlugin)`, guarded by `ctx.has("data")` so `data` stays optional
|
|
@@ -10848,10 +10848,10 @@ function disposeSpa() {
|
|
|
10848
10848
|
/**
|
|
10849
10849
|
* @file `lazyEmbed` island — activates the static embed facades emitted by the
|
|
10850
10850
|
* content pipeline's `::embed` directive (pipeline/embed.ts). Mounts on every
|
|
10851
|
-
* `[data-
|
|
10851
|
+
* `[data-island="lazy-embed"]` figure; a click on the facade's button swaps
|
|
10852
10852
|
* it for the real `<iframe loading="lazy">`. Until that click the embedded
|
|
10853
10853
|
* document costs the page nothing — no request, no third-party JS, no
|
|
10854
|
-
* scroll-jacking. Register it in `pluginConfigs.spa.
|
|
10854
|
+
* scroll-jacking. Register it in `pluginConfigs.spa.islands`; all visual
|
|
10855
10855
|
* chrome (`.lazy-embed*` classes) is consumer CSS.
|
|
10856
10856
|
*/
|
|
10857
10857
|
/** CSS class on the injected `<iframe>` (consumer CSS sizes it). */
|
|
@@ -10904,7 +10904,7 @@ function onFacadeClick(event) {
|
|
|
10904
10904
|
* Lazy-embed island: facade button click → real `<iframe loading="lazy">`.
|
|
10905
10905
|
* The companion of the content pipeline's `::embed` directive.
|
|
10906
10906
|
*/
|
|
10907
|
-
const lazyEmbed =
|
|
10907
|
+
const lazyEmbed = createIsland("lazy-embed", {
|
|
10908
10908
|
/**
|
|
10909
10909
|
* Bind the activation click handler when a facade mounts.
|
|
10910
10910
|
*
|
|
@@ -10930,18 +10930,18 @@ const lazyEmbed = createComponent("lazy-embed", {
|
|
|
10930
10930
|
//#region src/plugins/spa/index.ts
|
|
10931
10931
|
/**
|
|
10932
10932
|
* @file spa — Complex Plugin (WIRING ONLY, ≤30 lines). All logic lives in the
|
|
10933
|
-
* domain files (kernel/router/head/progress/
|
|
10933
|
+
* domain files (kernel/router/head/progress/islands/lifecycle); index wires.
|
|
10934
10934
|
*
|
|
10935
10935
|
* Depends: router, head.
|
|
10936
|
-
* Emits: spa:navigate, spa:navigated, spa:
|
|
10936
|
+
* Emits: spa:navigate, spa:navigated, spa:island-mount, spa:island-unmount.
|
|
10937
10937
|
* @see README.md
|
|
10938
10938
|
*/
|
|
10939
10939
|
/**
|
|
10940
10940
|
* SPA plugin — progressive client-side navigation layered over the static site:
|
|
10941
10941
|
* swaps a page region on navigation, with an optional progress bar and View
|
|
10942
|
-
* Transitions. Register interactive islands with {@link
|
|
10943
|
-
* on router and head; emits `spa:navigate`, `spa:navigated`, `spa:
|
|
10944
|
-
* and `spa:
|
|
10942
|
+
* Transitions. Register interactive islands with {@link createIsland}. Depends
|
|
10943
|
+
* on router and head; emits `spa:navigate`, `spa:navigated`, `spa:island-mount`,
|
|
10944
|
+
* and `spa:island-unmount`.
|
|
10945
10945
|
*
|
|
10946
10946
|
* @example Enable view transitions and a custom swap region
|
|
10947
10947
|
* ```ts
|
|
@@ -11924,8 +11924,8 @@ function EmbedFacadeButton(props) {
|
|
|
11924
11924
|
//#region src/plugins/content/pipeline/embed.ts
|
|
11925
11925
|
/** CSS class on the `<figure>` facade wrapping each embed. */
|
|
11926
11926
|
const EMBED_FIGURE_CLASS = "lazy-embed";
|
|
11927
|
-
/** `data-
|
|
11928
|
-
const
|
|
11927
|
+
/** `data-island` name binding the facade to the `lazyEmbed` SPA island. */
|
|
11928
|
+
const EMBED_ISLAND_NAME = "lazy-embed";
|
|
11929
11929
|
/**
|
|
11930
11930
|
* Type guard for an `::embed` leaf directive.
|
|
11931
11931
|
*
|
|
@@ -12035,7 +12035,7 @@ function collectAttributes$1(attributes) {
|
|
|
12035
12035
|
* ```
|
|
12036
12036
|
*/
|
|
12037
12037
|
function embedFacadeHtml(facade, props, dimensions) {
|
|
12038
|
-
return `<figure class="${EMBED_FIGURE_CLASS}" data-
|
|
12038
|
+
return `<figure class="${EMBED_FIGURE_CLASS}" data-island="${EMBED_ISLAND_NAME}" data-embed-src="${escapeAttribute(props.src)}" data-embed-title="${escapeAttribute(props.title)}"${dimensions ? ` data-embed-width="${dimensions.width}" data-embed-height="${dimensions.height}" style="aspect-ratio: ${dimensions.width} / ${dimensions.height}; max-width: ${dimensions.width}px;"` : ""}>${(0, preact_render_to_string.renderToString)((0, preact.h)(facade, props))}</figure>`;
|
|
12039
12039
|
}
|
|
12040
12040
|
/**
|
|
12041
12041
|
* Normalize the provider's `embed` config value (`boolean | options`) to a plain
|
|
@@ -12148,7 +12148,7 @@ function GalleryTrack(props) {
|
|
|
12148
12148
|
*
|
|
12149
12149
|
* Rewrites `::gallery{src="./images/dir/" caption="…"}` leaf directives into a
|
|
12150
12150
|
* static swipeable image set at the mdast stage (BEFORE the remark-rehype bridge):
|
|
12151
|
-
* a framework-owned `<div class="gallery" data-
|
|
12151
|
+
* a framework-owned `<div class="gallery" data-island="gallery">` carrying the
|
|
12152
12152
|
* island hook, wrapping inner content rendered (at build time, to static markup)
|
|
12153
12153
|
* by a Preact component — the built-in {@link GalleryTrack} by default, or a
|
|
12154
12154
|
* consumer component via `gallery.component`.
|
|
@@ -12160,12 +12160,12 @@ function GalleryTrack(props) {
|
|
|
12160
12160
|
* transform reads `<contentDir>/<slug>/<src>` from disk, sorts its images, and
|
|
12161
12161
|
* resolves each to its shared `/<slug>/<dir>/<file>` URL (identical from every
|
|
12162
12162
|
* locale page, mirroring co-located images). The companion gallery SPA island
|
|
12163
|
-
* (consumer-provided) wires swipe/keyboard/lightbox on `[data-
|
|
12163
|
+
* (consumer-provided) wires swipe/keyboard/lightbox on `[data-island="gallery"]`.
|
|
12164
12164
|
*/
|
|
12165
12165
|
/** CSS class on the `<div>` wrapping each gallery. */
|
|
12166
12166
|
const GALLERY_WRAPPER_CLASS = "gallery";
|
|
12167
|
-
/** `data-
|
|
12168
|
-
const
|
|
12167
|
+
/** `data-island` name binding the gallery to its SPA island. */
|
|
12168
|
+
const GALLERY_ISLAND_NAME = "gallery";
|
|
12169
12169
|
/** Image file extensions a gallery folder expands over. */
|
|
12170
12170
|
const IMAGE_EXTENSIONS = new Set([
|
|
12171
12171
|
".webp",
|
|
@@ -12258,7 +12258,7 @@ function collectAttributes(attributes) {
|
|
|
12258
12258
|
}
|
|
12259
12259
|
/**
|
|
12260
12260
|
* Build the static gallery HTML for one directive: the framework-owned `<div>`
|
|
12261
|
-
* (island hook in `data-
|
|
12261
|
+
* (island hook in `data-island`) wrapping the component's inner content, SSR'd
|
|
12262
12262
|
* to static markup.
|
|
12263
12263
|
*
|
|
12264
12264
|
* @param component - The gallery component (default {@link GalleryTrack}).
|
|
@@ -12272,7 +12272,7 @@ function collectAttributes(attributes) {
|
|
|
12272
12272
|
* ```
|
|
12273
12273
|
*/
|
|
12274
12274
|
function galleryHtml(component, slides, caption, attributes) {
|
|
12275
|
-
return `<div class="${GALLERY_WRAPPER_CLASS}" data-
|
|
12275
|
+
return `<div class="${GALLERY_WRAPPER_CLASS}" data-island="${GALLERY_ISLAND_NAME}">${(0, preact_render_to_string.renderToString)((0, preact.h)(component, {
|
|
12276
12276
|
slides,
|
|
12277
12277
|
caption,
|
|
12278
12278
|
attributes
|
|
@@ -13257,7 +13257,7 @@ Object.defineProperty(exports, "cloudflareBindings", {
|
|
|
13257
13257
|
});
|
|
13258
13258
|
exports.contentPlugin = contentPlugin;
|
|
13259
13259
|
exports.createApp = createApp;
|
|
13260
|
-
exports.
|
|
13260
|
+
exports.createIsland = createIsland;
|
|
13261
13261
|
exports.createPlugin = createPlugin;
|
|
13262
13262
|
exports.createUrls = createUrls;
|
|
13263
13263
|
exports.dataPlugin = dataPlugin;
|