@moku-labs/web 0.2.0 → 0.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,4 +1,4 @@
1
- import { t as __exportAll } from "./chunk-DQk6qfdC.mjs";
1
+ import { t as __exportAll } from "./chunk-D7D4PA-g.mjs";
2
2
  import { createCoreConfig, createCorePlugin } from "@moku-labs/core";
3
3
  import { existsSync, readFileSync, readdirSync } from "node:fs";
4
4
  import { cp, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
@@ -23,7 +23,6 @@ import { Resvg } from "@resvg/resvg-js";
23
23
  import satori from "satori";
24
24
  import { jsx } from "preact/jsx-runtime";
25
25
  import { renderToString } from "preact-render-to-string";
26
-
27
26
  //#region src/plugins/env/api.ts
28
27
  /** Error prefix for all env API failures. */
29
28
  const ERROR_PREFIX$13 = "[web]";
@@ -44,26 +43,75 @@ const ERROR_PREFIX$13 = "[web]";
44
43
  function createEnvApi(ctx) {
45
44
  const { resolved, publicMap } = ctx.state;
46
45
  return {
46
+ /**
47
+ * Reads a resolved variable.
48
+ *
49
+ * @param key - Variable name.
50
+ * @returns The value, or `undefined` if not present.
51
+ * @example
52
+ * ```ts
53
+ * api.get("PUBLIC_API_URL");
54
+ * ```
55
+ */
47
56
  get(key) {
48
57
  return resolved.get(key);
49
58
  },
59
+ /**
60
+ * Reads a variable that must exist.
61
+ *
62
+ * @param key - Variable name.
63
+ * @returns The value.
64
+ * @throws {Error} If the variable is undefined.
65
+ * @example
66
+ * ```ts
67
+ * api.require("DEPLOY_TOKEN");
68
+ * ```
69
+ */
50
70
  require(key) {
51
71
  const value = resolved.get(key);
52
72
  if (value === void 0) throw new Error(`${ERROR_PREFIX$13} env: required variable "${key}" is not defined.`);
53
73
  return value;
54
74
  },
75
+ /**
76
+ * Tests presence of a resolved variable.
77
+ *
78
+ * @param key - Variable name.
79
+ * @returns `true` if a value is present.
80
+ * @example
81
+ * ```ts
82
+ * api.has("PUBLIC_API_URL");
83
+ * ```
84
+ */
55
85
  has(key) {
56
86
  return resolved.has(key);
57
87
  },
88
+ /**
89
+ * Returns all public variables as a frozen plain object — a fresh copy,
90
+ * never the raw state map.
91
+ *
92
+ * @returns A frozen `Record` of public variable names to values.
93
+ * @example
94
+ * ```ts
95
+ * const payload = { ...api.getPublic() };
96
+ * ```
97
+ */
58
98
  getPublic() {
59
99
  return Object.freeze(Object.fromEntries(publicMap));
60
100
  },
101
+ /**
102
+ * Returns the already-frozen map of public variables.
103
+ *
104
+ * @returns The frozen public map.
105
+ * @example
106
+ * ```ts
107
+ * [...api.getPublicMap()];
108
+ * ```
109
+ */
61
110
  getPublicMap() {
62
111
  return publicMap;
63
112
  }
64
113
  };
65
114
  }
66
-
67
115
  //#endregion
68
116
  //#region src/plugins/env/state.ts
69
117
  /**
@@ -83,7 +131,6 @@ function createEnvState() {
83
131
  publicMap: /* @__PURE__ */ new Map()
84
132
  };
85
133
  }
86
-
87
134
  //#endregion
88
135
  //#region src/plugins/env/validate.ts
89
136
  /** Error message thrown by every frozen-map mutator. */
@@ -202,7 +249,6 @@ function validateSchema(ctx) {
202
249
  freezeMap(state.resolved);
203
250
  freezeMap(state.publicMap);
204
251
  }
205
-
206
252
  //#endregion
207
253
  //#region src/plugins/env/providers.ts
