@moku-labs/web 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -1,9 +1,9 @@
1
1
  import { ComponentChildren, VNode } from "preact";
2
- import { Pluggable, Processor } from "unified";
3
2
  import { EmitFn } from "@moku-labs/core";
3
+ import { Pluggable, Processor } from "unified";
4
4
 
5
5
  //#region \0rolldown/runtime.js
6
- declare namespace types_d_exports$5 {
6
+ declare namespace types_d_exports$6 {
7
7
  export { ExpectChain, LogApi, LogConfig, LogEntry, LogLevel, LogSink, LogState };
8
8
  }
9
9
  /**
@@ -143,7 +143,7 @@ type LogApi = {
143
143
  addSink(sink: LogSink): void; /** Clear all recorded entries while keeping registered sinks. */
144
144
  reset(): void;
145
145
  };
146
- declare namespace types_d_exports$3 {
146
+ declare namespace types_d_exports$4 {
147
147
  export { EnvApi, EnvConfig, EnvProvider, EnvState, EnvVarSpec };
148
148
  }
149
149
  /**
@@ -302,6 +302,26 @@ type EnvApi = {
302
302
  getPublicMap(): ReadonlyMap<string, string>;
303
303
  };
304
304
  //#endregion
305
+ //#region src/plugins/env/providers.browser.d.ts
306
+ /**
307
+ * A browser-safe {@link EnvProvider} that reads `import.meta.env` and an optional
308
+ * `globalThis[globalKey]` snapshot, merging them with the runtime global winning.
309
+ * Contains zero `node:*` imports, so it is safe to include in the client bundle.
310
+ * Never throws on missing sources — each absent source resolves to `{}`.
311
+ *
312
+ * @param options - Optional settings.
313
+ * @param options.globalKey - `globalThis` key to read a public-env snapshot from. Defaults to `"__ENV__"`.
314
+ * @returns An {@link EnvProvider} named `browser-env`.
315
+ * @example
316
+ * ```ts
317
+ * const provider = browserEnv();
318
+ * provider.load(); // { PUBLIC_API_URL: "/api", ... }
319
+ * ```
320
+ */
321
+ declare function browserEnv(options?: {
322
+ globalKey?: string;
323
+ }): EnvProvider;
324
+ //#endregion
305
325
  //#region src/plugins/env/index.d.ts
306
326
  /**
307
327
  * Core plugin that resolves, validates, and freezes the environment at `onInit`,
@@ -521,8 +541,8 @@ type Api$5 = {
521
541
  */
522
542
  t(locale: string, key: string): string;
523
543
  };
