@moku-labs/web 1.8.0 → 1.8.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.
@@ -1157,6 +1157,19 @@ type Api$1 = {
1157
1157
  url: string;
1158
1158
  locale?: string;
1159
1159
  }): string;
1160
+ /**
1161
+ * Resolve the FINAL document title for a route's head config — the same value `render`
1162
+ * emits in its `<title>` element (`titleTemplate` applied; a route-pinned `title`-keyed
1163
+ * element wins). Used by `spa` to sync `document.title` on client DATA-path navigation.
1164
+ *
1165
+ * @param head - The route's head config (may be `undefined` for head-less routes).
1166
+ * @returns The final document title string.
1167
+ * @example
1168
+ * ```ts
1169
+ * api.composeTitle({ title: "Page 2" }); // "Page 2 — Site"
1170
+ * ```
1171
+ */
1172
+ composeTitle(head: HeadConfig | undefined): string;
1160
1173
  };
1161
1174
  declare namespace types_d_exports$6 {
1162
1175
  export { COMPONENT_HOOK_NAMES, ComponentContext, ComponentDef, ComponentHooks, ComponentInstance, ExtractApi, PageData, ResolvedSpaConfig, SpaApi, SpaConfig, SpaContext, SpaDataReader, SpaEmitFunction, SpaEvents, SpaKernel, SpaKernelDeps, SpaRequire, SpaState };
package/dist/browser.mjs CHANGED
@@ -2579,6 +2579,26 @@ function composeHead(input) {
2579
2579
  }), ...head.elements ?? []]);
2580
2580
  }
2581
2581
  /**
2582
+ * Resolve the FINAL document title for a route's head config — the same value
2583
+ * {@link composeHead} emits in its `<title>` element. A route-supplied `title`-keyed
2584
+ * element wins the keyed last-wins de-dupe over the templated base title (how a route
2585
+ * pins a bare title past `titleTemplate`), so it must win here too; otherwise the
2586
+ * template is applied to `head.title ?? site.name()`. Reused by `spa` for the client
2587
+ * DATA-path `document.title` sync, so client-side navigation matches the SSG output.
2588
+ *
2589
+ * @param head - The route's head config (may be `undefined` for head-less routes).
2590
+ * @param defaults - The normalized head defaults (provides `titleTemplate`).
2591
+ * @param site - The site slice (title fallback).
2592
+ * @returns The final document title string.
2593
+ * @example composeTitle({ title: "Page 2" }, defaults, site) // "Page 2 — Site"
2594
+ */
2595
+ function composeTitle(head, defaults, site) {
2596
+ const config = head ?? {};
2597
+ const pinned = config.elements?.findLast((element) => element.key === "title");
2598
+ if (pinned?.children !== void 0) return pinned.children;
2599
+ return applyTemplate(config.title ?? site.name(), defaults.titleTemplate);
2600
+ }
2601
+ /**
2582
2602
  * Compose the SITE-LEVEL Open Graph / Twitter block for a bare-path redirect or landing
2583
2603
  * page that has no per-route head of its own. Returns `[]` UNLESS a `defaultOgImage` is
2584
2604
  * configured — so apps that opt out keep a bare redirect (no behavior change). The site
@@ -2748,6 +2768,21 @@ function createApi$1(ctx) {
2748
2768
  url: site.canonical(input.url),
2749
2769
  ...ogLocale === void 0 ? {} : { ogLocale }
2750
2770
  }));
2771
+ },
2772
+ /**
2773
+ * Resolve the FINAL document title for a route's head config — the same value `render`
2774
+ * emits in its `<title>` element. Pulled by `spa` on the client DATA path so a
2775
+ * client-side navigation's `document.title` matches the SSG output.
2776
+ *
2777
+ * @param head - The route's head config (may be `undefined` for head-less routes).
2778
+ * @returns The final document title string.
2779
+ * @example
2780
+ * ```ts
2781
+ * api.composeTitle({ title: "Page 2" }); // "Page 2 — Site"
2782
+ * ```
2783
+ */
2784
+ composeTitle(head) {
2785
+ return composeTitle(head, readDefaults(ctx.state), ctx.require(sitePlugin));
2751
2786
  }
2752
2787
  };
