@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.mjs
CHANGED
|
@@ -9134,15 +9134,15 @@ const cliPlugin = createPlugin$1("cli", {
|
|
|
9134
9134
|
function createApi(ctx) {
|
|
9135
9135
|
return {
|
|
9136
9136
|
/**
|
|
9137
|
-
* Register a
|
|
9137
|
+
* Register a island definition (last-registered-wins); warns on collision.
|
|
9138
9138
|
*
|
|
9139
|
-
* @param
|
|
9139
|
+
* @param island - The island definition created via `createIsland`.
|
|
9140
9140
|
* @example
|
|
9141
9141
|
* app.spa.register(counter);
|
|
9142
9142
|
*/
|
|
9143
|
-
register(
|
|
9144
|
-
if (ctx.state.
|
|
9145
|
-
ctx.state.kernel?.register(
|
|
9143
|
+
register(island) {
|
|
9144
|
+
if (ctx.state.registeredIslands.has(island.name)) ctx.log.warn("spa:island-collision", { name: island.name });
|
|
9145
|
+
ctx.state.kernel?.register(island);
|
|
9146
9146
|
},
|
|
9147
9147
|
/**
|
|
9148
9148
|
* Programmatically navigate to a path (client runtime; no-op without a DOM).
|
|
@@ -9168,13 +9168,13 @@ function createApi(ctx) {
|
|
|
9168
9168
|
* Resolve a registered island's api by name (the cross-island seam). Returns
|
|
9169
9169
|
* `undefined` when no provider with that name is currently registered.
|
|
9170
9170
|
*
|
|
9171
|
-
* @param name - The provider island's
|
|
9171
|
+
* @param name - The provider island's island name.
|
|
9172
9172
|
* @returns The provider's api, or `undefined`.
|
|
9173
9173
|
* @example
|
|
9174
|
-
* app.spa.
|
|
9174
|
+
* app.spa.island("lightbox");
|
|
9175
9175
|
*/
|
|
9176
|
-
|
|
9177
|
-
return ctx.state.
|
|
9176
|
+
island(name) {
|
|
9177
|
+
return ctx.state.islandApis.get(name);
|
|
9178
9178
|
}
|
|
9179
9179
|
};
|
|
9180
9180
|
}
|
|
@@ -9193,15 +9193,89 @@ function spaEvents(register) {
|
|
|
9193
9193
|
return {
|
|
9194
9194
|
"spa:navigate": register("A navigation has been intercepted and is starting."),
|
|
9195
9195
|
"spa:navigated": register("The swap completed and the new URL is active."),
|
|
9196
|
-
"spa:
|
|
9197
|
-
"spa:
|
|
9196
|
+
"spa:island-mount": register("A island instance attached to an element."),
|
|
9197
|
+
"spa:island-unmount": register("A island instance detached from an element.")
|
|
9198
9198
|
};
|
|
9199
9199
|
}
|
|
9200
9200
|
//#endregion
|
|
9201
|
+
//#region src/plugins/spa/head.ts
|
|
9202
|
+
/** Single-element head selectors synced by replace/append/remove on navigation. */
|
|
9203
|
+
const META_SELECTORS = [
|
|
9204
|
+
"meta[name=\"description\"]",
|
|
9205
|
+
"meta[property=\"og:title\"]",
|
|
9206
|
+
"meta[property=\"og:description\"]",
|
|
9207
|
+
"meta[property=\"og:url\"]",
|
|
9208
|
+
"meta[property=\"og:image\"]",
|
|
9209
|
+
"meta[property=\"og:type\"]",
|
|
9210
|
+
"meta[property=\"og:locale\"]",
|
|
9211
|
+
"meta[name=\"twitter:card\"]",
|
|
9212
|
+
"meta[name=\"twitter:title\"]",
|
|
9213
|
+
"meta[name=\"twitter:description\"]",
|
|
9214
|
+
"meta[name=\"twitter:image\"]",
|
|
9215
|
+
"meta[name=\"twitter:site\"]",
|
|
9216
|
+
"link[rel=\"canonical\"]"
|
|
9217
|
+
];
|
|
9218
|
+
/** Head element groups fully replaced (remove-all-then-clone) on navigation. */
|
|
9219
|
+
const REPLACE_ALL_SELECTORS = [
|
|
9220
|
+
"script[type=\"application/ld+json\"]",
|
|
9221
|
+
"link[rel=\"alternate\"][hreflang]",
|
|
9222
|
+
"meta[property^=\"article:\"]"
|
|
9223
|
+
];
|
|
9224
|
+
/**
|
|
9225
|
+
* Sync a single head element by selector between the fetched and live document:
|
|
9226
|
+
* replace when both exist, append when only the new doc has it, remove when only
|
|
9227
|
+
* the live doc has it.
|
|
9228
|
+
*
|
|
9229
|
+
* @param selector - CSS selector for the head element to sync.
|
|
9230
|
+
* @param doc - The fetched document (DOMParser-parsed).
|
|
9231
|
+
* @example
|
|
9232
|
+
* syncElement('link[rel="canonical"]', doc);
|
|
9233
|
+
*/
|
|
9234
|
+
function syncElement(selector, doc) {
|
|
9235
|
+
const newElement = doc.querySelector(selector);
|
|
9236
|
+
const oldElement = document.querySelector(selector);
|
|
9237
|
+
if (newElement && oldElement) oldElement.replaceWith(newElement.cloneNode(true));
|
|
9238
|
+
else if (newElement) document.head.append(newElement.cloneNode(true));
|
|
9239
|
+
else if (oldElement) oldElement.remove();
|
|
9240
|
+
}
|
|
9241
|
+
/**
|
|
9242
|
+
* Remove all live matches for a selector and re-clone the fetched document's
|
|
9243
|
+
* matches into the live `<head>`.
|
|
9244
|
+
*
|
|
9245
|
+
* @param selector - CSS selector for the element group to replace wholesale.
|
|
9246
|
+
* @param doc - The fetched document (DOMParser-parsed).
|
|
9247
|
+
* @example
|
|
9248
|
+
* replaceAllBySelector('script[type="application/ld+json"]', doc);
|
|
9249
|
+
*/
|
|
9250
|
+
function replaceAllBySelector(selector, doc) {
|
|
9251
|
+
for (const element of document.querySelectorAll(selector)) element.remove();
|
|
9252
|
+
for (const element of doc.querySelectorAll(selector)) document.head.append(element.cloneNode(true));
|
|
9253
|
+
}
|
|
9254
|
+
/**
|
|
9255
|
+
* Syncs the live document `<head>` after a navigation from the fetched document
|
|
9256
|
+
* (whose head was composed by the `head` plugin). Recomputes
|
|
9257
|
+
* title/meta/canonical/JSON-LD/hreflang/`<html lang>` once and applies them.
|
|
9258
|
+
* The `head` API is accepted to bind the structural dependency (spec/09 deps).
|
|
9259
|
+
*
|
|
9260
|
+
* @param _head - The head plugin API (dependency binding; composition reused via the fetched doc).
|
|
9261
|
+
* @param doc - The fetched document parsed from the navigated page's HTML.
|
|
9262
|
+
* @example
|
|
9263
|
+
* syncHead(headApi, parsedDoc);
|
|
9264
|
+
*/
|
|
9265
|
+
function syncHead(_head, doc) {
|
|
9266
|
+
if (typeof document === "undefined") return;
|
|
9267
|
+
const newTitle = doc.querySelector("title")?.textContent;
|
|
9268
|
+
if (newTitle) document.title = newTitle;
|
|
9269
|
+
const newLang = doc.documentElement.lang;
|
|
9270
|
+
if (newLang) document.documentElement.lang = newLang;
|
|
9271
|
+
for (const selector of META_SELECTORS) syncElement(selector, doc);
|
|
9272
|
+
for (const selector of REPLACE_ALL_SELECTORS) replaceAllBySelector(selector, doc);
|
|
9273
|
+
}
|
|
9274
|
+
//#endregion
|
|
9201
9275
|
//#region src/plugins/spa/types.ts
|
|
9202
|
-
var types_exports$7 = /* @__PURE__ */ __exportAll({
|
|
9276
|
+
var types_exports$7 = /* @__PURE__ */ __exportAll({ ISLAND_HOOK_NAMES: () => ISLAND_HOOK_NAMES });
|
|
9203
9277
|
/** Allowed hook names — single source of truth for fail-fast validation. */
|
|
9204
|
-
const
|
|
9278
|
+
const ISLAND_HOOK_NAMES = [
|
|
9205
9279
|
"onCreate",
|
|
9206
9280
|
"onMount",
|
|
9207
9281
|
"onNavStart",
|
|
@@ -9210,10 +9284,10 @@ const COMPONENT_HOOK_NAMES = [
|
|
|
9210
9284
|
"onDestroy"
|
|
9211
9285
|
];
|
|
9212
9286
|
//#endregion
|
|
9213
|
-
//#region src/plugins/spa/
|
|
9287
|
+
//#region src/plugins/spa/islands.ts
|
|
9214
9288
|
/**
|
|
9215
|
-
* @file spa plugin —
|
|
9216
|
-
* surface (`
|
|
9289
|
+
* @file spa plugin — island lifecycle, mounting, the plugin-mirror authoring
|
|
9290
|
+
* surface (`createIsland` with a typed `{ state, render, events, api }` spec),
|
|
9217
9291
|
* the per-instance state + microtask-batched render scheduler, declarative
|
|
9218
9292
|
* delegated events, and the cross-island api registry.
|
|
9219
9293
|
* @see README.md
|
|
@@ -9221,8 +9295,8 @@ const COMPONENT_HOOK_NAMES = [
|
|
|
9221
9295
|
/** Error prefix for spa fail-fast failures (spec/11 Part-3). */
|
|
9222
9296
|
const ERROR_PREFIX$2 = "[web]";
|
|
9223
9297
|
/** The set of legal hook names, frozen for O(1) membership checks. */
|
|
9224
|
-
const HOOK_NAME_SET = new Set(
|
|
9225
|
-
/** The spec-only keys that select the plugin-mirror form of {@link
|
|
9298
|
+
const HOOK_NAME_SET = new Set(ISLAND_HOOK_NAMES);
|
|
9299
|
+
/** The spec-only keys that select the plugin-mirror form of {@link createIsland}. */
|
|
9226
9300
|
const SPEC_KEYS = new Set([
|
|
9227
9301
|
"state",
|
|
9228
9302
|
"render",
|
|
@@ -9261,7 +9335,7 @@ let renderChunk;
|
|
|
9261
9335
|
let commitVNodeFunction;
|
|
9262
9336
|
/**
|
|
9263
9337
|
* Load the lazy `./render` chunk (once) and cache its `commitVNode` for synchronous
|
|
9264
|
-
* use by later renders. Awaited by a
|
|
9338
|
+
* use by later renders. Awaited by a island's `mountPromise` so the test harness's
|
|
9265
9339
|
* `settle()` can deterministically flush a VNode render.
|
|
9266
9340
|
*
|
|
9267
9341
|
* @returns A promise that resolves once `commitVNode` is available.
|
|
@@ -9269,7 +9343,7 @@ let commitVNodeFunction;
|
|
|
9269
9343
|
* await loadRenderChunk();
|
|
9270
9344
|
*/
|
|
9271
9345
|
async function loadRenderChunk() {
|
|
9272
|
-
renderChunk ??= import("./render-
|
|
9346
|
+
renderChunk ??= import("./render-yXHc9BWI.mjs");
|
|
9273
9347
|
commitVNodeFunction = (await renderChunk).commitVNode;
|
|
9274
9348
|
}
|
|
9275
9349
|
/**
|
|
@@ -9278,7 +9352,7 @@ async function loadRenderChunk() {
|
|
|
9278
9352
|
* a Preact `VNode` → committed through the lazy gate (loading it on demand if needed).
|
|
9279
9353
|
*
|
|
9280
9354
|
* @param host - The island host element to render into.
|
|
9281
|
-
* @param result - The value returned by the
|
|
9355
|
+
* @param result - The value returned by the island's `render`.
|
|
9282
9356
|
* @example
|
|
9283
9357
|
* commitResult(host, h(View, { items }));
|
|
9284
9358
|
*/
|
|
@@ -9300,7 +9374,7 @@ function commitResult(host, result) {
|
|
|
9300
9374
|
loadRenderChunk().then(() => commitVNodeFunction?.(vnode, host)).catch(() => {});
|
|
9301
9375
|
}
|
|
9302
9376
|
/**
|
|
9303
|
-
* Run a
|
|
9377
|
+
* Run a island's `render(state, ctx)` and commit the result now. Guards against
|
|
9304
9378
|
* synchronous re-entrancy (a render that calls `ctx.flush`) with a depth cap.
|
|
9305
9379
|
*
|
|
9306
9380
|
* @param instance - The instance to render.
|
|
@@ -9311,7 +9385,7 @@ function commitResult(host, result) {
|
|
|
9311
9385
|
function runRender(instance) {
|
|
9312
9386
|
const render = instance.def.spec?.render;
|
|
9313
9387
|
if (!render) return;
|
|
9314
|
-
if (instance.renderDepth > MAX_RENDER_DEPTH) throw new Error(`${ERROR_PREFIX$2}
|
|
9388
|
+
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)`);
|
|
9315
9389
|
instance.renderDepth += 1;
|
|
9316
9390
|
try {
|
|
9317
9391
|
commitResult(instance.el, render(instance.state ?? {}, instance.ctx));
|
|
@@ -9337,12 +9411,12 @@ function scheduleRender(instance) {
|
|
|
9337
9411
|
});
|
|
9338
9412
|
}
|
|
9339
9413
|
/**
|
|
9340
|
-
* Build the single per-instance {@link
|
|
9414
|
+
* Build the single per-instance {@link IslandContext} reused by every hook, event
|
|
9341
9415
|
* handler, and render. Route fields (`params`/`meta`/`locale`/`url`) and `data` read
|
|
9342
9416
|
* through the instance so a navigation update is reflected without rebuilding the ctx;
|
|
9343
|
-
* `state`/`set`/`flush`/`cleanup`/`
|
|
9417
|
+
* `state`/`set`/`flush`/`cleanup`/`island` are bound to the instance + plugin state.
|
|
9344
9418
|
*
|
|
9345
|
-
* @param state - The plugin state (for the cross-island `
|
|
9419
|
+
* @param state - The plugin state (for the cross-island `island` resolver).
|
|
9346
9420
|
* @param instance - The instance the context is bound to.
|
|
9347
9421
|
* @returns The instance-bound context.
|
|
9348
9422
|
* @example
|
|
@@ -9446,13 +9520,13 @@ function buildContext(state, instance) {
|
|
|
9446
9520
|
/**
|
|
9447
9521
|
* Resolve another island's registered api by name (`undefined` when absent).
|
|
9448
9522
|
*
|
|
9449
|
-
* @param name - The provider island's
|
|
9523
|
+
* @param name - The provider island's island name.
|
|
9450
9524
|
* @returns The provider's api, or `undefined`.
|
|
9451
9525
|
* @example
|
|
9452
|
-
* ctx.
|
|
9526
|
+
* ctx.island("lightbox");
|
|
9453
9527
|
*/
|
|
9454
|
-
|
|
9455
|
-
return state.
|
|
9528
|
+
island(name) {
|
|
9529
|
+
return state.islandApis.get(name);
|
|
9456
9530
|
}
|
|
9457
9531
|
};
|
|
9458
9532
|
}
|
|
@@ -9476,7 +9550,7 @@ function matchTarget(host, event, selector) {
|
|
|
9476
9550
|
return matched && host.contains(matched) ? matched : void 0;
|
|
9477
9551
|
}
|
|
9478
9552
|
/**
|
|
9479
|
-
* Attach a
|
|
9553
|
+
* Attach a island's declarative `events` map: one real listener per event TYPE on
|
|
9480
9554
|
* the host (dispatch walks `closest(selector)` for each registered selector), each
|
|
9481
9555
|
* removed via the instance's cleanup registry on destroy.
|
|
9482
9556
|
*
|
|
@@ -9493,7 +9567,7 @@ function attachEvents(instance, events) {
|
|
|
9493
9567
|
const space = key.indexOf(" ");
|
|
9494
9568
|
const type = (space === -1 ? key : key.slice(0, space)).trim();
|
|
9495
9569
|
const selector = space === -1 ? "" : key.slice(space + 1).trim();
|
|
9496
|
-
if (type === "") throw new Error(`${ERROR_PREFIX$2}
|
|
9570
|
+
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]")`);
|
|
9497
9571
|
const list = byType.get(type) ?? [];
|
|
9498
9572
|
list.push({
|
|
9499
9573
|
selector,
|
|
@@ -9516,62 +9590,62 @@ function attachEvents(instance, events) {
|
|
|
9516
9590
|
* Validate a single hook entry: its key must be a known hook name and its value
|
|
9517
9591
|
* must be a function. Throws fail-fast on the first violation.
|
|
9518
9592
|
*
|
|
9519
|
-
* @param
|
|
9593
|
+
* @param islandName - The owning island name (for error messages).
|
|
9520
9594
|
* @param source - The raw authoring object being validated.
|
|
9521
9595
|
* @param key - The hook key to validate.
|
|
9522
|
-
* @throws {Error} If `key` is not in `
|
|
9596
|
+
* @throws {Error} If `key` is not in `ISLAND_HOOK_NAMES`.
|
|
9523
9597
|
* @throws {TypeError} If the hook value is not a function.
|
|
9524
9598
|
* @example
|
|
9525
9599
|
* validateHookEntry("counter", source, "onMount");
|
|
9526
9600
|
*/
|
|
9527
|
-
function validateHookEntry(
|
|
9528
|
-
if (!HOOK_NAME_SET.has(key)) throw new Error(`${ERROR_PREFIX$2} unknown
|
|
9529
|
-
if (typeof source[key] !== "function") throw new TypeError(`${ERROR_PREFIX$2}
|
|
9601
|
+
function validateHookEntry(islandName, source, key) {
|
|
9602
|
+
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`);
|
|
9603
|
+
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`);
|
|
9530
9604
|
}
|
|
9531
9605
|
/**
|
|
9532
9606
|
* Validate the spec extras (`state`/`render`/`api` must be functions; `events` must be
|
|
9533
9607
|
* a plain object of functions). Throws fail-fast on the first violation.
|
|
9534
9608
|
*
|
|
9535
|
-
* @param
|
|
9609
|
+
* @param islandName - The owning island name (for error messages).
|
|
9536
9610
|
* @param extras - The partitioned spec extras to validate.
|
|
9537
9611
|
* @throws {TypeError} If a present extra has the wrong shape.
|
|
9538
9612
|
* @example
|
|
9539
9613
|
* validateSpecExtras("board", { state: () => ({}) });
|
|
9540
9614
|
*/
|
|
9541
|
-
function validateSpecExtras(
|
|
9615
|
+
function validateSpecExtras(islandName, extras) {
|
|
9542
9616
|
for (const key of [
|
|
9543
9617
|
"state",
|
|
9544
9618
|
"render",
|
|
9545
9619
|
"api"
|
|
9546
|
-
]) if (extras[key] !== void 0 && typeof extras[key] !== "function") throw new TypeError(`${ERROR_PREFIX$2}
|
|
9620
|
+
]) 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`);
|
|
9547
9621
|
if (extras.events !== void 0) {
|
|
9548
9622
|
const events = extras.events;
|
|
9549
|
-
if (!(typeof events === "object")) throw new TypeError(`${ERROR_PREFIX$2}
|
|
9550
|
-
for (const [key, handler] of Object.entries(events)) if (typeof handler !== "function") throw new TypeError(`${ERROR_PREFIX$2}
|
|
9623
|
+
if (!(typeof events === "object")) throw new TypeError(`${ERROR_PREFIX$2} island "events" on "${islandName}" must be an object of handlers`);
|
|
9624
|
+
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`);
|
|
9551
9625
|
}
|
|
9552
9626
|
}
|
|
9553
9627
|
/**
|
|
9554
|
-
* Create a validated
|
|
9555
|
-
* (`
|
|
9556
|
-
* (`
|
|
9628
|
+
* Create a validated island definition. Accepts either the legacy hooks-only form
|
|
9629
|
+
* (`createIsland("counter", { onMount() {} })`) or the plugin-mirror spec form
|
|
9630
|
+
* (`createIsland("board", { state, render, events, api, ...hooks })`). Spec-only
|
|
9557
9631
|
* keys (`state`/`render`/`events`/`api`) are partitioned out before hook-name
|
|
9558
9632
|
* validation, so a real typo (e.g. `onMout`) still throws immediately while the spec
|
|
9559
9633
|
* keys are accepted.
|
|
9560
9634
|
*
|
|
9561
|
-
* @param name - Unique
|
|
9635
|
+
* @param name - Unique island name.
|
|
9562
9636
|
* @param spec - Lifecycle hooks, or the `{ state, render, events, api, ...hooks }` spec.
|
|
9563
|
-
* @returns A `
|
|
9637
|
+
* @returns A `IslandDef` ready to `register`.
|
|
9564
9638
|
* @throws {Error} If `name` is empty, a hook key is unknown, or an extra/hook value has the wrong shape.
|
|
9565
9639
|
* @example
|
|
9566
|
-
* const counter =
|
|
9640
|
+
* const counter = createIsland("counter", { onMount({ el }) { el.textContent = "0"; } });
|
|
9567
9641
|
* @example
|
|
9568
|
-
* const list =
|
|
9642
|
+
* const list = createIsland<{ items: string[] }>("list", {
|
|
9569
9643
|
* state: () => ({ items: [] }),
|
|
9570
9644
|
* render: (s) => h(List, { items: s.items })
|
|
9571
9645
|
* });
|
|
9572
9646
|
*/
|
|
9573
|
-
function
|
|
9574
|
-
if (name.trim() === "") throw new Error(`${ERROR_PREFIX$2}
|
|
9647
|
+
function createIsland(name, spec) {
|
|
9648
|
+
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)`);
|
|
9575
9649
|
const source = spec;
|
|
9576
9650
|
const hooks = {};
|
|
9577
9651
|
const extras = {};
|
|
@@ -9648,30 +9722,30 @@ function disposeInstance(state, instance) {
|
|
|
9648
9722
|
} catch {}
|
|
9649
9723
|
instance.cleanups.length = 0;
|
|
9650
9724
|
instance.renderScheduled = false;
|
|
9651
|
-
if (instance.api !== void 0 && state.
|
|
9725
|
+
if (instance.api !== void 0 && state.islandApis.get(instance.def.name) === instance.api) state.islandApis.delete(instance.def.name);
|
|
9652
9726
|
}
|
|
9653
9727
|
/**
|
|
9654
|
-
* Mounts a single `data-
|
|
9728
|
+
* Mounts a single `data-island` element: classifies persistent vs page-specific,
|
|
9655
9729
|
* builds the instance + its bound context, initializes per-instance `state`, registers
|
|
9656
9730
|
* its `api`, attaches declarative `events`, fires `onCreate` then `onMount` (capturing
|
|
9657
9731
|
* an async `onMount` + render-chunk load as `mountPromise`), schedules the initial
|
|
9658
|
-
* render, records it, and emits `spa:
|
|
9659
|
-
* mounted, has no
|
|
9732
|
+
* render, records it, and emits `spa:island-mount`. No-ops if the element is already
|
|
9733
|
+
* mounted, has no island name, or names an unregistered island.
|
|
9660
9734
|
*
|
|
9661
|
-
* @param state - The plugin state (
|
|
9662
|
-
* @param emit - The event emitter for spa:
|
|
9735
|
+
* @param state - The plugin state (registeredIslands + instances + islandApis).
|
|
9736
|
+
* @param emit - The event emitter for spa:island-mount.
|
|
9663
9737
|
* @param swapArea - The swap-region element, or null when none was found.
|
|
9664
9738
|
* @param data - The current page data payload.
|
|
9665
|
-
* @param element - The candidate element carrying a `data-
|
|
9739
|
+
* @param element - The candidate element carrying a `data-island` attribute.
|
|
9666
9740
|
* @param route - The matched-route slice for the current URL (params/meta/locale/url).
|
|
9667
9741
|
* @example
|
|
9668
9742
|
* mountElement(state, emit, swapArea, data, element, route);
|
|
9669
9743
|
*/
|
|
9670
9744
|
function mountElement(state, emit, swapArea, data, element, route = EMPTY_ROUTE) {
|
|
9671
9745
|
if (state.instances.has(element)) return;
|
|
9672
|
-
const name = element.dataset.
|
|
9746
|
+
const name = element.dataset.island;
|
|
9673
9747
|
if (!name) return;
|
|
9674
|
-
const definition = state.
|
|
9748
|
+
const definition = state.registeredIslands.get(name);
|
|
9675
9749
|
if (!definition) return;
|
|
9676
9750
|
const instance = {
|
|
9677
9751
|
def: definition,
|
|
@@ -9697,7 +9771,7 @@ function mountElement(state, emit, swapArea, data, element, route = EMPTY_ROUTE)
|
|
|
9697
9771
|
if (spec?.state) instance.state = spec.state(instance.ctx);
|
|
9698
9772
|
if (spec?.api) {
|
|
9699
9773
|
instance.api = spec.api(instance.ctx);
|
|
9700
|
-
state.
|
|
9774
|
+
state.islandApis.set(definition.name, instance.api);
|
|
9701
9775
|
}
|
|
9702
9776
|
if (spec?.events) attachEvents(instance, spec.events);
|
|
9703
9777
|
runHook(instance, "onCreate");
|
|
@@ -9708,20 +9782,20 @@ function mountElement(state, emit, swapArea, data, element, route = EMPTY_ROUTE)
|
|
|
9708
9782
|
if (onMountResult && typeof onMountResult.then === "function") pending.push(onMountResult);
|
|
9709
9783
|
instance.mountPromise = pending.length > 0 ? Promise.all(pending).then(() => {}) : void 0;
|
|
9710
9784
|
state.instances.set(element, instance);
|
|
9711
|
-
emit("spa:
|
|
9785
|
+
emit("spa:island-mount", {
|
|
9712
9786
|
name: definition.name,
|
|
9713
9787
|
el: element
|
|
9714
9788
|
});
|
|
9715
9789
|
}
|
|
9716
9790
|
/**
|
|
9717
|
-
* Scans the swap region, mounts
|
|
9791
|
+
* Scans the swap region, mounts islands for matching `data-island` elements,
|
|
9718
9792
|
* classifies persistent (outside swap area) vs page-specific (inside), runs
|
|
9719
|
-
* `onCreate`/`onMount` + initial render, and emits `spa:
|
|
9793
|
+
* `onCreate`/`onMount` + initial render, and emits `spa:island-mount` per instance.
|
|
9720
9794
|
* Already-mounted elements are skipped.
|
|
9721
9795
|
*
|
|
9722
|
-
* @param state - The plugin state (
|
|
9723
|
-
* @param emit - The event emitter for spa:
|
|
9724
|
-
* @param swapSelector - CSS selector bounding page-specific
|
|
9796
|
+
* @param state - The plugin state (registeredIslands + instances + islandApis).
|
|
9797
|
+
* @param emit - The event emitter for spa:island-mount.
|
|
9798
|
+
* @param swapSelector - CSS selector bounding page-specific islands.
|
|
9725
9799
|
* @param route - The matched-route slice for the current URL (params/meta/locale/url).
|
|
9726
9800
|
* @example
|
|
9727
9801
|
* scanAndMount(state, emit, "main > section", route);
|
|
@@ -9730,16 +9804,16 @@ function scanAndMount(state, emit, swapSelector, route = EMPTY_ROUTE) {
|
|
|
9730
9804
|
if (typeof document === "undefined") return;
|
|
9731
9805
|
const swapArea = document.querySelector(swapSelector);
|
|
9732
9806
|
const data = extractPageData(document);
|
|
9733
|
-
for (const element of document.querySelectorAll("[data-
|
|
9807
|
+
for (const element of document.querySelectorAll("[data-island]")) mountElement(state, emit, swapArea, data, element, route);
|
|
9734
9808
|
}
|
|
9735
9809
|
/**
|
|
9736
9810
|
* Unmounts page-specific instances inside the swap region (runs `onUnMount` then
|
|
9737
9811
|
* `onDestroy`, then their cleanup disposers + api unregister), removes them from state,
|
|
9738
|
-
* and emits `spa:
|
|
9812
|
+
* and emits `spa:island-unmount`. Persistent instances (outside the swap area) are
|
|
9739
9813
|
* left in place.
|
|
9740
9814
|
*
|
|
9741
9815
|
* @param state - The plugin state holding live instances.
|
|
9742
|
-
* @param emit - The event emitter for spa:
|
|
9816
|
+
* @param emit - The event emitter for spa:island-unmount.
|
|
9743
9817
|
* @example
|
|
9744
9818
|
* unmountPageSpecific(state, emit);
|
|
9745
9819
|
*/
|
|
@@ -9752,7 +9826,7 @@ function unmountPageSpecific(state, emit) {
|
|
|
9752
9826
|
runHook(instance, "onDestroy");
|
|
9753
9827
|
disposeInstance(state, instance);
|
|
9754
9828
|
state.instances.delete(element);
|
|
9755
|
-
emit("spa:
|
|
9829
|
+
emit("spa:island-unmount", {
|
|
9756
9830
|
name: instance.def.name,
|
|
9757
9831
|
el: element
|
|
9758
9832
|
});
|
|
@@ -9761,11 +9835,11 @@ function unmountPageSpecific(state, emit) {
|
|
|
9761
9835
|
/**
|
|
9762
9836
|
* Disposes ALL live instances (persistent and page-specific) on teardown: runs
|
|
9763
9837
|
* `onUnMount` then `onDestroy`, then their cleanup disposers + api unregister, emits
|
|
9764
|
-
* `spa:
|
|
9838
|
+
* `spa:island-unmount`, and clears the instance + api maps. Used by the kernel's
|
|
9765
9839
|
* `dispose` on plugin stop.
|
|
9766
9840
|
*
|
|
9767
9841
|
* @param state - The plugin state holding live instances.
|
|
9768
|
-
* @param emit - The event emitter for spa:
|
|
9842
|
+
* @param emit - The event emitter for spa:island-unmount.
|
|
9769
9843
|
* @example
|
|
9770
9844
|
* unmountAll(state, emit);
|
|
9771
9845
|
*/
|
|
@@ -9776,13 +9850,13 @@ function unmountAll(state, emit) {
|
|
|
9776
9850
|
runHook(instance, "onUnMount");
|
|
9777
9851
|
runHook(instance, "onDestroy");
|
|
9778
9852
|
disposeInstance(state, instance);
|
|
9779
|
-
emit("spa:
|
|
9853
|
+
emit("spa:island-unmount", {
|
|
9780
9854
|
name: instance.def.name,
|
|
9781
9855
|
el: element
|
|
9782
9856
|
});
|
|
9783
9857
|
}
|
|
9784
9858
|
state.instances.clear();
|
|
9785
|
-
state.
|
|
9859
|
+
state.islandApis.clear();
|
|
9786
9860
|
}
|
|
9787
9861
|
/**
|
|
9788
9862
|
* Fires `onNavStart` on every currently-mounted instance (persistent instances
|
|
@@ -9819,80 +9893,6 @@ function notifyNavEnd(state, route = EMPTY_ROUTE) {
|
|
|
9819
9893
|
}
|
|
9820
9894
|
}
|
|
9821
9895
|
//#endregion
|
|
9822
|
-
//#region src/plugins/spa/head.ts
|
|
9823
|
-
/** Single-element head selectors synced by replace/append/remove on navigation. */
|
|
9824
|
-
const META_SELECTORS = [
|
|
9825
|
-
"meta[name=\"description\"]",
|
|
9826
|
-
"meta[property=\"og:title\"]",
|
|
9827
|
-
"meta[property=\"og:description\"]",
|
|
9828
|
-
"meta[property=\"og:url\"]",
|
|
9829
|
-
"meta[property=\"og:image\"]",
|
|
9830
|
-
"meta[property=\"og:type\"]",
|
|
9831
|
-
"meta[property=\"og:locale\"]",
|
|
9832
|
-
"meta[name=\"twitter:card\"]",
|
|
9833
|
-
"meta[name=\"twitter:title\"]",
|
|
9834
|
-
"meta[name=\"twitter:description\"]",
|
|
9835
|
-
"meta[name=\"twitter:image\"]",
|
|
9836
|
-
"meta[name=\"twitter:site\"]",
|
|
9837
|
-
"link[rel=\"canonical\"]"
|
|
9838
|
-
];
|
|
9839
|
-
/** Head element groups fully replaced (remove-all-then-clone) on navigation. */
|
|
9840
|
-
const REPLACE_ALL_SELECTORS = [
|
|
9841
|
-
"script[type=\"application/ld+json\"]",
|
|
9842
|
-
"link[rel=\"alternate\"][hreflang]",
|
|
9843
|
-
"meta[property^=\"article:\"]"
|
|
9844
|
-
];
|
|
9845
|
-
/**
|
|
9846
|
-
* Sync a single head element by selector between the fetched and live document:
|
|
9847
|
-
* replace when both exist, append when only the new doc has it, remove when only
|
|
9848
|
-
* the live doc has it.
|
|
9849
|
-
*
|
|
9850
|
-
* @param selector - CSS selector for the head element to sync.
|
|
9851
|
-
* @param doc - The fetched document (DOMParser-parsed).
|
|
9852
|
-
* @example
|
|
9853
|
-
* syncElement('link[rel="canonical"]', doc);
|
|
9854
|
-
*/
|
|
9855
|
-
function syncElement(selector, doc) {
|
|
9856
|
-
const newElement = doc.querySelector(selector);
|
|
9857
|
-
const oldElement = document.querySelector(selector);
|
|
9858
|
-
if (newElement && oldElement) oldElement.replaceWith(newElement.cloneNode(true));
|
|
9859
|
-
else if (newElement) document.head.append(newElement.cloneNode(true));
|
|
9860
|
-
else if (oldElement) oldElement.remove();
|
|
9861
|
-
}
|
|
9862
|
-
/**
|
|
9863
|
-
* Remove all live matches for a selector and re-clone the fetched document's
|
|
9864
|
-
* matches into the live `<head>`.
|
|
9865
|
-
*
|
|
9866
|
-
* @param selector - CSS selector for the element group to replace wholesale.
|
|
9867
|
-
* @param doc - The fetched document (DOMParser-parsed).
|
|
9868
|
-
* @example
|
|
9869
|
-
* replaceAllBySelector('script[type="application/ld+json"]', doc);
|
|
9870
|
-
*/
|
|
9871
|
-
function replaceAllBySelector(selector, doc) {
|
|
9872
|
-
for (const element of document.querySelectorAll(selector)) element.remove();
|
|
9873
|
-
for (const element of doc.querySelectorAll(selector)) document.head.append(element.cloneNode(true));
|
|
9874
|
-
}
|
|
9875
|
-
/**
|
|
9876
|
-
* Syncs the live document `<head>` after a navigation from the fetched document
|
|
9877
|
-
* (whose head was composed by the `head` plugin). Recomputes
|
|
9878
|
-
* title/meta/canonical/JSON-LD/hreflang/`<html lang>` once and applies them.
|
|
9879
|
-
* The `head` API is accepted to bind the structural dependency (spec/09 deps).
|
|
9880
|
-
*
|
|
9881
|
-
* @param _head - The head plugin API (dependency binding; composition reused via the fetched doc).
|
|
9882
|
-
* @param doc - The fetched document parsed from the navigated page's HTML.
|
|
9883
|
-
* @example
|
|
9884
|
-
* syncHead(headApi, parsedDoc);
|
|
9885
|
-
*/
|
|
9886
|
-
function syncHead(_head, doc) {
|
|
9887
|
-
if (typeof document === "undefined") return;
|
|
9888
|
-
const newTitle = doc.querySelector("title")?.textContent;
|
|
9889
|
-
if (newTitle) document.title = newTitle;
|
|
9890
|
-
const newLang = doc.documentElement.lang;
|
|
9891
|
-
if (newLang) document.documentElement.lang = newLang;
|
|
9892
|
-
for (const selector of META_SELECTORS) syncElement(selector, doc);
|
|
9893
|
-
for (const selector of REPLACE_ALL_SELECTORS) replaceAllBySelector(selector, doc);
|
|
9894
|
-
}
|
|
9895
|
-
//#endregion
|
|
9896
9896
|
//#region src/plugins/spa/progress.ts
|
|
9897
9897
|
/** Delay before the bar appears, so fast navigations show no indicator. */
|
|
9898
9898
|
const START_DELAY_MS = 150;
|
|
@@ -10299,7 +10299,7 @@ const defaultSpaConfig = {
|
|
|
10299
10299
|
swapSelector: "main > section",
|
|
10300
10300
|
viewTransitions: false,
|
|
10301
10301
|
progressBar: true,
|
|
10302
|
-
|
|
10302
|
+
islands: []
|
|
10303
10303
|
};
|
|
10304
10304
|
/**
|
|
10305
10305
|
* Whether a selector is syntactically valid (parseable by the DOM). Falls back
|
|
@@ -10321,8 +10321,8 @@ function isValidSelector(selector) {
|
|
|
10321
10321
|
}
|
|
10322
10322
|
/**
|
|
10323
10323
|
* Validates the spa config and applies defaults (Part-3 errors on an empty or
|
|
10324
|
-
* syntactically-invalid `swapSelector`).
|
|
10325
|
-
* `
|
|
10324
|
+
* syntactically-invalid `swapSelector`). Island-hook validation runs later in
|
|
10325
|
+
* `createIsland` when the islands are registered.
|
|
10326
10326
|
*
|
|
10327
10327
|
* @param config - The raw spa config to validate.
|
|
10328
10328
|
* @returns The fully-resolved config with defaults applied.
|
|
@@ -10338,7 +10338,7 @@ function resolveSpaConfig(config) {
|
|
|
10338
10338
|
swapSelector,
|
|
10339
10339
|
viewTransitions: config.viewTransitions ?? false,
|
|
10340
10340
|
progressBar: config.progressBar ?? true,
|
|
10341
|
-
|
|
10341
|
+
islands: config.islands ?? []
|
|
10342
10342
|
};
|
|
10343
10343
|
}
|
|
10344
10344
|
/**
|
|
@@ -10355,9 +10355,9 @@ function resolveSpaConfig(config) {
|
|
|
10355
10355
|
*/
|
|
10356
10356
|
function createState(_ctx) {
|
|
10357
10357
|
return {
|
|
10358
|
-
|
|
10358
|
+
registeredIslands: /* @__PURE__ */ new Map(),
|
|
10359
10359
|
instances: /* @__PURE__ */ new Map(),
|
|
10360
|
-
|
|
10360
|
+
islandApis: /* @__PURE__ */ new Map(),
|
|
10361
10361
|
currentUrl: "",
|
|
10362
10362
|
destroyRouter: null,
|
|
10363
10363
|
started: false,
|
|
@@ -10377,15 +10377,15 @@ function createState(_ctx) {
|
|
|
10377
10377
|
/** Error prefix for spa kernel failures (spec/11 Part-3). */
|
|
10378
10378
|
const ERROR_PREFIX = "[web]";
|
|
10379
10379
|
/**
|
|
10380
|
-
* Registers a
|
|
10380
|
+
* Registers a island definition into state (last-registered-wins).
|
|
10381
10381
|
*
|
|
10382
|
-
* @param state - The plugin state holding
|
|
10383
|
-
* @param
|
|
10382
|
+
* @param state - The plugin state holding registeredIslands.
|
|
10383
|
+
* @param island - The island definition to register.
|
|
10384
10384
|
* @example
|
|
10385
|
-
*
|
|
10385
|
+
* registerIsland(state, counter);
|
|
10386
10386
|
*/
|
|
10387
|
-
function
|
|
10388
|
-
state.
|
|
10387
|
+
function registerIsland(state, island) {
|
|
10388
|
+
state.registeredIslands.set(island.name, island);
|
|
10389
10389
|
}
|
|
10390
10390
|
/**
|
|
10391
10391
|
* Resolve the current document URL (pathname + search), or `""` when headless.
|
|
@@ -10460,15 +10460,15 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10460
10460
|
});
|
|
10461
10461
|
};
|
|
10462
10462
|
/**
|
|
10463
|
-
* Build the matched-route slice (params/meta/locale/url) for the
|
|
10463
|
+
* Build the matched-route slice (params/meta/locale/url) for the island context at `path`,
|
|
10464
10464
|
* so islands read their route's params/meta directly. An unmatched path yields an empty slice.
|
|
10465
10465
|
*
|
|
10466
10466
|
* @param path - The URL (pathname + search) to match.
|
|
10467
10467
|
* @returns The route slice for the matched route.
|
|
10468
10468
|
* @example
|
|
10469
|
-
* scanAndMount(state, emit, resolved.swapSelector,
|
|
10469
|
+
* scanAndMount(state, emit, resolved.swapSelector, islandRouteContext(pathname));
|
|
10470
10470
|
*/
|
|
10471
|
-
const
|
|
10471
|
+
const islandRouteContext = (path) => {
|
|
10472
10472
|
const matchPath = path.split("?")[0] ?? path;
|
|
10473
10473
|
const hit = deps.router.match(matchPath);
|
|
10474
10474
|
const locale = hit?.params.lang ?? (typeof document === "undefined" ? "" : document.documentElement.lang) ?? "";
|
|
@@ -10497,7 +10497,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10497
10497
|
syncHead(deps.head, doc);
|
|
10498
10498
|
unmountPageSpecific(state, emit);
|
|
10499
10499
|
if (!swapRegion(doc, resolved.swapSelector, resolved.viewTransitions, () => {
|
|
10500
|
-
const routeSlice =
|
|
10500
|
+
const routeSlice = islandRouteContext(pathname);
|
|
10501
10501
|
scanAndMount(state, emit, resolved.swapSelector, routeSlice);
|
|
10502
10502
|
notifyNavEnd(state, routeSlice);
|
|
10503
10503
|
}, applyPendingScroll)) {
|
|
@@ -10510,7 +10510,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10510
10510
|
emit("spa:navigated", { url: pathname });
|
|
10511
10511
|
};
|
|
10512
10512
|
/**
|
|
10513
|
-
* Begin a navigation: start progress, notify
|
|
10513
|
+
* Begin a navigation: start progress, notify islands, emit navigate.
|
|
10514
10514
|
*
|
|
10515
10515
|
* @param pathname - The destination pathname.
|
|
10516
10516
|
* @example
|
|
@@ -10596,11 +10596,11 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10596
10596
|
const commitDataRender = async (pathname, resolvedRender, signal) => {
|
|
10597
10597
|
if (signal?.aborted) return;
|
|
10598
10598
|
const { route, vnode, routeContext, region } = resolvedRender;
|
|
10599
|
-
const { renderVNode } = await import("./render-
|
|
10599
|
+
const { renderVNode } = await import("./render-yXHc9BWI.mjs");
|
|
10600
10600
|
if (signal?.aborted) return;
|
|
10601
10601
|
syncDataHead(deps.head, route, routeContext);
|
|
10602
10602
|
unmountPageSpecific(state, emit);
|
|
10603
|
-
const routeSlice =
|
|
10603
|
+
const routeSlice = islandRouteContext(pathname);
|
|
10604
10604
|
/**
|
|
10605
10605
|
* Render the VNode into the region and re-mount its islands in one paint — the
|
|
10606
10606
|
* swap body handed to `runSwap` (optionally wrapped in a View Transition).
|
|
@@ -10658,14 +10658,14 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10658
10658
|
* await bootRender("/b/abc123");
|
|
10659
10659
|
*/
|
|
10660
10660
|
const bootRender = async (pathname) => {
|
|
10661
|
-
const routeSlice =
|
|
10661
|
+
const routeSlice = islandRouteContext(pathname);
|
|
10662
10662
|
const resolvedRender = await resolveDataRender(pathname);
|
|
10663
10663
|
if (resolvedRender === false) {
|
|
10664
10664
|
scanAndMount(state, emit, resolved.swapSelector, routeSlice);
|
|
10665
10665
|
return;
|
|
10666
10666
|
}
|
|
10667
10667
|
const { vnode, region } = resolvedRender;
|
|
10668
|
-
const { renderVNode } = await import("./render-
|
|
10668
|
+
const { renderVNode } = await import("./render-yXHc9BWI.mjs");
|
|
10669
10669
|
renderVNode(vnode, region);
|
|
10670
10670
|
scanAndMount(state, emit, resolved.swapSelector, routeSlice);
|
|
10671
10671
|
};
|
|
@@ -10693,13 +10693,13 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10693
10693
|
};
|
|
10694
10694
|
return {
|
|
10695
10695
|
/**
|
|
10696
|
-
* Register config
|
|
10696
|
+
* Register config islands and seed currentUrl from the document.
|
|
10697
10697
|
*
|
|
10698
10698
|
* @example
|
|
10699
10699
|
* kernel.init();
|
|
10700
10700
|
*/
|
|
10701
10701
|
init() {
|
|
10702
|
-
for (const
|
|
10702
|
+
for (const island of resolved.islands) registerIsland(state, island);
|
|
10703
10703
|
state.currentUrl = currentLocationUrl();
|
|
10704
10704
|
},
|
|
10705
10705
|
/**
|
|
@@ -10717,18 +10717,18 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10717
10717
|
const matchPath = state.currentUrl.split("?")[0] ?? state.currentUrl;
|
|
10718
10718
|
const hit = deps.router.match(matchPath);
|
|
10719
10719
|
if (hit?.route._handlers.render && isClientOnlyRoute(deps.router.mode(), hit.route)) bootRender(state.currentUrl);
|
|
10720
|
-
else scanAndMount(state, emit, resolved.swapSelector,
|
|
10720
|
+
else scanAndMount(state, emit, resolved.swapSelector, islandRouteContext(state.currentUrl));
|
|
10721
10721
|
state.started = true;
|
|
10722
10722
|
},
|
|
10723
10723
|
/**
|
|
10724
|
-
* Register a
|
|
10724
|
+
* Register a island definition (last-registered-wins).
|
|
10725
10725
|
*
|
|
10726
|
-
* @param
|
|
10726
|
+
* @param island - The island definition to register.
|
|
10727
10727
|
* @example
|
|
10728
10728
|
* kernel.register(counter);
|
|
10729
10729
|
*/
|
|
10730
|
-
register(
|
|
10731
|
-
|
|
10730
|
+
register(island) {
|
|
10731
|
+
registerIsland(state, island);
|
|
10732
10732
|
},
|
|
10733
10733
|
/**
|
|
10734
10734
|
* Process a navigation to `path` (fetch then swap; full reload on error).
|
|
@@ -10742,13 +10742,13 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10742
10742
|
navigate(path).catch(() => {});
|
|
10743
10743
|
},
|
|
10744
10744
|
/**
|
|
10745
|
-
* Scan the swap region and mount
|
|
10745
|
+
* Scan the swap region and mount islands for matching elements.
|
|
10746
10746
|
*
|
|
10747
10747
|
* @example
|
|
10748
10748
|
* kernel.scan();
|
|
10749
10749
|
*/
|
|
10750
10750
|
scan() {
|
|
10751
|
-
scanAndMount(state, emit, resolved.swapSelector,
|
|
10751
|
+
scanAndMount(state, emit, resolved.swapSelector, islandRouteContext(state.currentUrl));
|
|
10752
10752
|
},
|
|
10753
10753
|
/**
|
|
10754
10754
|
* Tear down router listeners, dispose all instances, reset boot state.
|
|
@@ -10767,7 +10767,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10767
10767
|
}
|
|
10768
10768
|
/**
|
|
10769
10769
|
* Builds the shared kernel from the plugin context, stores it on `ctx.state`,
|
|
10770
|
-
* and runs its init step (validate config, register config.
|
|
10770
|
+
* and runs its init step (validate config, register config.islands, seed
|
|
10771
10771
|
* currentUrl). Captures the OPTIONAL `data` reader when the `data` plugin is
|
|
10772
10772
|
* composed (enabling client DATA navigation) — resolved by instance via
|
|
10773
10773
|
* `ctx.require(dataPlugin)`, guarded by `ctx.has("data")` so `data` stays optional
|
|
@@ -10835,10 +10835,10 @@ function disposeSpa() {
|
|
|
10835
10835
|
/**
|
|
10836
10836
|
* @file `lazyEmbed` island — activates the static embed facades emitted by the
|
|
10837
10837
|
* content pipeline's `::embed` directive (pipeline/embed.ts). Mounts on every
|
|
10838
|
-
* `[data-
|
|
10838
|
+
* `[data-island="lazy-embed"]` figure; a click on the facade's button swaps
|
|
10839
10839
|
* it for the real `<iframe loading="lazy">`. Until that click the embedded
|
|
10840
10840
|
* document costs the page nothing — no request, no third-party JS, no
|
|
10841
|
-
* scroll-jacking. Register it in `pluginConfigs.spa.
|
|
10841
|
+
* scroll-jacking. Register it in `pluginConfigs.spa.islands`; all visual
|
|
10842
10842
|
* chrome (`.lazy-embed*` classes) is consumer CSS.
|
|
10843
10843
|
*/
|
|
10844
10844
|
/** CSS class on the injected `<iframe>` (consumer CSS sizes it). */
|
|
@@ -10891,7 +10891,7 @@ function onFacadeClick(event) {
|
|
|
10891
10891
|
* Lazy-embed island: facade button click → real `<iframe loading="lazy">`.
|
|
10892
10892
|
* The companion of the content pipeline's `::embed` directive.
|
|
10893
10893
|
*/
|
|
10894
|
-
const lazyEmbed =
|
|
10894
|
+
const lazyEmbed = createIsland("lazy-embed", {
|
|
10895
10895
|
/**
|
|
10896
10896
|
* Bind the activation click handler when a facade mounts.
|
|
10897
10897
|
*
|
|
@@ -10917,18 +10917,18 @@ const lazyEmbed = createComponent("lazy-embed", {
|
|
|
10917
10917
|
//#region src/plugins/spa/index.ts
|
|
10918
10918
|
/**
|
|
10919
10919
|
* @file spa — Complex Plugin (WIRING ONLY, ≤30 lines). All logic lives in the
|
|
10920
|
-
* domain files (kernel/router/head/progress/
|
|
10920
|
+
* domain files (kernel/router/head/progress/islands/lifecycle); index wires.
|
|
10921
10921
|
*
|
|
10922
10922
|
* Depends: router, head.
|
|
10923
|
-
* Emits: spa:navigate, spa:navigated, spa:
|
|
10923
|
+
* Emits: spa:navigate, spa:navigated, spa:island-mount, spa:island-unmount.
|
|
10924
10924
|
* @see README.md
|
|
10925
10925
|
*/
|
|
10926
10926
|
/**
|
|
10927
10927
|
* SPA plugin — progressive client-side navigation layered over the static site:
|
|
10928
10928
|
* swaps a page region on navigation, with an optional progress bar and View
|
|
10929
|
-
* Transitions. Register interactive islands with {@link
|
|
10930
|
-
* on router and head; emits `spa:navigate`, `spa:navigated`, `spa:
|
|
10931
|
-
* and `spa:
|
|
10929
|
+
* Transitions. Register interactive islands with {@link createIsland}. Depends
|
|
10930
|
+
* on router and head; emits `spa:navigate`, `spa:navigated`, `spa:island-mount`,
|
|
10931
|
+
* and `spa:island-unmount`.
|
|
10932
10932
|
*
|
|
10933
10933
|
* @example Enable view transitions and a custom swap region
|
|
10934
10934
|
* ```ts
|
|
@@ -11911,8 +11911,8 @@ function EmbedFacadeButton(props) {
|
|
|
11911
11911
|
//#region src/plugins/content/pipeline/embed.ts
|
|
11912
11912
|
/** CSS class on the `<figure>` facade wrapping each embed. */
|
|
11913
11913
|
const EMBED_FIGURE_CLASS = "lazy-embed";
|
|
11914
|
-
/** `data-
|
|
11915
|
-
const
|
|
11914
|
+
/** `data-island` name binding the facade to the `lazyEmbed` SPA island. */
|
|
11915
|
+
const EMBED_ISLAND_NAME = "lazy-embed";
|
|
11916
11916
|
/**
|
|
11917
11917
|
* Type guard for an `::embed` leaf directive.
|
|
11918
11918
|
*
|
|
@@ -12022,7 +12022,7 @@ function collectAttributes$1(attributes) {
|
|
|
12022
12022
|
* ```
|
|
12023
12023
|
*/
|
|
12024
12024
|
function embedFacadeHtml(facade, props, dimensions) {
|
|
12025
|
-
return `<figure class="${EMBED_FIGURE_CLASS}" data-
|
|
12025
|
+
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;"` : ""}>${renderToString(h(facade, props))}</figure>`;
|
|
12026
12026
|
}
|
|
12027
12027
|
/**
|
|
12028
12028
|
* Normalize the provider's `embed` config value (`boolean | options`) to a plain
|
|
@@ -12135,7 +12135,7 @@ function GalleryTrack(props) {
|
|
|
12135
12135
|
*
|
|
12136
12136
|
* Rewrites `::gallery{src="./images/dir/" caption="…"}` leaf directives into a
|
|
12137
12137
|
* static swipeable image set at the mdast stage (BEFORE the remark-rehype bridge):
|
|
12138
|
-
* a framework-owned `<div class="gallery" data-
|
|
12138
|
+
* a framework-owned `<div class="gallery" data-island="gallery">` carrying the
|
|
12139
12139
|
* island hook, wrapping inner content rendered (at build time, to static markup)
|
|
12140
12140
|
* by a Preact component — the built-in {@link GalleryTrack} by default, or a
|
|
12141
12141
|
* consumer component via `gallery.component`.
|
|
@@ -12147,12 +12147,12 @@ function GalleryTrack(props) {
|
|
|
12147
12147
|
* transform reads `<contentDir>/<slug>/<src>` from disk, sorts its images, and
|
|
12148
12148
|
* resolves each to its shared `/<slug>/<dir>/<file>` URL (identical from every
|
|
12149
12149
|
* locale page, mirroring co-located images). The companion gallery SPA island
|
|
12150
|
-
* (consumer-provided) wires swipe/keyboard/lightbox on `[data-
|
|
12150
|
+
* (consumer-provided) wires swipe/keyboard/lightbox on `[data-island="gallery"]`.
|
|
12151
12151
|
*/
|
|
12152
12152
|
/** CSS class on the `<div>` wrapping each gallery. */
|
|
12153
12153
|
const GALLERY_WRAPPER_CLASS = "gallery";
|
|
12154
|
-
/** `data-
|
|
12155
|
-
const
|
|
12154
|
+
/** `data-island` name binding the gallery to its SPA island. */
|
|
12155
|
+
const GALLERY_ISLAND_NAME = "gallery";
|
|
12156
12156
|
/** Image file extensions a gallery folder expands over. */
|
|
12157
12157
|
const IMAGE_EXTENSIONS = new Set([
|
|
12158
12158
|
".webp",
|
|
@@ -12245,7 +12245,7 @@ function collectAttributes(attributes) {
|
|
|
12245
12245
|
}
|
|
12246
12246
|
/**
|
|
12247
12247
|
* Build the static gallery HTML for one directive: the framework-owned `<div>`
|
|
12248
|
-
* (island hook in `data-
|
|
12248
|
+
* (island hook in `data-island`) wrapping the component's inner content, SSR'd
|
|
12249
12249
|
* to static markup.
|
|
12250
12250
|
*
|
|
12251
12251
|
* @param component - The gallery component (default {@link GalleryTrack}).
|
|
@@ -12259,7 +12259,7 @@ function collectAttributes(attributes) {
|
|
|
12259
12259
|
* ```
|
|
12260
12260
|
*/
|
|
12261
12261
|
function galleryHtml(component, slides, caption, attributes) {
|
|
12262
|
-
return `<div class="${GALLERY_WRAPPER_CLASS}" data-
|
|
12262
|
+
return `<div class="${GALLERY_WRAPPER_CLASS}" data-island="${GALLERY_ISLAND_NAME}">${renderToString(h(component, {
|
|
12263
12263
|
slides,
|
|
12264
12264
|
caption,
|
|
12265
12265
|
attributes
|
|
@@ -13176,4 +13176,4 @@ const createApp = core.createApp;
|
|
|
13176
13176
|
*/
|
|
13177
13177
|
const createPlugin = core.createPlugin;
|
|
13178
13178
|
//#endregion
|
|
13179
|
-
export { types_exports as Build, types_exports$1 as Cli, types_exports$2 as Content, types_exports$3 as Data, types_exports$4 as Deploy, EmbedFacadeButton, GalleryTrack, types_exports$5 as Head, types_exports$6 as Router, types_exports$7 as Spa, browserEnv, buildArticleHead, buildPlugin, canonical, cliPlugin, cloudflareBindings, contentPlugin, createApp,
|
|
13179
|
+
export { types_exports as Build, types_exports$1 as Cli, types_exports$2 as Content, types_exports$3 as Data, types_exports$4 as Deploy, EmbedFacadeButton, GalleryTrack, types_exports$5 as Head, types_exports$6 as Router, types_exports$7 as Spa, browserEnv, buildArticleHead, buildPlugin, canonical, cliPlugin, cloudflareBindings, contentPlugin, createApp, createIsland, createPlugin, createUrls, dataPlugin, defineRoutes, deployPlugin, dotenv, envPlugin, feedLink, fileSystemContent, headPlugin, hreflang, i18nPlugin, jsonLd, lazyEmbed, logPlugin, meta, og, processEnv, route, routerPlugin, sitePlugin, spaPlugin, twitter };
|