@moku-labs/web 1.13.1 → 1.13.2
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.mjs +66 -4
- package/dist/index.cjs +87 -15
- package/dist/index.mjs +87 -15
- package/package.json +1 -1
package/dist/browser.mjs
CHANGED
|
@@ -471,6 +471,38 @@ function dynamicSegmentCount(pattern) {
|
|
|
471
471
|
return count;
|
|
472
472
|
}
|
|
473
473
|
/**
|
|
474
|
+
* Whether a route is rendered ENTIRELY on the client in `spa` mode: a dynamic route
|
|
475
|
+
* (≥1 non-lang param) that declares no build-time `.generate()` enumerator, so its
|
|
476
|
+
* concrete param paths are unknown until runtime.
|
|
477
|
+
*
|
|
478
|
+
* The build SKIPS such a route — emitting a static page for it would write one
|
|
479
|
+
* param-less shell whose path (`/b/{id}`) matches no file (a 404) and carries no
|
|
480
|
+
* param for the islands to read. Instead the SPA client-renders it from the URL on
|
|
481
|
+
* boot and on navigation. Build and client share this ONE predicate (the same way
|
|
482
|
+
* `dynamicSegmentCount`/`bySpecificity` are shared) so the two sides can never
|
|
483
|
+
* disagree about which routes are pre-rendered vs. client-only.
|
|
484
|
+
*
|
|
485
|
+
* Static routes (`/`) and dynamic routes WITH `.generate()` are pre-rendered as
|
|
486
|
+
* usual and so are NOT client-only. In `ssg`/`hybrid` mode nothing is client-only
|
|
487
|
+
* (the build pre-renders every route), so this is always `false` outside `spa`.
|
|
488
|
+
*
|
|
489
|
+
* @param mode - The global render mode (`router.mode()`).
|
|
490
|
+
* @param route - The route to test (only its `pattern` + `.generate()` presence are read).
|
|
491
|
+
* @param route.pattern - The route's URL pattern string.
|
|
492
|
+
* @param route._handlers - The route's handler bag.
|
|
493
|
+
* @param route._handlers.generate - The build-only static-paths enumerator, if any (presence only).
|
|
494
|
+
* @returns `true` when the route is client-only (spa mode, dynamic, no `.generate()`).
|
|
495
|
+
* @example
|
|
496
|
+
* ```ts
|
|
497
|
+
* isClientOnlyRoute("spa", { pattern: "/b/{id}", _handlers: {} }); // true
|
|
498
|
+
* isClientOnlyRoute("spa", { pattern: "/", _handlers: {} }); // false (static)
|
|
499
|
+
* isClientOnlyRoute("hybrid", { pattern: "/b/{id}", _handlers: {} }); // false (not spa)
|
|
500
|
+
* ```
|
|
501
|
+
*/
|
|
502
|
+
function isClientOnlyRoute(mode, route) {
|
|
503
|
+
return mode === "spa" && route._handlers.generate === void 0 && dynamicSegmentCount(route.pattern) > 0;
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
474
506
|
* Comparator that orders two routes most-specific-first (fewest dynamic segments
|
|
475
507
|
* first). Equal specificity yields `0` so a stable sort preserves declaration
|
|
476
508
|
* order — the exact ordering the compiled matcher table uses, guaranteeing
|
|
@@ -3374,12 +3406,16 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
3374
3406
|
* const resolved = await resolveDataRender("/en/world/");
|
|
3375
3407
|
*/
|
|
3376
3408
|
const resolveDataRender = async (pathname) => {
|
|
3377
|
-
if (!deps.dataAt) return false;
|
|
3378
3409
|
const matchPath = pathname.split("?")[0] ?? pathname;
|
|
3379
3410
|
const hit = deps.router.match(matchPath);
|
|
3380
3411
|
if (!hit?.route._handlers.render) return false;
|
|
3381
|
-
|
|
3382
|
-
if (
|
|
3412
|
+
let data = {};
|
|
3413
|
+
if (!isClientOnlyRoute(deps.router.mode(), hit.route)) {
|
|
3414
|
+
if (!deps.dataAt) return false;
|
|
3415
|
+
const persisted = await deps.dataAt(pathname);
|
|
3416
|
+
if (persisted === null) return false;
|
|
3417
|
+
data = persisted;
|
|
3418
|
+
}
|
|
3383
3419
|
const locale = hit.params.lang ?? document.documentElement.lang ?? "";
|
|
3384
3420
|
const routeContext = {
|
|
3385
3421
|
params: hit.params,
|
|
@@ -3461,6 +3497,29 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
3461
3497
|
}
|
|
3462
3498
|
};
|
|
3463
3499
|
/**
|
|
3500
|
+
* Initial-load render for a spa client-only route (dynamic, no `.generate()`): the build emitted
|
|
3501
|
+
* no static HTML for it, so the host served a fallback shell. Client-render the matched route into
|
|
3502
|
+
* the swap region from the URL, then mount its islands — the deep-link / refresh paint. Unlike a
|
|
3503
|
+
* navigation there is nothing to unmount and no `spa:navigated` to emit. If the route cannot be
|
|
3504
|
+
* resolved (defensive — a matched client-only route always resolves), fall back to mounting the
|
|
3505
|
+
* served body so boot still wires up whatever islands the shell does carry.
|
|
3506
|
+
*
|
|
3507
|
+
* @param pathname - The current document path (pathname + search).
|
|
3508
|
+
* @example
|
|
3509
|
+
* await bootRender("/b/abc123");
|
|
3510
|
+
*/
|
|
3511
|
+
const bootRender = async (pathname) => {
|
|
3512
|
+
const resolvedRender = await resolveDataRender(pathname);
|
|
3513
|
+
if (resolvedRender === false) {
|
|
3514
|
+
scanAndMount(state, emit, resolved.swapSelector);
|
|
3515
|
+
return;
|
|
3516
|
+
}
|
|
3517
|
+
const { vnode, region } = resolvedRender;
|
|
3518
|
+
const { renderVNode } = await import("./render-BNe0s7fr.mjs");
|
|
3519
|
+
renderVNode(vnode, region);
|
|
3520
|
+
scanAndMount(state, emit, resolved.swapSelector);
|
|
3521
|
+
};
|
|
3522
|
+
/**
|
|
3464
3523
|
* Unified navigation: try the client DATA path first (only when the `data`
|
|
3465
3524
|
* plugin is composed), then fall back to HTML-over-fetch (which itself falls
|
|
3466
3525
|
* back to a full `location.href` reload). Injected into the router so every
|
|
@@ -3505,7 +3564,10 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
3505
3564
|
progress = createProgressBar(resolved.progressBar);
|
|
3506
3565
|
state.currentUrl = currentLocationUrl();
|
|
3507
3566
|
state.destroyRouter = attachRouter(handlers, navigate);
|
|
3508
|
-
|
|
3567
|
+
const matchPath = state.currentUrl.split("?")[0] ?? state.currentUrl;
|
|
3568
|
+
const hit = deps.router.match(matchPath);
|
|
3569
|
+
if (hit?.route._handlers.render && isClientOnlyRoute(deps.router.mode(), hit.route)) bootRender(state.currentUrl);
|
|
3570
|
+
else scanAndMount(state, emit, resolved.swapSelector);
|
|
3509
3571
|
state.started = true;
|
|
3510
3572
|
},
|
|
3511
3573
|
/**
|
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);
|
|
@@ -4909,15 +4949,16 @@ async function prepareShell(ctx) {
|
|
|
4909
4949
|
* @param locales - Active locale codes from i18n.
|
|
4910
4950
|
* @param defaultLocale - The i18n default locale (kept when a route's locales collapse).
|
|
4911
4951
|
* @param byPattern - Pattern→compiled-`TypedRoute` map (see {@link makeEntryMap}).
|
|
4952
|
+
* @param mode - The global render mode (`router.mode()`); gates the spa client-only skip.
|
|
4912
4953
|
* @param ctx - Plugin context (provides `require`/`has` for generate contexts).
|
|
4913
4954
|
* @returns The flattened list of page instances to render.
|
|
4914
4955
|
* @example
|
|
4915
4956
|
* ```ts
|
|
4916
|
-
* const instances = await expandAllInstances(manifest, ["en"], "en", byPattern, ctx);
|
|
4957
|
+
* const instances = await expandAllInstances(manifest, ["en"], "en", byPattern, "hybrid", ctx);
|
|
4917
4958
|
* ```
|
|
4918
4959
|
*/
|
|
4919
|
-
async function expandAllInstances(manifest, locales, defaultLocale, byPattern, ctx) {
|
|
4920
|
-
return (await Promise.all(manifest.map((definition) => expandRoute(definition, locales, defaultLocale, byPattern, ctx)))).flat();
|
|
4960
|
+
async function expandAllInstances(manifest, locales, defaultLocale, byPattern, mode, ctx) {
|
|
4961
|
+
return (await Promise.all(manifest.map((definition) => expandRoute(definition, locales, defaultLocale, byPattern, mode, ctx)))).flat();
|
|
4921
4962
|
}
|
|
4922
4963
|
/**
|
|
4923
4964
|
* Persist per-page client-data sidecars when the app opts into client navigation
|
|
@@ -5027,14 +5068,15 @@ async function renderInBatches(items, batchSize, worker) {
|
|
|
5027
5068
|
async function renderPages(ctx, options) {
|
|
5028
5069
|
const reuse = options?.reuse === true;
|
|
5029
5070
|
const router = ctx.require(routerPlugin);
|
|
5071
|
+
const mode = router.mode();
|
|
5030
5072
|
const manifest = router.manifest();
|
|
5031
5073
|
ctx.state.manifest = [...manifest];
|
|
5032
5074
|
const locales = ctx.require(i18nPlugin).locales();
|
|
5033
5075
|
const byPattern = makeEntryMap(router);
|
|
5034
5076
|
if (!reuse) ctx.state.renderCache.clear();
|
|
5035
5077
|
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,
|
|
5078
|
+
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();
|
|
5079
|
+
await writeDataSidecars(ctx, rendered, mode);
|
|
5038
5080
|
ctx.log.debug("build:pages", { count: rendered.length });
|
|
5039
5081
|
return {
|
|
5040
5082
|
pageCount: rendered.length,
|
|
@@ -9989,12 +10031,16 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
9989
10031
|
* const resolved = await resolveDataRender("/en/world/");
|
|
9990
10032
|
*/
|
|
9991
10033
|
const resolveDataRender = async (pathname) => {
|
|
9992
|
-
if (!deps.dataAt) return false;
|
|
9993
10034
|
const matchPath = pathname.split("?")[0] ?? pathname;
|
|
9994
10035
|
const hit = deps.router.match(matchPath);
|
|
9995
10036
|
if (!hit?.route._handlers.render) return false;
|
|
9996
|
-
|
|
9997
|
-
if (
|
|
10037
|
+
let data = {};
|
|
10038
|
+
if (!isClientOnlyRoute(deps.router.mode(), hit.route)) {
|
|
10039
|
+
if (!deps.dataAt) return false;
|
|
10040
|
+
const persisted = await deps.dataAt(pathname);
|
|
10041
|
+
if (persisted === null) return false;
|
|
10042
|
+
data = persisted;
|
|
10043
|
+
}
|
|
9998
10044
|
const locale = hit.params.lang ?? document.documentElement.lang ?? "";
|
|
9999
10045
|
const routeContext = {
|
|
10000
10046
|
params: hit.params,
|
|
@@ -10076,6 +10122,29 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10076
10122
|
}
|
|
10077
10123
|
};
|
|
10078
10124
|
/**
|
|
10125
|
+
* Initial-load render for a spa client-only route (dynamic, no `.generate()`): the build emitted
|
|
10126
|
+
* no static HTML for it, so the host served a fallback shell. Client-render the matched route into
|
|
10127
|
+
* the swap region from the URL, then mount its islands — the deep-link / refresh paint. Unlike a
|
|
10128
|
+
* navigation there is nothing to unmount and no `spa:navigated` to emit. If the route cannot be
|
|
10129
|
+
* resolved (defensive — a matched client-only route always resolves), fall back to mounting the
|
|
10130
|
+
* served body so boot still wires up whatever islands the shell does carry.
|
|
10131
|
+
*
|
|
10132
|
+
* @param pathname - The current document path (pathname + search).
|
|
10133
|
+
* @example
|
|
10134
|
+
* await bootRender("/b/abc123");
|
|
10135
|
+
*/
|
|
10136
|
+
const bootRender = async (pathname) => {
|
|
10137
|
+
const resolvedRender = await resolveDataRender(pathname);
|
|
10138
|
+
if (resolvedRender === false) {
|
|
10139
|
+
scanAndMount(state, emit, resolved.swapSelector);
|
|
10140
|
+
return;
|
|
10141
|
+
}
|
|
10142
|
+
const { vnode, region } = resolvedRender;
|
|
10143
|
+
const { renderVNode } = await Promise.resolve().then(() => require("./render-DLZEOe4M.cjs"));
|
|
10144
|
+
renderVNode(vnode, region);
|
|
10145
|
+
scanAndMount(state, emit, resolved.swapSelector);
|
|
10146
|
+
};
|
|
10147
|
+
/**
|
|
10079
10148
|
* Unified navigation: try the client DATA path first (only when the `data`
|
|
10080
10149
|
* plugin is composed), then fall back to HTML-over-fetch (which itself falls
|
|
10081
10150
|
* back to a full `location.href` reload). Injected into the router so every
|
|
@@ -10120,7 +10189,10 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10120
10189
|
progress = createProgressBar(resolved.progressBar);
|
|
10121
10190
|
state.currentUrl = currentLocationUrl();
|
|
10122
10191
|
state.destroyRouter = attachRouter(handlers, navigate);
|
|
10123
|
-
|
|
10192
|
+
const matchPath = state.currentUrl.split("?")[0] ?? state.currentUrl;
|
|
10193
|
+
const hit = deps.router.match(matchPath);
|
|
10194
|
+
if (hit?.route._handlers.render && isClientOnlyRoute(deps.router.mode(), hit.route)) bootRender(state.currentUrl);
|
|
10195
|
+
else scanAndMount(state, emit, resolved.swapSelector);
|
|
10124
10196
|
state.started = true;
|
|
10125
10197
|
},
|
|
10126
10198
|
/**
|
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);
|
|
@@ -4896,15 +4936,16 @@ async function prepareShell(ctx) {
|
|
|
4896
4936
|
* @param locales - Active locale codes from i18n.
|
|
4897
4937
|
* @param defaultLocale - The i18n default locale (kept when a route's locales collapse).
|
|
4898
4938
|
* @param byPattern - Pattern→compiled-`TypedRoute` map (see {@link makeEntryMap}).
|
|
4939
|
+
* @param mode - The global render mode (`router.mode()`); gates the spa client-only skip.
|
|
4899
4940
|
* @param ctx - Plugin context (provides `require`/`has` for generate contexts).
|
|
4900
4941
|
* @returns The flattened list of page instances to render.
|
|
4901
4942
|
* @example
|
|
4902
4943
|
* ```ts
|
|
4903
|
-
* const instances = await expandAllInstances(manifest, ["en"], "en", byPattern, ctx);
|
|
4944
|
+
* const instances = await expandAllInstances(manifest, ["en"], "en", byPattern, "hybrid", ctx);
|
|
4904
4945
|
* ```
|
|
4905
4946
|
*/
|
|
4906
|
-
async function expandAllInstances(manifest, locales, defaultLocale, byPattern, ctx) {
|
|
4907
|
-
return (await Promise.all(manifest.map((definition) => expandRoute(definition, locales, defaultLocale, byPattern, ctx)))).flat();
|
|
4947
|
+
async function expandAllInstances(manifest, locales, defaultLocale, byPattern, mode, ctx) {
|
|
4948
|
+
return (await Promise.all(manifest.map((definition) => expandRoute(definition, locales, defaultLocale, byPattern, mode, ctx)))).flat();
|
|
4908
4949
|
}
|
|
4909
4950
|
/**
|
|
4910
4951
|
* Persist per-page client-data sidecars when the app opts into client navigation
|
|
@@ -5014,14 +5055,15 @@ async function renderInBatches(items, batchSize, worker) {
|
|
|
5014
5055
|
async function renderPages(ctx, options) {
|
|
5015
5056
|
const reuse = options?.reuse === true;
|
|
5016
5057
|
const router = ctx.require(routerPlugin);
|
|
5058
|
+
const mode = router.mode();
|
|
5017
5059
|
const manifest = router.manifest();
|
|
5018
5060
|
ctx.state.manifest = [...manifest];
|
|
5019
5061
|
const locales = ctx.require(i18nPlugin).locales();
|
|
5020
5062
|
const byPattern = makeEntryMap(router);
|
|
5021
5063
|
if (!reuse) ctx.state.renderCache.clear();
|
|
5022
5064
|
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,
|
|
5065
|
+
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();
|
|
5066
|
+
await writeDataSidecars(ctx, rendered, mode);
|
|
5025
5067
|
ctx.log.debug("build:pages", { count: rendered.length });
|
|
5026
5068
|
return {
|
|
5027
5069
|
pageCount: rendered.length,
|
|
@@ -9976,12 +10018,16 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
9976
10018
|
* const resolved = await resolveDataRender("/en/world/");
|
|
9977
10019
|
*/
|
|
9978
10020
|
const resolveDataRender = async (pathname) => {
|
|
9979
|
-
if (!deps.dataAt) return false;
|
|
9980
10021
|
const matchPath = pathname.split("?")[0] ?? pathname;
|
|
9981
10022
|
const hit = deps.router.match(matchPath);
|
|
9982
10023
|
if (!hit?.route._handlers.render) return false;
|
|
9983
|
-
|
|
9984
|
-
if (
|
|
10024
|
+
let data = {};
|
|
10025
|
+
if (!isClientOnlyRoute(deps.router.mode(), hit.route)) {
|
|
10026
|
+
if (!deps.dataAt) return false;
|
|
10027
|
+
const persisted = await deps.dataAt(pathname);
|
|
10028
|
+
if (persisted === null) return false;
|
|
10029
|
+
data = persisted;
|
|
10030
|
+
}
|
|
9985
10031
|
const locale = hit.params.lang ?? document.documentElement.lang ?? "";
|
|
9986
10032
|
const routeContext = {
|
|
9987
10033
|
params: hit.params,
|
|
@@ -10063,6 +10109,29 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10063
10109
|
}
|
|
10064
10110
|
};
|
|
10065
10111
|
/**
|
|
10112
|
+
* Initial-load render for a spa client-only route (dynamic, no `.generate()`): the build emitted
|
|
10113
|
+
* no static HTML for it, so the host served a fallback shell. Client-render the matched route into
|
|
10114
|
+
* the swap region from the URL, then mount its islands — the deep-link / refresh paint. Unlike a
|
|
10115
|
+
* navigation there is nothing to unmount and no `spa:navigated` to emit. If the route cannot be
|
|
10116
|
+
* resolved (defensive — a matched client-only route always resolves), fall back to mounting the
|
|
10117
|
+
* served body so boot still wires up whatever islands the shell does carry.
|
|
10118
|
+
*
|
|
10119
|
+
* @param pathname - The current document path (pathname + search).
|
|
10120
|
+
* @example
|
|
10121
|
+
* await bootRender("/b/abc123");
|
|
10122
|
+
*/
|
|
10123
|
+
const bootRender = async (pathname) => {
|
|
10124
|
+
const resolvedRender = await resolveDataRender(pathname);
|
|
10125
|
+
if (resolvedRender === false) {
|
|
10126
|
+
scanAndMount(state, emit, resolved.swapSelector);
|
|
10127
|
+
return;
|
|
10128
|
+
}
|
|
10129
|
+
const { vnode, region } = resolvedRender;
|
|
10130
|
+
const { renderVNode } = await import("./render-BNe0s7fr.mjs");
|
|
10131
|
+
renderVNode(vnode, region);
|
|
10132
|
+
scanAndMount(state, emit, resolved.swapSelector);
|
|
10133
|
+
};
|
|
10134
|
+
/**
|
|
10066
10135
|
* Unified navigation: try the client DATA path first (only when the `data`
|
|
10067
10136
|
* plugin is composed), then fall back to HTML-over-fetch (which itself falls
|
|
10068
10137
|
* back to a full `location.href` reload). Injected into the router so every
|
|
@@ -10107,7 +10176,10 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10107
10176
|
progress = createProgressBar(resolved.progressBar);
|
|
10108
10177
|
state.currentUrl = currentLocationUrl();
|
|
10109
10178
|
state.destroyRouter = attachRouter(handlers, navigate);
|
|
10110
|
-
|
|
10179
|
+
const matchPath = state.currentUrl.split("?")[0] ?? state.currentUrl;
|
|
10180
|
+
const hit = deps.router.match(matchPath);
|
|
10181
|
+
if (hit?.route._handlers.render && isClientOnlyRoute(deps.router.mode(), hit.route)) bootRender(state.currentUrl);
|
|
10182
|
+
else scanAndMount(state, emit, resolved.swapSelector);
|
|
10111
10183
|
state.started = true;
|
|
10112
10184
|
},
|
|
10113
10185
|
/**
|
package/package.json
CHANGED