@moku-labs/web 0.2.0 → 0.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.cjs CHANGED
@@ -1,4 +1,4 @@
1
- Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  //#region \0rolldown/runtime.js
3
3
  var __create = Object.create;
4
4
  var __defProp = Object.defineProperty;
@@ -8,28 +8,20 @@ var __getProtoOf = Object.getPrototypeOf;
8
8
  var __hasOwnProp = Object.prototype.hasOwnProperty;
9
9
  var __exportAll = (all, no_symbols) => {
10
10
  let target = {};
11
- for (var name in all) {
12
- __defProp(target, name, {
13
- get: all[name],
14
- enumerable: true
15
- });
16
- }
17
- if (!no_symbols) {
18
- __defProp(target, Symbol.toStringTag, { value: "Module" });
19
- }
11
+ for (var name in all) __defProp(target, name, {
12
+ get: all[name],
13
+ enumerable: true
14
+ });
15
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
20
16
  return target;
21
17
  };
22
18
  var __copyProps = (to, from, except, desc) => {
23
- if (from && typeof from === "object" || typeof from === "function") {
24
- for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
25
- key = keys[i];
26
- if (!__hasOwnProp.call(to, key) && key !== except) {
27
- __defProp(to, key, {
28
- get: ((k) => from[k]).bind(null, key),
29
- enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
30
- });
31
- }
32
- }
19
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
20
+ key = keys[i];
21
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
22
+ get: ((k) => from[k]).bind(null, key),
23
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
24
+ });
33
25
  }
34
26
  return to;
35
27
  };
@@ -37,38 +29,38 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
37
29
  value: mod,
38
30
  enumerable: true
39
31
  }) : target, mod));
40
-
41
32
  //#endregion
42
33
  let _moku_labs_core = require("@moku-labs/core");
43
34
  let node_fs = require("node:fs");
44
35
  let node_fs_promises = require("node:fs/promises");
45
36
  let node_path = require("node:path");
37
+ let node_path$1 = __toESM(node_path, 1);
46
38
  node_path = __toESM(node_path);
47
39
  let gray_matter = require("gray-matter");
48
- gray_matter = __toESM(gray_matter);
40
+ gray_matter = __toESM(gray_matter, 1);
49
41
  let _shikijs_rehype = require("@shikijs/rehype");
50
- _shikijs_rehype = __toESM(_shikijs_rehype);
42
+ _shikijs_rehype = __toESM(_shikijs_rehype, 1);
51
43
  let rehype_sanitize = require("rehype-sanitize");
52
- rehype_sanitize = __toESM(rehype_sanitize);
44
+ rehype_sanitize = __toESM(rehype_sanitize, 1);
53
45
  let rehype_stringify = require("rehype-stringify");
54
- rehype_stringify = __toESM(rehype_stringify);
46
+ rehype_stringify = __toESM(rehype_stringify, 1);
55
47
  let unified = require("unified");
56
48
  let rehype_raw = require("rehype-raw");
57
- rehype_raw = __toESM(rehype_raw);
49
+ rehype_raw = __toESM(rehype_raw, 1);
58
50
  let remark_directive = require("remark-directive");
59
- remark_directive = __toESM(remark_directive);
51
+ remark_directive = __toESM(remark_directive, 1);
60
52
  let remark_frontmatter = require("remark-frontmatter");
61
- remark_frontmatter = __toESM(remark_frontmatter);
53
+ remark_frontmatter = __toESM(remark_frontmatter, 1);
62
54
  let remark_gfm = require("remark-gfm");
63
- remark_gfm = __toESM(remark_gfm);
55
+ remark_gfm = __toESM(remark_gfm, 1);
64
56
  let remark_parse = require("remark-parse");
65
- remark_parse = __toESM(remark_parse);
57
+ remark_parse = __toESM(remark_parse, 1);
66
58
  let remark_rehype = require("remark-rehype");
67
- remark_rehype = __toESM(remark_rehype);
59
+ remark_rehype = __toESM(remark_rehype, 1);
68
60
  let unist_util_visit = require("unist-util-visit");
69
61
  let hast_util_sanitize = require("hast-util-sanitize");
70
62
  let reading_time = require("reading-time");
71
- reading_time = __toESM(reading_time);
63
+ reading_time = __toESM(reading_time, 1);
72
64
  let node_crypto = require("node:crypto");
73
65
  let feed = require("feed");
74
66
  let _resvg_resvg_js = require("@resvg/resvg-js");
@@ -76,7 +68,6 @@ let satori = require("satori");
76
68
  satori = __toESM(satori);
77
69
  let preact_jsx_runtime = require("preact/jsx-runtime");
78
70
  let preact_render_to_string = require("preact-render-to-string");
79
-
80
71
  //#region src/plugins/env/api.ts
81
72
  /** Error prefix for all env API failures. */
82
73
  const ERROR_PREFIX$13 = "[web]";
@@ -97,26 +88,75 @@ const ERROR_PREFIX$13 = "[web]";
97
88
  function createEnvApi(ctx) {
98
89
  const { resolved, publicMap } = ctx.state;
99
90
  return {
91
+ /**
92
+ * Reads a resolved variable.
93
+ *
94
+ * @param key - Variable name.
95
+ * @returns The value, or `undefined` if not present.
96
+ * @example
97
+ * ```ts
98
+ * api.get("PUBLIC_API_URL");
99
+ * ```
100
+ */
100
101
  get(key) {
101
102
  return resolved.get(key);
102
103
  },
104
+ /**
105
+ * Reads a variable that must exist.
106
+ *
107
+ * @param key - Variable name.
108
+ * @returns The value.
109
+ * @throws {Error} If the variable is undefined.
110
+ * @example
111
+ * ```ts
112
+ * api.require("DEPLOY_TOKEN");
113
+ * ```
114
+ */
103
115
  require(key) {
104
116
  const value = resolved.get(key);
105
117
  if (value === void 0) throw new Error(`${ERROR_PREFIX$13} env: required variable "${key}" is not defined.`);
106
118
  return value;
107
119
  },
120
+ /**
121
+ * Tests presence of a resolved variable.
122
+ *
123
+ * @param key - Variable name.
124
+ * @returns `true` if a value is present.
125
+ * @example
126
+ * ```ts
127
+ * api.has("PUBLIC_API_URL");
128
+ * ```
129
+ */
108
130
  has(key) {
109
131
  return resolved.has(key);
110
132
  },
133
+ /**
134
+ * Returns all public variables as a frozen plain object — a fresh copy,
135
+ * never the raw state map.
136
+ *
137
+ * @returns A frozen `Record` of public variable names to values.
138
+ * @example
139
+ * ```ts
140
+ * const payload = { ...api.getPublic() };
141
+ * ```
142
+ */
111
143
  getPublic() {
112
144
  return Object.freeze(Object.fromEntries(publicMap));
113
145
  },
146
+ /**
147
+ * Returns the already-frozen map of public variables.
148
+ *
149
+ * @returns The frozen public map.
150
+ * @example
151
+ * ```ts
152
+ * [...api.getPublicMap()];
153
+ * ```
154
+ */
114
155
  getPublicMap() {
115
156
  return publicMap;
116
157
  }
117
158
  };
118
159
  }
119
-
120
160
  //#endregion
121
161
  //#region src/plugins/env/state.ts
122
162
  /**
@@ -136,7 +176,6 @@ function createEnvState() {
136
176
  publicMap: /* @__PURE__ */ new Map()
137
177
  };
138
178
  }
139
-
140
179
  //#endregion
141
180
  //#region src/plugins/env/validate.ts
142
181
  /** Error message thrown by every frozen-map mutator. */
@@ -255,7 +294,6 @@ function validateSchema(ctx) {
255
294
  freezeMap(state.resolved);
256
295
  freezeMap(state.publicMap);
257
296
  }
258
-
259
297
  //#endregion
260
298
  //#region src/plugins/env/providers.ts