2753
2788
  }
@@ -4020,16 +4055,19 @@ function currentLocationUrl() {
4020
4055
  /**
4021
4056
  * Apply the matched route's `head` config to the live document (minimal client
4022
4057
  * head-sync for the DATA path: title only — the full meta sync runs on the
4023
- * HTML-over-fetch path from the fetched `<head>`).
4058
+ * HTML-over-fetch path from the fetched `<head>`). The title is resolved through
4059
+ * `head.composeTitle` — the SAME composition `render` uses (`titleTemplate` applied;
4060
+ * a route-pinned `title` element wins) — so a client-side navigation's
4061
+ * `document.title` matches the SSG output instead of the raw route title.
4024
4062
  *
4063
+ * @param head - The head plugin API (resolves the final templated title).
4025
4064
  * @param route - The matched route definition.
4026
4065
  * @param routeContext - The render context (params/data/locale).
4027
4066
  * @example
4028
- * syncDataHead(hit.route, { params, data, locale });
4067
+ * syncDataHead(deps.head, hit.route, { params, data, locale });
4029
4068
  */
4030
- function syncDataHead(route, routeContext) {
4031
- const title = route._handlers.head?.(routeContext)?.title;
4032
- if (title !== void 0 && title !== "") document.title = title;
4069
+ function syncDataHead(head, route, routeContext) {
4070
+ document.title = head.composeTitle(route._handlers.head?.(routeContext));
4033
4071
  }
4034
4072
  /**
4035
4073
  * Builds the single shared SPA kernel — a pure factory over state/config/emit.
@@ -4185,7 +4223,7 @@ function createSpaKernel(state, config, emit, deps) {
4185
4223
  handleStart(pathname);
4186
4224
  const { renderVNode } = await import("./render-BNe0s7fr.mjs");
4187
4225
  if (signal?.aborted) return;
4188
- syncDataHead(route, routeContext);
4226
+ syncDataHead(deps.head, route, routeContext);
4189
4227
  unmountPageSpecific(state, emit);
4190
4228
  /**
4191
4229
  * Render the VNode into the region and re-mount its islands in one paint — the
package/dist/index.cjs CHANGED
@@ -3169,6 +3169,26 @@ function composeHead(input) {
3169
3169
  }), ...head.elements ?? []]);
3170
3170
  }
3171
3171
  /**
3172
+ * Resolve the FINAL document title for a route's head config — the same value
3173
+ * {@link composeHead} emits in its `<title>` element. A route-supplied `title`-keyed
3174
+ * element wins the keyed last-wins de-dupe over the templated base title (how a route
3175
+ * pins a bare title past `titleTemplate`), so it must win here too; otherwise the
3176
+ * template is applied to `head.title ?? site.name()`. Reused by `spa` for the client
3177
+ * DATA-path `document.title` sync, so client-side navigation matches the SSG output.
3178
+ *
3179
+ * @param head - The route's head config (may be `undefined` for head-less routes).
3180
+ * @param defaults - The normalized head defaults (provides `titleTemplate`).
3181
+ * @param site - The site slice (title fallback).
3182
+ * @returns The final document title string.
3183
+ * @example composeTitle({ title: "Page 2" }, defaults, site) // "Page 2 — Site"
3184
+ */
3185
+ function composeTitle(head, defaults, site) {
3186
+ const config = head ?? {};
3187
+ const pinned = config.elements?.findLast((element) => element.key === "title");
3188
+ if (pinned?.children !== void 0) return pinned.children;
3189
+ return applyTemplate(config.title ?? site.name(), defaults.titleTemplate);
3190
+ }
3191
+ /**
3172
3192
  * Compose the SITE-LEVEL Open Graph / Twitter block for a bare-path redirect or landing
3173
3193
  * page that has no per-route head of its own. Returns `[]` UNLESS a `defaultOgImage` is
3174
3194
  * configured — so apps that opt out keep a bare redirect (no behavior change). The site
@@ -3338,6 +3358,21 @@ function createApi$4(ctx) {
3338
3358
  url: site.canonical(input.url),
3339
3359
  ...ogLocale === void 0 ? {} : { ogLocale }
3340
3360
  }));
3361
+ },
3362
+ /**
3363
+ * Resolve the FINAL document title for a route's head config — the same value `render`
3364
+ * emits in its `<title>` element. Pulled by `spa` on the client DATA path so a
3365
+ * client-side navigation's `document.title` matches the SSG output.
3366
+ *
3367
+ * @param head - The route's head config (may be `undefined` for head-less routes).
3368
+ * @returns The final document title string.
3369
+ * @example
3370
+ * ```ts
3371
+ * api.composeTitle({ title: "Page 2" }); // "Page 2 — Site"
3372
+ * ```
3373
+ */
3374
+ composeTitle(head) {
3375
+ return composeTitle(head, readDefaults(ctx.state), ctx.require(sitePlugin));
3341
3376
  }
3342
3377
  };
3343
3378
  }
