@moku-labs/web 2.4.11 → 2.4.12

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/index.cjs CHANGED
@@ -2299,7 +2299,8 @@ const headPlugin = createPlugin$1("head", {
2299
2299
  /**
2300
2300
  * @file build phase 1 — bundle. Runs `Bun.build` for CSS and JS separately into
2301
2301
  * outDir (honoring `config.minify`) with content-hashed output naming; caches the
2302
- * fingerprinted asset paths for the pages phase and the complete output list for
2302
+ * fingerprinted asset paths for the pages phase, the chunks the JS entry imports
2303
+ * statically (for `<link rel="modulepreload">`), and the complete output list for
2303
2304
  * the cache-headers phase.
2304
2305
  */
2305
2306
  /** Conventional CSS entry candidates (project-relative). */
@@ -2358,10 +2359,11 @@ const CSS_EXTERNAL_FONT_GLOBS = [
2358
2359
  * @param options.naming.asset - Naming template for additional emitted assets.
2359
2360
  * @param options.external - Import/url() globs left unresolved in the output.
2360
2361
  * @param options.define - Global names replaced with constant expressions.
2362
+ * @param options.metafile - Whether to return the build metafile.
2361
2363
  * @returns The structural build result.
2362
2364
  * @example
2363
2365
  * ```ts
2364
- * await defaultRunner({ entrypoints: ["a.css"], outdir: "dist", minify: true, splitting: true, target: "browser", naming: FINGERPRINT_NAMING, external: [], define: {} });
2366
+ * await defaultRunner({ entrypoints: ["a.css"], outdir: "dist", minify: true, splitting: true, target: "browser", naming: FINGERPRINT_NAMING, external: [], define: {}, metafile: false });
2365
2367
  * ```
2366
2368
  */
2367
2369
  async function defaultRunner(options) {
@@ -2456,6 +2458,73 @@ function normalizeAssetPath(absolutePath, outDir) {
2456
2458
  return node_path$1.default.relative(node_path$1.default.resolve(outDir), node_path$1.default.resolve(absolutePath)).split(node_path$1.default.sep).join("/");
2457
2459
  }
2458
2460
  /**
2461
+ * Parse the runner's metafile into the structural view (object or JSON string).
2462
+ *
2463
+ * @param metafile - The metafile as the runner returned it (possibly absent).
2464
+ * @returns The parsed metafile, or `undefined` when absent or unreadable.
2465
+ * @example
2466
+ * ```ts
2467
+ * parseMetafile('{"outputs":{}}'); // { outputs: {} }
2468
+ * ```
2469
+ */
2470
+ function parseMetafile(metafile) {
2471
+ if (typeof metafile !== "string") return metafile;
2472
+ try {
2473
+ return JSON.parse(metafile);
2474
+ } catch {
2475
+ return;
2476
+ }
2477
+ }
2478
+ /**
2479
+ * Normalize an outdir-relative output path into one graph key ("./a.js" and "a.js"
2480
+ * are the same file; Windows separators become "/").
2481
+ *
2482
+ * @param file - An outdir-relative output path.
2483
+ * @returns The normalized POSIX key.
2484
+ * @example
2485
+ * ```ts
2486
+ * toKey("./chunk-1.js"); // "chunk-1.js"
2487
+ * ```
2488
+ */
2489
+ function toKey(file) {
2490
+ return node_path$1.default.posix.normalize(file.split(node_path$1.default.sep).join("/"));
2491
+ }
2492
+ /**
2493
+ * Collect the chunks the JS entry points import STATICALLY, transitively, from the
2494
+ * metafile import graph. Only `import-statement` edges are followed: a chunk reached
2495
+ * only through a dynamic `import()` stays lazy and is never preloaded. These are the
2496
+ * files the browser would otherwise discover only after the entry finished
2497
+ * downloading (a request waterfall); the pages phase emits one
2498
+ * `<link rel="modulepreload">` per path so they load in parallel with the entry.
2499
+ * The walk starts from the `entry-point` artifacts, not from the metafile: Bun also
2500
+ * tags each lazy chunk with an `entryPoint`, and a lazy chunk may import the entry.
2501
+ *
2502
+ * @param result - The JS pass result (artifacts + metafile; no metafile → no preloads).
2503
+ * @param outdir - The bundler output directory the metafile paths are relative to.
2504
+ * @param outDir - The publish root the web paths are made relative to.
2505
+ * @returns The publish-root-relative paths of the static chunks (entries excluded).
2506
+ * @example
2507
+ * ```ts
2508
+ * staticEntryChunks(result, "dist/assets", "dist"); // ["assets/chunk-9f8e.js"]
2509
+ * ```
2510
+ */
2511
+ function staticEntryChunks(result, outdir, outDir) {
2512
+ const metafile = parseMetafile(result.metafile);
2513
+ if (!metafile) return [];
2514
+ const graph = new Map(Object.entries(metafile.outputs).map(([file, output]) => [toKey(file), (output.imports ?? []).filter((edge) => edge.kind === "import-statement").map((edge) => toKey(edge.path))]));
2515
+ const entries = result.outputs.filter((output) => output.kind === "entry-point" && output.path.endsWith(".js")).map((output) => toKey(node_path$1.default.relative(node_path$1.default.resolve(outdir), node_path$1.default.resolve(output.path))));
2516
+ const seen = new Set(entries);
2517
+ const queue = [...entries];
2518
+ const chunks = [];
2519
+ for (let file = queue.shift(); file !== void 0; file = queue.shift()) for (const target of graph.get(file) ?? []) {
2520
+ if (seen.has(target)) continue;
2521
+ seen.add(target);
2522
+ queue.push(target);
2523
+ chunks.push(normalizeAssetPath(node_path$1.default.join(outdir, target), outDir));
2524
+ }
2525
+ return chunks;
2526
+ }
2527
+ /**
2459
2528
  * Run one bundler pass for a single asset kind and record the hashed output
2460
2529
  * paths under `state.buildCache` keyed by the original entry basename. Lazy
2461
2530
  * split chunks are emitted to disk but excluded from the recorded manifest
@@ -2484,7 +2553,8 @@ async function runOne(ctx, runner, kind, entrypoints, outDir, outdir, minify, de
2484
2553
  target: "browser",
2485
2554
  naming: FINGERPRINT_NAMING,
2486
2555
  external: kind === "css" ? [...CSS_EXTERNAL_FONT_GLOBS] : [],
2487
- define
2556
+ define,
2557
+ metafile: kind === "js"
2488
2558
  });
2489
2559
  if (!result.success) throw new Error(`[web] build.bundle ${kind} build failed`);
2490
2560
  const hashed = {};
@@ -2494,6 +2564,7 @@ async function runOne(ctx, runner, kind, entrypoints, outDir, outdir, minify, de
2494
2564
  }
2495
2565
  ctx.state.buildCache.set(kind, hashed);
2496
2566
  ctx.state.buildCache.set(`${kind}:outputs`, result.outputs.map((output) => normalizeAssetPath(output.path, outDir)));
2567
+ if (kind === "js") ctx.state.buildCache.set("js:preload", staticEntryChunks(result, outdir, outDir));
2497
2568
  ctx.log.debug("build:bundle", {
2498
2569
  kind,
2499
2570
  count: result.outputs.length
@@ -2570,6 +2641,22 @@ function readBundleOutputs(ctx, kind) {
2570
2641
  return Array.isArray(entry) ? entry : [];
2571
2642
  }
2572
2643
  /**
2644
+ * Read the chunks the JS entry imports statically (publish-root-relative web paths),
2645
+ * recorded by the bundle phase under `js:preload`. Dynamic-import-only chunks are
2646
+ * never in this list.
2647
+ *
2648
+ * @param ctx - Plugin context (provides `state`).
2649
+ * @returns The static chunk paths, or an empty array when absent.
2650
+ * @example
2651
+ * ```ts
2652
+ * readPreloads(ctx); // ["assets/chunk-9f8e.js"]
2653
+ * ```
2654
+ */
2655
+ function readPreloads(ctx) {
2656
+ const entry = ctx.state.buildCache.get("js:preload");
2657
+ return Array.isArray(entry) ? entry : [];
2658
+ }
2659
+ /**
2573
2660
  * Render the stylesheet `<link>` tags for the fingerprinted CSS manifest.
2574
2661
  *
2575
2662
  * @param ctx - Plugin context (provides `state`).
@@ -2583,17 +2670,23 @@ function buildCssTags(ctx) {
2583
2670
  return Object.values(readManifest(ctx, "css")).map((href) => `<link rel="stylesheet" href="/${href}">`).join("");
2584
2671
  }
2585
2672
  /**
2586
- * Render the module `<script>` tags for the fingerprinted JS manifest.
2673
+ * Render the module `<script>` tags for the fingerprinted JS manifest, followed by
2674
+ * one `<link rel="modulepreload">` per chunk the entry imports statically. Without
2675
+ * the preloads the browser finds those chunks only after the entry has downloaded
2676
+ * and parsed, a request waterfall that delays hydration.
2587
2677
  *
2588
2678
  * @param ctx - Plugin context (provides `state`).
2589
- * @returns The concatenated `<script type="module">` tags (possibly `""`).
2679
+ * @returns The concatenated `<script type="module">` and modulepreload tags (possibly `""`).
2590
2680
  * @example
2591
2681
  * ```ts
2592
- * buildJsTags(ctx); // '<script type="module" src="/assets/spa-abc123.js"><\/script>'
2682
+ * buildJsTags(ctx);
2683
+ * // '<script type="module" src="/assets/spa-abc123.js"><\/script><link rel="modulepreload" href="/assets/chunk-9f8e.js">'
2593
2684
  * ```
2594
2685
  */
2595
2686
  function buildJsTags(ctx) {
2596
- return Object.values(readManifest(ctx, "js")).map((src) => `<script type="module" src="/${src}"><\/script>`).join("");
2687
+ const scripts = Object.values(readManifest(ctx, "js")).map((src) => `<script type="module" src="/${src}"><\/script>`).join("");
2688
+ if (scripts === "") return "";
2689
+ return scripts + readPreloads(ctx).map((href) => `<link rel="modulepreload" href="/${href}">`).join("");
2597
2690
  }
2598
2691
  /**
2599
2692
  * Build the asset tag block from the fingerprinted manifests — both kinds by
package/dist/index.mjs CHANGED
@@ -2255,7 +2255,8 @@ const headPlugin = createPlugin$1("head", {
2255
2255
  /**
2256
2256
  * @file build phase 1 — bundle. Runs `Bun.build` for CSS and JS separately into
2257
2257
  * outDir (honoring `config.minify`) with content-hashed output naming; caches the
2258
- * fingerprinted asset paths for the pages phase and the complete output list for
2258
+ * fingerprinted asset paths for the pages phase, the chunks the JS entry imports
2259
+ * statically (for `<link rel="modulepreload">`), and the complete output list for
2259
2260
  * the cache-headers phase.
2260
2261
  */
2261
2262
  /** Conventional CSS entry candidates (project-relative). */
@@ -2314,10 +2315,11 @@ const CSS_EXTERNAL_FONT_GLOBS = [
2314
2315
  * @param options.naming.asset - Naming template for additional emitted assets.
2315
2316
  * @param options.external - Import/url() globs left unresolved in the output.
2316
2317
  * @param options.define - Global names replaced with constant expressions.
2318
+ * @param options.metafile - Whether to return the build metafile.
2317
2319
  * @returns The structural build result.
2318
2320
  * @example
2319
2321
  * ```ts
2320
- * await defaultRunner({ entrypoints: ["a.css"], outdir: "dist", minify: true, splitting: true, target: "browser", naming: FINGERPRINT_NAMING, external: [], define: {} });
2322
+ * await defaultRunner({ entrypoints: ["a.css"], outdir: "dist", minify: true, splitting: true, target: "browser", naming: FINGERPRINT_NAMING, external: [], define: {}, metafile: false });
2321
2323
  * ```
2322
2324
  */
2323
2325
  async function defaultRunner(options) {
@@ -2412,6 +2414,73 @@ function normalizeAssetPath(absolutePath, outDir) {
2412
2414
  return path.relative(path.resolve(outDir), path.resolve(absolutePath)).split(path.sep).join("/");
2413
2415
  }
2414
2416
  /**
2417
+ * Parse the runner's metafile into the structural view (object or JSON string).
2418
+ *
2419
+ * @param metafile - The metafile as the runner returned it (possibly absent).
2420
+ * @returns The parsed metafile, or `undefined` when absent or unreadable.
2421
+ * @example
2422
+ * ```ts
2423
+ * parseMetafile('{"outputs":{}}'); // { outputs: {} }
2424
+ * ```
2425
+ */
2426
+ function parseMetafile(metafile) {
2427
+ if (typeof metafile !== "string") return metafile;
2428
+ try {
2429
+ return JSON.parse(metafile);
2430
+ } catch {
2431
+ return;
2432
+ }
2433
+ }
2434
+ /**
2435
+ * Normalize an outdir-relative output path into one graph key ("./a.js" and "a.js"
2436
+ * are the same file; Windows separators become "/").
2437
+ *
2438
+ * @param file - An outdir-relative output path.
2439
+ * @returns The normalized POSIX key.
2440
+ * @example
2441
+ * ```ts
2442
+ * toKey("./chunk-1.js"); // "chunk-1.js"
2443
+ * ```
2444
+ */
2445
+ function toKey(file) {
2446
+ return path.posix.normalize(file.split(path.sep).join("/"));
2447
+ }
2448
+ /**
2449
+ * Collect the chunks the JS entry points import STATICALLY, transitively, from the
2450
+ * metafile import graph. Only `import-statement` edges are followed: a chunk reached
2451
+ * only through a dynamic `import()` stays lazy and is never preloaded. These are the
2452
+ * files the browser would otherwise discover only after the entry finished
2453
+ * downloading (a request waterfall); the pages phase emits one
2454
+ * `<link rel="modulepreload">` per path so they load in parallel with the entry.
2455
+ * The walk starts from the `entry-point` artifacts, not from the metafile: Bun also
2456
+ * tags each lazy chunk with an `entryPoint`, and a lazy chunk may import the entry.
2457
+ *
2458
+ * @param result - The JS pass result (artifacts + metafile; no metafile → no preloads).
2459
+ * @param outdir - The bundler output directory the metafile paths are relative to.
2460
+ * @param outDir - The publish root the web paths are made relative to.
2461
+ * @returns The publish-root-relative paths of the static chunks (entries excluded).
2462
+ * @example
2463
+ * ```ts
2464
+ * staticEntryChunks(result, "dist/assets", "dist"); // ["assets/chunk-9f8e.js"]
2465
+ * ```
2466
+ */
2467
+ function staticEntryChunks(result, outdir, outDir) {
2468
+ const metafile = parseMetafile(result.metafile);
2469
+ if (!metafile) return [];
2470
+ const graph = new Map(Object.entries(metafile.outputs).map(([file, output]) => [toKey(file), (output.imports ?? []).filter((edge) => edge.kind === "import-statement").map((edge) => toKey(edge.path))]));
2471
+ const entries = result.outputs.filter((output) => output.kind === "entry-point" && output.path.endsWith(".js")).map((output) => toKey(path.relative(path.resolve(outdir), path.resolve(output.path))));
2472
+ const seen = new Set(entries);
2473
+ const queue = [...entries];
2474
+ const chunks = [];
2475
+ for (let file = queue.shift(); file !== void 0; file = queue.shift()) for (const target of graph.get(file) ?? []) {
2476
+ if (seen.has(target)) continue;
2477
+ seen.add(target);
2478
+ queue.push(target);
2479
+ chunks.push(normalizeAssetPath(path.join(outdir, target), outDir));
2480
+ }
2481
+ return chunks;
2482
+ }
2483
+ /**
2415
2484
  * Run one bundler pass for a single asset kind and record the hashed output
2416
2485
  * paths under `state.buildCache` keyed by the original entry basename. Lazy
2417
2486
  * split chunks are emitted to disk but excluded from the recorded manifest
@@ -2440,7 +2509,8 @@ async function runOne(ctx, runner, kind, entrypoints, outDir, outdir, minify, de
2440
2509
  target: "browser",
2441
2510
  naming: FINGERPRINT_NAMING,
2442
2511
  external: kind === "css" ? [...CSS_EXTERNAL_FONT_GLOBS] : [],
2443
- define
2512
+ define,
2513
+ metafile: kind === "js"
2444
2514
  });
2445
2515
  if (!result.success) throw new Error(`[web] build.bundle ${kind} build failed`);
2446
2516
  const hashed = {};
@@ -2450,6 +2520,7 @@ async function runOne(ctx, runner, kind, entrypoints, outDir, outdir, minify, de
2450
2520
  }
2451
2521
  ctx.state.buildCache.set(kind, hashed);
2452
2522
  ctx.state.buildCache.set(`${kind}:outputs`, result.outputs.map((output) => normalizeAssetPath(output.path, outDir)));
2523
+ if (kind === "js") ctx.state.buildCache.set("js:preload", staticEntryChunks(result, outdir, outDir));
2453
2524
  ctx.log.debug("build:bundle", {
2454
2525
  kind,
2455
2526
  count: result.outputs.length
@@ -2526,6 +2597,22 @@ function readBundleOutputs(ctx, kind) {
2526
2597
  return Array.isArray(entry) ? entry : [];
2527
2598
  }
2528
2599
  /**
2600
+ * Read the chunks the JS entry imports statically (publish-root-relative web paths),
2601
+ * recorded by the bundle phase under `js:preload`. Dynamic-import-only chunks are
2602
+ * never in this list.
2603
+ *
2604
+ * @param ctx - Plugin context (provides `state`).
2605
+ * @returns The static chunk paths, or an empty array when absent.
2606
+ * @example
2607
+ * ```ts
2608
+ * readPreloads(ctx); // ["assets/chunk-9f8e.js"]
2609
+ * ```
2610
+ */
2611
+ function readPreloads(ctx) {
2612
+ const entry = ctx.state.buildCache.get("js:preload");
2613
+ return Array.isArray(entry) ? entry : [];
2614
+ }
2615
+ /**
2529
2616
  * Render the stylesheet `<link>` tags for the fingerprinted CSS manifest.
2530
2617
  *
2531
2618
  * @param ctx - Plugin context (provides `state`).
@@ -2539,17 +2626,23 @@ function buildCssTags(ctx) {
2539
2626
  return Object.values(readManifest(ctx, "css")).map((href) => `<link rel="stylesheet" href="/${href}">`).join("");
2540
2627
  }
2541
2628
  /**
2542
- * Render the module `<script>` tags for the fingerprinted JS manifest.
2629
+ * Render the module `<script>` tags for the fingerprinted JS manifest, followed by
2630
+ * one `<link rel="modulepreload">` per chunk the entry imports statically. Without
2631
+ * the preloads the browser finds those chunks only after the entry has downloaded
2632
+ * and parsed, a request waterfall that delays hydration.
2543
2633
  *
2544
2634
  * @param ctx - Plugin context (provides `state`).
2545
- * @returns The concatenated `<script type="module">` tags (possibly `""`).
2635
+ * @returns The concatenated `<script type="module">` and modulepreload tags (possibly `""`).
2546
2636
  * @example
2547
2637
  * ```ts
2548
- * buildJsTags(ctx); // '<script type="module" src="/assets/spa-abc123.js"><\/script>'
2638
+ * buildJsTags(ctx);
2639
+ * // '<script type="module" src="/assets/spa-abc123.js"><\/script><link rel="modulepreload" href="/assets/chunk-9f8e.js">'
2549
2640
  * ```
2550
2641
  */
2551
2642
  function buildJsTags(ctx) {
2552
- return Object.values(readManifest(ctx, "js")).map((src) => `<script type="module" src="/${src}"><\/script>`).join("");
2643
+ const scripts = Object.values(readManifest(ctx, "js")).map((src) => `<script type="module" src="/${src}"><\/script>`).join("");
2644
+ if (scripts === "") return "";
2645
+ return scripts + readPreloads(ctx).map((href) => `<link rel="modulepreload" href="/${href}">`).join("");
2553
2646
  }
2554
2647
  /**
2555
2648
  * Build the asset tag block from the fingerprinted manifests — both kinds by
package/package.json CHANGED
@@ -139,5 +139,5 @@
139
139
  "typescript-eslint": "8.58.0",
140
140
  "vitest": "4.0.18"
141
141
  },
142
- "version": "2.4.11"
142
+ "version": "2.4.12"
143
143
  }