208
254
  /**
@@ -271,6 +317,15 @@ function parseDotenv(text) {
271
317
  function dotenv(path = DEFAULT_DOTENV_PATH) {
272
318
  return {
273
319
  name: `dotenv:${path}`,
320
+ /**
321
+ * Reads and parses the dotenv file fresh from disk; `{}` if it is missing.
322
+ *
323
+ * @returns The parsed environment record, or `{}` when the file is absent.
324
+ * @example
325
+ * ```ts
326
+ * dotenv(".env.local").load();
327
+ * ```
328
+ */
274
329
  load() {
275
330
  if (!existsSync(path)) return {};
276
331
  return parseDotenv(readFileSync(path, "utf8"));
@@ -290,24 +345,20 @@ function dotenv(path = DEFAULT_DOTENV_PATH) {
290
345
  function processEnv() {
291
346
  return {
292
347
  name: "process-env",
348
+ /**
349
+ * Returns a shallow copy of `process.env` at call time.
350
+ *
351
+ * @returns A fresh shallow copy of `process.env`.
352
+ * @example
353
+ * ```ts
354
+ * processEnv().load();
355
+ * ```
356
+ */
293
357
  load() {
294
358
  return { ...process.env };
295
359
  }
296
360
  };
297
361
  }
298
-
299
- //#endregion
300
- //#region src/plugins/env/index.ts
301
- /**
302
- * @file Core plugin: universal env injection — schema + providers + PUBLIC_ cross-validation at onInit.
303
- * @see README.md
304
- */
305
- /** Plugin config defaults (R6 typed const). `providers: []` — framework sets `[dotenv(), processEnv()]` via the 4-level cascade. */
306
- const defaultEnvConfig = {
307
- schema: {},
308
- providers: [],
309
- publicPrefix: "PUBLIC_"
310
- };
311
362
  /**
312
363
  * Core plugin that resolves, validates, and freezes the environment at `onInit`,
313
364
  * exposing a read-only accessor at `ctx.env`. No `onStart`/`onStop` — holds no resource.
@@ -318,12 +369,15 @@ const defaultEnvConfig = {
318
369
  * ```
319
370
  */
320
371
  const envPlugin = createCorePlugin("env", {
321
- config: defaultEnvConfig,
372
+ config: {
373
+ schema: {},
374
+ providers: [],
375
+ publicPrefix: "PUBLIC_"
376
+ },
322
377
  createState: createEnvState,
323
378
  api: createEnvApi,
324
379
  onInit: validateSchema
325
380
  });
326
-
327
381
  //#endregion
328
382
  //#region src/plugins/log/expect.ts
329
383
  /**
@@ -434,10 +488,33 @@ function describePartial(partial) {
434
488
  */
435
489
  function createExpectChain(entries) {
436
490
  const chain = {
491
+ /**
492
+ * Assert at least one entry has `event`, optionally matching `partial`.
493
+ *
494
+ * @param event - Event name to find.
495
+ * @param partial - Optional partial data shape (subset-matched).
496
+ * @returns The same chain for chaining.
497
+ * @throws {LogExpectAssertionError} When no matching entry exists.
498
+ * @example
499
+ * ```ts
500
+ * chain.toHaveEvent("build:phase", { status: "start" });
501
+ * ```
502
+ */
437
503
  toHaveEvent(event, partial) {
438
504
  if (!entries.some((entry) => entryMatches(entry, event, partial))) throw new LogExpectAssertionError(`Expected trace to contain event "${event}"${describePartial(partial)}, but none was found.`);
439
505
  return chain;
440
506
  },
507
+ /**
508
+ * Assert all of `events` appear in the trace in the given relative order.
509
+ *
510
+ * @param events - Ordered list of event names (gaps allowed).
511
+ * @returns The same chain for chaining.
512
+ * @throws {LogExpectAssertionError} When the ordering cannot be satisfied.
513
+ * @example
514
+ * ```ts
515
+ * chain.toHaveEventInOrder(["build:phase", "build:complete"]);
516
+ * ```
517
+ */
441
518
  toHaveEventInOrder(events) {
442
519
  let cursor = 0;
443
520
  for (const [position, event] of events.entries()) {
@@ -451,6 +528,18 @@ function createExpectChain(entries) {
451
528
  }
452
529
  return chain;
453
530
  },
531
+ /**
532
+ * Assert NO entry has `event` (optionally narrowed by `partial`).
533
+ *
534
+ * @param event - Event name that must be absent.
535
+ * @param partial - Optional partial data shape; only matching entries violate.
536
+ * @returns The same chain for chaining.
537
+ * @throws {LogExpectAssertionError} When a matching entry exists.
538
+ * @example
539
+ * ```ts
540
+ * chain.toNotHaveEvent("deploy:failed");
541
+ * ```
542
+ */
454
543
  toNotHaveEvent(event, partial) {
455
544
  const offending = entries.findIndex((entry) => entryMatches(entry, event, partial));
456
545
  if (offending !== -1) throw new LogExpectAssertionError(`Expected trace to NOT contain event "${event}"${describePartial(partial)}, but found one at index ${offending}.`);
@@ -459,7 +548,6 @@ function createExpectChain(entries) {
459
548
  };
460
549
  return chain;
461
550
  }
462
-
463
551
  //#endregion
464
552
  //#region src/plugins/log/api.ts
465
553
  /**
@@ -528,33 +616,110 @@ function mergeError(data, error) {
528
616
  function createLogApi(ctx) {
529
617
  const { state } = ctx;
530
618
  return {
619
+ /**
620
+ * Append an `info` entry and fan it out to every sink.
621
+ *
622
+ * @param event - Event identifier (convention: `domain:action`).
623
+ * @param data - Optional structured payload.
624
+ * @example
625
+ * ```ts
626
+ * log.info("content:ready", { count: 12 });
627
+ * ```
628
+ */
531
629
  info(event, data) {
532
630
  append(state, "info", event, data);
533
631
  },
632
+ /**
633
+ * Append a `debug` entry and fan it out to every sink.
634
+ *
635
+ * @param event - Event identifier (convention: `domain:action`).
636
+ * @param data - Optional structured payload.
637
+ * @example
638
+ * ```ts
639
+ * log.debug("router:match", { path: "/blog/" });
640
+ * ```
641
+ */
534
642
  debug(event, data) {
535
643
  append(state, "debug", event, data);
536
644
  },
645
+ /**
646
+ * Append a `warn` entry and fan it out to every sink.
647
+ *
648
+ * @param event - Event identifier (convention: `domain:action`).
649
+ * @param data - Optional structured payload.
650
+ * @example
651
+ * ```ts
652
+ * log.warn("build:skip", { reason: "no sitemap" });
653
+ * ```
654
+ */
537
655
  warn(event, data) {
538
656
  append(state, "warn", event, data);
539
657
  },
658
+ /**
659
+ * Append an `error` entry. When `error` is provided, its `message`/`stack`
660
+ * are merged into `data` under an `error` key (existing keys preserved);
661
+ * otherwise `data` is recorded as-is.
662
+ *
663
+ * @param event - Event identifier (convention: `domain:action`).
664
+ * @param data - Optional structured payload.
665
+ * @param error - Optional originating Error to merge into `data`.
666
+ * @example
667
+ * ```ts
668
+ * log.error("deploy:failed", { target: "cf" }, err);
669
+ * ```
670
+ */
540
671
  error(event, data, error) {
541
672
  append(state, "error", event, error === void 0 ? data : mergeError(data, error));
542
673
  },
674
+ /**
675
+ * Return a frozen snapshot (fresh copy) of the entries recorded so far.
676
+ *
677
+ * @returns A readonly, frozen copy of the recorded entries.
678
+ * @example
679
+ * ```ts
680
+ * const entries = log.trace();
681
+ * ```
682
+ */
543
683
  trace() {
544
684
  return Object.freeze([...state.entries]);
545
685
  },
686
+ /**
687
+ * Return a fluent assertion chain bound to the live entries array.
688
+ *
689
+ * @returns A fresh {@link ExpectChain} reading `state.entries` live.
690
+ * @example
691
+ * ```ts
692
+ * log.expect().toHaveEvent("build:complete");
693
+ * ```
694
+ */
546
695
  expect() {
547
696
  return createExpectChain(state.entries);
548
697
  },
698
+ /**
699
+ * Register an additional output sink at runtime.
700
+ *
701
+ * @param sink - The sink to add to the fan-out list.
702
+ * @example
703
+ * ```ts
704
+ * log.addSink({ write: (e) => stream.write(JSON.stringify(e)) });
705
+ * ```
706
+ */
549
707
  addSink(sink) {
550
708
  state.sinks.push(sink);
551
709
  },
710
+ /**
711
+ * Clear all recorded entries while keeping registered sinks.
712
+ *
713
+ * @example
714
+ * ```ts
715
+ * log.reset();
716
+ * ```
717
+ */
552
718
  reset() {
553
719
  state.entries.length = 0;
554
720
  }
555
721
  };
556
722
  }
557
-
558
723
  //#endregion
559
724
  //#region src/plugins/log/sinks.ts
560
725
  /**
@@ -569,7 +734,17 @@ function createLogApi(ctx) {
569
734
  * ```
570
735
  */
571
736
  function consoleSink() {
572
- return { write(entry) {
737
+ return {
738
+ /**
739
+ * Route a single entry to the console channel matching its level.
740
+ *
741
+ * @param entry - The entry to emit.
742
+ * @example
743
+ * ```ts
744
+ * sink.write({ level: "warn", event: "build:skip", ts: Date.now() });
745
+ * ```
746
+ */
747
+ write(entry) {
573
748
  if (entry.level === "error") console.error(entry);
574
749
  else if (entry.level === "warn") console.warn(entry);
575
750
  else console.log(entry);
@@ -590,7 +765,6 @@ function consoleSink() {
590
765
  function installDefaultSinks(ctx) {
591
766
  if (ctx.config.mode === "dev" || ctx.config.mode === "production") ctx.state.sinks.push(consoleSink());
592
767
  }
593
-
594
768
  //#endregion
595
769
  //#region src/plugins/log/state.ts
596
770
  /**
@@ -611,15 +785,6 @@ function createLogState(_ctx) {
611
785
  sinks: []
612
786
  };
613
787
  }
614
-
615
- //#endregion
616
- //#region src/plugins/log/index.ts
617
- /**
618
- * @file log — Core Plugin (Standard tier): in-memory trace + expect() DSL.
619
- * @see README.md
620
- */
621
- /** Default config; overridden via the 4-level pluginConfigs.log merge. */
622
- const defaultLogConfig = { mode: "production" };
623
788
  /**
624
789
  * Core logging plugin — always-on in-memory trace + `expect()` event-trace DSL.
625
790
  * API injected as `ctx.log` on every regular plugin and surfaced as `app.log`.
@@ -628,21 +793,13 @@ const defaultLogConfig = { mode: "production" };
628
793
  * @see README.md
629
794
  */
630
795
  const logPlugin = createCorePlugin("log", {
631
- config: defaultLogConfig,
796
+ config: { mode: "production" },
632
797
  createState: createLogState,
633
798
  api: createLogApi,
634
799
  onInit: installDefaultSinks
635
800
  });
636
-
637
- //#endregion
638
- //#region src/config.ts
639
- /**
640
- * @file Framework configuration — Config + Events types, core plugin registration.
641
- * @see README.md
642
- */
643
- const defaultConfig$6 = { mode: "production" };
644
801
  const coreConfig = createCoreConfig("web", {
645
- config: defaultConfig$6,
802
+ config: { mode: "production" },
646
803
  plugins: [logPlugin, envPlugin],
647
804
  pluginConfigs: {
648
805
  log: { mode: "production" },
@@ -650,7 +807,6 @@ const coreConfig = createCoreConfig("web", {
650
807
  }
651
808
  });
652
809
  const { createPlugin: createPlugin$1, createCore } = coreConfig;
653
-
654
810
  //#endregion
655
811
  //#region src/plugins/i18n/api.ts
656
812
  /** Error prefix for all i18n lifecycle failures. */
@@ -690,21 +846,83 @@ function validateI18nConfig(ctx) {
690
846
  function createI18nApi(ctx) {
691
847
  const { config } = ctx;
692
848
  return {
849
+ /**
850
+ * Returns the configured supported locales in declared order.
851
+ *
852
+ * @returns The configured `locales` list (priority/display order).
853
+ * @example
854
+ * ```ts
855
+ * api.locales(); // ["en", "uk"]
856
+ * ```
857
+ */
693
858
  locales() {
694
859
  return config.locales;
695
860
  },
861
+ /**
862
+ * Returns the fallback locale used when a requested locale is absent.
863
+ *
864
+ * @returns The configured `defaultLocale`.
865
+ * @example
866
+ * ```ts
867
+ * api.defaultLocale(); // "en"
868
+ * ```
869
+ */
696
870
  defaultLocale() {
697
871
  return config.defaultLocale;
698
872
  },
873
+ /**
874
+ * Membership guard: whether `x` is one of the supported locales
875
+ * (case-sensitive).
876
+ *
877
+ * @param x - Candidate locale code.
878
+ * @returns `true` if `x ∈ locales`, else `false`.
879
+ * @example
880
+ * ```ts
881
+ * api.isLocale("uk"); // true
882
+ * ```
883
+ */
699
884
  isLocale(x) {
700
885
  return config.locales.includes(x);
701
886
  },
887
+ /**
888
+ * Human-readable display name for a locale.
889
+ *
890
+ * @param locale - Locale code to look up.
891
+ * @returns The display name, or `undefined` if unmapped.
892
+ * @example
893
+ * ```ts
894
+ * api.localeName("uk"); // "Українська"
895
+ * ```
896
+ */
702
897
  localeName(locale) {
703
898
  return config.localeNames?.[locale];
704
899
  },
900
+ /**
901
+ * Open Graph `og:locale` value for a locale.
902
+ *
903
+ * @param locale - Locale code to look up.
904
+ * @returns The `og:locale` value (e.g. `"en_US"`), or `undefined` if unmapped.
905
+ * @example
906
+ * ```ts
907
+ * api.ogLocale("en"); // "en_US"
908
+ * ```
909
+ */
705
910
  ogLocale(locale) {
706
911
  return config.ogLocaleMap?.[locale];
707
912
  },
913
+ /**
914
+ * Translate `key` for `locale` with a deterministic fallback chain
915
+ * (requested locale → default locale → the key itself). The default-locale
916
+ * lookup is skipped when `locale === defaultLocale`.
917
+ *
918
+ * @param locale - Requested locale code.
919
+ * @param key - Translation key (e.g. `"nav.home"`).
920
+ * @returns The translated value, the default-locale value, or `key`.
921
+ * @example
922
+ * ```ts
923
+ * api.t("uk", "nav.home"); // "Головна"
924
+ * ```
925
+ */
708
926
  t(locale, key) {
709
927
  const exact = config.translations?.[locale]?.[key];
710
928
  if (exact !== void 0) return exact;
@@ -716,34 +934,17 @@ function createI18nApi(ctx) {
716
934
  }
717
935
  };
718
936
  }
719
-
720
- //#endregion
721
- //#region src/plugins/i18n/index.ts
722
- /**
723
- * i18n — Micro tier. Multi-file layout (index wiring + api.ts + types.ts) so
724
- * index.ts stays within the ≤30-line wiring-only hook; logic lives in api.ts.
725
- *
726
- * Locale registry + flat translation helper with default-locale fallback.
727
- * Pure config-as-data: no state, no events, no lifecycle resources.
728
- * Consumed read-only by content/router/head/build via `ctx.require(i18nPlugin)`.
729
- *
730
- * @file i18n plugin wiring harness.
731
- * @see README.md
732
- */
733
- /** Typed default config (R6: no inline `as`). Optional maps default to `{}` so every lookup is total. */
734
- const defaultConfig$5 = {
735
- locales: ["en"],
736
- defaultLocale: "en",
737
- localeNames: {},
738
- ogLocaleMap: {},
739
- translations: {}
740
- };
741
937
  const i18nPlugin = createPlugin$1("i18n", {
742
- config: defaultConfig$5,
938
+ config: {
939
+ locales: ["en"],
940
+ defaultLocale: "en",
941
+ localeNames: {},
942
+ ogLocaleMap: {},
943
+ translations: {}
944
+ },
743
945
  onInit: validateI18nConfig,
744
946
  api: createI18nApi
745
947
  });
746
-
747
948
  //#endregion
748
949
  //#region src/plugins/content/pipeline/frontmatter.ts
749
950
  /**
@@ -789,7 +990,6 @@ function parseFrontmatter(raw, config) {
789
990
  body: parsed.content
790
991
  };
791
992
  }
792
-
793
993
  //#endregion
794
994
  //#region src/plugins/content/pipeline/plugins.ts
795
995
  /**
@@ -946,7 +1146,6 @@ function defaultRehypePlugins() {
946
1146
  sectionDividerPlugin
947
1147
  ];
948
1148
  }
949
-
950
1149
  //#endregion
951
1150
  //#region src/plugins/content/pipeline/sanitize.ts
952
1151
  /**
@@ -1033,7 +1232,6 @@ function buildSanitizeSchema() {
1033
1232
  }
1034
1233
  };
1035
1234
  }
1036
-
1037
1235
  //#endregion
1038
1236
  //#region src/plugins/content/pipeline/markdown.ts
1039
1237
  /**
@@ -1091,7 +1289,6 @@ function applyPluggable(processor, plugin) {
1091
1289
  }
1092
1290
  processor.use(plugin);
1093
1291
  }
1094
-
1095
1292
  //#endregion
1096
1293
  //#region src/plugins/content/pipeline/reading-time.ts
1097
1294
  /**
@@ -1117,7 +1314,6 @@ function calculateReadingTime(text) {
1117
1314
  wordCount: stats.words
1118
1315
  };
1119
1316
  }
1120
-
1121
1317
  //#endregion
1122
1318
  //#region src/plugins/content/api.ts
1123
1319
  /**
@@ -1329,6 +1525,18 @@ function toCard(article) {
1329
1525
  */
1330
1526
  function createContentApi(ctx) {
1331
1527
  return {
1528
+ /**
1529
+ * Load every article across every active locale, returning a locale-keyed
1530
+ * map of date-descending Article arrays. Lazily builds the processor and
1531
+ * discovers slugs, applies locale fallback, excludes drafts in production,
1532
+ * assigns `contentId` after sorting, then emits `content:ready`.
1533
+ *
1534
+ * @returns A locale-keyed map of date-descending articles.
1535
+ * @example
1536
+ * ```ts
1537
+ * const byLocale = await api.loadAll();
1538
+ * ```
1539
+ */
1332
1540
  async loadAll() {
1333
1541
  const slugs = ctx.state.slugs ?? await discoverSlugs(ctx.config.contentDir);
1334
1542
  ctx.state.slugs = slugs;
@@ -1356,6 +1564,19 @@ function createContentApi(ctx) {
1356
1564
  });
1357
1565
  return result;
1358
1566
  },
1567
+ /**
1568
+ * Resolve and render a single article for one locale with locale fallback.
1569
+ * Throws a `[web] content` error when neither the requested nor the
1570
+ * default-locale file exists.
1571
+ *
1572
+ * @param slug - Article directory name.
1573
+ * @param locale - Requested locale code.
1574
+ * @returns The resolved Article.
1575
+ * @example
1576
+ * ```ts
1577
+ * const article = await api.load("intro", "uk");
1578
+ * ```
1579
+ */
1359
1580
  async load(slug, locale) {
1360
1581
  const article = await resolveArticle(ctx, slug, locale);
1361
1582
  if (article === null) throw new Error(`[web] content article "${slug}" not found for locale "${locale}".\n Looked for ${slug}/${locale}.md and the default-locale fallback.`);
@@ -1364,10 +1585,33 @@ function createContentApi(ctx) {
1364
1585
  ctx.state.articles.set(locale, cache);
1365
1586
  return article;
1366
1587
  },
1588
+ /**
1589
+ * Render a raw Markdown string to HTML through the full pipeline (sanitize
1590
+ * last when `trustedContent` is false). Lazily builds the processor.
1591
+ *
1592
+ * @param md - Raw Markdown source.
1593
+ * @returns The rendered HTML string.
1594
+ * @example
1595
+ * ```ts
1596
+ * const html = await api.renderMarkdown("# Hi");
1597
+ * ```
1598
+ */
1367
1599
  async renderMarkdown(md) {
1368
1600
  const processor = ensureProcessor(ctx.state, ctx.config);
1369
1601
  return String(await processor.process(md));
1370
1602
  },
1603
+ /**
1604
+ * Mark file paths stale for incremental dev rebuilds. Each non-blank path is
1605
+ * added to `dirtyPaths` and its derived slug cache entry is dropped so the
1606
+ * next `loadAll()` re-reads only those files. Empty/whitespace paths are
1607
+ * ignored. Emits `content:invalidated` with the accepted paths.
1608
+ *
1609
+ * @param paths - File paths to invalidate.
1610
+ * @example
1611
+ * ```ts
1612
+ * api.invalidate(["src/content/intro/en.md"]);
1613
+ * ```
1614
+ */
1371
1615
  invalidate(paths) {
1372
1616
  const accepted = [];
1373
1617
  for (const path of paths) {
@@ -1380,12 +1624,22 @@ function createContentApi(ctx) {
1380
1624
  ctx.state.slugs = null;
1381
1625
  ctx.emit("content:invalidated", { paths: accepted });
1382
1626
  },
1627
+ /**
1628
+ * Project a full Article to a lightweight ArticleCard for list/grid
1629
+ * rendering without shipping rendered HTML.
1630
+ *
1631
+ * @param article - The source article.
1632
+ * @returns The card projection.
1633
+ * @example
1634
+ * ```ts
1635
+ * const card = api.articleToCard(article);
1636
+ * ```
1637
+ */
1383
1638
  articleToCard(article) {
1384
1639
  return toCard(article);
1385
1640
  }
1386
1641
  };
1387
1642
  }
1388
-
1389
1643
  //#endregion
1390
1644
  //#region src/plugins/content/config.ts
1391
1645
  /**
@@ -1406,7 +1660,6 @@ const defaultContentConfig = {
1406
1660
  extraRehypePlugins: [],
1407
1661
  shikiTheme: "github-dark"
1408
1662
  };
1409
-
1410
1663
  //#endregion
1411
1664
  //#region src/plugins/content/events.ts
1412
1665
  /**
@@ -1425,7 +1678,6 @@ const contentEvents = (register) => ({
1425
1678
  "content:ready": register("All articles loaded across locales"),
1426
1679
  "content:invalidated": register("Article paths marked stale for dev rebuild")
1427
1680
  });
1428
-
1429
1681
  //#endregion
1430
1682
  //#region src/plugins/content/state.ts
1431
1683
  /**
@@ -1449,7 +1701,6 @@ function createContentState(_ctx) {
1449
1701
  dirtyPaths: /* @__PURE__ */ new Set()
1450
1702
  };
1451
1703
  }
1452
-
1453
1704
  //#endregion
1454
1705
  //#region src/plugins/content/validate.ts
1455
1706
  /**
@@ -1468,7 +1719,6 @@ function validateContentConfig(config) {
1468
1719
  if (typeof config.contentDir !== "string" || config.contentDir.trim() === "") throw new Error("[web] content.contentDir is required.\n Set pluginConfigs.content.contentDir to your content directory.");
1469
1720
  if (typeof config.trustedContent !== "boolean") throw new TypeError("[web] content.trustedContent must be a boolean.");
1470
1721
  }
1471
-
1472
1722
  //#endregion
1473
1723
  //#region src/plugins/content/index.ts
1474
1724
  /**
@@ -1487,7 +1737,6 @@ const contentPlugin = createPlugin$1("content", {
1487
1737
  onInit: (ctx) => validateContentConfig(ctx.config),
1488
1738
  api: contentApi
1489
1739
  });
1490
-
1491
1740
  //#endregion
1492
1741
  //#region src/plugins/site/api.ts
1493
1742
  /** Error prefix for all site lifecycle/validation failures. */
@@ -1579,50 +1828,80 @@ function validateSiteConfig(ctx) {
1579
1828
  function createSiteApi(ctx) {
1580
1829
  const { config } = ctx;
1581
1830
  return {
1831
+ /**
1832
+ * Returns the configured site name.
1833
+ *
1834
+ * @returns The human-readable site name from `config.name`.
1835
+ * @example
1836
+ * ```ts
1837
+ * api.name(); // "My Blog"
1838
+ * ```
1839
+ */
1582
1840
  name() {
1583
1841
  return config.name;
1584
1842
  },
1843
+ /**
1844
+ * Returns the configured absolute base URL of the site.
1845
+ *
1846
+ * @returns The base URL from `config.url`.
1847
+ * @example
1848
+ * ```ts
1849
+ * api.url(); // "https://blog.dev"
1850
+ * ```
1851
+ */
1585
1852
  url() {
1586
1853
  return config.url;
1587
1854
  },
1855
+ /**
1856
+ * Returns the configured site author/byline.
1857
+ *
1858
+ * @returns The author from `config.author`.
1859
+ * @example
1860
+ * ```ts
1861
+ * api.author(); // "Alex"
1862
+ * ```
1863
+ */
1588
1864
  author() {
1589
1865
  return config.author;
1590
1866
  },
1867
+ /**
1868
+ * Returns the configured site description.
1869
+ *
1870
+ * @returns The description from `config.description`.
1871
+ * @example
1872
+ * ```ts
1873
+ * api.description(); // "A personal blog about web frameworks."
1874
+ * ```
1875
+ */
1591
1876
  description() {
1592
1877
  return config.description;
1593
1878
  },
1879
+ /**
1880
+ * Joins a path against the configured base `url` to produce an absolute
1881
+ * canonical URL. An empty path (or "/") returns the base URL unchanged.
1882
+ *
1883
+ * @param path - Relative path for the page, e.g. "/about/".
1884
+ * @returns The absolute canonical URL.
1885
+ * @example
1886
+ * ```ts
1887
+ * api.canonical("/about/"); // "https://blog.dev/about/"
1888
+ * ```
1889
+ */
1594
1890
  canonical(path) {
1595
1891
  return joinCanonical(config.url, path);
1596
1892
  }
1597
1893
  };
1598
1894
  }
1599
-
1600
- //#endregion
1601
- //#region src/plugins/site/index.ts
1602
- /**
1603
- * site — Micro tier. Multi-file layout (index wiring + api.ts + types.ts) so
1604
- * index.ts stays within the ≤30-line wiring-only hook; logic lives in api.ts.
1605
- *
1606
- * Holds global, frozen site metadata (name, url, author, description) and
1607
- * constructs canonical URLs. Consumed by router/head/build via
1608
- * `ctx.require(sitePlugin)`. No events, no dependencies, no state.
1609
- *
1610
- * @file site plugin wiring harness.
1611
- * @see README.md
1612
- */
1613
- /** Typed default config (R6: no inline `as`). Consumers override via `pluginConfigs.site`. */
1614
- const defaultConfig$4 = {
1615
- name: "",
1616
- url: "",
1617
- author: "",
1618
- description: ""
1619
- };
1620
1895
  const sitePlugin = createPlugin$1("site", {
1621
- config: defaultConfig$4,
1896
+ config: {
1897
+ name: "",
1898
+ url: "",
1899
+ author: "",
1900
+ description: ""
1901
+ },
1622
1902
  onInit: validateSiteConfig,
1623
1903
  api: createSiteApi
1624
1904
  });
1625
-
1626
1905
  //#endregion
1627
1906
  //#region src/plugins/router/builders/match.ts
1628
1907
  /**
@@ -1692,7 +1971,6 @@ function matchRoute(compiled, pathname) {
1692
1971
  }
1693
1972
  return null;
1694
1973
  }
1695
-
1696
1974
  //#endregion
1697
1975
  //#region src/plugins/router/api.ts
1698
1976
  /**
@@ -1755,23 +2033,63 @@ function toTypedRoute(entry) {
1755
2033
  function createApi$4(ctx) {
1756
2034
  const { state } = ctx;
1757
2035
  return {
2036
+ /**
2037
+ * Match a pathname against the compiled route table (specificity-sorted).
2038
+ *
2039
+ * @param pathname - URL pathname, e.g. `/en/hello/`.
2040
+ * @returns `{ params, route }` for the most specific match, or `null`.
2041
+ * @example
2042
+ * ```ts
2043
+ * api.match("/en/hello/");
2044
+ * ```
2045
+ */
1758
2046
  match(pathname) {
1759
2047
  return matchRoute(readTable(state).compiled, pathname);
1760
2048
  },
2049
+ /**
2050
+ * Build a URL for a named route from params.
2051
+ *
2052
+ * @param routeName - Route name key from the route map.
2053
+ * @param params - Param values to substitute into the pattern.
2054
+ * @returns The resolved URL string (e.g. `/en/hello/`).
2055
+ * @throws {Error} If `routeName` is unknown.
2056
+ * @example
2057
+ * ```ts
2058
+ * api.toUrl("article", { lang: "en", slug: "hello" });
2059
+ * ```
2060
+ */
1761
2061
  toUrl(routeName, params) {
1762
2062
  const entry = readTable(state).byName.get(routeName);
1763
2063
  if (!entry) throw new Error(`${ERROR_PREFIX$9}: unknown route name "${routeName}".`);
1764
2064
  return entry.toUrl(params);
1765
2065
  },
2066
+ /**
2067
+ * All resolved routes as typed URL utilities, in specificity order.
2068
+ *
2069
+ * @returns A fresh read-only array of resolved typed routes.
2070
+ * @example
2071
+ * ```ts
2072
+ * for (const r of api.entries()) r.toUrl({ slug: "x" });
2073
+ * ```
2074
+ */
1766
2075
  entries() {
1767
2076
  return readTable(state).compiled.map((entry) => toTypedRoute(entry));
1768
2077
  },
2078
+ /**
2079
+ * The typed route set for build-time consumption (declaration order). An API
2080
+ * return, NOT a config readback — preserves per-route types despite erasure.
2081
+ *
2082
+ * @returns A fresh read-only array of the typed route definitions.
2083
+ * @example
2084
+ * ```ts
2085
+ * for (const def of api.manifest()) def._handlers.load?.({}, "en");
2086
+ * ```
2087
+ */
1769
2088
  manifest() {
1770
2089
  return [...readTable(state).byName.values()].map((entry) => entry.definition);
1771
2090
  }
1772
2091
  };
1773
2092
  }
1774
-
1775
2093
  //#endregion
1776
2094
  //#region src/plugins/router/builders/compile.ts
1777
2095
  /** Shared `[web]` error prefix for router validation failures. */
@@ -1933,9 +2251,31 @@ function compileRoute(name, definition, input) {
1933
2251
  dynamicSegmentCount: countDynamicSegments(pattern),
1934
2252
  matchers,
1935
2253
  matchFn: createMatchFunction(matchers, input.defaultLocale),
2254
+ /**
2255
+ * Build a URL for this route from params.
2256
+ *
2257
+ * @param params - Param values to substitute.
2258
+ * @returns The resolved relative URL.
2259
+ * @example
2260
+ * ```ts
2261
+ * entry.toUrl({ slug: "x" });
2262
+ * ```
2263
+ */
1936
2264
  toUrl(params) {
1937
2265
  return buildUrl(pattern, params, input.baseUrl);
1938
2266
  },
2267
+ /**
2268
+ * Build the output file path for this route from params. Honors a custom
2269
+ * `.toFile()` override (captured in `_handlers.toFile`) when present, falling
2270
+ * back to the pattern-derived `…/index.html` path otherwise.
2271
+ *
2272
+ * @param params - Param values to substitute.
2273
+ * @returns The output file path.
2274
+ * @example
2275
+ * ```ts
2276
+ * entry.toFile({ slug: "x" });
2277
+ * ```
2278
+ */
1939
2279
  toFile(params) {
1940
2280
  return definition._handlers.toFile?.(params) ?? buildFilePath(pattern, params);
1941
2281
  },
@@ -1996,7 +2336,6 @@ function buildRouterTable(config, baseUrl, locales, defaultLocale) {
1996
2336
  defaultLocale
1997
2337
  });
1998
2338
  }
1999
-
2000
2339
  //#endregion
2001
2340
  //#region src/plugins/router/builders/route-builder.ts
2002
2341
  /**
@@ -2042,28 +2381,108 @@ function route(pattern) {
2042
2381
  pattern: carrier.pattern,
2043
2382
  _meta: carrier._meta,
2044
2383
  _handlers: carrier._handlers,
2384
+ /**
2385
+ * Attach a data loader; widens the data generic for downstream handlers.
2386
+ *
2387
+ * @param loader - The loader producing this route's data.
2388
+ * @returns The same builder, with the data generic widened.
2389
+ * @example
2390
+ * ```ts
2391
+ * route("/{slug}/").load(({ slug }) => ({ slug }));
2392
+ * ```
2393
+ */
2045
2394
  load(loader) {
2046
2395
  return set("load", loader);
2047
2396
  },
2397
+ /**
2398
+ * Attach a layout wrapper component.
2399
+ *
2400
+ * @param component - The layout component.
2401
+ * @returns The same builder for chaining.
2402
+ * @example
2403
+ * ```ts
2404
+ * route("/").layout((children) => children);
2405
+ * ```
2406
+ */
2048
2407
  layout(component) {
2049
2408
  return set("layout", component);
2050
2409
  },
2410
+ /**
2411
+ * Attach the page render handler.
2412
+ *
2413
+ * @param handler - The render handler.
2414
+ * @returns The same builder for chaining.
2415
+ * @example
2416
+ * ```ts
2417
+ * route("/").render(() => null);
2418
+ * ```
2419
+ */
2051
2420
  render(handler) {
2052
2421
  return set("render", handler);
2053
2422
  },
2423
+ /**
2424
+ * Attach the head/SEO handler.
2425
+ *
2426
+ * @param handler - The head handler.
2427
+ * @returns The same builder for chaining.
2428
+ * @example
2429
+ * ```ts
2430
+ * route("/").head(() => ({ title: "Home" }));
2431
+ * ```
2432
+ */
2054
2433
  head(handler) {
2055
2434
  return set("head", handler);
2056
2435
  },
2436
+ /**
2437
+ * Attach a static-generation param producer.
2438
+ *
2439
+ * @param handler - The param producer.
2440
+ * @returns The same builder for chaining.
2441
+ * @example
2442
+ * ```ts
2443
+ * route("/{slug}/").generate(() => [{ slug: "x" }]);
2444
+ * ```
2445
+ */
2057
2446
  generate(handler) {
2058
2447
  return set("generate", handler);
2059
2448
  },
2449
+ /**
2450
+ * Merge an arbitrary metadata bag into the route's `_meta`.
2451
+ *
2452
+ * @param meta - Metadata to merge.
2453
+ * @returns The same builder for chaining.
2454
+ * @example
2455
+ * ```ts
2456
+ * route("/").meta({ activeTab: "home" });
2457
+ * ```
2458
+ */
2060
2459
  meta(meta) {
2061
2460
  Object.assign(carrier._meta, meta);
2062
2461
  return builder;
2063
2462
  },
2463
+ /**
2464
+ * Attach a JSON serializer for the route's data.
2465
+ *
2466
+ * @param handler - The JSON serializer.
2467
+ * @returns The same builder for chaining.
2468
+ * @example
2469
+ * ```ts
2470
+ * route("/api/").toJson(() => ({ ok: true }));
2471
+ * ```
2472
+ */
2064
2473
  toJson(handler) {
2065
2474
  return set("toJson", handler);
2066
2475
  },
2476
+ /**
2477
+ * Override the output file-path producer.
2478
+ *
2479
+ * @param handler - The file-path producer.
2480
+ * @returns The same builder for chaining.
2481
+ * @example
2482
+ * ```ts
2483
+ * route("/feed/").toFile(() => "feed.xml");
2484
+ * ```
2485
+ */
2067
2486
  toFile(handler) {
2068
2487
  return set("toFile", handler);
2069
2488
  }
@@ -2084,7 +2503,6 @@ function route(pattern) {
2084
2503
  function defineRoutes(routes) {
2085
2504
  return routes;
2086
2505
  }
2087
-
2088
2506
  //#endregion
2089
2507
  //#region src/plugins/router/state.ts
2090
2508
  /**
@@ -2103,25 +2521,16 @@ function defineRoutes(routes) {
2103
2521
  function createState$4(_ctx) {
2104
2522
  return { table: null };
2105
2523
  }
2106
-
2107
- //#endregion
2108
- //#region src/plugins/router/index.ts
2109
- /**
2110
- * @file router — Complex plugin wiring (logic in builders/, api.ts, state.ts).
2111
- * @see README.md
2112
- */
2113
- /** Default router config: empty route map (validated in onInit), hybrid mode. */
2114
- const defaultConfig$3 = {
2115
- routes: {},
2116
- mode: "hybrid"
2117
- };
2118
2524
  const routerPlugin = createPlugin$1("router", {
2119
2525
  depends: [sitePlugin, i18nPlugin],
2120
2526
  helpers: {
2121
2527
  route,
2122
2528
  defineRoutes
2123
2529
  },
2124
- config: defaultConfig$3,
2530
+ config: {
2531
+ routes: {},
2532
+ mode: "hybrid"
2533
+ },
2125
2534
  createState: createState$4,
2126
2535
  api: createApi$4,
2127
2536
  onInit(ctx) {
@@ -2130,7 +2539,6 @@ const routerPlugin = createPlugin$1("router", {
2130
2539
  ctx.state.table = buildRouterTable(ctx.config, baseUrl, i18n.locales(), i18n.defaultLocale());
2131
2540
  }
2132
2541
  });
2133
-
2134
2542
  //#endregion
2135
2543
  //#region src/plugins/head/primitives.ts
2136
2544
  /** OG/Twitter article-meta property prefixes (factored to satisfy no-duplicate-string). */
@@ -2294,7 +2702,6 @@ function buildArticleHead(articleMeta, canonicalUrl) {
2294
2702
  elements.push(jsonLd(ld));
2295
2703
  return elements;
2296
2704
  }
2297
-
2298
2705
  //#endregion
2299
2706
  //#region src/plugins/head/compose.ts
2300
2707
  /**
@@ -2455,7 +2862,6 @@ function serializeElement(element) {
2455
2862
  function serializeHead(elements) {
2456
2863
  return elements.map((element) => serializeElement(element)).join("");
2457
2864
  }
2458
-
2459
2865
  //#endregion
2460
2866
  //#region src/plugins/head/api.ts
2461
2867
  /**
@@ -2497,7 +2903,19 @@ function readDefaults(state) {
2497
2903
  * ```
2498
2904
  */
2499
2905
  function createApi$3(ctx) {
2500
- return { render(route, data) {
2906
+ return {
2907
+ /**
2908
+ * Compose the final `<head>` inner HTML for a route (pulled by `build`).
2909
+ *
2910
+ * @param route - The resolved route descriptor (incl. its `.head()` HeadConfig).
2911
+ * @param data - The page data object passed to the route's loader/render.
2912
+ * @returns The serialized inner HTML of `<head>`.
2913
+ * @example
2914
+ * ```ts
2915
+ * api.render(route, { title: "Post" });
2916
+ * ```
2917
+ */
2918
+ render(route, data) {
2501
2919
  return serializeHead(composeHead({
2502
2920
  route,
2503
2921
  data,
@@ -2508,7 +2926,6 @@ function createApi$3(ctx) {
2508
2926
  }));
2509
2927
  } };
2510
2928
  }
2511
-
2512
2929
  //#endregion
2513
2930
  //#region src/plugins/head/config.ts
2514
2931
  /** Error prefix for all head config-validation failures. */
@@ -2563,7 +2980,6 @@ function normalizeHeadConfig(config) {
2563
2980
  if (config.twitterHandle !== void 0) defaults.twitterHandle = config.twitterHandle;
2564
2981
  return Object.freeze(defaults);
2565
2982
  }
2566
-
2567
2983
  //#endregion
2568
2984
  //#region src/plugins/head/helpers.ts
2569
2985
  /**
@@ -2592,7 +3008,6 @@ const headHelpers = {
2592
3008
  feedLink,
2593
3009
  buildArticleHead
2594
3010
  };
2595
-
2596
3011
  //#endregion
2597
3012
  //#region src/plugins/head/state.ts
2598
3013
  /**
@@ -2613,7 +3028,6 @@ const headHelpers = {
2613
3028
  function createState$3(_ctx) {
2614
3029
  return { defaults: null };
2615
3030
  }
2616
-
2617
3031
  //#endregion
2618
3032
  //#region src/plugins/head/index.ts
2619
3033
  /**
@@ -2634,7 +3048,6 @@ const headPlugin = createPlugin$1("head", {
2634
3048
  ctx.state.defaults = normalizeHeadConfig(ctx.config);
2635
3049
  }
2636
3050
  });
2637
-
2638
3051
  //#endregion
2639
3052
  //#region src/plugins/build/phases/bundle.ts
2640
3053
  /**
@@ -2736,7 +3149,6 @@ async function bundle(ctx, options = {}) {
2736
3149
  await runOne(ctx, runner, "css", cssEntrypoints, path.join(outDir, "assets"), minify);
2737
3150
  await runOne(ctx, runner, "js", jsEntrypoints, path.join(outDir, "assets"), minify);
2738
3151
  }
2739
-
2740
3152
  //#endregion
2741
3153
  //#region src/plugins/build/phases/content.ts
2742
3154
  /**
@@ -2779,7 +3191,6 @@ function readCachedContent(ctx) {
2779
3191
  const cached = ctx.state.buildCache.get(CONTENT_CACHE_KEY);
2780
3192
  return cached instanceof Map ? cached : /* @__PURE__ */ new Map();
2781
3193
  }
2782
-
2783
3194
  //#endregion
2784
3195
  //#region src/plugins/build/phases/feeds.ts
2785
3196
  /**
@@ -2861,7 +3272,6 @@ async function generateFeeds(ctx) {
2861
3272
  ctx.log.debug("build:feeds", { items: guids.length });
2862
3273
  return result;
2863
3274
  }
2864
-
2865
3275
  //#endregion
2866
3276
  //#region src/plugins/build/phases/images.ts
2867
3277
  /**
@@ -2901,7 +3311,6 @@ async function processImages(ctx, options = {}) {
2901
3311
  ctx.log.debug("build:images", { copied });
2902
3312
  return copied;
2903
3313
  }
2904
-
2905
3314
  //#endregion
2906
3315
  //#region src/plugins/build/phases/og-images.tsx
2907
3316
  /**
@@ -2915,8 +3324,6 @@ const DEFAULT_SIZE = {
2915
3324
  width: 1200,
2916
3325
  height: 630
2917
3326
  };
2918
- /** The fixed concurrency bound for the OG render pool. */
2919
- const OG_CONCURRENCY = 4;
2920
3327
  /** Recognized font file extensions. */
2921
3328
  const FONT_EXTENSIONS$1 = [
2922
3329
  ".ttf",
@@ -3037,7 +3444,7 @@ async function generateOgImages(ctx, options = {}) {
3037
3444
  const articles = selectArticles(readCachedContent(ctx));
3038
3445
  const cache = ctx.state.ogImageHashCache;
3039
3446
  await loadDiskCache(ctx.config.outDir, cache);
3040
- const limit = pLimit(OG_CONCURRENCY);
3447
+ const limit = pLimit(4);
3041
3448
  let active = 0;
3042
3449
  let peakConcurrency = 0;
3043
3450
  let rendered = 0;
@@ -3110,7 +3517,6 @@ async function persistDiskCache(outDir, cache) {
3110
3517
  await mkdir(dir, { recursive: true });
3111
3518
  await writeFile(path.join(dir, "og-images.json"), JSON.stringify(Object.fromEntries(cache)), "utf8");
3112
3519
  }
3113
-
3114
3520
  //#endregion
3115
3521
  //#region src/plugins/build/phases/pages.tsx
3116
3522
  /**
@@ -3278,7 +3684,6 @@ async function renderPages(ctx) {
3278
3684
  rootHtml: root?.html ?? null
3279
3685
  };
3280
3686
  }
3281
-
3282
3687
  //#endregion
3283
3688
  //#region src/plugins/build/phases/sitemap.ts
3284
3689
  /**
@@ -3360,7 +3765,6 @@ async function generateSitemap(ctx) {
3360
3765
  robots
3361
3766
  };
3362
3767
  }
3363
-
3364
3768
  //#endregion
3365
3769
  //#region src/plugins/build/pipeline.ts
3366
3770
  /**
@@ -3491,7 +3895,6 @@ async function runPipeline(ctx, options) {
3491
3895
  phaseContext.emit("build:complete", result);
3492
3896
  return result;
3493
3897
  }
3494
-
3495
3898
  //#endregion
3496
3899
  //#region src/plugins/build/api.ts
3497
3900
  /**
@@ -3530,9 +3933,29 @@ const defaultConfig$1 = {
3530
3933
  */
3531
3934
  function createApi$2(ctx) {
3532
3935
  return {
3936
+ /**
3937
+ * Run the full SSG pipeline and write the site to disk.
3938
+ *
3939
+ * @param options - Optional run overrides.
3940
+ * @param options.outDir - Override the configured output directory for this run.
3941
+ * @returns The build result (outDir, pageCount, durationMs).
3942
+ * @example
3943
+ * ```ts
3944
+ * await api.run({ outDir: "./preview" });
3945
+ * ```
3946
+ */
3533
3947
  run(options) {
3534
3948
  return runPipeline(ctx, options);
3535
3949
  },
3950
+ /**
3951
+ * List the phases in execution order (introspection / tooling).
3952
+ *
3953
+ * @returns A fresh array of the static ordered phase names.
3954
+ * @example
3955
+ * ```ts
3956
+ * api.phases();
3957
+ * ```
3958
+ */
3536
3959
  phases() {
3537
3960
  return [...PHASE_ORDER];
3538
3961
  }
@@ -3567,7 +3990,6 @@ function validateConfig$1(config) {
3567
3990
  if (typeof config.outDir !== "string" || config.outDir.trim().length === 0) throw new Error(`${ERROR_PREFIX$5}.outDir: must be a non-empty string.`);
3568
3991
  if (config.ogImage) validateFonts(config.ogImage);
3569
3992
  }
3570
-
3571
3993
  //#endregion
3572
3994
  //#region src/plugins/build/events.ts
3573
3995
  /**
@@ -3587,7 +4009,6 @@ function createEvents(register) {
3587
4009
  "build:complete": register("Emitted once after a successful build run")
3588
4010
  };
3589
4011
  }
3590
-
3591
4012
  //#endregion
3592
4013
  //#region src/plugins/build/state.ts
3593
4014
  /**
@@ -3614,7 +4035,6 @@ function createState$2(ctx) {
3614
4035
  ogImageHashCache: /* @__PURE__ */ new Map()
3615
4036
  };
3616
4037
  }
3617
-
3618
4038
  //#endregion
3619
4039
  //#region src/plugins/build/index.ts
3620
4040
  /**
@@ -3635,7 +4055,6 @@ const buildPlugin = createPlugin$1("build", {
3635
4055
  api: createApi$2,
3636
4056
  onInit: (ctx) => validateConfig$1(ctx.config)
3637
4057
  });
3638
-
3639
4058
  //#endregion
3640
4059
  //#region src/plugins/deploy/wrangler.ts
3641
4060
  /**
@@ -3813,8 +4232,6 @@ const ERROR_SIGNATURES = [
3813
4232
  advice: "A network failure occurred. Check connectivity and retry."
3814
4233
  }
3815
4234
  ];
3816
- /** Number of trailing characters of scrubbed stderr to surface on an unknown failure. */
3817
- const STDERR_TAIL_LENGTH = 500;
3818
4235
  /**
3819
4236
  * Map a non-zero wrangler exit and scrubbed stderr to an actionable error
3820
4237
  * `code` + message. Matching is case-insensitive against the scrubbed stderr;
@@ -3835,7 +4252,7 @@ function classifyWranglerError(exitCode, scrubbedStderr) {
3835
4252
  };
3836
4253
  return {
3837
4254
  code: "ERR_DEPLOY_WRANGLER_FAILED",
3838
- message: `${ERROR_PREFIX$4}: wrangler failed (exit ${exitCode}).\n ${scrubbedStderr.trim().slice(-STDERR_TAIL_LENGTH)}`
4255
+ message: `${ERROR_PREFIX$4}: wrangler failed (exit ${exitCode}).\n ${scrubbedStderr.trim().slice(-500)}`
3839
4256
  };
3840
4257
  }
3841
4258
  /**
@@ -3894,7 +4311,6 @@ async function runWrangler(input) {
3894
4311
  exitCode
3895
4312
  };
3896
4313
  }
3897
-
3898
4314
  //#endregion
3899
4315
  //#region src/plugins/deploy/generators/github-workflow.ts
3900
4316
  /**
@@ -3949,7 +4365,6 @@ jobs:
3949
4365
  command: pages deploy dist --project-name ${input.slug}
3950
4366
  `;
3951
4367
  }
3952
-
3953
4368
  //#endregion
3954
4369
  //#region src/plugins/deploy/generators/wrangler-config.ts
3955
4370
  /**
@@ -3993,7 +4408,6 @@ async function readWranglerConfig(cwd) {
3993
4408
  return null;
3994
4409
  }
3995
4410
  }
3996
-
3997
4411
  //#endregion
3998
4412
  //#region src/plugins/deploy/init.ts
3999
4413
  /**
@@ -4097,7 +4511,6 @@ async function reconcile(input) {
4097
4511
  await writeFile(path.join(cwd, relativePath), expected, "utf8");
4098
4512
  result.written.push(relativePath);
4099
4513
  }
4100
-
4101
4514
  //#endregion
4102
4515
  //#region src/plugins/deploy/preflight.ts
4103
4516
  /**
@@ -4191,7 +4604,6 @@ async function runPreflight(config, root, env = process.env) {
4191
4604
  if (stats.fileCount > limit) throw deployError("ERR_DEPLOY_TOO_MANY_FILES", `${ERROR_PREFIX$3}: outDir contains ${stats.fileCount} files; the limit is ${limit}.\n Raise it with ${MAX_FILES_ENV} (paid tier) or reduce the output.`);
4192
4605
  if (stats.oversizePath !== null) throw deployError("ERR_DEPLOY_FILE_TOO_LARGE", `${ERROR_PREFIX$3}: file ${JSON.stringify(stats.oversizePath)} exceeds the 25 MiB per-file limit.`);
4193
4606
  }
4194
-
4195
4607
  //#endregion
4196
4608
  //#region src/plugins/deploy/slug.ts
4197
4609
  /**
@@ -4232,7 +4644,6 @@ function toSlug(name) {
4232
4644
  slug = slug.slice(0, end);
4233
4645
  return slug.length > 0 ? slug : FALLBACK_SLUG;
4234
4646
  }
4235
-
4236
4647
  //#endregion
4237
4648
  //#region src/plugins/deploy/api.ts
4238
4649
  /** Error prefix for deploy config/validation failures (spec/11 Part-3). */
@@ -4272,6 +4683,15 @@ function validateConfig(ctx) {
4272
4683
  */
4273
4684
  function createApi$1(ctx) {
4274
4685
  return {
4686
+ /**
4687
+ * Deploy the built outDir to Cloudflare Pages via the wrangler subprocess.
4688
+ *
4689
+ * @param options - Optional branch override and build toggle.
4690
+ * @returns The deploy result (url, deploymentId, branch, durationMs).
4691
+ * @throws {Error} With a `code` from the deploy error taxonomy on any failure.
4692
+ * @example
4693
+ * await api.run();
4694
+ */
4275
4695
  async run(options = {}) {
4276
4696
  const root = process.cwd();
4277
4697
  const slug = toSlug(ctx.require(sitePlugin).name());
@@ -4311,10 +4731,25 @@ function createApi$1(ctx) {
4311
4731
  });
4312
4732
  return result;
4313
4733
  },
4734
+ /**
4735
+ * Return the most recent successful deploy result, or null if none occurred.
4736
+ *
4737
+ * @returns A frozen snapshot of the last DeployResult, or null.
4738
+ * @example
4739
+ * const last = api.getLastDeployment();
4740
+ */
4314
4741
  getLastDeployment() {
4315
4742
  const last = ctx.state.lastDeployment;
4316
4743
  return last ? Object.freeze({ ...last }) : null;
4317
4744
  },
4745
+ /**
4746
+ * Generate deploy scaffolding (wrangler.jsonc + optional GitHub workflow).
4747
+ *
4748
+ * @param options - Optional ci toggle and check (drift-only) mode.
4749
+ * @returns Which files were written, skipped, or would drift.
4750
+ * @example
4751
+ * await api.init({ ci: true });
4752
+ */
4318
4753
  async init(options = {}) {
4319
4754
  const slug = toSlug(ctx.require(sitePlugin).name());
4320
4755
  return writeScaffolding({
@@ -4326,7 +4761,6 @@ function createApi$1(ctx) {
4326
4761
  }
4327
4762
  };
4328
4763
  }
4329
-
4330
4764
  //#endregion
4331
4765
  //#region src/plugins/deploy/defaults.ts
4332
4766
  /**
@@ -4346,7 +4780,6 @@ const defaultConfig = {
4346
4780
  compatibilityDate: "2024-01-01",
4347
4781
  ci: false
4348
4782
  };
4349
-
4350
4783
  //#endregion
4351
4784
  //#region src/plugins/deploy/events.ts
4352
4785
  /**
@@ -4361,7 +4794,6 @@ const defaultConfig = {
4361
4794
  * ```
4362
4795
  */
4363
4796
  const deployEvents = (register) => ({ "deploy:complete": register("Deployment completed successfully") });
4364
-
4365
4797
  //#endregion
4366
4798
  //#region src/plugins/deploy/state.ts
4367
4799
  /**
@@ -4399,7 +4831,6 @@ function createState$1(_ctx) {
4399
4831
  spawn: defaultSpawn
4400
4832
  };
4401
4833
  }
4402
-
4403
4834
  //#endregion
4404
4835
  //#region src/plugins/deploy/index.ts
4405
4836
  /**
@@ -4417,7 +4848,6 @@ const deployPlugin = createPlugin$1("deploy", {
4417
4848
  onInit: validateConfig,
4418
4849
  api: createApi$1
4419
4850
  });
4420
-
4421
4851
  //#endregion
4422
4852
  //#region src/plugins/spa/api.ts
4423
4853
  /**
@@ -4432,19 +4862,39 @@ const deployPlugin = createPlugin$1("deploy", {
4432
4862
  */
4433
4863
  function createApi(ctx) {
4434
4864
  return {
4865
+ /**
4866
+ * Register a component definition (last-registered-wins); warns on collision.
4867
+ *
4868
+ * @param component - The component definition created via `createComponent`.
4869
+ * @example
4870
+ * app.spa.register(counter);
4871
+ */
4435
4872
  register(component) {
4436
4873
  if (ctx.state.registeredComponents.has(component.name)) ctx.log.warn("spa:component-collision", { name: component.name });
4437
4874
  ctx.state.kernel?.register(component);
4438
4875
  },
4876
+ /**
4877
+ * Programmatically navigate to a path (client runtime; no-op without a DOM).
4878
+ *
4879
+ * @param path - Target path (pathname, optionally with search/hash).
4880
+ * @example
4881
+ * app.spa.navigate("/about");
4882
+ */
4439
4883
  navigate(path) {
4440
4884
  ctx.state.kernel?.processNav(path);
4441
4885
  },
4886
+ /**
4887
+ * Read the current resolved URL.
4888
+ *
4889
+ * @returns The current pathname + search.
4890
+ * @example
4891
+ * app.spa.current();
4892
+ */
4442
4893
  current() {
4443
4894
  return ctx.state.currentUrl;
4444
4895
  }
4445
4896
  };
4446
4897
  }
4447
-
4448
4898
  //#endregion
4449
4899
  //#region src/plugins/spa/events.ts
4450
4900
  /**
@@ -4464,7 +4914,6 @@ function spaEvents(register) {
4464
4914
  "spa:component-unmount": register("A component instance detached from an element.")
4465
4915
  };
4466
4916
  }
4467
-
4468
4917
  //#endregion
4469
4918
  //#region src/plugins/spa/types.ts
4470
4919
  var types_exports$7 = /* @__PURE__ */ __exportAll({ COMPONENT_HOOK_NAMES: () => COMPONENT_HOOK_NAMES });
@@ -4477,11 +4926,7 @@ const COMPONENT_HOOK_NAMES = [
4477
4926
  "onUnMount",
4478
4927
  "onDestroy"
4479
4928
  ];
4480
-
4481
- //#endregion
4482
- //#region src/plugins/spa/components.ts
4483
- /** The set of legal hook names, frozen for O(1) membership checks. */
4484
- const HOOK_NAME_SET = new Set(COMPONENT_HOOK_NAMES);
4929
+ new Set(COMPONENT_HOOK_NAMES);
4485
4930
  /**
4486
4931
  * Extracts the page data payload from the inline `script#__DATA__` element.
4487
4932
  * Returns an empty object when the script is absent, empty, or invalid JSON.
@@ -4649,7 +5094,6 @@ function notifyNavEnd(state) {
4649
5094
  const data = typeof document === "undefined" ? {} : extractPageData(document);
4650
5095
  for (const [element, instance] of state.instances) if (instance.persistent) runHook(instance, "onNavEnd", makeContext(element, data));
4651
5096
  }
4652
-
4653
5097
  //#endregion
4654
5098
  //#region src/plugins/spa/head.ts
4655
5099
  /** Single-element head selectors synced by replace/append/remove on navigation. */
@@ -4724,7 +5168,6 @@ function syncHead(_head, doc) {
4724
5168
  for (const selector of META_SELECTORS) syncElement(selector, doc);
4725
5169
  for (const selector of REPLACE_ALL_SELECTORS) replaceAllBySelector(selector, doc);
4726
5170
  }
4727
-
4728
5171
  //#endregion
4729
5172
  //#region src/plugins/spa/progress.ts
4730
5173
  /** Delay before the bar appears, so fast navigations show no indicator. */
@@ -4808,7 +5251,6 @@ function createProgressBar(enabled) {
4808
5251
  done
4809
5252
  };
4810
5253
  }
4811
-
4812
5254
  //#endregion
4813
5255
  //#region src/plugins/spa/router.ts
4814
5256
  /**
@@ -5043,7 +5485,6 @@ function attachRouter(handlers) {
5043
5485
  const navigation = getNavigation();
5044
5486
  return navigation ? attachNavigationApi(navigation, handlers) : attachHistoryFallback(handlers);
5045
5487
  }
5046
-
5047
5488
  //#endregion
5048
5489
  //#region src/plugins/spa/state.ts
5049
5490
  /** Error prefix for spa config-validation failures (spec/11 Part-3). */
@@ -5117,7 +5558,6 @@ function createState(_ctx) {
5117
5558
  kernel: null
5118
5559
  };
5119
5560
  }
5120
-
5121
5561
  //#endregion
5122
5562
  //#region src/plugins/spa/kernel.ts
5123
5563
  /**
@@ -5226,10 +5666,22 @@ function createSpaKernel(state, config, emit, deps) {
5226
5666
  onError: handleError
5227
5667
  };
5228
5668
  return {
5669
+ /**
5670
+ * Register config components and seed currentUrl from the document.
5671
+ *
5672
+ * @example
5673
+ * kernel.init();
5674
+ */
5229
5675
  init() {
5230
5676
  for (const component of resolved.components) registerComponent(state, component);
5231
5677
  state.currentUrl = currentLocationUrl();
5232
5678
  },
5679
+ /**
5680
+ * Boot navigation interception + initial scan (throws if already started).
5681
+ *
5682
+ * @example
5683
+ * kernel.boot();
5684
+ */
5233
5685
  boot() {
5234
5686
  if (typeof document === "undefined") return;
5235
5687
  if (state.started) throw new Error(`${ERROR_PREFIX} spa kernel already started.\n Call app.stop() before booting again (single boot per app).`);
@@ -5239,16 +5691,42 @@ function createSpaKernel(state, config, emit, deps) {
5239
5691
  scanAndMount(state, emit, resolved.swapSelector);
5240
5692
  state.started = true;
5241
5693
  },
5694
+ /**
5695
+ * Register a component definition (last-registered-wins).
5696
+ *
5697
+ * @param component - The component definition to register.
5698
+ * @example
5699
+ * kernel.register(counter);
5700
+ */
5242
5701
  register(component) {
5243
5702
  registerComponent(state, component);
5244
5703
  },
5704
+ /**
5705
+ * Process a navigation to `path` (fetch then swap; full reload on error).
5706
+ *
5707
+ * @param path - The target path to navigate to.
5708
+ * @example
5709
+ * kernel.processNav("/about");
5710
+ */
5245
5711
  processNav(path) {
5246
5712
  if (typeof document === "undefined") return;
5247
5713
  performNavigation(path, handlers).catch(() => {});
5248
5714
  },
5715
+ /**
5716
+ * Scan the swap region and mount components for matching elements.
5717
+ *
5718
+ * @example
5719
+ * kernel.scan();
5720
+ */
5249
5721
  scan() {
5250
5722
  scanAndMount(state, emit, resolved.swapSelector);
5251
5723
  },
5724
+ /**
5725
+ * Tear down router listeners, dispose all instances, reset boot state.
5726
+ *
5727
+ * @example
5728
+ * kernel.dispose();
5729
+ */
5252
5730
  dispose() {
5253
5731
  state.destroyRouter?.();
5254
5732
  state.destroyRouter = null;
@@ -5277,7 +5755,6 @@ function initSpa(ctx) {
5277
5755
  kernelRef.current = kernel;
5278
5756
  kernel.init();
5279
5757
  }
5280
-
5281
5758
  //#endregion
5282
5759
  //#region src/plugins/spa/lifecycle.ts
5283
5760
  /** Router/instance teardown captured during onStart (undefined when stopped). */
@@ -5325,7 +5802,6 @@ function disposeSpa() {
5325
5802
  logRef = void 0;
5326
5803
  }
5327
5804
  }
5328
-
5329
5805
  //#endregion
5330
5806
  //#region src/plugins/spa/index.ts
5331
5807
  /**
@@ -5349,42 +5825,28 @@ const spaPlugin = createPlugin$1("spa", {
5349
5825
  },
5350
5826
  onStop: disposeSpa
5351
5827
  });
5352
-
5353
5828
  //#endregion
5354
5829
  //#region src/plugins/build/types.ts
5355
5830
  var types_exports = /* @__PURE__ */ __exportAll({});
5356
-
5357
5831
  //#endregion
5358
5832
  //#region src/plugins/content/types.ts
5359
5833
  var types_exports$1 = /* @__PURE__ */ __exportAll({});
5360
-
5361
5834
  //#endregion
5362
5835
  //#region src/plugins/deploy/types.ts
5363
5836
  var types_exports$2 = /* @__PURE__ */ __exportAll({});
5364
-
5365
5837
  //#endregion
5366
5838
  //#region src/plugins/env/types.ts
5367
5839
  var types_exports$3 = /* @__PURE__ */ __exportAll({});
5368
-
5369
5840
  //#endregion
5370
5841
  //#region src/plugins/head/types.ts
5371
5842
  var types_exports$4 = /* @__PURE__ */ __exportAll({});
5372
-
5373
5843
  //#endregion
5374
5844
  //#region src/plugins/log/types.ts
5375
5845
  var types_exports$5 = /* @__PURE__ */ __exportAll({});
5376
-
5377
5846
  //#endregion
5378
5847
  //#region src/plugins/router/types.ts
5379
5848
  var types_exports$6 = /* @__PURE__ */ __exportAll({});
5380
-
5381
- //#endregion
5382
- //#region src/index.ts
5383
- /**
5384
- * @file `@moku-labs/web` — a Moku Layer-2 content static-site + SPA framework.
5385
- * @see README.md
5386
- */
5387
- const framework = createCore(coreConfig, {
5849
+ const { createApp, createPlugin } = createCore(coreConfig, {
5388
5850
  plugins: [
5389
5851
  sitePlugin,
5390
5852
  i18nPlugin,
@@ -5397,7 +5859,5 @@ const framework = createCore(coreConfig, {
5397
5859
  ],
5398
5860
  pluginConfigs: {}
5399
5861
  });
5400
- const { createApp, createPlugin } = framework;
5401
-
5402
5862
  //#endregion
5403
- export { types_exports as Build, types_exports$1 as Content, types_exports$2 as Deploy, types_exports$3 as Env, types_exports$4 as Head, types_exports$5 as Log, types_exports$6 as Router, types_exports$7 as Spa, buildArticleHead, buildPlugin, canonical, contentPlugin, createApp, createPlugin, defineRoutes, deployPlugin, envPlugin, feedLink, headPlugin, hreflang, i18nPlugin, jsonLd, logPlugin, meta, og, route, routerPlugin, sitePlugin, spaPlugin, twitter };
5863
+ export { types_exports as Build, types_exports$1 as Content, types_exports$2 as Deploy, types_exports$3 as Env, types_exports$4 as Head, types_exports$5 as Log, types_exports$6 as Router, types_exports$7 as Spa, buildArticleHead, buildPlugin, canonical, contentPlugin, createApp, createPlugin, defineRoutes, deployPlugin, envPlugin, feedLink, headPlugin, hreflang, i18nPlugin, jsonLd, logPlugin, meta, og, route, routerPlugin, sitePlugin, spaPlugin, twitter };