@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.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,29 +793,40 @@ 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" },
649
806
  env: { providers: [dotenv(), processEnv()] }
650
807
  }
651
808
  });
652
- const { createPlugin: createPlugin$1, createCore } = coreConfig;
653
-
809
+ /**
810
+ * Create a custom plugin bound to this framework's `Config`/`Events` and the core
811
+ * plugin APIs (`log`, `env`). Plugin types are fully inferred from the spec
812
+ * object — never write them explicitly. This is the binding every built-in
813
+ * plugin is wired with, and the one consumer plugins should use too.
814
+ *
815
+ * @example
816
+ * ```ts
817
+ * const analytics = createPlugin("analytics", {
818
+ * config: { writeKey: "" },
819
+ * api: (ctx) => ({ track: (event: string) => ctx.log.info("analytics:track", { event }) })
820
+ * });
821
+ * ```
822
+ */
823
+ const createPlugin$1 = coreConfig.createPlugin;
824
+ /**
825
+ * Step 2 of the factory chain — captures the framework's default plugin set and
826
+ * returns the consumer entry points ({@link createApp} + a re-exported
827
+ * `createPlugin`). Wired once in `src/index.ts`; consumers don't call it directly.
828
+ */
829
+ const createCore = coreConfig.createCore;
654
830
  //#endregion
655
831
  //#region src/plugins/i18n/api.ts
656
832
  /** Error prefix for all i18n lifecycle failures. */
