@moku-labs/web 1.13.1 → 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 +135 -22
- package/dist/index.cjs +160 -40
- package/dist/index.d.cts +31 -13
- package/dist/index.d.mts +31 -13
- package/dist/index.mjs +160 -40
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1084,6 +1084,38 @@ function dynamicSegmentCount(pattern) {
|
|
|
1084
1084
|
return count;
|
|
1085
1085
|
}
|
|
1086
1086
|
/**
|
|
1087
|
+
* Whether a route is rendered ENTIRELY on the client in `spa` mode: a dynamic route
|
|
1088
|
+
* (≥1 non-lang param) that declares no build-time `.generate()` enumerator, so its
|
|
1089
|
+
* concrete param paths are unknown until runtime.
|
|
1090
|
+
*
|
|
1091
|
+
* The build SKIPS such a route — emitting a static page for it would write one
|
|
1092
|
+
* param-less shell whose path (`/b/{id}`) matches no file (a 404) and carries no
|
|
1093
|
+
* param for the islands to read. Instead the SPA client-renders it from the URL on
|
|
1094
|
+
* boot and on navigation. Build and client share this ONE predicate (the same way
|
|
1095
|
+
* `dynamicSegmentCount`/`bySpecificity` are shared) so the two sides can never
|
|
1096
|
+
* disagree about which routes are pre-rendered vs. client-only.
|
|
1097
|
+
*
|
|
1098
|
+
* Static routes (`/`) and dynamic routes WITH `.generate()` are pre-rendered as
|
|
1099
|
+
* usual and so are NOT client-only. In `ssg`/`hybrid` mode nothing is client-only
|
|
1100
|
+
* (the build pre-renders every route), so this is always `false` outside `spa`.
|
|
1101
|
+
*
|
|
1102
|
+
* @param mode - The global render mode (`router.mode()`).
|
|
1103
|
+
* @param route - The route to test (only its `pattern` + `.generate()` presence are read).
|
|
1104
|
+
* @param route.pattern - The route's URL pattern string.
|
|
1105
|
+
* @param route._handlers - The route's handler bag.
|
|
1106
|
+
* @param route._handlers.generate - The build-only static-paths enumerator, if any (presence only).
|
|
1107
|
+
* @returns `true` when the route is client-only (spa mode, dynamic, no `.generate()`).
|
|
1108
|
+
* @example
|
|
1109
|
+
* ```ts
|
|
1110
|
+
* isClientOnlyRoute("spa", { pattern: "/b/{id}", _handlers: {} }); // true
|
|
1111
|
+
* isClientOnlyRoute("spa", { pattern: "/", _handlers: {} }); // false (static)
|
|
1112
|
+
* isClientOnlyRoute("hybrid", { pattern: "/b/{id}", _handlers: {} }); // false (not spa)
|
|
1113
|
+
* ```
|
|
1114
|
+
*/
|
|
1115
|
+
function isClientOnlyRoute(mode, route) {
|
|
1116
|
+
return mode === "spa" && route._handlers.generate === void 0 && dynamicSegmentCount(route.pattern) > 0;
|
|
1117
|
+
}
|
|
1118
|
+
/**
|
|
1087
1119
|
* Comparator that orders two routes most-specific-first (fewest dynamic segments
|
|
1088
1120
|
* first). Equal specificity yields `0` so a stable sort preserves declaration
|
|
1089
1121
|
* order — the exact ordering the compiled matcher table uses, guaranteeing
|
|
@@ -4528,16 +4560,23 @@ function resolveEntry(byPattern, definition) {
|
|
|
4528
4560
|
* generate context is the spec `{ locale, require, has }`, so a `.generate()` handler
|
|
4529
4561
|
* pulls sibling APIs the spec way.
|
|
4530
4562
|
*
|
|
4563
|
+
* In `spa` mode a client-only route (dynamic, no `.generate()`) is SKIPPED entirely
|
|
4564
|
+
* (`[]`) — it is rendered on the client from the URL, so emitting a static param-less
|
|
4565
|
+
* shell here would only write a file at the wrong path (a 404 for any real param path)
|
|
4566
|
+
* carrying no param. See {@link isClientOnlyRoute}.
|
|
4567
|
+
*
|
|
4531
4568
|
* @param definition - The route definition from the manifest.
|
|
4532
4569
|
* @param locale - The active locale to generate param sets for.
|
|
4570
|
+
* @param mode - The global render mode (`router.mode()`); gates the spa client-only skip.
|
|
4533
4571
|
* @param ctx - Plugin context (provides `require`/`has` for the generate context).
|
|
4534
|
-
* @returns The param sets for this route+locale (`[{}]` when there is no `.generate()`).
|
|
4572
|
+
* @returns The param sets for this route+locale (`[{}]` when there is no `.generate()`; `[]` when client-only).
|
|
4535
4573
|
* @example
|
|
4536
4574
|
* ```ts
|
|
4537
|
-
* const paramSets = await generateParamSets(def, "en", ctx);
|
|
4575
|
+
* const paramSets = await generateParamSets(def, "en", "hybrid", ctx);
|
|
4538
4576
|
* ```
|
|
4539
4577
|
*/
|
|
4540
|
-
async function generateParameterSets(definition, locale, ctx) {
|
|
4578
|
+
async function generateParameterSets(definition, locale, mode, ctx) {
|
|
4579
|
+
if (isClientOnlyRoute(mode, definition)) return [];
|
|
4541
4580
|
const generateContext = {
|
|
4542
4581
|
locale,
|
|
4543
4582
|
require: ctx.require,
|
|
@@ -4561,21 +4600,22 @@ async function generateParameterSets(definition, locale, ctx) {
|
|
|
4561
4600
|
* @param locales - Active locale codes from i18n.
|
|
4562
4601
|
* @param defaultLocale - The i18n default locale (kept when locales collapse to one file).
|
|
4563
4602
|
* @param byPattern - Pattern→compiled-`TypedRoute` map (see {@link makeEntryMap}).
|
|
4603
|
+
* @param mode - The global render mode (`router.mode()`); gates the spa client-only skip.
|
|
4564
4604
|
* @param ctx - Plugin context (provides `require`/`has` for the generate context).
|
|
4565
4605
|
* @returns The flattened, file-deduplicated list of page instances for this route.
|
|
4566
4606
|
* @example
|
|
4567
4607
|
* ```ts
|
|
4568
|
-
* await expandRoute(def, ["en"], "en", byPattern, ctx);
|
|
4608
|
+
* await expandRoute(def, ["en"], "en", byPattern, "hybrid", ctx);
|
|
4569
4609
|
* ```
|
|
4570
4610
|
*/
|
|
4571
|
-
async function expandRoute(definition, locales, defaultLocale, byPattern, ctx) {
|
|
4611
|
+
async function expandRoute(definition, locales, defaultLocale, byPattern, mode, ctx) {
|
|
4572
4612
|
const entry = resolveEntry(byPattern, definition);
|
|
4573
4613
|
const { name } = entry;
|
|
4574
4614
|
const orderedLocales = [defaultLocale, ...locales.filter((locale) => locale !== defaultLocale)];
|
|
4575
4615
|
const instances = [];
|
|
4576
4616
|
const claimedFiles = /* @__PURE__ */ new Set();
|
|
4577
4617
|
for (const locale of orderedLocales) {
|
|
4578
|
-
const parameterSets = await generateParameterSets(definition, locale, ctx);
|
|
4618
|
+
const parameterSets = await generateParameterSets(definition, locale, mode, ctx);
|
|
4579
4619
|
for (const raw of parameterSets) {
|
|
4580
4620
|
const params = raw ?? {};
|
|
4581
4621
|
const file = entry.toFile(params);
|
|
@@ -4698,8 +4738,8 @@ function composeHeadHtml(ctx, instance, url, routeContext, data) {
|
|
|
4698
4738
|
* layout is NOT re-applied on navigation), then serialize with
|
|
4699
4739
|
* preact-render-to-string. Returns `""` when the route has no `.render()`.
|
|
4700
4740
|
*
|
|
4701
|
-
* @param definition - The route definition (provides `.render()`/`.layout()
|
|
4702
|
-
* @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.
|
|
4703
4743
|
* @returns The SSR-rendered body HTML, or `""` when the route has no `.render()`.
|
|
4704
4744
|
* @example
|
|
4705
4745
|
* ```ts
|
|
@@ -4709,11 +4749,7 @@ function composeHeadHtml(ctx, instance, url, routeContext, data) {
|
|
|
4709
4749
|
function renderBody(definition, routeContext) {
|
|
4710
4750
|
const vnode = definition._handlers.render?.(routeContext);
|
|
4711
4751
|
if (!vnode) return "";
|
|
4712
|
-
|
|
4713
|
-
...routeContext,
|
|
4714
|
-
meta: definition._meta
|
|
4715
|
-
};
|
|
4716
|
-
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);
|
|
4717
4753
|
}
|
|
4718
4754
|
/**
|
|
4719
4755
|
* Hash a page's render inputs (its loaded data) for the render cache. `null` when the
|
|
@@ -4847,6 +4883,7 @@ async function renderInstance(ctx, instance, shell, reuse) {
|
|
|
4847
4883
|
params,
|
|
4848
4884
|
data,
|
|
4849
4885
|
locale,
|
|
4886
|
+
meta: definition._meta,
|
|
4850
4887
|
url: (routeName, routeParams = {}) => router.toUrl(routeName, routeParams)
|
|
4851
4888
|
};
|
|
4852
4889
|
const parts = {
|
|
@@ -4909,15 +4946,16 @@ async function prepareShell(ctx) {
|
|
|
4909
4946
|
* @param locales - Active locale codes from i18n.
|
|
4910
4947
|
* @param defaultLocale - The i18n default locale (kept when a route's locales collapse).
|
|
4911
4948
|
* @param byPattern - Pattern→compiled-`TypedRoute` map (see {@link makeEntryMap}).
|
|
4949
|
+
* @param mode - The global render mode (`router.mode()`); gates the spa client-only skip.
|
|
4912
4950
|
* @param ctx - Plugin context (provides `require`/`has` for generate contexts).
|
|
4913
4951
|
* @returns The flattened list of page instances to render.
|
|
4914
4952
|
* @example
|
|
4915
4953
|
* ```ts
|
|
4916
|
-
* const instances = await expandAllInstances(manifest, ["en"], "en", byPattern, ctx);
|
|
4954
|
+
* const instances = await expandAllInstances(manifest, ["en"], "en", byPattern, "hybrid", ctx);
|
|
4917
4955
|
* ```
|
|
4918
4956
|
*/
|
|
4919
|
-
async function expandAllInstances(manifest, locales, defaultLocale, byPattern, ctx) {
|
|
4920
|
-
return (await Promise.all(manifest.map((definition) => expandRoute(definition, locales, defaultLocale, byPattern, ctx)))).flat();
|
|
4957
|
+
async function expandAllInstances(manifest, locales, defaultLocale, byPattern, mode, ctx) {
|
|
4958
|
+
return (await Promise.all(manifest.map((definition) => expandRoute(definition, locales, defaultLocale, byPattern, mode, ctx)))).flat();
|
|
4921
4959
|
}
|
|
4922
4960
|
/**
|
|
4923
4961
|
* Persist per-page client-data sidecars when the app opts into client navigation
|
|
@@ -5027,14 +5065,15 @@ async function renderInBatches(items, batchSize, worker) {
|
|
|
5027
5065
|
async function renderPages(ctx, options) {
|
|
5028
5066
|
const reuse = options?.reuse === true;
|
|
5029
5067
|
const router = ctx.require(routerPlugin);
|
|
5068
|
+
const mode = router.mode();
|
|
5030
5069
|
const manifest = router.manifest();
|
|
5031
5070
|
ctx.state.manifest = [...manifest];
|
|
5032
5071
|
const locales = ctx.require(i18nPlugin).locales();
|
|
5033
5072
|
const byPattern = makeEntryMap(router);
|
|
5034
5073
|
if (!reuse) ctx.state.renderCache.clear();
|
|
5035
5074
|
const shell = await prepareShell(ctx);
|
|
5036
|
-
const rendered = (await renderInBatches(await expandAllInstances(manifest, locales, shell.defaultLocale, byPattern, ctx), reuse ? INCREMENTAL_BATCH_SIZE : RENDER_BATCH_SIZE, (instance) => renderInstance(ctx, instance, shell, reuse))).flat();
|
|
5037
|
-
await writeDataSidecars(ctx, rendered,
|
|
5075
|
+
const rendered = (await renderInBatches(await expandAllInstances(manifest, locales, shell.defaultLocale, byPattern, mode, ctx), reuse ? INCREMENTAL_BATCH_SIZE : RENDER_BATCH_SIZE, (instance) => renderInstance(ctx, instance, shell, reuse))).flat();
|
|
5076
|
+
await writeDataSidecars(ctx, rendered, mode);
|
|
5038
5077
|
ctx.log.debug("build:pages", { count: rendered.length });
|
|
5039
5078
|
return {
|
|
5040
5079
|
pageCount: rendered.length,
|
|
@@ -9056,6 +9095,23 @@ const ERROR_PREFIX$2 = "[web]";
|
|
|
9056
9095
|
/** The set of legal hook names, frozen for O(1) membership checks. */
|
|
9057
9096
|
const HOOK_NAME_SET = new Set(COMPONENT_HOOK_NAMES);
|
|
9058
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
|
+
/**
|
|
9059
9115
|
* Validate a single hook entry: its key must be a known hook name and its value
|
|
9060
9116
|
* must be a function. Throws fail-fast on the first violation.
|
|
9061
9117
|
*
|
|
@@ -9143,18 +9199,25 @@ function runHook(instance, hook, ctx) {
|
|
|
9143
9199
|
instance.def.hooks[hook]?.(ctx);
|
|
9144
9200
|
}
|
|
9145
9201
|
/**
|
|
9146
|
-
* 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()`).
|
|
9147
9205
|
*
|
|
9148
9206
|
* @param element - The element the instance is bound to.
|
|
9149
9207
|
* @param data - The current page data payload.
|
|
9208
|
+
* @param route - The matched-route slice for the current URL.
|
|
9150
9209
|
* @returns The hook context.
|
|
9151
9210
|
* @example
|
|
9152
|
-
* const ctx = makeContext(element, data);
|
|
9211
|
+
* const ctx = makeContext(element, data, route);
|
|
9153
9212
|
*/
|
|
9154
|
-
function makeContext(element, data) {
|
|
9213
|
+
function makeContext(element, data, route = EMPTY_ROUTE) {
|
|
9155
9214
|
return {
|
|
9156
9215
|
el: element,
|
|
9157
|
-
data
|
|
9216
|
+
data,
|
|
9217
|
+
params: route.params,
|
|
9218
|
+
meta: route.meta,
|
|
9219
|
+
locale: route.locale,
|
|
9220
|
+
url: route.url
|
|
9158
9221
|
};
|
|
9159
9222
|
}
|
|
9160
9223
|
/**
|
|
@@ -9168,17 +9231,18 @@ function makeContext(element, data) {
|
|
|
9168
9231
|
* @param swapArea - The swap-region element, or null when none was found.
|
|
9169
9232
|
* @param data - The current page data payload.
|
|
9170
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).
|
|
9171
9235
|
* @example
|
|
9172
|
-
* mountElement(state, emit, swapArea, data, element);
|
|
9236
|
+
* mountElement(state, emit, swapArea, data, element, route);
|
|
9173
9237
|
*/
|
|
9174
|
-
function mountElement(state, emit, swapArea, data, element) {
|
|
9238
|
+
function mountElement(state, emit, swapArea, data, element, route = EMPTY_ROUTE) {
|
|
9175
9239
|
if (state.instances.has(element)) return;
|
|
9176
9240
|
const name = element.dataset.component;
|
|
9177
9241
|
if (!name) return;
|
|
9178
9242
|
const definition = state.registeredComponents.get(name);
|
|
9179
9243
|
if (!definition) return;
|
|
9180
9244
|
const instance = createInstance(definition, element, swapArea ? !swapArea.contains(element) : true);
|
|
9181
|
-
const ctx = makeContext(element, data);
|
|
9245
|
+
const ctx = makeContext(element, data, route);
|
|
9182
9246
|
runHook(instance, "onCreate", ctx);
|
|
9183
9247
|
runHook(instance, "onMount", ctx);
|
|
9184
9248
|
state.instances.set(element, instance);
|
|
@@ -9196,14 +9260,15 @@ function mountElement(state, emit, swapArea, data, element) {
|
|
|
9196
9260
|
* @param state - The plugin state (registeredComponents + instances).
|
|
9197
9261
|
* @param emit - The event emitter for spa:component-mount.
|
|
9198
9262
|
* @param swapSelector - CSS selector bounding page-specific components.
|
|
9263
|
+
* @param route - The matched-route slice for the current URL (params/meta/locale/url).
|
|
9199
9264
|
* @example
|
|
9200
|
-
* scanAndMount(state, emit, "main > section");
|
|
9265
|
+
* scanAndMount(state, emit, "main > section", route);
|
|
9201
9266
|
*/
|
|
9202
|
-
function scanAndMount(state, emit, swapSelector) {
|
|
9267
|
+
function scanAndMount(state, emit, swapSelector, route = EMPTY_ROUTE) {
|
|
9203
9268
|
if (typeof document === "undefined") return;
|
|
9204
9269
|
const swapArea = document.querySelector(swapSelector);
|
|
9205
9270
|
const data = extractPageData(document);
|
|
9206
|
-
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);
|
|
9207
9272
|
}
|
|
9208
9273
|
/**
|
|
9209
9274
|
* Unmounts page-specific instances inside the swap region (runs `onUnMount`
|
|
@@ -9269,12 +9334,13 @@ function notifyNavStart(state) {
|
|
|
9269
9334
|
* instances were already destroyed and re-created by the swap).
|
|
9270
9335
|
*
|
|
9271
9336
|
* @param state - The plugin state holding live instances.
|
|
9337
|
+
* @param route - The matched-route slice for the destination URL (params/meta/locale/url).
|
|
9272
9338
|
* @example
|
|
9273
|
-
* notifyNavEnd(state);
|
|
9339
|
+
* notifyNavEnd(state, route);
|
|
9274
9340
|
*/
|
|
9275
|
-
function notifyNavEnd(state) {
|
|
9341
|
+
function notifyNavEnd(state, route = EMPTY_ROUTE) {
|
|
9276
9342
|
const data = typeof document === "undefined" ? {} : extractPageData(document);
|
|
9277
|
-
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));
|
|
9278
9344
|
}
|
|
9279
9345
|
//#endregion
|
|
9280
9346
|
//#region src/plugins/spa/head.ts
|
|
@@ -9917,6 +9983,26 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
9917
9983
|
});
|
|
9918
9984
|
};
|
|
9919
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
|
+
/**
|
|
9920
10006
|
* Process one navigation: head-sync, unmount, swap, re-mount, emit navigated.
|
|
9921
10007
|
* When the region cannot be swapped (either document lacks the swap selector)
|
|
9922
10008
|
* the SPA nav cannot complete — the head is already synced and the islands torn
|
|
@@ -9934,8 +10020,9 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
9934
10020
|
syncHead(deps.head, doc);
|
|
9935
10021
|
unmountPageSpecific(state, emit);
|
|
9936
10022
|
if (!swapRegion(doc, resolved.swapSelector, resolved.viewTransitions, () => {
|
|
9937
|
-
|
|
9938
|
-
|
|
10023
|
+
const routeSlice = componentRouteContext(pathname);
|
|
10024
|
+
scanAndMount(state, emit, resolved.swapSelector, routeSlice);
|
|
10025
|
+
notifyNavEnd(state, routeSlice);
|
|
9939
10026
|
}, applyPendingScroll)) {
|
|
9940
10027
|
handleError();
|
|
9941
10028
|
location.href = pathname;
|
|
@@ -9989,17 +10076,22 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
9989
10076
|
* const resolved = await resolveDataRender("/en/world/");
|
|
9990
10077
|
*/
|
|
9991
10078
|
const resolveDataRender = async (pathname) => {
|
|
9992
|
-
if (!deps.dataAt) return false;
|
|
9993
10079
|
const matchPath = pathname.split("?")[0] ?? pathname;
|
|
9994
10080
|
const hit = deps.router.match(matchPath);
|
|
9995
10081
|
if (!hit?.route._handlers.render) return false;
|
|
9996
|
-
|
|
9997
|
-
if (
|
|
10082
|
+
let data = {};
|
|
10083
|
+
if (!isClientOnlyRoute(deps.router.mode(), hit.route)) {
|
|
10084
|
+
if (!deps.dataAt) return false;
|
|
10085
|
+
const persisted = await deps.dataAt(pathname);
|
|
10086
|
+
if (persisted === null) return false;
|
|
10087
|
+
data = persisted;
|
|
10088
|
+
}
|
|
9998
10089
|
const locale = hit.params.lang ?? document.documentElement.lang ?? "";
|
|
9999
10090
|
const routeContext = {
|
|
10000
10091
|
params: hit.params,
|
|
10001
10092
|
data,
|
|
10002
10093
|
locale,
|
|
10094
|
+
meta: hit.route._meta,
|
|
10003
10095
|
url: (routeName, routeParams = {}) => deps.router.toUrl(routeName, routeParams)
|
|
10004
10096
|
};
|
|
10005
10097
|
const vnode = hit.route._handlers.render(routeContext);
|
|
@@ -10031,6 +10123,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10031
10123
|
if (signal?.aborted) return;
|
|
10032
10124
|
syncDataHead(deps.head, route, routeContext);
|
|
10033
10125
|
unmountPageSpecific(state, emit);
|
|
10126
|
+
const routeSlice = componentRouteContext(pathname);
|
|
10034
10127
|
/**
|
|
10035
10128
|
* Render the VNode into the region and re-mount its islands in one paint — the
|
|
10036
10129
|
* swap body handed to `runSwap` (optionally wrapped in a View Transition).
|
|
@@ -10042,8 +10135,8 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10042
10135
|
*/
|
|
10043
10136
|
const renderAndMount = () => {
|
|
10044
10137
|
renderVNode(vnode, region);
|
|
10045
|
-
scanAndMount(state, emit, resolved.swapSelector);
|
|
10046
|
-
notifyNavEnd(state);
|
|
10138
|
+
scanAndMount(state, emit, resolved.swapSelector, routeSlice);
|
|
10139
|
+
notifyNavEnd(state, routeSlice);
|
|
10047
10140
|
};
|
|
10048
10141
|
runSwap(renderAndMount, resolved.viewTransitions, applyPendingScroll);
|
|
10049
10142
|
state.currentUrl = pathname;
|
|
@@ -10076,6 +10169,30 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10076
10169
|
}
|
|
10077
10170
|
};
|
|
10078
10171
|
/**
|
|
10172
|
+
* Initial-load render for a spa client-only route (dynamic, no `.generate()`): the build emitted
|
|
10173
|
+
* no static HTML for it, so the host served a fallback shell. Client-render the matched route into
|
|
10174
|
+
* the swap region from the URL, then mount its islands — the deep-link / refresh paint. Unlike a
|
|
10175
|
+
* navigation there is nothing to unmount and no `spa:navigated` to emit. If the route cannot be
|
|
10176
|
+
* resolved (defensive — a matched client-only route always resolves), fall back to mounting the
|
|
10177
|
+
* served body so boot still wires up whatever islands the shell does carry.
|
|
10178
|
+
*
|
|
10179
|
+
* @param pathname - The current document path (pathname + search).
|
|
10180
|
+
* @example
|
|
10181
|
+
* await bootRender("/b/abc123");
|
|
10182
|
+
*/
|
|
10183
|
+
const bootRender = async (pathname) => {
|
|
10184
|
+
const routeSlice = componentRouteContext(pathname);
|
|
10185
|
+
const resolvedRender = await resolveDataRender(pathname);
|
|
10186
|
+
if (resolvedRender === false) {
|
|
10187
|
+
scanAndMount(state, emit, resolved.swapSelector, routeSlice);
|
|
10188
|
+
return;
|
|
10189
|
+
}
|
|
10190
|
+
const { vnode, region } = resolvedRender;
|
|
10191
|
+
const { renderVNode } = await Promise.resolve().then(() => require("./render-DLZEOe4M.cjs"));
|
|
10192
|
+
renderVNode(vnode, region);
|
|
10193
|
+
scanAndMount(state, emit, resolved.swapSelector, routeSlice);
|
|
10194
|
+
};
|
|
10195
|
+
/**
|
|
10079
10196
|
* Unified navigation: try the client DATA path first (only when the `data`
|
|
10080
10197
|
* plugin is composed), then fall back to HTML-over-fetch (which itself falls
|
|
10081
10198
|
* back to a full `location.href` reload). Injected into the router so every
|
|
@@ -10120,7 +10237,10 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10120
10237
|
progress = createProgressBar(resolved.progressBar);
|
|
10121
10238
|
state.currentUrl = currentLocationUrl();
|
|
10122
10239
|
state.destroyRouter = attachRouter(handlers, navigate);
|
|
10123
|
-
|
|
10240
|
+
const matchPath = state.currentUrl.split("?")[0] ?? state.currentUrl;
|
|
10241
|
+
const hit = deps.router.match(matchPath);
|
|
10242
|
+
if (hit?.route._handlers.render && isClientOnlyRoute(deps.router.mode(), hit.route)) bootRender(state.currentUrl);
|
|
10243
|
+
else scanAndMount(state, emit, resolved.swapSelector, componentRouteContext(state.currentUrl));
|
|
10124
10244
|
state.started = true;
|
|
10125
10245
|
},
|
|
10126
10246
|
/**
|
|
@@ -10151,7 +10271,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10151
10271
|
* kernel.scan();
|
|
10152
10272
|
*/
|
|
10153
10273
|
scan() {
|
|
10154
|
-
scanAndMount(state, emit, resolved.swapSelector);
|
|
10274
|
+
scanAndMount(state, emit, resolved.swapSelector, componentRouteContext(state.currentUrl));
|
|
10155
10275
|
},
|
|
10156
10276
|
/**
|
|
10157
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 {
|