@@ -3515,6 +3550,23 @@ const FINGERPRINT_NAMING = {
3515
3550
  asset: "[name]-[hash].[ext]"
3516
3551
  };
3517
3552
  /**
3553
+ * Font url() references the CSS pass leaves EXTERNAL instead of bundling.
3554
+ * Bun's CSS bundler cannot emit url() assets as files — every resolvable
3555
+ * font reference is inlined as a base64 data URI, ballooning the stylesheet
3556
+ * (a site vendoring a font family ships every weight/subset render-blocking
3557
+ * on every page). Marking font extensions external passes the URL through
3558
+ * verbatim, so apps reference fonts root-relative (e.g. `/fonts/x.woff2`)
3559
+ * and serve the files statically via `publicDir`. CSS-only: a font import
3560
+ * left external in JS would be an unresolvable module at runtime.
3561
+ */
3562
+ const CSS_EXTERNAL_FONT_GLOBS = [
3563
+ "*.woff2",
3564
+ "*.woff",
3565
+ "*.ttf",
3566
+ "*.otf",
3567
+ "*.eot"
3568
+ ];
3569
+ /**
3518
3570
  * The default bundler runner — adapts the built-in `Bun.build`.
3519
3571
  *
3520
3572
  * @param options - Entry/outdir/minify/splitting/target/naming settings forwarded to `Bun.build`.
@@ -3527,10 +3579,11 @@ const FINGERPRINT_NAMING = {
3527
3579
  * @param options.naming.entry - Naming template for entry-point outputs.
3528
3580
  * @param options.naming.chunk - Naming template for lazy split chunks.
3529
3581
  * @param options.naming.asset - Naming template for additional emitted assets.
3582
+ * @param options.external - Import/url() globs left unresolved in the output.
3530
3583
  * @returns The structural build result.
3531
3584
  * @example
3532
3585
  * ```ts
3533
- * await defaultRunner({ entrypoints: ["a.css"], outdir: "dist", minify: true, splitting: true, target: "browser", naming: FINGERPRINT_NAMING });
3586
+ * await defaultRunner({ entrypoints: ["a.css"], outdir: "dist", minify: true, splitting: true, target: "browser", naming: FINGERPRINT_NAMING, external: [] });
3534
3587
  * ```
3535
3588
  */
3536
3589
  async function defaultRunner(options) {
@@ -3623,7 +3676,8 @@ async function runOne(ctx, runner, kind, entrypoints, outDir, outdir, minify) {
3623
3676
  minify,
3624
3677
  splitting: true,
3625
3678
  target: "browser",
3626
- naming: FINGERPRINT_NAMING
3679
+ naming: FINGERPRINT_NAMING,
3680
+ external: kind === "css" ? [...CSS_EXTERNAL_FONT_GLOBS] : []
3627
3681
  });
3628
3682
  if (!result.success) throw new Error(`[web] build.bundle ${kind} build failed`);
3629
3683
  const hashed = {};