@@ -690,21 +866,83 @@ function validateI18nConfig(ctx) {
690
866
  function createI18nApi(ctx) {
691
867
  const { config } = ctx;
692
868
  return {
869
+ /**
870
+ * Returns the configured supported locales in declared order.
871
+ *
872
+ * @returns The configured `locales` list (priority/display order).
873
+ * @example
874
+ * ```ts
875
+ * api.locales(); // ["en", "uk"]
876
+ * ```
877
+ */
693
878
  locales() {
694
879
  return config.locales;
695
880
  },
881
+ /**
882
+ * Returns the fallback locale used when a requested locale is absent.
883
+ *
884
+ * @returns The configured `defaultLocale`.
885
+ * @example
886
+ * ```ts
887
+ * api.defaultLocale(); // "en"
888
+ * ```
889
+ */
696
890
  defaultLocale() {
697
891
  return config.defaultLocale;
698
892
  },
893
+ /**
894
+ * Membership guard: whether `x` is one of the supported locales
895
+ * (case-sensitive).
896
+ *
897
+ * @param x - Candidate locale code.
898
+ * @returns `true` if `x ∈ locales`, else `false`.
899
+ * @example
900
+ * ```ts
901
+ * api.isLocale("uk"); // true
902
+ * ```
903
+ */
699
904
  isLocale(x) {
700
905
  return config.locales.includes(x);
701
906
  },
907
+ /**
908
+ * Human-readable display name for a locale.
909
+ *
910
+ * @param locale - Locale code to look up.
911
+ * @returns The display name, or `undefined` if unmapped.
912
+ * @example
913
+ * ```ts
914
+ * api.localeName("uk"); // "Українська"
915
+ * ```
916
+ */
702
917
  localeName(locale) {
703
918
  return config.localeNames?.[locale];
704
919
  },
920
+ /**
921
+ * Open Graph `og:locale` value for a locale.
922
+ *
923
+ * @param locale - Locale code to look up.
924
+ * @returns The `og:locale` value (e.g. `"en_US"`), or `undefined` if unmapped.
925
+ * @example
926
+ * ```ts
927
+ * api.ogLocale("en"); // "en_US"
928
+ * ```
929
+ */
705
930
  ogLocale(locale) {
706
931
  return config.ogLocaleMap?.[locale];
707
932
  },
933
+ /**
934
+ * Translate `key` for `locale` with a deterministic fallback chain
935
+ * (requested locale → default locale → the key itself). The default-locale
936
+ * lookup is skipped when `locale === defaultLocale`.
937
+ *
938
+ * @param locale - Requested locale code.
939
+ * @param key - Translation key (e.g. `"nav.home"`).
940
+ * @returns The translated value, the default-locale value, or `key`.
941
+ * @example
942
+ * ```ts
943
+ * api.t("uk", "nav.home"); // "Головна"
944
+ * ```
945
+ */
708
946
  t(locale, key) {
709
947
  const exact = config.translations?.[locale]?.[key];
710
948
  if (exact !== void 0) return exact;
@@ -716,34 +954,36 @@ function createI18nApi(ctx) {
716
954
  }
717
955
  };
718
956
  }
719
-
720
- //#endregion
721
- //#region src/plugins/i18n/index.ts
722
957
  /**
723
- * i18nMicro 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)`.
958
+ * Internationalization plugin locale registry plus a flat translation helper
959
+ * with default-locale fallback. Pure config-as-data (no state or events);
960
+ * consumed read-only by content, router, head, and build.
729
961
  *
730
- * @file i18n plugin wiring harness.
731
- * @see README.md
962
+ * @example Register locales and translations
963
+ * ```ts
964
+ * const app = createApp({
965
+ * pluginConfigs: {
966
+ * i18n: {
967
+ * locales: ["en", "uk"],
968
+ * defaultLocale: "en",
969
+ * localeNames: { en: "English", uk: "Українська" },
970
+ * translations: { uk: { "nav.home": "Головна" } }
971
+ * }
972
+ * }
973
+ * });
974
+ * ```
732
975
  */
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
976
  const i18nPlugin = createPlugin$1("i18n", {
742
- config: defaultConfig$5,
977
+ config: {
978
+ locales: ["en"],
979
+ defaultLocale: "en",
980
+ localeNames: {},
981
+ ogLocaleMap: {},
982
+ translations: {}
983
+ },
743
984
  onInit: validateI18nConfig,
744
985
  api: createI18nApi
745
986
  });
746
-
747
987
  //#endregion
748
988
  //#region src/plugins/content/pipeline/frontmatter.ts
749
989
  /**
@@ -789,7 +1029,6 @@ function parseFrontmatter(raw, config) {
789
1029
  body: parsed.content
790
1030
  };
791
1031
  }
792
-
793
1032
  //#endregion
794
1033
  //#region src/plugins/content/pipeline/plugins.ts
795
1034
  /**
@@ -946,7 +1185,6 @@ function defaultRehypePlugins() {
946
1185
  sectionDividerPlugin
947
1186
  ];
948
1187
  }
949
-
950
1188
  //#endregion
951
1189
  //#region src/plugins/content/pipeline/sanitize.ts
952
1190
  /**
@@ -1033,7 +1271,6 @@ function buildSanitizeSchema() {
1033
1271
  }
1034
1272
  };
1035
1273
  }
1036
-
1037
1274
  //#endregion
1038
1275
  //#region src/plugins/content/pipeline/markdown.ts
1039
1276
  /**
@@ -1091,7 +1328,6 @@ function applyPluggable(processor, plugin) {
1091
1328
  }
1092
1329
  processor.use(plugin);
1093
1330
  }
1094
-
1095
1331
  //#endregion
1096
1332
  //#region src/plugins/content/pipeline/reading-time.ts
1097
1333
  /**
@@ -1117,7 +1353,6 @@ function calculateReadingTime(text) {
1117
1353
  wordCount: stats.words
1118
1354
  };
1119
1355
  }
1120
-
1121
1356
  //#endregion
1122
1357
  //#region src/plugins/content/api.ts
1123
1358
  /**
@@ -1329,6 +1564,18 @@ function toCard(article) {
1329
1564
  */
1330
1565
  function createContentApi(ctx) {
1331
1566
  return {
1567
+ /**
1568
+ * Load every article across every active locale, returning a locale-keyed
1569
+ * map of date-descending Article arrays. Lazily builds the processor and
1570
+ * discovers slugs, applies locale fallback, excludes drafts in production,
1571
+ * assigns `contentId` after sorting, then emits `content:ready`.
1572
+ *
1573
+ * @returns A locale-keyed map of date-descending articles.
1574
+ * @example
1575
+ * ```ts
1576
+ * const byLocale = await api.loadAll();
1577
+ * ```
1578
+ */
1332
1579
  async loadAll() {
1333
1580
  const slugs = ctx.state.slugs ?? await discoverSlugs(ctx.config.contentDir);
1334
1581
  ctx.state.slugs = slugs;
@@ -1356,6 +1603,19 @@ function createContentApi(ctx) {
1356
1603
  });
1357
1604
  return result;
1358
1605
  },
1606
+ /**
1607
+ * Resolve and render a single article for one locale with locale fallback.
1608
+ * Throws a `[web] content` error when neither the requested nor the
1609
+ * default-locale file exists.
1610
+ *
1611
+ * @param slug - Article directory name.
1612
+ * @param locale - Requested locale code.
1613
+ * @returns The resolved Article.
1614
+ * @example
1615
+ * ```ts
1616
+ * const article = await api.load("intro", "uk");
1617
+ * ```
1618
+ */
1359
1619
  async load(slug, locale) {
1360
1620
  const article = await resolveArticle(ctx, slug, locale);
1361
1621
  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 +1624,33 @@ function createContentApi(ctx) {
1364
1624
  ctx.state.articles.set(locale, cache);
1365
1625
  return article;
1366
1626
  },
1627
+ /**
1628
+ * Render a raw Markdown string to HTML through the full pipeline (sanitize
1629
+ * last when `trustedContent` is false). Lazily builds the processor.
1630
+ *
1631
+ * @param md - Raw Markdown source.
1632
+ * @returns The rendered HTML string.
1633
+ * @example
1634
+ * ```ts
1635
+ * const html = await api.renderMarkdown("# Hi");
1636
+ * ```
1637
+ */
1367
1638
  async renderMarkdown(md) {
1368
1639
  const processor = ensureProcessor(ctx.state, ctx.config);
1369
1640
  return String(await processor.process(md));
1370
1641
  },
1642
+ /**
1643
+ * Mark file paths stale for incremental dev rebuilds. Each non-blank path is
1644
+ * added to `dirtyPaths` and its derived slug cache entry is dropped so the
1645
+ * next `loadAll()` re-reads only those files. Empty/whitespace paths are
1646
+ * ignored. Emits `content:invalidated` with the accepted paths.
1647
+ *
1648
+ * @param paths - File paths to invalidate.
1649
+ * @example
1650
+ * ```ts
1651
+ * api.invalidate(["src/content/intro/en.md"]);
1652
+ * ```
1653
+ */
1371
1654
  invalidate(paths) {
1372
1655
  const accepted = [];
1373
1656
  for (const path of paths) {
@@ -1380,12 +1663,22 @@ function createContentApi(ctx) {
1380
1663
  ctx.state.slugs = null;
1381
1664
  ctx.emit("content:invalidated", { paths: accepted });
1382
1665
  },
1666
+ /**
1667
+ * Project a full Article to a lightweight ArticleCard for list/grid
1668
+ * rendering without shipping rendered HTML.
1669
+ *
1670
+ * @param article - The source article.
1671
+ * @returns The card projection.
1672
+ * @example
1673
+ * ```ts
1674
+ * const card = api.articleToCard(article);
1675
+ * ```
1676
+ */
1383
1677
  articleToCard(article) {
1384
1678
  return toCard(article);
1385
1679
  }
1386
1680
  };
1387
1681
  }
1388
-
1389
1682
  //#endregion
1390
1683
  //#region src/plugins/content/config.ts
1391
1684
  /**
@@ -1406,7 +1699,6 @@ const defaultContentConfig = {
1406
1699
  extraRehypePlugins: [],
1407
1700
  shikiTheme: "github-dark"
1408
1701
  };
1409
-
1410
1702
  //#endregion
1411
1703
  //#region src/plugins/content/events.ts
1412
1704
  /**
@@ -1425,7 +1717,6 @@ const contentEvents = (register) => ({
1425
1717
  "content:ready": register("All articles loaded across locales"),
1426
1718
  "content:invalidated": register("Article paths marked stale for dev rebuild")
1427
1719
  });
1428
-
1429
1720
  //#endregion
1430
1721
  //#region src/plugins/content/state.ts
1431
1722
  /**
@@ -1449,7 +1740,6 @@ function createContentState(_ctx) {
1449
1740
  dirtyPaths: /* @__PURE__ */ new Set()
1450
1741
  };
1451
1742
  }
1452
-
1453
1743
  //#endregion
1454
1744
  //#region src/plugins/content/validate.ts
1455
1745
  /**
@@ -1468,7 +1758,6 @@ function validateContentConfig(config) {
1468
1758
  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
1759
  if (typeof config.trustedContent !== "boolean") throw new TypeError("[web] content.trustedContent must be a boolean.");
1470
1760
  }
1471
-
1472
1761
  //#endregion
1473
1762
  //#region src/plugins/content/index.ts
1474
1763
  /**
@@ -1479,6 +1768,26 @@ function validateContentConfig(config) {
1479
1768
  * and `content:invalidated`.
1480
1769
  * @see README.md
1481
1770
  */
1771
+ /**
1772
+ * Content plugin — Markdown pipeline: discovers files, parses frontmatter, renders
1773
+ * to sanitized HTML (rehype-sanitize unless `trustedContent`), and exposes a
1774
+ * locale-keyed Article model. Depends on i18n; emits `content:ready` and
1775
+ * `content:invalidated`.
1776
+ *
1777
+ * @example Point at a content directory and pick a Shiki theme
1778
+ * ```ts
1779
+ * const app = createApp({
1780
+ * pluginConfigs: {
1781
+ * content: {
1782
+ * contentDir: "./content",
1783
+ * shikiTheme: "github-dark",
1784
+ * defaultAuthor: "Ada Lovelace"
1785
+ * // trustedContent: true // ONLY for fully author-controlled Markdown — disables sanitize
1786
+ * }
1787
+ * }
1788
+ * });
1789
+ * ```
1790
+ */
1482
1791
  const contentPlugin = createPlugin$1("content", {
1483
1792
  depends: [i18nPlugin],
1484
1793
  events: contentEvents,
@@ -1487,7 +1796,6 @@ const contentPlugin = createPlugin$1("content", {
1487
1796
  onInit: (ctx) => validateContentConfig(ctx.config),
1488
1797
  api: contentApi
1489
1798
  });
1490
-
1491
1799
  //#endregion
1492
1800
  //#region src/plugins/site/api.ts
1493
1801
  /** Error prefix for all site lifecycle/validation failures. */
@@ -1579,50 +1887,99 @@ function validateSiteConfig(ctx) {
1579
1887
  function createSiteApi(ctx) {
1580
1888
  const { config } = ctx;
1581
1889
  return {
1890
+ /**
1891
+ * Returns the configured site name.
1892
+ *
1893
+ * @returns The human-readable site name from `config.name`.
1894
+ * @example
1895
+ * ```ts
1896
+ * api.name(); // "My Blog"
1897
+ * ```
1898
+ */
1582
1899
  name() {
1583
1900
  return config.name;
1584
1901
  },
1902
+ /**
1903
+ * Returns the configured absolute base URL of the site.
1904
+ *
1905
+ * @returns The base URL from `config.url`.
1906
+ * @example
1907
+ * ```ts
1908
+ * api.url(); // "https://blog.dev"
1909
+ * ```
1910
+ */
1585
1911
  url() {
1586
1912
  return config.url;
1587
1913
  },
1914
+ /**
1915
+ * Returns the configured site author/byline.
1916
+ *
1917
+ * @returns The author from `config.author`.
1918
+ * @example
1919
+ * ```ts
1920
+ * api.author(); // "Alex"
1921
+ * ```
1922
+ */
1588
1923
  author() {
1589
1924
  return config.author;
1590
1925
  },
1926
+ /**
1927
+ * Returns the configured site description.
1928
+ *
1929
+ * @returns The description from `config.description`.
1930
+ * @example
1931
+ * ```ts
1932
+ * api.description(); // "A personal blog about web frameworks."
1933
+ * ```
1934
+ */
1591
1935
  description() {
1592
1936
  return config.description;
1593
1937
  },
1938
+ /**
1939
+ * Joins a path against the configured base `url` to produce an absolute
1940
+ * canonical URL. An empty path (or "/") returns the base URL unchanged.
1941
+ *
1942
+ * @param path - Relative path for the page, e.g. "/about/".
1943
+ * @returns The absolute canonical URL.
1944
+ * @example
1945
+ * ```ts
1946
+ * api.canonical("/about/"); // "https://blog.dev/about/"
1947
+ * ```
1948
+ */
1594
1949
  canonical(path) {
1595
1950
  return joinCanonical(config.url, path);
1596
1951
  }
1597
1952
  };
1598
1953
  }
1599
-
1600
- //#endregion
1601
- //#region src/plugins/site/index.ts
1602
1954
  /**
1603
- * siteMicro 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.
1955
+ * Site plugin holds global, frozen site metadata (name, url, author,
1956
+ * description) and builds canonical URLs. Consumed by router, head, and build.
1957
+ * `name` and `url` must be non-empty (validated at `onInit`).
1609
1958
  *
1610
- * @file site plugin wiring harness.
1611
- * @see README.md
1959
+ * @example Set your site identity
1960
+ * ```ts
1961
+ * const app = createApp({
1962
+ * pluginConfigs: {
1963
+ * site: {
1964
+ * name: "My Blog",
1965
+ * url: "https://blog.dev",
1966
+ * author: "Ada Lovelace",
1967
+ * description: "Notes on computing"
1968
+ * }
1969
+ * }
1970
+ * });
1971
+ * ```
1612
1972
  */
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
1973
  const sitePlugin = createPlugin$1("site", {
1621
- config: defaultConfig$4,
1974
+ config: {
1975
+ name: "",
1976
+ url: "",
1977
+ author: "",
1978
+ description: ""
1979
+ },
1622
1980
  onInit: validateSiteConfig,
1623
1981
  api: createSiteApi
1624
1982
  });
1625
-
1626
1983
  //#endregion
1627
1984
  //#region src/plugins/router/builders/match.ts
1628
1985
  /**
@@ -1692,7 +2049,6 @@ function matchRoute(compiled, pathname) {
1692
2049
  }
1693
2050
  return null;
1694
2051
  }
1695
-
1696
2052
  //#endregion
1697
2053
  //#region src/plugins/router/api.ts
1698
2054
  /**
@@ -1755,23 +2111,63 @@ function toTypedRoute(entry) {
1755
2111
  function createApi$4(ctx) {
1756
2112
  const { state } = ctx;
1757
2113
  return {
2114
+ /**
2115
+ * Match a pathname against the compiled route table (specificity-sorted).
2116
+ *
2117
+ * @param pathname - URL pathname, e.g. `/en/hello/`.
2118
+ * @returns `{ params, route }` for the most specific match, or `null`.
2119
+ * @example
2120
+ * ```ts
2121
+ * api.match("/en/hello/");
2122
+ * ```
2123
+ */
1758
2124
  match(pathname) {
1759
2125
  return matchRoute(readTable(state).compiled, pathname);
1760
2126
  },
2127
+ /**
2128
+ * Build a URL for a named route from params.
2129
+ *
2130
+ * @param routeName - Route name key from the route map.
2131
+ * @param params - Param values to substitute into the pattern.
2132
+ * @returns The resolved URL string (e.g. `/en/hello/`).
2133
+ * @throws {Error} If `routeName` is unknown.
2134
+ * @example
2135
+ * ```ts
2136
+ * api.toUrl("article", { lang: "en", slug: "hello" });
2137
+ * ```
2138
+ */
1761
2139
  toUrl(routeName, params) {
1762
2140
  const entry = readTable(state).byName.get(routeName);
1763
2141
  if (!entry) throw new Error(`${ERROR_PREFIX$9}: unknown route name "${routeName}".`);
1764
2142
  return entry.toUrl(params);
1765
2143
  },
2144
+ /**
2145
+ * All resolved routes as typed URL utilities, in specificity order.
2146
+ *
2147
+ * @returns A fresh read-only array of resolved typed routes.
2148
+ * @example
2149
+ * ```ts
2150
+ * for (const r of api.entries()) r.toUrl({ slug: "x" });
2151
+ * ```
2152
+ */
1766
2153
  entries() {
1767
2154
  return readTable(state).compiled.map((entry) => toTypedRoute(entry));
1768
2155
  },
2156
+ /**
2157
+ * The typed route set for build-time consumption (declaration order). An API
2158
+ * return, NOT a config readback — preserves per-route types despite erasure.
2159
+ *
2160
+ * @returns A fresh read-only array of the typed route definitions.
2161
+ * @example
2162
+ * ```ts
2163
+ * for (const def of api.manifest()) def._handlers.load?.({}, "en");
2164
+ * ```
2165
+ */
1769
2166
  manifest() {
1770
2167
  return [...readTable(state).byName.values()].map((entry) => entry.definition);
1771
2168
  }
1772
2169
  };
1773
2170
  }
1774
-
1775
2171
  //#endregion
1776
2172
  //#region src/plugins/router/builders/compile.ts
1777
2173
  /** Shared `[web]` error prefix for router validation failures. */
@@ -1933,9 +2329,31 @@ function compileRoute(name, definition, input) {
1933
2329
  dynamicSegmentCount: countDynamicSegments(pattern),
1934
2330
  matchers,
1935
2331
  matchFn: createMatchFunction(matchers, input.defaultLocale),
2332
+ /**
2333
+ * Build a URL for this route from params.
2334
+ *
2335
+ * @param params - Param values to substitute.
2336
+ * @returns The resolved relative URL.
2337
+ * @example
2338
+ * ```ts
2339
+ * entry.toUrl({ slug: "x" });
2340
+ * ```
2341
+ */
1936
2342
  toUrl(params) {
1937
2343
  return buildUrl(pattern, params, input.baseUrl);
1938
2344
  },
2345
+ /**
2346
+ * Build the output file path for this route from params. Honors a custom
2347
+ * `.toFile()` override (captured in `_handlers.toFile`) when present, falling
2348
+ * back to the pattern-derived `…/index.html` path otherwise.
2349
+ *
2350
+ * @param params - Param values to substitute.
2351
+ * @returns The output file path.
2352
+ * @example
2353
+ * ```ts
2354
+ * entry.toFile({ slug: "x" });
2355
+ * ```
2356
+ */
1939
2357
  toFile(params) {
1940
2358
  return definition._handlers.toFile?.(params) ?? buildFilePath(pattern, params);
1941
2359
  },
@@ -1996,7 +2414,6 @@ function buildRouterTable(config, baseUrl, locales, defaultLocale) {
1996
2414
  defaultLocale
1997
2415
  });
1998
2416
  }
1999
-
2000
2417
  //#endregion
2001
2418
  //#region src/plugins/router/builders/route-builder.ts
2002
2419
  /**
@@ -2042,28 +2459,108 @@ function route(pattern) {
2042
2459
  pattern: carrier.pattern,
2043
2460
  _meta: carrier._meta,
2044
2461
  _handlers: carrier._handlers,
2462
+ /**
2463
+ * Attach a data loader; widens the data generic for downstream handlers.
2464
+ *
2465
+ * @param loader - The loader producing this route's data.
2466
+ * @returns The same builder, with the data generic widened.
2467
+ * @example
2468
+ * ```ts
2469
+ * route("/{slug}/").load(({ slug }) => ({ slug }));
2470
+ * ```
2471
+ */
2045
2472
  load(loader) {
2046
2473
  return set("load", loader);
2047
2474
  },
2475
+ /**
2476
+ * Attach a layout wrapper component.
2477
+ *
2478
+ * @param component - The layout component.
2479
+ * @returns The same builder for chaining.
2480
+ * @example
2481
+ * ```ts
2482
+ * route("/").layout((children) => children);
2483
+ * ```
2484
+ */
2048
2485
  layout(component) {
2049
2486
  return set("layout", component);
2050
2487
  },
2488
+ /**
2489
+ * Attach the page render handler.
2490
+ *
2491
+ * @param handler - The render handler.
2492
+ * @returns The same builder for chaining.
2493
+ * @example
2494
+ * ```ts
2495
+ * route("/").render(() => null);
2496
+ * ```
2497
+ */
2051
2498
  render(handler) {
2052
2499
  return set("render", handler);
2053
2500
  },
2501
+ /**
2502
+ * Attach the head/SEO handler.
2503
+ *
2504
+ * @param handler - The head handler.
2505
+ * @returns The same builder for chaining.
2506
+ * @example
2507
+ * ```ts
2508
+ * route("/").head(() => ({ title: "Home" }));
2509
+ * ```
2510
+ */
2054
2511
  head(handler) {
2055
2512
  return set("head", handler);
2056
2513
  },
2514
+ /**
2515
+ * Attach a static-generation param producer.
2516
+ *
2517
+ * @param handler - The param producer.
2518
+ * @returns The same builder for chaining.
2519
+ * @example
2520
+ * ```ts
2521
+ * route("/{slug}/").generate(() => [{ slug: "x" }]);
2522
+ * ```
2523
+ */
2057
2524
  generate(handler) {
2058
2525
  return set("generate", handler);
2059
2526
  },
2527
+ /**
2528
+ * Merge an arbitrary metadata bag into the route's `_meta`.
2529
+ *
2530
+ * @param meta - Metadata to merge.
2531
+ * @returns The same builder for chaining.
2532
+ * @example
2533
+ * ```ts
2534
+ * route("/").meta({ activeTab: "home" });
2535
+ * ```
2536
+ */
2060
2537
  meta(meta) {
2061
2538
  Object.assign(carrier._meta, meta);
2062
2539
  return builder;
2063
2540
  },
2541
+ /**
2542
+ * Attach a JSON serializer for the route's data.
2543
+ *
2544
+ * @param handler - The JSON serializer.
2545
+ * @returns The same builder for chaining.
2546
+ * @example
2547
+ * ```ts
2548
+ * route("/api/").toJson(() => ({ ok: true }));
2549
+ * ```
2550
+ */
2064
2551
  toJson(handler) {
2065
2552
  return set("toJson", handler);
2066
2553
  },
2554
+ /**
2555
+ * Override the output file-path producer.
2556
+ *
2557
+ * @param handler - The file-path producer.
2558
+ * @returns The same builder for chaining.
2559
+ * @example
2560
+ * ```ts
2561
+ * route("/feed/").toFile(() => "feed.xml");
2562
+ * ```
2563
+ */
2067
2564
  toFile(handler) {
2068
2565
  return set("toFile", handler);
2069
2566
  }
@@ -2084,7 +2581,6 @@ function route(pattern) {
2084
2581
  function defineRoutes(routes) {
2085
2582
  return routes;
2086
2583
  }
2087
-
2088
2584
  //#endregion
2089
2585
  //#region src/plugins/router/state.ts
2090
2586
  /**
@@ -2103,25 +2599,36 @@ function defineRoutes(routes) {
2103
2599
  function createState$4(_ctx) {
2104
2600
  return { table: null };
2105
2601
  }
2106
-
2107
- //#endregion
2108
- //#region src/plugins/router/index.ts
2109
2602
  /**
2110
- * @file routerComplex plugin wiring (logic in builders/, api.ts, state.ts).
2111
- * @see README.md
2603
+ * Router plugintyped, named route definitions with locale-aware URL generation
2604
+ * and matching. Author routes with {@link route} + {@link defineRoutes}. Depends
2605
+ * on site (base URL) and i18n (locales).
2606
+ *
2607
+ * @example Define routes and choose a render mode
2608
+ * ```ts
2609
+ * const app = createApp({
2610
+ * pluginConfigs: {
2611
+ * router: {
2612
+ * routes: defineRoutes({
2613
+ * home: route("/"),
2614
+ * article: route("/blog/{slug}/")
2615
+ * }),
2616
+ * mode: "hybrid" // "ssg" | "spa" | "hybrid" (default)
2617
+ * }
2618
+ * }
2619
+ * });
2620
+ * ```
2112
2621
  */
2113
- /** Default router config: empty route map (validated in onInit), hybrid mode. */
2114
- const defaultConfig$3 = {
2115
- routes: {},
2116
- mode: "hybrid"
2117
- };
2118
2622
  const routerPlugin = createPlugin$1("router", {
2119
2623
  depends: [sitePlugin, i18nPlugin],
2120
2624
  helpers: {
2121
2625
  route,
2122
2626
  defineRoutes
2123
2627
  },
2124
- config: defaultConfig$3,
2628
+ config: {
2629
+ routes: {},
2630
+ mode: "hybrid"
2631
+ },
2125
2632
  createState: createState$4,
2126
2633
  api: createApi$4,
2127
2634
  onInit(ctx) {
@@ -2130,7 +2637,6 @@ const routerPlugin = createPlugin$1("router", {
2130
2637
  ctx.state.table = buildRouterTable(ctx.config, baseUrl, i18n.locales(), i18n.defaultLocale());
2131
2638
  }
2132
2639
  });
2133
-
2134
2640
  //#endregion
2135
2641
  //#region src/plugins/head/primitives.ts
2136
2642
  /** OG/Twitter article-meta property prefixes (factored to satisfy no-duplicate-string). */
@@ -2294,7 +2800,6 @@ function buildArticleHead(articleMeta, canonicalUrl) {
2294
2800
  elements.push(jsonLd(ld));
2295
2801
  return elements;
2296
2802
  }
2297
-
2298
2803
  //#endregion
2299
2804
  //#region src/plugins/head/compose.ts
2300
2805
  /**
@@ -2455,7 +2960,6 @@ function serializeElement(element) {
2455
2960
  function serializeHead(elements) {
2456
2961
  return elements.map((element) => serializeElement(element)).join("");
2457
2962
  }
2458
-
2459
2963
  //#endregion
2460
2964
  //#region src/plugins/head/api.ts
2461
2965
  /**
@@ -2497,7 +3001,19 @@ function readDefaults(state) {
2497
3001
  * ```
2498
3002
  */
2499
3003
  function createApi$3(ctx) {
2500
- return { render(route, data) {
3004
+ return {
3005
+ /**
3006
+ * Compose the final `<head>` inner HTML for a route (pulled by `build`).
3007
+ *
3008
+ * @param route - The resolved route descriptor (incl. its `.head()` HeadConfig).
3009
+ * @param data - The page data object passed to the route's loader/render.
3010
+ * @returns The serialized inner HTML of `<head>`.
3011
+ * @example
3012
+ * ```ts
3013
+ * api.render(route, { title: "Post" });
3014
+ * ```
3015
+ */
3016
+ render(route, data) {
2501
3017
  return serializeHead(composeHead({
2502
3018
  route,
2503
3019
  data,
@@ -2508,7 +3024,6 @@ function createApi$3(ctx) {
2508
3024
  }));
2509
3025
  } };
2510
3026
  }
2511
-
2512
3027
  //#endregion
2513
3028
  //#region src/plugins/head/config.ts
2514
3029
  /** Error prefix for all head config-validation failures. */
@@ -2563,7 +3078,6 @@ function normalizeHeadConfig(config) {
2563
3078
  if (config.twitterHandle !== void 0) defaults.twitterHandle = config.twitterHandle;
2564
3079
  return Object.freeze(defaults);
2565
3080
  }
2566
-
2567
3081
  //#endregion
2568
3082
  //#region src/plugins/head/helpers.ts
2569
3083
  /**
@@ -2592,7 +3106,6 @@ const headHelpers = {
2592
3106
  feedLink,
2593
3107
  buildArticleHead
2594
3108
  };
2595
-
2596
3109
  //#endregion
2597
3110
  //#region src/plugins/head/state.ts
2598
3111
  /**
@@ -2613,13 +3126,31 @@ const headHelpers = {
2613
3126
  function createState$3(_ctx) {
2614
3127
  return { defaults: null };
2615
3128
  }
2616
-
2617
3129
  //#endregion
2618
3130
  //#region src/plugins/head/index.ts
2619
3131
  /**
2620
3132
  * @file head — Standard Plugin wiring harness (logic in primitives/compose/api/config).
2621
3133
  * @see README.md
2622
3134
  */
3135
+ /**
3136
+ * Head plugin — composes per-route `<head>` metadata (title template, Open Graph,
3137
+ * Twitter cards, canonical, hreflang). Use the re-exported SEO primitives
3138
+ * ({@link meta}, {@link og}, {@link twitter}, …) inside a route's `.head()`.
3139
+ * Depends on site, i18n, and router.
3140
+ *
3141
+ * @example Set global head defaults
3142
+ * ```ts
3143
+ * const app = createApp({
3144
+ * pluginConfigs: {
3145
+ * head: {
3146
+ * titleTemplate: "%s — My Blog",
3147
+ * twitterCard: "summary_large_image",
3148
+ * twitterHandle: "@moku_labs"
3149
+ * }
3150
+ * }
3151
+ * });
3152
+ * ```
3153
+ */
2623
3154
  const headPlugin = createPlugin$1("head", {
2624
3155
  depends: [
2625
3156
  sitePlugin,
@@ -2634,7 +3165,6 @@ const headPlugin = createPlugin$1("head", {
2634
3165
  ctx.state.defaults = normalizeHeadConfig(ctx.config);
2635
3166
  }
2636
3167
  });
2637
-
2638
3168
  //#endregion
2639
3169
  //#region src/plugins/build/phases/bundle.ts
2640
3170
  /**
@@ -2736,7 +3266,6 @@ async function bundle(ctx, options = {}) {
2736
3266
  await runOne(ctx, runner, "css", cssEntrypoints, path.join(outDir, "assets"), minify);
2737
3267
  await runOne(ctx, runner, "js", jsEntrypoints, path.join(outDir, "assets"), minify);
2738
3268
  }
2739
-
2740
3269
  //#endregion
2741
3270
  //#region src/plugins/build/phases/content.ts
2742
3271
  /**
@@ -2779,7 +3308,6 @@ function readCachedContent(ctx) {
2779
3308
  const cached = ctx.state.buildCache.get(CONTENT_CACHE_KEY);
2780
3309
  return cached instanceof Map ? cached : /* @__PURE__ */ new Map();
2781
3310
  }
2782
-
2783
3311
  //#endregion
2784
3312
  //#region src/plugins/build/phases/feeds.ts
2785
3313
  /**
@@ -2861,7 +3389,6 @@ async function generateFeeds(ctx) {
2861
3389
  ctx.log.debug("build:feeds", { items: guids.length });
2862
3390
  return result;
2863
3391
  }
2864
-
2865
3392
  //#endregion
2866
3393
  //#region src/plugins/build/phases/images.ts
2867
3394
  /**
@@ -2901,7 +3428,6 @@ async function processImages(ctx, options = {}) {
2901
3428
  ctx.log.debug("build:images", { copied });
2902
3429
  return copied;
2903
3430
  }
2904
-
2905
3431
  //#endregion
2906
3432
  //#region src/plugins/build/phases/og-images.tsx
2907
3433
  /**
@@ -2915,8 +3441,6 @@ const DEFAULT_SIZE = {
2915
3441
  width: 1200,
2916
3442
  height: 630
2917
3443
  };
2918
- /** The fixed concurrency bound for the OG render pool. */
2919
- const OG_CONCURRENCY = 4;
2920
3444
  /** Recognized font file extensions. */
2921
3445
  const FONT_EXTENSIONS$1 = [
2922
3446
  ".ttf",
@@ -3037,7 +3561,7 @@ async function generateOgImages(ctx, options = {}) {
3037
3561
  const articles = selectArticles(readCachedContent(ctx));
3038
3562
  const cache = ctx.state.ogImageHashCache;
3039
3563
  await loadDiskCache(ctx.config.outDir, cache);
3040
- const limit = pLimit(OG_CONCURRENCY);
3564
+ const limit = pLimit(4);
3041
3565
  let active = 0;
3042
3566
  let peakConcurrency = 0;
3043
3567
  let rendered = 0;
@@ -3110,7 +3634,6 @@ async function persistDiskCache(outDir, cache) {
3110
3634
  await mkdir(dir, { recursive: true });
3111
3635
  await writeFile(path.join(dir, "og-images.json"), JSON.stringify(Object.fromEntries(cache)), "utf8");
3112
3636
  }
3113
-
3114
3637
  //#endregion
3115
3638
  //#region src/plugins/build/phases/pages.tsx
3116
3639
  /**
@@ -3278,7 +3801,6 @@ async function renderPages(ctx) {
3278
3801
  rootHtml: root?.html ?? null
3279
3802
  };
3280
3803
  }
3281
-
3282
3804
  //#endregion
3283
3805
  //#region src/plugins/build/phases/sitemap.ts
3284
3806
  /**
@@ -3360,7 +3882,6 @@ async function generateSitemap(ctx) {
3360
3882
  robots
3361
3883
  };
3362
3884
  }
3363
-
3364
3885
  //#endregion
3365
3886
  //#region src/plugins/build/pipeline.ts
3366
3887
  /**
@@ -3491,7 +4012,6 @@ async function runPipeline(ctx, options) {
3491
4012
  phaseContext.emit("build:complete", result);
3492
4013
  return result;
3493
4014
  }
3494
-
3495
4015
  //#endregion
3496
4016
  //#region src/plugins/build/api.ts
3497
4017
  /**
@@ -3530,9 +4050,29 @@ const defaultConfig$1 = {
3530
4050
  */
3531
4051
  function createApi$2(ctx) {
3532
4052
  return {
4053
+ /**
4054
+ * Run the full SSG pipeline and write the site to disk.
4055
+ *
4056
+ * @param options - Optional run overrides.
4057
+ * @param options.outDir - Override the configured output directory for this run.
4058
+ * @returns The build result (outDir, pageCount, durationMs).
4059
+ * @example
4060
+ * ```ts
4061
+ * await api.run({ outDir: "./preview" });
4062
+ * ```
4063
+ */
3533
4064
  run(options) {
3534
4065
  return runPipeline(ctx, options);
3535
4066
  },
4067
+ /**
4068
+ * List the phases in execution order (introspection / tooling).
4069
+ *
4070
+ * @returns A fresh array of the static ordered phase names.
4071
+ * @example
4072
+ * ```ts
4073
+ * api.phases();
4074
+ * ```
4075
+ */
3536
4076
  phases() {
3537
4077
  return [...PHASE_ORDER];
3538
4078
  }
@@ -3567,7 +4107,6 @@ function validateConfig$1(config) {
3567
4107
  if (typeof config.outDir !== "string" || config.outDir.trim().length === 0) throw new Error(`${ERROR_PREFIX$5}.outDir: must be a non-empty string.`);
3568
4108
  if (config.ogImage) validateFonts(config.ogImage);
3569
4109
  }
3570
-
3571
4110
  //#endregion
3572
4111
  //#region src/plugins/build/events.ts
3573
4112
  /**
@@ -3587,7 +4126,6 @@ function createEvents(register) {
3587
4126
  "build:complete": register("Emitted once after a successful build run")
3588
4127
  };
3589
4128
  }
3590
-
3591
4129
  //#endregion
3592
4130
  //#region src/plugins/build/state.ts
3593
4131
  /**
@@ -3614,13 +4152,33 @@ function createState$2(ctx) {
3614
4152
  ogImageHashCache: /* @__PURE__ */ new Map()
3615
4153
  };
3616
4154
  }
3617
-
3618
4155
  //#endregion
3619
4156
  //#region src/plugins/build/index.ts
3620
4157
  /**
3621
4158
  * @file build — Complex plugin: SSG orchestrator (wiring harness only).
3622
4159
  * @see README.md
3623
4160
  */
4161
+ /**
4162
+ * Build plugin — the static-site-generation orchestrator. Renders every route to
4163
+ * `outDir`, and optionally emits feeds, a sitemap, optimized images, and OG
4164
+ * images. Depends on site, i18n, content, router, and head; emits `build:phase`.
4165
+ *
4166
+ * @example Configure the production build
4167
+ * ```ts
4168
+ * const app = createApp({
4169
+ * pluginConfigs: {
4170
+ * build: {
4171
+ * outDir: "dist",
4172
+ * minify: true,
4173
+ * feeds: true,
4174
+ * sitemap: true,
4175
+ * images: true,
4176
+ * ogImage: false // or an object to enable + configure OG-image generation
4177
+ * }
4178
+ * }
4179
+ * });
4180
+ * ```
4181
+ */
3624
4182
  const buildPlugin = createPlugin$1("build", {
3625
4183
  depends: [
3626
4184
  sitePlugin,
@@ -3635,7 +4193,6 @@ const buildPlugin = createPlugin$1("build", {
3635
4193
  api: createApi$2,
3636
4194
  onInit: (ctx) => validateConfig$1(ctx.config)
3637
4195
  });
3638
-
3639
4196
  //#endregion
3640
4197
  //#region src/plugins/deploy/wrangler.ts
3641
4198
  /**
@@ -3813,8 +4370,6 @@ const ERROR_SIGNATURES = [
3813
4370
  advice: "A network failure occurred. Check connectivity and retry."
3814
4371
  }
3815
4372
  ];
3816
- /** Number of trailing characters of scrubbed stderr to surface on an unknown failure. */
3817
- const STDERR_TAIL_LENGTH = 500;
3818
4373
  /**
3819
4374
  * Map a non-zero wrangler exit and scrubbed stderr to an actionable error
3820
4375
  * `code` + message. Matching is case-insensitive against the scrubbed stderr;
@@ -3835,7 +4390,7 @@ function classifyWranglerError(exitCode, scrubbedStderr) {
3835
4390
  };
3836
4391
  return {
3837
4392
  code: "ERR_DEPLOY_WRANGLER_FAILED",
3838
- message: `${ERROR_PREFIX$4}: wrangler failed (exit ${exitCode}).\n ${scrubbedStderr.trim().slice(-STDERR_TAIL_LENGTH)}`
4393
+ message: `${ERROR_PREFIX$4}: wrangler failed (exit ${exitCode}).\n ${scrubbedStderr.trim().slice(-500)}`
3839
4394
  };
3840
4395
  }
3841
4396
  /**
@@ -3894,7 +4449,6 @@ async function runWrangler(input) {
3894
4449
  exitCode
3895
4450
  };
3896
4451
  }
3897
-
3898
4452
  //#endregion
3899
4453
  //#region src/plugins/deploy/generators/github-workflow.ts
3900
4454
  /**
@@ -3949,7 +4503,6 @@ jobs:
3949
4503
  command: pages deploy dist --project-name ${input.slug}
3950
4504
  `;
3951
4505
  }
3952
-
3953
4506
  //#endregion
3954
4507
  //#region src/plugins/deploy/generators/wrangler-config.ts
3955
4508
  /**
@@ -3993,7 +4546,6 @@ async function readWranglerConfig(cwd) {
3993
4546
  return null;
3994
4547
  }
3995
4548
  }
3996
-
3997
4549
  //#endregion
3998
4550
  //#region src/plugins/deploy/init.ts
3999
4551
  /**
@@ -4097,7 +4649,6 @@ async function reconcile(input) {
4097
4649
  await writeFile(path.join(cwd, relativePath), expected, "utf8");
4098
4650
  result.written.push(relativePath);
4099
4651
  }
4100
-
4101
4652
  //#endregion
4102
4653
  //#region src/plugins/deploy/preflight.ts
4103
4654
  /**
@@ -4191,7 +4742,6 @@ async function runPreflight(config, root, env = process.env) {
4191
4742
  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
4743
  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
4744
  }
4194
-
4195
4745
  //#endregion
4196
4746
  //#region src/plugins/deploy/slug.ts
4197
4747
  /**
@@ -4232,7 +4782,6 @@ function toSlug(name) {
4232
4782
  slug = slug.slice(0, end);
4233
4783
  return slug.length > 0 ? slug : FALLBACK_SLUG;
4234
4784
  }
4235
-
4236
4785
  //#endregion
4237
4786
  //#region src/plugins/deploy/api.ts
4238
4787
  /** Error prefix for deploy config/validation failures (spec/11 Part-3). */
@@ -4272,6 +4821,15 @@ function validateConfig(ctx) {
4272
4821
  */
4273
4822
  function createApi$1(ctx) {
4274
4823
  return {
4824
+ /**
4825
+ * Deploy the built outDir to Cloudflare Pages via the wrangler subprocess.
4826
+ *
4827
+ * @param options - Optional branch override and build toggle.
4828
+ * @returns The deploy result (url, deploymentId, branch, durationMs).
4829
+ * @throws {Error} With a `code` from the deploy error taxonomy on any failure.
4830
+ * @example
4831
+ * await api.run();
4832
+ */
4275
4833
  async run(options = {}) {
4276
4834
  const root = process.cwd();
4277
4835
  const slug = toSlug(ctx.require(sitePlugin).name());
@@ -4311,10 +4869,25 @@ function createApi$1(ctx) {
4311
4869
  });
4312
4870
  return result;
4313
4871
  },
4872
+ /**
4873
+ * Return the most recent successful deploy result, or null if none occurred.
4874
+ *
4875
+ * @returns A frozen snapshot of the last DeployResult, or null.
4876
+ * @example
4877
+ * const last = api.getLastDeployment();
4878
+ */
4314
4879
  getLastDeployment() {
4315
4880
  const last = ctx.state.lastDeployment;
4316
4881
  return last ? Object.freeze({ ...last }) : null;
4317
4882
  },
4883
+ /**
4884
+ * Generate deploy scaffolding (wrangler.jsonc + optional GitHub workflow).
4885
+ *
4886
+ * @param options - Optional ci toggle and check (drift-only) mode.
4887
+ * @returns Which files were written, skipped, or would drift.
4888
+ * @example
4889
+ * await api.init({ ci: true });
4890
+ */
4318
4891
  async init(options = {}) {
4319
4892
  const slug = toSlug(ctx.require(sitePlugin).name());
4320
4893
  return writeScaffolding({
@@ -4326,7 +4899,6 @@ function createApi$1(ctx) {
4326
4899
  }
4327
4900
  };
4328
4901
  }
4329
-
4330
4902
  //#endregion
4331
4903
  //#region src/plugins/deploy/defaults.ts
4332
4904
  /**
@@ -4346,7 +4918,6 @@ const defaultConfig = {
4346
4918
  compatibilityDate: "2024-01-01",
4347
4919
  ci: false
4348
4920
  };
4349
-
4350
4921
  //#endregion
4351
4922
  //#region src/plugins/deploy/events.ts
4352
4923
  /**
@@ -4361,7 +4932,6 @@ const defaultConfig = {
4361
4932
  * ```
4362
4933
  */
4363
4934
  const deployEvents = (register) => ({ "deploy:complete": register("Deployment completed successfully") });
4364
-
4365
4935
  //#endregion
4366
4936
  //#region src/plugins/deploy/state.ts
4367
4937
  /**
@@ -4399,7 +4969,6 @@ function createState$1(_ctx) {
4399
4969
  spawn: defaultSpawn
4400
4970
  };
4401
4971
  }
4402
-
4403
4972
  //#endregion
4404
4973
  //#region src/plugins/deploy/index.ts
4405
4974
  /**
@@ -4409,6 +4978,24 @@ function createState$1(_ctx) {
4409
4978
  * Depends: site. Emits: deploy:complete.
4410
4979
  * @see README.md
4411
4980
  */
4981
+ /**
4982
+ * Deploy plugin — ships the built `outDir` to Cloudflare Pages via the injectable
4983
+ * wrangler subprocess, with entropy-gated secret scrubbing of logged output.
4984
+ * Depends on site; emits `deploy:complete`.
4985
+ *
4986
+ * @example Configure the deploy target
4987
+ * ```ts
4988
+ * const app = createApp({
4989
+ * pluginConfigs: {
4990
+ * deploy: {
4991
+ * target: "cloudflare-pages",
4992
+ * outDir: "dist",
4993
+ * productionBranch: "main"
4994
+ * }
4995
+ * }
4996
+ * });
4997
+ * ```
4998
+ */
4412
4999
  const deployPlugin = createPlugin$1("deploy", {
4413
5000
  config: defaultConfig,
4414
5001
  depends: [sitePlugin],
@@ -4417,7 +5004,6 @@ const deployPlugin = createPlugin$1("deploy", {
4417
5004
  onInit: validateConfig,
4418
5005
  api: createApi$1
4419
5006
  });
4420
-
4421
5007
  //#endregion
4422
5008
  //#region src/plugins/spa/api.ts
4423
5009
  /**
@@ -4432,19 +5018,39 @@ const deployPlugin = createPlugin$1("deploy", {
4432
5018
  */
4433
5019
  function createApi(ctx) {
4434
5020
  return {
5021
+ /**
5022
+ * Register a component definition (last-registered-wins); warns on collision.
5023
+ *
5024
+ * @param component - The component definition created via `createComponent`.
5025
+ * @example
5026
+ * app.spa.register(counter);
5027
+ */
4435
5028
  register(component) {
4436
5029
  if (ctx.state.registeredComponents.has(component.name)) ctx.log.warn("spa:component-collision", { name: component.name });
4437
5030
  ctx.state.kernel?.register(component);
4438
5031
  },
5032
+ /**
5033
+ * Programmatically navigate to a path (client runtime; no-op without a DOM).
5034
+ *
5035
+ * @param path - Target path (pathname, optionally with search/hash).
5036
+ * @example
5037
+ * app.spa.navigate("/about");
5038
+ */
4439
5039
  navigate(path) {
4440
5040
  ctx.state.kernel?.processNav(path);
4441
5041
  },
5042
+ /**
5043
+ * Read the current resolved URL.
5044
+ *
5045
+ * @returns The current pathname + search.
5046
+ * @example
5047
+ * app.spa.current();
5048
+ */
4442
5049
  current() {
4443
5050
  return ctx.state.currentUrl;
4444
5051
  }
4445
5052
  };
4446
5053
  }
4447
-
4448
5054
  //#endregion
4449
5055
  //#region src/plugins/spa/events.ts
4450
5056
  /**
@@ -4464,7 +5070,6 @@ function spaEvents(register) {
4464
5070
  "spa:component-unmount": register("A component instance detached from an element.")
4465
5071
  };
4466
5072
  }
4467
-
4468
5073
  //#endregion
4469
5074
  //#region src/plugins/spa/types.ts
4470
5075
  var types_exports$7 = /* @__PURE__ */ __exportAll({ COMPONENT_HOOK_NAMES: () => COMPONENT_HOOK_NAMES });
@@ -4477,11 +5082,7 @@ const COMPONENT_HOOK_NAMES = [
4477
5082
  "onUnMount",
4478
5083
  "onDestroy"
4479
5084
  ];
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);
5085
+ new Set(COMPONENT_HOOK_NAMES);
4485
5086
  /**
4486
5087
  * Extracts the page data payload from the inline `script#__DATA__` element.
4487
5088
  * Returns an empty object when the script is absent, empty, or invalid JSON.
@@ -4649,7 +5250,6 @@ function notifyNavEnd(state) {
4649
5250
  const data = typeof document === "undefined" ? {} : extractPageData(document);
4650
5251
  for (const [element, instance] of state.instances) if (instance.persistent) runHook(instance, "onNavEnd", makeContext(element, data));
4651
5252
  }
4652
-
4653
5253
  //#endregion
4654
5254
  //#region src/plugins/spa/head.ts
4655
5255
  /** Single-element head selectors synced by replace/append/remove on navigation. */
@@ -4724,7 +5324,6 @@ function syncHead(_head, doc) {
4724
5324
  for (const selector of META_SELECTORS) syncElement(selector, doc);
4725
5325
  for (const selector of REPLACE_ALL_SELECTORS) replaceAllBySelector(selector, doc);
4726
5326
  }
4727
-
4728
5327
  //#endregion
4729
5328
  //#region src/plugins/spa/progress.ts
4730
5329
  /** Delay before the bar appears, so fast navigations show no indicator. */
@@ -4808,7 +5407,6 @@ function createProgressBar(enabled) {
4808
5407
  done
4809
5408
  };
4810
5409
  }
4811
-
4812
5410
  //#endregion
4813
5411
  //#region src/plugins/spa/router.ts
4814
5412
  /**
@@ -5043,7 +5641,6 @@ function attachRouter(handlers) {
5043
5641
  const navigation = getNavigation();
5044
5642
  return navigation ? attachNavigationApi(navigation, handlers) : attachHistoryFallback(handlers);
5045
5643
  }
5046
-
5047
5644
  //#endregion
5048
5645
  //#region src/plugins/spa/state.ts
5049
5646
  /** Error prefix for spa config-validation failures (spec/11 Part-3). */
@@ -5117,7 +5714,6 @@ function createState(_ctx) {
5117
5714
  kernel: null
5118
5715
  };
5119
5716
  }
5120
-
5121
5717
  //#endregion
5122
5718
  //#region src/plugins/spa/kernel.ts
5123
5719
  /**
@@ -5226,10 +5822,22 @@ function createSpaKernel(state, config, emit, deps) {
5226
5822
  onError: handleError
5227
5823
  };
5228
5824
  return {
5825
+ /**
5826
+ * Register config components and seed currentUrl from the document.
5827
+ *
5828
+ * @example
5829
+ * kernel.init();
5830
+ */
5229
5831
  init() {
5230
5832
  for (const component of resolved.components) registerComponent(state, component);
5231
5833
  state.currentUrl = currentLocationUrl();
5232
5834
  },
5835
+ /**
5836
+ * Boot navigation interception + initial scan (throws if already started).
5837
+ *
5838
+ * @example
5839
+ * kernel.boot();
5840
+ */
5233
5841
  boot() {
5234
5842
  if (typeof document === "undefined") return;
5235
5843
  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 +5847,42 @@ function createSpaKernel(state, config, emit, deps) {
5239
5847
  scanAndMount(state, emit, resolved.swapSelector);
5240
5848
  state.started = true;
5241
5849
  },
5850
+ /**
5851
+ * Register a component definition (last-registered-wins).
5852
+ *
5853
+ * @param component - The component definition to register.
5854
+ * @example
5855
+ * kernel.register(counter);
5856
+ */
5242
5857
  register(component) {
5243
5858
  registerComponent(state, component);
5244
5859
  },
5860
+ /**
5861
+ * Process a navigation to `path` (fetch then swap; full reload on error).
5862
+ *
5863
+ * @param path - The target path to navigate to.
5864
+ * @example
5865
+ * kernel.processNav("/about");
5866
+ */
5245
5867
  processNav(path) {
5246
5868
  if (typeof document === "undefined") return;
5247
5869
  performNavigation(path, handlers).catch(() => {});
5248
5870
  },
5871
+ /**
5872
+ * Scan the swap region and mount components for matching elements.
5873
+ *
5874
+ * @example
5875
+ * kernel.scan();
5876
+ */
5249
5877
  scan() {
5250
5878
  scanAndMount(state, emit, resolved.swapSelector);
5251
5879
  },
5880
+ /**
5881
+ * Tear down router listeners, dispose all instances, reset boot state.
5882
+ *
5883
+ * @example
5884
+ * kernel.dispose();
5885
+ */
5252
5886
  dispose() {
5253
5887
  state.destroyRouter?.();
5254
5888
  state.destroyRouter = null;
@@ -5277,7 +5911,6 @@ function initSpa(ctx) {
5277
5911
  kernelRef.current = kernel;
5278
5912
  kernel.init();
5279
5913
  }
5280
-
5281
5914
  //#endregion
5282
5915
  //#region src/plugins/spa/lifecycle.ts
5283
5916
  /** Router/instance teardown captured during onStart (undefined when stopped). */
@@ -5325,7 +5958,6 @@ function disposeSpa() {
5325
5958
  logRef = void 0;
5326
5959
  }
5327
5960
  }
5328
-
5329
5961
  //#endregion
5330
5962
  //#region src/plugins/spa/index.ts
5331
5963
  /**
@@ -5336,6 +5968,26 @@ function disposeSpa() {
5336
5968
  * Emits: spa:navigate, spa:navigated, spa:component-mount, spa:component-unmount.
5337
5969
  * @see README.md
5338
5970
  */
5971
+ /**
5972
+ * SPA plugin — progressive client-side navigation layered over the static site:
5973
+ * swaps a page region on navigation, with an optional progress bar and View
5974
+ * Transitions. Register interactive islands with {@link createComponent}. Depends
5975
+ * on router and head; emits `spa:navigate`, `spa:navigated`, `spa:component-mount`,
5976
+ * and `spa:component-unmount`.
5977
+ *
5978
+ * @example Enable view transitions and a custom swap region
5979
+ * ```ts
5980
+ * const app = createApp({
5981
+ * pluginConfigs: {
5982
+ * spa: {
5983
+ * swapSelector: "main > section",
5984
+ * viewTransitions: true,
5985
+ * progressBar: true
5986
+ * }
5987
+ * }
5988
+ * });
5989
+ * ```
5990
+ */
5339
5991
  const spaPlugin = createPlugin$1("spa", {
5340
5992
  depends: [routerPlugin, headPlugin],
5341
5993
  config: defaultSpaConfig,
@@ -5349,35 +6001,27 @@ const spaPlugin = createPlugin$1("spa", {
5349
6001
  },
5350
6002
  onStop: disposeSpa
5351
6003
  });
5352
-
5353
6004
  //#endregion
5354
6005
  //#region src/plugins/build/types.ts
5355
6006
  var types_exports = /* @__PURE__ */ __exportAll({});
5356
-
5357
6007
  //#endregion
5358
6008
  //#region src/plugins/content/types.ts
5359
6009
  var types_exports$1 = /* @__PURE__ */ __exportAll({});
5360
-
5361
6010
  //#endregion
5362
6011
  //#region src/plugins/deploy/types.ts
5363
6012
  var types_exports$2 = /* @__PURE__ */ __exportAll({});
5364
-
5365
6013
  //#endregion
5366
6014
  //#region src/plugins/env/types.ts
5367
6015
  var types_exports$3 = /* @__PURE__ */ __exportAll({});
5368
-
5369
6016
  //#endregion
5370
6017
  //#region src/plugins/head/types.ts
5371
6018
  var types_exports$4 = /* @__PURE__ */ __exportAll({});
5372
-
5373
6019
  //#endregion
5374
6020
  //#region src/plugins/log/types.ts
5375
6021
  var types_exports$5 = /* @__PURE__ */ __exportAll({});
5376
-
5377
6022
  //#endregion
5378
6023
  //#region src/plugins/router/types.ts
5379
6024
  var types_exports$6 = /* @__PURE__ */ __exportAll({});
5380
-
5381
6025
  //#endregion
5382
6026
  //#region src/index.ts
5383
6027
  /**
@@ -5397,7 +6041,45 @@ const framework = createCore(coreConfig, {
5397
6041
  ],
5398
6042
  pluginConfigs: {}
5399
6043
  });
5400
- const { createApp, createPlugin } = framework;
5401
-
6044
+ /**
6045
+ * Create and initialize a `@moku-labs/web` application — the Layer-3 entry point.
6046
+ * Your overrides are merged over the framework defaults through the 4-level config
6047
+ * cascade, every plugin's lifecycle runs, and a fully-typed, frozen app is returned.
6048
+ *
6049
+ * @param options - Optional configuration:
6050
+ * - `pluginConfigs` — per-plugin overrides, keyed by plugin name
6051
+ * (`site`, `i18n`, `router`, `content`, `head`, `build`, `spa`, `deploy`, `env`).
6052
+ * - `config` — global framework config (e.g. `{ mode: "development" }`).
6053
+ * - `plugins` — extra consumer plugins, merged into the app and its return type.
6054
+ * - `onReady` / `onError` / `onStart` / `onStop` — lifecycle callbacks.
6055
+ * @returns The initialized app: `start()`, `stop()`, every plugin's API, and `log`.
6056
+ * @example
6057
+ * ```ts
6058
+ * const app = createApp({
6059
+ * pluginConfigs: {
6060
+ * site: { name: "My Blog", url: "https://blog.dev", author: "Ada", description: "Notes" },
6061
+ * router: { routes: defineRoutes({ home: route("/"), post: route("/blog/{slug}/") }) }
6062
+ * }
6063
+ * });
6064
+ * await app.start();
6065
+ * ```
6066
+ */
6067
+ const createApp = framework.createApp;
6068
+ /**
6069
+ * Create a custom plugin bound to this framework's `Config`/`Events` and core
6070
+ * APIs. Plugin types are inferred from the spec object — never written explicitly.
6071
+ * Pass the result to {@link createApp} via `plugins`.
6072
+ *
6073
+ * @example
6074
+ * ```ts
6075
+ * const analytics = createPlugin("analytics", {
6076
+ * config: { writeKey: "" },
6077
+ * api: (ctx) => ({ track: (event: string) => ctx.log.info("analytics:track", { event }) })
6078
+ * });
6079
+ *
6080
+ * const app = createApp({ plugins: [analytics] });
6081
+ * ```
6082
+ */
6083
+ const createPlugin = framework.createPlugin;
5402
6084
  //#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 };
6085
+ 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 };