@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.mjs
CHANGED
|
@@ -1071,6 +1071,38 @@ function dynamicSegmentCount(pattern) {
|
|
|
1071
1071
|
return count;
|
|
1072
1072
|
}
|
|
1073
1073
|
/**
|
|
1074
|
+
* Whether a route is rendered ENTIRELY on the client in `spa` mode: a dynamic route
|
|
1075
|
+
* (≥1 non-lang param) that declares no build-time `.generate()` enumerator, so its
|
|
1076
|
+
* concrete param paths are unknown until runtime.
|
|
1077
|
+
*
|
|
1078
|
+
* The build SKIPS such a route — emitting a static page for it would write one
|
|
1079
|
+
* param-less shell whose path (`/b/{id}`) matches no file (a 404) and carries no
|
|
1080
|
+
* param for the islands to read. Instead the SPA client-renders it from the URL on
|
|
1081
|
+
* boot and on navigation. Build and client share this ONE predicate (the same way
|
|
1082
|
+
* `dynamicSegmentCount`/`bySpecificity` are shared) so the two sides can never
|
|
1083
|
+
* disagree about which routes are pre-rendered vs. client-only.
|
|
1084
|
+
*
|
|
1085
|
+
* Static routes (`/`) and dynamic routes WITH `.generate()` are pre-rendered as
|
|
1086
|
+
* usual and so are NOT client-only. In `ssg`/`hybrid` mode nothing is client-only
|
|
1087
|
+
* (the build pre-renders every route), so this is always `false` outside `spa`.
|
|
1088
|
+
*
|
|
1089
|
+
* @param mode - The global render mode (`router.mode()`).
|
|
1090
|
+
* @param route - The route to test (only its `pattern` + `.generate()` presence are read).
|
|
1091
|
+
* @param route.pattern - The route's URL pattern string.
|
|
1092
|
+
* @param route._handlers - The route's handler bag.
|
|
1093
|
+
* @param route._handlers.generate - The build-only static-paths enumerator, if any (presence only).
|
|
1094
|
+
* @returns `true` when the route is client-only (spa mode, dynamic, no `.generate()`).
|
|
1095
|
+
* @example
|
|
1096
|
+
* ```ts
|
|
1097
|
+
* isClientOnlyRoute("spa", { pattern: "/b/{id}", _handlers: {} }); // true
|
|
1098
|
+
* isClientOnlyRoute("spa", { pattern: "/", _handlers: {} }); // false (static)
|
|
1099
|
+
* isClientOnlyRoute("hybrid", { pattern: "/b/{id}", _handlers: {} }); // false (not spa)
|
|
1100
|
+
* ```
|
|
1101
|
+
*/
|
|
1102
|
+
function isClientOnlyRoute(mode, route) {
|
|
1103
|
+
return mode === "spa" && route._handlers.generate === void 0 && dynamicSegmentCount(route.pattern) > 0;
|
|
1104
|
+
}
|
|
1105
|
+
/**
|
|
1074
1106
|
* Comparator that orders two routes most-specific-first (fewest dynamic segments
|
|
1075
1107
|
* first). Equal specificity yields `0` so a stable sort preserves declaration
|
|
1076
1108
|
* order — the exact ordering the compiled matcher table uses, guaranteeing
|
|
@@ -4515,16 +4547,23 @@ function resolveEntry(byPattern, definition) {
|
|
|
4515
4547
|
* generate context is the spec `{ locale, require, has }`, so a `.generate()` handler
|
|
4516
4548
|
* pulls sibling APIs the spec way.
|
|
4517
4549
|
*
|
|
4550
|
+
* In `spa` mode a client-only route (dynamic, no `.generate()`) is SKIPPED entirely
|
|
4551
|
+
* (`[]`) — it is rendered on the client from the URL, so emitting a static param-less
|
|
4552
|
+
* shell here would only write a file at the wrong path (a 404 for any real param path)
|
|
4553
|
+
* carrying no param. See {@link isClientOnlyRoute}.
|
|
4554
|
+
*
|
|
4518
4555
|
* @param definition - The route definition from the manifest.
|
|
4519
4556
|
* @param locale - The active locale to generate param sets for.
|
|
4557
|
+
* @param mode - The global render mode (`router.mode()`); gates the spa client-only skip.
|
|
4520
4558
|
* @param ctx - Plugin context (provides `require`/`has` for the generate context).
|
|
4521
|
-
* @returns The param sets for this route+locale (`[{}]` when there is no `.generate()`).
|
|
4559
|
+
* @returns The param sets for this route+locale (`[{}]` when there is no `.generate()`; `[]` when client-only).
|
|
4522
4560
|
* @example
|
|
4523
4561
|
* ```ts
|
|
4524
|
-
* const paramSets = await generateParamSets(def, "en", ctx);
|
|
4562
|
+
* const paramSets = await generateParamSets(def, "en", "hybrid", ctx);
|
|
4525
4563
|
* ```
|
|
4526
4564
|
*/
|
|
4527
|
-
async function generateParameterSets(definition, locale, ctx) {
|
|
4565
|
+
async function generateParameterSets(definition, locale, mode, ctx) {
|
|
4566
|
+
if (isClientOnlyRoute(mode, definition)) return [];
|
|
4528
4567
|
const generateContext = {
|
|
4529
4568
|
locale,
|
|
4530
4569
|
require: ctx.require,
|
|
@@ -4548,21 +4587,22 @@ async function generateParameterSets(definition, locale, ctx) {
|
|
|
4548
4587
|
* @param locales - Active locale codes from i18n.
|
|
4549
4588
|
* @param defaultLocale - The i18n default locale (kept when locales collapse to one file).
|
|
4550
4589
|
* @param byPattern - Pattern→compiled-`TypedRoute` map (see {@link makeEntryMap}).
|
|
4590
|
+
* @param mode - The global render mode (`router.mode()`); gates the spa client-only skip.
|
|
4551
4591
|
* @param ctx - Plugin context (provides `require`/`has` for the generate context).
|
|
4552
4592
|
* @returns The flattened, file-deduplicated list of page instances for this route.
|
|
4553
4593
|
* @example
|
|
4554
4594
|
* ```ts
|
|
4555
|
-
* await expandRoute(def, ["en"], "en", byPattern, ctx);
|
|
4595
|
+
* await expandRoute(def, ["en"], "en", byPattern, "hybrid", ctx);
|
|
4556
4596
|
* ```
|
|
4557
4597
|
*/
|
|
4558
|
-
async function expandRoute(definition, locales, defaultLocale, byPattern, ctx) {
|
|
4598
|
+
async function expandRoute(definition, locales, defaultLocale, byPattern, mode, ctx) {
|
|
4559
4599
|
const entry = resolveEntry(byPattern, definition);
|
|
4560
4600
|
const { name } = entry;
|
|
4561
4601
|
const orderedLocales = [defaultLocale, ...locales.filter((locale) => locale !== defaultLocale)];
|
|
4562
4602
|
const instances = [];
|
|
4563
4603
|
const claimedFiles = /* @__PURE__ */ new Set();
|
|
4564
4604
|
for (const locale of orderedLocales) {
|
|
4565
|
-
const parameterSets = await generateParameterSets(definition, locale, ctx);
|
|
4605
|
+
const parameterSets = await generateParameterSets(definition, locale, mode, ctx);
|
|
4566
4606
|
for (const raw of parameterSets) {
|
|
4567
4607
|
const params = raw ?? {};
|
|
4568
4608
|
const file = entry.toFile(params);
|
|
@@ -4685,8 +4725,8 @@ function composeHeadHtml(ctx, instance, url, routeContext, data) {
|
|
|
4685
4725
|
* layout is NOT re-applied on navigation), then serialize with
|
|
4686
4726
|
* preact-render-to-string. Returns `""` when the route has no `.render()`.
|
|
4687
4727
|
*
|
|
4688
|
-
* @param definition - The route definition (provides `.render()`/`.layout()
|
|
4689
|
-
* @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.
|
|
4690
4730
|
* @returns The SSR-rendered body HTML, or `""` when the route has no `.render()`.
|
|
4691
4731
|
* @example
|
|
4692
4732
|
* ```ts
|
|
@@ -4696,11 +4736,7 @@ function composeHeadHtml(ctx, instance, url, routeContext, data) {
|
|
|
4696
4736
|
function renderBody(definition, routeContext) {
|
|
4697
4737
|
const vnode = definition._handlers.render?.(routeContext);
|
|
4698
4738
|
if (!vnode) return "";
|
|
4699
|
-
|
|
4700
|
-
...routeContext,
|
|
4701
|
-
meta: definition._meta
|
|
4702
|
-
};
|
|
4703
|
-
return renderToString(definition._handlers.layout ? definition._handlers.layout(layoutContext, vnode) : vnode);
|
|
4739
|
+
return renderToString(definition._handlers.layout ? definition._handlers.layout(routeContext, vnode) : vnode);
|
|
4704
4740
|
}
|
|
4705
4741
|
/**
|
|
4706
4742
|
* Hash a page's render inputs (its loaded data) for the render cache. `null` when the
|
|
@@ -4834,6 +4870,7 @@ async function renderInstance(ctx, instance, shell, reuse) {
|
|
|
4834
4870
|
params,
|
|
4835
4871
|
data,
|
|
4836
4872
|
locale,
|
|
4873
|
+
meta: definition._meta,
|
|
4837
4874
|
url: (routeName, routeParams = {}) => router.toUrl(routeName, routeParams)
|
|
4838
4875
|
};
|
|
4839
4876
|
const parts = {
|
|
@@ -4896,15 +4933,16 @@ async function prepareShell(ctx) {
|
|
|
4896
4933
|
* @param locales - Active locale codes from i18n.
|
|
4897
4934
|
* @param defaultLocale - The i18n default locale (kept when a route's locales collapse).
|
|
4898
4935
|
* @param byPattern - Pattern→compiled-`TypedRoute` map (see {@link makeEntryMap}).
|
|
4936
|
+
* @param mode - The global render mode (`router.mode()`); gates the spa client-only skip.
|
|
4899
4937
|
* @param ctx - Plugin context (provides `require`/`has` for generate contexts).
|
|
4900
4938
|
* @returns The flattened list of page instances to render.
|
|
4901
4939
|
* @example
|
|
4902
4940
|
* ```ts
|
|
4903
|
-
* const instances = await expandAllInstances(manifest, ["en"], "en", byPattern, ctx);
|
|
4941
|
+
* const instances = await expandAllInstances(manifest, ["en"], "en", byPattern, "hybrid", ctx);
|
|
4904
4942
|
* ```
|
|
4905
4943
|
*/
|
|
4906
|
-
async function expandAllInstances(manifest, locales, defaultLocale, byPattern, ctx) {
|
|
4907
|
-
return (await Promise.all(manifest.map((definition) => expandRoute(definition, locales, defaultLocale, byPattern, ctx)))).flat();
|
|
4944
|
+
async function expandAllInstances(manifest, locales, defaultLocale, byPattern, mode, ctx) {
|
|
4945
|
+
return (await Promise.all(manifest.map((definition) => expandRoute(definition, locales, defaultLocale, byPattern, mode, ctx)))).flat();
|
|
4908
4946
|
}
|
|
4909
4947
|
/**
|
|
4910
4948
|
* Persist per-page client-data sidecars when the app opts into client navigation
|
|
@@ -5014,14 +5052,15 @@ async function renderInBatches(items, batchSize, worker) {
|
|
|
5014
5052
|
async function renderPages(ctx, options) {
|
|
5015
5053
|
const reuse = options?.reuse === true;
|
|
5016
5054
|
const router = ctx.require(routerPlugin);
|
|
5055
|
+
const mode = router.mode();
|
|
5017
5056
|
const manifest = router.manifest();
|
|
5018
5057
|
ctx.state.manifest = [...manifest];
|
|
5019
5058
|
const locales = ctx.require(i18nPlugin).locales();
|
|
5020
5059
|
const byPattern = makeEntryMap(router);
|
|
5021
5060
|
if (!reuse) ctx.state.renderCache.clear();
|
|
5022
5061
|
const shell = await prepareShell(ctx);
|
|
5023
|
-
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();
|
|
5024
|
-
await writeDataSidecars(ctx, rendered,
|
|
5062
|
+
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();
|
|
5063
|
+
await writeDataSidecars(ctx, rendered, mode);
|
|
5025
5064
|
ctx.log.debug("build:pages", { count: rendered.length });
|
|
5026
5065
|
return {
|
|
5027
5066
|
pageCount: rendered.length,
|
|
@@ -9043,6 +9082,23 @@ const ERROR_PREFIX$2 = "[web]";
|
|
|
9043
9082
|
/** The set of legal hook names, frozen for O(1) membership checks. */
|
|
9044
9083
|
const HOOK_NAME_SET = new Set(COMPONENT_HOOK_NAMES);
|
|
9045
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
|
+
/**
|
|
9046
9102
|
* Validate a single hook entry: its key must be a known hook name and its value
|
|
9047
9103
|
* must be a function. Throws fail-fast on the first violation.
|
|
9048
9104
|
*
|
|
@@ -9130,18 +9186,25 @@ function runHook(instance, hook, ctx) {
|
|
|
9130
9186
|
instance.def.hooks[hook]?.(ctx);
|
|
9131
9187
|
}
|
|
9132
9188
|
/**
|
|
9133
|
-
* 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()`).
|
|
9134
9192
|
*
|
|
9135
9193
|
* @param element - The element the instance is bound to.
|
|
9136
9194
|
* @param data - The current page data payload.
|
|
9195
|
+
* @param route - The matched-route slice for the current URL.
|
|
9137
9196
|
* @returns The hook context.
|
|
9138
9197
|
* @example
|
|
9139
|
-
* const ctx = makeContext(element, data);
|
|
9198
|
+
* const ctx = makeContext(element, data, route);
|
|
9140
9199
|
*/
|
|
9141
|
-
function makeContext(element, data) {
|
|
9200
|
+
function makeContext(element, data, route = EMPTY_ROUTE) {
|
|
9142
9201
|
return {
|
|
9143
9202
|
el: element,
|
|
9144
|
-
data
|
|
9203
|
+
data,
|
|
9204
|
+
params: route.params,
|
|
9205
|
+
meta: route.meta,
|
|
9206
|
+
locale: route.locale,
|
|
9207
|
+
url: route.url
|
|
9145
9208
|
};
|
|
9146
9209
|
}
|
|
9147
9210
|
/**
|
|
@@ -9155,17 +9218,18 @@ function makeContext(element, data) {
|
|
|
9155
9218
|
* @param swapArea - The swap-region element, or null when none was found.
|
|
9156
9219
|
* @param data - The current page data payload.
|
|
9157
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).
|
|
9158
9222
|
* @example
|
|
9159
|
-
* mountElement(state, emit, swapArea, data, element);
|
|
9223
|
+
* mountElement(state, emit, swapArea, data, element, route);
|
|
9160
9224
|
*/
|
|
9161
|
-
function mountElement(state, emit, swapArea, data, element) {
|
|
9225
|
+
function mountElement(state, emit, swapArea, data, element, route = EMPTY_ROUTE) {
|
|
9162
9226
|
if (state.instances.has(element)) return;
|
|
9163
9227
|
const name = element.dataset.component;
|
|
9164
9228
|
if (!name) return;
|
|
9165
9229
|
const definition = state.registeredComponents.get(name);
|
|
9166
9230
|
if (!definition) return;
|
|
9167
9231
|
const instance = createInstance(definition, element, swapArea ? !swapArea.contains(element) : true);
|
|
9168
|
-
const ctx = makeContext(element, data);
|
|
9232
|
+
const ctx = makeContext(element, data, route);
|
|
9169
9233
|
runHook(instance, "onCreate", ctx);
|
|
9170
9234
|
runHook(instance, "onMount", ctx);
|
|
9171
9235
|
state.instances.set(element, instance);
|
|
@@ -9183,14 +9247,15 @@ function mountElement(state, emit, swapArea, data, element) {
|
|
|
9183
9247
|
* @param state - The plugin state (registeredComponents + instances).
|
|
9184
9248
|
* @param emit - The event emitter for spa:component-mount.
|
|
9185
9249
|
* @param swapSelector - CSS selector bounding page-specific components.
|
|
9250
|
+
* @param route - The matched-route slice for the current URL (params/meta/locale/url).
|
|
9186
9251
|
* @example
|
|
9187
|
-
* scanAndMount(state, emit, "main > section");
|
|
9252
|
+
* scanAndMount(state, emit, "main > section", route);
|
|
9188
9253
|
*/
|
|
9189
|
-
function scanAndMount(state, emit, swapSelector) {
|
|
9254
|
+
function scanAndMount(state, emit, swapSelector, route = EMPTY_ROUTE) {
|
|
9190
9255
|
if (typeof document === "undefined") return;
|
|
9191
9256
|
const swapArea = document.querySelector(swapSelector);
|
|
9192
9257
|
const data = extractPageData(document);
|
|
9193
|
-
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);
|
|
9194
9259
|
}
|
|
9195
9260
|
/**
|
|
9196
9261
|
* Unmounts page-specific instances inside the swap region (runs `onUnMount`
|
|
@@ -9256,12 +9321,13 @@ function notifyNavStart(state) {
|
|
|
9256
9321
|
* instances were already destroyed and re-created by the swap).
|
|
9257
9322
|
*
|
|
9258
9323
|
* @param state - The plugin state holding live instances.
|
|
9324
|
+
* @param route - The matched-route slice for the destination URL (params/meta/locale/url).
|
|
9259
9325
|
* @example
|
|
9260
|
-
* notifyNavEnd(state);
|
|
9326
|
+
* notifyNavEnd(state, route);
|
|
9261
9327
|
*/
|
|
9262
|
-
function notifyNavEnd(state) {
|
|
9328
|
+
function notifyNavEnd(state, route = EMPTY_ROUTE) {
|
|
9263
9329
|
const data = typeof document === "undefined" ? {} : extractPageData(document);
|
|
9264
|
-
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));
|
|
9265
9331
|
}
|
|
9266
9332
|
//#endregion
|
|
9267
9333
|
//#region src/plugins/spa/head.ts
|
|
@@ -9904,6 +9970,26 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
9904
9970
|
});
|
|
9905
9971
|
};
|
|
9906
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
|
+
/**
|
|
9907
9993
|
* Process one navigation: head-sync, unmount, swap, re-mount, emit navigated.
|
|
9908
9994
|
* When the region cannot be swapped (either document lacks the swap selector)
|
|
9909
9995
|
* the SPA nav cannot complete — the head is already synced and the islands torn
|
|
@@ -9921,8 +10007,9 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
9921
10007
|
syncHead(deps.head, doc);
|
|
9922
10008
|
unmountPageSpecific(state, emit);
|
|
9923
10009
|
if (!swapRegion(doc, resolved.swapSelector, resolved.viewTransitions, () => {
|
|
9924
|
-
|
|
9925
|
-
|
|
10010
|
+
const routeSlice = componentRouteContext(pathname);
|
|
10011
|
+
scanAndMount(state, emit, resolved.swapSelector, routeSlice);
|
|
10012
|
+
notifyNavEnd(state, routeSlice);
|
|
9926
10013
|
}, applyPendingScroll)) {
|
|
9927
10014
|
handleError();
|
|
9928
10015
|
location.href = pathname;
|
|
@@ -9976,17 +10063,22 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
9976
10063
|
* const resolved = await resolveDataRender("/en/world/");
|
|
9977
10064
|
*/
|
|
9978
10065
|
const resolveDataRender = async (pathname) => {
|
|
9979
|
-
if (!deps.dataAt) return false;
|
|
9980
10066
|
const matchPath = pathname.split("?")[0] ?? pathname;
|
|
9981
10067
|
const hit = deps.router.match(matchPath);
|
|
9982
10068
|
if (!hit?.route._handlers.render) return false;
|
|
9983
|
-
|
|
9984
|
-
if (
|
|
10069
|
+
let data = {};
|
|
10070
|
+
if (!isClientOnlyRoute(deps.router.mode(), hit.route)) {
|
|
10071
|
+
if (!deps.dataAt) return false;
|
|
10072
|
+
const persisted = await deps.dataAt(pathname);
|
|
10073
|
+
if (persisted === null) return false;
|
|
10074
|
+
data = persisted;
|
|
10075
|
+
}
|
|
9985
10076
|
const locale = hit.params.lang ?? document.documentElement.lang ?? "";
|
|
9986
10077
|
const routeContext = {
|
|
9987
10078
|
params: hit.params,
|
|
9988
10079
|
data,
|
|
9989
10080
|
locale,
|
|
10081
|
+
meta: hit.route._meta,
|
|
9990
10082
|
url: (routeName, routeParams = {}) => deps.router.toUrl(routeName, routeParams)
|
|
9991
10083
|
};
|
|
9992
10084
|
const vnode = hit.route._handlers.render(routeContext);
|
|
@@ -10018,6 +10110,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10018
10110
|
if (signal?.aborted) return;
|
|
10019
10111
|
syncDataHead(deps.head, route, routeContext);
|
|
10020
10112
|
unmountPageSpecific(state, emit);
|
|
10113
|
+
const routeSlice = componentRouteContext(pathname);
|
|
10021
10114
|
/**
|
|
10022
10115
|
* Render the VNode into the region and re-mount its islands in one paint — the
|
|
10023
10116
|
* swap body handed to `runSwap` (optionally wrapped in a View Transition).
|
|
@@ -10029,8 +10122,8 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10029
10122
|
*/
|
|
10030
10123
|
const renderAndMount = () => {
|
|
10031
10124
|
renderVNode(vnode, region);
|
|
10032
|
-
scanAndMount(state, emit, resolved.swapSelector);
|
|
10033
|
-
notifyNavEnd(state);
|
|
10125
|
+
scanAndMount(state, emit, resolved.swapSelector, routeSlice);
|
|
10126
|
+
notifyNavEnd(state, routeSlice);
|
|
10034
10127
|
};
|
|
10035
10128
|
runSwap(renderAndMount, resolved.viewTransitions, applyPendingScroll);
|
|
10036
10129
|
state.currentUrl = pathname;
|
|
@@ -10063,6 +10156,30 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10063
10156
|
}
|
|
10064
10157
|
};
|
|
10065
10158
|
/**
|
|
10159
|
+
* Initial-load render for a spa client-only route (dynamic, no `.generate()`): the build emitted
|
|
10160
|
+
* no static HTML for it, so the host served a fallback shell. Client-render the matched route into
|
|
10161
|
+
* the swap region from the URL, then mount its islands — the deep-link / refresh paint. Unlike a
|
|
10162
|
+
* navigation there is nothing to unmount and no `spa:navigated` to emit. If the route cannot be
|
|
10163
|
+
* resolved (defensive — a matched client-only route always resolves), fall back to mounting the
|
|
10164
|
+
* served body so boot still wires up whatever islands the shell does carry.
|
|
10165
|
+
*
|
|
10166
|
+
* @param pathname - The current document path (pathname + search).
|
|
10167
|
+
* @example
|
|
10168
|
+
* await bootRender("/b/abc123");
|
|
10169
|
+
*/
|
|
10170
|
+
const bootRender = async (pathname) => {
|
|
10171
|
+
const routeSlice = componentRouteContext(pathname);
|
|
10172
|
+
const resolvedRender = await resolveDataRender(pathname);
|
|
10173
|
+
if (resolvedRender === false) {
|
|
10174
|
+
scanAndMount(state, emit, resolved.swapSelector, routeSlice);
|
|
10175
|
+
return;
|
|
10176
|
+
}
|
|
10177
|
+
const { vnode, region } = resolvedRender;
|
|
10178
|
+
const { renderVNode } = await import("./render-BNe0s7fr.mjs");
|
|
10179
|
+
renderVNode(vnode, region);
|
|
10180
|
+
scanAndMount(state, emit, resolved.swapSelector, routeSlice);
|
|
10181
|
+
};
|
|
10182
|
+
/**
|
|
10066
10183
|
* Unified navigation: try the client DATA path first (only when the `data`
|
|
10067
10184
|
* plugin is composed), then fall back to HTML-over-fetch (which itself falls
|
|
10068
10185
|
* back to a full `location.href` reload). Injected into the router so every
|
|
@@ -10107,7 +10224,10 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10107
10224
|
progress = createProgressBar(resolved.progressBar);
|
|
10108
10225
|
state.currentUrl = currentLocationUrl();
|
|
10109
10226
|
state.destroyRouter = attachRouter(handlers, navigate);
|
|
10110
|
-
|
|
10227
|
+
const matchPath = state.currentUrl.split("?")[0] ?? state.currentUrl;
|
|
10228
|
+
const hit = deps.router.match(matchPath);
|
|
10229
|
+
if (hit?.route._handlers.render && isClientOnlyRoute(deps.router.mode(), hit.route)) bootRender(state.currentUrl);
|
|
10230
|
+
else scanAndMount(state, emit, resolved.swapSelector, componentRouteContext(state.currentUrl));
|
|
10111
10231
|
state.started = true;
|
|
10112
10232
|
},
|
|
10113
10233
|
/**
|
|
@@ -10138,7 +10258,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10138
10258
|
* kernel.scan();
|
|
10139
10259
|
*/
|
|
10140
10260
|
scan() {
|
|
10141
|
-
scanAndMount(state, emit, resolved.swapSelector);
|
|
10261
|
+
scanAndMount(state, emit, resolved.swapSelector, componentRouteContext(state.currentUrl));
|
|
10142
10262
|
},
|
|
10143
10263
|
/**
|
|
10144
10264
|
* Tear down router listeners, dispose all instances, reset boot state.
|
package/package.json
CHANGED