@@ -10868,16 +10922,19 @@ function currentLocationUrl() {
10868
10922
  /**
10869
10923
  * Apply the matched route's `head` config to the live document (minimal client
10870
10924
  * head-sync for the DATA path: title only — the full meta sync runs on the
10871
- * HTML-over-fetch path from the fetched `<head>`).
10925
+ * HTML-over-fetch path from the fetched `<head>`). The title is resolved through
10926
+ * `head.composeTitle` — the SAME composition `render` uses (`titleTemplate` applied;
10927
+ * a route-pinned `title` element wins) — so a client-side navigation's
10928
+ * `document.title` matches the SSG output instead of the raw route title.
10872
10929
  *
10930
+ * @param head - The head plugin API (resolves the final templated title).
10873
10931
  * @param route - The matched route definition.
10874
10932
  * @param routeContext - The render context (params/data/locale).
10875
10933
  * @example
10876
- * syncDataHead(hit.route, { params, data, locale });
10934
+ * syncDataHead(deps.head, hit.route, { params, data, locale });
10877
10935
  */
10878
- function syncDataHead(route, routeContext) {
10879
- const title = route._handlers.head?.(routeContext)?.title;
10880
- if (title !== void 0 && title !== "") document.title = title;
10936
+ function syncDataHead(head, route, routeContext) {
10937
+ document.title = head.composeTitle(route._handlers.head?.(routeContext));
10881
10938
  }
10882
10939
  /**
10883
10940
  * Builds the single shared SPA kernel — a pure factory over state/config/emit.
@@ -11033,7 +11090,7 @@ function createSpaKernel(state, config, emit, deps) {
11033
11090
  handleStart(pathname);
11034
11091
  const { renderVNode } = await Promise.resolve().then(() => require("./render-DLZEOe4M.cjs"));
11035
11092
  if (signal?.aborted) return;
11036
- syncDataHead(route, routeContext);
11093
+ syncDataHead(deps.head, route, routeContext);
11037
11094
  unmountPageSpecific(state, emit);
11038
11095
  /**
11039
11096
  * Render the VNode into the region and re-mount its islands in one paint — the
package/dist/index.d.cts CHANGED
@@ -1157,6 +1157,19 @@ type Api$4 = {
1157
1157
  url: string;
1158
1158
  locale?: string;
1159
1159
  }): string;
1160
+ /**
1161
+ * Resolve the FINAL document title for a route's head config — the same value `render`
1162
+ * emits in its `<title>` element (`titleTemplate` applied; a route-pinned `title`-keyed
1163
+ * element wins). Used by `spa` to sync `document.title` on client DATA-path navigation.
1164
+ *
1165
+ * @param head - The route's head config (may be `undefined` for head-less routes).
1166
+ * @returns The final document title string.
1167
+ * @example
1168
+ * ```ts
1169
+ * api.composeTitle({ title: "Page 2" }); // "Page 2 — Site"
1170
+ * ```
1171
+ */
1172
+ composeTitle(head: HeadConfig | undefined): string;
1160
1173
  };
1161
1174
  declare namespace types_d_exports$9 {
1162
1175
  export { COMPONENT_HOOK_NAMES, ComponentContext, ComponentDef, ComponentHooks, ComponentInstance, ExtractApi$1 as ExtractApi, PageData, ResolvedSpaConfig, SpaApi, SpaConfig, SpaContext, SpaDataReader, SpaEmitFunction, SpaEvents, SpaKernel, SpaKernelDeps, SpaRequire, SpaState };
package/dist/index.d.mts CHANGED
@@ -1157,6 +1157,19 @@ type Api$4 = {
1157
1157
  url: string;
1158
1158
  locale?: string;
1159
1159
  }): string;
1160
+ /**
1161
+ * Resolve the FINAL document title for a route's head config — the same value `render`
1162
+ * emits in its `<title>` element (`titleTemplate` applied; a route-pinned `title`-keyed
1163
+ * element wins). Used by `spa` to sync `document.title` on client DATA-path navigation.
1164
+ *
1165
+ * @param head - The route's head config (may be `undefined` for head-less routes).
1166
+ * @returns The final document title string.
1167
+ * @example
1168
+ * ```ts
1169
+ * api.composeTitle({ title: "Page 2" }); // "Page 2 — Site"
1170
+ * ```
1171
+ */
1172
+ composeTitle(head: HeadConfig | undefined): string;
1160
1173
  };
