@moku-labs/web 2.4.11 → 2.4.13
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 +6 -0
- package/dist/browser.mjs +35 -11
- package/dist/index.cjs +135 -18
- package/dist/index.d.cts +6 -0
- package/dist/index.d.mts +6 -0
- package/dist/index.mjs +135 -18
- package/package.json +1 -1
package/dist/browser.d.mts
CHANGED
|
@@ -1998,6 +1998,12 @@ interface DataState {
|
|
|
1998
1998
|
lastWrite: DataWriteSummary | null;
|
|
1999
1999
|
/** Per-path fetched data, cached after the first `at(path)` (browser). */
|
|
2000
2000
|
cache: Map<string, unknown>;
|
|
2001
|
+
/**
|
|
2002
|
+
* Per-path fetches still in flight (browser). A second `at(path)` awaits the
|
|
2003
|
+
* same promise instead of starting another request. The entry is removed when
|
|
2004
|
+
* the fetch settles, on success and on failure, so a failure is never cached.
|
|
2005
|
+
*/
|
|
2006
|
+
pending: Map<string, Promise<unknown | null>>;
|
|
2001
2007
|
}
|
|
2002
2008
|
/**
|
|
2003
2009
|
* Public API mounted at `app.data` — the agnostic data provider. `write()` is the
|
package/dist/browser.mjs
CHANGED
|
@@ -2367,6 +2367,30 @@ async function loadJson(pathOrUrl) {
|
|
|
2367
2367
|
* the isomorphic `loadJson` (whose Node branch is itself lazy).
|
|
2368
2368
|
*/
|
|
2369
2369
|
/**
|
|
2370
|
+
* Fetches one path's data, caches it on success, and always clears the path's
|
|
2371
|
+
* in-flight entry. Never rejects: a failed fetch or JSON parse resolves `null`
|
|
2372
|
+
* and caches nothing, so a later `at(path)` retries.
|
|
2373
|
+
*
|
|
2374
|
+
* @param ctx - The data plugin context.
|
|
2375
|
+
* @param path - The page URL path (e.g. `/en/hello/`).
|
|
2376
|
+
* @returns The page's raw data, or `null` on failure.
|
|
2377
|
+
* @example
|
|
2378
|
+
* ```ts
|
|
2379
|
+
* const raw = await fetchAndCache(ctx, "/en/hello/");
|
|
2380
|
+
* ```
|
|
2381
|
+
*/
|
|
2382
|
+
async function fetchAndCache(ctx, path) {
|
|
2383
|
+
try {
|
|
2384
|
+
const data = await loadJson(`${ctx.config.baseUrl}${dataSuffix(path)}`);
|
|
2385
|
+
ctx.state.cache.set(path, data);
|
|
2386
|
+
return data;
|
|
2387
|
+
} catch {
|
|
2388
|
+
return null;
|
|
2389
|
+
} finally {
|
|
2390
|
+
ctx.state.pending.delete(path);
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
/**
|
|
2370
2394
|
* Builds the data provider — the agnostic bridge. `write()` is the Node persist
|
|
2371
2395
|
* side; `at()` is the browser read side; `urlFor`/`fileFor` are the pure
|
|
2372
2396
|
* convention. No `onStart`/`onStop` (holds no long-lived resource).
|
|
@@ -2384,6 +2408,7 @@ function dataApi(ctx) {
|
|
|
2384
2408
|
return {
|
|
2385
2409
|
/**
|
|
2386
2410
|
* READ (browser) — fetch (and cache) the persisted data for a page path.
|
|
2411
|
+
* Concurrent calls for one path share one in-flight request.
|
|
2387
2412
|
* Returns the raw JSON as `unknown`, which the route uses directly as `ctx.data`
|
|
2388
2413
|
* (no route `.parse()`); returns `null` if the fetch or JSON parse fails (so
|
|
2389
2414
|
* `spa` can fall back to HTML).
|
|
@@ -2397,13 +2422,11 @@ function dataApi(ctx) {
|
|
|
2397
2422
|
*/
|
|
2398
2423
|
async at(path) {
|
|
2399
2424
|
if (ctx.state.cache.has(path)) return ctx.state.cache.get(path);
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
return null;
|
|
2406
|
-
}
|
|
2425
|
+
const inFlight = ctx.state.pending.get(path);
|
|
2426
|
+
if (inFlight) return inFlight;
|
|
2427
|
+
const request = fetchAndCache(ctx, path);
|
|
2428
|
+
ctx.state.pending.set(path, request);
|
|
2429
|
+
return request;
|
|
2407
2430
|
},
|
|
2408
2431
|
/**
|
|
2409
2432
|
* WRITE (Node) — persist one JSON file per entry, keyed by page path. Called by
|
|
@@ -2472,13 +2495,13 @@ const defaultDataConfig = {
|
|
|
2472
2495
|
//#region src/plugins/data/state.ts
|
|
2473
2496
|
/**
|
|
2474
2497
|
* Creates initial data state: a null `lastWrite` slot (populated by the Node
|
|
2475
|
-
* `write()` side)
|
|
2476
|
-
* side on first fetch).
|
|
2498
|
+
* `write()` side), an empty `cache` (populated lazily by the browser `at(path)`
|
|
2499
|
+
* side on first fetch) and an empty `pending` map of in-flight fetches.
|
|
2477
2500
|
*
|
|
2478
2501
|
* @param _ctx - Minimal context with global and config.
|
|
2479
2502
|
* @param _ctx.global - Global framework configuration.
|
|
2480
2503
|
* @param _ctx.config - Resolved plugin configuration.
|
|
2481
|
-
* @returns Fresh data state with no recorded write and an empty per-path cache.
|
|
2504
|
+
* @returns Fresh data state with no recorded write and an empty per-path cache and no pending fetch.
|
|
2482
2505
|
* @example
|
|
2483
2506
|
* ```ts
|
|
2484
2507
|
* const state = createDataState({ global: {}, config });
|
|
@@ -2487,7 +2510,8 @@ const defaultDataConfig = {
|
|
|
2487
2510
|
function createDataState(_ctx) {
|
|
2488
2511
|
return {
|
|
2489
2512
|
lastWrite: null,
|
|
2490
|
-
cache: /* @__PURE__ */ new Map()
|
|
2513
|
+
cache: /* @__PURE__ */ new Map(),
|
|
2514
|
+
pending: /* @__PURE__ */ new Map()
|
|
2491
2515
|
};
|
|
2492
2516
|
}
|
|
2493
2517
|
//#endregion
|
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
|
|
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);
|
|
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
|
-
|
|
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
|
|
@@ -4532,6 +4625,30 @@ async function loadJson(pathOrUrl) {
|
|
|
4532
4625
|
* the isomorphic `loadJson` (whose Node branch is itself lazy).
|
|
4533
4626
|
*/
|
|
4534
4627
|
/**
|
|
4628
|
+
* Fetches one path's data, caches it on success, and always clears the path's
|
|
4629
|
+
* in-flight entry. Never rejects: a failed fetch or JSON parse resolves `null`
|
|
4630
|
+
* and caches nothing, so a later `at(path)` retries.
|
|
4631
|
+
*
|
|
4632
|
+
* @param ctx - The data plugin context.
|
|
4633
|
+
* @param path - The page URL path (e.g. `/en/hello/`).
|
|
4634
|
+
* @returns The page's raw data, or `null` on failure.
|
|
4635
|
+
* @example
|
|
4636
|
+
* ```ts
|
|
4637
|
+
* const raw = await fetchAndCache(ctx, "/en/hello/");
|
|
4638
|
+
* ```
|
|
4639
|
+
*/
|
|
4640
|
+
async function fetchAndCache(ctx, path) {
|
|
4641
|
+
try {
|
|
4642
|
+
const data = await loadJson(`${ctx.config.baseUrl}${require_convention.dataSuffix(path)}`);
|
|
4643
|
+
ctx.state.cache.set(path, data);
|
|
4644
|
+
return data;
|
|
4645
|
+
} catch {
|
|
4646
|
+
return null;
|
|
4647
|
+
} finally {
|
|
4648
|
+
ctx.state.pending.delete(path);
|
|
4649
|
+
}
|
|
4650
|
+
}
|
|
4651
|
+
/**
|
|
4535
4652
|
* Builds the data provider — the agnostic bridge. `write()` is the Node persist
|
|
4536
4653
|
* side; `at()` is the browser read side; `urlFor`/`fileFor` are the pure
|
|
4537
4654
|
* convention. No `onStart`/`onStop` (holds no long-lived resource).
|
|
@@ -4549,6 +4666,7 @@ function dataApi(ctx) {
|
|
|
4549
4666
|
return {
|
|
4550
4667
|
/**
|
|
4551
4668
|
* READ (browser) — fetch (and cache) the persisted data for a page path.
|
|
4669
|
+
* Concurrent calls for one path share one in-flight request.
|
|
4552
4670
|
* Returns the raw JSON as `unknown`, which the route uses directly as `ctx.data`
|
|
4553
4671
|
* (no route `.parse()`); returns `null` if the fetch or JSON parse fails (so
|
|
4554
4672
|
* `spa` can fall back to HTML).
|
|
@@ -4562,13 +4680,11 @@ function dataApi(ctx) {
|
|
|
4562
4680
|
*/
|
|
4563
4681
|
async at(path) {
|
|
4564
4682
|
if (ctx.state.cache.has(path)) return ctx.state.cache.get(path);
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
return null;
|
|
4571
|
-
}
|
|
4683
|
+
const inFlight = ctx.state.pending.get(path);
|
|
4684
|
+
if (inFlight) return inFlight;
|
|
4685
|
+
const request = fetchAndCache(ctx, path);
|
|
4686
|
+
ctx.state.pending.set(path, request);
|
|
4687
|
+
return request;
|
|
4572
4688
|
},
|
|
4573
4689
|
/**
|
|
4574
4690
|
* WRITE (Node) — persist one JSON file per entry, keyed by page path. Called by
|
|
@@ -4637,13 +4753,13 @@ const defaultDataConfig = {
|
|
|
4637
4753
|
//#region src/plugins/data/state.ts
|
|
4638
4754
|
/**
|
|
4639
4755
|
* Creates initial data state: a null `lastWrite` slot (populated by the Node
|
|
4640
|
-
* `write()` side)
|
|
4641
|
-
* side on first fetch).
|
|
4756
|
+
* `write()` side), an empty `cache` (populated lazily by the browser `at(path)`
|
|
4757
|
+
* side on first fetch) and an empty `pending` map of in-flight fetches.
|
|
4642
4758
|
*
|
|
4643
4759
|
* @param _ctx - Minimal context with global and config.
|
|
4644
4760
|
* @param _ctx.global - Global framework configuration.
|
|
4645
4761
|
* @param _ctx.config - Resolved plugin configuration.
|
|
4646
|
-
* @returns Fresh data state with no recorded write and an empty per-path cache.
|
|
4762
|
+
* @returns Fresh data state with no recorded write and an empty per-path cache and no pending fetch.
|
|
4647
4763
|
* @example
|
|
4648
4764
|
* ```ts
|
|
4649
4765
|
* const state = createDataState({ global: {}, config });
|
|
@@ -4652,7 +4768,8 @@ const defaultDataConfig = {
|
|
|
4652
4768
|
function createDataState(_ctx) {
|
|
4653
4769
|
return {
|
|
4654
4770
|
lastWrite: null,
|
|
4655
|
-
cache: /* @__PURE__ */ new Map()
|
|
4771
|
+
cache: /* @__PURE__ */ new Map(),
|
|
4772
|
+
pending: /* @__PURE__ */ new Map()
|
|
4656
4773
|
};
|
|
4657
4774
|
}
|
|
4658
4775
|
//#endregion
|
package/dist/index.d.cts
CHANGED
|
@@ -3467,6 +3467,12 @@ interface DataState {
|
|
|
3467
3467
|
lastWrite: DataWriteSummary | null;
|
|
3468
3468
|
/** Per-path fetched data, cached after the first `at(path)` (browser). */
|
|
3469
3469
|
cache: Map<string, unknown>;
|
|
3470
|
+
/**
|
|
3471
|
+
* Per-path fetches still in flight (browser). A second `at(path)` awaits the
|
|
3472
|
+
* same promise instead of starting another request. The entry is removed when
|
|
3473
|
+
* the fetch settles, on success and on failure, so a failure is never cached.
|
|
3474
|
+
*/
|
|
3475
|
+
pending: Map<string, Promise<unknown | null>>;
|
|
3470
3476
|
}
|
|
3471
3477
|
/**
|
|
3472
3478
|
* Public API mounted at `app.data` — the agnostic data provider. `write()` is the
|
package/dist/index.d.mts
CHANGED
|
@@ -3465,6 +3465,12 @@ interface DataState {
|
|
|
3465
3465
|
lastWrite: DataWriteSummary | null;
|
|
3466
3466
|
/** Per-path fetched data, cached after the first `at(path)` (browser). */
|
|
3467
3467
|
cache: Map<string, unknown>;
|
|
3468
|
+
/**
|
|
3469
|
+
* Per-path fetches still in flight (browser). A second `at(path)` awaits the
|
|
3470
|
+
* same promise instead of starting another request. The entry is removed when
|
|
3471
|
+
* the fetch settles, on success and on failure, so a failure is never cached.
|
|
3472
|
+
*/
|
|
3473
|
+
pending: Map<string, Promise<unknown | null>>;
|
|
3468
3474
|
}
|
|
3469
3475
|
/**
|
|
3470
3476
|
* Public API mounted at `app.data` — the agnostic data provider. `write()` is the
|
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
|
|
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);
|
|
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
|
-
|
|
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
|
|
@@ -4488,6 +4581,30 @@ async function loadJson(pathOrUrl) {
|
|
|
4488
4581
|
* the isomorphic `loadJson` (whose Node branch is itself lazy).
|
|
4489
4582
|
*/
|
|
4490
4583
|
/**
|
|
4584
|
+
* Fetches one path's data, caches it on success, and always clears the path's
|
|
4585
|
+
* in-flight entry. Never rejects: a failed fetch or JSON parse resolves `null`
|
|
4586
|
+
* and caches nothing, so a later `at(path)` retries.
|
|
4587
|
+
*
|
|
4588
|
+
* @param ctx - The data plugin context.
|
|
4589
|
+
* @param path - The page URL path (e.g. `/en/hello/`).
|
|
4590
|
+
* @returns The page's raw data, or `null` on failure.
|
|
4591
|
+
* @example
|
|
4592
|
+
* ```ts
|
|
4593
|
+
* const raw = await fetchAndCache(ctx, "/en/hello/");
|
|
4594
|
+
* ```
|
|
4595
|
+
*/
|
|
4596
|
+
async function fetchAndCache(ctx, path) {
|
|
4597
|
+
try {
|
|
4598
|
+
const data = await loadJson(`${ctx.config.baseUrl}${dataSuffix(path)}`);
|
|
4599
|
+
ctx.state.cache.set(path, data);
|
|
4600
|
+
return data;
|
|
4601
|
+
} catch {
|
|
4602
|
+
return null;
|
|
4603
|
+
} finally {
|
|
4604
|
+
ctx.state.pending.delete(path);
|
|
4605
|
+
}
|
|
4606
|
+
}
|
|
4607
|
+
/**
|
|
4491
4608
|
* Builds the data provider — the agnostic bridge. `write()` is the Node persist
|
|
4492
4609
|
* side; `at()` is the browser read side; `urlFor`/`fileFor` are the pure
|
|
4493
4610
|
* convention. No `onStart`/`onStop` (holds no long-lived resource).
|
|
@@ -4505,6 +4622,7 @@ function dataApi(ctx) {
|
|
|
4505
4622
|
return {
|
|
4506
4623
|
/**
|
|
4507
4624
|
* READ (browser) — fetch (and cache) the persisted data for a page path.
|
|
4625
|
+
* Concurrent calls for one path share one in-flight request.
|
|
4508
4626
|
* Returns the raw JSON as `unknown`, which the route uses directly as `ctx.data`
|
|
4509
4627
|
* (no route `.parse()`); returns `null` if the fetch or JSON parse fails (so
|
|
4510
4628
|
* `spa` can fall back to HTML).
|
|
@@ -4518,13 +4636,11 @@ function dataApi(ctx) {
|
|
|
4518
4636
|
*/
|
|
4519
4637
|
async at(path) {
|
|
4520
4638
|
if (ctx.state.cache.has(path)) return ctx.state.cache.get(path);
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
return null;
|
|
4527
|
-
}
|
|
4639
|
+
const inFlight = ctx.state.pending.get(path);
|
|
4640
|
+
if (inFlight) return inFlight;
|
|
4641
|
+
const request = fetchAndCache(ctx, path);
|
|
4642
|
+
ctx.state.pending.set(path, request);
|
|
4643
|
+
return request;
|
|
4528
4644
|
},
|
|
4529
4645
|
/**
|
|
4530
4646
|
* WRITE (Node) — persist one JSON file per entry, keyed by page path. Called by
|
|
@@ -4593,13 +4709,13 @@ const defaultDataConfig = {
|
|
|
4593
4709
|
//#region src/plugins/data/state.ts
|
|
4594
4710
|
/**
|
|
4595
4711
|
* Creates initial data state: a null `lastWrite` slot (populated by the Node
|
|
4596
|
-
* `write()` side)
|
|
4597
|
-
* side on first fetch).
|
|
4712
|
+
* `write()` side), an empty `cache` (populated lazily by the browser `at(path)`
|
|
4713
|
+
* side on first fetch) and an empty `pending` map of in-flight fetches.
|
|
4598
4714
|
*
|
|
4599
4715
|
* @param _ctx - Minimal context with global and config.
|
|
4600
4716
|
* @param _ctx.global - Global framework configuration.
|
|
4601
4717
|
* @param _ctx.config - Resolved plugin configuration.
|
|
4602
|
-
* @returns Fresh data state with no recorded write and an empty per-path cache.
|
|
4718
|
+
* @returns Fresh data state with no recorded write and an empty per-path cache and no pending fetch.
|
|
4603
4719
|
* @example
|
|
4604
4720
|
* ```ts
|
|
4605
4721
|
* const state = createDataState({ global: {}, config });
|
|
@@ -4608,7 +4724,8 @@ const defaultDataConfig = {
|
|
|
4608
4724
|
function createDataState(_ctx) {
|
|
4609
4725
|
return {
|
|
4610
4726
|
lastWrite: null,
|
|
4611
|
-
cache: /* @__PURE__ */ new Map()
|
|
4727
|
+
cache: /* @__PURE__ */ new Map(),
|
|
4728
|
+
pending: /* @__PURE__ */ new Map()
|
|
4612
4729
|
};
|
|
4613
4730
|
}
|
|
4614
4731
|
//#endregion
|
package/package.json
CHANGED