261
299
  /**
@@ -324,6 +362,15 @@ function parseDotenv(text) {
324
362
  function dotenv(path = DEFAULT_DOTENV_PATH) {
325
363
  return {
326
364
  name: `dotenv:${path}`,
365
+ /**
366
+ * Reads and parses the dotenv file fresh from disk; `{}` if it is missing.
367
+ *
368
+ * @returns The parsed environment record, or `{}` when the file is absent.
369
+ * @example
370
+ * ```ts
371
+ * dotenv(".env.local").load();
372
+ * ```
373
+ */
327
374
  load() {
328
375
  if (!(0, node_fs.existsSync)(path)) return {};
329
376
  return parseDotenv((0, node_fs.readFileSync)(path, "utf8"));
@@ -343,24 +390,20 @@ function dotenv(path = DEFAULT_DOTENV_PATH) {
343
390
  function processEnv() {
344
391
  return {
345
392
  name: "process-env",
393
+ /**
394
+ * Returns a shallow copy of `process.env` at call time.
395
+ *
396
+ * @returns A fresh shallow copy of `process.env`.
397
+ * @example
398
+ * ```ts
399
+ * processEnv().load();
400
+ * ```
401
+ */
346
402
  load() {
347
403
  return { ...process.env };
348
404
  }
349
405
  };
350
406
  }
351
-
352
- //#endregion
353
- //#region src/plugins/env/index.ts
354
- /**
355
- * @file Core plugin: universal env injection — schema + providers + PUBLIC_ cross-validation at onInit.
356
- * @see README.md
357
- */
358
- /** Plugin config defaults (R6 typed const). `providers: []` — framework sets `[dotenv(), processEnv()]` via the 4-level cascade. */
359
- const defaultEnvConfig = {
360
- schema: {},
361
- providers: [],
362
- publicPrefix: "PUBLIC_"
363
- };
364
407
  /**
365
408
  * Core plugin that resolves, validates, and freezes the environment at `onInit`,
366
409
  * exposing a read-only accessor at `ctx.env`. No `onStart`/`onStop` — holds no resource.
@@ -371,12 +414,15 @@ const defaultEnvConfig = {
371
414
  * ```
372
415
  */
373
416
  const envPlugin = (0, _moku_labs_core.createCorePlugin)("env", {
374
- config: defaultEnvConfig,
417
+ config: {
418
+ schema: {},
419
+ providers: [],
420
+ publicPrefix: "PUBLIC_"
421
+ },
375
422
  createState: createEnvState,
376
423
  api: createEnvApi,
377
424
  onInit: validateSchema
378
425
  });
379
-
380
426
  //#endregion
381
427
  //#region src/plugins/log/expect.ts
382
428
  /**
@@ -487,10 +533,33 @@ function describePartial(partial) {
487
533
  */
488
534
  function createExpectChain(entries) {
489
535
  const chain = {
536
+ /**
537
+ * Assert at least one entry has `event`, optionally matching `partial`.
538
+ *
539
+ * @param event - Event name to find.
540
+ * @param partial - Optional partial data shape (subset-matched).
541
+ * @returns The same chain for chaining.
542
+ * @throws {LogExpectAssertionError} When no matching entry exists.
543
+ * @example
544
+ * ```ts
545
+ * chain.toHaveEvent("build:phase", { status: "start" });
546
+ * ```
547
+ */
490
548
  toHaveEvent(event, partial) {
491
549
  if (!entries.some((entry) => entryMatches(entry, event, partial))) throw new LogExpectAssertionError(`Expected trace to contain event "${event}"${describePartial(partial)}, but none was found.`);
492
550
  return chain;
493
551
  },
552
+ /**
553
+ * Assert all of `events` appear in the trace in the given relative order.
554
+ *
555
+ * @param events - Ordered list of event names (gaps allowed).
556
+ * @returns The same chain for chaining.
557
+ * @throws {LogExpectAssertionError} When the ordering cannot be satisfied.
558
+ * @example
559
+ * ```ts
560
+ * chain.toHaveEventInOrder(["build:phase", "build:complete"]);
561
+ * ```
562
+ */
494
563
  toHaveEventInOrder(events) {
495
564
  let cursor = 0;
496
565
  for (const [position, event] of events.entries()) {
@@ -504,6 +573,18 @@ function createExpectChain(entries) {
504
573
  }
505
574
  return chain;
506
575
  },
576
+ /**
577
+ * Assert NO entry has `event` (optionally narrowed by `partial`).
578
+ *
579
+ * @param event - Event name that must be absent.
580
+ * @param partial - Optional partial data shape; only matching entries violate.
581
+ * @returns The same chain for chaining.
582
+ * @throws {LogExpectAssertionError} When a matching entry exists.
583
+ * @example
584
+ * ```ts
585
+ * chain.toNotHaveEvent("deploy:failed");
586
+ * ```
587
+ */
507
588
  toNotHaveEvent(event, partial) {
508
589
  const offending = entries.findIndex((entry) => entryMatches(entry, event, partial));
509
590
  if (offending !== -1) throw new LogExpectAssertionError(`Expected trace to NOT contain event "${event}"${describePartial(partial)}, but found one at index ${offending}.`);
@@ -512,7 +593,6 @@ function createExpectChain(entries) {
512
593
  };
513
594
  return chain;
514
595
  }
515
-
516
596
  //#endregion
517
597
  //#region src/plugins/log/api.ts
518
598
  /**
@@ -581,33 +661,110 @@ function mergeError(data, error) {
581
661
  function createLogApi(ctx) {
582
662
  const { state } = ctx;
583
663
  return {
664
+ /**
665
+ * Append an `info` entry and fan it out to every sink.
666
+ *
667
+ * @param event - Event identifier (convention: `domain:action`).
668
+ * @param data - Optional structured payload.
669
+ * @example
670
+ * ```ts
671
+ * log.info("content:ready", { count: 12 });
672
+ * ```
673
+ */
584
674
  info(event, data) {
585
675
  append(state, "info", event, data);
586
676
  },
677
+ /**
678
+ * Append a `debug` entry and fan it out to every sink.
679
+ *
680
+ * @param event - Event identifier (convention: `domain:action`).
681
+ * @param data - Optional structured payload.
682
+ * @example
683
+ * ```ts
684
+ * log.debug("router:match", { path: "/blog/" });
685
+ * ```
686
+ */
587
687
  debug(event, data) {
588
688
  append(state, "debug", event, data);
589
689
  },
690
+ /**
691
+ * Append a `warn` entry and fan it out to every sink.
692
+ *
693
+ * @param event - Event identifier (convention: `domain:action`).
694
+ * @param data - Optional structured payload.
695
+ * @example
696
+ * ```ts
697
+ * log.warn("build:skip", { reason: "no sitemap" });
698
+ * ```
699
+ */
590
700
  warn(event, data) {
591
701
  append(state, "warn", event, data);
592
702
  },
703
+ /**
704
+ * Append an `error` entry. When `error` is provided, its `message`/`stack`
705
+ * are merged into `data` under an `error` key (existing keys preserved);
706
+ * otherwise `data` is recorded as-is.
707
+ *
708
+ * @param event - Event identifier (convention: `domain:action`).
709
+ * @param data - Optional structured payload.
710
+ * @param error - Optional originating Error to merge into `data`.
711
+ * @example
712
+ * ```ts
713
+ * log.error("deploy:failed", { target: "cf" }, err);
714
+ * ```
715
+ */
593
716
  error(event, data, error) {
594
717
  append(state, "error", event, error === void 0 ? data : mergeError(data, error));
595
718
  },
719
+ /**
720
+ * Return a frozen snapshot (fresh copy) of the entries recorded so far.
721
+ *
722
+ * @returns A readonly, frozen copy of the recorded entries.
723
+ * @example
724
+ * ```ts
725
+ * const entries = log.trace();
726
+ * ```
727
+ */
596
728
  trace() {
597
729
  return Object.freeze([...state.entries]);
598
730
  },
731
+ /**
732
+ * Return a fluent assertion chain bound to the live entries array.
733
+ *
734
+ * @returns A fresh {@link ExpectChain} reading `state.entries` live.
735
+ * @example
736
+ * ```ts
737
+ * log.expect().toHaveEvent("build:complete");
738
+ * ```
739
+ */
599
740
  expect() {
600
741
  return createExpectChain(state.entries);
601
742
  },
743
+ /**
744
+ * Register an additional output sink at runtime.
745
+ *
746
+ * @param sink - The sink to add to the fan-out list.
747
+ * @example
748
+ * ```ts
749
+ * log.addSink({ write: (e) => stream.write(JSON.stringify(e)) });
750
+ * ```
751
+ */
602
752
  addSink(sink) {
603
753
  state.sinks.push(sink);
604
754
  },
755
+ /**
756
+ * Clear all recorded entries while keeping registered sinks.
757
+ *
758
+ * @example
759
+ * ```ts
760
+ * log.reset();
761
+ * ```
762
+ */
605
763
  reset() {
606
764
  state.entries.length = 0;
607
765
  }
608
766
  };
609
767
  }
610
-
611
768
  //#endregion
612
769
  //#region src/plugins/log/sinks.ts
613
770
  /**
@@ -622,7 +779,17 @@ function createLogApi(ctx) {
622
779
  * ```
623
780
  */
624
781
  function consoleSink() {
625
- return { write(entry) {
782
+ return {
783
+ /**
784
+ * Route a single entry to the console channel matching its level.
785
+ *
786
+ * @param entry - The entry to emit.
787
+ * @example
788
+ * ```ts
789
+ * sink.write({ level: "warn", event: "build:skip", ts: Date.now() });
790
+ * ```
791
+ */
792
+ write(entry) {
626
793
  if (entry.level === "error") console.error(entry);
627
794
  else if (entry.level === "warn") console.warn(entry);
628
795
  else console.log(entry);
@@ -643,7 +810,6 @@ function consoleSink() {
643
810
  function installDefaultSinks(ctx) {
644
811
  if (ctx.config.mode === "dev" || ctx.config.mode === "production") ctx.state.sinks.push(consoleSink());
645
812
  }
646
-
647
813
  //#endregion
648
814
  //#region src/plugins/log/state.ts
649
815
  /**
@@ -664,15 +830,6 @@ function createLogState(_ctx) {
664
830
  sinks: []
665
831
  };
666
832
  }
667
-
668
- //#endregion
669
- //#region src/plugins/log/index.ts
670
- /**
671
- * @file log — Core Plugin (Standard tier): in-memory trace + expect() DSL.
672
- * @see README.md
673
- */
674
- /** Default config; overridden via the 4-level pluginConfigs.log merge. */
675
- const defaultLogConfig = { mode: "production" };
676
833
  /**
677
834
  * Core logging plugin — always-on in-memory trace + `expect()` event-trace DSL.
678
835
  * API injected as `ctx.log` on every regular plugin and surfaced as `app.log`.
@@ -681,29 +838,40 @@ const defaultLogConfig = { mode: "production" };
681
838
  * @see README.md
682
839
  */
683
840
  const logPlugin = (0, _moku_labs_core.createCorePlugin)("log", {
684
- config: defaultLogConfig,
841
+ config: { mode: "production" },
685
842
  createState: createLogState,
686
843
  api: createLogApi,
687
844
  onInit: installDefaultSinks
688
845
  });
689
-
690
- //#endregion
691
- //#region src/config.ts
692
- /**
693
- * @file Framework configuration — Config + Events types, core plugin registration.
694
- * @see README.md
695
- */
696
- const defaultConfig$6 = { mode: "production" };
697
846
  const coreConfig = (0, _moku_labs_core.createCoreConfig)("web", {
698
- config: defaultConfig$6,
847
+ config: { mode: "production" },
699
848
  plugins: [logPlugin, envPlugin],
700
849
  pluginConfigs: {
701
850
  log: { mode: "production" },
702
851
  env: { providers: [dotenv(), processEnv()] }
703
852
  }
704
853
  });
705
- const { createPlugin: createPlugin$1, createCore } = coreConfig;
706
-
854
+ /**
855
+ * Create a custom plugin bound to this framework's `Config`/`Events` and the core
856
+ * plugin APIs (`log`, `env`). Plugin types are fully inferred from the spec
857
+ * object — never write them explicitly. This is the binding every built-in
858
+ * plugin is wired with, and the one consumer plugins should use too.
859
+ *
860
+ * @example
861
+ * ```ts
862
+ * const analytics = createPlugin("analytics", {
863
+ * config: { writeKey: "" },
864
+ * api: (ctx) => ({ track: (event: string) => ctx.log.info("analytics:track", { event }) })
865
+ * });
866
+ * ```
867
+ */
868
+ const createPlugin$1 = coreConfig.createPlugin;
869
+ /**
870
+ * Step 2 of the factory chain — captures the framework's default plugin set and
871
+ * returns the consumer entry points ({@link createApp} + a re-exported
872
+ * `createPlugin`). Wired once in `src/index.ts`; consumers don't call it directly.
873
+ */
874
+ const createCore = coreConfig.createCore;
707
875
  //#endregion
708
876
  //#region src/plugins/i18n/api.ts
709
877
  /** Error prefix for all i18n lifecycle failures. */
@@ -743,21 +911,83 @@ function validateI18nConfig(ctx) {
743
911
  function createI18nApi(ctx) {
744
912
  const { config } = ctx;
745
913
  return {
914
+ /**
915
+ * Returns the configured supported locales in declared order.
916
+ *
917
+ * @returns The configured `locales` list (priority/display order).
918
+ * @example
919
+ * ```ts
920
+ * api.locales(); // ["en", "uk"]
921
+ * ```
922
+ */
746
923
  locales() {
747
924
  return config.locales;
748
925
  },
926
+ /**
927
+ * Returns the fallback locale used when a requested locale is absent.
928
+ *
929
+ * @returns The configured `defaultLocale`.
930
+ * @example
931
+ * ```ts
932
+ * api.defaultLocale(); // "en"
933
+ * ```
934
+ */
749
935
  defaultLocale() {
750
936
  return config.defaultLocale;
751
937
  },
938
+ /**
939
+ * Membership guard: whether `x` is one of the supported locales
940
+ * (case-sensitive).
941
+ *
942
+ * @param x - Candidate locale code.
943
+ * @returns `true` if `x ∈ locales`, else `false`.
944
+ * @example
945
+ * ```ts
946
+ * api.isLocale("uk"); // true
947
+ * ```
948
+ */
752
949
  isLocale(x) {
753
950
  return config.locales.includes(x);
754
951
  },
952
+ /**
953
+ * Human-readable display name for a locale.
954
+ *
955
+ * @param locale - Locale code to look up.
956
+ * @returns The display name, or `undefined` if unmapped.
957
+ * @example
958
+ * ```ts
959
+ * api.localeName("uk"); // "Українська"
960
+ * ```
961
+ */
755
962
  localeName(locale) {
756
963
  return config.localeNames?.[locale];
757
964
  },
965
+ /**
966
+ * Open Graph `og:locale` value for a locale.
967
+ *
968
+ * @param locale - Locale code to look up.
969
+ * @returns The `og:locale` value (e.g. `"en_US"`), or `undefined` if unmapped.
970
+ * @example
971
+ * ```ts
972
+ * api.ogLocale("en"); // "en_US"
973
+ * ```
974
+ */
758
975
  ogLocale(locale) {
759
976
  return config.ogLocaleMap?.[locale];
760
977
  },
978
+ /**
979
+ * Translate `key` for `locale` with a deterministic fallback chain
980
+ * (requested locale → default locale → the key itself). The default-locale
981
+ * lookup is skipped when `locale === defaultLocale`.
982
+ *
983
+ * @param locale - Requested locale code.
984
+ * @param key - Translation key (e.g. `"nav.home"`).
985
+ * @returns The translated value, the default-locale value, or `key`.
986
+ * @example
987
+ * ```ts
988
+ * api.t("uk", "nav.home"); // "Головна"
989
+ * ```
990
+ */
761
991
  t(locale, key) {
762
992
  const exact = config.translations?.[locale]?.[key];
763
993
  if (exact !== void 0) return exact;
@@ -769,34 +999,36 @@ function createI18nApi(ctx) {
769
999
  }
770
1000
  };
771
1001
  }
772
-
773
- //#endregion
774
- //#region src/plugins/i18n/index.ts
775
1002
  /**
776
- * i18nMicro tier. Multi-file layout (index wiring + api.ts + types.ts) so
777
- * index.ts stays within the ≤30-line wiring-only hook; logic lives in api.ts.
778
- *
779
- * Locale registry + flat translation helper with default-locale fallback.
780
- * Pure config-as-data: no state, no events, no lifecycle resources.
781
- * Consumed read-only by content/router/head/build via `ctx.require(i18nPlugin)`.
1003
+ * Internationalization plugin locale registry plus a flat translation helper
1004
+ * with default-locale fallback. Pure config-as-data (no state or events);
1005
+ * consumed read-only by content, router, head, and build.
782
1006
  *
783
- * @file i18n plugin wiring harness.
784
- * @see README.md
1007
+ * @example Register locales and translations
1008
+ * ```ts
1009
+ * const app = createApp({
1010
+ * pluginConfigs: {
1011
+ * i18n: {
1012
+ * locales: ["en", "uk"],
1013
+ * defaultLocale: "en",
1014
+ * localeNames: { en: "English", uk: "Українська" },
1015
+ * translations: { uk: { "nav.home": "Головна" } }
1016
+ * }
1017
+ * }
1018
+ * });
1019
+ * ```
785
1020
  */
786
- /** Typed default config (R6: no inline `as`). Optional maps default to `{}` so every lookup is total. */
787
- const defaultConfig$5 = {
788
- locales: ["en"],
789
- defaultLocale: "en",
790
- localeNames: {},
791
- ogLocaleMap: {},
792
- translations: {}
793
- };
794
1021
  const i18nPlugin = createPlugin$1("i18n", {
795
- config: defaultConfig$5,
1022
+ config: {
1023
+ locales: ["en"],
1024
+ defaultLocale: "en",
1025
+ localeNames: {},
1026
+ ogLocaleMap: {},
1027
+ translations: {}
1028
+ },
796
1029
  onInit: validateI18nConfig,
797
1030
  api: createI18nApi
798
1031
  });
799
-
800
1032
  //#endregion
801
1033
  //#region src/plugins/content/pipeline/frontmatter.ts
802
1034
  /**
@@ -842,7 +1074,6 @@ function parseFrontmatter(raw, config) {
842
1074
  body: parsed.content
843
1075
  };
844
1076
  }
845
-
846
1077
  //#endregion
847
1078
  //#region src/plugins/content/pipeline/plugins.ts
848
1079
  /**
@@ -999,7 +1230,6 @@ function defaultRehypePlugins() {
999
1230
  sectionDividerPlugin
1000
1231
  ];
1001
1232
  }
1002
-
1003
1233
  //#endregion
1004
1234
  //#region src/plugins/content/pipeline/sanitize.ts
1005
1235
  /**
@@ -1086,7 +1316,6 @@ function buildSanitizeSchema() {
1086
1316
  }
1087
1317
  };
1088
1318
  }
1089
-
1090
1319
  //#endregion
1091
1320
  //#region src/plugins/content/pipeline/markdown.ts
1092
1321
  /**
@@ -1144,7 +1373,6 @@ function applyPluggable(processor, plugin) {
1144
1373
  }
1145
1374
  processor.use(plugin);
1146
1375
  }
1147
-
1148
1376
  //#endregion
1149
1377
  //#region src/plugins/content/pipeline/reading-time.ts
1150
1378
  /**
@@ -1170,7 +1398,6 @@ function calculateReadingTime(text) {
1170
1398
  wordCount: stats.words
1171
1399
  };
1172
1400
  }
1173
-
1174
1401
  //#endregion
1175
1402
  //#region src/plugins/content/api.ts
1176
1403
  /**
@@ -1278,7 +1505,7 @@ async function discoverSlugs(dir) {
1278
1505
  * ```
1279
1506
  */
1280
1507
  async function readArticle(ctx, slug, fileLocale, outLocale, isFallback) {
1281
- const filePath = node_path.default.join(ctx.config.contentDir, slug, `${fileLocale}.md`);
1508
+ const filePath = node_path$1.default.join(ctx.config.contentDir, slug, `${fileLocale}.md`);
1282
1509
  let raw;
1283
1510
  try {
1284
1511
  raw = await (0, node_fs_promises.readFile)(filePath, "utf8");
@@ -1382,6 +1609,18 @@ function toCard(article) {
1382
1609
  */
1383
1610
  function createContentApi(ctx) {
1384
1611
  return {
1612
+ /**
1613
+ * Load every article across every active locale, returning a locale-keyed
1614
+ * map of date-descending Article arrays. Lazily builds the processor and
1615
+ * discovers slugs, applies locale fallback, excludes drafts in production,
1616
+ * assigns `contentId` after sorting, then emits `content:ready`.
1617
+ *
1618
+ * @returns A locale-keyed map of date-descending articles.
1619
+ * @example
1620
+ * ```ts
1621
+ * const byLocale = await api.loadAll();
1622
+ * ```
1623
+ */
1385
1624
  async loadAll() {
1386
1625
  const slugs = ctx.state.slugs ?? await discoverSlugs(ctx.config.contentDir);
1387
1626
  ctx.state.slugs = slugs;
@@ -1409,6 +1648,19 @@ function createContentApi(ctx) {
1409
1648
  });
1410
1649
  return result;
1411
1650
  },
1651
+ /**
1652
+ * Resolve and render a single article for one locale with locale fallback.
1653
+ * Throws a `[web] content` error when neither the requested nor the
1654
+ * default-locale file exists.
1655
+ *
1656
+ * @param slug - Article directory name.
1657
+ * @param locale - Requested locale code.
1658
+ * @returns The resolved Article.
1659
+ * @example
1660
+ * ```ts
1661
+ * const article = await api.load("intro", "uk");
1662
+ * ```
1663
+ */
1412
1664
  async load(slug, locale) {
1413
1665
  const article = await resolveArticle(ctx, slug, locale);
1414
1666
  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.`);
@@ -1417,10 +1669,33 @@ function createContentApi(ctx) {
1417
1669
  ctx.state.articles.set(locale, cache);
1418
1670
  return article;
1419
1671
  },
1672
+ /**
1673
+ * Render a raw Markdown string to HTML through the full pipeline (sanitize
1674
+ * last when `trustedContent` is false). Lazily builds the processor.
1675
+ *
1676
+ * @param md - Raw Markdown source.
1677
+ * @returns The rendered HTML string.
1678
+ * @example
1679
+ * ```ts
1680
+ * const html = await api.renderMarkdown("# Hi");
1681
+ * ```
1682
+ */
1420
1683
  async renderMarkdown(md) {
1421
1684
  const processor = ensureProcessor(ctx.state, ctx.config);
1422
1685
  return String(await processor.process(md));
1423
1686
  },
1687
+ /**
1688
+ * Mark file paths stale for incremental dev rebuilds. Each non-blank path is
1689
+ * added to `dirtyPaths` and its derived slug cache entry is dropped so the
1690
+ * next `loadAll()` re-reads only those files. Empty/whitespace paths are
1691
+ * ignored. Emits `content:invalidated` with the accepted paths.
1692
+ *
1693
+ * @param paths - File paths to invalidate.
1694
+ * @example
1695
+ * ```ts
1696
+ * api.invalidate(["src/content/intro/en.md"]);
1697
+ * ```
1698
+ */
1424
1699
  invalidate(paths) {
1425
1700
  const accepted = [];
1426
1701
  for (const path of paths) {
@@ -1433,12 +1708,22 @@ function createContentApi(ctx) {
1433
1708
  ctx.state.slugs = null;
1434
1709
  ctx.emit("content:invalidated", { paths: accepted });
1435
1710
  },
1711
+ /**
1712
+ * Project a full Article to a lightweight ArticleCard for list/grid
1713
+ * rendering without shipping rendered HTML.
1714
+ *
1715
+ * @param article - The source article.
1716
+ * @returns The card projection.
1717
+ * @example
1718
+ * ```ts
1719
+ * const card = api.articleToCard(article);
1720
+ * ```
1721
+ */
1436
1722
  articleToCard(article) {
1437
1723
  return toCard(article);
1438
1724
  }
1439
1725
  };
1440
1726
  }
1441
-
1442
1727
  //#endregion
1443
1728
  //#region src/plugins/content/config.ts
1444
1729
  /**
@@ -1459,7 +1744,6 @@ const defaultContentConfig = {
1459
1744
  extraRehypePlugins: [],
1460
1745
  shikiTheme: "github-dark"
1461
1746
  };
1462
-
1463
1747
  //#endregion
1464
1748
  //#region src/plugins/content/events.ts
1465
1749
  /**
@@ -1478,7 +1762,6 @@ const contentEvents = (register) => ({
1478
1762
  "content:ready": register("All articles loaded across locales"),
1479
1763
  "content:invalidated": register("Article paths marked stale for dev rebuild")
1480
1764
  });
1481
-
1482
1765
  //#endregion
1483
1766
  //#region src/plugins/content/state.ts
1484
1767
  /**
@@ -1502,7 +1785,6 @@ function createContentState(_ctx) {
1502
1785
  dirtyPaths: /* @__PURE__ */ new Set()
1503
1786
  };
1504
1787
  }
1505
-
1506
1788
  //#endregion
1507
1789
  //#region src/plugins/content/validate.ts
1508
1790
  /**
@@ -1521,7 +1803,6 @@ function validateContentConfig(config) {
1521
1803
  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.");
1522
1804
  if (typeof config.trustedContent !== "boolean") throw new TypeError("[web] content.trustedContent must be a boolean.");
1523
1805
  }
1524
-
1525
1806
  //#endregion
1526
1807
  //#region src/plugins/content/index.ts
1527
1808
  /**
@@ -1532,6 +1813,26 @@ function validateContentConfig(config) {
1532
1813
  * and `content:invalidated`.
1533
1814
  * @see README.md
1534
1815
  */
1816
+ /**
1817
+ * Content plugin — Markdown pipeline: discovers files, parses frontmatter, renders
1818
+ * to sanitized HTML (rehype-sanitize unless `trustedContent`), and exposes a
1819
+ * locale-keyed Article model. Depends on i18n; emits `content:ready` and
1820
+ * `content:invalidated`.
1821
+ *
1822
+ * @example Point at a content directory and pick a Shiki theme
1823
+ * ```ts
1824
+ * const app = createApp({
1825
+ * pluginConfigs: {
1826
+ * content: {
1827
+ * contentDir: "./content",
1828
+ * shikiTheme: "github-dark",
1829
+ * defaultAuthor: "Ada Lovelace"
1830
+ * // trustedContent: true // ONLY for fully author-controlled Markdown — disables sanitize
1831
+ * }
1832
+ * }
1833
+ * });
1834
+ * ```
1835
+ */
1535
1836
  const contentPlugin = createPlugin$1("content", {
1536
1837
  depends: [i18nPlugin],
1537
1838
  events: contentEvents,
@@ -1540,7 +1841,6 @@ const contentPlugin = createPlugin$1("content", {
1540
1841
  onInit: (ctx) => validateContentConfig(ctx.config),
1541
1842
  api: contentApi
1542
1843
  });
1543
-
1544
1844
  //#endregion
1545
1845
  //#region src/plugins/site/api.ts
1546
1846
  /** Error prefix for all site lifecycle/validation failures. */
@@ -1632,50 +1932,99 @@ function validateSiteConfig(ctx) {
1632
1932
  function createSiteApi(ctx) {
1633
1933
  const { config } = ctx;
1634
1934
  return {
1935
+ /**
1936
+ * Returns the configured site name.
1937
+ *
1938
+ * @returns The human-readable site name from `config.name`.
1939
+ * @example
1940
+ * ```ts
1941
+ * api.name(); // "My Blog"
1942
+ * ```
1943
+ */
1635
1944
  name() {
1636
1945
  return config.name;
1637
1946
  },
1947
+ /**
1948
+ * Returns the configured absolute base URL of the site.
1949
+ *
1950
+ * @returns The base URL from `config.url`.
1951
+ * @example
1952
+ * ```ts
1953
+ * api.url(); // "https://blog.dev"
1954
+ * ```
1955
+ */
1638
1956
  url() {
1639
1957
  return config.url;
1640
1958
  },
1959
+ /**
1960
+ * Returns the configured site author/byline.
1961
+ *
1962
+ * @returns The author from `config.author`.
1963
+ * @example
1964
+ * ```ts
1965
+ * api.author(); // "Alex"
1966
+ * ```
1967
+ */
1641
1968
  author() {
1642
1969
  return config.author;
1643
1970
  },
1971
+ /**
1972
+ * Returns the configured site description.
1973
+ *
1974
+ * @returns The description from `config.description`.
1975
+ * @example
1976
+ * ```ts
1977
+ * api.description(); // "A personal blog about web frameworks."
1978
+ * ```
1979
+ */
1644
1980
  description() {
1645
1981
  return config.description;
1646
1982
  },
1983
+ /**
1984
+ * Joins a path against the configured base `url` to produce an absolute
1985
+ * canonical URL. An empty path (or "/") returns the base URL unchanged.
1986
+ *
1987
+ * @param path - Relative path for the page, e.g. "/about/".
1988
+ * @returns The absolute canonical URL.
1989
+ * @example
1990
+ * ```ts
1991
+ * api.canonical("/about/"); // "https://blog.dev/about/"
1992
+ * ```
1993
+ */
1647
1994
  canonical(path) {
1648
1995
  return joinCanonical(config.url, path);
1649
1996
  }
1650
1997
  };
1651
1998
  }
1652
-
1653
- //#endregion
1654
- //#region src/plugins/site/index.ts
1655
1999
  /**
1656
- * siteMicro tier. Multi-file layout (index wiring + api.ts + types.ts) so
1657
- * index.ts stays within the ≤30-line wiring-only hook; logic lives in api.ts.
1658
- *
1659
- * Holds global, frozen site metadata (name, url, author, description) and
1660
- * constructs canonical URLs. Consumed by router/head/build via
1661
- * `ctx.require(sitePlugin)`. No events, no dependencies, no state.
2000
+ * Site plugin holds global, frozen site metadata (name, url, author,
2001
+ * description) and builds canonical URLs. Consumed by router, head, and build.
2002
+ * `name` and `url` must be non-empty (validated at `onInit`).
1662
2003
  *
1663
- * @file site plugin wiring harness.
1664
- * @see README.md
2004
+ * @example Set your site identity
2005
+ * ```ts
2006
+ * const app = createApp({
2007
+ * pluginConfigs: {
2008
+ * site: {
2009
+ * name: "My Blog",
2010
+ * url: "https://blog.dev",
2011
+ * author: "Ada Lovelace",
2012
+ * description: "Notes on computing"
2013
+ * }
2014
+ * }
2015
+ * });
2016
+ * ```
1665
2017
  */
1666
- /** Typed default config (R6: no inline `as`). Consumers override via `pluginConfigs.site`. */
1667
- const defaultConfig$4 = {
1668
- name: "",
1669
- url: "",
1670
- author: "",
1671
- description: ""
1672
- };
1673
2018
  const sitePlugin = createPlugin$1("site", {
1674
- config: defaultConfig$4,
2019
+ config: {
2020
+ name: "",
2021
+ url: "",
2022
+ author: "",
2023
+ description: ""
2024
+ },
1675
2025
  onInit: validateSiteConfig,
1676
2026
  api: createSiteApi
1677
2027
  });
1678
-
1679
2028
  //#endregion
1680
2029
  //#region src/plugins/router/builders/match.ts
1681
2030
  /**
@@ -1745,7 +2094,6 @@ function matchRoute(compiled, pathname) {
1745
2094
  }
1746
2095
  return null;
1747
2096
  }
1748
-
1749
2097
  //#endregion
1750
2098
  //#region src/plugins/router/api.ts
1751
2099
  /**
@@ -1808,23 +2156,63 @@ function toTypedRoute(entry) {
1808
2156
  function createApi$4(ctx) {
1809
2157
  const { state } = ctx;
1810
2158
  return {
2159
+ /**
2160
+ * Match a pathname against the compiled route table (specificity-sorted).
2161
+ *
2162
+ * @param pathname - URL pathname, e.g. `/en/hello/`.
2163
+ * @returns `{ params, route }` for the most specific match, or `null`.
2164
+ * @example
2165
+ * ```ts
2166
+ * api.match("/en/hello/");
2167
+ * ```
2168
+ */
1811
2169
  match(pathname) {
1812
2170
  return matchRoute(readTable(state).compiled, pathname);
1813
2171
  },
2172
+ /**
2173
+ * Build a URL for a named route from params.
2174
+ *
2175
+ * @param routeName - Route name key from the route map.
2176
+ * @param params - Param values to substitute into the pattern.
2177
+ * @returns The resolved URL string (e.g. `/en/hello/`).
2178
+ * @throws {Error} If `routeName` is unknown.
2179
+ * @example
2180
+ * ```ts
2181
+ * api.toUrl("article", { lang: "en", slug: "hello" });
2182
+ * ```
2183
+ */
1814
2184
  toUrl(routeName, params) {
1815
2185
  const entry = readTable(state).byName.get(routeName);
1816
2186
  if (!entry) throw new Error(`${ERROR_PREFIX$9}: unknown route name "${routeName}".`);
1817
2187
  return entry.toUrl(params);
1818
2188
  },
2189
+ /**
2190
+ * All resolved routes as typed URL utilities, in specificity order.
2191
+ *
2192
+ * @returns A fresh read-only array of resolved typed routes.
2193
+ * @example
2194
+ * ```ts
2195
+ * for (const r of api.entries()) r.toUrl({ slug: "x" });
2196
+ * ```
2197
+ */
1819
2198
  entries() {
1820
2199
  return readTable(state).compiled.map((entry) => toTypedRoute(entry));
1821
2200
  },
2201
+ /**
2202
+ * The typed route set for build-time consumption (declaration order). An API
2203
+ * return, NOT a config readback — preserves per-route types despite erasure.
2204
+ *
2205
+ * @returns A fresh read-only array of the typed route definitions.
2206
+ * @example
2207
+ * ```ts
2208
+ * for (const def of api.manifest()) def._handlers.load?.({}, "en");
2209
+ * ```
2210
+ */
1822
2211
  manifest() {
1823
2212
  return [...readTable(state).byName.values()].map((entry) => entry.definition);
1824
2213
  }
1825
2214
  };
1826
2215
  }
1827
-
1828
2216
  //#endregion
1829
2217
  //#region src/plugins/router/builders/compile.ts
1830
2218
  /** Shared `[web]` error prefix for router validation failures. */
@@ -1986,9 +2374,31 @@ function compileRoute(name, definition, input) {
1986
2374
  dynamicSegmentCount: countDynamicSegments(pattern),
1987
2375
  matchers,
1988
2376
  matchFn: createMatchFunction(matchers, input.defaultLocale),
2377
+ /**
2378
+ * Build a URL for this route from params.
2379
+ *
2380
+ * @param params - Param values to substitute.
2381
+ * @returns The resolved relative URL.
2382
+ * @example
2383
+ * ```ts
2384
+ * entry.toUrl({ slug: "x" });
2385
+ * ```
2386
+ */
1989
2387
  toUrl(params) {
1990
2388
  return buildUrl(pattern, params, input.baseUrl);
1991
2389
  },
2390
+ /**
2391
+ * Build the output file path for this route from params. Honors a custom
2392
+ * `.toFile()` override (captured in `_handlers.toFile`) when present, falling
2393
+ * back to the pattern-derived `…/index.html` path otherwise.
2394
+ *
2395
+ * @param params - Param values to substitute.
2396
+ * @returns The output file path.
2397
+ * @example
2398
+ * ```ts
2399
+ * entry.toFile({ slug: "x" });
2400
+ * ```
2401
+ */
1992
2402
  toFile(params) {
1993
2403
  return definition._handlers.toFile?.(params) ?? buildFilePath(pattern, params);
1994
2404
  },
@@ -2049,7 +2459,6 @@ function buildRouterTable(config, baseUrl, locales, defaultLocale) {
2049
2459
  defaultLocale
2050
2460
  });
2051
2461
  }
2052
-
2053
2462
  //#endregion
2054
2463
  //#region src/plugins/router/builders/route-builder.ts
2055
2464
  /**
@@ -2095,28 +2504,108 @@ function route(pattern) {
2095
2504
  pattern: carrier.pattern,
2096
2505
  _meta: carrier._meta,
2097
2506
  _handlers: carrier._handlers,
2507
+ /**
2508
+ * Attach a data loader; widens the data generic for downstream handlers.
2509
+ *
2510
+ * @param loader - The loader producing this route's data.
2511
+ * @returns The same builder, with the data generic widened.
2512
+ * @example
2513
+ * ```ts
2514
+ * route("/{slug}/").load(({ slug }) => ({ slug }));
2515
+ * ```
2516
+ */
2098
2517
  load(loader) {
2099
2518
  return set("load", loader);
2100
2519
  },
2520
+ /**
2521
+ * Attach a layout wrapper component.
2522
+ *
2523
+ * @param component - The layout component.
2524
+ * @returns The same builder for chaining.
2525
+ * @example
2526
+ * ```ts
2527
+ * route("/").layout((children) => children);
2528
+ * ```
2529
+ */
2101
2530
  layout(component) {
2102
2531
  return set("layout", component);
2103
2532
  },
2533
+ /**
2534
+ * Attach the page render handler.
2535
+ *
2536
+ * @param handler - The render handler.
2537
+ * @returns The same builder for chaining.
2538
+ * @example
2539
+ * ```ts
2540
+ * route("/").render(() => null);
2541
+ * ```
2542
+ */
2104
2543
  render(handler) {
2105
2544
  return set("render", handler);
2106
2545
  },
2546
+ /**
2547
+ * Attach the head/SEO handler.
2548
+ *
2549
+ * @param handler - The head handler.
2550
+ * @returns The same builder for chaining.
2551
+ * @example
2552
+ * ```ts
2553
+ * route("/").head(() => ({ title: "Home" }));
2554
+ * ```
2555
+ */
2107
2556
  head(handler) {
2108
2557
  return set("head", handler);
2109
2558
  },
2559
+ /**
2560
+ * Attach a static-generation param producer.
2561
+ *
2562
+ * @param handler - The param producer.
2563
+ * @returns The same builder for chaining.
2564
+ * @example
2565
+ * ```ts
2566
+ * route("/{slug}/").generate(() => [{ slug: "x" }]);
2567
+ * ```
2568
+ */
2110
2569
  generate(handler) {
2111
2570
  return set("generate", handler);
2112
2571
  },
2572
+ /**
2573
+ * Merge an arbitrary metadata bag into the route's `_meta`.
2574
+ *
2575
+ * @param meta - Metadata to merge.
2576
+ * @returns The same builder for chaining.
2577
+ * @example
2578
+ * ```ts
2579
+ * route("/").meta({ activeTab: "home" });
2580
+ * ```
2581
+ */
2113
2582
  meta(meta) {
2114
2583
  Object.assign(carrier._meta, meta);
2115
2584
  return builder;
2116
2585
  },
2586
+ /**
2587
+ * Attach a JSON serializer for the route's data.
2588
+ *
2589
+ * @param handler - The JSON serializer.
2590
+ * @returns The same builder for chaining.
2591
+ * @example
2592
+ * ```ts
2593
+ * route("/api/").toJson(() => ({ ok: true }));
2594
+ * ```
2595
+ */
2117
2596
  toJson(handler) {
2118
2597
  return set("toJson", handler);
2119
2598
  },
2599
+ /**
2600
+ * Override the output file-path producer.
2601
+ *
2602
+ * @param handler - The file-path producer.
2603
+ * @returns The same builder for chaining.
2604
+ * @example
2605
+ * ```ts
2606
+ * route("/feed/").toFile(() => "feed.xml");
2607
+ * ```
2608
+ */
2120
2609
  toFile(handler) {
2121
2610
  return set("toFile", handler);
2122
2611
  }
@@ -2137,7 +2626,6 @@ function route(pattern) {
2137
2626
  function defineRoutes(routes) {
2138
2627
  return routes;
2139
2628
  }
2140
-
2141
2629
  //#endregion
2142
2630
  //#region src/plugins/router/state.ts
2143
2631
  /**
@@ -2156,25 +2644,36 @@ function defineRoutes(routes) {
2156
2644
  function createState$4(_ctx) {
2157
2645
  return { table: null };
2158
2646
  }
2159
-
2160
- //#endregion
2161
- //#region src/plugins/router/index.ts
2162
2647
  /**
2163
- * @file routerComplex plugin wiring (logic in builders/, api.ts, state.ts).
2164
- * @see README.md
2648
+ * Router plugintyped, named route definitions with locale-aware URL generation
2649
+ * and matching. Author routes with {@link route} + {@link defineRoutes}. Depends
2650
+ * on site (base URL) and i18n (locales).
2651
+ *
2652
+ * @example Define routes and choose a render mode
2653
+ * ```ts
2654
+ * const app = createApp({
2655
+ * pluginConfigs: {
2656
+ * router: {
2657
+ * routes: defineRoutes({
2658
+ * home: route("/"),
2659
+ * article: route("/blog/{slug}/")
2660
+ * }),
2661
+ * mode: "hybrid" // "ssg" | "spa" | "hybrid" (default)
2662
+ * }
2663
+ * }
2664
+ * });
2665
+ * ```
2165
2666
  */
2166
- /** Default router config: empty route map (validated in onInit), hybrid mode. */
2167
- const defaultConfig$3 = {
2168
- routes: {},
2169
- mode: "hybrid"
2170
- };
2171
2667
  const routerPlugin = createPlugin$1("router", {
2172
2668
  depends: [sitePlugin, i18nPlugin],
2173
2669
  helpers: {
2174
2670
  route,
2175
2671
  defineRoutes
2176
2672
  },
2177
- config: defaultConfig$3,
2673
+ config: {
2674
+ routes: {},
2675
+ mode: "hybrid"
2676
+ },
2178
2677
  createState: createState$4,
2179
2678
  api: createApi$4,
2180
2679
  onInit(ctx) {
@@ -2183,7 +2682,6 @@ const routerPlugin = createPlugin$1("router", {
2183
2682
  ctx.state.table = buildRouterTable(ctx.config, baseUrl, i18n.locales(), i18n.defaultLocale());
2184
2683
  }
2185
2684
  });
2186
-
2187
2685
  //#endregion
2188
2686
  //#region src/plugins/head/primitives.ts
2189
2687
  /** OG/Twitter article-meta property prefixes (factored to satisfy no-duplicate-string). */
@@ -2347,7 +2845,6 @@ function buildArticleHead(articleMeta, canonicalUrl) {
2347
2845
  elements.push(jsonLd(ld));
2348
2846
  return elements;
2349
2847
  }
2350
-
2351
2848
  //#endregion
2352
2849
  //#region src/plugins/head/compose.ts
2353
2850
  /**
@@ -2508,7 +3005,6 @@ function serializeElement(element) {
2508
3005
  function serializeHead(elements) {
2509
3006
  return elements.map((element) => serializeElement(element)).join("");
2510
3007
  }
2511
-
2512
3008
  //#endregion
2513
3009
  //#region src/plugins/head/api.ts
2514
3010
  /**
@@ -2550,7 +3046,19 @@ function readDefaults(state) {
2550
3046
  * ```
2551
3047
  */
2552
3048
  function createApi$3(ctx) {
2553
- return { render(route, data) {
3049
+ return {
3050
+ /**
3051
+ * Compose the final `<head>` inner HTML for a route (pulled by `build`).
3052
+ *
3053
+ * @param route - The resolved route descriptor (incl. its `.head()` HeadConfig).
3054
+ * @param data - The page data object passed to the route's loader/render.
3055
+ * @returns The serialized inner HTML of `<head>`.
3056
+ * @example
3057
+ * ```ts
3058
+ * api.render(route, { title: "Post" });
3059
+ * ```
3060
+ */
3061
+ render(route, data) {
2554
3062
  return serializeHead(composeHead({
2555
3063
  route,
2556
3064
  data,
@@ -2561,7 +3069,6 @@ function createApi$3(ctx) {
2561
3069
  }));
2562
3070
  } };
2563
3071
  }
2564
-
2565
3072
  //#endregion
2566
3073
  //#region src/plugins/head/config.ts
2567
3074
  /** Error prefix for all head config-validation failures. */
@@ -2616,7 +3123,6 @@ function normalizeHeadConfig(config) {
2616
3123
  if (config.twitterHandle !== void 0) defaults.twitterHandle = config.twitterHandle;
2617
3124
  return Object.freeze(defaults);
2618
3125
  }
2619
-
2620
3126
  //#endregion
2621
3127
  //#region src/plugins/head/helpers.ts
2622
3128
  /**
@@ -2645,7 +3151,6 @@ const headHelpers = {
2645
3151
  feedLink,
2646
3152
  buildArticleHead
2647
3153
  };
2648
-
2649
3154
  //#endregion
2650
3155
  //#region src/plugins/head/state.ts
2651
3156
  /**
@@ -2666,13 +3171,31 @@ const headHelpers = {
2666
3171
  function createState$3(_ctx) {
2667
3172
  return { defaults: null };
2668
3173
  }
2669
-
2670
3174
  //#endregion
2671
3175
  //#region src/plugins/head/index.ts
2672
3176
  /**
2673
3177
  * @file head — Standard Plugin wiring harness (logic in primitives/compose/api/config).
2674
3178
  * @see README.md
2675
3179
  */
3180
+ /**
3181
+ * Head plugin — composes per-route `<head>` metadata (title template, Open Graph,
3182
+ * Twitter cards, canonical, hreflang). Use the re-exported SEO primitives
3183
+ * ({@link meta}, {@link og}, {@link twitter}, …) inside a route's `.head()`.
3184
+ * Depends on site, i18n, and router.
3185
+ *
3186
+ * @example Set global head defaults
3187
+ * ```ts
3188
+ * const app = createApp({
3189
+ * pluginConfigs: {
3190
+ * head: {
3191
+ * titleTemplate: "%s — My Blog",
3192
+ * twitterCard: "summary_large_image",
3193
+ * twitterHandle: "@moku_labs"
3194
+ * }
3195
+ * }
3196
+ * });
3197
+ * ```
3198
+ */
2676
3199
  const headPlugin = createPlugin$1("head", {
2677
3200
  depends: [
2678
3201
  sitePlugin,
@@ -2687,7 +3210,6 @@ const headPlugin = createPlugin$1("head", {
2687
3210
  ctx.state.defaults = normalizeHeadConfig(ctx.config);
2688
3211
  }
2689
3212
  });
2690
-
2691
3213
  //#endregion
2692
3214
  //#region src/plugins/build/phases/bundle.ts
2693
3215
  /**
@@ -2761,7 +3283,7 @@ async function runOne(ctx, runner, kind, entrypoints, outdir, minify) {
2761
3283
  });
2762
3284
  if (!result.success) throw new Error(`[web] build.bundle ${kind} build failed`);
2763
3285
  const hashed = {};
2764
- for (const output of result.outputs) hashed[node_path.default.basename(output.path)] = output.path;
3286
+ for (const output of result.outputs) hashed[node_path$1.default.basename(output.path)] = output.path;
2765
3287
  ctx.state.buildCache.set(kind, hashed);
2766
3288
  ctx.log.debug("build:bundle", {
2767
3289
  kind,
@@ -2786,10 +3308,9 @@ async function bundle(ctx, options = {}) {
2786
3308
  const { minify, outDir } = ctx.config;
2787
3309
  const cssEntrypoints = options.cssEntrypoints ?? resolveEntrypoints(CSS_ENTRY_CANDIDATES);
2788
3310
  const jsEntrypoints = options.jsEntrypoints ?? resolveEntrypoints(JS_ENTRY_CANDIDATES);
2789
- await runOne(ctx, runner, "css", cssEntrypoints, node_path.default.join(outDir, "assets"), minify);
2790
- await runOne(ctx, runner, "js", jsEntrypoints, node_path.default.join(outDir, "assets"), minify);
3311
+ await runOne(ctx, runner, "css", cssEntrypoints, node_path$1.default.join(outDir, "assets"), minify);
3312
+ await runOne(ctx, runner, "js", jsEntrypoints, node_path$1.default.join(outDir, "assets"), minify);
2791
3313
  }
2792
-
2793
3314
  //#endregion
2794
3315
  //#region src/plugins/build/phases/content.ts
2795
3316
  /**
@@ -2832,7 +3353,6 @@ function readCachedContent(ctx) {
2832
3353
  const cached = ctx.state.buildCache.get(CONTENT_CACHE_KEY);
2833
3354
  return cached instanceof Map ? cached : /* @__PURE__ */ new Map();
2834
3355
  }
2835
-
2836
3356
  //#endregion
2837
3357
  //#region src/plugins/build/phases/feeds.ts
2838
3358
  /**
@@ -2907,14 +3427,13 @@ async function generateFeeds(ctx) {
2907
3427
  };
2908
3428
  await (0, node_fs_promises.mkdir)(ctx.config.outDir, { recursive: true });
2909
3429
  await Promise.all([
2910
- (0, node_fs_promises.writeFile)(node_path.default.join(ctx.config.outDir, "feed.xml"), result.rss, "utf8"),
2911
- (0, node_fs_promises.writeFile)(node_path.default.join(ctx.config.outDir, "atom.xml"), result.atom, "utf8"),
2912
- (0, node_fs_promises.writeFile)(node_path.default.join(ctx.config.outDir, "feed.json"), result.json, "utf8")
3430
+ (0, node_fs_promises.writeFile)(node_path$1.default.join(ctx.config.outDir, "feed.xml"), result.rss, "utf8"),
3431
+ (0, node_fs_promises.writeFile)(node_path$1.default.join(ctx.config.outDir, "atom.xml"), result.atom, "utf8"),
3432
+ (0, node_fs_promises.writeFile)(node_path$1.default.join(ctx.config.outDir, "feed.json"), result.json, "utf8")
2913
3433
  ]);
2914
3434
  ctx.log.debug("build:feeds", { items: guids.length });
2915
3435
  return result;
2916
3436
  }
2917
-
2918
3437
  //#endregion
2919
3438
  //#region src/plugins/build/phases/images.ts
2920
3439
  /**
@@ -2942,7 +3461,7 @@ async function processImages(ctx, options = {}) {
2942
3461
  return 0;
2943
3462
  }
2944
3463
  const sourceDirectories = options.sourceDirectories ?? IMAGE_SOURCE_DIRECTORIES;
2945
- const target = node_path.default.join(ctx.config.outDir, "assets");
3464
+ const target = node_path$1.default.join(ctx.config.outDir, "assets");
2946
3465
  let copied = 0;
2947
3466
  for (const directory of sourceDirectories) {
2948
3467
  if (!(0, node_fs.existsSync)(directory)) continue;
@@ -2954,7 +3473,6 @@ async function processImages(ctx, options = {}) {
2954
3473
  ctx.log.debug("build:images", { copied });
2955
3474
  return copied;
2956
3475
  }
2957
-
2958
3476
  //#endregion
2959
3477
  //#region src/plugins/build/phases/og-images.tsx
2960
3478
  /**
@@ -2968,8 +3486,6 @@ const DEFAULT_SIZE = {
2968
3486
  width: 1200,
2969
3487
  height: 630
2970
3488
  };
2971
- /** The fixed concurrency bound for the OG render pool. */
2972
- const OG_CONCURRENCY = 4;
2973
3489
  /** Recognized font file extensions. */
2974
3490
  const FONT_EXTENSIONS$1 = [
2975
3491
  ".ttf",
@@ -3090,7 +3606,7 @@ async function generateOgImages(ctx, options = {}) {
3090
3606
  const articles = selectArticles(readCachedContent(ctx));
3091
3607
  const cache = ctx.state.ogImageHashCache;
3092
3608
  await loadDiskCache(ctx.config.outDir, cache);
3093
- const limit = pLimit(OG_CONCURRENCY);
3609
+ const limit = pLimit(4);
3094
3610
  let active = 0;
3095
3611
  let peakConcurrency = 0;
3096
3612
  let rendered = 0;
@@ -3163,7 +3679,6 @@ async function persistDiskCache(outDir, cache) {
3163
3679
  await (0, node_fs_promises.mkdir)(dir, { recursive: true });
3164
3680
  await (0, node_fs_promises.writeFile)(node_path.default.join(dir, "og-images.json"), JSON.stringify(Object.fromEntries(cache)), "utf8");
3165
3681
  }
3166
-
3167
3682
  //#endregion
3168
3683
  //#region src/plugins/build/phases/pages.tsx
3169
3684
  /**
@@ -3331,7 +3846,6 @@ async function renderPages(ctx) {
3331
3846
  rootHtml: root?.html ?? null
3332
3847
  };
3333
3848
  }
3334
-
3335
3849
  //#endregion
3336
3850
  //#region src/plugins/build/phases/sitemap.ts
3337
3851
  /**
@@ -3405,7 +3919,7 @@ async function generateSitemap(ctx) {
3405
3919
  const xml = serializeSitemap(urls);
3406
3920
  const robots = `User-agent: *\nAllow: /\nSitemap: ${site.canonical("/sitemap.xml")}\n`;
3407
3921
  await (0, node_fs_promises.mkdir)(ctx.config.outDir, { recursive: true });
3408
- await Promise.all([(0, node_fs_promises.writeFile)(node_path.default.join(ctx.config.outDir, "sitemap.xml"), xml, "utf8"), (0, node_fs_promises.writeFile)(node_path.default.join(ctx.config.outDir, "robots.txt"), robots, "utf8")]);
3922
+ await Promise.all([(0, node_fs_promises.writeFile)(node_path$1.default.join(ctx.config.outDir, "sitemap.xml"), xml, "utf8"), (0, node_fs_promises.writeFile)(node_path$1.default.join(ctx.config.outDir, "robots.txt"), robots, "utf8")]);
3409
3923
  ctx.log.debug("build:sitemap", { urls: urls.length });
3410
3924
  return {
3411
3925
  urls,
@@ -3413,7 +3927,6 @@ async function generateSitemap(ctx) {
3413
3927
  robots
3414
3928
  };
3415
3929
  }
3416
-
3417
3930
  //#endregion
3418
3931
  //#region src/plugins/build/pipeline.ts
3419
3932
  /**
@@ -3534,7 +4047,7 @@ async function runPipeline(ctx, options) {
3534
4047
  const pages = await withPhase(phaseContext, "pages", () => renderPages(phaseContext));
3535
4048
  await runOutputs(phaseContext);
3536
4049
  await withPhase(phaseContext, "root-index", async () => {
3537
- if (pages.rootHtml !== null) await (0, node_fs_promises.writeFile)(node_path.default.join(outDir, "index.html"), pages.rootHtml, "utf8");
4050
+ if (pages.rootHtml !== null) await (0, node_fs_promises.writeFile)(node_path$1.default.join(outDir, "index.html"), pages.rootHtml, "utf8");
3538
4051
  });
3539
4052
  const result = {
3540
4053
  outDir,
@@ -3544,7 +4057,6 @@ async function runPipeline(ctx, options) {
3544
4057
  phaseContext.emit("build:complete", result);
3545
4058
  return result;
3546
4059
  }
3547
-
3548
4060
  //#endregion
3549
4061
  //#region src/plugins/build/api.ts
3550
4062
  /**
@@ -3583,9 +4095,29 @@ const defaultConfig$1 = {
3583
4095
  */
3584
4096
  function createApi$2(ctx) {
3585
4097
  return {
4098
+ /**
4099
+ * Run the full SSG pipeline and write the site to disk.
4100
+ *
4101
+ * @param options - Optional run overrides.
4102
+ * @param options.outDir - Override the configured output directory for this run.
4103
+ * @returns The build result (outDir, pageCount, durationMs).
4104
+ * @example
4105
+ * ```ts
4106
+ * await api.run({ outDir: "./preview" });
4107
+ * ```
4108
+ */
3586
4109
  run(options) {
3587
4110
  return runPipeline(ctx, options);
3588
4111
  },
4112
+ /**
4113
+ * List the phases in execution order (introspection / tooling).
4114
+ *
4115
+ * @returns A fresh array of the static ordered phase names.
4116
+ * @example
4117
+ * ```ts
4118
+ * api.phases();
4119
+ * ```
4120
+ */
3589
4121
  phases() {
3590
4122
  return [...PHASE_ORDER];
3591
4123
  }
@@ -3620,7 +4152,6 @@ function validateConfig$1(config) {
3620
4152
  if (typeof config.outDir !== "string" || config.outDir.trim().length === 0) throw new Error(`${ERROR_PREFIX$5}.outDir: must be a non-empty string.`);
3621
4153
  if (config.ogImage) validateFonts(config.ogImage);
3622
4154
  }
3623
-
3624
4155
  //#endregion
3625
4156
  //#region src/plugins/build/events.ts
3626
4157
  /**
@@ -3640,7 +4171,6 @@ function createEvents(register) {
3640
4171
  "build:complete": register("Emitted once after a successful build run")
3641
4172
  };
3642
4173
  }
3643
-
3644
4174
  //#endregion
3645
4175
  //#region src/plugins/build/state.ts
3646
4176
  /**
@@ -3667,13 +4197,33 @@ function createState$2(ctx) {
3667
4197
  ogImageHashCache: /* @__PURE__ */ new Map()
3668
4198
  };
3669
4199
  }
3670
-
3671
4200
  //#endregion
3672
4201
  //#region src/plugins/build/index.ts
3673
4202
  /**
3674
4203
  * @file build — Complex plugin: SSG orchestrator (wiring harness only).
3675
4204
  * @see README.md
3676
4205
  */
4206
+ /**
4207
+ * Build plugin — the static-site-generation orchestrator. Renders every route to
4208
+ * `outDir`, and optionally emits feeds, a sitemap, optimized images, and OG
4209
+ * images. Depends on site, i18n, content, router, and head; emits `build:phase`.
4210
+ *
4211
+ * @example Configure the production build
4212
+ * ```ts
4213
+ * const app = createApp({
4214
+ * pluginConfigs: {
4215
+ * build: {
4216
+ * outDir: "dist",
4217
+ * minify: true,
4218
+ * feeds: true,
4219
+ * sitemap: true,
4220
+ * images: true,
4221
+ * ogImage: false // or an object to enable + configure OG-image generation
4222
+ * }
4223
+ * }
4224
+ * });
4225
+ * ```
4226
+ */
3677
4227
  const buildPlugin = createPlugin$1("build", {
3678
4228
  depends: [
3679
4229
  sitePlugin,
@@ -3688,7 +4238,6 @@ const buildPlugin = createPlugin$1("build", {
3688
4238
  api: createApi$2,
3689
4239
  onInit: (ctx) => validateConfig$1(ctx.config)
3690
4240
  });
3691
-
3692
4241
  //#endregion
3693
4242
  //#region src/plugins/deploy/wrangler.ts
3694
4243
  /**
@@ -3795,9 +4344,9 @@ function guardBranch(branch) {
3795
4344
  * assertWithinRoot("dist", process.cwd());
3796
4345
  */
3797
4346
  function assertWithinRoot(outDir, root) {
3798
- const resolved = node_path.default.isAbsolute(outDir) ? node_path.default.resolve(outDir) : node_path.default.resolve(root, outDir);
3799
- const rootResolved = node_path.default.resolve(root);
3800
- if (resolved !== rootResolved && !resolved.startsWith(rootResolved + node_path.default.sep)) throw deployError("ERR_DEPLOY_PATH_TRAVERSAL", `${ERROR_PREFIX$4}: outDir ${JSON.stringify(outDir)} resolves outside the project root.\n Point outDir at a directory inside ${JSON.stringify(rootResolved)}.`);
4347
+ const resolved = node_path$1.default.isAbsolute(outDir) ? node_path$1.default.resolve(outDir) : node_path$1.default.resolve(root, outDir);
4348
+ const rootResolved = node_path$1.default.resolve(root);
4349
+ if (resolved !== rootResolved && !resolved.startsWith(rootResolved + node_path$1.default.sep)) throw deployError("ERR_DEPLOY_PATH_TRAVERSAL", `${ERROR_PREFIX$4}: outDir ${JSON.stringify(outDir)} resolves outside the project root.\n Point outDir at a directory inside ${JSON.stringify(rootResolved)}.`);
3801
4350
  return resolved;
3802
4351
  }
3803
4352
  /**
@@ -3866,8 +4415,6 @@ const ERROR_SIGNATURES = [
3866
4415
  advice: "A network failure occurred. Check connectivity and retry."
3867
4416
  }
3868
4417
  ];
3869
- /** Number of trailing characters of scrubbed stderr to surface on an unknown failure. */
3870
- const STDERR_TAIL_LENGTH = 500;
3871
4418
  /**
3872
4419
  * Map a non-zero wrangler exit and scrubbed stderr to an actionable error
3873
4420
  * `code` + message. Matching is case-insensitive against the scrubbed stderr;
@@ -3888,7 +4435,7 @@ function classifyWranglerError(exitCode, scrubbedStderr) {
3888
4435
  };
3889
4436
  return {
3890
4437
  code: "ERR_DEPLOY_WRANGLER_FAILED",
3891
- message: `${ERROR_PREFIX$4}: wrangler failed (exit ${exitCode}).\n ${scrubbedStderr.trim().slice(-STDERR_TAIL_LENGTH)}`
4438
+ message: `${ERROR_PREFIX$4}: wrangler failed (exit ${exitCode}).\n ${scrubbedStderr.trim().slice(-500)}`
3892
4439
  };
3893
4440
  }
3894
4441
  /**
@@ -3947,7 +4494,6 @@ async function runWrangler(input) {
3947
4494
  exitCode
3948
4495
  };
3949
4496
  }
3950
-
3951
4497
  //#endregion
3952
4498
  //#region src/plugins/deploy/generators/github-workflow.ts
3953
4499
  /**
@@ -4002,7 +4548,6 @@ jobs:
4002
4548
  command: pages deploy dist --project-name ${input.slug}
4003
4549
  `;
4004
4550
  }
4005
-
4006
4551
  //#endregion
4007
4552
  //#region src/plugins/deploy/generators/wrangler-config.ts
4008
4553
  /**
@@ -4041,12 +4586,11 @@ function generateWranglerConfig(input) {
4041
4586
  */
4042
4587
  async function readWranglerConfig(cwd) {
4043
4588
  try {
4044
- return await (0, node_fs_promises.readFile)(node_path.default.join(cwd, "wrangler.jsonc"), "utf8");
4589
+ return await (0, node_fs_promises.readFile)(node_path$1.default.join(cwd, "wrangler.jsonc"), "utf8");
4045
4590
  } catch {
4046
4591
  return null;
4047
4592
  }
4048
4593
  }
4049
-
4050
4594
  //#endregion
4051
4595
  //#region src/plugins/deploy/init.ts
4052
4596
  /**
@@ -4068,7 +4612,7 @@ const WORKFLOW_PATH = ".github/workflows/deploy.yml";
4068
4612
  */
4069
4613
  async function readMaybe(cwd, relativePath) {
4070
4614
  try {
4071
- return await (0, node_fs_promises.readFile)(node_path.default.join(cwd, relativePath), "utf8");
4615
+ return await (0, node_fs_promises.readFile)(node_path$1.default.join(cwd, relativePath), "utf8");
4072
4616
  } catch {
4073
4617
  return null;
4074
4618
  }
@@ -4146,11 +4690,10 @@ async function reconcile(input) {
4146
4690
  result.skipped.push(relativePath);
4147
4691
  return;
4148
4692
  }
4149
- await (0, node_fs_promises.mkdir)(node_path.default.dirname(node_path.default.join(cwd, relativePath)), { recursive: true });
4150
- await (0, node_fs_promises.writeFile)(node_path.default.join(cwd, relativePath), expected, "utf8");
4693
+ await (0, node_fs_promises.mkdir)(node_path$1.default.dirname(node_path$1.default.join(cwd, relativePath)), { recursive: true });
4694
+ await (0, node_fs_promises.writeFile)(node_path$1.default.join(cwd, relativePath), expected, "utf8");
4151
4695
  result.written.push(relativePath);
4152
4696
  }
4153
-
4154
4697
  //#endregion
4155
4698
  //#region src/plugins/deploy/preflight.ts
4156
4699
  /**
@@ -4202,7 +4745,7 @@ async function inspectOutdir(dir) {
4202
4745
  if (current === void 0) break;
4203
4746
  const entries = await (0, node_fs_promises.readdir)(current, { withFileTypes: true });
4204
4747
  for (const entry of entries) {
4205
- const entryPath = node_path.default.join(current, entry.name);
4748
+ const entryPath = node_path$1.default.join(current, entry.name);
4206
4749
  if (entry.isDirectory()) stack.push(entryPath);
4207
4750
  else if (entry.isFile()) {
4208
4751
  result.fileCount += 1;
@@ -4230,13 +4773,13 @@ async function inspectOutdir(dir) {
4230
4773
  * await runPreflight(config, process.cwd());
4231
4774
  */
4232
4775
  async function runPreflight(config, root, env = process.env) {
4233
- const wranglerPath = node_path.default.join(root, "wrangler.jsonc");
4776
+ const wranglerPath = node_path$1.default.join(root, "wrangler.jsonc");
4234
4777
  try {
4235
4778
  await (0, node_fs_promises.stat)(wranglerPath);
4236
4779
  } catch {
4237
4780
  throw deployError("ERR_DEPLOY_NO_WRANGLER_CONFIG", `${ERROR_PREFIX$3}: wrangler.jsonc not found.\n Run \`app.deploy.init()\` to scaffold it, then retry.`);
4238
4781
  }
4239
- const stats = await inspectOutdir(node_path.default.isAbsolute(config.outDir) ? node_path.default.resolve(config.outDir) : node_path.default.resolve(root, config.outDir)).catch(() => {
4782
+ const stats = await inspectOutdir(node_path$1.default.isAbsolute(config.outDir) ? node_path$1.default.resolve(config.outDir) : node_path$1.default.resolve(root, config.outDir)).catch(() => {
4240
4783
  throw deployError("ERR_DEPLOY_EMPTY_OUTDIR", `${ERROR_PREFIX$3}: outDir ${JSON.stringify(config.outDir)} is missing.\n Run your build first, then retry.`);
4241
4784
  });
4242
4785
  if (stats.fileCount === 0) throw deployError("ERR_DEPLOY_EMPTY_OUTDIR", `${ERROR_PREFIX$3}: outDir ${JSON.stringify(config.outDir)} is empty — nothing to deploy.`);
@@ -4244,7 +4787,6 @@ async function runPreflight(config, root, env = process.env) {
4244
4787
  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.`);
4245
4788
  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.`);
4246
4789
  }
4247
-
4248
4790
  //#endregion
4249
4791
  //#region src/plugins/deploy/slug.ts
4250
4792
  /**
@@ -4285,7 +4827,6 @@ function toSlug(name) {
4285
4827
  slug = slug.slice(0, end);
4286
4828
  return slug.length > 0 ? slug : FALLBACK_SLUG;
4287
4829
  }
4288
-
4289
4830
  //#endregion
4290
4831
  //#region src/plugins/deploy/api.ts
4291
4832
  /** Error prefix for deploy config/validation failures (spec/11 Part-3). */
@@ -4325,6 +4866,15 @@ function validateConfig(ctx) {
4325
4866
  */
4326
4867
  function createApi$1(ctx) {
4327
4868
  return {
4869
+ /**
4870
+ * Deploy the built outDir to Cloudflare Pages via the wrangler subprocess.
4871
+ *
4872
+ * @param options - Optional branch override and build toggle.
4873
+ * @returns The deploy result (url, deploymentId, branch, durationMs).
4874
+ * @throws {Error} With a `code` from the deploy error taxonomy on any failure.
4875
+ * @example
4876
+ * await api.run();
4877
+ */
4328
4878
  async run(options = {}) {
4329
4879
  const root = process.cwd();
4330
4880
  const slug = toSlug(ctx.require(sitePlugin).name());
@@ -4364,10 +4914,25 @@ function createApi$1(ctx) {
4364
4914
  });
4365
4915
  return result;
4366
4916
  },
4917
+ /**
4918
+ * Return the most recent successful deploy result, or null if none occurred.
4919
+ *
4920
+ * @returns A frozen snapshot of the last DeployResult, or null.
4921
+ * @example
4922
+ * const last = api.getLastDeployment();
4923
+ */
4367
4924
  getLastDeployment() {
4368
4925
  const last = ctx.state.lastDeployment;
4369
4926
  return last ? Object.freeze({ ...last }) : null;
4370
4927
  },
4928
+ /**
4929
+ * Generate deploy scaffolding (wrangler.jsonc + optional GitHub workflow).
4930
+ *
4931
+ * @param options - Optional ci toggle and check (drift-only) mode.
4932
+ * @returns Which files were written, skipped, or would drift.
4933
+ * @example
4934
+ * await api.init({ ci: true });
4935
+ */
4371
4936
  async init(options = {}) {
4372
4937
  const slug = toSlug(ctx.require(sitePlugin).name());
4373
4938
  return writeScaffolding({
@@ -4379,7 +4944,6 @@ function createApi$1(ctx) {
4379
4944
  }
4380
4945
  };
4381
4946
  }
4382
-
4383
4947
  //#endregion
4384
4948
  //#region src/plugins/deploy/defaults.ts
4385
4949
  /**
@@ -4399,7 +4963,6 @@ const defaultConfig = {
4399
4963
  compatibilityDate: "2024-01-01",
4400
4964
  ci: false
4401
4965
  };
4402
-
4403
4966
  //#endregion
4404
4967
  //#region src/plugins/deploy/events.ts
4405
4968
  /**
@@ -4414,7 +4977,6 @@ const defaultConfig = {
4414
4977
  * ```
4415
4978
  */
4416
4979
  const deployEvents = (register) => ({ "deploy:complete": register("Deployment completed successfully") });
4417
-
4418
4980
  //#endregion
4419
4981
  //#region src/plugins/deploy/state.ts
4420
4982
  /**
@@ -4452,7 +5014,6 @@ function createState$1(_ctx) {
4452
5014
  spawn: defaultSpawn
4453
5015
  };
4454
5016
  }
4455
-
4456
5017
  //#endregion
4457
5018
  //#region src/plugins/deploy/index.ts
4458
5019
  /**
@@ -4462,6 +5023,24 @@ function createState$1(_ctx) {
4462
5023
  * Depends: site. Emits: deploy:complete.
4463
5024
  * @see README.md
4464
5025
  */
5026
+ /**
5027
+ * Deploy plugin — ships the built `outDir` to Cloudflare Pages via the injectable
5028
+ * wrangler subprocess, with entropy-gated secret scrubbing of logged output.
5029
+ * Depends on site; emits `deploy:complete`.
5030
+ *
5031
+ * @example Configure the deploy target
5032
+ * ```ts
5033
+ * const app = createApp({
5034
+ * pluginConfigs: {
5035
+ * deploy: {
5036
+ * target: "cloudflare-pages",
5037
+ * outDir: "dist",
5038
+ * productionBranch: "main"
5039
+ * }
5040
+ * }
5041
+ * });
5042
+ * ```
5043
+ */
4465
5044
  const deployPlugin = createPlugin$1("deploy", {
4466
5045
  config: defaultConfig,
4467
5046
  depends: [sitePlugin],
@@ -4470,7 +5049,6 @@ const deployPlugin = createPlugin$1("deploy", {
4470
5049
  onInit: validateConfig,
4471
5050
  api: createApi$1
4472
5051
  });
4473
-
4474
5052
  //#endregion
4475
5053
  //#region src/plugins/spa/api.ts
4476
5054
  /**
@@ -4485,19 +5063,39 @@ const deployPlugin = createPlugin$1("deploy", {
4485
5063
  */
4486
5064
  function createApi(ctx) {
4487
5065
  return {
5066
+ /**
5067
+ * Register a component definition (last-registered-wins); warns on collision.
5068
+ *
5069
+ * @param component - The component definition created via `createComponent`.
5070
+ * @example
5071
+ * app.spa.register(counter);
5072
+ */
4488
5073
  register(component) {
4489
5074
  if (ctx.state.registeredComponents.has(component.name)) ctx.log.warn("spa:component-collision", { name: component.name });
4490
5075
  ctx.state.kernel?.register(component);
4491
5076
  },
5077
+ /**
5078
+ * Programmatically navigate to a path (client runtime; no-op without a DOM).
5079
+ *
5080
+ * @param path - Target path (pathname, optionally with search/hash).
5081
+ * @example
5082
+ * app.spa.navigate("/about");
5083
+ */
4492
5084
  navigate(path) {
4493
5085
  ctx.state.kernel?.processNav(path);
4494
5086
  },
5087
+ /**
5088
+ * Read the current resolved URL.
5089
+ *
5090
+ * @returns The current pathname + search.
5091
+ * @example
5092
+ * app.spa.current();
5093
+ */
4495
5094
  current() {
4496
5095
  return ctx.state.currentUrl;
4497
5096
  }
4498
5097
  };
4499
5098
  }
4500
-
4501
5099
  //#endregion
4502
5100
  //#region src/plugins/spa/events.ts
4503
5101
  /**
@@ -4517,7 +5115,6 @@ function spaEvents(register) {
4517
5115
  "spa:component-unmount": register("A component instance detached from an element.")
4518
5116
  };
4519
5117
  }
4520
-
4521
5118
  //#endregion
4522
5119
  //#region src/plugins/spa/types.ts
4523
5120
  var types_exports$7 = /* @__PURE__ */ __exportAll({ COMPONENT_HOOK_NAMES: () => COMPONENT_HOOK_NAMES });
@@ -4530,11 +5127,7 @@ const COMPONENT_HOOK_NAMES = [
4530
5127
  "onUnMount",
4531
5128
  "onDestroy"
4532
5129
  ];
4533
-
4534
- //#endregion
4535
- //#region src/plugins/spa/components.ts
4536
- /** The set of legal hook names, frozen for O(1) membership checks. */
4537
- const HOOK_NAME_SET = new Set(COMPONENT_HOOK_NAMES);
5130
+ new Set(COMPONENT_HOOK_NAMES);
4538
5131
  /**
4539
5132
  * Extracts the page data payload from the inline `script#__DATA__` element.
4540
5133
  * Returns an empty object when the script is absent, empty, or invalid JSON.
@@ -4702,7 +5295,6 @@ function notifyNavEnd(state) {
4702
5295
  const data = typeof document === "undefined" ? {} : extractPageData(document);
4703
5296
  for (const [element, instance] of state.instances) if (instance.persistent) runHook(instance, "onNavEnd", makeContext(element, data));
4704
5297
  }
4705
-
4706
5298
  //#endregion
4707
5299
  //#region src/plugins/spa/head.ts
4708
5300
  /** Single-element head selectors synced by replace/append/remove on navigation. */
@@ -4777,7 +5369,6 @@ function syncHead(_head, doc) {
4777
5369
  for (const selector of META_SELECTORS) syncElement(selector, doc);
4778
5370
  for (const selector of REPLACE_ALL_SELECTORS) replaceAllBySelector(selector, doc);
4779
5371
  }
4780
-
4781
5372
  //#endregion
4782
5373
  //#region src/plugins/spa/progress.ts
4783
5374
  /** Delay before the bar appears, so fast navigations show no indicator. */
@@ -4861,7 +5452,6 @@ function createProgressBar(enabled) {
4861
5452
  done
4862
5453
  };
4863
5454
  }
4864
-
4865
5455
  //#endregion
4866
5456
  //#region src/plugins/spa/router.ts
4867
5457
  /**
@@ -5096,7 +5686,6 @@ function attachRouter(handlers) {
5096
5686
  const navigation = getNavigation();
5097
5687
  return navigation ? attachNavigationApi(navigation, handlers) : attachHistoryFallback(handlers);
5098
5688
  }
5099
-
5100
5689
  //#endregion
5101
5690
  //#region src/plugins/spa/state.ts
5102
5691
  /** Error prefix for spa config-validation failures (spec/11 Part-3). */
@@ -5170,7 +5759,6 @@ function createState(_ctx) {
5170
5759
  kernel: null
5171
5760
  };
5172
5761
  }
5173
-
5174
5762
  //#endregion
5175
5763
  //#region src/plugins/spa/kernel.ts
5176
5764
  /**
@@ -5279,10 +5867,22 @@ function createSpaKernel(state, config, emit, deps) {
5279
5867
  onError: handleError
5280
5868
  };
5281
5869
  return {
5870
+ /**
5871
+ * Register config components and seed currentUrl from the document.
5872
+ *
5873
+ * @example
5874
+ * kernel.init();
5875
+ */
5282
5876
  init() {
5283
5877
  for (const component of resolved.components) registerComponent(state, component);
5284
5878
  state.currentUrl = currentLocationUrl();
5285
5879
  },
5880
+ /**
5881
+ * Boot navigation interception + initial scan (throws if already started).
5882
+ *
5883
+ * @example
5884
+ * kernel.boot();
5885
+ */
5286
5886
  boot() {
5287
5887
  if (typeof document === "undefined") return;
5288
5888
  if (state.started) throw new Error(`${ERROR_PREFIX} spa kernel already started.\n Call app.stop() before booting again (single boot per app).`);
@@ -5292,16 +5892,42 @@ function createSpaKernel(state, config, emit, deps) {
5292
5892
  scanAndMount(state, emit, resolved.swapSelector);
5293
5893
  state.started = true;
5294
5894
  },
5895
+ /**
5896
+ * Register a component definition (last-registered-wins).
5897
+ *
5898
+ * @param component - The component definition to register.
5899
+ * @example
5900
+ * kernel.register(counter);
5901
+ */
5295
5902
  register(component) {
5296
5903
  registerComponent(state, component);
5297
5904
  },
5905
+ /**
5906
+ * Process a navigation to `path` (fetch then swap; full reload on error).
5907
+ *
5908
+ * @param path - The target path to navigate to.
5909
+ * @example
5910
+ * kernel.processNav("/about");
5911
+ */
5298
5912
  processNav(path) {
5299
5913
  if (typeof document === "undefined") return;
5300
5914
  performNavigation(path, handlers).catch(() => {});
5301
5915
  },
5916
+ /**
5917
+ * Scan the swap region and mount components for matching elements.
5918
+ *
5919
+ * @example
5920
+ * kernel.scan();
5921
+ */
5302
5922
  scan() {
5303
5923
  scanAndMount(state, emit, resolved.swapSelector);
5304
5924
  },
5925
+ /**
5926
+ * Tear down router listeners, dispose all instances, reset boot state.
5927
+ *
5928
+ * @example
5929
+ * kernel.dispose();
5930
+ */
5305
5931
  dispose() {
5306
5932
  state.destroyRouter?.();
5307
5933
  state.destroyRouter = null;
@@ -5330,7 +5956,6 @@ function initSpa(ctx) {
5330
5956
  kernelRef.current = kernel;
5331
5957
  kernel.init();
5332
5958
  }
5333
-
5334
5959
  //#endregion
5335
5960
  //#region src/plugins/spa/lifecycle.ts
5336
5961
  /** Router/instance teardown captured during onStart (undefined when stopped). */
@@ -5378,7 +6003,6 @@ function disposeSpa() {
5378
6003
  logRef = void 0;
5379
6004
  }
5380
6005
  }
5381
-
5382
6006
  //#endregion
5383
6007
  //#region src/plugins/spa/index.ts
5384
6008
  /**
@@ -5389,6 +6013,26 @@ function disposeSpa() {
5389
6013
  * Emits: spa:navigate, spa:navigated, spa:component-mount, spa:component-unmount.
5390
6014
  * @see README.md
5391
6015
  */
6016
+ /**
6017
+ * SPA plugin — progressive client-side navigation layered over the static site:
6018
+ * swaps a page region on navigation, with an optional progress bar and View
6019
+ * Transitions. Register interactive islands with {@link createComponent}. Depends
6020
+ * on router and head; emits `spa:navigate`, `spa:navigated`, `spa:component-mount`,
6021
+ * and `spa:component-unmount`.
6022
+ *
6023
+ * @example Enable view transitions and a custom swap region
6024
+ * ```ts
6025
+ * const app = createApp({
6026
+ * pluginConfigs: {
6027
+ * spa: {
6028
+ * swapSelector: "main > section",
6029
+ * viewTransitions: true,
6030
+ * progressBar: true
6031
+ * }
6032
+ * }
6033
+ * });
6034
+ * ```
6035
+ */
5392
6036
  const spaPlugin = createPlugin$1("spa", {
5393
6037
  depends: [routerPlugin, headPlugin],
5394
6038
  config: defaultSpaConfig,
@@ -5402,35 +6046,27 @@ const spaPlugin = createPlugin$1("spa", {
5402
6046
  },
5403
6047
  onStop: disposeSpa
5404
6048
  });
5405
-
5406
6049
  //#endregion
5407
6050
  //#region src/plugins/build/types.ts
5408
6051
  var types_exports = /* @__PURE__ */ __exportAll({});
5409
-
5410
6052
  //#endregion
5411
6053
  //#region src/plugins/content/types.ts
5412
6054
  var types_exports$1 = /* @__PURE__ */ __exportAll({});
5413
-
5414
6055
  //#endregion
5415
6056
  //#region src/plugins/deploy/types.ts
5416
6057
  var types_exports$2 = /* @__PURE__ */ __exportAll({});
5417
-
5418
6058
  //#endregion
5419
6059
  //#region src/plugins/env/types.ts
5420
6060
  var types_exports$3 = /* @__PURE__ */ __exportAll({});
5421
-
5422
6061
  //#endregion
5423
6062
  //#region src/plugins/head/types.ts
5424
6063
  var types_exports$4 = /* @__PURE__ */ __exportAll({});
5425
-
5426
6064
  //#endregion
5427
6065
  //#region src/plugins/log/types.ts
5428
6066
  var types_exports$5 = /* @__PURE__ */ __exportAll({});
5429
-
5430
6067
  //#endregion
5431
6068
  //#region src/plugins/router/types.ts
5432
6069
  var types_exports$6 = /* @__PURE__ */ __exportAll({});
5433
-
5434
6070
  //#endregion
5435
6071
  //#region src/index.ts
5436
6072
  /**
@@ -5450,56 +6086,94 @@ const framework = createCore(coreConfig, {
5450
6086
  ],
5451
6087
  pluginConfigs: {}
5452
6088
  });
5453
- const { createApp, createPlugin } = framework;
5454
-
6089
+ /**
6090
+ * Create and initialize a `@moku-labs/web` application — the Layer-3 entry point.
6091
+ * Your overrides are merged over the framework defaults through the 4-level config
6092
+ * cascade, every plugin's lifecycle runs, and a fully-typed, frozen app is returned.
6093
+ *
6094
+ * @param options - Optional configuration:
6095
+ * - `pluginConfigs` — per-plugin overrides, keyed by plugin name
6096
+ * (`site`, `i18n`, `router`, `content`, `head`, `build`, `spa`, `deploy`, `env`).
6097
+ * - `config` — global framework config (e.g. `{ mode: "development" }`).
6098
+ * - `plugins` — extra consumer plugins, merged into the app and its return type.
6099
+ * - `onReady` / `onError` / `onStart` / `onStop` — lifecycle callbacks.
6100
+ * @returns The initialized app: `start()`, `stop()`, every plugin's API, and `log`.
6101
+ * @example
6102
+ * ```ts
6103
+ * const app = createApp({
6104
+ * pluginConfigs: {
6105
+ * site: { name: "My Blog", url: "https://blog.dev", author: "Ada", description: "Notes" },
6106
+ * router: { routes: defineRoutes({ home: route("/"), post: route("/blog/{slug}/") }) }
6107
+ * }
6108
+ * });
6109
+ * await app.start();
6110
+ * ```
6111
+ */
6112
+ const createApp = framework.createApp;
6113
+ /**
6114
+ * Create a custom plugin bound to this framework's `Config`/`Events` and core
6115
+ * APIs. Plugin types are inferred from the spec object — never written explicitly.
6116
+ * Pass the result to {@link createApp} via `plugins`.
6117
+ *
6118
+ * @example
6119
+ * ```ts
6120
+ * const analytics = createPlugin("analytics", {
6121
+ * config: { writeKey: "" },
6122
+ * api: (ctx) => ({ track: (event: string) => ctx.log.info("analytics:track", { event }) })
6123
+ * });
6124
+ *
6125
+ * const app = createApp({ plugins: [analytics] });
6126
+ * ```
6127
+ */
6128
+ const createPlugin = framework.createPlugin;
5455
6129
  //#endregion
5456
- Object.defineProperty(exports, 'Build', {
5457
- enumerable: true,
5458
- get: function () {
5459
- return types_exports;
5460
- }
6130
+ Object.defineProperty(exports, "Build", {
6131
+ enumerable: true,
6132
+ get: function() {
6133
+ return types_exports;
6134
+ }
5461
6135
  });
5462
- Object.defineProperty(exports, 'Content', {
5463
- enumerable: true,
5464
- get: function () {
5465
- return types_exports$1;
5466
- }
6136
+ Object.defineProperty(exports, "Content", {
6137
+ enumerable: true,
6138
+ get: function() {
6139
+ return types_exports$1;
6140
+ }
5467
6141
  });
5468
- Object.defineProperty(exports, 'Deploy', {
5469
- enumerable: true,
5470
- get: function () {
5471
- return types_exports$2;
5472
- }
6142
+ Object.defineProperty(exports, "Deploy", {
6143
+ enumerable: true,
6144
+ get: function() {
6145
+ return types_exports$2;
6146
+ }
5473
6147
  });
5474
- Object.defineProperty(exports, 'Env', {
5475
- enumerable: true,
5476
- get: function () {
5477
- return types_exports$3;
5478
- }
6148
+ Object.defineProperty(exports, "Env", {
6149
+ enumerable: true,
6150
+ get: function() {
6151
+ return types_exports$3;
6152
+ }
5479
6153
  });
5480
- Object.defineProperty(exports, 'Head', {
5481
- enumerable: true,
5482
- get: function () {
5483
- return types_exports$4;
5484
- }
6154
+ Object.defineProperty(exports, "Head", {
6155
+ enumerable: true,
6156
+ get: function() {
6157
+ return types_exports$4;
6158
+ }
5485
6159
  });
5486
- Object.defineProperty(exports, 'Log', {
5487
- enumerable: true,
5488
- get: function () {
5489
- return types_exports$5;
5490
- }
6160
+ Object.defineProperty(exports, "Log", {
6161
+ enumerable: true,
6162
+ get: function() {
6163
+ return types_exports$5;
6164
+ }
5491
6165
  });
5492
- Object.defineProperty(exports, 'Router', {
5493
- enumerable: true,
5494
- get: function () {
5495
- return types_exports$6;
5496
- }
6166
+ Object.defineProperty(exports, "Router", {
6167
+ enumerable: true,
6168
+ get: function() {
6169
+ return types_exports$6;
6170
+ }
5497
6171
  });
5498
- Object.defineProperty(exports, 'Spa', {
5499
- enumerable: true,
5500
- get: function () {
5501
- return types_exports$7;
5502
- }
6172
+ Object.defineProperty(exports, "Spa", {
6173
+ enumerable: true,
6174
+ get: function() {
6175
+ return types_exports$7;
6176
+ }
5503
6177
  });
5504
6178
  exports.buildArticleHead = buildArticleHead;
5505
6179
  exports.buildPlugin = buildPlugin;
@@ -5522,4 +6196,4 @@ exports.route = route;
5522
6196
  exports.routerPlugin = routerPlugin;
5523
6197
  exports.sitePlugin = sitePlugin;
5524
6198
  exports.spaPlugin = spaPlugin;
5525
- exports.twitter = twitter;
6199
+ exports.twitter = twitter;