1161
1174
  declare namespace types_d_exports$9 {
1162
1175
  export { COMPONENT_HOOK_NAMES, ComponentContext, ComponentDef, ComponentHooks, ComponentInstance, ExtractApi$1 as ExtractApi, PageData, ResolvedSpaConfig, SpaApi, SpaConfig, SpaContext, SpaDataReader, SpaEmitFunction, SpaEvents, SpaKernel, SpaKernelDeps, SpaRequire, SpaState };
package/dist/index.mjs CHANGED
@@ -3156,6 +3156,26 @@ function composeHead(input) {
3156
3156
  }), ...head.elements ?? []]);
3157
3157
  }
3158
3158
  /**
3159
+ * Resolve the FINAL document title for a route's head config — the same value
3160
+ * {@link composeHead} emits in its `<title>` element. A route-supplied `title`-keyed
3161
+ * element wins the keyed last-wins de-dupe over the templated base title (how a route
3162
+ * pins a bare title past `titleTemplate`), so it must win here too; otherwise the
3163
+ * template is applied to `head.title ?? site.name()`. Reused by `spa` for the client
3164
+ * DATA-path `document.title` sync, so client-side navigation matches the SSG output.
3165
+ *
3166
+ * @param head - The route's head config (may be `undefined` for head-less routes).
3167
+ * @param defaults - The normalized head defaults (provides `titleTemplate`).
3168
+ * @param site - The site slice (title fallback).
3169
+ * @returns The final document title string.
3170
+ * @example composeTitle({ title: "Page 2" }, defaults, site) // "Page 2 — Site"
3171
+ */
3172
+ function composeTitle(head, defaults, site) {
3173
+ const config = head ?? {};
3174
+ const pinned = config.elements?.findLast((element) => element.key === "title");
3175
+ if (pinned?.children !== void 0) return pinned.children;
3176
+ return applyTemplate(config.title ?? site.name(), defaults.titleTemplate);
3177
+ }
3178
+ /**
3159
3179
  * Compose the SITE-LEVEL Open Graph / Twitter block for a bare-path redirect or landing
3160
3180
  * page that has no per-route head of its own. Returns `[]` UNLESS a `defaultOgImage` is
3161
3181
  * configured — so apps that opt out keep a bare redirect (no behavior change). The site
@@ -3325,6 +3345,21 @@ function createApi$4(ctx) {
3325
3345
  url: site.canonical(input.url),
3326
3346
  ...ogLocale === void 0 ? {} : { ogLocale }
3327
3347
  }));
3348
+ },
3349
+ /**
3350
+ * Resolve the FINAL document title for a route's head config — the same value `render`
3351
+ * emits in its `<title>` element. Pulled by `spa` on the client DATA path so a
3352
+ * client-side navigation's `document.title` matches the SSG output.
3353
+ *
3354
+ * @param head - The route's head config (may be `undefined` for head-less routes).
3355
+ * @returns The final document title string.
3356
+ * @example
3357
+ * ```ts
3358
+ * api.composeTitle({ title: "Page 2" }); // "Page 2 — Site"
3359
+ * ```
3360
+ */
3361
+ composeTitle(head) {
3362
+ return composeTitle(head, readDefaults(ctx.state), ctx.require(sitePlugin));
3328
3363
  }
3329
3364
  };
3330
3365
  }
@@ -3502,6 +3537,23 @@ const FINGERPRINT_NAMING = {
3502
3537
  asset: "[name]-[hash].[ext]"
3503
3538
  };
