@moku-labs/web 1.13.2 → 1.14.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 +31 -13
- package/dist/browser.mjs +72 -21
- package/dist/index.cjs +76 -28
- package/dist/index.d.cts +31 -13
- package/dist/index.d.mts +31 -13
- package/dist/index.mjs +76 -28
- package/package.json +1 -1
package/dist/browser.d.mts
CHANGED
|
@@ -287,7 +287,10 @@ interface RouteState<P extends string = string, D = unknown> {
|
|
|
287
287
|
/** Loaded data type produced by `.load()` (widened only by `.load()`). */
|
|
288
288
|
readonly data: D;
|
|
289
289
|
}
|
|
290
|
-
/**
|
|
290
|
+
/**
|
|
291
|
+
* Render-time context handed to `.render()` / `.head()`; `data` is `.load()`'s return,
|
|
292
|
+
* `meta` the route's `.meta()` bag.
|
|
293
|
+
*/
|
|
291
294
|
interface RouteContext<S extends RouteState> {
|
|
292
295
|
/** Resolved path params. */
|
|
293
296
|
readonly params: S["params"];
|
|
@@ -295,6 +298,13 @@ interface RouteContext<S extends RouteState> {
|
|
|
295
298
|
readonly data: S["data"];
|
|
296
299
|
/** Active locale for this render. */
|
|
297
300
|
readonly locale: string;
|
|
301
|
+
/**
|
|
302
|
+
* The route's `.meta()` bag (e.g. `{ activeTab: "home" }`). Available in `.render()` and
|
|
303
|
+
* `.head()`, identically at build and on the client — `meta` is compiled into the route and
|
|
304
|
+
* shipped in the client manifest, so a client-only route (dynamic, no `.generate()`, whose
|
|
305
|
+
* `.load()` data is `{}` on the client) can feed static per-route config into its render.
|
|
306
|
+
*/
|
|
307
|
+
readonly meta: Record<string, unknown>;
|
|
298
308
|
/**
|
|
299
309
|
* Build a link to a named route by pattern substitution — the framework delivers
|
|
300
310
|
* this on the context (same output as `app.router.toUrl`), so render/head build
|
|
@@ -375,21 +385,16 @@ interface GenerateContext {
|
|
|
375
385
|
readonly has: (name: string) => boolean;
|
|
376
386
|
}
|
|
377
387
|
/**
|
|
378
|
-
* Context handed to a route's `.layout()` wrapper
|
|
379
|
-
*
|
|
380
|
-
*
|
|
381
|
-
* `RouteContext` because the layout is the only handler that needs `meta`; keeping
|
|
382
|
-
* it on its own type leaves `.render()`/`.head()` contexts unchanged.
|
|
388
|
+
* Context handed to a route's `.layout()` wrapper — identical to {@link RouteContext}
|
|
389
|
+
* (which now carries `meta` for every handler). Retained as a named alias so existing
|
|
390
|
+
* `.layout((ctx, children) => …)` typings keep compiling.
|
|
383
391
|
*
|
|
384
392
|
* @remarks
|
|
385
393
|
* The layout is applied in the SSG render path ONLY. On client (SPA) navigation the
|
|
386
|
-
* chrome is persistent and the layout is intentionally NOT re-applied — only the
|
|
387
|
-
*
|
|
394
|
+
* chrome is persistent and the layout is intentionally NOT re-applied — only the inner
|
|
395
|
+
* swap region is replaced. See `build`'s pages phase and `spa`'s kernel.
|
|
388
396
|
*/
|
|
389
|
-
|
|
390
|
-
/** The route's `.meta()` bag (e.g. `{ activeTab: "home" }`). */
|
|
391
|
-
readonly meta: Record<string, unknown>;
|
|
392
|
-
}
|
|
397
|
+
type LayoutContext<S extends RouteState> = RouteContext<S>;
|
|
393
398
|
/** Head metadata produced by a route's `.head()` handler. */
|
|
394
399
|
interface HeadConfig$1 {
|
|
395
400
|
/** Document title. */
|
|
@@ -944,12 +949,25 @@ interface ResolvedSpaConfig {
|
|
|
944
949
|
/** Pre-registered components. */
|
|
945
950
|
components: ComponentDef[];
|
|
946
951
|
}
|
|
947
|
-
/**
|
|
952
|
+
/**
|
|
953
|
+
* Context handed to every component lifecycle hook — the bound element + page data,
|
|
954
|
+
* plus the matched route's `params`/`meta`/`locale` and a link builder, so an island
|
|
955
|
+
* can read its route context (e.g. a `card` route's `ctx.meta.focus` + `ctx.params.id`)
|
|
956
|
+
* directly, without the page bridging it through `data-*` attributes.
|
|
957
|
+
*/
|
|
948
958
|
interface ComponentContext {
|
|
949
959
|
/** The element the component instance is bound to. */
|
|
950
960
|
el: Element;
|
|
951
961
|
/** Page data extracted from the `script#__DATA__` payload. */
|
|
952
962
|
data: PageData;
|
|
963
|
+
/** Resolved path params of the route matched for the current URL (empty if unmatched). */
|
|
964
|
+
readonly params: Record<string, string | undefined>;
|
|
965
|
+
/** The matched route's `.meta()` bag (empty if unmatched). */
|
|
966
|
+
readonly meta: Record<string, unknown>;
|
|
967
|
+
/** Active locale for the current route (empty string if unknown). */
|
|
968
|
+
readonly locale: string;
|
|
969
|
+
/** Build a link to a named route by pattern substitution (same output as `app.router.toUrl`). */
|
|
970
|
+
readonly url: (name: string, params?: Record<string, string>) => string;
|
|
953
971
|
}
|
|
954
972
|
/** Lifecycle hooks a component may implement. */
|
|
955
973
|
interface ComponentHooks {
|
package/dist/browser.mjs
CHANGED
|
@@ -2473,6 +2473,23 @@ const ERROR_PREFIX$2 = "[web]";
|
|
|
2473
2473
|
/** The set of legal hook names, frozen for O(1) membership checks. */
|
|
2474
2474
|
const HOOK_NAME_SET = new Set(COMPONENT_HOOK_NAMES);
|
|
2475
2475
|
/**
|
|
2476
|
+
* No-op link builder for the {@link EMPTY_ROUTE} slice (used when no route matched).
|
|
2477
|
+
*
|
|
2478
|
+
* @returns An empty string.
|
|
2479
|
+
* @example
|
|
2480
|
+
* const href = noUrl();
|
|
2481
|
+
*/
|
|
2482
|
+
function noUrl() {
|
|
2483
|
+
return "";
|
|
2484
|
+
}
|
|
2485
|
+
/** Empty route slice — used for mounts with no matched route (headless, tests, public `scan()`). */
|
|
2486
|
+
const EMPTY_ROUTE = {
|
|
2487
|
+
params: {},
|
|
2488
|
+
meta: {},
|
|
2489
|
+
locale: "",
|
|
2490
|
+
url: noUrl
|
|
2491
|
+
};
|
|
2492
|
+
/**
|
|
2476
2493
|
* Validate a single hook entry: its key must be a known hook name and its value
|
|
2477
2494
|
* must be a function. Throws fail-fast on the first violation.
|
|
2478
2495
|
*
|
|
@@ -2560,18 +2577,25 @@ function runHook(instance, hook, ctx) {
|
|
|
2560
2577
|
instance.def.hooks[hook]?.(ctx);
|
|
2561
2578
|
}
|
|
2562
2579
|
/**
|
|
2563
|
-
* Builds the component context handed to a hook
|
|
2580
|
+
* Builds the component context handed to a hook: the bound element + page data, merged
|
|
2581
|
+
* with the matched route's slice (params/meta/locale/url). Defaults to {@link EMPTY_ROUTE}
|
|
2582
|
+
* when no route is supplied (headless, tests, public `scan()`).
|
|
2564
2583
|
*
|
|
2565
2584
|
* @param element - The element the instance is bound to.
|
|
2566
2585
|
* @param data - The current page data payload.
|
|
2586
|
+
* @param route - The matched-route slice for the current URL.
|
|
2567
2587
|
* @returns The hook context.
|
|
2568
2588
|
* @example
|
|
2569
|
-
* const ctx = makeContext(element, data);
|
|
2589
|
+
* const ctx = makeContext(element, data, route);
|
|
2570
2590
|
*/
|
|
2571
|
-
function makeContext(element, data) {
|
|
2591
|
+
function makeContext(element, data, route = EMPTY_ROUTE) {
|
|
2572
2592
|
return {
|
|
2573
2593
|
el: element,
|
|
2574
|
-
data
|
|
2594
|
+
data,
|
|
2595
|
+
params: route.params,
|
|
2596
|
+
meta: route.meta,
|
|
2597
|
+
locale: route.locale,
|
|
2598
|
+
url: route.url
|
|
2575
2599
|
};
|
|
2576
2600
|
}
|
|
2577
2601
|
/**
|
|
@@ -2585,17 +2609,18 @@ function makeContext(element, data) {
|
|
|
2585
2609
|
* @param swapArea - The swap-region element, or null when none was found.
|
|
2586
2610
|
* @param data - The current page data payload.
|
|
2587
2611
|
* @param element - The candidate element carrying a `data-component` attribute.
|
|
2612
|
+
* @param route - The matched-route slice for the current URL (params/meta/locale/url).
|
|
2588
2613
|
* @example
|
|
2589
|
-
* mountElement(state, emit, swapArea, data, element);
|
|
2614
|
+
* mountElement(state, emit, swapArea, data, element, route);
|
|
2590
2615
|
*/
|
|
2591
|
-
function mountElement(state, emit, swapArea, data, element) {
|
|
2616
|
+
function mountElement(state, emit, swapArea, data, element, route = EMPTY_ROUTE) {
|
|
2592
2617
|
if (state.instances.has(element)) return;
|
|
2593
2618
|
const name = element.dataset.component;
|
|
2594
2619
|
if (!name) return;
|
|
2595
2620
|
const definition = state.registeredComponents.get(name);
|
|
2596
2621
|
if (!definition) return;
|
|
2597
2622
|
const instance = createInstance(definition, element, swapArea ? !swapArea.contains(element) : true);
|
|
2598
|
-
const ctx = makeContext(element, data);
|
|
2623
|
+
const ctx = makeContext(element, data, route);
|
|
2599
2624
|
runHook(instance, "onCreate", ctx);
|
|
2600
2625
|
runHook(instance, "onMount", ctx);
|
|
2601
2626
|
state.instances.set(element, instance);
|
|
@@ -2613,14 +2638,15 @@ function mountElement(state, emit, swapArea, data, element) {
|
|
|
2613
2638
|
* @param state - The plugin state (registeredComponents + instances).
|
|
2614
2639
|
* @param emit - The event emitter for spa:component-mount.
|
|
2615
2640
|
* @param swapSelector - CSS selector bounding page-specific components.
|
|
2641
|
+
* @param route - The matched-route slice for the current URL (params/meta/locale/url).
|
|
2616
2642
|
* @example
|
|
2617
|
-
* scanAndMount(state, emit, "main > section");
|
|
2643
|
+
* scanAndMount(state, emit, "main > section", route);
|
|
2618
2644
|
*/
|
|
2619
|
-
function scanAndMount(state, emit, swapSelector) {
|
|
2645
|
+
function scanAndMount(state, emit, swapSelector, route = EMPTY_ROUTE) {
|
|
2620
2646
|
if (typeof document === "undefined") return;
|
|
2621
2647
|
const swapArea = document.querySelector(swapSelector);
|
|
2622
2648
|
const data = extractPageData(document);
|
|
2623
|
-
for (const element of document.querySelectorAll("[data-component]")) mountElement(state, emit, swapArea, data, element);
|
|
2649
|
+
for (const element of document.querySelectorAll("[data-component]")) mountElement(state, emit, swapArea, data, element, route);
|
|
2624
2650
|
}
|
|
2625
2651
|
/**
|
|
2626
2652
|
* Unmounts page-specific instances inside the swap region (runs `onUnMount`
|
|
@@ -2686,12 +2712,13 @@ function notifyNavStart(state) {
|
|
|
2686
2712
|
* instances were already destroyed and re-created by the swap).
|
|
2687
2713
|
*
|
|
2688
2714
|
* @param state - The plugin state holding live instances.
|
|
2715
|
+
* @param route - The matched-route slice for the destination URL (params/meta/locale/url).
|
|
2689
2716
|
* @example
|
|
2690
|
-
* notifyNavEnd(state);
|
|
2717
|
+
* notifyNavEnd(state, route);
|
|
2691
2718
|
*/
|
|
2692
|
-
function notifyNavEnd(state) {
|
|
2719
|
+
function notifyNavEnd(state, route = EMPTY_ROUTE) {
|
|
2693
2720
|
const data = typeof document === "undefined" ? {} : extractPageData(document);
|
|
2694
|
-
for (const [element, instance] of state.instances) if (instance.persistent) runHook(instance, "onNavEnd", makeContext(element, data));
|
|
2721
|
+
for (const [element, instance] of state.instances) if (instance.persistent) runHook(instance, "onNavEnd", makeContext(element, data, route));
|
|
2695
2722
|
}
|
|
2696
2723
|
//#endregion
|
|
2697
2724
|
//#region src/plugins/spa/head.ts
|
|
@@ -3334,6 +3361,26 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
3334
3361
|
});
|
|
3335
3362
|
};
|
|
3336
3363
|
/**
|
|
3364
|
+
* Build the matched-route slice (params/meta/locale/url) for the component context at `path`,
|
|
3365
|
+
* so islands read their route's params/meta directly. An unmatched path yields an empty slice.
|
|
3366
|
+
*
|
|
3367
|
+
* @param path - The URL (pathname + search) to match.
|
|
3368
|
+
* @returns The route slice for the matched route.
|
|
3369
|
+
* @example
|
|
3370
|
+
* scanAndMount(state, emit, resolved.swapSelector, componentRouteContext(pathname));
|
|
3371
|
+
*/
|
|
3372
|
+
const componentRouteContext = (path) => {
|
|
3373
|
+
const matchPath = path.split("?")[0] ?? path;
|
|
3374
|
+
const hit = deps.router.match(matchPath);
|
|
3375
|
+
const locale = hit?.params.lang ?? (typeof document === "undefined" ? "" : document.documentElement.lang) ?? "";
|
|
3376
|
+
return {
|
|
3377
|
+
params: hit?.params ?? {},
|
|
3378
|
+
meta: hit?.route._meta ?? {},
|
|
3379
|
+
locale,
|
|
3380
|
+
url: (name, params = {}) => deps.router.toUrl(name, params)
|
|
3381
|
+
};
|
|
3382
|
+
};
|
|
3383
|
+
/**
|
|
3337
3384
|
* Process one navigation: head-sync, unmount, swap, re-mount, emit navigated.
|
|
3338
3385
|
* When the region cannot be swapped (either document lacks the swap selector)
|
|
3339
3386
|
* the SPA nav cannot complete — the head is already synced and the islands torn
|
|
@@ -3351,8 +3398,9 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
3351
3398
|
syncHead(deps.head, doc);
|
|
3352
3399
|
unmountPageSpecific(state, emit);
|
|
3353
3400
|
if (!swapRegion(doc, resolved.swapSelector, resolved.viewTransitions, () => {
|
|
3354
|
-
|
|
3355
|
-
|
|
3401
|
+
const routeSlice = componentRouteContext(pathname);
|
|
3402
|
+
scanAndMount(state, emit, resolved.swapSelector, routeSlice);
|
|
3403
|
+
notifyNavEnd(state, routeSlice);
|
|
3356
3404
|
}, applyPendingScroll)) {
|
|
3357
3405
|
handleError();
|
|
3358
3406
|
location.href = pathname;
|
|
@@ -3421,6 +3469,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
3421
3469
|
params: hit.params,
|
|
3422
3470
|
data,
|
|
3423
3471
|
locale,
|
|
3472
|
+
meta: hit.route._meta,
|
|
3424
3473
|
url: (routeName, routeParams = {}) => deps.router.toUrl(routeName, routeParams)
|
|
3425
3474
|
};
|
|
3426
3475
|
const vnode = hit.route._handlers.render(routeContext);
|
|
@@ -3452,6 +3501,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
3452
3501
|
if (signal?.aborted) return;
|
|
3453
3502
|
syncDataHead(deps.head, route, routeContext);
|
|
3454
3503
|
unmountPageSpecific(state, emit);
|
|
3504
|
+
const routeSlice = componentRouteContext(pathname);
|
|
3455
3505
|
/**
|
|
3456
3506
|
* Render the VNode into the region and re-mount its islands in one paint — the
|
|
3457
3507
|
* swap body handed to `runSwap` (optionally wrapped in a View Transition).
|
|
@@ -3463,8 +3513,8 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
3463
3513
|
*/
|
|
3464
3514
|
const renderAndMount = () => {
|
|
3465
3515
|
renderVNode(vnode, region);
|
|
3466
|
-
scanAndMount(state, emit, resolved.swapSelector);
|
|
3467
|
-
notifyNavEnd(state);
|
|
3516
|
+
scanAndMount(state, emit, resolved.swapSelector, routeSlice);
|
|
3517
|
+
notifyNavEnd(state, routeSlice);
|
|
3468
3518
|
};
|
|
3469
3519
|
runSwap(renderAndMount, resolved.viewTransitions, applyPendingScroll);
|
|
3470
3520
|
state.currentUrl = pathname;
|
|
@@ -3509,15 +3559,16 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
3509
3559
|
* await bootRender("/b/abc123");
|
|
3510
3560
|
*/
|
|
3511
3561
|
const bootRender = async (pathname) => {
|
|
3562
|
+
const routeSlice = componentRouteContext(pathname);
|
|
3512
3563
|
const resolvedRender = await resolveDataRender(pathname);
|
|
3513
3564
|
if (resolvedRender === false) {
|
|
3514
|
-
scanAndMount(state, emit, resolved.swapSelector);
|
|
3565
|
+
scanAndMount(state, emit, resolved.swapSelector, routeSlice);
|
|
3515
3566
|
return;
|
|
3516
3567
|
}
|
|
3517
3568
|
const { vnode, region } = resolvedRender;
|
|
3518
3569
|
const { renderVNode } = await import("./render-BNe0s7fr.mjs");
|
|
3519
3570
|
renderVNode(vnode, region);
|
|
3520
|
-
scanAndMount(state, emit, resolved.swapSelector);
|
|
3571
|
+
scanAndMount(state, emit, resolved.swapSelector, routeSlice);
|
|
3521
3572
|
};
|
|
3522
3573
|
/**
|
|
3523
3574
|
* Unified navigation: try the client DATA path first (only when the `data`
|
|
@@ -3567,7 +3618,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
3567
3618
|
const matchPath = state.currentUrl.split("?")[0] ?? state.currentUrl;
|
|
3568
3619
|
const hit = deps.router.match(matchPath);
|
|
3569
3620
|
if (hit?.route._handlers.render && isClientOnlyRoute(deps.router.mode(), hit.route)) bootRender(state.currentUrl);
|
|
3570
|
-
else scanAndMount(state, emit, resolved.swapSelector);
|
|
3621
|
+
else scanAndMount(state, emit, resolved.swapSelector, componentRouteContext(state.currentUrl));
|
|
3571
3622
|
state.started = true;
|
|
3572
3623
|
},
|
|
3573
3624
|
/**
|
|
@@ -3598,7 +3649,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
3598
3649
|
* kernel.scan();
|
|
3599
3650
|
*/
|
|
3600
3651
|
scan() {
|
|
3601
|
-
scanAndMount(state, emit, resolved.swapSelector);
|
|
3652
|
+
scanAndMount(state, emit, resolved.swapSelector, componentRouteContext(state.currentUrl));
|
|
3602
3653
|
},
|
|
3603
3654
|
/**
|
|
3604
3655
|
* Tear down router listeners, dispose all instances, reset boot state.
|
package/dist/index.cjs
CHANGED
|
@@ -4738,8 +4738,8 @@ function composeHeadHtml(ctx, instance, url, routeContext, data) {
|
|
|
4738
4738
|
* layout is NOT re-applied on navigation), then serialize with
|
|
4739
4739
|
* preact-render-to-string. Returns `""` when the route has no `.render()`.
|
|
4740
4740
|
*
|
|
4741
|
-
* @param definition - The route definition (provides `.render()`/`.layout()
|
|
4742
|
-
* @param routeContext - The route context (params/data/locale/url)
|
|
4741
|
+
* @param definition - The route definition (provides `.render()`/`.layout()`).
|
|
4742
|
+
* @param routeContext - The route context (params/data/locale/meta/url); `meta` flows to the layout.
|
|
4743
4743
|
* @returns The SSR-rendered body HTML, or `""` when the route has no `.render()`.
|
|
4744
4744
|
* @example
|
|
4745
4745
|
* ```ts
|
|
@@ -4749,11 +4749,7 @@ function composeHeadHtml(ctx, instance, url, routeContext, data) {
|
|
|
4749
4749
|
function renderBody(definition, routeContext) {
|
|
4750
4750
|
const vnode = definition._handlers.render?.(routeContext);
|
|
4751
4751
|
if (!vnode) return "";
|
|
4752
|
-
|
|
4753
|
-
...routeContext,
|
|
4754
|
-
meta: definition._meta
|
|
4755
|
-
};
|
|
4756
|
-
return (0, preact_render_to_string.renderToString)(definition._handlers.layout ? definition._handlers.layout(layoutContext, vnode) : vnode);
|
|
4752
|
+
return (0, preact_render_to_string.renderToString)(definition._handlers.layout ? definition._handlers.layout(routeContext, vnode) : vnode);
|
|
4757
4753
|
}
|
|
4758
4754
|
/**
|
|
4759
4755
|
* Hash a page's render inputs (its loaded data) for the render cache. `null` when the
|
|
@@ -4887,6 +4883,7 @@ async function renderInstance(ctx, instance, shell, reuse) {
|
|
|
4887
4883
|
params,
|
|
4888
4884
|
data,
|
|
4889
4885
|
locale,
|
|
4886
|
+
meta: definition._meta,
|
|
4890
4887
|
url: (routeName, routeParams = {}) => router.toUrl(routeName, routeParams)
|
|
4891
4888
|
};
|
|
4892
4889
|
const parts = {
|
|
@@ -9098,6 +9095,23 @@ const ERROR_PREFIX$2 = "[web]";
|
|
|
9098
9095
|
/** The set of legal hook names, frozen for O(1) membership checks. */
|
|
9099
9096
|
const HOOK_NAME_SET = new Set(COMPONENT_HOOK_NAMES);
|
|
9100
9097
|
/**
|
|
9098
|
+
* No-op link builder for the {@link EMPTY_ROUTE} slice (used when no route matched).
|
|
9099
|
+
*
|
|
9100
|
+
* @returns An empty string.
|
|
9101
|
+
* @example
|
|
9102
|
+
* const href = noUrl();
|
|
9103
|
+
*/
|
|
9104
|
+
function noUrl() {
|
|
9105
|
+
return "";
|
|
9106
|
+
}
|
|
9107
|
+
/** Empty route slice — used for mounts with no matched route (headless, tests, public `scan()`). */
|
|
9108
|
+
const EMPTY_ROUTE = {
|
|
9109
|
+
params: {},
|
|
9110
|
+
meta: {},
|
|
9111
|
+
locale: "",
|
|
9112
|
+
url: noUrl
|
|
9113
|
+
};
|
|
9114
|
+
/**
|
|
9101
9115
|
* Validate a single hook entry: its key must be a known hook name and its value
|
|
9102
9116
|
* must be a function. Throws fail-fast on the first violation.
|
|
9103
9117
|
*
|
|
@@ -9185,18 +9199,25 @@ function runHook(instance, hook, ctx) {
|
|
|
9185
9199
|
instance.def.hooks[hook]?.(ctx);
|
|
9186
9200
|
}
|
|
9187
9201
|
/**
|
|
9188
|
-
* Builds the component context handed to a hook
|
|
9202
|
+
* Builds the component context handed to a hook: the bound element + page data, merged
|
|
9203
|
+
* with the matched route's slice (params/meta/locale/url). Defaults to {@link EMPTY_ROUTE}
|
|
9204
|
+
* when no route is supplied (headless, tests, public `scan()`).
|
|
9189
9205
|
*
|
|
9190
9206
|
* @param element - The element the instance is bound to.
|
|
9191
9207
|
* @param data - The current page data payload.
|
|
9208
|
+
* @param route - The matched-route slice for the current URL.
|
|
9192
9209
|
* @returns The hook context.
|
|
9193
9210
|
* @example
|
|
9194
|
-
* const ctx = makeContext(element, data);
|
|
9211
|
+
* const ctx = makeContext(element, data, route);
|
|
9195
9212
|
*/
|
|
9196
|
-
function makeContext(element, data) {
|
|
9213
|
+
function makeContext(element, data, route = EMPTY_ROUTE) {
|
|
9197
9214
|
return {
|
|
9198
9215
|
el: element,
|
|
9199
|
-
data
|
|
9216
|
+
data,
|
|
9217
|
+
params: route.params,
|
|
9218
|
+
meta: route.meta,
|
|
9219
|
+
locale: route.locale,
|
|
9220
|
+
url: route.url
|
|
9200
9221
|
};
|
|
9201
9222
|
}
|
|
9202
9223
|
/**
|
|
@@ -9210,17 +9231,18 @@ function makeContext(element, data) {
|
|
|
9210
9231
|
* @param swapArea - The swap-region element, or null when none was found.
|
|
9211
9232
|
* @param data - The current page data payload.
|
|
9212
9233
|
* @param element - The candidate element carrying a `data-component` attribute.
|
|
9234
|
+
* @param route - The matched-route slice for the current URL (params/meta/locale/url).
|
|
9213
9235
|
* @example
|
|
9214
|
-
* mountElement(state, emit, swapArea, data, element);
|
|
9236
|
+
* mountElement(state, emit, swapArea, data, element, route);
|
|
9215
9237
|
*/
|
|
9216
|
-
function mountElement(state, emit, swapArea, data, element) {
|
|
9238
|
+
function mountElement(state, emit, swapArea, data, element, route = EMPTY_ROUTE) {
|
|
9217
9239
|
if (state.instances.has(element)) return;
|
|
9218
9240
|
const name = element.dataset.component;
|
|
9219
9241
|
if (!name) return;
|
|
9220
9242
|
const definition = state.registeredComponents.get(name);
|
|
9221
9243
|
if (!definition) return;
|
|
9222
9244
|
const instance = createInstance(definition, element, swapArea ? !swapArea.contains(element) : true);
|
|
9223
|
-
const ctx = makeContext(element, data);
|
|
9245
|
+
const ctx = makeContext(element, data, route);
|
|
9224
9246
|
runHook(instance, "onCreate", ctx);
|
|
9225
9247
|
runHook(instance, "onMount", ctx);
|
|
9226
9248
|
state.instances.set(element, instance);
|
|
@@ -9238,14 +9260,15 @@ function mountElement(state, emit, swapArea, data, element) {
|
|
|
9238
9260
|
* @param state - The plugin state (registeredComponents + instances).
|
|
9239
9261
|
* @param emit - The event emitter for spa:component-mount.
|
|
9240
9262
|
* @param swapSelector - CSS selector bounding page-specific components.
|
|
9263
|
+
* @param route - The matched-route slice for the current URL (params/meta/locale/url).
|
|
9241
9264
|
* @example
|
|
9242
|
-
* scanAndMount(state, emit, "main > section");
|
|
9265
|
+
* scanAndMount(state, emit, "main > section", route);
|
|
9243
9266
|
*/
|
|
9244
|
-
function scanAndMount(state, emit, swapSelector) {
|
|
9267
|
+
function scanAndMount(state, emit, swapSelector, route = EMPTY_ROUTE) {
|
|
9245
9268
|
if (typeof document === "undefined") return;
|
|
9246
9269
|
const swapArea = document.querySelector(swapSelector);
|
|
9247
9270
|
const data = extractPageData(document);
|
|
9248
|
-
for (const element of document.querySelectorAll("[data-component]")) mountElement(state, emit, swapArea, data, element);
|
|
9271
|
+
for (const element of document.querySelectorAll("[data-component]")) mountElement(state, emit, swapArea, data, element, route);
|
|
9249
9272
|
}
|
|
9250
9273
|
/**
|
|
9251
9274
|
* Unmounts page-specific instances inside the swap region (runs `onUnMount`
|
|
@@ -9311,12 +9334,13 @@ function notifyNavStart(state) {
|
|
|
9311
9334
|
* instances were already destroyed and re-created by the swap).
|
|
9312
9335
|
*
|
|
9313
9336
|
* @param state - The plugin state holding live instances.
|
|
9337
|
+
* @param route - The matched-route slice for the destination URL (params/meta/locale/url).
|
|
9314
9338
|
* @example
|
|
9315
|
-
* notifyNavEnd(state);
|
|
9339
|
+
* notifyNavEnd(state, route);
|
|
9316
9340
|
*/
|
|
9317
|
-
function notifyNavEnd(state) {
|
|
9341
|
+
function notifyNavEnd(state, route = EMPTY_ROUTE) {
|
|
9318
9342
|
const data = typeof document === "undefined" ? {} : extractPageData(document);
|
|
9319
|
-
for (const [element, instance] of state.instances) if (instance.persistent) runHook(instance, "onNavEnd", makeContext(element, data));
|
|
9343
|
+
for (const [element, instance] of state.instances) if (instance.persistent) runHook(instance, "onNavEnd", makeContext(element, data, route));
|
|
9320
9344
|
}
|
|
9321
9345
|
//#endregion
|
|
9322
9346
|
//#region src/plugins/spa/head.ts
|
|
@@ -9959,6 +9983,26 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
9959
9983
|
});
|
|
9960
9984
|
};
|
|
9961
9985
|
/**
|
|
9986
|
+
* Build the matched-route slice (params/meta/locale/url) for the component context at `path`,
|
|
9987
|
+
* so islands read their route's params/meta directly. An unmatched path yields an empty slice.
|
|
9988
|
+
*
|
|
9989
|
+
* @param path - The URL (pathname + search) to match.
|
|
9990
|
+
* @returns The route slice for the matched route.
|
|
9991
|
+
* @example
|
|
9992
|
+
* scanAndMount(state, emit, resolved.swapSelector, componentRouteContext(pathname));
|
|
9993
|
+
*/
|
|
9994
|
+
const componentRouteContext = (path) => {
|
|
9995
|
+
const matchPath = path.split("?")[0] ?? path;
|
|
9996
|
+
const hit = deps.router.match(matchPath);
|
|
9997
|
+
const locale = hit?.params.lang ?? (typeof document === "undefined" ? "" : document.documentElement.lang) ?? "";
|
|
9998
|
+
return {
|
|
9999
|
+
params: hit?.params ?? {},
|
|
10000
|
+
meta: hit?.route._meta ?? {},
|
|
10001
|
+
locale,
|
|
10002
|
+
url: (name, params = {}) => deps.router.toUrl(name, params)
|
|
10003
|
+
};
|
|
10004
|
+
};
|
|
10005
|
+
/**
|
|
9962
10006
|
* Process one navigation: head-sync, unmount, swap, re-mount, emit navigated.
|
|
9963
10007
|
* When the region cannot be swapped (either document lacks the swap selector)
|
|
9964
10008
|
* the SPA nav cannot complete — the head is already synced and the islands torn
|
|
@@ -9976,8 +10020,9 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
9976
10020
|
syncHead(deps.head, doc);
|
|
9977
10021
|
unmountPageSpecific(state, emit);
|
|
9978
10022
|
if (!swapRegion(doc, resolved.swapSelector, resolved.viewTransitions, () => {
|
|
9979
|
-
|
|
9980
|
-
|
|
10023
|
+
const routeSlice = componentRouteContext(pathname);
|
|
10024
|
+
scanAndMount(state, emit, resolved.swapSelector, routeSlice);
|
|
10025
|
+
notifyNavEnd(state, routeSlice);
|
|
9981
10026
|
}, applyPendingScroll)) {
|
|
9982
10027
|
handleError();
|
|
9983
10028
|
location.href = pathname;
|
|
@@ -10046,6 +10091,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10046
10091
|
params: hit.params,
|
|
10047
10092
|
data,
|
|
10048
10093
|
locale,
|
|
10094
|
+
meta: hit.route._meta,
|
|
10049
10095
|
url: (routeName, routeParams = {}) => deps.router.toUrl(routeName, routeParams)
|
|
10050
10096
|
};
|
|
10051
10097
|
const vnode = hit.route._handlers.render(routeContext);
|
|
@@ -10077,6 +10123,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10077
10123
|
if (signal?.aborted) return;
|
|
10078
10124
|
syncDataHead(deps.head, route, routeContext);
|
|
10079
10125
|
unmountPageSpecific(state, emit);
|
|
10126
|
+
const routeSlice = componentRouteContext(pathname);
|
|
10080
10127
|
/**
|
|
10081
10128
|
* Render the VNode into the region and re-mount its islands in one paint — the
|
|
10082
10129
|
* swap body handed to `runSwap` (optionally wrapped in a View Transition).
|
|
@@ -10088,8 +10135,8 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10088
10135
|
*/
|
|
10089
10136
|
const renderAndMount = () => {
|
|
10090
10137
|
renderVNode(vnode, region);
|
|
10091
|
-
scanAndMount(state, emit, resolved.swapSelector);
|
|
10092
|
-
notifyNavEnd(state);
|
|
10138
|
+
scanAndMount(state, emit, resolved.swapSelector, routeSlice);
|
|
10139
|
+
notifyNavEnd(state, routeSlice);
|
|
10093
10140
|
};
|
|
10094
10141
|
runSwap(renderAndMount, resolved.viewTransitions, applyPendingScroll);
|
|
10095
10142
|
state.currentUrl = pathname;
|
|
@@ -10134,15 +10181,16 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10134
10181
|
* await bootRender("/b/abc123");
|
|
10135
10182
|
*/
|
|
10136
10183
|
const bootRender = async (pathname) => {
|
|
10184
|
+
const routeSlice = componentRouteContext(pathname);
|
|
10137
10185
|
const resolvedRender = await resolveDataRender(pathname);
|
|
10138
10186
|
if (resolvedRender === false) {
|
|
10139
|
-
scanAndMount(state, emit, resolved.swapSelector);
|
|
10187
|
+
scanAndMount(state, emit, resolved.swapSelector, routeSlice);
|
|
10140
10188
|
return;
|
|
10141
10189
|
}
|
|
10142
10190
|
const { vnode, region } = resolvedRender;
|
|
10143
10191
|
const { renderVNode } = await Promise.resolve().then(() => require("./render-DLZEOe4M.cjs"));
|
|
10144
10192
|
renderVNode(vnode, region);
|
|
10145
|
-
scanAndMount(state, emit, resolved.swapSelector);
|
|
10193
|
+
scanAndMount(state, emit, resolved.swapSelector, routeSlice);
|
|
10146
10194
|
};
|
|
10147
10195
|
/**
|
|
10148
10196
|
* Unified navigation: try the client DATA path first (only when the `data`
|
|
@@ -10192,7 +10240,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10192
10240
|
const matchPath = state.currentUrl.split("?")[0] ?? state.currentUrl;
|
|
10193
10241
|
const hit = deps.router.match(matchPath);
|
|
10194
10242
|
if (hit?.route._handlers.render && isClientOnlyRoute(deps.router.mode(), hit.route)) bootRender(state.currentUrl);
|
|
10195
|
-
else scanAndMount(state, emit, resolved.swapSelector);
|
|
10243
|
+
else scanAndMount(state, emit, resolved.swapSelector, componentRouteContext(state.currentUrl));
|
|
10196
10244
|
state.started = true;
|
|
10197
10245
|
},
|
|
10198
10246
|
/**
|
|
@@ -10223,7 +10271,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10223
10271
|
* kernel.scan();
|
|
10224
10272
|
*/
|
|
10225
10273
|
scan() {
|
|
10226
|
-
scanAndMount(state, emit, resolved.swapSelector);
|
|
10274
|
+
scanAndMount(state, emit, resolved.swapSelector, componentRouteContext(state.currentUrl));
|
|
10227
10275
|
},
|
|
10228
10276
|
/**
|
|
10229
10277
|
* Tear down router listeners, dispose all instances, reset boot state.
|
package/dist/index.d.cts
CHANGED
|
@@ -288,7 +288,10 @@ interface RouteState<P extends string = string, D = unknown> {
|
|
|
288
288
|
/** Loaded data type produced by `.load()` (widened only by `.load()`). */
|
|
289
289
|
readonly data: D;
|
|
290
290
|
}
|
|
291
|
-
/**
|
|
291
|
+
/**
|
|
292
|
+
* Render-time context handed to `.render()` / `.head()`; `data` is `.load()`'s return,
|
|
293
|
+
* `meta` the route's `.meta()` bag.
|
|
294
|
+
*/
|
|
292
295
|
interface RouteContext<S extends RouteState> {
|
|
293
296
|
/** Resolved path params. */
|
|
294
297
|
readonly params: S["params"];
|
|
@@ -296,6 +299,13 @@ interface RouteContext<S extends RouteState> {
|
|
|
296
299
|
readonly data: S["data"];
|
|
297
300
|
/** Active locale for this render. */
|
|
298
301
|
readonly locale: string;
|
|
302
|
+
/**
|
|
303
|
+
* The route's `.meta()` bag (e.g. `{ activeTab: "home" }`). Available in `.render()` and
|
|
304
|
+
* `.head()`, identically at build and on the client — `meta` is compiled into the route and
|
|
305
|
+
* shipped in the client manifest, so a client-only route (dynamic, no `.generate()`, whose
|
|
306
|
+
* `.load()` data is `{}` on the client) can feed static per-route config into its render.
|
|
307
|
+
*/
|
|
308
|
+
readonly meta: Record<string, unknown>;
|
|
299
309
|
/**
|
|
300
310
|
* Build a link to a named route by pattern substitution — the framework delivers
|
|
301
311
|
* this on the context (same output as `app.router.toUrl`), so render/head build
|
|
@@ -376,21 +386,16 @@ interface GenerateContext {
|
|
|
376
386
|
readonly has: (name: string) => boolean;
|
|
377
387
|
}
|
|
378
388
|
/**
|
|
379
|
-
* Context handed to a route's `.layout()` wrapper
|
|
380
|
-
*
|
|
381
|
-
*
|
|
382
|
-
* `RouteContext` because the layout is the only handler that needs `meta`; keeping
|
|
383
|
-
* it on its own type leaves `.render()`/`.head()` contexts unchanged.
|
|
389
|
+
* Context handed to a route's `.layout()` wrapper — identical to {@link RouteContext}
|
|
390
|
+
* (which now carries `meta` for every handler). Retained as a named alias so existing
|
|
391
|
+
* `.layout((ctx, children) => …)` typings keep compiling.
|
|
384
392
|
*
|
|
385
393
|
* @remarks
|
|
386
394
|
* The layout is applied in the SSG render path ONLY. On client (SPA) navigation the
|
|
387
|
-
* chrome is persistent and the layout is intentionally NOT re-applied — only the
|
|
388
|
-
*
|
|
395
|
+
* chrome is persistent and the layout is intentionally NOT re-applied — only the inner
|
|
396
|
+
* swap region is replaced. See `build`'s pages phase and `spa`'s kernel.
|
|
389
397
|
*/
|
|
390
|
-
|
|
391
|
-
/** The route's `.meta()` bag (e.g. `{ activeTab: "home" }`). */
|
|
392
|
-
readonly meta: Record<string, unknown>;
|
|
393
|
-
}
|
|
398
|
+
type LayoutContext<S extends RouteState> = RouteContext<S>;
|
|
394
399
|
/** Head metadata produced by a route's `.head()` handler. */
|
|
395
400
|
interface HeadConfig$1 {
|
|
396
401
|
/** Document title. */
|
|
@@ -945,12 +950,25 @@ interface ResolvedSpaConfig {
|
|
|
945
950
|
/** Pre-registered components. */
|
|
946
951
|
components: ComponentDef[];
|
|
947
952
|
}
|
|
948
|
-
/**
|
|
953
|
+
/**
|
|
954
|
+
* Context handed to every component lifecycle hook — the bound element + page data,
|
|
955
|
+
* plus the matched route's `params`/`meta`/`locale` and a link builder, so an island
|
|
956
|
+
* can read its route context (e.g. a `card` route's `ctx.meta.focus` + `ctx.params.id`)
|
|
957
|
+
* directly, without the page bridging it through `data-*` attributes.
|
|
958
|
+
*/
|
|
949
959
|
interface ComponentContext {
|
|
950
960
|
/** The element the component instance is bound to. */
|
|
951
961
|
el: Element;
|
|
952
962
|
/** Page data extracted from the `script#__DATA__` payload. */
|
|
953
963
|
data: PageData;
|
|
964
|
+
/** Resolved path params of the route matched for the current URL (empty if unmatched). */
|
|
965
|
+
readonly params: Record<string, string | undefined>;
|
|
966
|
+
/** The matched route's `.meta()` bag (empty if unmatched). */
|
|
967
|
+
readonly meta: Record<string, unknown>;
|
|
968
|
+
/** Active locale for the current route (empty string if unknown). */
|
|
969
|
+
readonly locale: string;
|
|
970
|
+
/** Build a link to a named route by pattern substitution (same output as `app.router.toUrl`). */
|
|
971
|
+
readonly url: (name: string, params?: Record<string, string>) => string;
|
|
954
972
|
}
|
|
955
973
|
/** Lifecycle hooks a component may implement. */
|
|
956
974
|
interface ComponentHooks {
|
package/dist/index.d.mts
CHANGED
|
@@ -286,7 +286,10 @@ interface RouteState<P extends string = string, D = unknown> {
|
|
|
286
286
|
/** Loaded data type produced by `.load()` (widened only by `.load()`). */
|
|
287
287
|
readonly data: D;
|
|
288
288
|
}
|
|
289
|
-
/**
|
|
289
|
+
/**
|
|
290
|
+
* Render-time context handed to `.render()` / `.head()`; `data` is `.load()`'s return,
|
|
291
|
+
* `meta` the route's `.meta()` bag.
|
|
292
|
+
*/
|
|
290
293
|
interface RouteContext<S extends RouteState> {
|
|
291
294
|
/** Resolved path params. */
|
|
292
295
|
readonly params: S["params"];
|
|
@@ -294,6 +297,13 @@ interface RouteContext<S extends RouteState> {
|
|
|
294
297
|
readonly data: S["data"];
|
|
295
298
|
/** Active locale for this render. */
|
|
296
299
|
readonly locale: string;
|
|
300
|
+
/**
|
|
301
|
+
* The route's `.meta()` bag (e.g. `{ activeTab: "home" }`). Available in `.render()` and
|
|
302
|
+
* `.head()`, identically at build and on the client — `meta` is compiled into the route and
|
|
303
|
+
* shipped in the client manifest, so a client-only route (dynamic, no `.generate()`, whose
|
|
304
|
+
* `.load()` data is `{}` on the client) can feed static per-route config into its render.
|
|
305
|
+
*/
|
|
306
|
+
readonly meta: Record<string, unknown>;
|
|
297
307
|
/**
|
|
298
308
|
* Build a link to a named route by pattern substitution — the framework delivers
|
|
299
309
|
* this on the context (same output as `app.router.toUrl`), so render/head build
|
|
@@ -374,21 +384,16 @@ interface GenerateContext {
|
|
|
374
384
|
readonly has: (name: string) => boolean;
|
|
375
385
|
}
|
|
376
386
|
/**
|
|
377
|
-
* Context handed to a route's `.layout()` wrapper
|
|
378
|
-
*
|
|
379
|
-
*
|
|
380
|
-
* `RouteContext` because the layout is the only handler that needs `meta`; keeping
|
|
381
|
-
* it on its own type leaves `.render()`/`.head()` contexts unchanged.
|
|
387
|
+
* Context handed to a route's `.layout()` wrapper — identical to {@link RouteContext}
|
|
388
|
+
* (which now carries `meta` for every handler). Retained as a named alias so existing
|
|
389
|
+
* `.layout((ctx, children) => …)` typings keep compiling.
|
|
382
390
|
*
|
|
383
391
|
* @remarks
|
|
384
392
|
* The layout is applied in the SSG render path ONLY. On client (SPA) navigation the
|
|
385
|
-
* chrome is persistent and the layout is intentionally NOT re-applied — only the
|
|
386
|
-
*
|
|
393
|
+
* chrome is persistent and the layout is intentionally NOT re-applied — only the inner
|
|
394
|
+
* swap region is replaced. See `build`'s pages phase and `spa`'s kernel.
|
|
387
395
|
*/
|
|
388
|
-
|
|
389
|
-
/** The route's `.meta()` bag (e.g. `{ activeTab: "home" }`). */
|
|
390
|
-
readonly meta: Record<string, unknown>;
|
|
391
|
-
}
|
|
396
|
+
type LayoutContext<S extends RouteState> = RouteContext<S>;
|
|
392
397
|
/** Head metadata produced by a route's `.head()` handler. */
|
|
393
398
|
interface HeadConfig$1 {
|
|
394
399
|
/** Document title. */
|
|
@@ -943,12 +948,25 @@ interface ResolvedSpaConfig {
|
|
|
943
948
|
/** Pre-registered components. */
|
|
944
949
|
components: ComponentDef[];
|
|
945
950
|
}
|
|
946
|
-
/**
|
|
951
|
+
/**
|
|
952
|
+
* Context handed to every component lifecycle hook — the bound element + page data,
|
|
953
|
+
* plus the matched route's `params`/`meta`/`locale` and a link builder, so an island
|
|
954
|
+
* can read its route context (e.g. a `card` route's `ctx.meta.focus` + `ctx.params.id`)
|
|
955
|
+
* directly, without the page bridging it through `data-*` attributes.
|
|
956
|
+
*/
|
|
947
957
|
interface ComponentContext {
|
|
948
958
|
/** The element the component instance is bound to. */
|
|
949
959
|
el: Element;
|
|
950
960
|
/** Page data extracted from the `script#__DATA__` payload. */
|
|
951
961
|
data: PageData;
|
|
962
|
+
/** Resolved path params of the route matched for the current URL (empty if unmatched). */
|
|
963
|
+
readonly params: Record<string, string | undefined>;
|
|
964
|
+
/** The matched route's `.meta()` bag (empty if unmatched). */
|
|
965
|
+
readonly meta: Record<string, unknown>;
|
|
966
|
+
/** Active locale for the current route (empty string if unknown). */
|
|
967
|
+
readonly locale: string;
|
|
968
|
+
/** Build a link to a named route by pattern substitution (same output as `app.router.toUrl`). */
|
|
969
|
+
readonly url: (name: string, params?: Record<string, string>) => string;
|
|
952
970
|
}
|
|
953
971
|
/** Lifecycle hooks a component may implement. */
|
|
954
972
|
interface ComponentHooks {
|
package/dist/index.mjs
CHANGED
|
@@ -4725,8 +4725,8 @@ function composeHeadHtml(ctx, instance, url, routeContext, data) {
|
|
|
4725
4725
|
* layout is NOT re-applied on navigation), then serialize with
|
|
4726
4726
|
* preact-render-to-string. Returns `""` when the route has no `.render()`.
|
|
4727
4727
|
*
|
|
4728
|
-
* @param definition - The route definition (provides `.render()`/`.layout()
|
|
4729
|
-
* @param routeContext - The route context (params/data/locale/url)
|
|
4728
|
+
* @param definition - The route definition (provides `.render()`/`.layout()`).
|
|
4729
|
+
* @param routeContext - The route context (params/data/locale/meta/url); `meta` flows to the layout.
|
|
4730
4730
|
* @returns The SSR-rendered body HTML, or `""` when the route has no `.render()`.
|
|
4731
4731
|
* @example
|
|
4732
4732
|
* ```ts
|
|
@@ -4736,11 +4736,7 @@ function composeHeadHtml(ctx, instance, url, routeContext, data) {
|
|
|
4736
4736
|
function renderBody(definition, routeContext) {
|
|
4737
4737
|
const vnode = definition._handlers.render?.(routeContext);
|
|
4738
4738
|
if (!vnode) return "";
|
|
4739
|
-
|
|
4740
|
-
...routeContext,
|
|
4741
|
-
meta: definition._meta
|
|
4742
|
-
};
|
|
4743
|
-
return renderToString(definition._handlers.layout ? definition._handlers.layout(layoutContext, vnode) : vnode);
|
|
4739
|
+
return renderToString(definition._handlers.layout ? definition._handlers.layout(routeContext, vnode) : vnode);
|
|
4744
4740
|
}
|
|
4745
4741
|
/**
|
|
4746
4742
|
* Hash a page's render inputs (its loaded data) for the render cache. `null` when the
|
|
@@ -4874,6 +4870,7 @@ async function renderInstance(ctx, instance, shell, reuse) {
|
|
|
4874
4870
|
params,
|
|
4875
4871
|
data,
|
|
4876
4872
|
locale,
|
|
4873
|
+
meta: definition._meta,
|
|
4877
4874
|
url: (routeName, routeParams = {}) => router.toUrl(routeName, routeParams)
|
|
4878
4875
|
};
|
|
4879
4876
|
const parts = {
|
|
@@ -9085,6 +9082,23 @@ const ERROR_PREFIX$2 = "[web]";
|
|
|
9085
9082
|
/** The set of legal hook names, frozen for O(1) membership checks. */
|
|
9086
9083
|
const HOOK_NAME_SET = new Set(COMPONENT_HOOK_NAMES);
|
|
9087
9084
|
/**
|
|
9085
|
+
* No-op link builder for the {@link EMPTY_ROUTE} slice (used when no route matched).
|
|
9086
|
+
*
|
|
9087
|
+
* @returns An empty string.
|
|
9088
|
+
* @example
|
|
9089
|
+
* const href = noUrl();
|
|
9090
|
+
*/
|
|
9091
|
+
function noUrl() {
|
|
9092
|
+
return "";
|
|
9093
|
+
}
|
|
9094
|
+
/** Empty route slice — used for mounts with no matched route (headless, tests, public `scan()`). */
|
|
9095
|
+
const EMPTY_ROUTE = {
|
|
9096
|
+
params: {},
|
|
9097
|
+
meta: {},
|
|
9098
|
+
locale: "",
|
|
9099
|
+
url: noUrl
|
|
9100
|
+
};
|
|
9101
|
+
/**
|
|
9088
9102
|
* Validate a single hook entry: its key must be a known hook name and its value
|
|
9089
9103
|
* must be a function. Throws fail-fast on the first violation.
|
|
9090
9104
|
*
|
|
@@ -9172,18 +9186,25 @@ function runHook(instance, hook, ctx) {
|
|
|
9172
9186
|
instance.def.hooks[hook]?.(ctx);
|
|
9173
9187
|
}
|
|
9174
9188
|
/**
|
|
9175
|
-
* Builds the component context handed to a hook
|
|
9189
|
+
* Builds the component context handed to a hook: the bound element + page data, merged
|
|
9190
|
+
* with the matched route's slice (params/meta/locale/url). Defaults to {@link EMPTY_ROUTE}
|
|
9191
|
+
* when no route is supplied (headless, tests, public `scan()`).
|
|
9176
9192
|
*
|
|
9177
9193
|
* @param element - The element the instance is bound to.
|
|
9178
9194
|
* @param data - The current page data payload.
|
|
9195
|
+
* @param route - The matched-route slice for the current URL.
|
|
9179
9196
|
* @returns The hook context.
|
|
9180
9197
|
* @example
|
|
9181
|
-
* const ctx = makeContext(element, data);
|
|
9198
|
+
* const ctx = makeContext(element, data, route);
|
|
9182
9199
|
*/
|
|
9183
|
-
function makeContext(element, data) {
|
|
9200
|
+
function makeContext(element, data, route = EMPTY_ROUTE) {
|
|
9184
9201
|
return {
|
|
9185
9202
|
el: element,
|
|
9186
|
-
data
|
|
9203
|
+
data,
|
|
9204
|
+
params: route.params,
|
|
9205
|
+
meta: route.meta,
|
|
9206
|
+
locale: route.locale,
|
|
9207
|
+
url: route.url
|
|
9187
9208
|
};
|
|
9188
9209
|
}
|
|
9189
9210
|
/**
|
|
@@ -9197,17 +9218,18 @@ function makeContext(element, data) {
|
|
|
9197
9218
|
* @param swapArea - The swap-region element, or null when none was found.
|
|
9198
9219
|
* @param data - The current page data payload.
|
|
9199
9220
|
* @param element - The candidate element carrying a `data-component` attribute.
|
|
9221
|
+
* @param route - The matched-route slice for the current URL (params/meta/locale/url).
|
|
9200
9222
|
* @example
|
|
9201
|
-
* mountElement(state, emit, swapArea, data, element);
|
|
9223
|
+
* mountElement(state, emit, swapArea, data, element, route);
|
|
9202
9224
|
*/
|
|
9203
|
-
function mountElement(state, emit, swapArea, data, element) {
|
|
9225
|
+
function mountElement(state, emit, swapArea, data, element, route = EMPTY_ROUTE) {
|
|
9204
9226
|
if (state.instances.has(element)) return;
|
|
9205
9227
|
const name = element.dataset.component;
|
|
9206
9228
|
if (!name) return;
|
|
9207
9229
|
const definition = state.registeredComponents.get(name);
|
|
9208
9230
|
if (!definition) return;
|
|
9209
9231
|
const instance = createInstance(definition, element, swapArea ? !swapArea.contains(element) : true);
|
|
9210
|
-
const ctx = makeContext(element, data);
|
|
9232
|
+
const ctx = makeContext(element, data, route);
|
|
9211
9233
|
runHook(instance, "onCreate", ctx);
|
|
9212
9234
|
runHook(instance, "onMount", ctx);
|
|
9213
9235
|
state.instances.set(element, instance);
|
|
@@ -9225,14 +9247,15 @@ function mountElement(state, emit, swapArea, data, element) {
|
|
|
9225
9247
|
* @param state - The plugin state (registeredComponents + instances).
|
|
9226
9248
|
* @param emit - The event emitter for spa:component-mount.
|
|
9227
9249
|
* @param swapSelector - CSS selector bounding page-specific components.
|
|
9250
|
+
* @param route - The matched-route slice for the current URL (params/meta/locale/url).
|
|
9228
9251
|
* @example
|
|
9229
|
-
* scanAndMount(state, emit, "main > section");
|
|
9252
|
+
* scanAndMount(state, emit, "main > section", route);
|
|
9230
9253
|
*/
|
|
9231
|
-
function scanAndMount(state, emit, swapSelector) {
|
|
9254
|
+
function scanAndMount(state, emit, swapSelector, route = EMPTY_ROUTE) {
|
|
9232
9255
|
if (typeof document === "undefined") return;
|
|
9233
9256
|
const swapArea = document.querySelector(swapSelector);
|
|
9234
9257
|
const data = extractPageData(document);
|
|
9235
|
-
for (const element of document.querySelectorAll("[data-component]")) mountElement(state, emit, swapArea, data, element);
|
|
9258
|
+
for (const element of document.querySelectorAll("[data-component]")) mountElement(state, emit, swapArea, data, element, route);
|
|
9236
9259
|
}
|
|
9237
9260
|
/**
|
|
9238
9261
|
* Unmounts page-specific instances inside the swap region (runs `onUnMount`
|
|
@@ -9298,12 +9321,13 @@ function notifyNavStart(state) {
|
|
|
9298
9321
|
* instances were already destroyed and re-created by the swap).
|
|
9299
9322
|
*
|
|
9300
9323
|
* @param state - The plugin state holding live instances.
|
|
9324
|
+
* @param route - The matched-route slice for the destination URL (params/meta/locale/url).
|
|
9301
9325
|
* @example
|
|
9302
|
-
* notifyNavEnd(state);
|
|
9326
|
+
* notifyNavEnd(state, route);
|
|
9303
9327
|
*/
|
|
9304
|
-
function notifyNavEnd(state) {
|
|
9328
|
+
function notifyNavEnd(state, route = EMPTY_ROUTE) {
|
|
9305
9329
|
const data = typeof document === "undefined" ? {} : extractPageData(document);
|
|
9306
|
-
for (const [element, instance] of state.instances) if (instance.persistent) runHook(instance, "onNavEnd", makeContext(element, data));
|
|
9330
|
+
for (const [element, instance] of state.instances) if (instance.persistent) runHook(instance, "onNavEnd", makeContext(element, data, route));
|
|
9307
9331
|
}
|
|
9308
9332
|
//#endregion
|
|
9309
9333
|
//#region src/plugins/spa/head.ts
|
|
@@ -9946,6 +9970,26 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
9946
9970
|
});
|
|
9947
9971
|
};
|
|
9948
9972
|
/**
|
|
9973
|
+
* Build the matched-route slice (params/meta/locale/url) for the component context at `path`,
|
|
9974
|
+
* so islands read their route's params/meta directly. An unmatched path yields an empty slice.
|
|
9975
|
+
*
|
|
9976
|
+
* @param path - The URL (pathname + search) to match.
|
|
9977
|
+
* @returns The route slice for the matched route.
|
|
9978
|
+
* @example
|
|
9979
|
+
* scanAndMount(state, emit, resolved.swapSelector, componentRouteContext(pathname));
|
|
9980
|
+
*/
|
|
9981
|
+
const componentRouteContext = (path) => {
|
|
9982
|
+
const matchPath = path.split("?")[0] ?? path;
|
|
9983
|
+
const hit = deps.router.match(matchPath);
|
|
9984
|
+
const locale = hit?.params.lang ?? (typeof document === "undefined" ? "" : document.documentElement.lang) ?? "";
|
|
9985
|
+
return {
|
|
9986
|
+
params: hit?.params ?? {},
|
|
9987
|
+
meta: hit?.route._meta ?? {},
|
|
9988
|
+
locale,
|
|
9989
|
+
url: (name, params = {}) => deps.router.toUrl(name, params)
|
|
9990
|
+
};
|
|
9991
|
+
};
|
|
9992
|
+
/**
|
|
9949
9993
|
* Process one navigation: head-sync, unmount, swap, re-mount, emit navigated.
|
|
9950
9994
|
* When the region cannot be swapped (either document lacks the swap selector)
|
|
9951
9995
|
* the SPA nav cannot complete — the head is already synced and the islands torn
|
|
@@ -9963,8 +10007,9 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
9963
10007
|
syncHead(deps.head, doc);
|
|
9964
10008
|
unmountPageSpecific(state, emit);
|
|
9965
10009
|
if (!swapRegion(doc, resolved.swapSelector, resolved.viewTransitions, () => {
|
|
9966
|
-
|
|
9967
|
-
|
|
10010
|
+
const routeSlice = componentRouteContext(pathname);
|
|
10011
|
+
scanAndMount(state, emit, resolved.swapSelector, routeSlice);
|
|
10012
|
+
notifyNavEnd(state, routeSlice);
|
|
9968
10013
|
}, applyPendingScroll)) {
|
|
9969
10014
|
handleError();
|
|
9970
10015
|
location.href = pathname;
|
|
@@ -10033,6 +10078,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10033
10078
|
params: hit.params,
|
|
10034
10079
|
data,
|
|
10035
10080
|
locale,
|
|
10081
|
+
meta: hit.route._meta,
|
|
10036
10082
|
url: (routeName, routeParams = {}) => deps.router.toUrl(routeName, routeParams)
|
|
10037
10083
|
};
|
|
10038
10084
|
const vnode = hit.route._handlers.render(routeContext);
|
|
@@ -10064,6 +10110,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10064
10110
|
if (signal?.aborted) return;
|
|
10065
10111
|
syncDataHead(deps.head, route, routeContext);
|
|
10066
10112
|
unmountPageSpecific(state, emit);
|
|
10113
|
+
const routeSlice = componentRouteContext(pathname);
|
|
10067
10114
|
/**
|
|
10068
10115
|
* Render the VNode into the region and re-mount its islands in one paint — the
|
|
10069
10116
|
* swap body handed to `runSwap` (optionally wrapped in a View Transition).
|
|
@@ -10075,8 +10122,8 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10075
10122
|
*/
|
|
10076
10123
|
const renderAndMount = () => {
|
|
10077
10124
|
renderVNode(vnode, region);
|
|
10078
|
-
scanAndMount(state, emit, resolved.swapSelector);
|
|
10079
|
-
notifyNavEnd(state);
|
|
10125
|
+
scanAndMount(state, emit, resolved.swapSelector, routeSlice);
|
|
10126
|
+
notifyNavEnd(state, routeSlice);
|
|
10080
10127
|
};
|
|
10081
10128
|
runSwap(renderAndMount, resolved.viewTransitions, applyPendingScroll);
|
|
10082
10129
|
state.currentUrl = pathname;
|
|
@@ -10121,15 +10168,16 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10121
10168
|
* await bootRender("/b/abc123");
|
|
10122
10169
|
*/
|
|
10123
10170
|
const bootRender = async (pathname) => {
|
|
10171
|
+
const routeSlice = componentRouteContext(pathname);
|
|
10124
10172
|
const resolvedRender = await resolveDataRender(pathname);
|
|
10125
10173
|
if (resolvedRender === false) {
|
|
10126
|
-
scanAndMount(state, emit, resolved.swapSelector);
|
|
10174
|
+
scanAndMount(state, emit, resolved.swapSelector, routeSlice);
|
|
10127
10175
|
return;
|
|
10128
10176
|
}
|
|
10129
10177
|
const { vnode, region } = resolvedRender;
|
|
10130
10178
|
const { renderVNode } = await import("./render-BNe0s7fr.mjs");
|
|
10131
10179
|
renderVNode(vnode, region);
|
|
10132
|
-
scanAndMount(state, emit, resolved.swapSelector);
|
|
10180
|
+
scanAndMount(state, emit, resolved.swapSelector, routeSlice);
|
|
10133
10181
|
};
|
|
10134
10182
|
/**
|
|
10135
10183
|
* Unified navigation: try the client DATA path first (only when the `data`
|
|
@@ -10179,7 +10227,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10179
10227
|
const matchPath = state.currentUrl.split("?")[0] ?? state.currentUrl;
|
|
10180
10228
|
const hit = deps.router.match(matchPath);
|
|
10181
10229
|
if (hit?.route._handlers.render && isClientOnlyRoute(deps.router.mode(), hit.route)) bootRender(state.currentUrl);
|
|
10182
|
-
else scanAndMount(state, emit, resolved.swapSelector);
|
|
10230
|
+
else scanAndMount(state, emit, resolved.swapSelector, componentRouteContext(state.currentUrl));
|
|
10183
10231
|
state.started = true;
|
|
10184
10232
|
},
|
|
10185
10233
|
/**
|
|
@@ -10210,7 +10258,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10210
10258
|
* kernel.scan();
|
|
10211
10259
|
*/
|
|
10212
10260
|
scan() {
|
|
10213
|
-
scanAndMount(state, emit, resolved.swapSelector);
|
|
10261
|
+
scanAndMount(state, emit, resolved.swapSelector, componentRouteContext(state.currentUrl));
|
|
10214
10262
|
},
|
|
10215
10263
|
/**
|
|
10216
10264
|
* Tear down router listeners, dispose all instances, reset boot state.
|
package/package.json
CHANGED