@moku-labs/web 1.15.0 → 1.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser.d.mts +216 -26
- package/dist/browser.mjs +445 -84
- package/dist/index.cjs +473 -84
- package/dist/index.d.cts +249 -27
- package/dist/index.d.mts +249 -27
- package/dist/index.mjs +473 -84
- package/dist/{render-DLZEOe4M.cjs → render-KdufA3_b.cjs} +23 -0
- package/dist/{render-BNe0s7fr.mjs → render-UO4nimWr.mjs} +23 -1
- package/dist/testing.d.mts +383 -0
- package/dist/testing.mjs +847 -0
- package/package.json +7 -1
package/dist/index.cjs
CHANGED
|
@@ -8024,6 +8024,34 @@ function createApi$1(ctx) {
|
|
|
8024
8024
|
return result;
|
|
8025
8025
|
},
|
|
8026
8026
|
/**
|
|
8027
|
+
* Incremental dev rebuild from a set of changed paths — the fast counterpart to
|
|
8028
|
+
* `build()` for an external dev driver (e.g. the worker's `dev({ onChange })`). Skips
|
|
8029
|
+
* the destructive clean, scopes the rebuild to `changes`, and applies the same dev
|
|
8030
|
+
* overrides `serve()` uses (minify off; OG/sitemap/feeds off unless re-enabled). Renders
|
|
8031
|
+
* no header and skips the not-found assertion — the driver owns the dev TUI and the cold
|
|
8032
|
+
* build already asserted. Live per-phase progress still streams via the build hooks (as
|
|
8033
|
+
* the cold `build()` does today).
|
|
8034
|
+
*
|
|
8035
|
+
* @param changes - The paths changed since the last build (incremental rebuild hint).
|
|
8036
|
+
* @param options - Optional per-session dev-output opt-ins (default all off).
|
|
8037
|
+
* @returns The rebuild summary (`outDir`, `pageCount`, `durationMs`).
|
|
8038
|
+
* @throws {Error} When the underlying incremental build fails.
|
|
8039
|
+
* @example
|
|
8040
|
+
* await api.update(["src/islands/board.ts"]);
|
|
8041
|
+
*/
|
|
8042
|
+
update(changes, options = {}) {
|
|
8043
|
+
const overrides = devBuildOverrides({
|
|
8044
|
+
og: options.og ?? false,
|
|
8045
|
+
sitemap: options.sitemap ?? false,
|
|
8046
|
+
feeds: options.feeds ?? false
|
|
8047
|
+
});
|
|
8048
|
+
return ctx.require(buildPlugin).run({
|
|
8049
|
+
skipClean: true,
|
|
8050
|
+
overrides,
|
|
8051
|
+
changed: changes
|
|
8052
|
+
});
|
|
8053
|
+
},
|
|
8054
|
+
/**
|
|
8027
8055
|
* Dev loop: build once, serve `dist/` in-process (live-reload injected), watch
|
|
8028
8056
|
* `watchDirs`, debounced + incremental rebuild + reload. For a fast rebuild the dev
|
|
8029
8057
|
* build disables minification + expensive, NON-navigational outputs (feeds / sitemap /
|
|
@@ -9128,6 +9156,18 @@ function createApi(ctx) {
|
|
|
9128
9156
|
*/
|
|
9129
9157
|
current() {
|
|
9130
9158
|
return ctx.state.currentUrl;
|
|
9159
|
+
},
|
|
9160
|
+
/**
|
|
9161
|
+
* Resolve a registered island's api by name (the cross-island seam). Returns
|
|
9162
|
+
* `undefined` when no provider with that name is currently registered.
|
|
9163
|
+
*
|
|
9164
|
+
* @param name - The provider island's component name.
|
|
9165
|
+
* @returns The provider's api, or `undefined`.
|
|
9166
|
+
* @example
|
|
9167
|
+
* app.spa.component("lightbox");
|
|
9168
|
+
*/
|
|
9169
|
+
component(name) {
|
|
9170
|
+
return ctx.state.componentApis.get(name);
|
|
9131
9171
|
}
|
|
9132
9172
|
};
|
|
9133
9173
|
}
|
|
@@ -9168,6 +9208,15 @@ const COMPONENT_HOOK_NAMES = [
|
|
|
9168
9208
|
const ERROR_PREFIX$2 = "[web]";
|
|
9169
9209
|
/** The set of legal hook names, frozen for O(1) membership checks. */
|
|
9170
9210
|
const HOOK_NAME_SET = new Set(COMPONENT_HOOK_NAMES);
|
|
9211
|
+
/** The spec-only keys that select the plugin-mirror form of {@link createComponent}. */
|
|
9212
|
+
const SPEC_KEYS = new Set([
|
|
9213
|
+
"state",
|
|
9214
|
+
"render",
|
|
9215
|
+
"events",
|
|
9216
|
+
"api"
|
|
9217
|
+
]);
|
|
9218
|
+
/** Synchronous re-entrancy cap for the render scheduler (a render that calls `ctx.flush`). */
|
|
9219
|
+
const MAX_RENDER_DEPTH = 25;
|
|
9171
9220
|
/**
|
|
9172
9221
|
* No-op link builder for the {@link EMPTY_ROUTE} slice (used when no route matched).
|
|
9173
9222
|
*
|
|
@@ -9186,40 +9235,346 @@ const EMPTY_ROUTE = {
|
|
|
9186
9235
|
url: noUrl
|
|
9187
9236
|
};
|
|
9188
9237
|
/**
|
|
9238
|
+
* No-op placeholder for an instance's `flush` slot until the real one is bound at mount.
|
|
9239
|
+
*
|
|
9240
|
+
* @example
|
|
9241
|
+
* const instance = { flush: noop };
|
|
9242
|
+
*/
|
|
9243
|
+
function noop() {}
|
|
9244
|
+
/** Cached promise for the lazy `./render` chunk (loaded at most once per module). */
|
|
9245
|
+
let renderChunk;
|
|
9246
|
+
/** The resolved VNode committer once the chunk loads (undefined until then). */
|
|
9247
|
+
let commitVNodeFunction;
|
|
9248
|
+
/**
|
|
9249
|
+
* Load the lazy `./render` chunk (once) and cache its `commitVNode` for synchronous
|
|
9250
|
+
* use by later renders. Awaited by a component's `mountPromise` so the test harness's
|
|
9251
|
+
* `settle()` can deterministically flush a VNode render.
|
|
9252
|
+
*
|
|
9253
|
+
* @returns A promise that resolves once `commitVNode` is available.
|
|
9254
|
+
* @example
|
|
9255
|
+
* await loadRenderChunk();
|
|
9256
|
+
*/
|
|
9257
|
+
async function loadRenderChunk() {
|
|
9258
|
+
renderChunk ??= Promise.resolve().then(() => require("./render-KdufA3_b.cjs"));
|
|
9259
|
+
commitVNodeFunction = (await renderChunk).commitVNode;
|
|
9260
|
+
}
|
|
9261
|
+
/**
|
|
9262
|
+
* Commit a {@link RenderResult} into a host: `string` → `innerHTML`, `Node` →
|
|
9263
|
+
* `replaceChildren`, `void`/`undefined` → no-op (the render mutated the DOM itself), and
|
|
9264
|
+
* a Preact `VNode` → committed through the lazy gate (loading it on demand if needed).
|
|
9265
|
+
*
|
|
9266
|
+
* @param host - The island host element to render into.
|
|
9267
|
+
* @param result - The value returned by the component's `render`.
|
|
9268
|
+
* @example
|
|
9269
|
+
* commitResult(host, h(View, { items }));
|
|
9270
|
+
*/
|
|
9271
|
+
function commitResult(host, result) {
|
|
9272
|
+
if (result === void 0) return;
|
|
9273
|
+
if (typeof result === "string") {
|
|
9274
|
+
host.innerHTML = result;
|
|
9275
|
+
return;
|
|
9276
|
+
}
|
|
9277
|
+
if (result instanceof Node) {
|
|
9278
|
+
host.replaceChildren(result);
|
|
9279
|
+
return;
|
|
9280
|
+
}
|
|
9281
|
+
const vnode = result;
|
|
9282
|
+
if (commitVNodeFunction) {
|
|
9283
|
+
commitVNodeFunction(vnode, host);
|
|
9284
|
+
return;
|
|
9285
|
+
}
|
|
9286
|
+
loadRenderChunk().then(() => commitVNodeFunction?.(vnode, host)).catch(() => {});
|
|
9287
|
+
}
|
|
9288
|
+
/**
|
|
9289
|
+
* Run a component's `render(state, ctx)` and commit the result now. Guards against
|
|
9290
|
+
* synchronous re-entrancy (a render that calls `ctx.flush`) with a depth cap.
|
|
9291
|
+
*
|
|
9292
|
+
* @param instance - The instance to render.
|
|
9293
|
+
* @throws {Error} When the synchronous render depth exceeds {@link MAX_RENDER_DEPTH}.
|
|
9294
|
+
* @example
|
|
9295
|
+
* runRender(instance);
|
|
9296
|
+
*/
|
|
9297
|
+
function runRender(instance) {
|
|
9298
|
+
const render = instance.def.spec?.render;
|
|
9299
|
+
if (!render) return;
|
|
9300
|
+
if (instance.renderDepth > MAX_RENDER_DEPTH) throw new Error(`${ERROR_PREFIX$2} component "${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)`);
|
|
9301
|
+
instance.renderDepth += 1;
|
|
9302
|
+
try {
|
|
9303
|
+
commitResult(instance.el, render(instance.state ?? {}, instance.ctx));
|
|
9304
|
+
} finally {
|
|
9305
|
+
instance.renderDepth -= 1;
|
|
9306
|
+
}
|
|
9307
|
+
}
|
|
9308
|
+
/**
|
|
9309
|
+
* Schedule a microtask-batched render for an instance (no-op when it has no `render`).
|
|
9310
|
+
* Multiple `ctx.set` calls in the same tick coalesce into a single render.
|
|
9311
|
+
*
|
|
9312
|
+
* @param instance - The instance to schedule a render for.
|
|
9313
|
+
* @example
|
|
9314
|
+
* scheduleRender(instance);
|
|
9315
|
+
*/
|
|
9316
|
+
function scheduleRender(instance) {
|
|
9317
|
+
if (!instance.def.spec?.render || instance.renderScheduled) return;
|
|
9318
|
+
instance.renderScheduled = true;
|
|
9319
|
+
queueMicrotask(() => {
|
|
9320
|
+
if (!instance.renderScheduled) return;
|
|
9321
|
+
instance.renderScheduled = false;
|
|
9322
|
+
runRender(instance);
|
|
9323
|
+
});
|
|
9324
|
+
}
|
|
9325
|
+
/**
|
|
9326
|
+
* Build the single per-instance {@link ComponentContext} reused by every hook, event
|
|
9327
|
+
* handler, and render. Route fields (`params`/`meta`/`locale`/`url`) and `data` read
|
|
9328
|
+
* through the instance so a navigation update is reflected without rebuilding the ctx;
|
|
9329
|
+
* `state`/`set`/`flush`/`cleanup`/`component` are bound to the instance + plugin state.
|
|
9330
|
+
*
|
|
9331
|
+
* @param state - The plugin state (for the cross-island `component` resolver).
|
|
9332
|
+
* @param instance - The instance the context is bound to.
|
|
9333
|
+
* @returns The instance-bound context.
|
|
9334
|
+
* @example
|
|
9335
|
+
* instance.ctx = buildContext(state, instance);
|
|
9336
|
+
*/
|
|
9337
|
+
function buildContext(state, instance) {
|
|
9338
|
+
return {
|
|
9339
|
+
el: instance.el,
|
|
9340
|
+
/**
|
|
9341
|
+
* The current page data payload (live; updated across navigations).
|
|
9342
|
+
*
|
|
9343
|
+
* @returns The page data.
|
|
9344
|
+
* @example
|
|
9345
|
+
* ctx.data;
|
|
9346
|
+
*/
|
|
9347
|
+
get data() {
|
|
9348
|
+
return instance.data;
|
|
9349
|
+
},
|
|
9350
|
+
/**
|
|
9351
|
+
* The matched route's path params (live; updated across navigations).
|
|
9352
|
+
*
|
|
9353
|
+
* @returns The route params.
|
|
9354
|
+
* @example
|
|
9355
|
+
* ctx.params.id;
|
|
9356
|
+
*/
|
|
9357
|
+
get params() {
|
|
9358
|
+
return instance.route.params;
|
|
9359
|
+
},
|
|
9360
|
+
/**
|
|
9361
|
+
* The matched route's `.meta()` bag (live; updated across navigations).
|
|
9362
|
+
*
|
|
9363
|
+
* @returns The route meta.
|
|
9364
|
+
* @example
|
|
9365
|
+
* ctx.meta.focus;
|
|
9366
|
+
*/
|
|
9367
|
+
get meta() {
|
|
9368
|
+
return instance.route.meta;
|
|
9369
|
+
},
|
|
9370
|
+
/**
|
|
9371
|
+
* The active locale for the current route (live; updated across navigations).
|
|
9372
|
+
*
|
|
9373
|
+
* @returns The locale code.
|
|
9374
|
+
* @example
|
|
9375
|
+
* ctx.locale;
|
|
9376
|
+
*/
|
|
9377
|
+
get locale() {
|
|
9378
|
+
return instance.route.locale;
|
|
9379
|
+
},
|
|
9380
|
+
/**
|
|
9381
|
+
* The named-route link builder for the current route.
|
|
9382
|
+
*
|
|
9383
|
+
* @returns The link builder.
|
|
9384
|
+
* @example
|
|
9385
|
+
* ctx.url("board", { id });
|
|
9386
|
+
*/
|
|
9387
|
+
get url() {
|
|
9388
|
+
return instance.route.url;
|
|
9389
|
+
},
|
|
9390
|
+
/**
|
|
9391
|
+
* The live per-instance state (`undefined` for legacy hooks-only islands).
|
|
9392
|
+
*
|
|
9393
|
+
* @returns The current state.
|
|
9394
|
+
* @example
|
|
9395
|
+
* ctx.state.count;
|
|
9396
|
+
*/
|
|
9397
|
+
get state() {
|
|
9398
|
+
return instance.state;
|
|
9399
|
+
},
|
|
9400
|
+
/**
|
|
9401
|
+
* Merge a patch into the per-instance state and schedule one batched render.
|
|
9402
|
+
*
|
|
9403
|
+
* @param patch - A partial state object, or an updater `(prev) => partial`.
|
|
9404
|
+
* @example
|
|
9405
|
+
* ctx.set(prev => ({ count: prev.count + 1 }));
|
|
9406
|
+
*/
|
|
9407
|
+
set(patch) {
|
|
9408
|
+
const previous = instance.state ?? {};
|
|
9409
|
+
const next = typeof patch === "function" ? patch(previous) : patch;
|
|
9410
|
+
instance.state = Object.assign({}, previous, next);
|
|
9411
|
+
scheduleRender(instance);
|
|
9412
|
+
},
|
|
9413
|
+
/**
|
|
9414
|
+
* Force a synchronous render now (drains any pending scheduled render).
|
|
9415
|
+
*
|
|
9416
|
+
* @example
|
|
9417
|
+
* ctx.flush();
|
|
9418
|
+
*/
|
|
9419
|
+
flush() {
|
|
9420
|
+
instance.flush();
|
|
9421
|
+
},
|
|
9422
|
+
/**
|
|
9423
|
+
* Register a disposer run on destroy (subscriptions, timers, manual listeners).
|
|
9424
|
+
*
|
|
9425
|
+
* @param dispose - The teardown function.
|
|
9426
|
+
* @example
|
|
9427
|
+
* ctx.cleanup(off);
|
|
9428
|
+
*/
|
|
9429
|
+
cleanup(dispose) {
|
|
9430
|
+
instance.cleanups.push(dispose);
|
|
9431
|
+
},
|
|
9432
|
+
/**
|
|
9433
|
+
* Resolve another island's registered api by name (`undefined` when absent).
|
|
9434
|
+
*
|
|
9435
|
+
* @param name - The provider island's component name.
|
|
9436
|
+
* @returns The provider's api, or `undefined`.
|
|
9437
|
+
* @example
|
|
9438
|
+
* ctx.component("lightbox");
|
|
9439
|
+
*/
|
|
9440
|
+
component(name) {
|
|
9441
|
+
return state.componentApis.get(name);
|
|
9442
|
+
}
|
|
9443
|
+
};
|
|
9444
|
+
}
|
|
9445
|
+
/**
|
|
9446
|
+
* Resolve the element a delegated handler should receive for an event: the host for a
|
|
9447
|
+
* host-level binding (empty selector), else the nearest ancestor of `event.target`
|
|
9448
|
+
* matching the selector that is still inside the host.
|
|
9449
|
+
*
|
|
9450
|
+
* @param host - The island host element.
|
|
9451
|
+
* @param event - The dispatched DOM event.
|
|
9452
|
+
* @param selector - The key's selector (empty string → host-level).
|
|
9453
|
+
* @returns The matched element, or `undefined` when nothing matches inside the host.
|
|
9454
|
+
* @example
|
|
9455
|
+
* const target = matchTarget(host, event, "[data-action]");
|
|
9456
|
+
*/
|
|
9457
|
+
function matchTarget(host, event, selector) {
|
|
9458
|
+
if (selector === "") return host;
|
|
9459
|
+
const target = event.target;
|
|
9460
|
+
if (!(target instanceof Element)) return void 0;
|
|
9461
|
+
const matched = target.closest(selector);
|
|
9462
|
+
return matched && host.contains(matched) ? matched : void 0;
|
|
9463
|
+
}
|
|
9464
|
+
/**
|
|
9465
|
+
* Attach a component's declarative `events` map: one real listener per event TYPE on
|
|
9466
|
+
* the host (dispatch walks `closest(selector)` for each registered selector), each
|
|
9467
|
+
* removed via the instance's cleanup registry on destroy.
|
|
9468
|
+
*
|
|
9469
|
+
* @param instance - The instance whose host the listeners attach to.
|
|
9470
|
+
* @param events - The declarative `{ "<type> <selector>": handler }` map.
|
|
9471
|
+
* @throws {Error} When a key has no event type.
|
|
9472
|
+
* @example
|
|
9473
|
+
* attachEvents(instance, { "click [data-action]": (ctx, e, el) => {} });
|
|
9474
|
+
*/
|
|
9475
|
+
function attachEvents(instance, events) {
|
|
9476
|
+
const host = instance.el;
|
|
9477
|
+
const byType = /* @__PURE__ */ new Map();
|
|
9478
|
+
for (const [key, handler] of Object.entries(events)) {
|
|
9479
|
+
const space = key.indexOf(" ");
|
|
9480
|
+
const type = (space === -1 ? key : key.slice(0, space)).trim();
|
|
9481
|
+
const selector = space === -1 ? "" : key.slice(space + 1).trim();
|
|
9482
|
+
if (type === "") throw new Error(`${ERROR_PREFIX$2} component "${instance.def.name}" event key must start with an event type: "${key}"\n → use "<type>" or "<type> <selector>" (e.g. "click [data-action]")`);
|
|
9483
|
+
const list = byType.get(type) ?? [];
|
|
9484
|
+
list.push({
|
|
9485
|
+
selector,
|
|
9486
|
+
handler
|
|
9487
|
+
});
|
|
9488
|
+
byType.set(type, list);
|
|
9489
|
+
}
|
|
9490
|
+
for (const [type, handlers] of byType) {
|
|
9491
|
+
const listener = (event) => {
|
|
9492
|
+
for (const { selector, handler } of handlers) {
|
|
9493
|
+
const target = matchTarget(host, event, selector);
|
|
9494
|
+
if (target) handler(instance.ctx, event, target);
|
|
9495
|
+
}
|
|
9496
|
+
};
|
|
9497
|
+
host.addEventListener(type, listener);
|
|
9498
|
+
instance.cleanups.push(() => host.removeEventListener(type, listener));
|
|
9499
|
+
}
|
|
9500
|
+
}
|
|
9501
|
+
/**
|
|
9189
9502
|
* Validate a single hook entry: its key must be a known hook name and its value
|
|
9190
9503
|
* must be a function. Throws fail-fast on the first violation.
|
|
9191
9504
|
*
|
|
9192
9505
|
* @param componentName - The owning component name (for error messages).
|
|
9193
|
-
* @param
|
|
9506
|
+
* @param source - The raw authoring object being validated.
|
|
9194
9507
|
* @param key - The hook key to validate.
|
|
9195
9508
|
* @throws {Error} If `key` is not in `COMPONENT_HOOK_NAMES`.
|
|
9196
9509
|
* @throws {TypeError} If the hook value is not a function.
|
|
9197
9510
|
* @example
|
|
9198
|
-
* validateHookEntry("counter",
|
|
9511
|
+
* validateHookEntry("counter", source, "onMount");
|
|
9199
9512
|
*/
|
|
9200
|
-
function validateHookEntry(componentName,
|
|
9201
|
-
if (!HOOK_NAME_SET.has(key)) throw new Error(`${ERROR_PREFIX$2} unknown component hook "${key}" on "${componentName}"\n → valid hooks: ${COMPONENT_HOOK_NAMES.join(", ")}`);
|
|
9202
|
-
if (typeof
|
|
9513
|
+
function validateHookEntry(componentName, source, key) {
|
|
9514
|
+
if (!HOOK_NAME_SET.has(key)) throw new Error(`${ERROR_PREFIX$2} unknown component hook "${key}" on "${componentName}"\n → valid hooks: ${COMPONENT_HOOK_NAMES.join(", ")}\n → spec keys: state, render, events, api`);
|
|
9515
|
+
if (typeof source[key] !== "function") throw new TypeError(`${ERROR_PREFIX$2} component hook "${key}" on "${componentName}" must be a function\n → provide a function or omit the hook`);
|
|
9203
9516
|
}
|
|
9204
9517
|
/**
|
|
9205
|
-
*
|
|
9206
|
-
*
|
|
9207
|
-
*
|
|
9518
|
+
* Validate the spec extras (`state`/`render`/`api` must be functions; `events` must be
|
|
9519
|
+
* a plain object of functions). Throws fail-fast on the first violation.
|
|
9520
|
+
*
|
|
9521
|
+
* @param componentName - The owning component name (for error messages).
|
|
9522
|
+
* @param extras - The partitioned spec extras to validate.
|
|
9523
|
+
* @throws {TypeError} If a present extra has the wrong shape.
|
|
9524
|
+
* @example
|
|
9525
|
+
* validateSpecExtras("board", { state: () => ({}) });
|
|
9526
|
+
*/
|
|
9527
|
+
function validateSpecExtras(componentName, extras) {
|
|
9528
|
+
for (const key of [
|
|
9529
|
+
"state",
|
|
9530
|
+
"render",
|
|
9531
|
+
"api"
|
|
9532
|
+
]) if (extras[key] !== void 0 && typeof extras[key] !== "function") throw new TypeError(`${ERROR_PREFIX$2} component "${key}" on "${componentName}" must be a function\n → provide a function or omit it`);
|
|
9533
|
+
if (extras.events !== void 0) {
|
|
9534
|
+
const events = extras.events;
|
|
9535
|
+
if (!(typeof events === "object")) throw new TypeError(`${ERROR_PREFIX$2} component "events" on "${componentName}" must be an object of handlers`);
|
|
9536
|
+
for (const [key, handler] of Object.entries(events)) if (typeof handler !== "function") throw new TypeError(`${ERROR_PREFIX$2} component event "${key}" on "${componentName}" must be a function`);
|
|
9537
|
+
}
|
|
9538
|
+
}
|
|
9539
|
+
/**
|
|
9540
|
+
* Create a validated component definition. Accepts either the legacy hooks-only form
|
|
9541
|
+
* (`createComponent("counter", { onMount() {} })`) or the plugin-mirror spec form
|
|
9542
|
+
* (`createComponent("board", { state, render, events, api, ...hooks })`). Spec-only
|
|
9543
|
+
* keys (`state`/`render`/`events`/`api`) are partitioned out before hook-name
|
|
9544
|
+
* validation, so a real typo (e.g. `onMout`) still throws immediately while the spec
|
|
9545
|
+
* keys are accepted.
|
|
9208
9546
|
*
|
|
9209
9547
|
* @param name - Unique component name.
|
|
9210
|
-
* @param
|
|
9548
|
+
* @param spec - Lifecycle hooks, or the `{ state, render, events, api, ...hooks }` spec.
|
|
9211
9549
|
* @returns A `ComponentDef` ready to `register`.
|
|
9212
|
-
* @throws {Error} If `name` is empty,
|
|
9213
|
-
*
|
|
9550
|
+
* @throws {Error} If `name` is empty, a hook key is unknown, or an extra/hook value has the wrong shape.
|
|
9551
|
+
* @example
|
|
9552
|
+
* const counter = createComponent("counter", { onMount({ el }) { el.textContent = "0"; } });
|
|
9214
9553
|
* @example
|
|
9215
|
-
* const
|
|
9216
|
-
*
|
|
9554
|
+
* const list = createComponent<{ items: string[] }>("list", {
|
|
9555
|
+
* state: () => ({ items: [] }),
|
|
9556
|
+
* render: (s) => h(List, { items: s.items })
|
|
9217
9557
|
* });
|
|
9218
9558
|
*/
|
|
9219
|
-
function createComponent(name,
|
|
9559
|
+
function createComponent(name, spec) {
|
|
9220
9560
|
if (name.trim() === "") throw new Error(`${ERROR_PREFIX$2} component name must be a non-empty string\n → pass a unique name to createComponent("name", hooks)`);
|
|
9221
|
-
|
|
9222
|
-
|
|
9561
|
+
const source = spec;
|
|
9562
|
+
const hooks = {};
|
|
9563
|
+
const extras = {};
|
|
9564
|
+
for (const key of Object.keys(source)) {
|
|
9565
|
+
if (SPEC_KEYS.has(key)) {
|
|
9566
|
+
extras[key] = source[key];
|
|
9567
|
+
continue;
|
|
9568
|
+
}
|
|
9569
|
+
validateHookEntry(name, source, key);
|
|
9570
|
+
hooks[key] = source[key];
|
|
9571
|
+
}
|
|
9572
|
+
validateSpecExtras(name, extras);
|
|
9573
|
+
return Object.keys(extras).length > 0 ? {
|
|
9574
|
+
name,
|
|
9575
|
+
hooks,
|
|
9576
|
+
spec: extras
|
|
9577
|
+
} : {
|
|
9223
9578
|
name,
|
|
9224
9579
|
hooks
|
|
9225
9580
|
};
|
|
@@ -9243,64 +9598,53 @@ function extractPageData(doc) {
|
|
|
9243
9598
|
}
|
|
9244
9599
|
}
|
|
9245
9600
|
/**
|
|
9246
|
-
*
|
|
9601
|
+
* Read the current page data, or `{}` in a headless (non-browser) context.
|
|
9247
9602
|
*
|
|
9248
|
-
* @
|
|
9249
|
-
* @param element - The element the instance binds to.
|
|
9250
|
-
* @param persistent - Whether the instance survives navigation.
|
|
9251
|
-
* @returns The constructed (not-yet-mounted) instance.
|
|
9603
|
+
* @returns The current page data payload.
|
|
9252
9604
|
* @example
|
|
9253
|
-
* const
|
|
9605
|
+
* const data = currentPageData();
|
|
9254
9606
|
*/
|
|
9255
|
-
function
|
|
9256
|
-
return {
|
|
9257
|
-
def: definition,
|
|
9258
|
-
el: element,
|
|
9259
|
-
persistent
|
|
9260
|
-
};
|
|
9607
|
+
function currentPageData() {
|
|
9608
|
+
return typeof document === "undefined" ? {} : extractPageData(document);
|
|
9261
9609
|
}
|
|
9262
9610
|
/**
|
|
9263
|
-
* Invokes a single lifecycle hook on an instance with its
|
|
9264
|
-
*
|
|
9611
|
+
* Invokes a single lifecycle hook on an instance with its bound context. Missing
|
|
9612
|
+
* hooks are skipped silently.
|
|
9265
9613
|
*
|
|
9266
9614
|
* @param instance - The instance whose hook to run.
|
|
9267
9615
|
* @param hook - The hook name to invoke.
|
|
9268
|
-
* @param ctx - The component context passed to the hook.
|
|
9269
9616
|
* @example
|
|
9270
|
-
* runHook(instance, "
|
|
9617
|
+
* runHook(instance, "onDestroy");
|
|
9271
9618
|
*/
|
|
9272
|
-
function runHook(instance, hook
|
|
9273
|
-
instance.def.hooks[hook]?.(ctx);
|
|
9619
|
+
function runHook(instance, hook) {
|
|
9620
|
+
instance.def.hooks[hook]?.(instance.ctx);
|
|
9274
9621
|
}
|
|
9275
9622
|
/**
|
|
9276
|
-
*
|
|
9277
|
-
*
|
|
9278
|
-
* when no route is supplied (headless, tests, public `scan()`).
|
|
9623
|
+
* Run an instance's registered cleanup disposers (LIFO) and unregister its api. Each
|
|
9624
|
+
* disposer runs in isolation so a throwing one never strands the others during teardown.
|
|
9279
9625
|
*
|
|
9280
|
-
* @param
|
|
9281
|
-
* @param
|
|
9282
|
-
* @param route - The matched-route slice for the current URL.
|
|
9283
|
-
* @returns The hook context.
|
|
9626
|
+
* @param state - The plugin state (for the api registry).
|
|
9627
|
+
* @param instance - The instance being disposed.
|
|
9284
9628
|
* @example
|
|
9285
|
-
*
|
|
9629
|
+
* disposeInstance(state, instance);
|
|
9286
9630
|
*/
|
|
9287
|
-
function
|
|
9288
|
-
|
|
9289
|
-
|
|
9290
|
-
|
|
9291
|
-
|
|
9292
|
-
|
|
9293
|
-
|
|
9294
|
-
url: route.url
|
|
9295
|
-
};
|
|
9631
|
+
function disposeInstance(state, instance) {
|
|
9632
|
+
for (let index = instance.cleanups.length - 1; index >= 0; index -= 1) try {
|
|
9633
|
+
instance.cleanups[index]?.();
|
|
9634
|
+
} catch {}
|
|
9635
|
+
instance.cleanups.length = 0;
|
|
9636
|
+
instance.renderScheduled = false;
|
|
9637
|
+
if (instance.api !== void 0 && state.componentApis.get(instance.def.name) === instance.api) state.componentApis.delete(instance.def.name);
|
|
9296
9638
|
}
|
|
9297
9639
|
/**
|
|
9298
|
-
* Mounts a single `data-component` element: classifies persistent vs
|
|
9299
|
-
*
|
|
9300
|
-
*
|
|
9640
|
+
* Mounts a single `data-component` element: classifies persistent vs page-specific,
|
|
9641
|
+
* builds the instance + its bound context, initializes per-instance `state`, registers
|
|
9642
|
+
* its `api`, attaches declarative `events`, fires `onCreate` then `onMount` (capturing
|
|
9643
|
+
* an async `onMount` + render-chunk load as `mountPromise`), schedules the initial
|
|
9644
|
+
* render, records it, and emits `spa:component-mount`. No-ops if the element is already
|
|
9301
9645
|
* mounted, has no component name, or names an unregistered component.
|
|
9302
9646
|
*
|
|
9303
|
-
* @param state - The plugin state (registeredComponents + instances).
|
|
9647
|
+
* @param state - The plugin state (registeredComponents + instances + componentApis).
|
|
9304
9648
|
* @param emit - The event emitter for spa:component-mount.
|
|
9305
9649
|
* @param swapArea - The swap-region element, or null when none was found.
|
|
9306
9650
|
* @param data - The current page data payload.
|
|
@@ -9315,10 +9659,40 @@ function mountElement(state, emit, swapArea, data, element, route = EMPTY_ROUTE)
|
|
|
9315
9659
|
if (!name) return;
|
|
9316
9660
|
const definition = state.registeredComponents.get(name);
|
|
9317
9661
|
if (!definition) return;
|
|
9318
|
-
const instance =
|
|
9319
|
-
|
|
9320
|
-
|
|
9321
|
-
|
|
9662
|
+
const instance = {
|
|
9663
|
+
def: definition,
|
|
9664
|
+
el: element,
|
|
9665
|
+
persistent: swapArea ? !swapArea.contains(element) : true,
|
|
9666
|
+
ctx: void 0,
|
|
9667
|
+
state: void 0,
|
|
9668
|
+
api: void 0,
|
|
9669
|
+
route,
|
|
9670
|
+
data,
|
|
9671
|
+
cleanups: [],
|
|
9672
|
+
flush: noop,
|
|
9673
|
+
renderScheduled: false,
|
|
9674
|
+
renderDepth: 0,
|
|
9675
|
+
mountPromise: void 0
|
|
9676
|
+
};
|
|
9677
|
+
instance.ctx = buildContext(state, instance);
|
|
9678
|
+
instance.flush = () => {
|
|
9679
|
+
instance.renderScheduled = false;
|
|
9680
|
+
runRender(instance);
|
|
9681
|
+
};
|
|
9682
|
+
const spec = definition.spec;
|
|
9683
|
+
if (spec?.state) instance.state = spec.state(instance.ctx);
|
|
9684
|
+
if (spec?.api) {
|
|
9685
|
+
instance.api = spec.api(instance.ctx);
|
|
9686
|
+
state.componentApis.set(definition.name, instance.api);
|
|
9687
|
+
}
|
|
9688
|
+
if (spec?.events) attachEvents(instance, spec.events);
|
|
9689
|
+
runHook(instance, "onCreate");
|
|
9690
|
+
const onMountResult = definition.hooks.onMount?.(instance.ctx);
|
|
9691
|
+
if (spec?.render) scheduleRender(instance);
|
|
9692
|
+
const pending = [];
|
|
9693
|
+
if (spec?.render) pending.push(loadRenderChunk());
|
|
9694
|
+
if (onMountResult && typeof onMountResult.then === "function") pending.push(onMountResult);
|
|
9695
|
+
instance.mountPromise = pending.length > 0 ? Promise.all(pending).then(() => {}) : void 0;
|
|
9322
9696
|
state.instances.set(element, instance);
|
|
9323
9697
|
emit("spa:component-mount", {
|
|
9324
9698
|
name: definition.name,
|
|
@@ -9326,12 +9700,12 @@ function mountElement(state, emit, swapArea, data, element, route = EMPTY_ROUTE)
|
|
|
9326
9700
|
});
|
|
9327
9701
|
}
|
|
9328
9702
|
/**
|
|
9329
|
-
* Scans the swap region, mounts components for matching `data-component`
|
|
9330
|
-
*
|
|
9331
|
-
*
|
|
9703
|
+
* Scans the swap region, mounts components for matching `data-component` elements,
|
|
9704
|
+
* classifies persistent (outside swap area) vs page-specific (inside), runs
|
|
9705
|
+
* `onCreate`/`onMount` + initial render, and emits `spa:component-mount` per instance.
|
|
9332
9706
|
* Already-mounted elements are skipped.
|
|
9333
9707
|
*
|
|
9334
|
-
* @param state - The plugin state (registeredComponents + instances).
|
|
9708
|
+
* @param state - The plugin state (registeredComponents + instances + componentApis).
|
|
9335
9709
|
* @param emit - The event emitter for spa:component-mount.
|
|
9336
9710
|
* @param swapSelector - CSS selector bounding page-specific components.
|
|
9337
9711
|
* @param route - The matched-route slice for the current URL (params/meta/locale/url).
|
|
@@ -9345,9 +9719,10 @@ function scanAndMount(state, emit, swapSelector, route = EMPTY_ROUTE) {
|
|
|
9345
9719
|
for (const element of document.querySelectorAll("[data-component]")) mountElement(state, emit, swapArea, data, element, route);
|
|
9346
9720
|
}
|
|
9347
9721
|
/**
|
|
9348
|
-
* Unmounts page-specific instances inside the swap region (runs `onUnMount`
|
|
9349
|
-
* then
|
|
9350
|
-
* Persistent instances (outside the swap area) are
|
|
9722
|
+
* Unmounts page-specific instances inside the swap region (runs `onUnMount` then
|
|
9723
|
+
* `onDestroy`, then their cleanup disposers + api unregister), removes them from state,
|
|
9724
|
+
* and emits `spa:component-unmount`. Persistent instances (outside the swap area) are
|
|
9725
|
+
* left in place.
|
|
9351
9726
|
*
|
|
9352
9727
|
* @param state - The plugin state holding live instances.
|
|
9353
9728
|
* @param emit - The event emitter for spa:component-unmount.
|
|
@@ -9355,12 +9730,13 @@ function scanAndMount(state, emit, swapSelector, route = EMPTY_ROUTE) {
|
|
|
9355
9730
|
* unmountPageSpecific(state, emit);
|
|
9356
9731
|
*/
|
|
9357
9732
|
function unmountPageSpecific(state, emit) {
|
|
9358
|
-
const data =
|
|
9733
|
+
const data = currentPageData();
|
|
9359
9734
|
for (const [element, instance] of state.instances) {
|
|
9360
9735
|
if (instance.persistent) continue;
|
|
9361
|
-
|
|
9362
|
-
runHook(instance, "onUnMount"
|
|
9363
|
-
runHook(instance, "onDestroy"
|
|
9736
|
+
instance.data = data;
|
|
9737
|
+
runHook(instance, "onUnMount");
|
|
9738
|
+
runHook(instance, "onDestroy");
|
|
9739
|
+
disposeInstance(state, instance);
|
|
9364
9740
|
state.instances.delete(element);
|
|
9365
9741
|
emit("spa:component-unmount", {
|
|
9366
9742
|
name: instance.def.name,
|
|
@@ -9369,9 +9745,10 @@ function unmountPageSpecific(state, emit) {
|
|
|
9369
9745
|
}
|
|
9370
9746
|
}
|
|
9371
9747
|
/**
|
|
9372
|
-
* Disposes ALL live instances (persistent and page-specific) on teardown:
|
|
9373
|
-
*
|
|
9374
|
-
* the instance
|
|
9748
|
+
* Disposes ALL live instances (persistent and page-specific) on teardown: runs
|
|
9749
|
+
* `onUnMount` then `onDestroy`, then their cleanup disposers + api unregister, emits
|
|
9750
|
+
* `spa:component-unmount`, and clears the instance + api maps. Used by the kernel's
|
|
9751
|
+
* `dispose` on plugin stop.
|
|
9375
9752
|
*
|
|
9376
9753
|
* @param state - The plugin state holding live instances.
|
|
9377
9754
|
* @param emit - The event emitter for spa:component-unmount.
|
|
@@ -9379,17 +9756,19 @@ function unmountPageSpecific(state, emit) {
|
|
|
9379
9756
|
* unmountAll(state, emit);
|
|
9380
9757
|
*/
|
|
9381
9758
|
function unmountAll(state, emit) {
|
|
9382
|
-
const data =
|
|
9759
|
+
const data = currentPageData();
|
|
9383
9760
|
for (const [element, instance] of state.instances) {
|
|
9384
|
-
|
|
9385
|
-
runHook(instance, "onUnMount"
|
|
9386
|
-
runHook(instance, "onDestroy"
|
|
9761
|
+
instance.data = data;
|
|
9762
|
+
runHook(instance, "onUnMount");
|
|
9763
|
+
runHook(instance, "onDestroy");
|
|
9764
|
+
disposeInstance(state, instance);
|
|
9387
9765
|
emit("spa:component-unmount", {
|
|
9388
9766
|
name: instance.def.name,
|
|
9389
9767
|
el: element
|
|
9390
9768
|
});
|
|
9391
9769
|
}
|
|
9392
9770
|
state.instances.clear();
|
|
9771
|
+
state.componentApis.clear();
|
|
9393
9772
|
}
|
|
9394
9773
|
/**
|
|
9395
9774
|
* Fires `onNavStart` on every currently-mounted instance (persistent instances
|
|
@@ -9400,12 +9779,16 @@ function unmountAll(state, emit) {
|
|
|
9400
9779
|
* notifyNavStart(state);
|
|
9401
9780
|
*/
|
|
9402
9781
|
function notifyNavStart(state) {
|
|
9403
|
-
const data =
|
|
9404
|
-
for (const
|
|
9782
|
+
const data = currentPageData();
|
|
9783
|
+
for (const instance of state.instances.values()) {
|
|
9784
|
+
instance.data = data;
|
|
9785
|
+
runHook(instance, "onNavStart");
|
|
9786
|
+
}
|
|
9405
9787
|
}
|
|
9406
9788
|
/**
|
|
9407
9789
|
* Fires `onNavEnd` on persistent instances that survived the swap (page-specific
|
|
9408
|
-
* instances were already destroyed and re-created by the swap)
|
|
9790
|
+
* instances were already destroyed and re-created by the swap), updating their route
|
|
9791
|
+
* slice to the destination first.
|
|
9409
9792
|
*
|
|
9410
9793
|
* @param state - The plugin state holding live instances.
|
|
9411
9794
|
* @param route - The matched-route slice for the destination URL (params/meta/locale/url).
|
|
@@ -9413,8 +9796,13 @@ function notifyNavStart(state) {
|
|
|
9413
9796
|
* notifyNavEnd(state, route);
|
|
9414
9797
|
*/
|
|
9415
9798
|
function notifyNavEnd(state, route = EMPTY_ROUTE) {
|
|
9416
|
-
const data =
|
|
9417
|
-
for (const
|
|
9799
|
+
const data = currentPageData();
|
|
9800
|
+
for (const instance of state.instances.values()) {
|
|
9801
|
+
if (!instance.persistent) continue;
|
|
9802
|
+
instance.data = data;
|
|
9803
|
+
instance.route = route;
|
|
9804
|
+
runHook(instance, "onNavEnd");
|
|
9805
|
+
}
|
|
9418
9806
|
}
|
|
9419
9807
|
//#endregion
|
|
9420
9808
|
//#region src/plugins/spa/head.ts
|
|
@@ -9955,6 +10343,7 @@ function createState(_ctx) {
|
|
|
9955
10343
|
return {
|
|
9956
10344
|
registeredComponents: /* @__PURE__ */ new Map(),
|
|
9957
10345
|
instances: /* @__PURE__ */ new Map(),
|
|
10346
|
+
componentApis: /* @__PURE__ */ new Map(),
|
|
9958
10347
|
currentUrl: "",
|
|
9959
10348
|
destroyRouter: null,
|
|
9960
10349
|
started: false,
|
|
@@ -10193,7 +10582,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10193
10582
|
const commitDataRender = async (pathname, resolvedRender, signal) => {
|
|
10194
10583
|
if (signal?.aborted) return;
|
|
10195
10584
|
const { route, vnode, routeContext, region } = resolvedRender;
|
|
10196
|
-
const { renderVNode } = await Promise.resolve().then(() => require("./render-
|
|
10585
|
+
const { renderVNode } = await Promise.resolve().then(() => require("./render-KdufA3_b.cjs"));
|
|
10197
10586
|
if (signal?.aborted) return;
|
|
10198
10587
|
syncDataHead(deps.head, route, routeContext);
|
|
10199
10588
|
unmountPageSpecific(state, emit);
|
|
@@ -10262,7 +10651,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10262
10651
|
return;
|
|
10263
10652
|
}
|
|
10264
10653
|
const { vnode, region } = resolvedRender;
|
|
10265
|
-
const { renderVNode } = await Promise.resolve().then(() => require("./render-
|
|
10654
|
+
const { renderVNode } = await Promise.resolve().then(() => require("./render-KdufA3_b.cjs"));
|
|
10266
10655
|
renderVNode(vnode, region);
|
|
10267
10656
|
scanAndMount(state, emit, resolved.swapSelector, routeSlice);
|
|
10268
10657
|
};
|