3504
3539
  /**
3540
+ * Font url() references the CSS pass leaves EXTERNAL instead of bundling.
3541
+ * Bun's CSS bundler cannot emit url() assets as files — every resolvable
3542
+ * font reference is inlined as a base64 data URI, ballooning the stylesheet
3543
+ * (a site vendoring a font family ships every weight/subset render-blocking
3544
+ * on every page). Marking font extensions external passes the URL through
3545
+ * verbatim, so apps reference fonts root-relative (e.g. `/fonts/x.woff2`)
3546
+ * and serve the files statically via `publicDir`. CSS-only: a font import
3547
+ * left external in JS would be an unresolvable module at runtime.
3548
+ */
3549
+ const CSS_EXTERNAL_FONT_GLOBS = [
3550
+ "*.woff2",
3551
+ "*.woff",
3552
+ "*.ttf",
3553
+ "*.otf",
3554
+ "*.eot"
3555
+ ];
3556
+ /**
3505
3557
  * The default bundler runner — adapts the built-in `Bun.build`.
3506
3558
  *
3507
3559
  * @param options - Entry/outdir/minify/splitting/target/naming settings forwarded to `Bun.build`.
@@ -3514,10 +3566,11 @@ const FINGERPRINT_NAMING = {
3514
3566
  * @param options.naming.entry - Naming template for entry-point outputs.
3515
3567
  * @param options.naming.chunk - Naming template for lazy split chunks.
3516
3568
  * @param options.naming.asset - Naming template for additional emitted assets.
3569
+ * @param options.external - Import/url() globs left unresolved in the output.
3517
3570
  * @returns The structural build result.
3518
3571
  * @example
3519
3572
  * ```ts
3520
- * await defaultRunner({ entrypoints: ["a.css"], outdir: "dist", minify: true, splitting: true, target: "browser", naming: FINGERPRINT_NAMING });
3573
+ * await defaultRunner({ entrypoints: ["a.css"], outdir: "dist", minify: true, splitting: true, target: "browser", naming: FINGERPRINT_NAMING, external: [] });
3521
3574
  * ```
3522
3575
  */
3523
3576
  async function defaultRunner(options) {
@@ -3610,7 +3663,8 @@ async function runOne(ctx, runner, kind, entrypoints, outDir, outdir, minify) {
3610
3663
  minify,
3611
3664
  splitting: true,
3612
3665
  target: "browser",
3613
- naming: FINGERPRINT_NAMING
3666
+ naming: FINGERPRINT_NAMING,
3667
+ external: kind === "css" ? [...CSS_EXTERNAL_FONT_GLOBS] : []
3614
3668
  });
3615
3669
  if (!result.success) throw new Error(`[web] build.bundle ${kind} build failed`);
3616
3670
  const hashed = {};
@@ -10855,16 +10909,19 @@ function currentLocationUrl() {
10855
10909
  /**
10856
10910
  * Apply the matched route's `head` config to the live document (minimal client
10857
10911
  * head-sync for the DATA path: title only — the full meta sync runs on the
10858
- * HTML-over-fetch path from the fetched `<head>`).
10912
+ * HTML-over-fetch path from the fetched `<head>`). The title is resolved through
10913
+ * `head.composeTitle` — the SAME composition `render` uses (`titleTemplate` applied;
10914
+ * a route-pinned `title` element wins) — so a client-side navigation's
10915
+ * `document.title` matches the SSG output instead of the raw route title.
10859
10916
  *
10917
+ * @param head - The head plugin API (resolves the final templated title).
10860
10918
  * @param route - The matched route definition.
10861
10919
  * @param routeContext - The render context (params/data/locale).
10862
10920
  * @example
10863
- * syncDataHead(hit.route, { params, data, locale });
10921
+ * syncDataHead(deps.head, hit.route, { params, data, locale });
10864
10922
  */
10865
- function syncDataHead(route, routeContext) {
10866
- const title = route._handlers.head?.(routeContext)?.title;
10867
- if (title !== void 0 && title !== "") document.title = title;
10923
+ function syncDataHead(head, route, routeContext) {
10924
+ document.title = head.composeTitle(route._handlers.head?.(routeContext));
10868
10925
  }
10869
10926
  /**
10870
10927
  * Builds the single shared SPA kernel — a pure factory over state/config/emit.
@@ -11020,7 +11077,7 @@ function createSpaKernel(state, config, emit, deps) {
11020
11077
  handleStart(pathname);
11021
11078
  const { renderVNode } = await import("./render-BNe0s7fr.mjs");
11022
11079
  if (signal?.aborted) return;
11023
- syncDataHead(route, routeContext);
11080
+ syncDataHead(deps.head, route, routeContext);
11024
11081
  unmountPageSpecific(state, emit);
11025
11082
  /**
11026
11083
  * Render the VNode into the region and re-mount its islands in one paint — the
package/package.json CHANGED
@@ -118,5 +118,5 @@
118
118
  "test:build-e2e": "bun test src/plugins/build/__tests__/e2e/",
119
119
  "test:coverage": "vitest run --project unit --project integration --coverage"
120
120
  },
121
- "version": "1.8.0"
121
+ "version": "1.8.2"
122
122
  }