@moku-labs/web 2.2.2 → 2.3.0

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,5 +1,6 @@
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";
@@ -4531,7 +4532,7 @@ function createDataState(_ctx) {
4531
4532
  * isInvalidBaseUrl("_data"); // true
4532
4533
  * ```
4533
4534
  */
4534
- function isInvalidBaseUrl(baseUrl) {
4535
+ function isInvalidBaseUrl$1(baseUrl) {
4535
4536
  return typeof baseUrl !== "string" || !baseUrl.startsWith("/");
4536
4537
  }
4537
4538
  /**
@@ -4546,7 +4547,7 @@ function isInvalidBaseUrl(baseUrl) {
4546
4547
  * ```
4547
4548
  */
4548
4549
  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/").`);
4550
+ 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
4551
  }
4551
4552
  //#endregion
4552
4553
  //#region src/plugins/data/index.ts
@@ -9168,6 +9169,252 @@ const cliPlugin = createPlugin$1("cli", {
9168
9169
  api: createApi$1
9169
9170
  });
9170
9171
  //#endregion
9172
+ //#region src/plugins/collection/read.ts
9173
+ /**
9174
+ * @file `loadCollectionShard` — the collection plugin's standalone BROWSER read
9175
+ * primitive. Internal to the `collection` plugin AND exported for standalone
9176
+ * consumers: `collection.at(...)` wraps it, and a room-layer consumer can import it
9177
+ * directly from `@moku-labs/web/browser`.
9178
+ *
9179
+ * Unlike the `data` plugin's isomorphic `loadJson`, this uses the `fetch` global
9180
+ * DIRECTLY (no `node:fs` branch). Consumers — and tests — rely on `fetch` even
9181
+ * under Node test environments, so there is no document sniffing here: a shard is
9182
+ * always fetched over HTTP from the build-authored URL. This keeps the module
9183
+ * fully node-free.
9184
+ */
9185
+ /**
9186
+ * Fetch + parse a build-authored collection shard over HTTP. Builds the shard URL
9187
+ * via {@link collectionUrl}, fetches it, and throws on a non-OK response so the
9188
+ * caller (`collection.at`) can decide whether to fall back.
9189
+ *
9190
+ * @template T - The expected shape of the parsed shard JSON.
9191
+ * @param baseUrl - The site-root URL prefix (ends with `/`, e.g. `"/"`).
9192
+ * @param collection - The collection name (e.g. `"bank"`).
9193
+ * @param shard - The shard key within the collection (e.g. `"en/animals"`).
9194
+ * @returns The parsed shard JSON, typed as `T`.
9195
+ * @throws {Error} If the fetch is not OK.
9196
+ * @example
9197
+ * ```ts
9198
+ * const animals = await loadCollectionShard<Animal[]>("/", "bank", "en/animals");
9199
+ * ```
9200
+ */
9201
+ async function loadCollectionShard(baseUrl, collection, shard) {
9202
+ const url = collectionUrl(baseUrl, collection, shard);
9203
+ const response = await fetch(url);
9204
+ if (!response.ok) throw new Error(`[web] collection: failed to fetch ${url} (${String(response.status)}).`);
9205
+ return response.json();
9206
+ }
9207
+ //#endregion
9208
+ //#region src/plugins/collection/api.ts
9209
+ /**
9210
+ * @file collection plugin — API factory (the collection provider surface).
9211
+ *
9212
+ * Node-free by construction: this module statically imports only types + the pure
9213
+ * convention + the node-free `read` primitive. The Node write side (`write()`)
9214
+ * reaches its `node:fs` writer through a lazy `await import("./writer")` at call
9215
+ * time, so a browser bundle that composes `collection` for the read side never
9216
+ * pulls `node:*`. The read side (`at()`) uses only the `fetch`-based
9217
+ * `loadCollectionShard`.
9218
+ */
9219
+ /**
9220
+ * Builds the collection provider — the static-data bridge. `write()` is the Node
9221
+ * persist side; `at()` is the browser read side; `urlFor`/`fileFor` are the pure
9222
+ * convention. No `onStart`/`onStop` (holds no long-lived resource).
9223
+ *
9224
+ * @param ctx - The collection plugin context.
9225
+ * @returns The {@link CollectionProvider} mounted at `app.collection`.
9226
+ * @example
9227
+ * ```ts
9228
+ * const api = collectionApi(ctx);
9229
+ * await api.write([{ collection: "bank", shard: "en/animals", data }]); // Node build
9230
+ * await api.at("bank", "en/animals"); // browser
9231
+ * ```
9232
+ */
9233
+ function collectionApi(ctx) {
9234
+ return {
9235
+ /**
9236
+ * READ (browser) — fetch (and cache) the persisted data for a `(collection,
9237
+ * shard)` key. Returns the raw JSON as `unknown`; returns `null` if the fetch
9238
+ * or JSON parse fails (so the consumer can fall back). Wraps the throwing
9239
+ * {@link loadCollectionShard} reader into a soft null-returning app read.
9240
+ *
9241
+ * @param collection - The collection name (e.g. `"bank"`).
9242
+ * @param shard - The shard key within the collection (e.g. `"en/animals"`).
9243
+ * @returns The shard's raw data, or `null` on failure.
9244
+ * @example
9245
+ * ```ts
9246
+ * const raw = await api.at("bank", "en/animals");
9247
+ * ```
9248
+ */
9249
+ async at(collection, shard) {
9250
+ const key = `${collection}/${shard}`;
9251
+ if (ctx.state.cache.has(key)) return ctx.state.cache.get(key);
9252
+ try {
9253
+ const data = await loadCollectionShard(ctx.config.baseUrl, collection, shard);
9254
+ ctx.state.cache.set(key, data);
9255
+ return data;
9256
+ } catch {
9257
+ return null;
9258
+ }
9259
+ },
9260
+ /**
9261
+ * WRITE (Node) — persist one JSON file per entry, keyed by `(collection,
9262
+ * shard)`. Called by the build after it authors the shards. Lazily loads its
9263
+ * `node:fs` writer (keeping a browser bundle node-free).
9264
+ *
9265
+ * @param entries - The per-shard data to persist.
9266
+ * @param options - Optional `{ outDir }` override (defaults to `./dist`).
9267
+ * @param options.outDir - Build output directory the write happens under.
9268
+ * @returns A summary of the written files.
9269
+ * @example
9270
+ * ```ts
9271
+ * await api.write([{ collection: "bank", shard: "ru", data }], { outDir: "dist" });
9272
+ * ```
9273
+ */
9274
+ async write(entries, options) {
9275
+ const { writeCollection } = await import("./writer-C9BfpPio.mjs");
9276
+ return writeCollection(ctx, entries, options);
9277
+ },
9278
+ /**
9279
+ * PURE — the browser fetch URL for a `(collection, shard)` key.
9280
+ *
9281
+ * @param collection - The collection name.
9282
+ * @param shard - The shard key within the collection.
9283
+ * @returns The site-root-relative shard URL.
9284
+ * @example
9285
+ * ```ts
9286
+ * api.urlFor("bank", "en/animals"); // "/bank/en/animals.json"
9287
+ * ```
9288
+ */
9289
+ urlFor(collection, shard) {
9290
+ return collectionUrl(ctx.config.baseUrl, collection, shard);
9291
+ },
9292
+ /**
9293
+ * PURE — the `outDir`-relative file path for a `(collection, shard)` key.
9294
+ *
9295
+ * @param collection - The collection name.
9296
+ * @param shard - The shard key within the collection.
9297
+ * @returns The output-relative file path.
9298
+ * @example
9299
+ * ```ts
9300
+ * api.fileFor("bank", "en/animals"); // "bank/en/animals.json"
9301
+ * ```
9302
+ */
9303
+ fileFor(collection, shard) {
9304
+ return relativeShardFile(collection, shard);
9305
+ }
9306
+ };
9307
+ }
9308
+ //#endregion
9309
+ //#region src/plugins/collection/config.ts
9310
+ /**
9311
+ * Typed default collection config (R6: no inline `as`). `baseUrl` is the READ-side
9312
+ * site-root URL prefix the browser fetches shards from; the collection name is the
9313
+ * top directory under it. The default `"/"` serves shards straight from the site
9314
+ * root (e.g. `/bank/en/animals.json`).
9315
+ *
9316
+ * @example
9317
+ * ```ts
9318
+ * createPlugin("collection", { config: defaultCollectionConfig });
9319
+ * ```
9320
+ */
9321
+ const defaultCollectionConfig = { baseUrl: "/" };
9322
+ //#endregion
9323
+ //#region src/plugins/collection/state.ts
9324
+ /**
9325
+ * Creates initial collection state: a null `lastWrite` slot (populated by the Node
9326
+ * `write()` side) and an empty `cache` (populated lazily by the browser
9327
+ * `at(collection, shard)` side on first fetch).
9328
+ *
9329
+ * @param _ctx - Minimal context with global and config.
9330
+ * @param _ctx.global - Global framework configuration.
9331
+ * @param _ctx.config - Resolved plugin configuration.
9332
+ * @returns Fresh collection state with no recorded write and an empty per-shard cache.
9333
+ * @example
9334
+ * ```ts
9335
+ * const state = createCollectionState({ global: {}, config });
9336
+ * ```
9337
+ */
9338
+ function createCollectionState(_ctx) {
9339
+ return {
9340
+ lastWrite: null,
9341
+ cache: /* @__PURE__ */ new Map()
9342
+ };
9343
+ }
9344
+ //#endregion
9345
+ //#region src/plugins/collection/validate.ts
9346
+ /**
9347
+ * Reports whether a `baseUrl` value is invalid: it must be a string that ends with
9348
+ * a `"/"` so the collection name appends cleanly (`baseUrl + collection + "/" + …`).
9349
+ *
9350
+ * @param baseUrl - The candidate `baseUrl` value to check.
9351
+ * @returns `true` when `baseUrl` is not a string or does not end with "/".
9352
+ * @example
9353
+ * ```ts
9354
+ * isInvalidBaseUrl("/"); // false
9355
+ * isInvalidBaseUrl("/cdn"); // true
9356
+ * ```
9357
+ */
9358
+ function isInvalidBaseUrl(baseUrl) {
9359
+ return typeof baseUrl !== "string" || !baseUrl.endsWith("/");
9360
+ }
9361
+ /**
9362
+ * Validates the resolved collection config: the browser `baseUrl` must be a string
9363
+ * ending with `"/"` so the collection name appends cleanly.
9364
+ *
9365
+ * @param config - The resolved plugin configuration.
9366
+ * @throws {Error} If `baseUrl` is not a string ending with "/".
9367
+ * @example
9368
+ * ```ts
9369
+ * validateCollectionConfig({ baseUrl: "/" });
9370
+ * ```
9371
+ */
9372
+ function validateCollectionConfig(config) {
9373
+ if (isInvalidBaseUrl(config.baseUrl)) throw new Error(`[web] collection.baseUrl: must be a URL prefix ending with "/" (e.g. "/" or "/cdn/").`);
9374
+ }
9375
+ //#endregion
9376
+ //#region src/plugins/collection/index.ts
9377
+ /**
9378
+ * @file collection — Standard tier plugin (wiring-only). The static-data
9379
+ * COLLECTION provider — the collection-keyed sibling of the page-path-keyed `data`
9380
+ * plugin.
9381
+ *
9382
+ * Owns ONE contract — `(collection, shard) → persisted JSON file` — and nothing
9383
+ * about what the data is: `write(entries)` persists per-shard JSON on Node (the
9384
+ * build supplies the shards it already authored); `at(collection, shard)` fetches +
9385
+ * caches it in the browser as `unknown`, read on demand by the consumer. NOT a
9386
+ * framework default — the consumer composes it where needed (Node build AND/OR
9387
+ * browser app).
9388
+ *
9389
+ * **No hard `depends`** — fully browser-composable; the `node:fs` writer is behind
9390
+ * a lazy `import()` inside `write()`. Build ordering is a call-site contract: the
9391
+ * build writes shards during/after its pages phase, via `app.collection.write`.
9392
+ * No `onStart`/`onStop`.
9393
+ * @see README.md
9394
+ */
9395
+ /**
9396
+ * Collection plugin — the static-data collection provider. Mounts
9397
+ * `write(entries)` (Node persist), `at(collection, shard)` (browser read), and the
9398
+ * pure `urlFor`/`fileFor` convention at `app.collection`.
9399
+ *
9400
+ * @example
9401
+ * ```ts
9402
+ * // Node build: write build-authored shards during/after the build's pages phase.
9403
+ * const app = createApp({ plugins: [collectionPlugin, buildPlugin] });
9404
+ * await app.build.run();
9405
+ * await app.collection.write([{ collection: "bank", shard: "en/animals", data }]);
9406
+ *
9407
+ * // Browser app: compose `collectionPlugin` too; fetch a shard on demand.
9408
+ * const raw = await app.collection.at("bank", "en/animals"); // unknown | null
9409
+ * ```
9410
+ */
9411
+ const collectionPlugin = createPlugin$1("collection", {
9412
+ config: defaultCollectionConfig,
9413
+ createState: createCollectionState,
9414
+ onInit: (ctx) => validateCollectionConfig(ctx.config),
9415
+ api: collectionApi
9416
+ });
9417
+ //#endregion
9171
9418
  //#region src/plugins/spa/api.ts