524
- declare namespace types_d_exports$6 {
525
- export { Api$4 as Api, CompileInput, CompiledRoute, Config$4 as Config, ExtractRouteParams, ExtractSegmentParameter, HeadConfig$1 as HeadConfig, MatcherTable, Prettify, RouteBuilder, RouteContext, RouteDefinition, RouteHandlers, RouteMap, RouteState, RouterApi, RouterConfig, RouterState, State$4 as State, TypedRoute };
544
+ declare namespace types_d_exports$7 {
545
+ export { Api$4 as Api, ClientRoute, CompileInput, CompiledRoute, Config$4 as Config, ExtractRouteParams, ExtractSegmentParameter, HeadConfig$1 as HeadConfig, MatcherTable, Prettify, RouteBuilder, RouteContext, RouteDefinition, RouteHandlers, RouteMap, RouteState, RouterApi, RouterConfig, RouterState, State$4 as State, TypedRoute };
526
546
  }
527
547
  /**
528
548
  * Param contribution of a single path segment. `{name:?}` / `:name?` → optional;
@@ -588,11 +608,23 @@ interface RouteBuilder<S extends RouteState> extends RouteDefinition {
588
608
  layout(component: (children: ComponentChildren) => VNode): RouteBuilder<S>;
589
609
  /** Attach the page render handler. */
590
610
  render(handler: (ctx: RouteContext<S>) => VNode): RouteBuilder<S>;
611
+ /**
612
+ * Attach the client-side validation gate: parse the raw `unknown` fetched from
613
+ * the persisted data file back into this route's data type `S["data"]`. Runs at
614
+ * the trust boundary before `render` on the client (and MUST return `S["data"]`,
615
+ * so a mismatched schema is a compile error). Throw inside it to reject malformed
616
+ * data — `spa` then falls back to HTML-over-fetch. Use a hand guard or any
617
+ * Standard-Schema validator (zod/valibot/arktype).
618
+ */
619
+ parse(handler: (raw: unknown) => S["data"]): RouteBuilder<S>;
591
620
  /** Attach the head/SEO handler. */
592
621
  head(handler: (ctx: RouteContext<S>) => HeadConfig$1): RouteBuilder<S>;
593
622
  /** Attach a static-generation param producer. */
594
623
  generate(handler: (locale: string) => S["params"][] | Promise<S["params"][]>): RouteBuilder<S>;
595
- /** Attach an arbitrary metadata bag. */
624
+ /**
625
+ * Attach an arbitrary metadata bag. The bag MUST be JSON-serializable: it is
626
+ * projected verbatim into `clientManifest()` and shipped to the browser.
627
+ */
596
628
  meta(meta: Record<string, unknown>): RouteBuilder<S>;
597
629
  /** Attach a JSON serializer for the route's data. */
598
630
  toJson(handler: (ctx: RouteContext<S>) => unknown): RouteBuilder<S>;
@@ -607,6 +639,8 @@ interface RouteHandlers {
607
639
  readonly layout?: (children: ComponentChildren) => VNode;
608
640
  /** Page renderer. */
609
641
  readonly render?: (ctx: RouteContext<RouteState>) => VNode;
642
+ /** Client-side validation gate: `unknown` (fetched JSON) → the route's data type, or throw. */
643
+ readonly parse?: (raw: unknown) => unknown;
610
644
  /** Head/SEO producer. */
611
645
  readonly head?: (ctx: RouteContext<RouteState>) => HeadConfig$1;
612
646
  /** Static-generation param producer. */
@@ -711,6 +745,8 @@ interface MatcherTable {
711
745
  interface RouterState {
712
746
  /** Compiled matcher table; `null` until `onInit` assigns it. */
713
747
  table: MatcherTable | null;
748
+ /** Resolved render mode (single source of truth; set in `onInit`). Defaults `"hybrid"`. */
749
+ mode: "ssg" | "spa" | "hybrid";
714
750
  }
715
751
  /** Plain-data input to `compileRoutes` — resolved DATA only, never the plugin ctx. */
716
752
  interface CompileInput {
@@ -725,6 +761,24 @@ interface CompileInput {
725
761
  /** Default locale used for bare-pattern fallback. */
726
762
  readonly defaultLocale: string;
727
763
  }
764
+ /**
765
+ * Serializable route entry for the client route-index — a projection of the
766
+ * compiled route table with NO `_handlers` closures, safe to ship to the browser
767
+ * (the SPA recompiles matchers lazily from `pattern`).
768
+ *
769
+ * @remarks
770
+ * `meta` MUST be JSON-serializable: `clientManifest()` is intended to survive a
771
+ * `JSON.stringify`/`JSON.parse` round-trip, so a route's `.meta()` bag must contain
772
+ * only JSON-safe values (no functions, symbols, or class instances).
773
+ */
774
+ interface ClientRoute {
775
+ /** URL pattern string, e.g. `/{lang:?}/{slug}/`. */
776
+ readonly pattern: string;
777
+ /** Route name key from the route map. */
778
+ readonly name: string;
779
+ /** Route metadata bag from `.meta()`. MUST be JSON-serializable. */
780
+ readonly meta: Record<string, unknown>;
781
+ }
728
782
  /** Public API exposed via `ctx.require(routerPlugin)`. */
729
783
  type RouterApi = {
730
784
  /**
@@ -767,197 +821,36 @@ type RouterApi = {
767
821
  * for (const def of ctx.require(routerPlugin).manifest()) { def._handlers.load?.({}, "en"); }
768
822
  */
769
823
  manifest(): readonly RouteDefinition[];
770
- };
771
- /** Re-export under the canonical `Config` name for the plugin-types barrel. */
772
- type Config$4 = RouterConfig;
773
- /** Re-export under the canonical `State` name for the plugin-types barrel. */
774
- type State$4 = RouterState;
775
- /** Re-export under the canonical `Api` name for the plugin-types barrel. */
776
- type Api$4 = RouterApi;
777
- declare namespace types_d_exports$1 {
778
- export { Api$3 as Api, Article, ArticleCard, ComputedFields, Config$3 as Config, ContentApiContext, ContentEvents, Frontmatter, State$3 as State };
779
- }
780
- /**
781
- * Configuration for the content plugin.
782
- *
783
- * @example
784
- * ```ts
785
- * { contentDir: "./src/content", trustedContent: false, shikiTheme: "github-dark" }
786
- * ```
787
- */
788
- type Config$3 = {
789
- /** Absolute or project-relative path to the content directory. Validated in onInit. */contentDir: string;
790
- /**
791
- * SECURITY GATE. When false (the default), rehype-sanitize runs as the final
792
- * pipeline step. Set true ONLY for fully author-controlled Markdown — true
793
- * disables sanitize and trusts all raw HTML.
794
- */
795
- trustedContent: boolean; /** Additional remark plugins, concatenated AFTER framework defaults. Defaults to []. */
796
- extraRemarkPlugins?: readonly Pluggable[]; /** Additional rehype plugins, concatenated after custom transforms, before Shiki + sanitize. Defaults to []. */
797
- extraRehypePlugins?: readonly Pluggable[]; /** Shiki theme name for syntax highlighting. Defaults to "github-dark". */
798
- shikiTheme?: string; /** Author applied to articles whose frontmatter omits author. Defaults to undefined. */
799
- defaultAuthor?: string;
800
- };
801
- /**
802
- * Internal mutable state for the content plugin.
803
- *
804
- * @example
805
- * ```ts
806
- * { processor: null, articles: new Map(), slugs: null, dirtyPaths: new Set() }
807
- * ```
808
- */
809
- type State$3 = {
810
- /** Lazily-created unified processor singleton. null until first render()/loadAll(). */processor: Processor | null; /** Article cache keyed locale -> (slug -> Article). Starts empty. */
811
- articles: Map<string, Map<string, Article>>; /** Discovered, sorted slug list cached after first disk scan. null until first discovery. */
812
- slugs: string[] | null; /** Paths marked stale by invalidate(); next loadAll() re-reads only these. Starts empty. */
813
- dirtyPaths: Set<string>;
814
- };
815
- /**
816
- * YAML frontmatter parsed from each article file.
817
- *
818
- * @example
819
- * ```ts
820
- * { title: "Hello", date: "2026-01-15", description: "Intro", tags: [], language: "en" }
821
- * ```
822
- */
823
- type Frontmatter = {
824
- /** Article title. Required. */title: string; /** ISO 8601 date string, e.g. "2026-01-15". Required. */
825
- date: string; /** Short summary used in cards, feeds, and meta description. Required. */
826
- description: string; /** Topic tags. Required (may be empty array). */
827
- tags: string[]; /** Source language code of this file. Required. */
828
- language: string; /** Draft flag. Excluded from output in production mode. Defaults to false. */
829
- draft?: boolean; /** Author name. Falls back to config.defaultAuthor when omitted. */
830
- author?: string;
831
- };
832
- /**
833
- * Fields computed by the pipeline (not authored in frontmatter).
834
- *
835
- * @example
836
- * ```ts
837
- * { slug: "hello", readingTime: 1, contentId: "hello", status: "published", wordCount: 42 }
838
- * ```
839
- */
840
- type ComputedFields = {
841
- /** Article directory name. */slug: string; /** Reading time in minutes (ceiling, minimum 1). */
842
- readingTime: number; /** Stable content identifier (slug by default). */
843
- contentId: string; /** Derived publication status. */
844
- status: "published" | "draft"; /** Word count from the source body. */
845
- wordCount: number;
846
- };
847
- /**
848
- * A fully processed, render-ready article.
849
- *
850
- * @example
851
- * ```ts
852
- * { frontmatter, computed, html: "<p>…</p>", locale: "en", isFallback: false, url: "/en/hello/" }
853
- * ```
854
- */
855
- type Article = {
856
- /** Parsed frontmatter. */frontmatter: Frontmatter; /** Pipeline-computed metadata. */
857
- computed: ComputedFields; /** Sanitized rendered HTML body. */
858
- html: string; /** Locale this Article instance represents (the requested locale, even on fallback). */
859
- locale: string; /** True when the default-locale file was used because the requested locale was missing. */
860
- isFallback: boolean; /** Canonical URL for this article in this locale. */
861
- url: string;
862
- };
863
- /**
864
- * Lightweight projection of Article for cards/lists.
865
- *
866
- * @example
867
- * ```ts
868
- * { contentId: "hello", status: "published", title: "Hello", date: "2026-01-15", description: "Intro", tags: [], readingTime: 1, url: "/en/hello/" }
869
- * ```
870
- */
871
- type ArticleCard = {
872
- /** Stable content identifier. */contentId: string; /** Derived publication status. */
873
- status: "published" | "draft"; /** Article title. */
874
- title: string; /** ISO 8601 date string. */
875
- date: string; /** Short summary. */
876
- description: string; /** Topic tags. */
877
- tags: string[]; /** Reading time in minutes. */
878
- readingTime: number; /** Canonical URL for this article in this locale. */
879
- url: string;
880
- };
881
- /**
882
- * Notification-only events emitted by the content plugin.
883
- *
884
- * @example
885
- * ```ts
886
- * emit("content:ready", { locales: ["en"], articleCount: 3 });
887
- * ```
888
- */
889
- type ContentEvents = {
890
- /** All articles loaded across locales. */"content:ready": {
891
- locales: readonly string[];
892
- articleCount: number;
893
- }; /** Article paths marked stale for dev rebuild. */
894
- "content:invalidated": {
895
- paths: readonly string[];
896
- };
897
- };
898
- /**
899
- * Kernel-free domain context handed to createContentApi by the wiring harness.
900
- * Carries ctx.state (mutable escape hatch), config, global, emit, and the
901
- * i18n-derived locale/url helpers — so api.ts stays free of createPlugin/ctx.
902
- *
903
- * @example
904
- * ```ts
905
- * const apiContext: ContentApiContext = { state, config, global, emit, locales, defaultLocale, articleToUrl };
906
- * ```
907
- */
908
- type ContentApiContext = {
909
- /** Mutable plugin state (article cache + lazy processor). */state: State$3; /** Resolved plugin configuration. */
910
- config: Config$3; /** Global framework configuration (mode, etc.). */
911
- global: {
912
- mode: "production" | "development";
913
- }; /** Emit a registered content event. */
914
- emit: <K extends keyof ContentEvents>(event: K, payload: ContentEvents[K]) => void; /** Active locale codes from i18n. */
915
- locales: () => readonly string[]; /** Default locale code from i18n (fallback source). */
916
- defaultLocale: () => string; /** Build a canonical article URL for a locale + slug. */
917
- articleToUrl: (locale: string, slug: string) => string;
918
- };
919
- /**
920
- * Public API for the content plugin.
921
- *
922
- * @example
923
- * ```ts
924
- * const map = await app.content.loadAll();
925
- * ```
926
- */
927
- type Api$3 = {
928
- /**
929
- * Load every article across every active locale, returning a locale-keyed
930
- * map of date-descending Article arrays. Emits content:ready.
931
- */
932
- loadAll(): Promise<Map<string, Article[]>>;
933
- /**
934
- * Resolve and render a single article for one locale, with locale fallback.
935
- *
936
- * @param slug - Article directory name.
937
- * @param locale - Requested locale code.
938
- */
939
- load(slug: string, locale: string): Promise<Article>;
940
- /**
941
- * Render a raw Markdown string to HTML through the full pipeline.
942
- *
943
- * @param md - Raw Markdown source.
944
- */
945
- renderMarkdown(md: string): Promise<string>;
946
824
  /**
947
- * Mark file paths stale for incremental dev rebuilds. Emits content:invalidated.
825
+ * Serializable, specificity-sorted projection of the route table for client
826
+ * shipping. Maps the compiled table to `{ pattern, name, meta }` entries with NO
827
+ * `_handlers` closures, returned as a fresh frozen array. JSON-serializable so the
828
+ * SPA can embed it and recompile matchers lazily in the browser.
948
829
  *
949
- * @param paths - File paths to invalidate.
830
+ * @returns A fresh, frozen, specificity-sorted read-only array of {@link ClientRoute}.
831
+ * @example
832
+ * const json = JSON.stringify(ctx.require(routerPlugin).clientManifest());
950
833
  */
951
- invalidate(paths: readonly string[]): void;
834
+ clientManifest(): readonly ClientRoute[];
952
835
  /**
953
- * Project a full Article to a lightweight ArticleCard for list/grid rendering.
836
+ * The resolved render mode the single source of truth for static/hybrid/spa
837
+ * behavior. `build` reads it to decide whether to emit client data sidecars;
838
+ * `spa` reads it to decide whether to attempt client DATA navigation.
954
839
  *
955
- * @param article - The source article.
840
+ * @returns `"ssg" | "spa" | "hybrid"`.
841
+ * @example
842
+ * if (ctx.require(routerPlugin).mode() !== "ssg") { ... }
956
843
  */
957
- articleToCard(article: Article): ArticleCard;
844
+ mode(): "ssg" | "spa" | "hybrid";
958
845
  };
959
- declare namespace types_d_exports$4 {
960
- export { Api$2 as Api, ArticleMeta, Config$2 as Config, HeadConfig, HeadDefaults, HeadElement, ResolvedRoute, State$2 as State };
846
+ /** Re-export under the canonical `Config` name for the plugin-types barrel. */
847
+ type Config$4 = RouterConfig;
848
+ /** Re-export under the canonical `State` name for the plugin-types barrel. */
849
+ type State$4 = RouterState;
850
+ /** Re-export under the canonical `Api` name for the plugin-types barrel. */
851
+ type Api$4 = RouterApi;
852
+ declare namespace types_d_exports$5 {
853
+ export { Api$3 as Api, ArticleMeta, Config$3 as Config, HeadConfig, HeadDefaults, HeadElement, ResolvedRoute, State$3 as State };
961
854
  }
962
855
  /**
963
856
  * @file head plugin — type definitions skeleton
@@ -973,7 +866,7 @@ declare namespace types_d_exports$4 {
973
866
  * const config: Config = { titleTemplate: "%s — Moku" };
974
867
  * ```
975
868
  */
976
- type Config$2 = {
869
+ type Config$3 = {
977
870
  /** Title template applied to per-route titles. `%s` is replaced by the route title. */titleTemplate?: string; /** Default Open Graph image URL used when a route does not supply one. */
978
871
  defaultOgImage?: string; /** Default Twitter card type emitted when og/twitter content is present. */
979
872
  twitterCard?: "summary" | "summary_large_image"; /** Default Twitter site handle (e.g. `"@moku_labs"`) emitted as `twitter:site`. */
@@ -991,7 +884,7 @@ type Config$2 = {
991
884
  * const state: State = { defaults: null };
992
885
  * ```
993
886
  */
994
- type State$2 = {
887
+ type State$3 = {
995
888
  /** Normalized head defaults, assigned once in `onInit` (initially `null`). */defaults: HeadDefaults | null;
996
889
  };
997
890
  /**
@@ -1085,7 +978,7 @@ type ResolvedRoute = {
1085
978
  * const html: string = api.render(route, data);
1086
979
  * ```
1087
980
  */
1088
- type Api$2 = {
981
+ type Api$3 = {
1089
982
  /**
1090
983
  * Compose the final `<head>` inner HTML for a route. Pulled synchronously by `build`.
1091
984
  *
@@ -1099,263 +992,41 @@ type Api$2 = {
1099
992
  */
1100
993
  render(route: ResolvedRoute, data: unknown): string;
1101
994
  };
1102
- declare namespace types_d_exports {
1103
- export { Api$1 as Api, BuildEvents, BuildResult, Config$1 as Config, ExtractApi$1 as ExtractApi, OgImageConfig, OgPngRenderer, PhaseContext, PhaseEmit, PhaseLog, PhaseName, PhaseRequire, State$1 as State };
995
+ declare namespace types_d_exports$8 {
996
+ export { COMPONENT_HOOK_NAMES, ComponentContext, ComponentDef, ComponentHooks, ComponentInstance, ExtractApi$1 as ExtractApi, PageData, ResolvedSpaConfig, SpaApi, SpaConfig, SpaContext, SpaDataReader, SpaEmitFunction, SpaEvents, SpaKernel, SpaKernelDeps, SpaRequire, SpaState };
1104
997
  }
998
+ /** Payload map for the events `spa` emits, used to type the kernel's `emit` closure. */
999
+ type SpaEvents = {
1000
+ /** A navigation has been intercepted and is starting. */"spa:navigate": {
1001
+ from: string;
1002
+ to: string;
1003
+ }; /** The swap completed and the new URL is active. */
1004
+ "spa:navigated": {
1005
+ url: string;
1006
+ }; /** A component instance attached to an element. */
1007
+ "spa:component-mount": {
1008
+ name: string;
1009
+ el: Element;
1010
+ }; /** A component instance detached from an element. */
1011
+ "spa:component-unmount": {
1012
+ name: string;
1013
+ el: Element;
1014
+ };
1015
+ };
1016
+ /** Strictly-typed emit closure for the spa events (kernel overload form). */
1017
+ type SpaEmitFunction = EmitFn<SpaEvents>;
1105
1018
  /**
1106
1019
  * Structural extraction of a plugin instance's public API from its `_phantom`
1107
- * carrier (mirrors the kernel's non-exported `ExtractPluginApi`) so the
1108
- * framework's generic `require` is assignable to {@link PhaseContext.require}.
1020
+ * carrier (mirrors the kernel's non-exported `ExtractPluginApi`).
1109
1021
  *
1110
1022
  * @example
1111
- * ```ts
1112
- * type ContentApi = ExtractApi<typeof contentPlugin>;
1113
- * ```
1023
+ * type RApi = ExtractApi<typeof routerPlugin>;
1114
1024
  */
1115
1025
  type ExtractApi$1<PluginCandidate> = PluginCandidate extends {
1116
1026
  readonly _phantom: {
1117
1027
  readonly api: infer PluginApi;
1118
1028
  };
1119
1029
  } ? PluginApi : never;
1120
- /**
1121
- * Minimal logger slice used by the pipeline and phases (the core `log` API).
1122
- *
1123
- * @example
1124
- * ```ts
1125
- * const log: PhaseLog = { info: () => {}, debug: () => {}, warn: () => {}, error: () => {} };
1126
- * ```
1127
- */
1128
- type PhaseLog = {
1129
- /** Record an informational event. */info(event: string, data?: unknown): void; /** Record a debug event. */
1130
- debug(event: string, data?: unknown): void; /** Record a warning event. */
1131
- warn(event: string, data?: unknown): void; /** Record an error event. */
1132
- error(event: string, data?: unknown): void;
1133
- };
1134
- /**
1135
- * Payload map for the events `build` emits, used to type the `emit` closure
1136
- * handed to the pipeline driver and phases.
1137
- *
1138
- * @example
1139
- * ```ts
1140
- * const emit: PhaseEmit = (name, payload) => kernel.emit(name, payload);
1141
- * ```
1142
- */
1143
- type BuildEvents = {
1144
- /** Phase boundary marker (start, then done with durationMs). */"build:phase": {
1145
- phase: PhaseName;
1146
- status: "start" | "done";
1147
- durationMs?: number;
1148
- }; /** One successful-run summary. */
1149
- "build:complete": {
1150
- outDir: string;
1151
- pageCount: number;
1152
- durationMs: number;
1153
- };
1154
- };
1155
- /** Strictly-typed emit closure for the build events (kernel overload form). */
1156
- type PhaseEmit = EmitFn<BuildEvents>;
1157
- /** Generic `require` closure for pulling dependency plugin APIs at run time. */
1158
- type PhaseRequire = <PluginCandidate extends {
1159
- readonly name: string;
1160
- readonly spec: unknown;
1161
- readonly _phantom: {
1162
- readonly config: unknown;
1163
- readonly state: unknown;
1164
- readonly api: unknown;
1165
- readonly events: Record<string, unknown>;
1166
- };
1167
- }>(plugin: PluginCandidate) => ExtractApi$1<PluginCandidate>;
1168
- /**
1169
- * The plugin-context slice the pipeline driver and every phase consume: the
1170
- * mutable `state`, the resolved `config`/`global`, plus `require`/`emit`/`log`.
1171
- * Typed to match the kernel's generic context so the framework execution
1172
- * context is structurally assignable.
1173
- *
1174
- * @example
1175
- * ```ts
1176
- * const ctx: PhaseContext = { state, config, global, require, emit, log };
1177
- * ```
1178
- */
1179
- type PhaseContext = {
1180
- /** Mutable per-run build state (caches + runId). */state: State$1; /** Resolved, frozen build config. */
1181
- readonly config: Readonly<Config$1>; /** Global framework config (mode, etc.). */
1182
- readonly global: Readonly<{
1183
- mode: "production" | "development";
1184
- }>; /** Resolve a depended-upon plugin instance to its public API. */
1185
- require: PhaseRequire; /** Emit a build event (notification-only). */
1186
- emit: PhaseEmit; /** Structured logger (core `log` API). */
1187
- readonly log: PhaseLog;
1188
- };
1189
- /**
1190
- * Injectable PNG renderer for the og-images phase. Defaults to the real
1191
- * Satori → resvg pipeline; unit tests inject a fake to assert hash-cache skip
1192
- * and the `p-limit` bound without rasterizing real images.
1193
- *
1194
- * @example
1195
- * ```ts
1196
- * const render: OgPngRenderer = async () => new Uint8Array();
1197
- * ```
1198
- */
1199
- type OgPngRenderer = (input: {
1200
- /** Article title rendered into the card. */title: string; /** Output width in pixels. */
1201
- width: number; /** Output height in pixels. */
1202
- height: number;
1203
- }) => Promise<Uint8Array>;
1204
- /**
1205
- * Optional OG-image generation config. Omit the field (or set `false`) to disable.
1206
- *
1207
- * @example
1208
- * ```ts
1209
- * const og: OgImageConfig = { fontDir: "./fonts" };
1210
- * ```
1211
- */
1212
- interface OgImageConfig {
1213
- /** Directory containing at least one .ttf/.otf/.woff font. Validated in onInit (void — config check only). */
1214
- fontDir: string;
1215
- /** Optional path to a custom OG template module. Falls back to the built-in template. */
1216
- template?: string;
1217
- /** Output dimensions. Defaults to 1200x630. */
1218
- size?: {
1219
- width: number;
1220
- height: number;
1221
- };
1222
- }
1223
- /**
1224
- * Public configuration for the `build` plugin. Flags give opt-in granularity over
1225
- * individual outputs without rewriting the pipeline.
1226
- *
1227
- * @example
1228
- * ```ts
1229
- * const config: Config = { outDir: "./dist", minify: true, feeds: true, sitemap: true, images: true, ogImage: false };
1230
- * ```
1231
- */
1232
- type Config$1 = {
1233
- /** Output directory for the built site. */outDir: string; /** Minify bundled CSS/JS. */
1234
- minify: boolean; /** Generate RSS/Atom/JSON feeds. */
1235
- feeds: boolean; /** Generate sitemap.xml + robots.txt. */
1236
- sitemap: boolean; /** Optimize + copy content images. */
1237
- images: boolean; /** OG-image generation. `false` (or omitted) disables it; an object enables and configures it. */
1238
- ogImage: OgImageConfig | false;
1239
- };
1240
- /**
1241
- * Per-run closure state for the `build` plugin. Holds caches and config only —
1242
- * no domain data is duplicated here (pulled fresh via `ctx.require` each run).
1243
- *
1244
- * @example
1245
- * ```ts
1246
- * const state: State = { config, manifest: null, buildCache: new Map(), runId: null, ogImageHashCache: new Map() };
1247
- * ```
1248
- */
1249
- interface State$1 {
1250
- /** Resolved, frozen config snapshot. */
1251
- config: Config$1;
1252
- /** Cached route manifest for the current run (populated in Phase 3 from router). */
1253
- manifest: RouteDefinition[] | null;
1254
- /** Per-run build artifacts (e.g. hashed CSS/JS asset paths from Phase 1). */
1255
- buildCache: Map<string, unknown>;
1256
- /** Unique id for the current run (timestamp/uuid) — injected as build-id meta. */
1257
- runId: string | null;
1258
- /**
1259
- * Content-hash cache for OG images: slug -> sha256(title + template + size).
1260
- * Loaded from `<outDir>/.cache/og-images.json` at the OG phase and written back,
1261
- * so unchanged articles are skipped on the next run.
1262
- */
1263
- ogImageHashCache: Map<string, string>;
1264
- }
1265
- /**
1266
- * Ordered names of the build pipeline phases, in execution order.
1267
- *
1268
- * @example
1269
- * ```ts
1270
- * const phase: PhaseName = "bundle";
1271
- * ```
1272
- */
1273
- type PhaseName = "bundle" | "content" | "images" | "pages" | "feeds" | "sitemap" | "og-images" | "root-index";
1274
- /**
1275
- * Result of a completed build run.
1276
- *
1277
- * @example
1278
- * ```ts
1279
- * const result: BuildResult = { outDir: "./dist", pageCount: 12, durationMs: 840 };
1280
- * ```
1281
- */
1282
- interface BuildResult {
1283
- /** Resolved output directory the site was written to. */
1284
- outDir: string;
1285
- /** Number of route pages rendered. */
1286
- pageCount: number;
1287
- /** Total wall-clock duration of the run, in milliseconds. */
1288
- durationMs: number;
1289
- }
1290
- /**
1291
- * Public API surface mounted on `app.build`.
1292
- *
1293
- * @example
1294
- * ```ts
1295
- * const result = await app.build.run();
1296
- * ```
1297
- */
1298
- type Api$1 = {
1299
- /**
1300
- * Run the full SSG pipeline and write the site to disk.
1301
- *
1302
- * @param options - Optional run overrides.
1303
- * @param options.outDir - Override the configured output directory for this run.
1304
- * @returns The build result (outDir, pageCount, durationMs).
1305
- * @example
1306
- * ```ts
1307
- * const result = await app.build.run({ outDir: "./preview" });
1308
- * ```
1309
- */
1310
- run(options?: {
1311
- outDir?: string;
1312
- }): Promise<BuildResult>;
1313
- /**
1314
- * List the phases in execution order (introspection / tooling).
1315
- *
1316
- * @returns The static ordered phase names.
1317
- * @example
1318
- * ```ts
1319
- * const order = app.build.phases();
1320
- * ```
1321
- */
1322
- phases(): PhaseName[];
1323
- };
1324
- declare namespace types_d_exports$7 {
1325
- export { COMPONENT_HOOK_NAMES, ComponentContext, ComponentDef, ComponentHooks, ComponentInstance, ExtractApi, PageData, ResolvedSpaConfig, SpaApi, SpaConfig, SpaContext, SpaEmitFunction, SpaEvents, SpaKernel, SpaKernelDeps, SpaRequire, SpaState };
1326
- }
1327
- /** Payload map for the events `spa` emits, used to type the kernel's `emit` closure. */
1328
- type SpaEvents = {
1329
- /** A navigation has been intercepted and is starting. */"spa:navigate": {
1330
- from: string;
1331
- to: string;
1332
- }; /** The swap completed and the new URL is active. */
1333
- "spa:navigated": {
1334
- url: string;
1335
- }; /** A component instance attached to an element. */
1336
- "spa:component-mount": {
1337
- name: string;
1338
- el: Element;
1339
- }; /** A component instance detached from an element. */
1340
- "spa:component-unmount": {
1341
- name: string;
1342
- el: Element;
1343
- };
1344
- };
1345
- /** Strictly-typed emit closure for the spa events (kernel overload form). */
1346
- type SpaEmitFunction = EmitFn<SpaEvents>;
1347
- /**
1348
- * Structural extraction of a plugin instance's public API from its `_phantom`
1349
- * carrier (mirrors the kernel's non-exported `ExtractPluginApi`).
1350
- *
1351
- * @example
1352
- * type RApi = ExtractApi<typeof routerPlugin>;
1353
- */
1354
- type ExtractApi<PluginCandidate> = PluginCandidate extends {
1355
- readonly _phantom: {
1356
- readonly api: infer PluginApi;
1357
- };
1358
- } ? PluginApi : never;
1359
1030
  /** Generic `require` closure for pulling dependency plugin APIs at init time. */
1360
1031
  type SpaRequire = <PluginCandidate extends {
1361
1032
  readonly name: string;
@@ -1366,7 +1037,7 @@ type SpaRequire = <PluginCandidate extends {
1366
1037
  readonly api: unknown;
1367
1038
  readonly events: Record<string, unknown>;
1368
1039
  };
1369
- }>(plugin: PluginCandidate) => ExtractApi<PluginCandidate>;
1040
+ }>(plugin: PluginCandidate) => ExtractApi$1<PluginCandidate>;
1370
1041
  /**
1371
1042
  * The plugin-context slice the spa wiring consumes in `onInit`/`onStart`:
1372
1043
  * mutable `state`, resolved `config`, `require`/`emit`/`log`. Structurally
@@ -1379,6 +1050,11 @@ interface SpaContext {
1379
1050
  readonly config: Readonly<SpaConfig>;
1380
1051
  /** Resolve a depended-upon plugin instance to its public API. */
1381
1052
  require: SpaRequire;
1053
+ /**
1054
+ * Whether a plugin is registered (by name). Used to detect the OPTIONAL `data`
1055
+ * plugin at init — `spa` enables client DATA navigation only when `has("data")`.
1056
+ */
1057
+ has: (name: string) => boolean;
1382
1058
  /** Emit a spa lifecycle event (notification-only). */
1383
1059
  emit: SpaEmitFunction;
1384
1060
  /** Structured logger (core `log` API). */
@@ -1505,12 +1181,26 @@ interface ComponentInstance {
1505
1181
  }
1506
1182
  /** Page data payload parsed from the inline `script#__DATA__` element. */
1507
1183
  type PageData = Record<string, unknown>;
1508
- /** Resolved dependency APIs the kernel reuses (router match/manifest, head compose). */
1184
+ /**
1185
+ * The OPTIONAL `data` provider reader the kernel uses for client DATA navigation —
1186
+ * a structural slice of the `data` plugin's API (fetch the persisted JSON for a
1187
+ * page path). Captured at init via `ctx.has("data")`/`ctx.require` so `spa` never
1188
+ * imports the `data` plugin or its types.
1189
+ */
1190
+ type SpaDataReader = (path: string) => Promise<unknown | null>;
1191
+ /** Resolved dependency APIs the kernel reuses (router match/mode, head compose, optional data). */
1509
1192
  interface SpaKernelDeps {
1510
- /** Router plugin API — used for client-side route classification/matching. */
1193
+ /** Router plugin API — used for client-side route matching (`match`) + the resolved `mode`. */
1511
1194
  router: RouterApi;
1512
1195
  /** Head plugin API — its pure compose is reused for client head-sync. */
1513
- head: Api$2;
1196
+ head: Api$3;
1197
+ /**
1198
+ * The OPTIONAL `data` reader. Present only when the `data` plugin is composed.
1199
+ * When present (and `router.mode() !== "ssg"`), navigation first tries the client
1200
+ * DATA path (match → `dataAt(path)` → `route.parse` → `route.render`); otherwise
1201
+ * it always uses HTML-over-fetch.
1202
+ */
1203
+ dataAt?: SpaDataReader;
1514
1204
  }
1515
1205
  /** The single shared SPA kernel — pure factory over state/config/emit/deps. */
1516
1206
  interface SpaKernel {
@@ -1540,76 +1230,774 @@ interface SpaKernel {
1540
1230
  */
1541
1231
  register(component: ComponentDef): void;
1542
1232
  /**
1543
- * Process a navigation to `path`: fetch then swap then head-sync then emit.
1544
- *
1545
- * @param path - The target path to navigate to.
1546
- * @returns void
1547
- * @example
1548
- * kernel.processNav("/about");
1233
+ * Process a navigation to `path`: fetch then swap then head-sync then emit.
1234
+ *
1235
+ * @param path - The target path to navigate to.
1236
+ * @returns void
1237
+ * @example
1238
+ * kernel.processNav("/about");
1239
+ */
1240
+ processNav(path: string): void;
1241
+ /**
1242
+ * Query the swap region and mount components for matching elements.
1243
+ *
1244
+ * @returns void
1245
+ * @example
1246
+ * kernel.scan();
1247
+ */
1248
+ scan(): void;
1249
+ /**
1250
+ * Tear down router listeners, run unmount/destroy, clear instances.
1251
+ *
1252
+ * @returns void
1253
+ * @example
1254
+ * kernel.dispose();
1255
+ */
1256
+ dispose(): void;
1257
+ }
1258
+ /** Internal mutable state for the spa plugin (all kernel data lives here). */
1259
+ interface SpaState {
1260
+ /** Components registered by name (last-registered-wins). */
1261
+ registeredComponents: Map<string, ComponentDef>;
1262
+ /** Live component instances keyed by their bound element. */
1263
+ instances: Map<Element, ComponentInstance>;
1264
+ /** The current resolved URL (pathname + search). */
1265
+ currentUrl: string;
1266
+ /** Teardown handle for the attached router listeners (null when detached). */
1267
+ destroyRouter: (() => void) | null;
1268
+ /** Whether the browser runtime has been booted. */
1269
+ started: boolean;
1270
+ /** The single shared SPA kernel instance (null until onInit builds it). */
1271
+ kernel: SpaKernel | null;
1272
+ }
1273
+ /** Public API of the spa plugin (registration / control surface). */
1274
+ type SpaApi = {
1275
+ /**
1276
+ * Register a component definition for client mounting.
1277
+ *
1278
+ * @param component - The component definition created via `createComponent`.
1279
+ * @returns void
1280
+ * @example
1281
+ * app.spa.register(counter);
1282
+ */
1283
+ register(component: ComponentDef): void;
1284
+ /**
1285
+ * Programmatically navigate to a path (client runtime; no-op without a DOM).
1286
+ *
1287
+ * @param path - Target path (pathname, optionally with search/hash).
1288
+ * @returns void
1289
+ * @example
1290
+ * app.spa.navigate("/about");
1291
+ */
1292
+ navigate(path: string): void;
1293
+ /**
1294
+ * Read the current resolved URL.
1295
+ *
1296
+ * @returns The current pathname + search.
1297
+ * @example
1298
+ * const url = app.spa.current(); // "/about"
1299
+ */
1300
+ current(): string;
1301
+ };
1302
+ declare namespace types_d_exports {
1303
+ export { Api$2 as Api, BuildCacheEntry, BuildEvents, BuildResult, Config$2 as Config, ExtractApi, OgFont, OgImageConfig, OgPngRenderer, PhaseContext, PhaseEmit, PhaseLog, PhaseName, PhaseRequire, RichOgInput, State$2 as State };
1304
+ }
1305
+ /**
1306
+ * Structural extraction of a plugin instance's public API from its `_phantom`
1307
+ * carrier (mirrors the kernel's non-exported `ExtractPluginApi`) so the
1308
+ * framework's generic `require` is assignable to {@link PhaseContext.require}.
1309
+ *
1310
+ * @example
1311
+ * ```ts
1312
+ * type ContentApi = ExtractApi<typeof contentPlugin>;
1313
+ * ```
1314
+ */
1315
+ type ExtractApi<PluginCandidate> = PluginCandidate extends {
1316
+ readonly _phantom: {
1317
+ readonly api: infer PluginApi;
1318
+ };
1319
+ } ? PluginApi : never;
1320
+ /**
1321
+ * Minimal logger slice used by the pipeline and phases (the core `log` API).
1322
+ *
1323
+ * @example
1324
+ * ```ts
1325
+ * const log: PhaseLog = { info: () => {}, debug: () => {}, warn: () => {}, error: () => {} };
1326
+ * ```
1327
+ */
1328
+ type PhaseLog = {
1329
+ /** Record an informational event. */info(event: string, data?: unknown): void; /** Record a debug event. */
1330
+ debug(event: string, data?: unknown): void; /** Record a warning event. */
1331
+ warn(event: string, data?: unknown): void; /** Record an error event. */
1332
+ error(event: string, data?: unknown): void;
1333
+ };
1334
+ /**
1335
+ * Payload map for the events `build` emits, used to type the `emit` closure
1336
+ * handed to the pipeline driver and phases.
1337
+ *
1338
+ * @example
1339
+ * ```ts
1340
+ * const emit: PhaseEmit = (name, payload) => kernel.emit(name, payload);
1341
+ * ```
1342
+ */
1343
+ type BuildEvents = {
1344
+ /** Phase boundary marker (start, then done with durationMs). */"build:phase": {
1345
+ phase: PhaseName;
1346
+ status: "start" | "done";
1347
+ durationMs?: number;
1348
+ }; /** One successful-run summary. */
1349
+ "build:complete": {
1350
+ outDir: string;
1351
+ pageCount: number;
1352
+ durationMs: number;
1353
+ };
1354
+ };
1355
+ /** Strictly-typed emit closure for the build events (kernel overload form). */
1356
+ type PhaseEmit = EmitFn<BuildEvents>;
1357
+ /** Generic `require` closure for pulling dependency plugin APIs at run time. */
1358
+ type PhaseRequire = <PluginCandidate extends {
1359
+ readonly name: string;
1360
+ readonly spec: unknown;
1361
+ readonly _phantom: {
1362
+ readonly config: unknown;
1363
+ readonly state: unknown;
1364
+ readonly api: unknown;
1365
+ readonly events: Record<string, unknown>;
1366
+ };
1367
+ }>(plugin: PluginCandidate) => ExtractApi<PluginCandidate>;
1368
+ /**
1369
+ * The plugin-context slice the pipeline driver and every phase consume: the
1370
+ * mutable `state`, the resolved `config`/`global`, plus `require`/`emit`/`log`.
1371
+ * Typed to match the kernel's generic context so the framework execution
1372
+ * context is structurally assignable.
1373
+ *
1374
+ * @example
1375
+ * ```ts
1376
+ * const ctx: PhaseContext = { state, config, global, require, emit, log };
1377
+ * ```
1378
+ */
1379
+ type PhaseContext = {
1380
+ /** Mutable per-run build state (caches + runId). */state: State$2; /** Resolved, frozen build config. */
1381
+ readonly config: Readonly<Config$2>; /** Global framework config (mode, etc.). */
1382
+ readonly global: Readonly<{
1383
+ mode: "production" | "development";
1384
+ }>; /** Resolve a depended-upon plugin instance to its public API. */
1385
+ require: PhaseRequire; /** Whether a plugin is registered (by name) — used to detect the OPTIONAL `data` plugin. */
1386
+ has: (name: string) => boolean; /** Emit a build event (notification-only). */
1387
+ emit: PhaseEmit; /** Structured logger (core `log` API). */
1388
+ readonly log: PhaseLog;
1389
+ };
1390
+ /**
1391
+ * Injectable PNG renderer for the og-images phase. Defaults to the real
1392
+ * Satori → resvg pipeline; unit tests inject a fake to assert hash-cache skip
1393
+ * and the `p-limit` bound without rasterizing real images.
1394
+ *
1395
+ * @example
1396
+ * ```ts
1397
+ * const render: OgPngRenderer = async () => new Uint8Array();
1398
+ * ```
1399
+ */
1400
+ type OgPngRenderer = (input: RichOgInput) => Promise<Uint8Array>;
1401
+ /**
1402
+ * Rich input handed to a custom OG `render` hook for a single article card. Carries
1403
+ * the full article + site metadata so a consumer can compose any layout. Returned by
1404
+ * the framework, not authored by consumers directly.
1405
+ *
1406
+ * @example
1407
+ * ```ts
1408
+ * const input: RichOgInput = {
1409
+ * title: "Hello", description: "Intro", date: "2026-01-15", tags: ["a"],
1410
+ * locale: "en", siteName: "Blog", size: { width: 1200, height: 630 }
1411
+ * };
1412
+ * ```
1413
+ */
1414
+ interface RichOgInput {
1415
+ /** Article title rendered into the card. */
1416
+ title: string;
1417
+ /** Article description / summary. */
1418
+ description: string;
1419
+ /** Publication date (ISO string from frontmatter). */
1420
+ date: string;
1421
+ /** Article tags. */
1422
+ tags: string[];
1423
+ /** Optional author name. */
1424
+ author?: string;
1425
+ /** Active locale for the card. */
1426
+ locale: string;
1427
+ /** Site name (from the site plugin / config). */
1428
+ siteName: string;
1429
+ /** Output dimensions for the card. */
1430
+ size: {
1431
+ width: number;
1432
+ height: number;
1433
+ };
1434
+ }
1435
+ /**
1436
+ * A single custom OG font entry. Each `path` is read to a Buffer ONCE per build and
1437
+ * handed to Satori. `weight`/`style` default to `400`/`"normal"` when omitted.
1438
+ *
1439
+ * @example
1440
+ * ```ts
1441
+ * const font: OgFont = { name: "Inter", path: "./fonts/Inter.ttf", weight: 600 };
1442
+ * ```
1443
+ */
1444
+ interface OgFont {
1445
+ /** Font family name referenced by the rendered card. */
1446
+ name: string;
1447
+ /** Path to the .ttf/.otf/.woff file. */
1448
+ path: string;
1449
+ /** Numeric weight (defaults to 400). */
1450
+ weight?: number;
1451
+ /** Font style (defaults to "normal"). */
1452
+ style?: "normal" | "italic";
1453
+ }
1454
+ /**
1455
+ * Optional OG-image generation config. Omit the field (or set `false`) to disable.
1456
+ *
1457
+ * The optional `render` hook (`@jsxImportSource preact`) lets a consumer return a
1458
+ * Preact `VNode` for the card; the framework casts it to Satori's input at the single
1459
+ * render boundary. `fonts` supplies multiple named fonts loaded once per build.
1460
+ *
1461
+ * @example
1462
+ * ```ts
1463
+ * const og: OgImageConfig = { fontDir: "./fonts" };
1464
+ * ```
1465
+ */
1466
+ interface OgImageConfig {
1467
+ /** Directory containing at least one .ttf/.otf/.woff font. Validated in onInit (void — config check only). */
1468
+ fontDir: string;
1469
+ /** Optional path to a custom OG template module. Falls back to the built-in template. */
1470
+ template?: string;
1471
+ /** Output dimensions. Defaults to 1200x630. */
1472
+ size?: {
1473
+ width: number;
1474
+ height: number;
1475
+ };
1476
+ /** Custom card renderer; returns a Preact `VNode` from the {@link RichOgInput}. */
1477
+ render?(input: RichOgInput): import("preact").VNode;
1478
+ /** Explicit named fonts loaded once per build (overrides the first-file scan). */
1479
+ fonts?: OgFont[];
1480
+ }
1481
+ /**
1482
+ * Public configuration for the `build` plugin. Flags give opt-in granularity over
1483
+ * individual outputs without rewriting the pipeline.
1484
+ *
1485
+ * @example
1486
+ * ```ts
1487
+ * const config: Config = { outDir: "./dist", minify: true, feeds: true, sitemap: true, images: true, ogImage: false };
1488
+ * ```
1489
+ */
1490
+ type Config$2 = {
1491
+ /** Output directory for the built site. */outDir: string; /** Minify bundled CSS/JS. */
1492
+ minify: boolean; /** Generate RSS/Atom/JSON feeds. */
1493
+ feeds: boolean; /** Generate sitemap.xml + robots.txt. */
1494
+ sitemap: boolean; /** Optimize + copy content images. */
1495
+ images: boolean; /** OG-image generation. `false` (or omitted) disables it; an object enables and configures it. */
1496
+ ogImage: OgImageConfig | false; /** Auto-inject bundled `main.{css,js}` into rendered pages. Default `true`. */
1497
+ injectAssets?: boolean; /** Directory copied verbatim into `outDir` (skipped silently if absent). Default `"public"`. */
1498
+ publicDir?: string;
1499
+ /**
1500
+ * Emit `outDir/404.html`. `true` for the built-in default page, or
1501
+ * `{ route }` to supply the page's literal HTML body content (NOT a route
1502
+ * path/slug — the string is written into the 404 page verbatim). Default `false`.
1503
+ */
1504
+ notFound?: boolean | {
1505
+ route?: string;
1506
+ }; /** Emit per-path i18n bare-path redirect HTML pages. Default `false`. */
1507
+ localeRedirects?: boolean; /** Authoritative client bundle entry path (overrides the conventional scan). */
1508
+ clientEntry?: string; /** HTML shell template with `<!--moku:head-->`/`<!--moku:body-->`/`<!--moku:assets-->` placeholders. */
1509
+ template?: string;
1510
+ };
1511
+ /**
1512
+ * A typed asset-manifest entry for one bundled asset kind (CSS or JS): a map of the
1513
+ * original entry basename to its hashed on-disk output path. Replaces the untyped
1514
+ * `Map<string, unknown>` reads when emitting `<link>`/`<script>` tags in pages.tsx.
1515
+ *
1516
+ * @example
1517
+ * ```ts
1518
+ * const entry: BuildCacheEntry = { "main.css": "assets/main-abc123.css" };
1519
+ * ```
1520
+ */
1521
+ type BuildCacheEntry = Record<string, string>;
1522
+ /**
1523
+ * Per-run closure state for the `build` plugin. Holds caches and config only —
1524
+ * no domain data is duplicated here (pulled fresh via `ctx.require` each run).
1525
+ *
1526
+ * @example
1527
+ * ```ts
1528
+ * const state: State = { config, manifest: null, buildCache: new Map(), runId: null, ogImageHashCache: new Map() };
1529
+ * ```
1530
+ */
1531
+ interface State$2 {
1532
+ /** Resolved, frozen config snapshot. */
1533
+ config: Config$2;
1534
+ /** Cached route manifest for the current run (populated in Phase 3 from router). */
1535
+ manifest: RouteDefinition[] | null;
1536
+ /** Per-run build artifacts (e.g. hashed CSS/JS asset paths from Phase 1). */
1537
+ buildCache: Map<string, unknown>;
1538
+ /** Unique id for the current run (timestamp/uuid) — injected as build-id meta. */
1539
+ runId: string | null;
1540
+ /**
1541
+ * Content-hash cache for OG images: slug -> sha256(title + template + size).
1542
+ * Loaded from `<outDir>/.cache/og-images.json` at the OG phase and written back,
1543
+ * so unchanged articles are skipped on the next run.
1544
+ */
1545
+ ogImageHashCache: Map<string, string>;
1546
+ }
1547
+ /**
1548
+ * Ordered names of the build pipeline phases, in execution order.
1549
+ *
1550
+ * @example
1551
+ * ```ts
1552
+ * const phase: PhaseName = "bundle";
1553
+ * ```
1554
+ */
1555
+ type PhaseName = "bundle" | "content" | "images" | "pages" | "feeds" | "sitemap" | "og-images" | "public" | "not-found" | "locale-redirects" | "root-index";
1556
+ /**
1557
+ * Result of a completed build run.
1558
+ *
1559
+ * @example
1560
+ * ```ts
1561
+ * const result: BuildResult = { outDir: "./dist", pageCount: 12, durationMs: 840 };
1562
+ * ```
1563
+ */
1564
+ interface BuildResult {
1565
+ /** Resolved output directory the site was written to. */
1566
+ outDir: string;
1567
+ /** Number of route pages rendered. */
1568
+ pageCount: number;
1569
+ /** Total wall-clock duration of the run, in milliseconds. */
1570
+ durationMs: number;
1571
+ }
1572
+ /**
1573
+ * Public API surface mounted on `app.build`.
1574
+ *
1575
+ * @example
1576
+ * ```ts
1577
+ * const result = await app.build.run();
1578
+ * ```
1579
+ */
1580
+ type Api$2 = {
1581
+ /**
1582
+ * Run the full SSG pipeline and write the site to disk.
1583
+ *
1584
+ * @param options - Optional run overrides.
1585
+ * @param options.outDir - Override the configured output directory for this run.
1586
+ * @returns The build result (outDir, pageCount, durationMs).
1587
+ * @example
1588
+ * ```ts
1589
+ * const result = await app.build.run({ outDir: "./preview" });
1590
+ * ```
1591
+ */
1592
+ run(options?: {
1593
+ outDir?: string;
1594
+ }): Promise<BuildResult>;
1595
+ /**
1596
+ * List the phases in execution order (introspection / tooling).
1597
+ *
1598
+ * @returns The static ordered phase names.
1599
+ * @example
1600
+ * ```ts
1601
+ * const order = app.build.phases();
1602
+ * ```
1603
+ */
1604
+ phases(): PhaseName[];
1605
+ };
1606
+ //#endregion
1607
+ //#region src/plugins/build/index.d.ts
1608
+ /**
1609
+ * Build plugin — the static-site-generation orchestrator. Renders every route to
1610
+ * `outDir`, and optionally emits feeds, a sitemap, optimized images, and OG
1611
+ * images. Depends on site, i18n, content, router, and head; emits `build:phase`.
1612
+ *
1613
+ * @example Configure the production build
1614
+ * ```ts
1615
+ * const app = createApp({
1616
+ * pluginConfigs: {
1617
+ * build: {
1618
+ * outDir: "dist",
1619
+ * minify: true,
1620
+ * feeds: true,
1621
+ * sitemap: true,
1622
+ * images: true,
1623
+ * ogImage: false // or an object to enable + configure OG-image generation
1624
+ * }
1625
+ * }
1626
+ * });
1627
+ * ```
1628
+ */
1629
+ declare const buildPlugin: import("@moku-labs/core").PluginInstance<"build", Config$2, State$2, Api$2, {
1630
+ "build:phase": {
1631
+ phase: PhaseName;
1632
+ status: "start" | "done";
1633
+ durationMs?: number;
1634
+ };
1635
+ "build:complete": {
1636
+ outDir: string;
1637
+ pageCount: number;
1638
+ durationMs: number;
1639
+ };
1640
+ }> & Record<never, never>;
1641
+ declare namespace types_d_exports$1 {
1642
+ export { Api$1 as Api, Article, ArticleCard, ComputedFields, Config$1 as Config, ContentApiContext, ContentEvents, Frontmatter, State$1 as State };
1643
+ }
1644
+ /**
1645
+ * Configuration for the content plugin.
1646
+ *
1647
+ * @example
1648
+ * ```ts
1649
+ * { contentDir: "./src/content", trustedContent: false, shikiTheme: "github-dark" }
1650
+ * ```
1651
+ */
1652
+ type Config$1 = {
1653
+ /** Absolute or project-relative path to the content directory. Validated in onInit. */contentDir: string;
1654
+ /**
1655
+ * SECURITY GATE. When false (the default), rehype-sanitize runs as the final
1656
+ * pipeline step. Set true ONLY for fully author-controlled Markdown — true
1657
+ * disables sanitize and trusts all raw HTML.
1658
+ */
1659
+ trustedContent: boolean; /** Additional remark plugins, concatenated AFTER framework defaults. Defaults to []. */
1660
+ extraRemarkPlugins?: readonly Pluggable[]; /** Additional rehype plugins, concatenated after custom transforms, before Shiki + sanitize. Defaults to []. */
1661
+ extraRehypePlugins?: readonly Pluggable[]; /** Shiki theme name for syntax highlighting. Defaults to "github-dark". */
1662
+ shikiTheme?: string; /** Author applied to articles whose frontmatter omits author. Defaults to undefined. */
1663
+ defaultAuthor?: string;
1664
+ };
1665
+ /**
1666
+ * Internal mutable state for the content plugin.
1667
+ *
1668
+ * @example
1669
+ * ```ts
1670
+ * { processor: null, articles: new Map(), slugs: null, dirtyPaths: new Set() }
1671
+ * ```
1672
+ */
1673
+ type State$1 = {
1674
+ /** Lazily-created unified processor singleton. null until first render()/loadAll(). */processor: Processor | null; /** Article cache keyed locale -> (slug -> Article). Starts empty. */
1675
+ articles: Map<string, Map<string, Article>>; /** Discovered, sorted slug list cached after first disk scan. null until first discovery. */
1676
+ slugs: string[] | null; /** Paths marked stale by invalidate(); next loadAll() re-reads only these. Starts empty. */
1677
+ dirtyPaths: Set<string>;
1678
+ };
1679
+ /**
1680
+ * YAML frontmatter parsed from each article file.
1681
+ *
1682
+ * @example
1683
+ * ```ts
1684
+ * { title: "Hello", date: "2026-01-15", description: "Intro", tags: [], language: "en" }
1685
+ * ```
1686
+ */
1687
+ type Frontmatter = {
1688
+ /** Article title. Required. */title: string; /** ISO 8601 date string, e.g. "2026-01-15". Required. */
1689
+ date: string; /** Short summary used in cards, feeds, and meta description. Required. */
1690
+ description: string; /** Topic tags. Required (may be empty array). */
1691
+ tags: string[]; /** Source language code of this file. Required. */
1692
+ language: string; /** Draft flag. Excluded from output in production mode. Defaults to false. */
1693
+ draft?: boolean; /** Author name. Falls back to config.defaultAuthor when omitted. */
1694
+ author?: string;
1695
+ };
1696
+ /**
1697
+ * Fields computed by the pipeline (not authored in frontmatter).
1698
+ *
1699
+ * @example
1700
+ * ```ts
1701
+ * { slug: "hello", readingTime: 1, contentId: "hello", status: "published", wordCount: 42 }
1702
+ * ```
1703
+ */
1704
+ type ComputedFields = {
1705
+ /** Article directory name. */slug: string; /** Reading time in minutes (ceiling, minimum 1). */
1706
+ readingTime: number; /** Stable content identifier (slug by default). */
1707
+ contentId: string; /** Derived publication status. */
1708
+ status: "published" | "draft"; /** Word count from the source body. */
1709
+ wordCount: number;
1710
+ };
1711
+ /**
1712
+ * A fully processed, render-ready article.
1713
+ *
1714
+ * @example
1715
+ * ```ts
1716
+ * { frontmatter, computed, html: "<p>…</p>", locale: "en", isFallback: false, url: "/en/hello/" }
1717
+ * ```
1718
+ */
1719
+ type Article = {
1720
+ /** Parsed frontmatter. */frontmatter: Frontmatter; /** Pipeline-computed metadata. */
1721
+ computed: ComputedFields; /** Sanitized rendered HTML body. */
1722
+ html: string; /** Locale this Article instance represents (the requested locale, even on fallback). */
1723
+ locale: string; /** True when the default-locale file was used because the requested locale was missing. */
1724
+ isFallback: boolean; /** Canonical URL for this article in this locale. */
1725
+ url: string;
1726
+ };
1727
+ /**
1728
+ * Lightweight projection of Article for cards/lists.
1729
+ *
1730
+ * @example
1731
+ * ```ts
1732
+ * { contentId: "hello", status: "published", title: "Hello", date: "2026-01-15", description: "Intro", tags: [], readingTime: 1, url: "/en/hello/" }
1733
+ * ```
1734
+ */
1735
+ type ArticleCard = {
1736
+ /** Stable content identifier. */contentId: string; /** Derived publication status. */
1737
+ status: "published" | "draft"; /** Article title. */
1738
+ title: string; /** ISO 8601 date string. */
1739
+ date: string; /** Short summary. */
1740
+ description: string; /** Topic tags. */
1741
+ tags: string[]; /** Reading time in minutes. */
1742
+ readingTime: number; /** Canonical URL for this article in this locale. */
1743
+ url: string;
1744
+ };
1745
+ /**
1746
+ * Notification-only events emitted by the content plugin.
1747
+ *
1748
+ * @example
1749
+ * ```ts
1750
+ * emit("content:ready", { locales: ["en"], articleCount: 3 });
1751
+ * ```
1752
+ */
1753
+ type ContentEvents = {
1754
+ /** All articles loaded across locales. */"content:ready": {
1755
+ locales: readonly string[];
1756
+ articleCount: number;
1757
+ }; /** Article paths marked stale for dev rebuild. */
1758
+ "content:invalidated": {
1759
+ paths: readonly string[];
1760
+ };
1761
+ };
1762
+ /**
1763
+ * Kernel-free domain context handed to createContentApi by the wiring harness.
1764
+ * Carries ctx.state (mutable escape hatch), config, global, emit, and the
1765
+ * i18n-derived locale/url helpers — so api.ts stays free of createPlugin/ctx.
1766
+ *
1767
+ * @example
1768
+ * ```ts
1769
+ * const apiContext: ContentApiContext = { state, config, global, emit, locales, defaultLocale, articleToUrl };
1770
+ * ```
1771
+ */
1772
+ type ContentApiContext = {
1773
+ /** Mutable plugin state (article cache + lazy processor). */state: State$1; /** Resolved plugin configuration. */
1774
+ config: Config$1; /** Global framework configuration (mode, etc.). */
1775
+ global: {
1776
+ mode: "production" | "development";
1777
+ }; /** Emit a registered content event. */
1778
+ emit: <K extends keyof ContentEvents>(event: K, payload: ContentEvents[K]) => void; /** Active locale codes from i18n. */
1779
+ locales: () => readonly string[]; /** Default locale code from i18n (fallback source). */
1780
+ defaultLocale: () => string; /** Build a canonical article URL for a locale + slug. */
1781
+ articleToUrl: (locale: string, slug: string) => string;
1782
+ };
1783
+ /**
1784
+ * Public API for the content plugin.
1785
+ *
1786
+ * @example
1787
+ * ```ts
1788
+ * const map = await app.content.loadAll();
1789
+ * ```
1790
+ */
1791
+ type Api$1 = {
1792
+ /**
1793
+ * Load every article across every active locale, returning a locale-keyed
1794
+ * map of date-descending Article arrays. Emits content:ready.
1795
+ */
1796
+ loadAll(): Promise<Map<string, Article[]>>;
1797
+ /**
1798
+ * Resolve and render a single article for one locale, with locale fallback.
1799
+ *
1800
+ * @param slug - Article directory name.
1801
+ * @param locale - Requested locale code.
1802
+ */
1803
+ load(slug: string, locale: string): Promise<Article>;
1804
+ /**
1805
+ * Render a raw Markdown string to HTML through the full pipeline.
1806
+ *
1807
+ * @param md - Raw Markdown source.
1808
+ */
1809
+ renderMarkdown(md: string): Promise<string>;
1810
+ /**
1811
+ * Mark file paths stale for incremental dev rebuilds. Emits content:invalidated.
1812
+ *
1813
+ * @param paths - File paths to invalidate.
1814
+ */
1815
+ invalidate(paths: readonly string[]): void;
1816
+ /**
1817
+ * Project a full Article to a lightweight ArticleCard for list/grid rendering.
1818
+ *
1819
+ * @param article - The source article.
1820
+ */
1821
+ articleToCard(article: Article): ArticleCard;
1822
+ };
1823
+ //#endregion
1824
+ //#region src/plugins/content/index.d.ts
1825
+ /**
1826
+ * Content plugin — Markdown pipeline: discovers files, parses frontmatter, renders
1827
+ * to sanitized HTML (rehype-sanitize unless `trustedContent`), and exposes a
1828
+ * locale-keyed Article model. Depends on i18n; emits `content:ready` and
1829
+ * `content:invalidated`.
1830
+ *
1831
+ * @example Point at a content directory and pick a Shiki theme
1832
+ * ```ts
1833
+ * const app = createApp({
1834
+ * pluginConfigs: {
1835
+ * content: {
1836
+ * contentDir: "./content",
1837
+ * shikiTheme: "github-dark",
1838
+ * defaultAuthor: "Ada Lovelace"
1839
+ * // trustedContent: true // ONLY for fully author-controlled Markdown — disables sanitize
1840
+ * }
1841
+ * }
1842
+ * });
1843
+ * ```
1844
+ */
1845
+ declare const contentPlugin: import("@moku-labs/core").PluginInstance<"content", Config$1, State$1, Api$1, {
1846
+ "content:ready": {
1847
+ locales: readonly string[];
1848
+ articleCount: number;
1849
+ };
1850
+ "content:invalidated": {
1851
+ paths: readonly string[];
1852
+ };
1853
+ }> & Record<never, never>;
1854
+ declare namespace types_d_exports$2 {
1855
+ export { DataConfig, DataEntry, DataProvider, DataState, DataWriteSummary };
1856
+ }
1857
+ /**
1858
+ * @file data plugin — type definitions (Standard tier).
1859
+ *
1860
+ * The `data` plugin is the **agnostic data provider** for the SSG→DATA→SPA pattern.
1861
+ * It owns ONE thing: the contract `page path → persisted JSON file`. It knows
1862
+ * NOTHING about what the data *is* — no domain types appear here. A route decides
1863
+ * its own data shape (`load`'s return) and its own validation (`route.parse`).
1864
+ *
1865
+ * - **Node (build):** `write(entries)` persists one JSON file per page, keyed by
1866
+ * the page's URL via {@link DataProvider.fileFor}. `build` supplies the entries
1867
+ * (it already expanded the routes), so there is no duplicate expansion here.
1868
+ * - **Browser (runtime):** `at(path)` fetches + caches that file as `unknown`; the
1869
+ * route's `parse` validates it into the route's data type before `render`.
1870
+ *
1871
+ * The Node-only file-writing code (`node:fs`) is isolated behind a lazy `import()`
1872
+ * inside `write()`, so composing `data` in a browser app keeps the bundle free of
1873
+ * `node:*`.
1874
+ */
1875
+ /**
1876
+ * Configuration for {@link dataPlugin}. All fields have defaults (see `./config`).
1877
+ *
1878
+ * @example
1879
+ * ```ts
1880
+ * const cfg: DataConfig = { outputDir: "_data", baseUrl: "/_data/" };
1881
+ * ```
1882
+ */
1883
+ type DataConfig = {
1884
+ /**
1885
+ * WRITE side (Node): output subdir relative to the build `outDir`, a filesystem
1886
+ * path where `write()` persists the per-page JSON. Default `"_data"`.
1549
1887
  */
1550
- processNav(path: string): void;
1888
+ outputDir: string;
1551
1889
  /**
1552
- * Query the swap region and mount components for matching elements.
1553
- *
1554
- * @returns void
1555
- * @example
1556
- * kernel.scan();
1890
+ * READ side (browser): site-root-relative URL the client fetches the per-page
1891
+ * JSON from. A different domain from {@link DataConfig.outputDir} (a filesystem
1892
+ * path); keep consistent (`"/" + trim(outputDir) + "/"`). Default `"/_data/"`.
1557
1893
  */
1558
- scan(): void;
1894
+ baseUrl: string;
1895
+ };
1896
+ /** One page's data to persist — `build` produces these from its route expansion. */
1897
+ interface DataEntry {
1898
+ /** The page's URL path (e.g. `/en/hello/`); maps to a file via {@link DataProvider.fileFor}. */
1899
+ path: string;
1900
+ /** The serializable data for this page (the route's `load`/projection output). */
1901
+ data: unknown;
1902
+ }
1903
+ /** Summary returned by {@link DataProvider.write} and cached in state. */
1904
+ interface DataWriteSummary {
1905
+ /** Number of per-page JSON files written. */
1906
+ fileCount: number;
1907
+ /** Total bytes written across all files. */
1908
+ bytes: number;
1909
+ /** The written file paths, relative to the build `outDir`. */
1910
+ files: string[];
1911
+ }
1912
+ /**
1913
+ * Internal data state. `lastWrite` records the most recent `write()` (Node);
1914
+ * `cache` memoizes fetched per-path data (browser, lazy). Both empty until their
1915
+ * respective side first runs.
1916
+ */
1917
+ interface DataState {
1918
+ /** Result of the last `write()`, or `null` if it has not run yet (Node). */
1919
+ lastWrite: DataWriteSummary | null;
1920
+ /** Per-path fetched data, cached after the first `at(path)` (browser). */
1921
+ cache: Map<string, unknown>;
1922
+ }
1923
+ /**
1924
+ * Public API mounted at `app.data` — the agnostic data provider. `write()` is the
1925
+ * Node persist side; `at()` is the browser read side; `urlFor`/`fileFor` are the
1926
+ * pure URL convention shared by both so the written file and fetched URL can never
1927
+ * drift.
1928
+ *
1929
+ * @example
1930
+ * ```ts
1931
+ * // Node build (build supplies the entries it already expanded):
1932
+ * await app.data.write([{ path: "/en/hello/", data: article }]);
1933
+ *
1934
+ * // Browser (inside spa nav): fetch the page's data, then route.parse validates it:
1935
+ * const raw = await app.data.at("/en/hello/"); // unknown | null
1936
+ * ```
1937
+ */
1938
+ type DataProvider = {
1559
1939
  /**
1560
- * Tear down router listeners, run unmount/destroy, clear instances.
1940
+ * READ (browser) fetch (and cache) the persisted data for a page path from
1941
+ * `config.baseUrl`. Returns the raw parsed JSON as `unknown` (the caller's
1942
+ * `route.parse` validates it), or `null` if the fetch/parse fails.
1561
1943
  *
1562
- * @returns void
1563
- * @example
1564
- * kernel.dispose();
1944
+ * @param path - The page URL path (e.g. `/en/hello/`).
1945
+ * @returns The page's raw data, or `null` on failure.
1565
1946
  */
1566
- dispose(): void;
1567
- }
1568
- /** Internal mutable state for the spa plugin (all kernel data lives here). */
1569
- interface SpaState {
1570
- /** Components registered by name (last-registered-wins). */
1571
- registeredComponents: Map<string, ComponentDef>;
1572
- /** Live component instances keyed by their bound element. */
1573
- instances: Map<Element, ComponentInstance>;
1574
- /** The current resolved URL (pathname + search). */
1575
- currentUrl: string;
1576
- /** Teardown handle for the attached router listeners (null when detached). */
1577
- destroyRouter: (() => void) | null;
1578
- /** Whether the browser runtime has been booted. */
1579
- started: boolean;
1580
- /** The single shared SPA kernel instance (null until onInit builds it). */
1581
- kernel: SpaKernel | null;
1582
- }
1583
- /** Public API of the spa plugin (registration / control surface). */
1584
- type SpaApi = {
1947
+ at(path: string): Promise<unknown | null>;
1585
1948
  /**
1586
- * Register a component definition for client mounting.
1949
+ * WRITE (Node) persist one JSON file per entry, keyed by page path via
1950
+ * {@link DataProvider.fileFor}. Called by `build` after it expands routes (no
1951
+ * duplicate expansion). Lazily loads its `node:fs` writer, so it never
1952
+ * contaminates a browser bundle.
1587
1953
  *
1588
- * @param component - The component definition created via `createComponent`.
1589
- * @returns void
1590
- * @example
1591
- * app.spa.register(counter);
1954
+ * @param entries - The per-page data to persist.
1955
+ * @param options - Optional overrides.
1956
+ * @param options.outDir - Build output directory to write under (default `./dist`).
1957
+ * @returns A summary of the written files.
1592
1958
  */
1593
- register(component: ComponentDef): void;
1959
+ write(entries: readonly DataEntry[], options?: {
1960
+ outDir?: string;
1961
+ }): Promise<DataWriteSummary>;
1594
1962
  /**
1595
- * Programmatically navigate to a path (client runtime; no-op without a DOM).
1963
+ * PURE the browser fetch URL for a page path (e.g. `/en/hello/` →
1964
+ * `/_data/en/hello/index.json`). Shared with {@link DataProvider.fileFor}.
1596
1965
  *
1597
- * @param path - Target path (pathname, optionally with search/hash).
1598
- * @returns void
1599
- * @example
1600
- * app.spa.navigate("/about");
1966
+ * @param path - The page URL path.
1967
+ * @returns The site-root-relative data URL.
1601
1968
  */
1602
- navigate(path: string): void;
1969
+ urlFor(path: string): string;
1603
1970
  /**
1604
- * Read the current resolved URL.
1971
+ * PURE the `outDir`-relative file path for a page path (e.g. `/en/hello/` →
1972
+ * `_data/en/hello/index.json`). Shared with {@link DataProvider.urlFor}.
1605
1973
  *
1606
- * @returns The current pathname + search.
1607
- * @example
1608
- * const url = app.spa.current(); // "/about"
1974
+ * @param path - The page URL path.
1975
+ * @returns The output-relative file path.
1609
1976
  */
1610
- current(): string;
1977
+ fileFor(path: string): string;
1611
1978
  };
1612
- declare namespace types_d_exports$2 {
1979
+ //#endregion
1980
+ //#region src/plugins/data/index.d.ts
1981
+ /**
1982
+ * Data plugin — the agnostic data provider. Mounts `write(entries)` (Node persist),
1983
+ * `at(path)` (browser read), and the pure `urlFor`/`fileFor` convention at `app.data`.
1984
+ *
1985
+ * @example
1986
+ * ```ts
1987
+ * // Node build: `build` calls app.data.write(...) during its pages phase when
1988
+ * // router.mode !== "ssg". Just compose the plugin:
1989
+ * const app = createApp({
1990
+ * plugins: [dataPlugin, contentPlugin, buildPlugin],
1991
+ * pluginConfigs: { content: { contentDir: "./content" }, router: { routes, mode: "hybrid" } }
1992
+ * });
1993
+ * await app.start();
1994
+ * await app.build.run(); // writes HTML + per-page data sidecars
1995
+ *
1996
+ * // Browser app: compose `dataPlugin` too; spa fetches via app.data.at(path) on nav.
1997
+ * ```
1998
+ */
1999
+ declare const dataPlugin: import("@moku-labs/core").PluginInstance<"data", DataConfig, DataState, DataProvider, {}> & Record<never, never>;
2000
+ declare namespace types_d_exports$3 {
1613
2001
  export { Api, Config, DeployErrorCode, DeployInitOptions, DeployResult, DeployRunOptions, InitResult, SpawnFunction, SpawnOptions, SpawnedProcess, State, WranglerErrorKind };
1614
2002
  }
1615
2003
  /**
@@ -1797,6 +2185,24 @@ type Api = {
1797
2185
  * Depends: site. Emits: deploy:complete.
1798
2186
  * @see README.md
1799
2187
  */
2188
+ /**
2189
+ * Deploy plugin — ships the built `outDir` to Cloudflare Pages via the injectable
2190
+ * wrangler subprocess, with entropy-gated secret scrubbing of logged output.
2191
+ * Depends on site; emits `deploy:complete`.
2192
+ *
2193
+ * @example Configure the deploy target
2194
+ * ```ts
2195
+ * const app = createApp({
2196
+ * pluginConfigs: {
2197
+ * deploy: {
2198
+ * target: "cloudflare-pages",
2199
+ * outDir: "dist",
2200
+ * productionBranch: "main"
2201
+ * }
2202
+ * }
2203
+ * });
2204
+ * ```
2205
+ */
1800
2206
  declare const deployPlugin: import("@moku-labs/core").PluginInstance<"deploy", Config, State, Api, {
1801
2207
  "deploy:complete": {
1802
2208
  url: string;
@@ -1806,31 +2212,6 @@ declare const deployPlugin: import("@moku-labs/core").PluginInstance<"deploy", C
1806
2212
  };
1807
2213
  }> & Record<never, never>;
1808
2214
  //#endregion
1809
- //#region src/plugins/build/index.d.ts
1810
- declare const buildPlugin: import("@moku-labs/core").PluginInstance<"build", Config$1, State$1, Api$1, {
1811
- "build:phase": {
1812
- phase: PhaseName;
1813
- status: "start" | "done";
1814
- durationMs?: number;
1815
- };
1816
- "build:complete": {
1817
- outDir: string;
1818
- pageCount: number;
1819
- durationMs: number;
1820
- };
1821
- }> & Record<never, never>;
1822
- //#endregion
1823
- //#region src/plugins/content/index.d.ts
1824
- declare const contentPlugin: import("@moku-labs/core").PluginInstance<"content", Config$3, State$3, Api$3, {
1825
- "content:ready": {
1826
- locales: readonly string[];
1827
- articleCount: number;
1828
- };
1829
- "content:invalidated": {
1830
- paths: readonly string[];
1831
- };
1832
- }> & Record<never, never>;
1833
- //#endregion
1834
2215
  //#region src/plugins/head/primitives.d.ts
1835
2216
  /**
1836
2217
  * Build a `<meta name=… content=…>` descriptor.
@@ -1910,7 +2291,26 @@ declare function feedLink(title: string, url: string, type?: string): HeadElemen
1910
2291
  declare function buildArticleHead(articleMeta: ArticleMeta, canonicalUrl: string): HeadElement[];
1911
2292
  //#endregion
1912
2293
  //#region src/plugins/head/index.d.ts
1913
- declare const headPlugin: import("@moku-labs/core").PluginInstance<"head", Config$2, State$2, Api$2, {}> & {
2294
+ /**
2295
+ * Head plugin — composes per-route `<head>` metadata (title template, Open Graph,
2296
+ * Twitter cards, canonical, hreflang). Use the re-exported SEO primitives
2297
+ * ({@link meta}, {@link og}, {@link twitter}, …) inside a route's `.head()`.
2298
+ * Depends on site, i18n, and router.
2299
+ *
2300
+ * @example Set global head defaults
2301
+ * ```ts
2302
+ * const app = createApp({
2303
+ * pluginConfigs: {
2304
+ * head: {
2305
+ * titleTemplate: "%s — My Blog",
2306
+ * twitterCard: "summary_large_image",
2307
+ * twitterHandle: "@moku_labs"
2308
+ * }
2309
+ * }
2310
+ * });
2311
+ * ```
2312
+ */
2313
+ declare const headPlugin: import("@moku-labs/core").PluginInstance<"head", Config$3, State$3, Api$3, {}> & {
1914
2314
  meta: typeof meta;
1915
2315
  og: typeof og;
1916
2316
  twitter: typeof twitter;
@@ -1922,6 +2322,25 @@ declare const headPlugin: import("@moku-labs/core").PluginInstance<"head", Confi
1922
2322
  };
1923
2323
  //#endregion
1924
2324
  //#region src/plugins/i18n/index.d.ts
2325
+ /**
2326
+ * Internationalization plugin — locale registry plus a flat translation helper
2327
+ * with default-locale fallback. Pure config-as-data (no state or events);
2328
+ * consumed read-only by content, router, head, and build.
2329
+ *
2330
+ * @example Register locales and translations
2331
+ * ```ts
2332
+ * const app = createApp({
2333
+ * pluginConfigs: {
2334
+ * i18n: {
2335
+ * locales: ["en", "uk"],
2336
+ * defaultLocale: "en",
2337
+ * localeNames: { en: "English", uk: "Українська" },
2338
+ * translations: { uk: { "nav.home": "Головна" } }
2339
+ * }
2340
+ * }
2341
+ * });
2342
+ * ```
2343
+ */
1925
2344
  declare const i18nPlugin: import("@moku-labs/core").PluginInstance<"i18n", Config$5, Record<string, never>, Api$5, {}> & Record<never, never>;
1926
2345
  //#endregion
1927
2346
  //#region src/plugins/log/index.d.ts
@@ -1967,15 +2386,74 @@ declare function route<P extends string>(pattern: P): RouteBuilder<RouteState<P>
1967
2386
  declare function defineRoutes<T extends RouteMap>(routes: T): T;
1968
2387
  //#endregion
1969
2388
  //#region src/plugins/router/index.d.ts
2389
+ /**
2390
+ * Router plugin — typed, named route definitions with locale-aware URL generation
2391
+ * and matching. Author routes with {@link route} + {@link defineRoutes}. Depends
2392
+ * on site (base URL) and i18n (locales).
2393
+ *
2394
+ * @example Define routes and choose a render mode
2395
+ * ```ts
2396
+ * const app = createApp({
2397
+ * pluginConfigs: {
2398
+ * router: {
2399
+ * routes: defineRoutes({
2400
+ * home: route("/"),
2401
+ * article: route("/blog/{slug}/")
2402
+ * }),
2403
+ * mode: "hybrid" // "ssg" | "spa" | "hybrid" (default)
2404
+ * }
2405
+ * }
2406
+ * });
2407
+ * ```
2408
+ */
1970
2409
  declare const routerPlugin: import("@moku-labs/core").PluginInstance<"router", RouterConfig, RouterState, RouterApi, {}> & {
1971
2410
  route: typeof route;
1972
2411
  defineRoutes: typeof defineRoutes;
1973
2412
  };
1974
2413
  //#endregion
1975
2414
  //#region src/plugins/site/index.d.ts
2415
+ /**
2416
+ * Site plugin — holds global, frozen site metadata (name, url, author,
2417
+ * description) and builds canonical URLs. Consumed by router, head, and build.
2418
+ * `name` and `url` must be non-empty (validated at `onInit`).
2419
+ *
2420
+ * @example Set your site identity
2421
+ * ```ts
2422
+ * const app = createApp({
2423
+ * pluginConfigs: {
2424
+ * site: {
2425
+ * name: "My Blog",
2426
+ * url: "https://blog.dev",
2427
+ * author: "Ada Lovelace",
2428
+ * description: "Notes on computing"
2429
+ * }
2430
+ * }
2431
+ * });
2432
+ * ```
2433
+ */
1976
2434
  declare const sitePlugin: import("@moku-labs/core").PluginInstance<"site", Config$6, Record<string, never>, Api$6, {}> & Record<never, never>;
1977
2435
  //#endregion
1978
2436
  //#region src/plugins/spa/index.d.ts
2437
+ /**
2438
+ * SPA plugin — progressive client-side navigation layered over the static site:
2439
+ * swaps a page region on navigation, with an optional progress bar and View
2440
+ * Transitions. Register interactive islands with {@link createComponent}. Depends
2441
+ * on router and head; emits `spa:navigate`, `spa:navigated`, `spa:component-mount`,
2442
+ * and `spa:component-unmount`.
2443
+ *
2444
+ * @example Enable view transitions and a custom swap region
2445
+ * ```ts
2446
+ * const app = createApp({
2447
+ * pluginConfigs: {
2448
+ * spa: {
2449
+ * swapSelector: "main > section",
2450
+ * viewTransitions: true,
2451
+ * progressBar: true
2452
+ * }
2453
+ * }
2454
+ * });
2455
+ * ```
2456
+ */
1979
2457
  declare const spaPlugin: import("@moku-labs/core").PluginInstance<"spa", SpaConfig, SpaState, SpaApi, {
1980
2458
  "spa:navigate": {
1981
2459
  from: string;
@@ -1994,115 +2472,150 @@ declare const spaPlugin: import("@moku-labs/core").PluginInstance<"spa", SpaConf
1994
2472
  };
1995
2473
  }> & Record<never, never>;
1996
2474
  //#endregion
2475
+ //#region src/plugins/env/providers.d.ts
2476
+ /**
2477
+ * A zero-dependency `.env`-style provider that re-reads and re-parses the file
2478
+ * from disk on every `load()`. Missing file resolves to `{}` (optional
2479
+ * overrides). Strips a single outer quote pair; does not strip trailing inline
2480
+ * comments on unquoted values.
2481
+ *
2482
+ * @param path - Path to the dotenv file. Defaults to `.env.local`.
2483
+ * @returns An {@link EnvProvider} named `dotenv:<path>` that reads fresh per call.
2484
+ * @example
2485
+ * ```ts
2486
+ * const provider = dotenv(".env.local");
2487
+ * provider.load(); // { PUBLIC_API_URL: "/api", ... }
2488
+ * ```
2489
+ */
2490
+ declare function dotenv(path?: string): EnvProvider;
2491
+ /**
2492
+ * A provider that returns a shallow copy of `process.env` at `load()` time.
2493
+ *
2494
+ * @returns An {@link EnvProvider} named `process-env`.
2495
+ * @example
2496
+ * ```ts
2497
+ * const provider = processEnv();
2498
+ * provider.load().HOME; // current process value
2499
+ * ```
2500
+ */
2501
+ declare function processEnv(): EnvProvider;
2502
+ /**
2503
+ * A provider that reads live, per-request Cloudflare bindings from
2504
+ * `globalThis.__CLOUDFLARE_ENV__` at `load()` time (`?? {}` when absent). Never
2505
+ * caches the binding object; the consumer owns the global's request lifecycle.
2506
+ *
2507
+ * @returns An {@link EnvProvider} named `cloudflare`.
2508
+ * @example
2509
+ * ```ts
2510
+ * globalThis.__CLOUDFLARE_ENV__ = env; // set by the request handler
2511
+ * const provider = cloudflareBindings();
2512
+ * provider.load(); // reads the current request's bindings
2513
+ * ```
2514
+ */
2515
+ declare function cloudflareBindings(): EnvProvider;
2516
+ //#endregion
1997
2517
  //#region src/index.d.ts
2518
+ /**
2519
+ * Create and initialize a `@moku-labs/web` application — the Layer-3 entry point.
2520
+ * Your overrides are merged over the framework defaults through the 4-level config
2521
+ * cascade, every plugin's lifecycle runs, and a fully-typed, frozen app is returned.
2522
+ *
2523
+ * The defaults are the isomorphic plugin set (`site`, `i18n`, `router`, `head`,
2524
+ * `spa` + `log`/`env` core). Add the Node-only plugins for an SSG build:
2525
+ * `createApp({ plugins: [contentPlugin, buildPlugin, deployPlugin] })`.
2526
+ *
2527
+ * @param options - Optional configuration:
2528
+ * - `pluginConfigs` — per-plugin overrides, keyed by plugin name.
2529
+ * - `config` — global framework config (e.g. `{ mode: "development" }`).
2530
+ * - `plugins` — extra plugins (Node-only built-ins or your own) merged into the app and its type.
2531
+ * - `onReady` / `onError` / `onStart` / `onStop` — lifecycle callbacks.
2532
+ * @returns The initialized app: `start()`, `stop()`, every plugin's API, and `log`.
2533
+ * @example
2534
+ * ```ts
2535
+ * // Node SSG build — add the node-only plugins:
2536
+ * const app = createApp({
2537
+ * plugins: [contentPlugin, buildPlugin, deployPlugin],
2538
+ * pluginConfigs: {
2539
+ * site: { name: "My Blog", url: "https://blog.dev", author: "Ada", description: "Notes" },
2540
+ * router: { routes: defineRoutes({ home: route("/"), post: route("/blog/{slug}/") }) }
2541
+ * }
2542
+ * });
2543
+ * await app.start();
2544
+ * await app.build.run();
2545
+ * ```
2546
+ */
1998
2547
  declare const createApp: <const ExtraPlugins extends readonly import("@moku-labs/core").AnyPluginInstance[] = readonly []>(options?: import("@moku-labs/core").CreateAppOptions<Config$7, Events, (import("@moku-labs/core").PluginInstance<"site", Config$6, Record<string, never>, Api$6, {}> & Record<never, never>) | (import("@moku-labs/core").PluginInstance<"i18n", Config$5, Record<string, never>, Api$5, {}> & Record<never, never>) | (import("@moku-labs/core").PluginInstance<"router", RouterConfig, RouterState, RouterApi, {}> & {
1999
- route: typeof route;
2000
- defineRoutes: typeof defineRoutes;
2001
- }) | (import("@moku-labs/core").PluginInstance<"content", Config$3, State$3, Api$3, {
2002
- "content:ready": {
2003
- locales: readonly string[];
2004
- articleCount: number;
2005
- };
2006
- "content:invalidated": {
2007
- paths: readonly string[];
2008
- };
2009
- }> & Record<never, never>) | (import("@moku-labs/core").PluginInstance<"head", Config$2, State$2, Api$2, {}> & {
2010
- meta: typeof meta;
2011
- og: typeof og;
2012
- twitter: typeof twitter;
2013
- jsonLd: typeof jsonLd;
2014
- canonical: typeof canonical;
2015
- hreflang: typeof hreflang;
2016
- feedLink: typeof feedLink;
2017
- buildArticleHead: typeof buildArticleHead;
2018
- }) | (import("@moku-labs/core").PluginInstance<"build", Config$1, State$1, Api$1, {
2019
- "build:phase": {
2020
- phase: PhaseName;
2021
- status: "start" | "done";
2022
- durationMs?: number;
2023
- };
2024
- "build:complete": {
2025
- outDir: string;
2026
- pageCount: number;
2027
- durationMs: number;
2028
- };
2029
- }> & Record<never, never>) | (import("@moku-labs/core").PluginInstance<"spa", SpaConfig, SpaState, SpaApi, {
2030
- "spa:navigate": {
2031
- from: string;
2032
- to: string;
2033
- };
2034
- "spa:navigated": {
2035
- url: string;
2036
- };
2037
- "spa:component-mount": {
2038
- name: string;
2039
- el: Element;
2040
- };
2041
- "spa:component-unmount": {
2042
- name: string;
2043
- el: Element;
2044
- };
2045
- }> & Record<never, never>) | (import("@moku-labs/core").PluginInstance<"deploy", Config, State, Api, {
2046
- "deploy:complete": {
2047
- url: string;
2048
- deploymentId: string;
2049
- branch: string;
2050
- durationMs: number;
2051
- };
2052
- }> & Record<never, never>) | ExtraPlugins[number], [...ExtraPlugins], import("@moku-labs/core").CoreApisFromTuple<[import("@moku-labs/core").CorePluginInstance<"log", LogConfig, LogState, LogApi>, import("@moku-labs/core").CorePluginInstance<"env", EnvConfig, EnvState, EnvApi>]>> | undefined) => import("@moku-labs/core").App<Config$7, Events, (import("@moku-labs/core").PluginInstance<"site", Config$6, Record<string, never>, Api$6, {}> & Record<never, never>) | (import("@moku-labs/core").PluginInstance<"i18n", Config$5, Record<string, never>, Api$5, {}> & Record<never, never>) | (import("@moku-labs/core").PluginInstance<"router", RouterConfig, RouterState, RouterApi, {}> & {
2053
- route: typeof route;
2054
- defineRoutes: typeof defineRoutes;
2055
- }) | (import("@moku-labs/core").PluginInstance<"content", Config$3, State$3, Api$3, {
2056
- "content:ready": {
2057
- locales: readonly string[];
2058
- articleCount: number;
2059
- };
2060
- "content:invalidated": {
2061
- paths: readonly string[];
2062
- };
2063
- }> & Record<never, never>) | (import("@moku-labs/core").PluginInstance<"head", Config$2, State$2, Api$2, {}> & {
2064
- meta: typeof meta;
2065
- og: typeof og;
2066
- twitter: typeof twitter;
2067
- jsonLd: typeof jsonLd;
2068
- canonical: typeof canonical;
2069
- hreflang: typeof hreflang;
2070
- feedLink: typeof feedLink;
2071
- buildArticleHead: typeof buildArticleHead;
2072
- }) | (import("@moku-labs/core").PluginInstance<"build", Config$1, State$1, Api$1, {
2073
- "build:phase": {
2074
- phase: PhaseName;
2075
- status: "start" | "done";
2076
- durationMs?: number;
2077
- };
2078
- "build:complete": {
2079
- outDir: string;
2080
- pageCount: number;
2081
- durationMs: number;
2082
- };
2083
- }> & Record<never, never>) | (import("@moku-labs/core").PluginInstance<"spa", SpaConfig, SpaState, SpaApi, {
2084
- "spa:navigate": {
2085
- from: string;
2086
- to: string;
2087
- };
2088
- "spa:navigated": {
2089
- url: string;
2090
- };
2091
- "spa:component-mount": {
2092
- name: string;
2093
- el: Element;
2094
- };
2095
- "spa:component-unmount": {
2096
- name: string;
2097
- el: Element;
2098
- };
2099
- }> & Record<never, never>) | (import("@moku-labs/core").PluginInstance<"deploy", Config, State, Api, {
2100
- "deploy:complete": {
2101
- url: string;
2102
- deploymentId: string;
2103
- branch: string;
2104
- durationMs: number;
2105
- };
2106
- }> & Record<never, never>) | ExtraPlugins[number], import("@moku-labs/core").CoreApisFromTuple<[import("@moku-labs/core").CorePluginInstance<"log", LogConfig, LogState, LogApi>, import("@moku-labs/core").CorePluginInstance<"env", EnvConfig, EnvState, EnvApi>]>>, createPlugin: import("@moku-labs/core").BoundCreatePluginFunction<Config$7, Events, import("@moku-labs/core").CoreApisFromTuple<[import("@moku-labs/core").CorePluginInstance<"log", LogConfig, LogState, LogApi>, import("@moku-labs/core").CorePluginInstance<"env", EnvConfig, EnvState, EnvApi>]>>;
2548
+ route: typeof route;
2549
+ defineRoutes: typeof defineRoutes;
2550
+ }) | (import("@moku-labs/core").PluginInstance<"head", Config$3, State$3, Api$3, {}> & {
2551
+ meta: typeof meta;
2552
+ og: typeof og;
2553
+ twitter: typeof twitter;
2554
+ jsonLd: typeof jsonLd;
2555
+ canonical: typeof canonical;
2556
+ hreflang: typeof hreflang;
2557
+ feedLink: typeof feedLink;
2558
+ buildArticleHead: typeof buildArticleHead;
2559
+ }) | (import("@moku-labs/core").PluginInstance<"spa", SpaConfig, SpaState, SpaApi, {
2560
+ "spa:navigate": {
2561
+ from: string;
2562
+ to: string;
2563
+ };
2564
+ "spa:navigated": {
2565
+ url: string;
2566
+ };
2567
+ "spa:component-mount": {
2568
+ name: string;
2569
+ el: Element;
2570
+ };
2571
+ "spa:component-unmount": {
2572
+ name: string;
2573
+ el: Element;
2574
+ };
2575
+ }> & Record<never, never>) | ExtraPlugins[number], [...ExtraPlugins], import("@moku-labs/core").CoreApisFromTuple<[import("@moku-labs/core").CorePluginInstance<"log", LogConfig, LogState, LogApi>, import("@moku-labs/core").CorePluginInstance<"env", EnvConfig, EnvState, EnvApi>]>> | undefined) => import("@moku-labs/core").App<Config$7, Events, (import("@moku-labs/core").PluginInstance<"site", Config$6, Record<string, never>, Api$6, {}> & Record<never, never>) | (import("@moku-labs/core").PluginInstance<"i18n", Config$5, Record<string, never>, Api$5, {}> & Record<never, never>) | (import("@moku-labs/core").PluginInstance<"router", RouterConfig, RouterState, RouterApi, {}> & {
2576
+ route: typeof route;
2577
+ defineRoutes: typeof defineRoutes;
2578
+ }) | (import("@moku-labs/core").PluginInstance<"head", Config$3, State$3, Api$3, {}> & {
2579
+ meta: typeof meta;
2580
+ og: typeof og;
2581
+ twitter: typeof twitter;
2582
+ jsonLd: typeof jsonLd;
2583
+ canonical: typeof canonical;
2584
+ hreflang: typeof hreflang;
2585
+ feedLink: typeof feedLink;
2586
+ buildArticleHead: typeof buildArticleHead;
2587
+ }) | (import("@moku-labs/core").PluginInstance<"spa", SpaConfig, SpaState, SpaApi, {
2588
+ "spa:navigate": {
2589
+ from: string;
2590
+ to: string;
2591
+ };
2592
+ "spa:navigated": {
2593
+ url: string;
2594
+ };
2595
+ "spa:component-mount": {
2596
+ name: string;
2597
+ el: Element;
2598
+ };
2599
+ "spa:component-unmount": {
2600
+ name: string;
2601
+ el: Element;
2602
+ };
2603
+ }> & Record<never, never>) | ExtraPlugins[number], import("@moku-labs/core").CoreApisFromTuple<[import("@moku-labs/core").CorePluginInstance<"log", LogConfig, LogState, LogApi>, import("@moku-labs/core").CorePluginInstance<"env", EnvConfig, EnvState, EnvApi>]>>;
2604
+ /**
2605
+ * Create a custom plugin bound to this framework's `Config`/`Events` and core
2606
+ * APIs. Plugin types are inferred from the spec object — never written explicitly.
2607
+ * Pass the result to {@link createApp} via `plugins`.
2608
+ *
2609
+ * @example
2610
+ * ```ts
2611
+ * const analytics = createPlugin("analytics", {
2612
+ * config: { writeKey: "" },
2613
+ * api: (ctx) => ({ track: (event: string) => ctx.log.info("analytics:track", { event }) })
2614
+ * });
2615
+ *
2616
+ * const app = createApp({ plugins: [analytics] });
2617
+ * ```
2618
+ */
2619
+ declare const createPlugin: import("@moku-labs/core").BoundCreatePluginFunction<Config$7, Events, import("@moku-labs/core").CoreApisFromTuple<[import("@moku-labs/core").CorePluginInstance<"log", LogConfig, LogState, LogApi>, import("@moku-labs/core").CorePluginInstance<"env", EnvConfig, EnvState, EnvApi>]>>;
2107
2620
  //#endregion
2108
- export { types_d_exports as Build, types_d_exports$1 as Content, types_d_exports$2 as Deploy, types_d_exports$3 as Env, types_d_exports$4 as Head, types_d_exports$5 as Log, types_d_exports$6 as Router, types_d_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 };
2621
+ export { types_d_exports as Build, types_d_exports$1 as Content, types_d_exports$2 as Data, types_d_exports$3 as Deploy, types_d_exports$4 as Env, types_d_exports$5 as Head, types_d_exports$6 as Log, types_d_exports$7 as Router, types_d_exports$8 as Spa, browserEnv, buildArticleHead, buildPlugin, canonical, cloudflareBindings, contentPlugin, createApp, createPlugin, dataPlugin, defineRoutes, deployPlugin, dotenv, envPlugin, feedLink, headPlugin, hreflang, i18nPlugin, jsonLd, logPlugin, meta, og, processEnv, route, routerPlugin, sitePlugin, spaPlugin, twitter };