@moku-labs/web 2.4.12 → 2.4.14
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 +33 -1
- package/dist/browser.mjs +119 -19
- package/dist/{convention-CLIDd9k-.cjs → convention-BO-VoFLO.cjs} +8 -0
- package/dist/{convention-Dp650o3y.mjs → convention-BtVf429P.mjs} +3 -1
- package/dist/index.cjs +119 -19
- package/dist/index.d.cts +33 -1
- package/dist/index.d.mts +33 -1
- package/dist/index.mjs +119 -19
- package/dist/{writer-CaoyORyZ.mjs → writer-Dwd02T9h.mjs} +30 -1
- package/dist/{writer-BEM-Pbgb.cjs → writer-ryba09yV.cjs} +30 -1
- package/package.json +1 -1
package/dist/browser.d.mts
CHANGED
|
@@ -1931,7 +1931,7 @@ declare const spaPlugin: import("@moku-labs/core").PluginInstance<"spa", SpaConf
|
|
|
1931
1931
|
};
|
|
1932
1932
|
}> & Record<never, never>;
|
|
1933
1933
|
declare namespace types_d_exports$2 {
|
|
1934
|
-
export { DataConfig, DataEntry, DataProvider, DataState, DataWriteSummary };
|
|
1934
|
+
export { DataConfig, DataEntry, DataPreload, DataProvider, DataState, DataWriteSummary };
|
|
1935
1935
|
}
|
|
1936
1936
|
/**
|
|
1937
1937
|
* @file data plugin — type definitions (Standard tier).
|
|
@@ -1951,6 +1951,12 @@ declare namespace types_d_exports$2 {
|
|
|
1951
1951
|
* inside `write()`, so composing `data` in a browser app keeps the bundle free of
|
|
1952
1952
|
* `node:*`.
|
|
1953
1953
|
*/
|
|
1954
|
+
/**
|
|
1955
|
+
* How the browser loads page data. `"none"` fetches one page file per navigation
|
|
1956
|
+
* (lazy). `"all"` also loads every page's data once, from one bundle file, after
|
|
1957
|
+
* the app starts and the main thread is idle.
|
|
1958
|
+
*/
|
|
1959
|
+
type DataPreload = "none" | "all";
|
|
1954
1960
|
/**
|
|
1955
1961
|
* Configuration for {@link dataPlugin}. All fields have defaults (see `./config`).
|
|
1956
1962
|
*
|
|
@@ -1971,6 +1977,13 @@ type DataConfig = {
|
|
|
1971
1977
|
* path); keep them consistent (`"/" + trim(outputDir) + "/"`). Default `"/_data/"`.
|
|
1972
1978
|
*/
|
|
1973
1979
|
baseUrl: string;
|
|
1980
|
+
/**
|
|
1981
|
+
* BOTH sides. `"all"`: the Node `write()` also writes one bundle file
|
|
1982
|
+
* `<outputDir>/_all.json` (`{ "<page path>": data }`), and the browser fetches
|
|
1983
|
+
* it once when idle and fills the `at()` cache. Set the same value in the Node
|
|
1984
|
+
* app and the browser app. Default `"none"`: no bundle, lazy per-page fetches.
|
|
1985
|
+
*/
|
|
1986
|
+
preload?: DataPreload;
|
|
1974
1987
|
};
|
|
1975
1988
|
/** One page's data to persist — `build` produces these from its route expansion. */
|
|
1976
1989
|
interface DataEntry {
|
|
@@ -1987,6 +2000,11 @@ interface DataWriteSummary {
|
|
|
1987
2000
|
bytes: number;
|
|
1988
2001
|
/** The written file paths, relative to the build `outDir`. */
|
|
1989
2002
|
files: string[];
|
|
2003
|
+
/** The preload bundle, present only when `preload` is `"all"`. Not counted in `fileCount`/`bytes`. */
|
|
2004
|
+
bundle?: {
|
|
2005
|
+
file: string;
|
|
2006
|
+
bytes: number;
|
|
2007
|
+
};
|
|
1990
2008
|
}
|
|
1991
2009
|
/**
|
|
1992
2010
|
* Internal data state. `lastWrite` records the most recent `write()` (Node);
|
|
@@ -1998,6 +2016,20 @@ interface DataState {
|
|
|
1998
2016
|
lastWrite: DataWriteSummary | null;
|
|
1999
2017
|
/** Per-path fetched data, cached after the first `at(path)` (browser). */
|
|
2000
2018
|
cache: Map<string, unknown>;
|
|
2019
|
+
/**
|
|
2020
|
+
* Per-path fetches still in flight (browser). A second `at(path)` awaits the
|
|
2021
|
+
* same promise instead of starting another request. The entry is removed when
|
|
2022
|
+
* the fetch settles, on success and on failure, so a failure is never cached.
|
|
2023
|
+
*/
|
|
2024
|
+
pending: Map<string, Promise<unknown | null>>;
|
|
2025
|
+
/**
|
|
2026
|
+
* The preload bundle fetch while it is in flight (browser, `preload: "all"`).
|
|
2027
|
+
* `at()` awaits it before fetching a single file. Never rejects. `null` when no
|
|
2028
|
+
* bundle fetch is running.
|
|
2029
|
+
*/
|
|
2030
|
+
bundle: Promise<void> | null;
|
|
2031
|
+
/** Cancels the scheduled idle callback or aborts the bundle fetch. `null` when nothing is scheduled. */
|
|
2032
|
+
cancelPreload: (() => void) | null;
|
|
2001
2033
|
}
|
|
2002
2034
|
/**
|
|
2003
2035
|
* Public API mounted at `app.data` — the agnostic data provider. `write()` is the
|
package/dist/browser.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as __exportAll } from "./chunk-D7D4PA-g.mjs";
|
|
2
|
-
import { n as relativeDataFile, t as
|
|
2
|
+
import { n as dataSuffix, r as relativeDataFile, t as BUNDLE_FILE } from "./convention-BtVf429P.mjs";
|
|
3
3
|
import { envPlugin as envPlugin$1, logPlugin as logPlugin$1 } from "@moku-labs/common";
|
|
4
4
|
import { createCoreConfig } from "@moku-labs/core";
|
|
5
5
|
import { browserEnv, browserEnv as browserEnv$1, envPlugin, logPlugin } from "@moku-labs/common/browser";
|
|
@@ -2367,9 +2367,33 @@ 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
|
-
* convention.
|
|
2396
|
+
* convention. The optional preload lives in `./preload` (started by `onStart`).
|
|
2373
2397
|
*
|
|
2374
2398
|
* @param ctx - The data plugin context.
|
|
2375
2399
|
* @returns The {@link DataProvider} mounted at `app.data`.
|
|
@@ -2384,6 +2408,9 @@ 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. While a preload
|
|
2412
|
+
* bundle is in flight, it waits for the bundle and fetches the single file
|
|
2413
|
+
* only if the bundle did not bring the path.
|
|
2387
2414
|
* Returns the raw JSON as `unknown`, which the route uses directly as `ctx.data`
|
|
2388
2415
|
* (no route `.parse()`); returns `null` if the fetch or JSON parse fails (so
|
|
2389
2416
|
* `spa` can fall back to HTML).
|
|
@@ -2397,13 +2424,15 @@ function dataApi(ctx) {
|
|
|
2397
2424
|
*/
|
|
2398
2425
|
async at(path) {
|
|
2399
2426
|
if (ctx.state.cache.has(path)) return ctx.state.cache.get(path);
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
ctx.state.cache.
|
|
2403
|
-
return data;
|
|
2404
|
-
} catch {
|
|
2405
|
-
return null;
|
|
2427
|
+
if (ctx.state.bundle) {
|
|
2428
|
+
await ctx.state.bundle;
|
|
2429
|
+
if (ctx.state.cache.has(path)) return ctx.state.cache.get(path);
|
|
2406
2430
|
}
|
|
2431
|
+
const inFlight = ctx.state.pending.get(path);
|
|
2432
|
+
if (inFlight) return inFlight;
|
|
2433
|
+
const request = fetchAndCache(ctx, path);
|
|
2434
|
+
ctx.state.pending.set(path, request);
|
|
2435
|
+
return request;
|
|
2407
2436
|
},
|
|
2408
2437
|
/**
|
|
2409
2438
|
* WRITE (Node) — persist one JSON file per entry, keyed by page path. Called by
|
|
@@ -2420,7 +2449,7 @@ function dataApi(ctx) {
|
|
|
2420
2449
|
* ```
|
|
2421
2450
|
*/
|
|
2422
2451
|
async write(entries, options) {
|
|
2423
|
-
const { writeData } = await import("./writer-
|
|
2452
|
+
const { writeData } = await import("./writer-Dwd02T9h.mjs");
|
|
2424
2453
|
return writeData(ctx, entries, options);
|
|
2425
2454
|
},
|
|
2426
2455
|
/**
|
|
@@ -2457,7 +2486,7 @@ function dataApi(ctx) {
|
|
|
2457
2486
|
* Typed default data config (R6: no inline `as`). `outputDir` is the WRITE path
|
|
2458
2487
|
* (filesystem, relative to the build `outDir`); `baseUrl` is the matching READ URL
|
|
2459
2488
|
* (site-root-relative) the browser fetches from — the defaults agree
|
|
2460
|
-
* (`"_data"` ↔ `"/_data/"`).
|
|
2489
|
+
* (`"_data"` ↔ `"/_data/"`). `preload` is `"none"`: lazy per-page fetches only.
|
|
2461
2490
|
*
|
|
2462
2491
|
* @example
|
|
2463
2492
|
* ```ts
|
|
@@ -2466,19 +2495,82 @@ function dataApi(ctx) {
|
|
|
2466
2495
|
*/
|
|
2467
2496
|
const defaultDataConfig = {
|
|
2468
2497
|
outputDir: "_data",
|
|
2469
|
-
baseUrl: "/_data/"
|
|
2498
|
+
baseUrl: "/_data/",
|
|
2499
|
+
preload: "none"
|
|
2470
2500
|
};
|
|
2471
2501
|
//#endregion
|
|
2502
|
+
//#region src/plugins/data/preload.ts
|
|
2503
|
+
/**
|
|
2504
|
+
* Fetches the bundle and caches every path not already cached. Never rejects:
|
|
2505
|
+
* a failed fetch leaves lazy `at()` working. Clears the preload slots when done.
|
|
2506
|
+
*
|
|
2507
|
+
* @param ctx - The data plugin context.
|
|
2508
|
+
* @param signal - Aborts the fetch on stop.
|
|
2509
|
+
* @returns Resolves when the fill is done or failed.
|
|
2510
|
+
* @example
|
|
2511
|
+
* ```ts
|
|
2512
|
+
* await fill(ctx, signal);
|
|
2513
|
+
* ```
|
|
2514
|
+
*/
|
|
2515
|
+
async function fill(ctx, signal) {
|
|
2516
|
+
try {
|
|
2517
|
+
const response = await fetch(`${ctx.config.baseUrl}${BUNDLE_FILE}`, { signal });
|
|
2518
|
+
const bundle = response.ok ? await response.json() : {};
|
|
2519
|
+
for (const [path, data] of Object.entries(bundle)) if (!signal.aborted && !ctx.state.cache.has(path)) ctx.state.cache.set(path, data);
|
|
2520
|
+
} catch {} finally {
|
|
2521
|
+
ctx.state.bundle = ctx.state.cancelPreload = null;
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
/**
|
|
2525
|
+
* From `onStart`: when idle, fetch `<baseUrl>_all.json` once and fill the cache.
|
|
2526
|
+
* No-op on Node, with `preload: "none"`, with save-data on, or if already scheduled.
|
|
2527
|
+
*
|
|
2528
|
+
* @param ctx - The data plugin context.
|
|
2529
|
+
* @example
|
|
2530
|
+
* ```ts
|
|
2531
|
+
* startPreload(ctx);
|
|
2532
|
+
* ```
|
|
2533
|
+
*/
|
|
2534
|
+
function startPreload(ctx) {
|
|
2535
|
+
const nav = globalThis.navigator;
|
|
2536
|
+
if (ctx.config.preload !== "all" || typeof document === "undefined" || ctx.state.cancelPreload !== null || nav?.connection?.saveData === true) return;
|
|
2537
|
+
const controller = new AbortController();
|
|
2538
|
+
const run = () => {
|
|
2539
|
+
ctx.state.bundle = fill(ctx, controller.signal);
|
|
2540
|
+
};
|
|
2541
|
+
const idle = typeof requestIdleCallback === "function";
|
|
2542
|
+
const handle = idle ? requestIdleCallback(run, { timeout: 2e3 }) : setTimeout(run, 0);
|
|
2543
|
+
ctx.state.cancelPreload = () => {
|
|
2544
|
+
if (idle) cancelIdleCallback(handle);
|
|
2545
|
+
else clearTimeout(handle);
|
|
2546
|
+
controller.abort();
|
|
2547
|
+
};
|
|
2548
|
+
}
|
|
2549
|
+
/**
|
|
2550
|
+
* From `onStop`: cancel the idle callback or abort the bundle fetch.
|
|
2551
|
+
*
|
|
2552
|
+
* @param ctx - The data plugin context.
|
|
2553
|
+
* @example
|
|
2554
|
+
* ```ts
|
|
2555
|
+
* stopPreload(ctx);
|
|
2556
|
+
* ```
|
|
2557
|
+
*/
|
|
2558
|
+
function stopPreload(ctx) {
|
|
2559
|
+
ctx.state.cancelPreload?.();
|
|
2560
|
+
ctx.state.cancelPreload = null;
|
|
2561
|
+
}
|
|
2562
|
+
//#endregion
|
|
2472
2563
|
//#region src/plugins/data/state.ts
|
|
2473
2564
|
/**
|
|
2474
2565
|
* Creates initial data state: a null `lastWrite` slot (populated by the Node
|
|
2475
|
-
* `write()` side)
|
|
2476
|
-
* side on first fetch)
|
|
2566
|
+
* `write()` side), an empty `cache` (populated lazily by the browser `at(path)`
|
|
2567
|
+
* side on first fetch), an empty `pending` map of in-flight fetches, and no
|
|
2568
|
+
* preload bundle in flight or scheduled.
|
|
2477
2569
|
*
|
|
2478
2570
|
* @param _ctx - Minimal context with global and config.
|
|
2479
2571
|
* @param _ctx.global - Global framework configuration.
|
|
2480
2572
|
* @param _ctx.config - Resolved plugin configuration.
|
|
2481
|
-
* @returns Fresh data state with no recorded write and an empty per-path cache.
|
|
2573
|
+
* @returns Fresh data state with no recorded write and an empty per-path cache and no pending fetch.
|
|
2482
2574
|
* @example
|
|
2483
2575
|
* ```ts
|
|
2484
2576
|
* const state = createDataState({ global: {}, config });
|
|
@@ -2487,7 +2579,10 @@ const defaultDataConfig = {
|
|
|
2487
2579
|
function createDataState(_ctx) {
|
|
2488
2580
|
return {
|
|
2489
2581
|
lastWrite: null,
|
|
2490
|
-
cache: /* @__PURE__ */ new Map()
|
|
2582
|
+
cache: /* @__PURE__ */ new Map(),
|
|
2583
|
+
pending: /* @__PURE__ */ new Map(),
|
|
2584
|
+
bundle: null,
|
|
2585
|
+
cancelPreload: null
|
|
2491
2586
|
};
|
|
2492
2587
|
}
|
|
2493
2588
|
//#endregion
|
|
@@ -2509,10 +2604,10 @@ function isInvalidBaseUrl(baseUrl) {
|
|
|
2509
2604
|
}
|
|
2510
2605
|
/**
|
|
2511
2606
|
* Validates the resolved data config: the browser `baseUrl` must be a non-empty,
|
|
2512
|
-
* site-root-relative URL path
|
|
2607
|
+
* site-root-relative URL path, and `preload` must be `"none"` or `"all"`.
|
|
2513
2608
|
*
|
|
2514
2609
|
* @param config - The resolved plugin configuration.
|
|
2515
|
-
* @throws {Error} If `baseUrl` is empty or not a rooted URL path.
|
|
2610
|
+
* @throws {Error} If `baseUrl` is empty or not a rooted URL path, or `preload` is unknown.
|
|
2516
2611
|
* @example
|
|
2517
2612
|
* ```ts
|
|
2518
2613
|
* validateDataConfig({ outputDir: "_data", baseUrl: "/_data/" });
|
|
@@ -2520,6 +2615,8 @@ function isInvalidBaseUrl(baseUrl) {
|
|
|
2520
2615
|
*/
|
|
2521
2616
|
function validateDataConfig(config) {
|
|
2522
2617
|
if (isInvalidBaseUrl(config.baseUrl)) throw new Error(`[web] data.baseUrl: must be a site-root-relative URL path starting with "/" (e.g. "/_data/").`);
|
|
2618
|
+
const { preload } = config;
|
|
2619
|
+
if (preload !== void 0 && preload !== "none" && preload !== "all") throw new Error(`[web] data.preload: must be "none" or "all".\n Set it to "all" in both apps, or remove it.`);
|
|
2523
2620
|
}
|
|
2524
2621
|
//#endregion
|
|
2525
2622
|
//#region src/plugins/data/index.ts
|
|
@@ -2536,7 +2633,8 @@ function validateDataConfig(config) {
|
|
|
2536
2633
|
* **No hard `depends`** — fully browser-composable; the `node:fs` writer is behind
|
|
2537
2634
|
* a lazy `import()` inside `write()`. Build ordering is a call-site contract: build
|
|
2538
2635
|
* writes data during its pages phase (after its Phase-0 clean), via `app.data.write`.
|
|
2539
|
-
*
|
|
2636
|
+
* `onStart` schedules the optional `preload: "all"` bundle fetch (browser, idle);
|
|
2637
|
+
* `onStop` cancels it.
|
|
2540
2638
|
* @see README.md
|
|
2541
2639
|
*/
|
|
2542
2640
|
/**
|
|
@@ -2562,7 +2660,9 @@ const dataPlugin = createPlugin$1("data", {
|
|
|
2562
2660
|
config: defaultDataConfig,
|
|
2563
2661
|
createState: createDataState,
|
|
2564
2662
|
onInit: (ctx) => validateDataConfig(ctx.config),
|
|
2565
|
-
api: dataApi
|
|
2663
|
+
api: dataApi,
|
|
2664
|
+
onStart: (ctx) => startPreload(ctx),
|
|
2665
|
+
onStop: (ctx) => stopPreload(ctx)
|
|
2566
2666
|
});
|
|
2567
2667
|
//#endregion
|
|
2568
2668
|
//#region src/plugins/spa/head.ts
|
|
@@ -70,7 +70,15 @@ function decodeSuffix(suffix) {
|
|
|
70
70
|
function relativeDataFile(outputDir, path) {
|
|
71
71
|
return `${outputDir.endsWith("/") ? outputDir.slice(0, -1) : outputDir}/${decodeSuffix(dataSuffix(path))}`;
|
|
72
72
|
}
|
|
73
|
+
/** Preload bundle file name. Page files end in `/index.json`, so it never collides. */
|
|
74
|
+
const BUNDLE_FILE = "_all.json";
|
|
73
75
|
//#endregion
|
|
76
|
+
Object.defineProperty(exports, "BUNDLE_FILE", {
|
|
77
|
+
enumerable: true,
|
|
78
|
+
get: function() {
|
|
79
|
+
return BUNDLE_FILE;
|
|
80
|
+
}
|
|
81
|
+
});
|
|
74
82
|
Object.defineProperty(exports, "dataSuffix", {
|
|
75
83
|
enumerable: true,
|
|
76
84
|
get: function() {
|
|
@@ -70,5 +70,7 @@ function decodeSuffix(suffix) {
|
|
|
70
70
|
function relativeDataFile(outputDir, path) {
|
|
71
71
|
return `${outputDir.endsWith("/") ? outputDir.slice(0, -1) : outputDir}/${decodeSuffix(dataSuffix(path))}`;
|
|
72
72
|
}
|
|
73
|
+
/** Preload bundle file name. Page files end in `/index.json`, so it never collides. */
|
|
74
|
+
const BUNDLE_FILE = "_all.json";
|
|
73
75
|
//#endregion
|
|
74
|
-
export {
|
|
76
|
+
export { dataSuffix as n, relativeDataFile as r, BUNDLE_FILE as t };
|
package/dist/index.cjs
CHANGED
|
@@ -30,7 +30,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
30
30
|
enumerable: true
|
|
31
31
|
}) : target, mod));
|
|
32
32
|
//#endregion
|
|
33
|
-
const require_convention = require("./convention-
|
|
33
|
+
const require_convention = require("./convention-BO-VoFLO.cjs");
|
|
34
34
|
const require_convention$1 = require("./convention-CsJw7CML.cjs");
|
|
35
35
|
let _moku_labs_common = require("@moku-labs/common");
|
|
36
36
|
let _moku_labs_core = require("@moku-labs/core");
|
|
@@ -4625,9 +4625,33 @@ async function loadJson(pathOrUrl) {
|
|
|
4625
4625
|
* the isomorphic `loadJson` (whose Node branch is itself lazy).
|
|
4626
4626
|
*/
|
|
4627
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
|
+
/**
|
|
4628
4652
|
* Builds the data provider — the agnostic bridge. `write()` is the Node persist
|
|
4629
4653
|
* side; `at()` is the browser read side; `urlFor`/`fileFor` are the pure
|
|
4630
|
-
* convention.
|
|
4654
|
+
* convention. The optional preload lives in `./preload` (started by `onStart`).
|
|
4631
4655
|
*
|
|
4632
4656
|
* @param ctx - The data plugin context.
|
|
4633
4657
|
* @returns The {@link DataProvider} mounted at `app.data`.
|
|
@@ -4642,6 +4666,9 @@ function dataApi(ctx) {
|
|
|
4642
4666
|
return {
|
|
4643
4667
|
/**
|
|
4644
4668
|
* READ (browser) — fetch (and cache) the persisted data for a page path.
|
|
4669
|
+
* Concurrent calls for one path share one in-flight request. While a preload
|
|
4670
|
+
* bundle is in flight, it waits for the bundle and fetches the single file
|
|
4671
|
+
* only if the bundle did not bring the path.
|
|
4645
4672
|
* Returns the raw JSON as `unknown`, which the route uses directly as `ctx.data`
|
|
4646
4673
|
* (no route `.parse()`); returns `null` if the fetch or JSON parse fails (so
|
|
4647
4674
|
* `spa` can fall back to HTML).
|
|
@@ -4655,13 +4682,15 @@ function dataApi(ctx) {
|
|
|
4655
4682
|
*/
|
|
4656
4683
|
async at(path) {
|
|
4657
4684
|
if (ctx.state.cache.has(path)) return ctx.state.cache.get(path);
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
ctx.state.cache.
|
|
4661
|
-
return data;
|
|
4662
|
-
} catch {
|
|
4663
|
-
return null;
|
|
4685
|
+
if (ctx.state.bundle) {
|
|
4686
|
+
await ctx.state.bundle;
|
|
4687
|
+
if (ctx.state.cache.has(path)) return ctx.state.cache.get(path);
|
|
4664
4688
|
}
|
|
4689
|
+
const inFlight = ctx.state.pending.get(path);
|
|
4690
|
+
if (inFlight) return inFlight;
|
|
4691
|
+
const request = fetchAndCache(ctx, path);
|
|
4692
|
+
ctx.state.pending.set(path, request);
|
|
4693
|
+
return request;
|
|
4665
4694
|
},
|
|
4666
4695
|
/**
|
|
4667
4696
|
* WRITE (Node) — persist one JSON file per entry, keyed by page path. Called by
|
|
@@ -4678,7 +4707,7 @@ function dataApi(ctx) {
|
|
|
4678
4707
|
* ```
|
|
4679
4708
|
*/
|
|
4680
4709
|
async write(entries, options) {
|
|
4681
|
-
const { writeData } = await Promise.resolve().then(() => require("./writer-
|
|
4710
|
+
const { writeData } = await Promise.resolve().then(() => require("./writer-ryba09yV.cjs"));
|
|
4682
4711
|
return writeData(ctx, entries, options);
|
|
4683
4712
|
},
|
|
4684
4713
|
/**
|
|
@@ -4715,7 +4744,7 @@ function dataApi(ctx) {
|
|
|
4715
4744
|
* Typed default data config (R6: no inline `as`). `outputDir` is the WRITE path
|
|
4716
4745
|
* (filesystem, relative to the build `outDir`); `baseUrl` is the matching READ URL
|
|
4717
4746
|
* (site-root-relative) the browser fetches from — the defaults agree
|
|
4718
|
-
* (`"_data"` ↔ `"/_data/"`).
|
|
4747
|
+
* (`"_data"` ↔ `"/_data/"`). `preload` is `"none"`: lazy per-page fetches only.
|
|
4719
4748
|
*
|
|
4720
4749
|
* @example
|
|
4721
4750
|
* ```ts
|
|
@@ -4724,19 +4753,82 @@ function dataApi(ctx) {
|
|
|
4724
4753
|
*/
|
|
4725
4754
|
const defaultDataConfig = {
|
|
4726
4755
|
outputDir: "_data",
|
|
4727
|
-
baseUrl: "/_data/"
|
|
4756
|
+
baseUrl: "/_data/",
|
|
4757
|
+
preload: "none"
|
|
4728
4758
|
};
|
|
4729
4759
|
//#endregion
|
|
4760
|
+
//#region src/plugins/data/preload.ts
|
|
4761
|
+
/**
|
|
4762
|
+
* Fetches the bundle and caches every path not already cached. Never rejects:
|
|
4763
|
+
* a failed fetch leaves lazy `at()` working. Clears the preload slots when done.
|
|
4764
|
+
*
|
|
4765
|
+
* @param ctx - The data plugin context.
|
|
4766
|
+
* @param signal - Aborts the fetch on stop.
|
|
4767
|
+
* @returns Resolves when the fill is done or failed.
|
|
4768
|
+
* @example
|
|
4769
|
+
* ```ts
|
|
4770
|
+
* await fill(ctx, signal);
|
|
4771
|
+
* ```
|
|
4772
|
+
*/
|
|
4773
|
+
async function fill(ctx, signal) {
|
|
4774
|
+
try {
|
|
4775
|
+
const response = await fetch(`${ctx.config.baseUrl}${require_convention.BUNDLE_FILE}`, { signal });
|
|
4776
|
+
const bundle = response.ok ? await response.json() : {};
|
|
4777
|
+
for (const [path, data] of Object.entries(bundle)) if (!signal.aborted && !ctx.state.cache.has(path)) ctx.state.cache.set(path, data);
|
|
4778
|
+
} catch {} finally {
|
|
4779
|
+
ctx.state.bundle = ctx.state.cancelPreload = null;
|
|
4780
|
+
}
|
|
4781
|
+
}
|
|
4782
|
+
/**
|
|
4783
|
+
* From `onStart`: when idle, fetch `<baseUrl>_all.json` once and fill the cache.
|
|
4784
|
+
* No-op on Node, with `preload: "none"`, with save-data on, or if already scheduled.
|
|
4785
|
+
*
|
|
4786
|
+
* @param ctx - The data plugin context.
|
|
4787
|
+
* @example
|
|
4788
|
+
* ```ts
|
|
4789
|
+
* startPreload(ctx);
|
|
4790
|
+
* ```
|
|
4791
|
+
*/
|
|
4792
|
+
function startPreload(ctx) {
|
|
4793
|
+
const nav = globalThis.navigator;
|
|
4794
|
+
if (ctx.config.preload !== "all" || typeof document === "undefined" || ctx.state.cancelPreload !== null || nav?.connection?.saveData === true) return;
|
|
4795
|
+
const controller = new AbortController();
|
|
4796
|
+
const run = () => {
|
|
4797
|
+
ctx.state.bundle = fill(ctx, controller.signal);
|
|
4798
|
+
};
|
|
4799
|
+
const idle = typeof requestIdleCallback === "function";
|
|
4800
|
+
const handle = idle ? requestIdleCallback(run, { timeout: 2e3 }) : setTimeout(run, 0);
|
|
4801
|
+
ctx.state.cancelPreload = () => {
|
|
4802
|
+
if (idle) cancelIdleCallback(handle);
|
|
4803
|
+
else clearTimeout(handle);
|
|
4804
|
+
controller.abort();
|
|
4805
|
+
};
|
|
4806
|
+
}
|
|
4807
|
+
/**
|
|
4808
|
+
* From `onStop`: cancel the idle callback or abort the bundle fetch.
|
|
4809
|
+
*
|
|
4810
|
+
* @param ctx - The data plugin context.
|
|
4811
|
+
* @example
|
|
4812
|
+
* ```ts
|
|
4813
|
+
* stopPreload(ctx);
|
|
4814
|
+
* ```
|
|
4815
|
+
*/
|
|
4816
|
+
function stopPreload(ctx) {
|
|
4817
|
+
ctx.state.cancelPreload?.();
|
|
4818
|
+
ctx.state.cancelPreload = null;
|
|
4819
|
+
}
|
|
4820
|
+
//#endregion
|
|
4730
4821
|
//#region src/plugins/data/state.ts
|
|
4731
4822
|
/**
|
|
4732
4823
|
* Creates initial data state: a null `lastWrite` slot (populated by the Node
|
|
4733
|
-
* `write()` side)
|
|
4734
|
-
* side on first fetch)
|
|
4824
|
+
* `write()` side), an empty `cache` (populated lazily by the browser `at(path)`
|
|
4825
|
+
* side on first fetch), an empty `pending` map of in-flight fetches, and no
|
|
4826
|
+
* preload bundle in flight or scheduled.
|
|
4735
4827
|
*
|
|
4736
4828
|
* @param _ctx - Minimal context with global and config.
|
|
4737
4829
|
* @param _ctx.global - Global framework configuration.
|
|
4738
4830
|
* @param _ctx.config - Resolved plugin configuration.
|
|
4739
|
-
* @returns Fresh data state with no recorded write and an empty per-path cache.
|
|
4831
|
+
* @returns Fresh data state with no recorded write and an empty per-path cache and no pending fetch.
|
|
4740
4832
|
* @example
|
|
4741
4833
|
* ```ts
|
|
4742
4834
|
* const state = createDataState({ global: {}, config });
|
|
@@ -4745,7 +4837,10 @@ const defaultDataConfig = {
|
|
|
4745
4837
|
function createDataState(_ctx) {
|
|
4746
4838
|
return {
|
|
4747
4839
|
lastWrite: null,
|
|
4748
|
-
cache: /* @__PURE__ */ new Map()
|
|
4840
|
+
cache: /* @__PURE__ */ new Map(),
|
|
4841
|
+
pending: /* @__PURE__ */ new Map(),
|
|
4842
|
+
bundle: null,
|
|
4843
|
+
cancelPreload: null
|
|
4749
4844
|
};
|
|
4750
4845
|
}
|
|
4751
4846
|
//#endregion
|
|
@@ -4767,10 +4862,10 @@ function isInvalidBaseUrl$1(baseUrl) {
|
|
|
4767
4862
|
}
|
|
4768
4863
|
/**
|
|
4769
4864
|
* Validates the resolved data config: the browser `baseUrl` must be a non-empty,
|
|
4770
|
-
* site-root-relative URL path
|
|
4865
|
+
* site-root-relative URL path, and `preload` must be `"none"` or `"all"`.
|
|
4771
4866
|
*
|
|
4772
4867
|
* @param config - The resolved plugin configuration.
|
|
4773
|
-
* @throws {Error} If `baseUrl` is empty or not a rooted URL path.
|
|
4868
|
+
* @throws {Error} If `baseUrl` is empty or not a rooted URL path, or `preload` is unknown.
|
|
4774
4869
|
* @example
|
|
4775
4870
|
* ```ts
|
|
4776
4871
|
* validateDataConfig({ outputDir: "_data", baseUrl: "/_data/" });
|
|
@@ -4778,6 +4873,8 @@ function isInvalidBaseUrl$1(baseUrl) {
|
|
|
4778
4873
|
*/
|
|
4779
4874
|
function validateDataConfig(config) {
|
|
4780
4875
|
if (isInvalidBaseUrl$1(config.baseUrl)) throw new Error(`[web] data.baseUrl: must be a site-root-relative URL path starting with "/" (e.g. "/_data/").`);
|
|
4876
|
+
const { preload } = config;
|
|
4877
|
+
if (preload !== void 0 && preload !== "none" && preload !== "all") throw new Error(`[web] data.preload: must be "none" or "all".\n Set it to "all" in both apps, or remove it.`);
|
|
4781
4878
|
}
|
|
4782
4879
|
//#endregion
|
|
4783
4880
|
//#region src/plugins/data/index.ts
|
|
@@ -4794,7 +4891,8 @@ function validateDataConfig(config) {
|
|
|
4794
4891
|
* **No hard `depends`** — fully browser-composable; the `node:fs` writer is behind
|
|
4795
4892
|
* a lazy `import()` inside `write()`. Build ordering is a call-site contract: build
|
|
4796
4893
|
* writes data during its pages phase (after its Phase-0 clean), via `app.data.write`.
|
|
4797
|
-
*
|
|
4894
|
+
* `onStart` schedules the optional `preload: "all"` bundle fetch (browser, idle);
|
|
4895
|
+
* `onStop` cancels it.
|
|
4798
4896
|
* @see README.md
|
|
4799
4897
|
*/
|
|
4800
4898
|
/**
|
|
@@ -4820,7 +4918,9 @@ const dataPlugin = createPlugin$1("data", {
|
|
|
4820
4918
|
config: defaultDataConfig,
|
|
4821
4919
|
createState: createDataState,
|
|
4822
4920
|
onInit: (ctx) => validateDataConfig(ctx.config),
|
|
4823
|
-
api: dataApi
|
|
4921
|
+
api: dataApi,
|
|
4922
|
+
onStart: (ctx) => startPreload(ctx),
|
|
4923
|
+
onStop: (ctx) => stopPreload(ctx)
|
|
4824
4924
|
});
|
|
4825
4925
|
//#endregion
|
|
4826
4926
|
//#region src/plugins/build/phases/pages.tsx
|
package/dist/index.d.cts
CHANGED
|
@@ -3400,7 +3400,7 @@ declare const contentPlugin: import("@moku-labs/core").PluginInstance<"content",
|
|
|
3400
3400
|
};
|
|
3401
3401
|
}> & Record<never, never>;
|
|
3402
3402
|
declare namespace types_d_exports$4 {
|
|
3403
|
-
export { DataConfig, DataEntry, DataProvider, DataState, DataWriteSummary };
|
|
3403
|
+
export { DataConfig, DataEntry, DataPreload, DataProvider, DataState, DataWriteSummary };
|
|
3404
3404
|
}
|
|
3405
3405
|
/**
|
|
3406
3406
|
* @file data plugin — type definitions (Standard tier).
|
|
@@ -3420,6 +3420,12 @@ declare namespace types_d_exports$4 {
|
|
|
3420
3420
|
* inside `write()`, so composing `data` in a browser app keeps the bundle free of
|
|
3421
3421
|
* `node:*`.
|
|
3422
3422
|
*/
|
|
3423
|
+
/**
|
|
3424
|
+
* How the browser loads page data. `"none"` fetches one page file per navigation
|
|
3425
|
+
* (lazy). `"all"` also loads every page's data once, from one bundle file, after
|
|
3426
|
+
* the app starts and the main thread is idle.
|
|
3427
|
+
*/
|
|
3428
|
+
type DataPreload = "none" | "all";
|
|
3423
3429
|
/**
|
|
3424
3430
|
* Configuration for {@link dataPlugin}. All fields have defaults (see `./config`).
|
|
3425
3431
|
*
|
|
@@ -3440,6 +3446,13 @@ type DataConfig = {
|
|
|
3440
3446
|
* path); keep them consistent (`"/" + trim(outputDir) + "/"`). Default `"/_data/"`.
|
|
3441
3447
|
*/
|
|
3442
3448
|
baseUrl: string;
|
|
3449
|
+
/**
|
|
3450
|
+
* BOTH sides. `"all"`: the Node `write()` also writes one bundle file
|
|
3451
|
+
* `<outputDir>/_all.json` (`{ "<page path>": data }`), and the browser fetches
|
|
3452
|
+
* it once when idle and fills the `at()` cache. Set the same value in the Node
|
|
3453
|
+
* app and the browser app. Default `"none"`: no bundle, lazy per-page fetches.
|
|
3454
|
+
*/
|
|
3455
|
+
preload?: DataPreload;
|
|
3443
3456
|
};
|
|
3444
3457
|
/** One page's data to persist — `build` produces these from its route expansion. */
|
|
3445
3458
|
interface DataEntry {
|
|
@@ -3456,6 +3469,11 @@ interface DataWriteSummary {
|
|
|
3456
3469
|
bytes: number;
|
|
3457
3470
|
/** The written file paths, relative to the build `outDir`. */
|
|
3458
3471
|
files: string[];
|
|
3472
|
+
/** The preload bundle, present only when `preload` is `"all"`. Not counted in `fileCount`/`bytes`. */
|
|
3473
|
+
bundle?: {
|
|
3474
|
+
file: string;
|
|
3475
|
+
bytes: number;
|
|
3476
|
+
};
|
|
3459
3477
|
}
|
|
3460
3478
|
/**
|
|
3461
3479
|
* Internal data state. `lastWrite` records the most recent `write()` (Node);
|
|
@@ -3467,6 +3485,20 @@ interface DataState {
|
|
|
3467
3485
|
lastWrite: DataWriteSummary | null;
|
|
3468
3486
|
/** Per-path fetched data, cached after the first `at(path)` (browser). */
|
|
3469
3487
|
cache: Map<string, unknown>;
|
|
3488
|
+
/**
|
|
3489
|
+
* Per-path fetches still in flight (browser). A second `at(path)` awaits the
|
|
3490
|
+
* same promise instead of starting another request. The entry is removed when
|
|
3491
|
+
* the fetch settles, on success and on failure, so a failure is never cached.
|
|
3492
|
+
*/
|
|
3493
|
+
pending: Map<string, Promise<unknown | null>>;
|
|
3494
|
+
/**
|
|
3495
|
+
* The preload bundle fetch while it is in flight (browser, `preload: "all"`).
|
|
3496
|
+
* `at()` awaits it before fetching a single file. Never rejects. `null` when no
|
|
3497
|
+
* bundle fetch is running.
|
|
3498
|
+
*/
|
|
3499
|
+
bundle: Promise<void> | null;
|
|
3500
|
+
/** Cancels the scheduled idle callback or aborts the bundle fetch. `null` when nothing is scheduled. */
|
|
3501
|
+
cancelPreload: (() => void) | null;
|
|
3470
3502
|
}
|
|
3471
3503
|
/**
|
|
3472
3504
|
* Public API mounted at `app.data` — the agnostic data provider. `write()` is the
|
package/dist/index.d.mts
CHANGED
|
@@ -3398,7 +3398,7 @@ declare const contentPlugin: import("@moku-labs/core").PluginInstance<"content",
|
|
|
3398
3398
|
};
|
|
3399
3399
|
}> & Record<never, never>;
|
|
3400
3400
|
declare namespace types_d_exports$4 {
|
|
3401
|
-
export { DataConfig, DataEntry, DataProvider, DataState, DataWriteSummary };
|
|
3401
|
+
export { DataConfig, DataEntry, DataPreload, DataProvider, DataState, DataWriteSummary };
|
|
3402
3402
|
}
|
|
3403
3403
|
/**
|
|
3404
3404
|
* @file data plugin — type definitions (Standard tier).
|
|
@@ -3418,6 +3418,12 @@ declare namespace types_d_exports$4 {
|
|
|
3418
3418
|
* inside `write()`, so composing `data` in a browser app keeps the bundle free of
|
|
3419
3419
|
* `node:*`.
|
|
3420
3420
|
*/
|
|
3421
|
+
/**
|
|
3422
|
+
* How the browser loads page data. `"none"` fetches one page file per navigation
|
|
3423
|
+
* (lazy). `"all"` also loads every page's data once, from one bundle file, after
|
|
3424
|
+
* the app starts and the main thread is idle.
|
|
3425
|
+
*/
|
|
3426
|
+
type DataPreload = "none" | "all";
|
|
3421
3427
|
/**
|
|
3422
3428
|
* Configuration for {@link dataPlugin}. All fields have defaults (see `./config`).
|
|
3423
3429
|
*
|
|
@@ -3438,6 +3444,13 @@ type DataConfig = {
|
|
|
3438
3444
|
* path); keep them consistent (`"/" + trim(outputDir) + "/"`). Default `"/_data/"`.
|
|
3439
3445
|
*/
|
|
3440
3446
|
baseUrl: string;
|
|
3447
|
+
/**
|
|
3448
|
+
* BOTH sides. `"all"`: the Node `write()` also writes one bundle file
|
|
3449
|
+
* `<outputDir>/_all.json` (`{ "<page path>": data }`), and the browser fetches
|
|
3450
|
+
* it once when idle and fills the `at()` cache. Set the same value in the Node
|
|
3451
|
+
* app and the browser app. Default `"none"`: no bundle, lazy per-page fetches.
|
|
3452
|
+
*/
|
|
3453
|
+
preload?: DataPreload;
|
|
3441
3454
|
};
|
|
3442
3455
|
/** One page's data to persist — `build` produces these from its route expansion. */
|
|
3443
3456
|
interface DataEntry {
|
|
@@ -3454,6 +3467,11 @@ interface DataWriteSummary {
|
|
|
3454
3467
|
bytes: number;
|
|
3455
3468
|
/** The written file paths, relative to the build `outDir`. */
|
|
3456
3469
|
files: string[];
|
|
3470
|
+
/** The preload bundle, present only when `preload` is `"all"`. Not counted in `fileCount`/`bytes`. */
|
|
3471
|
+
bundle?: {
|
|
3472
|
+
file: string;
|
|
3473
|
+
bytes: number;
|
|
3474
|
+
};
|
|
3457
3475
|
}
|
|
3458
3476
|
/**
|
|
3459
3477
|
* Internal data state. `lastWrite` records the most recent `write()` (Node);
|
|
@@ -3465,6 +3483,20 @@ interface DataState {
|
|
|
3465
3483
|
lastWrite: DataWriteSummary | null;
|
|
3466
3484
|
/** Per-path fetched data, cached after the first `at(path)` (browser). */
|
|
3467
3485
|
cache: Map<string, unknown>;
|
|
3486
|
+
/**
|
|
3487
|
+
* Per-path fetches still in flight (browser). A second `at(path)` awaits the
|
|
3488
|
+
* same promise instead of starting another request. The entry is removed when
|
|
3489
|
+
* the fetch settles, on success and on failure, so a failure is never cached.
|
|
3490
|
+
*/
|
|
3491
|
+
pending: Map<string, Promise<unknown | null>>;
|
|
3492
|
+
/**
|
|
3493
|
+
* The preload bundle fetch while it is in flight (browser, `preload: "all"`).
|
|
3494
|
+
* `at()` awaits it before fetching a single file. Never rejects. `null` when no
|
|
3495
|
+
* bundle fetch is running.
|
|
3496
|
+
*/
|
|
3497
|
+
bundle: Promise<void> | null;
|
|
3498
|
+
/** Cancels the scheduled idle callback or aborts the bundle fetch. `null` when nothing is scheduled. */
|
|
3499
|
+
cancelPreload: (() => void) | null;
|
|
3468
3500
|
}
|
|
3469
3501
|
/**
|
|
3470
3502
|
* Public API mounted at `app.data` — the agnostic data provider. `write()` is the
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as __exportAll } from "./chunk-D7D4PA-g.mjs";
|
|
2
|
-
import { n as relativeDataFile, t as
|
|
2
|
+
import { n as dataSuffix, r as relativeDataFile, t as BUNDLE_FILE } from "./convention-BtVf429P.mjs";
|
|
3
3
|
import { n as relativeShardFile, t as collectionUrl } from "./convention-Bs8SrQS9.mjs";
|
|
4
4
|
import { browserEnv, cloudflareBindings, dotenv, envPlugin, envPlugin as envPlugin$1, logPlugin, logPlugin as logPlugin$1, processEnv } from "@moku-labs/common";
|
|
5
5
|
import { createCoreConfig } from "@moku-labs/core";
|
|
@@ -4581,9 +4581,33 @@ async function loadJson(pathOrUrl) {
|
|
|
4581
4581
|
* the isomorphic `loadJson` (whose Node branch is itself lazy).
|
|
4582
4582
|
*/
|
|
4583
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
|
+
/**
|
|
4584
4608
|
* Builds the data provider — the agnostic bridge. `write()` is the Node persist
|
|
4585
4609
|
* side; `at()` is the browser read side; `urlFor`/`fileFor` are the pure
|
|
4586
|
-
* convention.
|
|
4610
|
+
* convention. The optional preload lives in `./preload` (started by `onStart`).
|
|
4587
4611
|
*
|
|
4588
4612
|
* @param ctx - The data plugin context.
|
|
4589
4613
|
* @returns The {@link DataProvider} mounted at `app.data`.
|
|
@@ -4598,6 +4622,9 @@ function dataApi(ctx) {
|
|
|
4598
4622
|
return {
|
|
4599
4623
|
/**
|
|
4600
4624
|
* READ (browser) — fetch (and cache) the persisted data for a page path.
|
|
4625
|
+
* Concurrent calls for one path share one in-flight request. While a preload
|
|
4626
|
+
* bundle is in flight, it waits for the bundle and fetches the single file
|
|
4627
|
+
* only if the bundle did not bring the path.
|
|
4601
4628
|
* Returns the raw JSON as `unknown`, which the route uses directly as `ctx.data`
|
|
4602
4629
|
* (no route `.parse()`); returns `null` if the fetch or JSON parse fails (so
|
|
4603
4630
|
* `spa` can fall back to HTML).
|
|
@@ -4611,13 +4638,15 @@ function dataApi(ctx) {
|
|
|
4611
4638
|
*/
|
|
4612
4639
|
async at(path) {
|
|
4613
4640
|
if (ctx.state.cache.has(path)) return ctx.state.cache.get(path);
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
ctx.state.cache.
|
|
4617
|
-
return data;
|
|
4618
|
-
} catch {
|
|
4619
|
-
return null;
|
|
4641
|
+
if (ctx.state.bundle) {
|
|
4642
|
+
await ctx.state.bundle;
|
|
4643
|
+
if (ctx.state.cache.has(path)) return ctx.state.cache.get(path);
|
|
4620
4644
|
}
|
|
4645
|
+
const inFlight = ctx.state.pending.get(path);
|
|
4646
|
+
if (inFlight) return inFlight;
|
|
4647
|
+
const request = fetchAndCache(ctx, path);
|
|
4648
|
+
ctx.state.pending.set(path, request);
|
|
4649
|
+
return request;
|
|
4621
4650
|
},
|
|
4622
4651
|
/**
|
|
4623
4652
|
* WRITE (Node) — persist one JSON file per entry, keyed by page path. Called by
|
|
@@ -4634,7 +4663,7 @@ function dataApi(ctx) {
|
|
|
4634
4663
|
* ```
|
|
4635
4664
|
*/
|
|
4636
4665
|
async write(entries, options) {
|
|
4637
|
-
const { writeData } = await import("./writer-
|
|
4666
|
+
const { writeData } = await import("./writer-Dwd02T9h.mjs");
|
|
4638
4667
|
return writeData(ctx, entries, options);
|
|
4639
4668
|
},
|
|
4640
4669
|
/**
|
|
@@ -4671,7 +4700,7 @@ function dataApi(ctx) {
|
|
|
4671
4700
|
* Typed default data config (R6: no inline `as`). `outputDir` is the WRITE path
|
|
4672
4701
|
* (filesystem, relative to the build `outDir`); `baseUrl` is the matching READ URL
|
|
4673
4702
|
* (site-root-relative) the browser fetches from — the defaults agree
|
|
4674
|
-
* (`"_data"` ↔ `"/_data/"`).
|
|
4703
|
+
* (`"_data"` ↔ `"/_data/"`). `preload` is `"none"`: lazy per-page fetches only.
|
|
4675
4704
|
*
|
|
4676
4705
|
* @example
|
|
4677
4706
|
* ```ts
|
|
@@ -4680,19 +4709,82 @@ function dataApi(ctx) {
|
|
|
4680
4709
|
*/
|
|
4681
4710
|
const defaultDataConfig = {
|
|
4682
4711
|
outputDir: "_data",
|
|
4683
|
-
baseUrl: "/_data/"
|
|
4712
|
+
baseUrl: "/_data/",
|
|
4713
|
+
preload: "none"
|
|
4684
4714
|
};
|
|
4685
4715
|
//#endregion
|
|
4716
|
+
//#region src/plugins/data/preload.ts
|
|
4717
|
+
/**
|
|
4718
|
+
* Fetches the bundle and caches every path not already cached. Never rejects:
|
|
4719
|
+
* a failed fetch leaves lazy `at()` working. Clears the preload slots when done.
|
|
4720
|
+
*
|
|
4721
|
+
* @param ctx - The data plugin context.
|
|
4722
|
+
* @param signal - Aborts the fetch on stop.
|
|
4723
|
+
* @returns Resolves when the fill is done or failed.
|
|
4724
|
+
* @example
|
|
4725
|
+
* ```ts
|
|
4726
|
+
* await fill(ctx, signal);
|
|
4727
|
+
* ```
|
|
4728
|
+
*/
|
|
4729
|
+
async function fill(ctx, signal) {
|
|
4730
|
+
try {
|
|
4731
|
+
const response = await fetch(`${ctx.config.baseUrl}${BUNDLE_FILE}`, { signal });
|
|
4732
|
+
const bundle = response.ok ? await response.json() : {};
|
|
4733
|
+
for (const [path, data] of Object.entries(bundle)) if (!signal.aborted && !ctx.state.cache.has(path)) ctx.state.cache.set(path, data);
|
|
4734
|
+
} catch {} finally {
|
|
4735
|
+
ctx.state.bundle = ctx.state.cancelPreload = null;
|
|
4736
|
+
}
|
|
4737
|
+
}
|
|
4738
|
+
/**
|
|
4739
|
+
* From `onStart`: when idle, fetch `<baseUrl>_all.json` once and fill the cache.
|
|
4740
|
+
* No-op on Node, with `preload: "none"`, with save-data on, or if already scheduled.
|
|
4741
|
+
*
|
|
4742
|
+
* @param ctx - The data plugin context.
|
|
4743
|
+
* @example
|
|
4744
|
+
* ```ts
|
|
4745
|
+
* startPreload(ctx);
|
|
4746
|
+
* ```
|
|
4747
|
+
*/
|
|
4748
|
+
function startPreload(ctx) {
|
|
4749
|
+
const nav = globalThis.navigator;
|
|
4750
|
+
if (ctx.config.preload !== "all" || typeof document === "undefined" || ctx.state.cancelPreload !== null || nav?.connection?.saveData === true) return;
|
|
4751
|
+
const controller = new AbortController();
|
|
4752
|
+
const run = () => {
|
|
4753
|
+
ctx.state.bundle = fill(ctx, controller.signal);
|
|
4754
|
+
};
|
|
4755
|
+
const idle = typeof requestIdleCallback === "function";
|
|
4756
|
+
const handle = idle ? requestIdleCallback(run, { timeout: 2e3 }) : setTimeout(run, 0);
|
|
4757
|
+
ctx.state.cancelPreload = () => {
|
|
4758
|
+
if (idle) cancelIdleCallback(handle);
|
|
4759
|
+
else clearTimeout(handle);
|
|
4760
|
+
controller.abort();
|
|
4761
|
+
};
|
|
4762
|
+
}
|
|
4763
|
+
/**
|
|
4764
|
+
* From `onStop`: cancel the idle callback or abort the bundle fetch.
|
|
4765
|
+
*
|
|
4766
|
+
* @param ctx - The data plugin context.
|
|
4767
|
+
* @example
|
|
4768
|
+
* ```ts
|
|
4769
|
+
* stopPreload(ctx);
|
|
4770
|
+
* ```
|
|
4771
|
+
*/
|
|
4772
|
+
function stopPreload(ctx) {
|
|
4773
|
+
ctx.state.cancelPreload?.();
|
|
4774
|
+
ctx.state.cancelPreload = null;
|
|
4775
|
+
}
|
|
4776
|
+
//#endregion
|
|
4686
4777
|
//#region src/plugins/data/state.ts
|
|
4687
4778
|
/**
|
|
4688
4779
|
* Creates initial data state: a null `lastWrite` slot (populated by the Node
|
|
4689
|
-
* `write()` side)
|
|
4690
|
-
* side on first fetch)
|
|
4780
|
+
* `write()` side), an empty `cache` (populated lazily by the browser `at(path)`
|
|
4781
|
+
* side on first fetch), an empty `pending` map of in-flight fetches, and no
|
|
4782
|
+
* preload bundle in flight or scheduled.
|
|
4691
4783
|
*
|
|
4692
4784
|
* @param _ctx - Minimal context with global and config.
|
|
4693
4785
|
* @param _ctx.global - Global framework configuration.
|
|
4694
4786
|
* @param _ctx.config - Resolved plugin configuration.
|
|
4695
|
-
* @returns Fresh data state with no recorded write and an empty per-path cache.
|
|
4787
|
+
* @returns Fresh data state with no recorded write and an empty per-path cache and no pending fetch.
|
|
4696
4788
|
* @example
|
|
4697
4789
|
* ```ts
|
|
4698
4790
|
* const state = createDataState({ global: {}, config });
|
|
@@ -4701,7 +4793,10 @@ const defaultDataConfig = {
|
|
|
4701
4793
|
function createDataState(_ctx) {
|
|
4702
4794
|
return {
|
|
4703
4795
|
lastWrite: null,
|
|
4704
|
-
cache: /* @__PURE__ */ new Map()
|
|
4796
|
+
cache: /* @__PURE__ */ new Map(),
|
|
4797
|
+
pending: /* @__PURE__ */ new Map(),
|
|
4798
|
+
bundle: null,
|
|
4799
|
+
cancelPreload: null
|
|
4705
4800
|
};
|
|
4706
4801
|
}
|
|
4707
4802
|
//#endregion
|
|
@@ -4723,10 +4818,10 @@ function isInvalidBaseUrl$1(baseUrl) {
|
|
|
4723
4818
|
}
|
|
4724
4819
|
/**
|
|
4725
4820
|
* Validates the resolved data config: the browser `baseUrl` must be a non-empty,
|
|
4726
|
-
* site-root-relative URL path
|
|
4821
|
+
* site-root-relative URL path, and `preload` must be `"none"` or `"all"`.
|
|
4727
4822
|
*
|
|
4728
4823
|
* @param config - The resolved plugin configuration.
|
|
4729
|
-
* @throws {Error} If `baseUrl` is empty or not a rooted URL path.
|
|
4824
|
+
* @throws {Error} If `baseUrl` is empty or not a rooted URL path, or `preload` is unknown.
|
|
4730
4825
|
* @example
|
|
4731
4826
|
* ```ts
|
|
4732
4827
|
* validateDataConfig({ outputDir: "_data", baseUrl: "/_data/" });
|
|
@@ -4734,6 +4829,8 @@ function isInvalidBaseUrl$1(baseUrl) {
|
|
|
4734
4829
|
*/
|
|
4735
4830
|
function validateDataConfig(config) {
|
|
4736
4831
|
if (isInvalidBaseUrl$1(config.baseUrl)) throw new Error(`[web] data.baseUrl: must be a site-root-relative URL path starting with "/" (e.g. "/_data/").`);
|
|
4832
|
+
const { preload } = config;
|
|
4833
|
+
if (preload !== void 0 && preload !== "none" && preload !== "all") throw new Error(`[web] data.preload: must be "none" or "all".\n Set it to "all" in both apps, or remove it.`);
|
|
4737
4834
|
}
|
|
4738
4835
|
//#endregion
|
|
4739
4836
|
//#region src/plugins/data/index.ts
|
|
@@ -4750,7 +4847,8 @@ function validateDataConfig(config) {
|
|
|
4750
4847
|
* **No hard `depends`** — fully browser-composable; the `node:fs` writer is behind
|
|
4751
4848
|
* a lazy `import()` inside `write()`. Build ordering is a call-site contract: build
|
|
4752
4849
|
* writes data during its pages phase (after its Phase-0 clean), via `app.data.write`.
|
|
4753
|
-
*
|
|
4850
|
+
* `onStart` schedules the optional `preload: "all"` bundle fetch (browser, idle);
|
|
4851
|
+
* `onStop` cancels it.
|
|
4754
4852
|
* @see README.md
|
|
4755
4853
|
*/
|
|
4756
4854
|
/**
|
|
@@ -4776,7 +4874,9 @@ const dataPlugin = createPlugin$1("data", {
|
|
|
4776
4874
|
config: defaultDataConfig,
|
|
4777
4875
|
createState: createDataState,
|
|
4778
4876
|
onInit: (ctx) => validateDataConfig(ctx.config),
|
|
4779
|
-
api: dataApi
|
|
4877
|
+
api: dataApi,
|
|
4878
|
+
onStart: (ctx) => startPreload(ctx),
|
|
4879
|
+
onStop: (ctx) => stopPreload(ctx)
|
|
4780
4880
|
});
|
|
4781
4881
|
//#endregion
|
|
4782
4882
|
//#region src/plugins/build/phases/pages.tsx
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { r as relativeDataFile, t as BUNDLE_FILE } from "./convention-BtVf429P.mjs";
|
|
2
2
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import pLimit from "p-limit";
|
|
@@ -63,8 +63,36 @@ function compileSummary(written) {
|
|
|
63
63
|
};
|
|
64
64
|
}
|
|
65
65
|
/**
|
|
66
|
+
* Persist the preload bundle: one JSON object `{ "<page path>": data }` keyed by
|
|
67
|
+
* each entry's `path`, exactly the key `at(path)` caches under. A later entry for
|
|
68
|
+
* the same path wins, like the per-page file it overwrites.
|
|
69
|
+
*
|
|
70
|
+
* @param entries - The per-page data to bundle.
|
|
71
|
+
* @param outDir - The build output root.
|
|
72
|
+
* @param outputDir - The configured data output subdir.
|
|
73
|
+
* @returns The written bundle's `outDir`-relative path and byte length.
|
|
74
|
+
* @example
|
|
75
|
+
* ```ts
|
|
76
|
+
* await writeBundle([{ path: "/en/hello/", data }], "./dist", "_data");
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
async function writeBundle(entries, outDir, outputDir) {
|
|
80
|
+
const bundle = {};
|
|
81
|
+
for (const entry of entries) bundle[entry.path] = entry.data;
|
|
82
|
+
const file = `${outputDir.endsWith("/") ? outputDir.slice(0, -1) : outputDir}/${BUNDLE_FILE}`;
|
|
83
|
+
const filePath = path.join(outDir, file);
|
|
84
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
85
|
+
const body = JSON.stringify(bundle);
|
|
86
|
+
await writeFile(filePath, body, "utf8");
|
|
87
|
+
return {
|
|
88
|
+
file,
|
|
89
|
+
bytes: Buffer.byteLength(body, "utf8")
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
66
93
|
* The Node write side of the provider. Persists one JSON file per entry (bounded
|
|
67
94
|
* by `p-limit`) — domain-agnostic, no route expansion (`build` already did that).
|
|
95
|
+
* With `preload: "all"` it also writes the bundle file (see {@link writeBundle}).
|
|
68
96
|
* Records the summary in `ctx.state.lastWrite`.
|
|
69
97
|
*
|
|
70
98
|
* @param ctx - The data plugin context (state, config).
|
|
@@ -81,6 +109,7 @@ async function writeData(ctx, entries, options) {
|
|
|
81
109
|
const outDir = options?.outDir ?? DEFAULT_OUT_DIR;
|
|
82
110
|
const limit = pLimit(WRITE_CONCURRENCY);
|
|
83
111
|
const summary = compileSummary(await Promise.all(entries.map((entry) => limit(() => writeEntry(entry, outDir, ctx.config.outputDir)))));
|
|
112
|
+
if (ctx.config.preload === "all") summary.bundle = await writeBundle(entries, outDir, ctx.config.outputDir);
|
|
84
113
|
ctx.state.lastWrite = summary;
|
|
85
114
|
return summary;
|
|
86
115
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const require_convention = require("./convention-
|
|
1
|
+
const require_convention = require("./convention-BO-VoFLO.cjs");
|
|
2
2
|
const require_index = require("./index.cjs");
|
|
3
3
|
let node_fs_promises = require("node:fs/promises");
|
|
4
4
|
let node_path = require("node:path");
|
|
@@ -66,8 +66,36 @@ function compileSummary(written) {
|
|
|
66
66
|
};
|
|
67
67
|
}
|
|
68
68
|
/**
|
|
69
|
+
* Persist the preload bundle: one JSON object `{ "<page path>": data }` keyed by
|
|
70
|
+
* each entry's `path`, exactly the key `at(path)` caches under. A later entry for
|
|
71
|
+
* the same path wins, like the per-page file it overwrites.
|
|
72
|
+
*
|
|
73
|
+
* @param entries - The per-page data to bundle.
|
|
74
|
+
* @param outDir - The build output root.
|
|
75
|
+
* @param outputDir - The configured data output subdir.
|
|
76
|
+
* @returns The written bundle's `outDir`-relative path and byte length.
|
|
77
|
+
* @example
|
|
78
|
+
* ```ts
|
|
79
|
+
* await writeBundle([{ path: "/en/hello/", data }], "./dist", "_data");
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
async function writeBundle(entries, outDir, outputDir) {
|
|
83
|
+
const bundle = {};
|
|
84
|
+
for (const entry of entries) bundle[entry.path] = entry.data;
|
|
85
|
+
const file = `${outputDir.endsWith("/") ? outputDir.slice(0, -1) : outputDir}/${require_convention.BUNDLE_FILE}`;
|
|
86
|
+
const filePath = node_path.default.join(outDir, file);
|
|
87
|
+
await (0, node_fs_promises.mkdir)(node_path.default.dirname(filePath), { recursive: true });
|
|
88
|
+
const body = JSON.stringify(bundle);
|
|
89
|
+
await (0, node_fs_promises.writeFile)(filePath, body, "utf8");
|
|
90
|
+
return {
|
|
91
|
+
file,
|
|
92
|
+
bytes: Buffer.byteLength(body, "utf8")
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
69
96
|
* The Node write side of the provider. Persists one JSON file per entry (bounded
|
|
70
97
|
* by `p-limit`) — domain-agnostic, no route expansion (`build` already did that).
|
|
98
|
+
* With `preload: "all"` it also writes the bundle file (see {@link writeBundle}).
|
|
71
99
|
* Records the summary in `ctx.state.lastWrite`.
|
|
72
100
|
*
|
|
73
101
|
* @param ctx - The data plugin context (state, config).
|
|
@@ -84,6 +112,7 @@ async function writeData(ctx, entries, options) {
|
|
|
84
112
|
const outDir = options?.outDir ?? DEFAULT_OUT_DIR;
|
|
85
113
|
const limit = (0, p_limit.default)(WRITE_CONCURRENCY);
|
|
86
114
|
const summary = compileSummary(await Promise.all(entries.map((entry) => limit(() => writeEntry(entry, outDir, ctx.config.outputDir)))));
|
|
115
|
+
if (ctx.config.preload === "all") summary.bundle = await writeBundle(entries, outDir, ctx.config.outputDir);
|
|
87
116
|
ctx.state.lastWrite = summary;
|
|
88
117
|
return summary;
|
|
89
118
|
}
|
package/package.json
CHANGED