@moku-labs/web 2.2.2 → 2.3.1

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.mjs CHANGED
@@ -1,10 +1,11 @@
1
1
  import { t as __exportAll } from "./chunk-D7D4PA-g.mjs";
2
2
  import { n as relativeDataFile, t as dataSuffix } from "./convention-Dp650o3y.mjs";
3
+ import { n as relativeShardFile, t as collectionUrl } from "./convention-Bs8SrQS9.mjs";
3
4
  import { browserEnv, cloudflareBindings, dotenv, envPlugin, envPlugin as envPlugin$1, logPlugin, logPlugin as logPlugin$1, processEnv } from "@moku-labs/common";
4
5
  import { createCoreConfig } from "@moku-labs/core";
5
6
  import { appendFileSync, existsSync, readFileSync, readdirSync, realpathSync, statSync, watch } from "node:fs";
6
7
  import { createHash, randomUUID } from "node:crypto";
7
- import { cp, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
8
+ import { copyFile, cp, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
8
9
  import { homedir, networkInterfaces, tmpdir } from "node:os";
9
10
  import minpath, { default as path } from "node:path";
10
11
  import { Feed } from "feed";
@@ -2558,9 +2559,70 @@ function substituteAssetPlaceholders(ctx, html) {
2558
2559
  return html.replaceAll(ASSETS_PLACEHOLDER, buildAssetTags(ctx)).replaceAll(CSS_ASSETS_PLACEHOLDER, buildAssetTags(ctx, "css")).replaceAll(JS_ASSETS_PLACEHOLDER, buildAssetTags(ctx, "js"));
2559
2560
  }
2560
2561
  /**
2561
- * Copies the configured `publicDir` (default `"public"`) verbatim into `outDir`,
2562
- * preserving the nested directory structure. Skips silently (returns `null`) when
2563
- * the source directory does not exist.
2562
+ * Test whether `destination` is already an up-to-date copy of the `src` file: same
2563
+ * size and at least as new (mtime). A missing destination — or any stat failure —
2564
+ * reports stale: when in doubt, copy.
2565
+ *
2566
+ * @param src - The source file path.
2567
+ * @param destination - The candidate destination file path.
2568
+ * @returns `true` when the destination can be skipped.
2569
+ * @example
2570
+ * ```ts
2571
+ * if (await isFreshCopy(src, destination)) return;
2572
+ * ```
2573
+ */
2574
+ async function isFreshCopy(src, destination) {
2575
+ try {
2576
+ const [srcStat, destinationStat] = await Promise.all([stat(src), stat(destination)]);
2577
+ return destinationStat.size === srcStat.size && destinationStat.mtimeMs >= srcStat.mtimeMs;
2578
+ } catch {
2579
+ return false;
2580
+ }
2581
+ }
2582
+ /**
2583
+ * Recursively copy `from` into `to`, skipping every file whose destination is already
2584
+ * fresh. Skipping matters in dev rebuilds (which keep the previous outDir): re-copying
2585
+ * unchanged assets makes asset-watching dev servers (wrangler) reload on src-only
2586
+ * edits, and on macOS/APFS under Bun a large-file re-copy goes through clonefile(2),
2587
+ * whose FSEvents "ItemCloned" echo on the SOURCE path re-triggers `public/**` watchers
2588
+ * — a self-sustaining rebuild loop. Serial on purpose: public dirs can hold thousands
2589
+ * of assets and the skip path is stat-only, while an unbounded parallel copy risks fd
2590
+ * exhaustion.
2591
+ *
2592
+ * @param from - The source directory.
2593
+ * @param to - The destination directory (created when missing).
2594
+ * @returns The number of entries actually copied.
2595
+ * @example
2596
+ * ```ts
2597
+ * const copied = await copyDirIncremental("public", "dist");
2598
+ * ```
2599
+ */
2600
+ async function copyDirIncremental(from, to) {
2601
+ await mkdir(to, { recursive: true });
2602
+ const entries = await readdir(from, { withFileTypes: true });
2603
+ let copied = 0;
2604
+ for (const entry of entries) {
2605
+ const src = path.join(from, entry.name);
2606
+ const destination = path.join(to, entry.name);
2607
+ if (entry.isDirectory()) copied += await copyDirIncremental(src, destination);
2608
+ else if (entry.isFile()) {
2609
+ if (await isFreshCopy(src, destination)) continue;
2610
+ await copyFile(src, destination);
2611
+ copied += 1;
2612
+ } else {
2613
+ await cp(src, destination, { recursive: true });
2614
+ copied += 1;
2615
+ }
2616
+ }
2617
+ return copied;
2618
+ }
2619
+ /**
2620
+ * Copies the configured `publicDir` (default `"public"`) into `outDir`, preserving the
2621
+ * nested directory structure. The copy is incremental: a file whose destination already
2622
+ * exists with the same size and an mtime at least as new as the source is skipped. A
2623
+ * full build cleans outDir first, so every file misses the compare and the output is
2624
+ * identical to a verbatim copy; a dev rebuild (`skipClean`) rewrites only what changed.
2625
+ * Skips silently (returns `null`) when the source directory does not exist.
2564
2626
  *
2565
2627
  * @param ctx - Plugin context (provides `config`, `log`).
2566
2628
  * @returns The copy result, or `null` when the public directory is absent.
@@ -2578,14 +2640,15 @@ async function copyPublic(ctx) {
2578
2640
  });
2579
2641
  return null;
2580
2642
  }
2581
- await cp(from, ctx.config.outDir, { recursive: true });
2643
+ const copied = await copyDirIncremental(from, ctx.config.outDir);
2582
2644
  ctx.log.debug("build:public", {
2583
2645
  from,
2584
- dest: ctx.config.outDir
2646
+ dest: ctx.config.outDir,
2647
+ copied
2585
2648
  });
2586
2649
  return {
2587
2650
  from: path.normalize(from),
2588
- copied: 1
2651
+ copied
2589
2652
  };
2590
2653
  }
2591
2654
  //#endregion
@@ -4531,7 +4594,7 @@ function createDataState(_ctx) {
4531
4594
  * isInvalidBaseUrl("_data"); // true
4532
4595
  * ```
4533
4596
  */
4534
- function isInvalidBaseUrl(baseUrl) {
4597
+ function isInvalidBaseUrl$1(baseUrl) {
4535
4598
  return typeof baseUrl !== "string" || !baseUrl.startsWith("/");
4536
4599
  }
4537
4600
  /**
@@ -4546,7 +4609,7 @@ function isInvalidBaseUrl(baseUrl) {
4546
4609
  * ```
4547
4610
  */
4548
4611
  function validateDataConfig(config) {
4549
- if (isInvalidBaseUrl(config.baseUrl)) throw new Error(`[web] data.baseUrl: must be a site-root-relative URL path starting with "/" (e.g. "/_data/").`);
4612
+ if (isInvalidBaseUrl$1(config.baseUrl)) throw new Error(`[web] data.baseUrl: must be a site-root-relative URL path starting with "/" (e.g. "/_data/").`);
4550
4613
  }
4551
4614
  //#endregion
4552
4615
  //#region src/plugins/data/index.ts
@@ -9168,6 +9231,252 @@ const cliPlugin = createPlugin$1("cli", {
9168
9231
  api: createApi$1
9169
9232
  });
9170
9233
  //#endregion
9234
+ //#region src/plugins/collection/read.ts
9235
+ /**
9236
+ * @file `loadCollectionShard` — the collection plugin's standalone BROWSER read
9237
+ * primitive. Internal to the `collection` plugin AND exported for standalone
9238
+ * consumers: `collection.at(...)` wraps it, and a room-layer consumer can import it
9239
+ * directly from `@moku-labs/web/browser`.
9240
+ *
9241
+ * Unlike the `data` plugin's isomorphic `loadJson`, this uses the `fetch` global
9242
+ * DIRECTLY (no `node:fs` branch). Consumers — and tests — rely on `fetch` even
9243
+ * under Node test environments, so there is no document sniffing here: a shard is
9244
+ * always fetched over HTTP from the build-authored URL. This keeps the module
9245
+ * fully node-free.
9246
+ */
9247
+ /**
9248
+ * Fetch + parse a build-authored collection shard over HTTP. Builds the shard URL
9249
+ * via {@link collectionUrl}, fetches it, and throws on a non-OK response so the
9250
+ * caller (`collection.at`) can decide whether to fall back.
9251
+ *
9252
+ * @template T - The expected shape of the parsed shard JSON.
9253
+ * @param baseUrl - The site-root URL prefix (ends with `/`, e.g. `"/"`).
9254
+ * @param collection - The collection name (e.g. `"bank"`).
9255
+ * @param shard - The shard key within the collection (e.g. `"en/animals"`).
9256
+ * @returns The parsed shard JSON, typed as `T`.
9257
+ * @throws {Error} If the fetch is not OK.
9258
+ * @example
9259
+ * ```ts
9260
+ * const animals = await loadCollectionShard<Animal[]>("/", "bank", "en/animals");
9261
+ * ```
9262
+ */
9263
+ async function loadCollectionShard(baseUrl, collection, shard) {
9264
+ const url = collectionUrl(baseUrl, collection, shard);
9265
+ const response = await fetch(url);
9266
+ if (!response.ok) throw new Error(`[web] collection: failed to fetch ${url} (${String(response.status)}).`);
9267
+ return response.json();
9268
+ }
9269
+ //#endregion
9270
+ //#region src/plugins/collection/api.ts
9271
+ /**
9272
+ * @file collection plugin — API factory (the collection provider surface).
9273
+ *
9274
+ * Node-free by construction: this module statically imports only types + the pure
9275
+ * convention + the node-free `read` primitive. The Node write side (`write()`)
9276
+ * reaches its `node:fs` writer through a lazy `await import("./writer")` at call
9277
+ * time, so a browser bundle that composes `collection` for the read side never
9278
+ * pulls `node:*`. The read side (`at()`) uses only the `fetch`-based
9279
+ * `loadCollectionShard`.
9280
+ */
9281
+ /**
9282
+ * Builds the collection provider — the static-data bridge. `write()` is the Node
9283
+ * persist side; `at()` is the browser read side; `urlFor`/`fileFor` are the pure
9284
+ * convention. No `onStart`/`onStop` (holds no long-lived resource).
9285
+ *
9286
+ * @param ctx - The collection plugin context.
9287
+ * @returns The {@link CollectionProvider} mounted at `app.collection`.
9288
+ * @example
9289
+ * ```ts
9290
+ * const api = collectionApi(ctx);
9291
+ * await api.write([{ collection: "bank", shard: "en/animals", data }]); // Node build
9292
+ * await api.at("bank", "en/animals"); // browser
9293
+ * ```
9294
+ */
9295
+ function collectionApi(ctx) {
9296
+ return {
9297
+ /**
9298
+ * READ (browser) — fetch (and cache) the persisted data for a `(collection,
9299
+ * shard)` key. Returns the raw JSON as `unknown`; returns `null` if the fetch
9300
+ * or JSON parse fails (so the consumer can fall back). Wraps the throwing
9301
+ * {@link loadCollectionShard} reader into a soft null-returning app read.
9302
+ *
9303
+ * @param collection - The collection name (e.g. `"bank"`).
9304
+ * @param shard - The shard key within the collection (e.g. `"en/animals"`).
9305
+ * @returns The shard's raw data, or `null` on failure.
9306
+ * @example
9307
+ * ```ts
9308
+ * const raw = await api.at("bank", "en/animals");
9309
+ * ```
9310
+ */
9311
+ async at(collection, shard) {
9312
+ const key = `${collection}/${shard}`;
9313
+ if (ctx.state.cache.has(key)) return ctx.state.cache.get(key);
9314
+ try {
9315
+ const data = await loadCollectionShard(ctx.config.baseUrl, collection, shard);
9316
+ ctx.state.cache.set(key, data);
9317
+ return data;
9318
+ } catch {
9319
+ return null;
9320
+ }
9321
+ },
9322
+ /**
9323
+ * WRITE (Node) — persist one JSON file per entry, keyed by `(collection,
9324
+ * shard)`. Called by the build after it authors the shards. Lazily loads its
9325
+ * `node:fs` writer (keeping a browser bundle node-free).
9326
+ *
9327
+ * @param entries - The per-shard data to persist.
9328
+ * @param options - Optional `{ outDir }` override (defaults to `./dist`).
9329
+ * @param options.outDir - Build output directory the write happens under.
9330
+ * @returns A summary of the written files.
9331
+ * @example
9332
+ * ```ts
9333
+ * await api.write([{ collection: "bank", shard: "ru", data }], { outDir: "dist" });
9334
+ * ```
9335
+ */
9336
+ async write(entries, options) {
9337
+ const { writeCollection } = await import("./writer-C9BfpPio.mjs");
9338
+ return writeCollection(ctx, entries, options);
9339
+ },
9340
+ /**
9341
+ * PURE — the browser fetch URL for a `(collection, shard)` key.
9342
+ *
9343
+ * @param collection - The collection name.
9344
+ * @param shard - The shard key within the collection.
9345
+ * @returns The site-root-relative shard URL.
9346
+ * @example
9347
+ * ```ts
9348
+ * api.urlFor("bank", "en/animals"); // "/bank/en/animals.json"
9349
+ * ```
9350
+ */
9351
+ urlFor(collection, shard) {
9352
+ return collectionUrl(ctx.config.baseUrl, collection, shard);
9353
+ },
9354
+ /**
9355
+ * PURE — the `outDir`-relative file path for a `(collection, shard)` key.
9356
+ *
9357
+ * @param collection - The collection name.
9358
+ * @param shard - The shard key within the collection.
9359
+ * @returns The output-relative file path.
9360
+ * @example
9361
+ * ```ts
9362
+ * api.fileFor("bank", "en/animals"); // "bank/en/animals.json"
9363
+ * ```
9364
+ */
9365
+ fileFor(collection, shard) {
9366
+ return relativeShardFile(collection, shard);
9367
+ }
9368
+ };
9369
+ }
9370
+ //#endregion
9371
+ //#region src/plugins/collection/config.ts
9372
+ /**
9373
+ * Typed default collection config (R6: no inline `as`). `baseUrl` is the READ-side
9374
+ * site-root URL prefix the browser fetches shards from; the collection name is the
9375
+ * top directory under it. The default `"/"` serves shards straight from the site
9376
+ * root (e.g. `/bank/en/animals.json`).
9377
+ *
9378
+ * @example
9379
+ * ```ts
9380
+ * createPlugin("collection", { config: defaultCollectionConfig });
9381
+ * ```
9382
+ */
9383
+ const defaultCollectionConfig = { baseUrl: "/" };
9384
+ //#endregion
9385
+ //#region src/plugins/collection/state.ts
9386
+ /**
9387
+ * Creates initial collection state: a null `lastWrite` slot (populated by the Node
9388
+ * `write()` side) and an empty `cache` (populated lazily by the browser
9389
+ * `at(collection, shard)` side on first fetch).
9390
+ *
9391
+ * @param _ctx - Minimal context with global and config.
9392
+ * @param _ctx.global - Global framework configuration.
9393
+ * @param _ctx.config - Resolved plugin configuration.
9394
+ * @returns Fresh collection state with no recorded write and an empty per-shard cache.
9395
+ * @example
9396
+ * ```ts
9397
+ * const state = createCollectionState({ global: {}, config });
9398
+ * ```
9399
+ */
9400
+ function createCollectionState(_ctx) {
9401
+ return {
9402
+ lastWrite: null,
9403
+ cache: /* @__PURE__ */ new Map()
9404
+ };
9405
+ }
9406
+ //#endregion
9407
+ //#region src/plugins/collection/validate.ts
9408
+ /**
9409
+ * Reports whether a `baseUrl` value is invalid: it must be a string that ends with
9410
+ * a `"/"` so the collection name appends cleanly (`baseUrl + collection + "/" + …`).
9411
+ *
9412
+ * @param baseUrl - The candidate `baseUrl` value to check.
9413
+ * @returns `true` when `baseUrl` is not a string or does not end with "/".
9414
+ * @example
9415
+ * ```ts
9416
+ * isInvalidBaseUrl("/"); // false
9417
+ * isInvalidBaseUrl("/cdn"); // true
9418
+ * ```
9419
+ */
9420
+ function isInvalidBaseUrl(baseUrl) {
9421
+ return typeof baseUrl !== "string" || !baseUrl.endsWith("/");
9422
+ }
9423
+ /**
9424
+ * Validates the resolved collection config: the browser `baseUrl` must be a string
9425
+ * ending with `"/"` so the collection name appends cleanly.
9426
+ *
9427
+ * @param config - The resolved plugin configuration.
9428
+ * @throws {Error} If `baseUrl` is not a string ending with "/".
9429
+ * @example
9430
+ * ```ts
9431
+ * validateCollectionConfig({ baseUrl: "/" });
9432
+ * ```
9433
+ */
9434
+ function validateCollectionConfig(config) {
9435
+ if (isInvalidBaseUrl(config.baseUrl)) throw new Error(`[web] collection.baseUrl: must be a URL prefix ending with "/" (e.g. "/" or "/cdn/").`);
9436
+ }
9437
+ //#endregion
9438
+ //#region src/plugins/collection/index.ts
9439
+ /**
9440
+ * @file collection — Standard tier plugin (wiring-only). The static-data
9441
+ * COLLECTION provider — the collection-keyed sibling of the page-path-keyed `data`
9442
+ * plugin.
9443
+ *
9444
+ * Owns ONE contract — `(collection, shard) → persisted JSON file` — and nothing
9445
+ * about what the data is: `write(entries)` persists per-shard JSON on Node (the
9446
+ * build supplies the shards it already authored); `at(collection, shard)` fetches +
9447
+ * caches it in the browser as `unknown`, read on demand by the consumer. NOT a
9448
+ * framework default — the consumer composes it where needed (Node build AND/OR
9449
+ * browser app).
9450
+ *
9451
+ * **No hard `depends`** — fully browser-composable; the `node:fs` writer is behind
9452
+ * a lazy `import()` inside `write()`. Build ordering is a call-site contract: the
9453
+ * build writes shards during/after its pages phase, via `app.collection.write`.
9454
+ * No `onStart`/`onStop`.
9455
+ * @see README.md
9456
+ */
9457
+ /**
9458
+ * Collection plugin — the static-data collection provider. Mounts
9459
+ * `write(entries)` (Node persist), `at(collection, shard)` (browser read), and the
9460
+ * pure `urlFor`/`fileFor` convention at `app.collection`.
9461
+ *
9462
+ * @example
9463
+ * ```ts
9464
+ * // Node build: write build-authored shards during/after the build's pages phase.
9465
+ * const app = createApp({ plugins: [collectionPlugin, buildPlugin] });
9466
+ * await app.build.run();
9467
+ * await app.collection.write([{ collection: "bank", shard: "en/animals", data }]);
9468
+ *
9469
+ * // Browser app: compose `collectionPlugin` too; fetch a shard on demand.
9470
+ * const raw = await app.collection.at("bank", "en/animals"); // unknown | null
9471
+ * ```
9472
+ */
9473
+ const collectionPlugin = createPlugin$1("collection", {
9474
+ config: defaultCollectionConfig,
9475
+ createState: createCollectionState,
9476
+ onInit: (ctx) => validateCollectionConfig(ctx.config),
9477
+ api: collectionApi
9478
+ });
9479
+ //#endregion
9171
9480
  //#region src/plugins/spa/api.ts
9172
9481
  /**
9173
9482
  * Creates the spa plugin API surface (registration / control). All methods
@@ -9333,7 +9642,7 @@ function syncHead(_head, doc) {
9333
9642
  }
9334
9643
  //#endregion
9335
9644
  //#region src/plugins/spa/types.ts
9336
- var types_exports$7 = /* @__PURE__ */ __exportAll({ ISLAND_HOOK_NAMES: () => ISLAND_HOOK_NAMES });
9645
+ var types_exports$8 = /* @__PURE__ */ __exportAll({ ISLAND_HOOK_NAMES: () => ISLAND_HOOK_NAMES });
9337
9646
  /** Allowed hook names — single source of truth for fail-fast validation. */
9338
9647
  const ISLAND_HOOK_NAMES = [
9339
9648
  "onCreate",
@@ -11471,21 +11780,24 @@ var types_exports = /* @__PURE__ */ __exportAll({});
11471
11780
  //#region src/plugins/cli/types.ts
11472
11781
  var types_exports$1 = /* @__PURE__ */ __exportAll({});
11473
11782
  //#endregion
11474
- //#region src/plugins/content/types.ts
11783
+ //#region src/plugins/collection/types.ts
11475
11784
  var types_exports$2 = /* @__PURE__ */ __exportAll({});
11476
11785
  //#endregion
11477
- //#region src/plugins/data/types.ts
11786
+ //#region src/plugins/content/types.ts
11478
11787
  var types_exports$3 = /* @__PURE__ */ __exportAll({});
11479
11788
  //#endregion
11480
- //#region src/plugins/deploy/types.ts
11789
+ //#region src/plugins/data/types.ts
11481
11790
  var types_exports$4 = /* @__PURE__ */ __exportAll({});
11482
11791
  //#endregion
11483
- //#region src/plugins/head/types.ts
11792
+ //#region src/plugins/deploy/types.ts
11484
11793
  var types_exports$5 = /* @__PURE__ */ __exportAll({});
11485
11794
  //#endregion
11486
- //#region src/plugins/router/types.ts
11795
+ //#region src/plugins/head/types.ts
11487
11796
  var types_exports$6 = /* @__PURE__ */ __exportAll({});
11488
11797
  //#endregion
11798
+ //#region src/plugins/router/types.ts
11799
+ var types_exports$7 = /* @__PURE__ */ __exportAll({});
11800
+ //#endregion
11489
11801
  //#region node_modules/unist-util-stringify-position/lib/index.js
11490
11802
  /**
11491
11803
  * @typedef {import('unist').Node} Node
@@ -13684,4 +13996,4 @@ const createApp = core.createApp;
13684
13996
  */
13685
13997
  const createPlugin = core.createPlugin;
13686
13998
  //#endregion
13687
- export { types_exports as Build, types_exports$1 as Cli, types_exports$2 as Content, types_exports$3 as Data, types_exports$4 as Deploy, EmbedFacadeButton, GalleryTrack, types_exports$5 as Head, types_exports$6 as Router, types_exports$7 as Spa, browserEnv, buildArticleHead, buildPlugin, canonical, cliPlugin, cloudflareBindings, contentPlugin, createApp, createChannel, createIsland, createPlugin, createUrls, dataPlugin, defineRoutes, deployPlugin, dotenv, envPlugin, feedLink, fileSystemContent, hardNavigate, headPlugin, hreflang, i18nPlugin, jsonLd, lazyEmbed, logPlugin, meta, navigate, og, processEnv, route, routerPlugin, sitePlugin, spaPlugin, twitter };
13999
+ export { types_exports as Build, types_exports$1 as Cli, types_exports$2 as Collection, types_exports$3 as Content, types_exports$4 as Data, types_exports$5 as Deploy, EmbedFacadeButton, GalleryTrack, types_exports$6 as Head, types_exports$7 as Router, types_exports$8 as Spa, browserEnv, buildArticleHead, buildPlugin, canonical, cliPlugin, cloudflareBindings, collectionPlugin, collectionUrl, contentPlugin, createApp, createChannel, createIsland, createPlugin, createUrls, dataPlugin, defineRoutes, deployPlugin, dotenv, envPlugin, feedLink, fileSystemContent, hardNavigate, headPlugin, hreflang, i18nPlugin, jsonLd, lazyEmbed, loadCollectionShard, logPlugin, meta, navigate, og, processEnv, route, routerPlugin, sitePlugin, spaPlugin, twitter };
@@ -0,0 +1,90 @@
1
+ const require_convention = require("./convention-CsJw7CML.cjs");
2
+ const require_index = require("./index.cjs");
3
+ let node_fs_promises = require("node:fs/promises");
4
+ let node_path = require("node:path");
5
+ node_path = require_index.__toESM(node_path, 1);
6
+ let p_limit = require("p-limit");
7
+ p_limit = require_index.__toESM(p_limit, 1);
8
+ //#region src/plugins/collection/writer.ts
9
+ /**
10
+ * @file collection plugin — Node write side of the provider (`write()`).
11
+ *
12
+ * This is the ONLY module in the `collection` plugin that touches `node:*`. It is
13
+ * reached exclusively through a lazy `await import("./writer")` inside `api.ts`'s
14
+ * `write()`, so a browser bundle that composes `collection` for the read side never
15
+ * statically pulls `node:fs` (see `__tests__/unit/isolation.test.ts`).
16
+ *
17
+ * It is domain-agnostic: it persists whatever `data` each {@link CollectionShard}
18
+ * carries (the build authored these), one JSON file per shard, at the path
19
+ * {@link relativeShardFile} derives from `(collection, shard)`. No domain knowledge,
20
+ * no expansion (build already authored the shards).
21
+ */
22
+ /** Default build output root, matching build's default `outDir` ("./dist", build/api.ts). */
23
+ const DEFAULT_OUT_DIR = "./dist";
24
+ /** Concurrency bound for per-shard JSON writes. */
25
+ const WRITE_CONCURRENCY = 8;
26
+ /**
27
+ * Persist one shard's data as JSON under `<outDir>/<relativeShardFile>` and return
28
+ * the written `{ relative, bytes }`.
29
+ *
30
+ * @param entry - The shard entry to persist.
31
+ * @param outDir - The build output root.
32
+ * @returns The written file's `outDir`-relative path and byte length.
33
+ * @example
34
+ * ```ts
35
+ * await writeEntry({ collection: "bank", shard: "en/animals", data }, "./dist");
36
+ * ```
37
+ */
38
+ async function writeEntry(entry, outDir) {
39
+ const relative = require_convention.relativeShardFile(entry.collection, entry.shard);
40
+ const filePath = node_path.default.join(outDir, relative);
41
+ await (0, node_fs_promises.mkdir)(node_path.default.dirname(filePath), { recursive: true });
42
+ const body = JSON.stringify(entry.data);
43
+ await (0, node_fs_promises.writeFile)(filePath, body, "utf8");
44
+ return {
45
+ relative,
46
+ bytes: Buffer.byteLength(body, "utf8")
47
+ };
48
+ }
49
+ /**
50
+ * Roll up the per-file write records into the public {@link CollectionWriteSummary}:
51
+ * file count, total byte length, and the `outDir`-relative paths in write order.
52
+ *
53
+ * @param written - The per-file `{ relative, bytes }` records from {@link writeEntry}.
54
+ * @returns The aggregated write summary.
55
+ * @example
56
+ * ```ts
57
+ * compileSummary([{ relative: "bank/en/animals.json", bytes: 12 }]);
58
+ * ```
59
+ */
60
+ function compileSummary(written) {
61
+ return {
62
+ fileCount: written.length,
63
+ bytes: written.reduce((total, file) => total + file.bytes, 0),
64
+ files: written.map((file) => file.relative)
65
+ };
66
+ }
67
+ /**
68
+ * The Node write side of the provider. Persists one JSON file per shard (bounded
69
+ * by `p-limit`) — domain-agnostic, no expansion (`build` already authored the
70
+ * shards). Records the summary in `ctx.state.lastWrite`.
71
+ *
72
+ * @param ctx - The collection plugin context (state, config).
73
+ * @param entries - The per-shard data to persist.
74
+ * @param options - Optional overrides.
75
+ * @param options.outDir - Build output directory to write under (default `./dist`).
76
+ * @returns A summary of the written files.
77
+ * @example
78
+ * ```ts
79
+ * const summary = await writeCollection(ctx, [{ collection: "bank", shard: "ru", data }]);
80
+ * ```
81
+ */
82
+ async function writeCollection(ctx, entries, options) {
83
+ const outDir = options?.outDir ?? DEFAULT_OUT_DIR;
84
+ const limit = (0, p_limit.default)(WRITE_CONCURRENCY);
85
+ const summary = compileSummary(await Promise.all(entries.map((entry) => limit(() => writeEntry(entry, outDir)))));
86
+ ctx.state.lastWrite = summary;
87
+ return summary;
88
+ }
89
+ //#endregion
90
+ exports.writeCollection = writeCollection;
@@ -1,9 +1,10 @@
1
- const require_convention = require("./convention-BpDfzX7e.cjs");
1
+ const require_convention = require("./convention-CLIDd9k-.cjs");
2
+ const require_index = require("./index.cjs");
2
3
  let node_fs_promises = require("node:fs/promises");
3
4
  let node_path = require("node:path");
4
- node_path = require_convention.__toESM(node_path, 1);
5
+ node_path = require_index.__toESM(node_path, 1);
5
6
  let p_limit = require("p-limit");
6
- p_limit = require_convention.__toESM(p_limit, 1);
7
+ p_limit = require_index.__toESM(p_limit, 1);
7
8
  //#region src/plugins/data/writer.ts
8
9
  /**
9
10
  * @file data plugin — Node write side of the agnostic provider (`write()`).
@@ -0,0 +1,87 @@
1
+ import { n as relativeShardFile } from "./convention-Bs8SrQS9.mjs";
2
+ import { mkdir, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import pLimit from "p-limit";
5
+ //#region src/plugins/collection/writer.ts
6
+ /**
7
+ * @file collection plugin — Node write side of the provider (`write()`).
8
+ *
9
+ * This is the ONLY module in the `collection` plugin that touches `node:*`. It is
10
+ * reached exclusively through a lazy `await import("./writer")` inside `api.ts`'s
11
+ * `write()`, so a browser bundle that composes `collection` for the read side never
12
+ * statically pulls `node:fs` (see `__tests__/unit/isolation.test.ts`).
13
+ *
14
+ * It is domain-agnostic: it persists whatever `data` each {@link CollectionShard}
15
+ * carries (the build authored these), one JSON file per shard, at the path
16
+ * {@link relativeShardFile} derives from `(collection, shard)`. No domain knowledge,
17
+ * no expansion (build already authored the shards).
18
+ */
19
+ /** Default build output root, matching build's default `outDir` ("./dist", build/api.ts). */
20
+ const DEFAULT_OUT_DIR = "./dist";
21
+ /** Concurrency bound for per-shard JSON writes. */
22
+ const WRITE_CONCURRENCY = 8;
23
+ /**
24
+ * Persist one shard's data as JSON under `<outDir>/<relativeShardFile>` and return
25
+ * the written `{ relative, bytes }`.
26
+ *
27
+ * @param entry - The shard entry to persist.
28
+ * @param outDir - The build output root.
29
+ * @returns The written file's `outDir`-relative path and byte length.
30
+ * @example
31
+ * ```ts
32
+ * await writeEntry({ collection: "bank", shard: "en/animals", data }, "./dist");
33
+ * ```
34
+ */
35
+ async function writeEntry(entry, outDir) {
36
+ const relative = relativeShardFile(entry.collection, entry.shard);
37
+ const filePath = path.join(outDir, relative);
38
+ await mkdir(path.dirname(filePath), { recursive: true });
39
+ const body = JSON.stringify(entry.data);
40
+ await writeFile(filePath, body, "utf8");
41
+ return {
42
+ relative,
43
+ bytes: Buffer.byteLength(body, "utf8")
44
+ };
45
+ }
46
+ /**
47
+ * Roll up the per-file write records into the public {@link CollectionWriteSummary}:
48
+ * file count, total byte length, and the `outDir`-relative paths in write order.
49
+ *
50
+ * @param written - The per-file `{ relative, bytes }` records from {@link writeEntry}.
51
+ * @returns The aggregated write summary.
52
+ * @example
53
+ * ```ts
54
+ * compileSummary([{ relative: "bank/en/animals.json", bytes: 12 }]);
55
+ * ```
56
+ */
57
+ function compileSummary(written) {
58
+ return {
59
+ fileCount: written.length,
60
+ bytes: written.reduce((total, file) => total + file.bytes, 0),
61
+ files: written.map((file) => file.relative)
62
+ };
63
+ }
64
+ /**
65
+ * The Node write side of the provider. Persists one JSON file per shard (bounded
66
+ * by `p-limit`) — domain-agnostic, no expansion (`build` already authored the
67
+ * shards). Records the summary in `ctx.state.lastWrite`.
68
+ *
69
+ * @param ctx - The collection plugin context (state, config).
70
+ * @param entries - The per-shard data to persist.
71
+ * @param options - Optional overrides.
72
+ * @param options.outDir - Build output directory to write under (default `./dist`).
73
+ * @returns A summary of the written files.
74
+ * @example
75
+ * ```ts
76
+ * const summary = await writeCollection(ctx, [{ collection: "bank", shard: "ru", data }]);
77
+ * ```
78
+ */
79
+ async function writeCollection(ctx, entries, options) {
80
+ const outDir = options?.outDir ?? DEFAULT_OUT_DIR;
81
+ const limit = pLimit(WRITE_CONCURRENCY);
82
+ const summary = compileSummary(await Promise.all(entries.map((entry) => limit(() => writeEntry(entry, outDir)))));
83
+ ctx.state.lastWrite = summary;
84
+ return summary;
85
+ }
86
+ //#endregion
87
+ export { writeCollection };
package/package.json CHANGED
@@ -132,5 +132,5 @@
132
132
  "test:build-e2e": "bun test src/plugins/build/__tests__/e2e/",
133
133
  "test:coverage": "vitest run --project unit --project integration --coverage"
134
134
  },
135
- "version": "2.2.2"
135
+ "version": "2.3.1"
136
136
  }