9172
9419
  /**
9173
9420
  * Creates the spa plugin API surface (registration / control). All methods
@@ -9333,7 +9580,7 @@ function syncHead(_head, doc) {
9333
9580
  }
9334
9581
  //#endregion
9335
9582
  //#region src/plugins/spa/types.ts
9336
- var types_exports$7 = /* @__PURE__ */ __exportAll({ ISLAND_HOOK_NAMES: () => ISLAND_HOOK_NAMES });
9583
+ var types_exports$8 = /* @__PURE__ */ __exportAll({ ISLAND_HOOK_NAMES: () => ISLAND_HOOK_NAMES });
9337
9584
  /** Allowed hook names — single source of truth for fail-fast validation. */
9338
9585
  const ISLAND_HOOK_NAMES = [
9339
9586
  "onCreate",
@@ -11471,21 +11718,24 @@ var types_exports = /* @__PURE__ */ __exportAll({});
11471
11718
  //#region src/plugins/cli/types.ts
11472
11719
  var types_exports$1 = /* @__PURE__ */ __exportAll({});
11473
11720
  //#endregion
11474
- //#region src/plugins/content/types.ts
11721
+ //#region src/plugins/collection/types.ts
11475
11722
  var types_exports$2 = /* @__PURE__ */ __exportAll({});
11476
11723
  //#endregion
11477
- //#region src/plugins/data/types.ts
11724
+ //#region src/plugins/content/types.ts
11478
11725
  var types_exports$3 = /* @__PURE__ */ __exportAll({});
11479
11726
  //#endregion
11480
- //#region src/plugins/deploy/types.ts
11727
+ //#region src/plugins/data/types.ts
11481
11728
  var types_exports$4 = /* @__PURE__ */ __exportAll({});
11482
11729
  //#endregion
11483
- //#region src/plugins/head/types.ts
11730
+ //#region src/plugins/deploy/types.ts
11484
11731
  var types_exports$5 = /* @__PURE__ */ __exportAll({});
11485
11732
  //#endregion
11486
- //#region src/plugins/router/types.ts
11733
+ //#region src/plugins/head/types.ts
11487
11734
  var types_exports$6 = /* @__PURE__ */ __exportAll({});
11488
11735
  //#endregion
11736
+ //#region src/plugins/router/types.ts
11737
+ var types_exports$7 = /* @__PURE__ */ __exportAll({});
11738
+ //#endregion
11489
11739
  //#region node_modules/unist-util-stringify-position/lib/index.js
11490
11740
  /**
11491
11741
  * @typedef {import('unist').Node} Node
@@ -13684,4 +13934,4 @@ const createApp = core.createApp;
13684
13934
  */
13685
13935
  const createPlugin = core.createPlugin;
13686
13936
  //#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 };
13937
+ 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.0"
136
136
  }