@moku-labs/web 0.3.1 → 0.4.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.d.mts CHANGED
@@ -3,7 +3,7 @@ import { Pluggable, Processor } from "unified";
3
3
  import { ComponentChildren, VNode } from "preact";
4
4
 
5
5
  //#region src/plugins/log/types.d.ts
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, LayoutContext, 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;
@@ -562,6 +582,22 @@ interface RouteContext<S extends RouteState> {
562
582
  /** Active locale for this render. */
563
583
  readonly locale: string;
564
584
  }
585
+ /**
586
+ * Context handed to a route's `.layout()` wrapper: the render-time
587
+ * {@link RouteContext} plus the route's `.meta()` bag, so persistent chrome (e.g. a
588
+ * TopBar/TabNav) can read `locale` and `meta.activeTab`. Distinct from
589
+ * `RouteContext` because the layout is the only handler that needs `meta`; keeping
590
+ * it on its own type leaves `.render()`/`.head()` contexts unchanged.
591
+ *
592
+ * @remarks
593
+ * The layout is applied in the SSG render path ONLY. On client (SPA) navigation the
594
+ * chrome is persistent and the layout is intentionally NOT re-applied — only the
595
+ * inner swap region is replaced. See `build`'s pages phase and `spa`'s kernel.
596
+ */
597
+ interface LayoutContext<S extends RouteState> extends RouteContext<S> {
598
+ /** The route's `.meta()` bag (e.g. `{ activeTab: "home" }`). */
599
+ readonly meta: Record<string, unknown>;
600
+ }
565
601
  /** Head metadata produced by a route's `.head()` handler. */
566
602
  interface HeadConfig$1 {
567
603
  /** Document title. */
@@ -584,15 +620,33 @@ interface RouteBuilder<S extends RouteState> extends RouteDefinition {
584
620
  readonly params: S["params"];
585
621
  readonly data: Awaited<D>;
586
622
  }>;
587
- /** Attach a layout wrapper component. */
588
- layout(component: (children: ComponentChildren) => VNode): RouteBuilder<S>;
623
+ /**
624
+ * Attach a ctx-aware layout wrapper that frames this route's rendered page in
625
+ * persistent chrome. Receives the route's {@link LayoutContext} (render context +
626
+ * `meta`) and the page `children`. Applied in the SSG render path ONLY — on client
627
+ * navigation the chrome persists and only the inner swap region is replaced, so the
628
+ * layout is not re-run.
629
+ */
630
+ layout(component: (ctx: LayoutContext<S>, children: ComponentChildren) => VNode): RouteBuilder<S>;
589
631
  /** Attach the page render handler. */
590
632
  render(handler: (ctx: RouteContext<S>) => VNode): RouteBuilder<S>;
633
+ /**
634
+ * Attach the client-side validation gate: parse the raw `unknown` fetched from
635
+ * the persisted data file back into this route's data type `S["data"]`. Runs at
636
+ * the trust boundary before `render` on the client (and MUST return `S["data"]`,
637
+ * so a mismatched schema is a compile error). Throw inside it to reject malformed
638
+ * data — `spa` then falls back to HTML-over-fetch. Use a hand guard or any
639
+ * Standard-Schema validator (zod/valibot/arktype).
640
+ */
641
+ parse(handler: (raw: unknown) => S["data"]): RouteBuilder<S>;
591
642
  /** Attach the head/SEO handler. */
592
643
  head(handler: (ctx: RouteContext<S>) => HeadConfig$1): RouteBuilder<S>;
593
644
  /** Attach a static-generation param producer. */
594
645
  generate(handler: (locale: string) => S["params"][] | Promise<S["params"][]>): RouteBuilder<S>;
595
- /** Attach an arbitrary metadata bag. */
646
+ /**
647
+ * Attach an arbitrary metadata bag. The bag MUST be JSON-serializable: it is
648
+ * projected verbatim into `clientManifest()` and shipped to the browser.
649
+ */
596
650
  meta(meta: Record<string, unknown>): RouteBuilder<S>;
597
651
  /** Attach a JSON serializer for the route's data. */
598
652
  toJson(handler: (ctx: RouteContext<S>) => unknown): RouteBuilder<S>;
@@ -603,10 +657,12 @@ interface RouteBuilder<S extends RouteState> extends RouteDefinition {
603
657
  interface RouteHandlers {
604
658
  /** Data loader. */
605
659
  readonly load?: (params: Record<string, string>, locale: string) => unknown;
606
- /** Layout wrapper. */
607
- readonly layout?: (children: ComponentChildren) => VNode;
660
+ /** Layout wrapper (ctx-aware): frames the page in persistent chrome. SSG-only. */
661
+ readonly layout?: (ctx: LayoutContext<RouteState>, children: ComponentChildren) => VNode;
608
662
  /** Page renderer. */
609
663
  readonly render?: (ctx: RouteContext<RouteState>) => VNode;
664
+ /** Client-side validation gate: `unknown` (fetched JSON) → the route's data type, or throw. */
665
+ readonly parse?: (raw: unknown) => unknown;
610
666
  /** Head/SEO producer. */
611
667
  readonly head?: (ctx: RouteContext<RouteState>) => HeadConfig$1;
612
668
  /** Static-generation param producer. */
@@ -711,6 +767,8 @@ interface MatcherTable {
711
767
  interface RouterState {
712
768
  /** Compiled matcher table; `null` until `onInit` assigns it. */
713
769
  table: MatcherTable | null;
770
+ /** Resolved render mode (single source of truth; set in `onInit`). Defaults `"hybrid"`. */
771
+ mode: "ssg" | "spa" | "hybrid";
714
772
  }
715
773
  /** Plain-data input to `compileRoutes` — resolved DATA only, never the plugin ctx. */
716
774
  interface CompileInput {
@@ -725,6 +783,24 @@ interface CompileInput {
725
783
  /** Default locale used for bare-pattern fallback. */
726
784
  readonly defaultLocale: string;
727
785
  }
786
+ /**
787
+ * Serializable route entry for the client route-index — a projection of the
788
+ * compiled route table with NO `_handlers` closures, safe to ship to the browser
789
+ * (the SPA recompiles matchers lazily from `pattern`).
790
+ *
791
+ * @remarks
792
+ * `meta` MUST be JSON-serializable: `clientManifest()` is intended to survive a
793
+ * `JSON.stringify`/`JSON.parse` round-trip, so a route's `.meta()` bag must contain
794
+ * only JSON-safe values (no functions, symbols, or class instances).
795
+ */
796
+ interface ClientRoute {
797
+ /** URL pattern string, e.g. `/{lang:?}/{slug}/`. */
798
+ readonly pattern: string;
799
+ /** Route name key from the route map. */
800
+ readonly name: string;
801
+ /** Route metadata bag from `.meta()`. MUST be JSON-serializable. */
802
+ readonly meta: Record<string, unknown>;
803
+ }
728
804
  /** Public API exposed via `ctx.require(routerPlugin)`. */
729
805
  type RouterApi = {
730
806
  /**
@@ -767,197 +843,36 @@ type RouterApi = {
767
843
  * for (const def of ctx.require(routerPlugin).manifest()) { def._handlers.load?.({}, "en"); }
768
844
  */
769
845
  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
846
  /**
947
- * Mark file paths stale for incremental dev rebuilds. Emits content:invalidated.
847
+ * Serializable, specificity-sorted projection of the route table for client
848
+ * shipping. Maps the compiled table to `{ pattern, name, meta }` entries with NO
849
+ * `_handlers` closures, returned as a fresh frozen array. JSON-serializable so the
850
+ * SPA can embed it and recompile matchers lazily in the browser.
948
851
  *
949
- * @param paths - File paths to invalidate.
852
+ * @returns A fresh, frozen, specificity-sorted read-only array of {@link ClientRoute}.
853
+ * @example
854
+ * const json = JSON.stringify(ctx.require(routerPlugin).clientManifest());
950
855
  */
951
- invalidate(paths: readonly string[]): void;
856
+ clientManifest(): readonly ClientRoute[];
952
857
  /**
953
- * Project a full Article to a lightweight ArticleCard for list/grid rendering.
858
+ * The resolved render mode the single source of truth for static/hybrid/spa
859
+ * behavior. `build` reads it to decide whether to emit client data sidecars;
860
+ * `spa` reads it to decide whether to attempt client DATA navigation.
954
861
  *
955
- * @param article - The source article.
862
+ * @returns `"ssg" | "spa" | "hybrid"`.
863
+ * @example
864
+ * if (ctx.require(routerPlugin).mode() !== "ssg") { ... }
956
865
  */
957
- articleToCard(article: Article): ArticleCard;
866
+ mode(): "ssg" | "spa" | "hybrid";
958
867
  };
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 };
868
+ /** Re-export under the canonical `Config` name for the plugin-types barrel. */
869
+ type Config$4 = RouterConfig;
870
+ /** Re-export under the canonical `State` name for the plugin-types barrel. */
871
+ type State$4 = RouterState;
872
+ /** Re-export under the canonical `Api` name for the plugin-types barrel. */
873
+ type Api$4 = RouterApi;
874
+ declare namespace types_d_exports$5 {
875
+ export { Api$3 as Api, ArticleMeta, Config$3 as Config, HeadConfig, HeadDefaults, HeadElement, ResolvedRoute, State$3 as State };
961
876
  }
962
877
  /**
963
878
  * @file head plugin — type definitions skeleton
@@ -973,7 +888,7 @@ declare namespace types_d_exports$4 {
973
888
  * const config: Config = { titleTemplate: "%s — Moku" };
974
889
  * ```
975
890
  */
976
- type Config$2 = {
891
+ type Config$3 = {
977
892
  /** 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
893
  defaultOgImage?: string; /** Default Twitter card type emitted when og/twitter content is present. */
979
894
  twitterCard?: "summary" | "summary_large_image"; /** Default Twitter site handle (e.g. `"@moku_labs"`) emitted as `twitter:site`. */
@@ -991,7 +906,7 @@ type Config$2 = {
991
906
  * const state: State = { defaults: null };
992
907
  * ```
993
908
  */
994
- type State$2 = {
909
+ type State$3 = {
995
910
  /** Normalized head defaults, assigned once in `onInit` (initially `null`). */defaults: HeadDefaults | null;
996
911
  };
997
912
  /**
@@ -1085,7 +1000,7 @@ type ResolvedRoute = {
1085
1000
  * const html: string = api.render(route, data);
1086
1001
  * ```
1087
1002
  */
1088
- type Api$2 = {
1003
+ type Api$3 = {
1089
1004
  /**
1090
1005
  * Compose the final `<head>` inner HTML for a route. Pulled synchronously by `build`.
1091
1006
  *
@@ -1099,259 +1014,37 @@ type Api$2 = {
1099
1014
  */
1100
1015
  render(route: ResolvedRoute, data: unknown): string;
1101
1016
  };
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 };
1017
+ declare namespace types_d_exports$8 {
1018
+ 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
1019
  }
1020
+ /** Payload map for the events `spa` emits, used to type the kernel's `emit` closure. */
1021
+ type SpaEvents = {
1022
+ /** A navigation has been intercepted and is starting. */"spa:navigate": {
1023
+ from: string;
1024
+ to: string;
1025
+ }; /** The swap completed and the new URL is active. */
1026
+ "spa:navigated": {
1027
+ url: string;
1028
+ }; /** A component instance attached to an element. */
1029
+ "spa:component-mount": {
1030
+ name: string;
1031
+ el: Element;
1032
+ }; /** A component instance detached from an element. */
1033
+ "spa:component-unmount": {
1034
+ name: string;
1035
+ el: Element;
1036
+ };
1037
+ };
1038
+ /** Strictly-typed emit closure for the spa events (kernel overload form). */
1039
+ type SpaEmitFunction = EmitFn<SpaEvents>;
1105
1040
  /**
1106
1041
  * 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}.
1109
- *
1110
- * @example
1111
- * ```ts
1112
- * type ContentApi = ExtractApi<typeof contentPlugin>;
1113
- * ```
1114
- */
1115
- type ExtractApi$1<PluginCandidate> = PluginCandidate extends {
1116
- readonly _phantom: {
1117
- readonly api: infer PluginApi;
1118
- };
1119
- } ? 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`).
1042
+ * carrier (mirrors the kernel's non-exported `ExtractPluginApi`).
1350
1043
  *
1351
1044
  * @example
1352
1045
  * type RApi = ExtractApi<typeof routerPlugin>;
1353
1046
  */
1354
- type ExtractApi<PluginCandidate> = PluginCandidate extends {
1047
+ type ExtractApi$1<PluginCandidate> = PluginCandidate extends {
1355
1048
  readonly _phantom: {
1356
1049
  readonly api: infer PluginApi;
1357
1050
  };
@@ -1366,7 +1059,7 @@ type SpaRequire = <PluginCandidate extends {
1366
1059
  readonly api: unknown;
1367
1060
  readonly events: Record<string, unknown>;
1368
1061
  };
1369
- }>(plugin: PluginCandidate) => ExtractApi<PluginCandidate>;
1062
+ }>(plugin: PluginCandidate) => ExtractApi$1<PluginCandidate>;
1370
1063
  /**
1371
1064
  * The plugin-context slice the spa wiring consumes in `onInit`/`onStart`:
1372
1065
  * mutable `state`, resolved `config`, `require`/`emit`/`log`. Structurally
@@ -1379,6 +1072,11 @@ interface SpaContext {
1379
1072
  readonly config: Readonly<SpaConfig>;
1380
1073
  /** Resolve a depended-upon plugin instance to its public API. */
1381
1074
  require: SpaRequire;
1075
+ /**
1076
+ * Whether a plugin is registered (by name). Used to detect the OPTIONAL `data`
1077
+ * plugin at init — `spa` enables client DATA navigation only when `has("data")`.
1078
+ */
1079
+ has: (name: string) => boolean;
1382
1080
  /** Emit a spa lifecycle event (notification-only). */
1383
1081
  emit: SpaEmitFunction;
1384
1082
  /** Structured logger (core `log` API). */
@@ -1505,12 +1203,26 @@ interface ComponentInstance {
1505
1203
  }
1506
1204
  /** Page data payload parsed from the inline `script#__DATA__` element. */
1507
1205
  type PageData = Record<string, unknown>;
1508
- /** Resolved dependency APIs the kernel reuses (router match/manifest, head compose). */
1206
+ /**
1207
+ * The OPTIONAL `data` provider reader the kernel uses for client DATA navigation —
1208
+ * a structural slice of the `data` plugin's API (fetch the persisted JSON for a
1209
+ * page path). Captured at init via `ctx.has("data")`/`ctx.require` so `spa` never
1210
+ * imports the `data` plugin or its types.
1211
+ */
1212
+ type SpaDataReader = (path: string) => Promise<unknown | null>;
1213
+ /** Resolved dependency APIs the kernel reuses (router match/mode, head compose, optional data). */
1509
1214
  interface SpaKernelDeps {
1510
- /** Router plugin API — used for client-side route classification/matching. */
1215
+ /** Router plugin API — used for client-side route matching (`match`) + the resolved `mode`. */
1511
1216
  router: RouterApi;
1512
1217
  /** Head plugin API — its pure compose is reused for client head-sync. */
1513
- head: Api$2;
1218
+ head: Api$3;
1219
+ /**
1220
+ * The OPTIONAL `data` reader. Present only when the `data` plugin is composed.
1221
+ * When present (and `router.mode() !== "ssg"`), navigation first tries the client
1222
+ * DATA path (match → `dataAt(path)` → `route.parse` → `route.render`); otherwise
1223
+ * it always uses HTML-over-fetch.
1224
+ */
1225
+ dataAt?: SpaDataReader;
1514
1226
  }
1515
1227
  /** The single shared SPA kernel — pure factory over state/config/emit/deps. */
1516
1228
  interface SpaKernel {
@@ -1540,76 +1252,774 @@ interface SpaKernel {
1540
1252
  */
1541
1253
  register(component: ComponentDef): void;
1542
1254
  /**
1543
- * Process a navigation to `path`: fetch then swap then head-sync then emit.
1255
+ * Process a navigation to `path`: fetch then swap then head-sync then emit.
1256
+ *
1257
+ * @param path - The target path to navigate to.
1258
+ * @returns void
1259
+ * @example
1260
+ * kernel.processNav("/about");
1261
+ */
1262
+ processNav(path: string): void;
1263
+ /**
1264
+ * Query the swap region and mount components for matching elements.
1265
+ *
1266
+ * @returns void
1267
+ * @example
1268
+ * kernel.scan();
1269
+ */
1270
+ scan(): void;
1271
+ /**
1272
+ * Tear down router listeners, run unmount/destroy, clear instances.
1273
+ *
1274
+ * @returns void
1275
+ * @example
1276
+ * kernel.dispose();
1277
+ */
1278
+ dispose(): void;
1279
+ }
1280
+ /** Internal mutable state for the spa plugin (all kernel data lives here). */
1281
+ interface SpaState {
1282
+ /** Components registered by name (last-registered-wins). */
1283
+ registeredComponents: Map<string, ComponentDef>;
1284
+ /** Live component instances keyed by their bound element. */
1285
+ instances: Map<Element, ComponentInstance>;
1286
+ /** The current resolved URL (pathname + search). */
1287
+ currentUrl: string;
1288
+ /** Teardown handle for the attached router listeners (null when detached). */
1289
+ destroyRouter: (() => void) | null;
1290
+ /** Whether the browser runtime has been booted. */
1291
+ started: boolean;
1292
+ /** The single shared SPA kernel instance (null until onInit builds it). */
1293
+ kernel: SpaKernel | null;
1294
+ }
1295
+ /** Public API of the spa plugin (registration / control surface). */
1296
+ type SpaApi = {
1297
+ /**
1298
+ * Register a component definition for client mounting.
1299
+ *
1300
+ * @param component - The component definition created via `createComponent`.
1301
+ * @returns void
1302
+ * @example
1303
+ * app.spa.register(counter);
1304
+ */
1305
+ register(component: ComponentDef): void;
1306
+ /**
1307
+ * Programmatically navigate to a path (client runtime; no-op without a DOM).
1308
+ *
1309
+ * @param path - Target path (pathname, optionally with search/hash).
1310
+ * @returns void
1311
+ * @example
1312
+ * app.spa.navigate("/about");
1313
+ */
1314
+ navigate(path: string): void;
1315
+ /**
1316
+ * Read the current resolved URL.
1317
+ *
1318
+ * @returns The current pathname + search.
1319
+ * @example
1320
+ * const url = app.spa.current(); // "/about"
1321
+ */
1322
+ current(): string;
1323
+ };
1324
+ declare namespace types_d_exports {
1325
+ 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 };
1326
+ }
1327
+ /**
1328
+ * Structural extraction of a plugin instance's public API from its `_phantom`
1329
+ * carrier (mirrors the kernel's non-exported `ExtractPluginApi`) so the
1330
+ * framework's generic `require` is assignable to {@link PhaseContext.require}.
1331
+ *
1332
+ * @example
1333
+ * ```ts
1334
+ * type ContentApi = ExtractApi<typeof contentPlugin>;
1335
+ * ```
1336
+ */
1337
+ type ExtractApi<PluginCandidate> = PluginCandidate extends {
1338
+ readonly _phantom: {
1339
+ readonly api: infer PluginApi;
1340
+ };
1341
+ } ? PluginApi : never;
1342
+ /**
1343
+ * Minimal logger slice used by the pipeline and phases (the core `log` API).
1344
+ *
1345
+ * @example
1346
+ * ```ts
1347
+ * const log: PhaseLog = { info: () => {}, debug: () => {}, warn: () => {}, error: () => {} };
1348
+ * ```
1349
+ */
1350
+ type PhaseLog = {
1351
+ /** Record an informational event. */info(event: string, data?: unknown): void; /** Record a debug event. */
1352
+ debug(event: string, data?: unknown): void; /** Record a warning event. */
1353
+ warn(event: string, data?: unknown): void; /** Record an error event. */
1354
+ error(event: string, data?: unknown): void;
1355
+ };
1356
+ /**
1357
+ * Payload map for the events `build` emits, used to type the `emit` closure
1358
+ * handed to the pipeline driver and phases.
1359
+ *
1360
+ * @example
1361
+ * ```ts
1362
+ * const emit: PhaseEmit = (name, payload) => kernel.emit(name, payload);
1363
+ * ```
1364
+ */
1365
+ type BuildEvents = {
1366
+ /** Phase boundary marker (start, then done with durationMs). */"build:phase": {
1367
+ phase: PhaseName;
1368
+ status: "start" | "done";
1369
+ durationMs?: number;
1370
+ }; /** One successful-run summary. */
1371
+ "build:complete": {
1372
+ outDir: string;
1373
+ pageCount: number;
1374
+ durationMs: number;
1375
+ };
1376
+ };
1377
+ /** Strictly-typed emit closure for the build events (kernel overload form). */
1378
+ type PhaseEmit = EmitFn<BuildEvents>;
1379
+ /** Generic `require` closure for pulling dependency plugin APIs at run time. */
1380
+ type PhaseRequire = <PluginCandidate extends {
1381
+ readonly name: string;
1382
+ readonly spec: unknown;
1383
+ readonly _phantom: {
1384
+ readonly config: unknown;
1385
+ readonly state: unknown;
1386
+ readonly api: unknown;
1387
+ readonly events: Record<string, unknown>;
1388
+ };
1389
+ }>(plugin: PluginCandidate) => ExtractApi<PluginCandidate>;
1390
+ /**
1391
+ * The plugin-context slice the pipeline driver and every phase consume: the
1392
+ * mutable `state`, the resolved `config`/`global`, plus `require`/`emit`/`log`.
1393
+ * Typed to match the kernel's generic context so the framework execution
1394
+ * context is structurally assignable.
1395
+ *
1396
+ * @example
1397
+ * ```ts
1398
+ * const ctx: PhaseContext = { state, config, global, require, emit, log };
1399
+ * ```
1400
+ */
1401
+ type PhaseContext = {
1402
+ /** Mutable per-run build state (caches + runId). */state: State$2; /** Resolved, frozen build config. */
1403
+ readonly config: Readonly<Config$2>; /** Global framework config (mode, etc.). */
1404
+ readonly global: Readonly<{
1405
+ mode: "production" | "development";
1406
+ }>; /** Resolve a depended-upon plugin instance to its public API. */
1407
+ require: PhaseRequire; /** Whether a plugin is registered (by name) — used to detect the OPTIONAL `data` plugin. */
1408
+ has: (name: string) => boolean; /** Emit a build event (notification-only). */
1409
+ emit: PhaseEmit; /** Structured logger (core `log` API). */
1410
+ readonly log: PhaseLog;
1411
+ };
1412
+ /**
1413
+ * Injectable PNG renderer for the og-images phase. Defaults to the real
1414
+ * Satori → resvg pipeline; unit tests inject a fake to assert hash-cache skip
1415
+ * and the `p-limit` bound without rasterizing real images.
1416
+ *
1417
+ * @example
1418
+ * ```ts
1419
+ * const render: OgPngRenderer = async () => new Uint8Array();
1420
+ * ```
1421
+ */
1422
+ type OgPngRenderer = (input: RichOgInput) => Promise<Uint8Array>;
1423
+ /**
1424
+ * Rich input handed to a custom OG `render` hook for a single article card. Carries
1425
+ * the full article + site metadata so a consumer can compose any layout. Returned by
1426
+ * the framework, not authored by consumers directly.
1427
+ *
1428
+ * @example
1429
+ * ```ts
1430
+ * const input: RichOgInput = {
1431
+ * title: "Hello", description: "Intro", date: "2026-01-15", tags: ["a"],
1432
+ * locale: "en", siteName: "Blog", size: { width: 1200, height: 630 }
1433
+ * };
1434
+ * ```
1435
+ */
1436
+ interface RichOgInput {
1437
+ /** Article title rendered into the card. */
1438
+ title: string;
1439
+ /** Article description / summary. */
1440
+ description: string;
1441
+ /** Publication date (ISO string from frontmatter). */
1442
+ date: string;
1443
+ /** Article tags. */
1444
+ tags: string[];
1445
+ /** Optional author name. */
1446
+ author?: string;
1447
+ /** Active locale for the card. */
1448
+ locale: string;
1449
+ /** Site name (from the site plugin / config). */
1450
+ siteName: string;
1451
+ /** Output dimensions for the card. */
1452
+ size: {
1453
+ width: number;
1454
+ height: number;
1455
+ };
1456
+ }
1457
+ /**
1458
+ * A single custom OG font entry. Each `path` is read to a Buffer ONCE per build and
1459
+ * handed to Satori. `weight`/`style` default to `400`/`"normal"` when omitted.
1460
+ *
1461
+ * @example
1462
+ * ```ts
1463
+ * const font: OgFont = { name: "Inter", path: "./fonts/Inter.ttf", weight: 600 };
1464
+ * ```
1465
+ */
1466
+ interface OgFont {
1467
+ /** Font family name referenced by the rendered card. */
1468
+ name: string;
1469
+ /** Path to the .ttf/.otf/.woff file. */
1470
+ path: string;
1471
+ /** Numeric weight (defaults to 400). */
1472
+ weight?: number;
1473
+ /** Font style (defaults to "normal"). */
1474
+ style?: "normal" | "italic";
1475
+ }
1476
+ /**
1477
+ * Optional OG-image generation config. Omit the field (or set `false`) to disable.
1478
+ *
1479
+ * The optional `render` hook (`@jsxImportSource preact`) lets a consumer return a
1480
+ * Preact `VNode` for the card; the framework casts it to Satori's input at the single
1481
+ * render boundary. `fonts` supplies multiple named fonts loaded once per build.
1482
+ *
1483
+ * @example
1484
+ * ```ts
1485
+ * const og: OgImageConfig = { fontDir: "./fonts" };
1486
+ * ```
1487
+ */
1488
+ interface OgImageConfig {
1489
+ /** Directory containing at least one .ttf/.otf/.woff font. Validated in onInit (void — config check only). */
1490
+ fontDir: string;
1491
+ /** Optional path to a custom OG template module. Falls back to the built-in template. */
1492
+ template?: string;
1493
+ /** Output dimensions. Defaults to 1200x630. */
1494
+ size?: {
1495
+ width: number;
1496
+ height: number;
1497
+ };
1498
+ /** Custom card renderer; returns a Preact `VNode` from the {@link RichOgInput}. */
1499
+ render?(input: RichOgInput): import("preact").VNode;
1500
+ /** Explicit named fonts loaded once per build (overrides the first-file scan). */
1501
+ fonts?: OgFont[];
1502
+ }
1503
+ /**
1504
+ * Public configuration for the `build` plugin. Flags give opt-in granularity over
1505
+ * individual outputs without rewriting the pipeline.
1506
+ *
1507
+ * @example
1508
+ * ```ts
1509
+ * const config: Config = { outDir: "./dist", minify: true, feeds: true, sitemap: true, images: true, ogImage: false };
1510
+ * ```
1511
+ */
1512
+ type Config$2 = {
1513
+ /** Output directory for the built site. */outDir: string; /** Minify bundled CSS/JS. */
1514
+ minify: boolean; /** Generate RSS/Atom/JSON feeds. */
1515
+ feeds: boolean; /** Generate sitemap.xml + robots.txt. */
1516
+ sitemap: boolean; /** Optimize + copy content images. */
1517
+ images: boolean; /** OG-image generation. `false` (or omitted) disables it; an object enables and configures it. */
1518
+ ogImage: OgImageConfig | false; /** Auto-inject bundled `main.{css,js}` into rendered pages. Default `true`. */
1519
+ injectAssets?: boolean; /** Directory copied verbatim into `outDir` (skipped silently if absent). Default `"public"`. */
1520
+ publicDir?: string;
1521
+ /**
1522
+ * Emit `outDir/404.html`. `true` for the built-in default page, or
1523
+ * `{ route }` to supply the page's literal HTML body content (NOT a route
1524
+ * path/slug — the string is written into the 404 page verbatim). Default `false`.
1525
+ */
1526
+ notFound?: boolean | {
1527
+ route?: string;
1528
+ }; /** Emit per-path i18n bare-path redirect HTML pages. Default `false`. */
1529
+ localeRedirects?: boolean; /** Authoritative client bundle entry path (overrides the conventional scan). */
1530
+ clientEntry?: string; /** HTML shell template with `<!--moku:head-->`/`<!--moku:body-->`/`<!--moku:assets-->` placeholders. */
1531
+ template?: string;
1532
+ };
1533
+ /**
1534
+ * A typed asset-manifest entry for one bundled asset kind (CSS or JS): a map of the
1535
+ * original entry basename to its hashed on-disk output path. Replaces the untyped
1536
+ * `Map<string, unknown>` reads when emitting `<link>`/`<script>` tags in pages.tsx.
1537
+ *
1538
+ * @example
1539
+ * ```ts
1540
+ * const entry: BuildCacheEntry = { "main.css": "assets/main-abc123.css" };
1541
+ * ```
1542
+ */
1543
+ type BuildCacheEntry = Record<string, string>;
1544
+ /**
1545
+ * Per-run closure state for the `build` plugin. Holds caches and config only —
1546
+ * no domain data is duplicated here (pulled fresh via `ctx.require` each run).
1547
+ *
1548
+ * @example
1549
+ * ```ts
1550
+ * const state: State = { config, manifest: null, buildCache: new Map(), runId: null, ogImageHashCache: new Map() };
1551
+ * ```
1552
+ */
1553
+ interface State$2 {
1554
+ /** Resolved, frozen config snapshot. */
1555
+ config: Config$2;
1556
+ /** Cached route manifest for the current run (populated in Phase 3 from router). */
1557
+ manifest: RouteDefinition[] | null;
1558
+ /** Per-run build artifacts (e.g. hashed CSS/JS asset paths from Phase 1). */
1559
+ buildCache: Map<string, unknown>;
1560
+ /** Unique id for the current run (timestamp/uuid) — injected as build-id meta. */
1561
+ runId: string | null;
1562
+ /**
1563
+ * Content-hash cache for OG images: slug -> sha256(title + template + size).
1564
+ * Loaded from `<outDir>/.cache/og-images.json` at the OG phase and written back,
1565
+ * so unchanged articles are skipped on the next run.
1566
+ */
1567
+ ogImageHashCache: Map<string, string>;
1568
+ }
1569
+ /**
1570
+ * Ordered names of the build pipeline phases, in execution order.
1571
+ *
1572
+ * @example
1573
+ * ```ts
1574
+ * const phase: PhaseName = "bundle";
1575
+ * ```
1576
+ */
1577
+ type PhaseName = "bundle" | "content" | "images" | "pages" | "feeds" | "sitemap" | "og-images" | "public" | "not-found" | "locale-redirects" | "root-index";
1578
+ /**
1579
+ * Result of a completed build run.
1580
+ *
1581
+ * @example
1582
+ * ```ts
1583
+ * const result: BuildResult = { outDir: "./dist", pageCount: 12, durationMs: 840 };
1584
+ * ```
1585
+ */
1586
+ interface BuildResult {
1587
+ /** Resolved output directory the site was written to. */
1588
+ outDir: string;
1589
+ /** Number of route pages rendered. */
1590
+ pageCount: number;
1591
+ /** Total wall-clock duration of the run, in milliseconds. */
1592
+ durationMs: number;
1593
+ }
1594
+ /**
1595
+ * Public API surface mounted on `app.build`.
1596
+ *
1597
+ * @example
1598
+ * ```ts
1599
+ * const result = await app.build.run();
1600
+ * ```
1601
+ */
1602
+ type Api$2 = {
1603
+ /**
1604
+ * Run the full SSG pipeline and write the site to disk.
1605
+ *
1606
+ * @param options - Optional run overrides.
1607
+ * @param options.outDir - Override the configured output directory for this run.
1608
+ * @returns The build result (outDir, pageCount, durationMs).
1609
+ * @example
1610
+ * ```ts
1611
+ * const result = await app.build.run({ outDir: "./preview" });
1612
+ * ```
1613
+ */
1614
+ run(options?: {
1615
+ outDir?: string;
1616
+ }): Promise<BuildResult>;
1617
+ /**
1618
+ * List the phases in execution order (introspection / tooling).
1619
+ *
1620
+ * @returns The static ordered phase names.
1621
+ * @example
1622
+ * ```ts
1623
+ * const order = app.build.phases();
1624
+ * ```
1625
+ */
1626
+ phases(): PhaseName[];
1627
+ };
1628
+ //#endregion
1629
+ //#region src/plugins/build/index.d.ts
1630
+ /**
1631
+ * Build plugin — the static-site-generation orchestrator. Renders every route to
1632
+ * `outDir`, and optionally emits feeds, a sitemap, optimized images, and OG
1633
+ * images. Depends on site, i18n, content, router, and head; emits `build:phase`.
1634
+ *
1635
+ * @example Configure the production build
1636
+ * ```ts
1637
+ * const app = createApp({
1638
+ * pluginConfigs: {
1639
+ * build: {
1640
+ * outDir: "dist",
1641
+ * minify: true,
1642
+ * feeds: true,
1643
+ * sitemap: true,
1644
+ * images: true,
1645
+ * ogImage: false // or an object to enable + configure OG-image generation
1646
+ * }
1647
+ * }
1648
+ * });
1649
+ * ```
1650
+ */
1651
+ declare const buildPlugin: import("@moku-labs/core").PluginInstance<"build", Config$2, State$2, Api$2, {
1652
+ "build:phase": {
1653
+ phase: PhaseName;
1654
+ status: "start" | "done";
1655
+ durationMs?: number;
1656
+ };
1657
+ "build:complete": {
1658
+ outDir: string;
1659
+ pageCount: number;
1660
+ durationMs: number;
1661
+ };
1662
+ }> & Record<never, never>;
1663
+ declare namespace types_d_exports$1 {
1664
+ export { Api$1 as Api, Article, ArticleCard, ComputedFields, Config$1 as Config, ContentApiContext, ContentEvents, Frontmatter, State$1 as State };
1665
+ }
1666
+ /**
1667
+ * Configuration for the content plugin.
1668
+ *
1669
+ * @example
1670
+ * ```ts
1671
+ * { contentDir: "./src/content", trustedContent: false, shikiTheme: "github-dark" }
1672
+ * ```
1673
+ */
1674
+ type Config$1 = {
1675
+ /** Absolute or project-relative path to the content directory. Validated in onInit. */contentDir: string;
1676
+ /**
1677
+ * SECURITY GATE. When false (the default), rehype-sanitize runs as the final
1678
+ * pipeline step. Set true ONLY for fully author-controlled Markdown — true
1679
+ * disables sanitize and trusts all raw HTML.
1680
+ */
1681
+ trustedContent: boolean; /** Additional remark plugins, concatenated AFTER framework defaults. Defaults to []. */
1682
+ extraRemarkPlugins?: readonly Pluggable[]; /** Additional rehype plugins, concatenated after custom transforms, before Shiki + sanitize. Defaults to []. */
1683
+ extraRehypePlugins?: readonly Pluggable[]; /** Shiki theme name for syntax highlighting. Defaults to "github-dark". */
1684
+ shikiTheme?: string; /** Author applied to articles whose frontmatter omits author. Defaults to undefined. */
1685
+ defaultAuthor?: string;
1686
+ };
1687
+ /**
1688
+ * Internal mutable state for the content plugin.
1689
+ *
1690
+ * @example
1691
+ * ```ts
1692
+ * { processor: null, articles: new Map(), slugs: null, dirtyPaths: new Set() }
1693
+ * ```
1694
+ */
1695
+ type State$1 = {
1696
+ /** Lazily-created unified processor singleton. null until first render()/loadAll(). */processor: Processor | null; /** Article cache keyed locale -> (slug -> Article). Starts empty. */
1697
+ articles: Map<string, Map<string, Article>>; /** Discovered, sorted slug list cached after first disk scan. null until first discovery. */
1698
+ slugs: string[] | null; /** Paths marked stale by invalidate(); next loadAll() re-reads only these. Starts empty. */
1699
+ dirtyPaths: Set<string>;
1700
+ };
1701
+ /**
1702
+ * YAML frontmatter parsed from each article file.
1703
+ *
1704
+ * @example
1705
+ * ```ts
1706
+ * { title: "Hello", date: "2026-01-15", description: "Intro", tags: [], language: "en" }
1707
+ * ```
1708
+ */
1709
+ type Frontmatter = {
1710
+ /** Article title. Required. */title: string; /** ISO 8601 date string, e.g. "2026-01-15". Required. */
1711
+ date: string; /** Short summary used in cards, feeds, and meta description. Required. */
1712
+ description: string; /** Topic tags. Required (may be empty array). */
1713
+ tags: string[]; /** Source language code of this file. Required. */
1714
+ language: string; /** Draft flag. Excluded from output in production mode. Defaults to false. */
1715
+ draft?: boolean; /** Author name. Falls back to config.defaultAuthor when omitted. */
1716
+ author?: string;
1717
+ };
1718
+ /**
1719
+ * Fields computed by the pipeline (not authored in frontmatter).
1720
+ *
1721
+ * @example
1722
+ * ```ts
1723
+ * { slug: "hello", readingTime: 1, contentId: "hello", status: "published", wordCount: 42 }
1724
+ * ```
1725
+ */
1726
+ type ComputedFields = {
1727
+ /** Article directory name. */slug: string; /** Reading time in minutes (ceiling, minimum 1). */
1728
+ readingTime: number; /** Stable content identifier (slug by default). */
1729
+ contentId: string; /** Derived publication status. */
1730
+ status: "published" | "draft"; /** Word count from the source body. */
1731
+ wordCount: number;
1732
+ };
1733
+ /**
1734
+ * A fully processed, render-ready article.
1735
+ *
1736
+ * @example
1737
+ * ```ts
1738
+ * { frontmatter, computed, html: "<p>…</p>", locale: "en", isFallback: false, url: "/en/hello/" }
1739
+ * ```
1740
+ */
1741
+ type Article = {
1742
+ /** Parsed frontmatter. */frontmatter: Frontmatter; /** Pipeline-computed metadata. */
1743
+ computed: ComputedFields; /** Sanitized rendered HTML body. */
1744
+ html: string; /** Locale this Article instance represents (the requested locale, even on fallback). */
1745
+ locale: string; /** True when the default-locale file was used because the requested locale was missing. */
1746
+ isFallback: boolean; /** Canonical URL for this article in this locale. */
1747
+ url: string;
1748
+ };
1749
+ /**
1750
+ * Lightweight projection of Article for cards/lists.
1751
+ *
1752
+ * @example
1753
+ * ```ts
1754
+ * { contentId: "hello", status: "published", title: "Hello", date: "2026-01-15", description: "Intro", tags: [], readingTime: 1, url: "/en/hello/" }
1755
+ * ```
1756
+ */
1757
+ type ArticleCard = {
1758
+ /** Stable content identifier. */contentId: string; /** Derived publication status. */
1759
+ status: "published" | "draft"; /** Article title. */
1760
+ title: string; /** ISO 8601 date string. */
1761
+ date: string; /** Short summary. */
1762
+ description: string; /** Topic tags. */
1763
+ tags: string[]; /** Reading time in minutes. */
1764
+ readingTime: number; /** Canonical URL for this article in this locale. */
1765
+ url: string;
1766
+ };
1767
+ /**
1768
+ * Notification-only events emitted by the content plugin.
1769
+ *
1770
+ * @example
1771
+ * ```ts
1772
+ * emit("content:ready", { locales: ["en"], articleCount: 3 });
1773
+ * ```
1774
+ */
1775
+ type ContentEvents = {
1776
+ /** All articles loaded across locales. */"content:ready": {
1777
+ locales: readonly string[];
1778
+ articleCount: number;
1779
+ }; /** Article paths marked stale for dev rebuild. */
1780
+ "content:invalidated": {
1781
+ paths: readonly string[];
1782
+ };
1783
+ };
1784
+ /**
1785
+ * Kernel-free domain context handed to createContentApi by the wiring harness.
1786
+ * Carries ctx.state (mutable escape hatch), config, global, emit, and the
1787
+ * i18n-derived locale/url helpers — so api.ts stays free of createPlugin/ctx.
1788
+ *
1789
+ * @example
1790
+ * ```ts
1791
+ * const apiContext: ContentApiContext = { state, config, global, emit, locales, defaultLocale, articleToUrl };
1792
+ * ```
1793
+ */
1794
+ type ContentApiContext = {
1795
+ /** Mutable plugin state (article cache + lazy processor). */state: State$1; /** Resolved plugin configuration. */
1796
+ config: Config$1; /** Global framework configuration (mode, etc.). */
1797
+ global: {
1798
+ mode: "production" | "development";
1799
+ }; /** Emit a registered content event. */
1800
+ emit: <K extends keyof ContentEvents>(event: K, payload: ContentEvents[K]) => void; /** Active locale codes from i18n. */
1801
+ locales: () => readonly string[]; /** Default locale code from i18n (fallback source). */
1802
+ defaultLocale: () => string; /** Build a canonical article URL for a locale + slug. */
1803
+ articleToUrl: (locale: string, slug: string) => string;
1804
+ };
1805
+ /**
1806
+ * Public API for the content plugin.
1807
+ *
1808
+ * @example
1809
+ * ```ts
1810
+ * const map = await app.content.loadAll();
1811
+ * ```
1812
+ */
1813
+ type Api$1 = {
1814
+ /**
1815
+ * Load every article across every active locale, returning a locale-keyed
1816
+ * map of date-descending Article arrays. Emits content:ready.
1817
+ */
1818
+ loadAll(): Promise<Map<string, Article[]>>;
1819
+ /**
1820
+ * Resolve and render a single article for one locale, with locale fallback.
1821
+ *
1822
+ * @param slug - Article directory name.
1823
+ * @param locale - Requested locale code.
1824
+ */
1825
+ load(slug: string, locale: string): Promise<Article>;
1826
+ /**
1827
+ * Render a raw Markdown string to HTML through the full pipeline.
1828
+ *
1829
+ * @param md - Raw Markdown source.
1830
+ */
1831
+ renderMarkdown(md: string): Promise<string>;
1832
+ /**
1833
+ * Mark file paths stale for incremental dev rebuilds. Emits content:invalidated.
1834
+ *
1835
+ * @param paths - File paths to invalidate.
1836
+ */
1837
+ invalidate(paths: readonly string[]): void;
1838
+ /**
1839
+ * Project a full Article to a lightweight ArticleCard for list/grid rendering.
1544
1840
  *
1545
- * @param path - The target path to navigate to.
1546
- * @returns void
1547
- * @example
1548
- * kernel.processNav("/about");
1841
+ * @param article - The source article.
1549
1842
  */
1550
- processNav(path: string): void;
1843
+ articleToCard(article: Article): ArticleCard;
1844
+ };
1845
+ //#endregion
1846
+ //#region src/plugins/content/index.d.ts
1847
+ /**
1848
+ * Content plugin — Markdown pipeline: discovers files, parses frontmatter, renders
1849
+ * to sanitized HTML (rehype-sanitize unless `trustedContent`), and exposes a
1850
+ * locale-keyed Article model. Depends on i18n; emits `content:ready` and
1851
+ * `content:invalidated`.
1852
+ *
1853
+ * @example Point at a content directory and pick a Shiki theme
1854
+ * ```ts
1855
+ * const app = createApp({
1856
+ * pluginConfigs: {
1857
+ * content: {
1858
+ * contentDir: "./content",
1859
+ * shikiTheme: "github-dark",
1860
+ * defaultAuthor: "Ada Lovelace"
1861
+ * // trustedContent: true // ONLY for fully author-controlled Markdown — disables sanitize
1862
+ * }
1863
+ * }
1864
+ * });
1865
+ * ```
1866
+ */
1867
+ declare const contentPlugin: import("@moku-labs/core").PluginInstance<"content", Config$1, State$1, Api$1, {
1868
+ "content:ready": {
1869
+ locales: readonly string[];
1870
+ articleCount: number;
1871
+ };
1872
+ "content:invalidated": {
1873
+ paths: readonly string[];
1874
+ };
1875
+ }> & Record<never, never>;
1876
+ declare namespace types_d_exports$2 {
1877
+ export { DataConfig, DataEntry, DataProvider, DataState, DataWriteSummary };
1878
+ }
1879
+ /**
1880
+ * @file data plugin — type definitions (Standard tier).
1881
+ *
1882
+ * The `data` plugin is the **agnostic data provider** for the SSG→DATA→SPA pattern.
1883
+ * It owns ONE thing: the contract `page path → persisted JSON file`. It knows
1884
+ * NOTHING about what the data *is* — no domain types appear here. A route decides
1885
+ * its own data shape (`load`'s return) and its own validation (`route.parse`).
1886
+ *
1887
+ * - **Node (build):** `write(entries)` persists one JSON file per page, keyed by
1888
+ * the page's URL via {@link DataProvider.fileFor}. `build` supplies the entries
1889
+ * (it already expanded the routes), so there is no duplicate expansion here.
1890
+ * - **Browser (runtime):** `at(path)` fetches + caches that file as `unknown`; the
1891
+ * route's `parse` validates it into the route's data type before `render`.
1892
+ *
1893
+ * The Node-only file-writing code (`node:fs`) is isolated behind a lazy `import()`
1894
+ * inside `write()`, so composing `data` in a browser app keeps the bundle free of
1895
+ * `node:*`.
1896
+ */
1897
+ /**
1898
+ * Configuration for {@link dataPlugin}. All fields have defaults (see `./config`).
1899
+ *
1900
+ * @example
1901
+ * ```ts
1902
+ * const cfg: DataConfig = { outputDir: "_data", baseUrl: "/_data/" };
1903
+ * ```
1904
+ */
1905
+ type DataConfig = {
1551
1906
  /**
1552
- * Query the swap region and mount components for matching elements.
1553
- *
1554
- * @returns void
1555
- * @example
1556
- * kernel.scan();
1907
+ * WRITE side (Node): output subdir relative to the build `outDir`, a filesystem
1908
+ * path where `write()` persists the per-page JSON. Default `"_data"`.
1557
1909
  */
1558
- scan(): void;
1910
+ outputDir: string;
1559
1911
  /**
1560
- * Tear down router listeners, run unmount/destroy, clear instances.
1561
- *
1562
- * @returns void
1563
- * @example
1564
- * kernel.dispose();
1912
+ * READ side (browser): site-root-relative URL the client fetches the per-page
1913
+ * JSON from. A different domain from {@link DataConfig.outputDir} (a filesystem
1914
+ * path); keep consistent (`"/" + trim(outputDir) + "/"`). Default `"/_data/"`.
1565
1915
  */
1566
- dispose(): void;
1916
+ baseUrl: string;
1917
+ };
1918
+ /** One page's data to persist — `build` produces these from its route expansion. */
1919
+ interface DataEntry {
1920
+ /** The page's URL path (e.g. `/en/hello/`); maps to a file via {@link DataProvider.fileFor}. */
1921
+ path: string;
1922
+ /** The serializable data for this page (the route's `load`/projection output). */
1923
+ data: unknown;
1567
1924
  }
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;
1925
+ /** Summary returned by {@link DataProvider.write} and cached in state. */
1926
+ interface DataWriteSummary {
1927
+ /** Number of per-page JSON files written. */
1928
+ fileCount: number;
1929
+ /** Total bytes written across all files. */
1930
+ bytes: number;
1931
+ /** The written file paths, relative to the build `outDir`. */
1932
+ files: string[];
1582
1933
  }
1583
- /** Public API of the spa plugin (registration / control surface). */
1584
- type SpaApi = {
1934
+ /**
1935
+ * Internal data state. `lastWrite` records the most recent `write()` (Node);
1936
+ * `cache` memoizes fetched per-path data (browser, lazy). Both empty until their
1937
+ * respective side first runs.
1938
+ */
1939
+ interface DataState {
1940
+ /** Result of the last `write()`, or `null` if it has not run yet (Node). */
1941
+ lastWrite: DataWriteSummary | null;
1942
+ /** Per-path fetched data, cached after the first `at(path)` (browser). */
1943
+ cache: Map<string, unknown>;
1944
+ }
1945
+ /**
1946
+ * Public API mounted at `app.data` — the agnostic data provider. `write()` is the
1947
+ * Node persist side; `at()` is the browser read side; `urlFor`/`fileFor` are the
1948
+ * pure URL convention shared by both so the written file and fetched URL can never
1949
+ * drift.
1950
+ *
1951
+ * @example
1952
+ * ```ts
1953
+ * // Node build (build supplies the entries it already expanded):
1954
+ * await app.data.write([{ path: "/en/hello/", data: article }]);
1955
+ *
1956
+ * // Browser (inside spa nav): fetch the page's data, then route.parse validates it:
1957
+ * const raw = await app.data.at("/en/hello/"); // unknown | null
1958
+ * ```
1959
+ */
1960
+ type DataProvider = {
1585
1961
  /**
1586
- * Register a component definition for client mounting.
1962
+ * READ (browser) fetch (and cache) the persisted data for a page path from
1963
+ * `config.baseUrl`. Returns the raw parsed JSON as `unknown` (the caller's
1964
+ * `route.parse` validates it), or `null` if the fetch/parse fails.
1587
1965
  *
1588
- * @param component - The component definition created via `createComponent`.
1589
- * @returns void
1590
- * @example
1591
- * app.spa.register(counter);
1966
+ * @param path - The page URL path (e.g. `/en/hello/`).
1967
+ * @returns The page's raw data, or `null` on failure.
1592
1968
  */
1593
- register(component: ComponentDef): void;
1969
+ at(path: string): Promise<unknown | null>;
1594
1970
  /**
1595
- * Programmatically navigate to a path (client runtime; no-op without a DOM).
1971
+ * WRITE (Node) persist one JSON file per entry, keyed by page path via
1972
+ * {@link DataProvider.fileFor}. Called by `build` after it expands routes (no
1973
+ * duplicate expansion). Lazily loads its `node:fs` writer, so it never
1974
+ * contaminates a browser bundle.
1596
1975
  *
1597
- * @param path - Target path (pathname, optionally with search/hash).
1598
- * @returns void
1599
- * @example
1600
- * app.spa.navigate("/about");
1976
+ * @param entries - The per-page data to persist.
1977
+ * @param options - Optional overrides.
1978
+ * @param options.outDir - Build output directory to write under (default `./dist`).
1979
+ * @returns A summary of the written files.
1601
1980
  */
1602
- navigate(path: string): void;
1981
+ write(entries: readonly DataEntry[], options?: {
1982
+ outDir?: string;
1983
+ }): Promise<DataWriteSummary>;
1603
1984
  /**
1604
- * Read the current resolved URL.
1985
+ * PURE the browser fetch URL for a page path (e.g. `/en/hello/` →
1986
+ * `/_data/en/hello/index.json`). Shared with {@link DataProvider.fileFor}.
1605
1987
  *
1606
- * @returns The current pathname + search.
1607
- * @example
1608
- * const url = app.spa.current(); // "/about"
1988
+ * @param path - The page URL path.
1989
+ * @returns The site-root-relative data URL.
1609
1990
  */
1610
- current(): string;
1991
+ urlFor(path: string): string;
1992
+ /**
1993
+ * PURE — the `outDir`-relative file path for a page path (e.g. `/en/hello/` →
1994
+ * `_data/en/hello/index.json`). Shared with {@link DataProvider.urlFor}.
1995
+ *
1996
+ * @param path - The page URL path.
1997
+ * @returns The output-relative file path.
1998
+ */
1999
+ fileFor(path: string): string;
1611
2000
  };
1612
- declare namespace types_d_exports$2 {
2001
+ //#endregion
2002
+ //#region src/plugins/data/index.d.ts
2003
+ /**
2004
+ * Data plugin — the agnostic data provider. Mounts `write(entries)` (Node persist),
2005
+ * `at(path)` (browser read), and the pure `urlFor`/`fileFor` convention at `app.data`.
2006
+ *
2007
+ * @example
2008
+ * ```ts
2009
+ * // Node build: `build` calls app.data.write(...) during its pages phase when
2010
+ * // router.mode !== "ssg". Just compose the plugin:
2011
+ * const app = createApp({
2012
+ * plugins: [dataPlugin, contentPlugin, buildPlugin],
2013
+ * pluginConfigs: { content: { contentDir: "./content" }, router: { routes, mode: "hybrid" } }
2014
+ * });
2015
+ * await app.start();
2016
+ * await app.build.run(); // writes HTML + per-page data sidecars
2017
+ *
2018
+ * // Browser app: compose `dataPlugin` too; spa fetches via app.data.at(path) on nav.
2019
+ * ```
2020
+ */
2021
+ declare const dataPlugin: import("@moku-labs/core").PluginInstance<"data", DataConfig, DataState, DataProvider, {}> & Record<never, never>;
2022
+ declare namespace types_d_exports$3 {
1613
2023
  export { Api, Config, DeployErrorCode, DeployInitOptions, DeployResult, DeployRunOptions, InitResult, SpawnFunction, SpawnOptions, SpawnedProcess, State, WranglerErrorKind };
1614
2024
  }
1615
2025
  /**
@@ -1824,72 +2234,6 @@ declare const deployPlugin: import("@moku-labs/core").PluginInstance<"deploy", C
1824
2234
  };
1825
2235
  }> & Record<never, never>;
1826
2236
  //#endregion
1827
- //#region src/plugins/build/index.d.ts
1828
- /**
1829
- * Build plugin — the static-site-generation orchestrator. Renders every route to
1830
- * `outDir`, and optionally emits feeds, a sitemap, optimized images, and OG
1831
- * images. Depends on site, i18n, content, router, and head; emits `build:phase`.
1832
- *
1833
- * @example Configure the production build
1834
- * ```ts
1835
- * const app = createApp({
1836
- * pluginConfigs: {
1837
- * build: {
1838
- * outDir: "dist",
1839
- * minify: true,
1840
- * feeds: true,
1841
- * sitemap: true,
1842
- * images: true,
1843
- * ogImage: false // or an object to enable + configure OG-image generation
1844
- * }
1845
- * }
1846
- * });
1847
- * ```
1848
- */
1849
- declare const buildPlugin: import("@moku-labs/core").PluginInstance<"build", Config$1, State$1, Api$1, {
1850
- "build:phase": {
1851
- phase: PhaseName;
1852
- status: "start" | "done";
1853
- durationMs?: number;
1854
- };
1855
- "build:complete": {
1856
- outDir: string;
1857
- pageCount: number;
1858
- durationMs: number;
1859
- };
1860
- }> & Record<never, never>;
1861
- //#endregion
1862
- //#region src/plugins/content/index.d.ts
1863
- /**
1864
- * Content plugin — Markdown pipeline: discovers files, parses frontmatter, renders
1865
- * to sanitized HTML (rehype-sanitize unless `trustedContent`), and exposes a
1866
- * locale-keyed Article model. Depends on i18n; emits `content:ready` and
1867
- * `content:invalidated`.
1868
- *
1869
- * @example Point at a content directory and pick a Shiki theme
1870
- * ```ts
1871
- * const app = createApp({
1872
- * pluginConfigs: {
1873
- * content: {
1874
- * contentDir: "./content",
1875
- * shikiTheme: "github-dark",
1876
- * defaultAuthor: "Ada Lovelace"
1877
- * // trustedContent: true // ONLY for fully author-controlled Markdown — disables sanitize
1878
- * }
1879
- * }
1880
- * });
1881
- * ```
1882
- */
1883
- declare const contentPlugin: import("@moku-labs/core").PluginInstance<"content", Config$3, State$3, Api$3, {
1884
- "content:ready": {
1885
- locales: readonly string[];
1886
- articleCount: number;
1887
- };
1888
- "content:invalidated": {
1889
- paths: readonly string[];
1890
- };
1891
- }> & Record<never, never>;
1892
- //#endregion
1893
2237
  //#region src/plugins/head/primitives.d.ts
1894
2238
  /**
1895
2239
  * Build a `<meta name=… content=…>` descriptor.
@@ -1988,7 +2332,7 @@ declare function buildArticleHead(articleMeta: ArticleMeta, canonicalUrl: string
1988
2332
  * });
1989
2333
  * ```
1990
2334
  */
1991
- declare const headPlugin: import("@moku-labs/core").PluginInstance<"head", Config$2, State$2, Api$2, {}> & {
2335
+ declare const headPlugin: import("@moku-labs/core").PluginInstance<"head", Config$3, State$3, Api$3, {}> & {
1992
2336
  meta: typeof meta;
1993
2337
  og: typeof og;
1994
2338
  twitter: typeof twitter;
@@ -2150,42 +2494,82 @@ declare const spaPlugin: import("@moku-labs/core").PluginInstance<"spa", SpaConf
2150
2494
  };
2151
2495
  }> & Record<never, never>;
2152
2496
  //#endregion
2497
+ //#region src/plugins/env/providers.d.ts
2498
+ /**
2499
+ * A zero-dependency `.env`-style provider that re-reads and re-parses the file
2500
+ * from disk on every `load()`. Missing file resolves to `{}` (optional
2501
+ * overrides). Strips a single outer quote pair; does not strip trailing inline
2502
+ * comments on unquoted values.
2503
+ *
2504
+ * @param path - Path to the dotenv file. Defaults to `.env.local`.
2505
+ * @returns An {@link EnvProvider} named `dotenv:<path>` that reads fresh per call.
2506
+ * @example
2507
+ * ```ts
2508
+ * const provider = dotenv(".env.local");
2509
+ * provider.load(); // { PUBLIC_API_URL: "/api", ... }
2510
+ * ```
2511
+ */
2512
+ declare function dotenv(path?: string): EnvProvider;
2513
+ /**
2514
+ * A provider that returns a shallow copy of `process.env` at `load()` time.
2515
+ *
2516
+ * @returns An {@link EnvProvider} named `process-env`.
2517
+ * @example
2518
+ * ```ts
2519
+ * const provider = processEnv();
2520
+ * provider.load().HOME; // current process value
2521
+ * ```
2522
+ */
2523
+ declare function processEnv(): EnvProvider;
2524
+ /**
2525
+ * A provider that reads live, per-request Cloudflare bindings from
2526
+ * `globalThis.__CLOUDFLARE_ENV__` at `load()` time (`?? {}` when absent). Never
2527
+ * caches the binding object; the consumer owns the global's request lifecycle.
2528
+ *
2529
+ * @returns An {@link EnvProvider} named `cloudflare`.
2530
+ * @example
2531
+ * ```ts
2532
+ * globalThis.__CLOUDFLARE_ENV__ = env; // set by the request handler
2533
+ * const provider = cloudflareBindings();
2534
+ * provider.load(); // reads the current request's bindings
2535
+ * ```
2536
+ */
2537
+ declare function cloudflareBindings(): EnvProvider;
2538
+ //#endregion
2153
2539
  //#region src/index.d.ts
2154
2540
  /**
2155
2541
  * Create and initialize a `@moku-labs/web` application — the Layer-3 entry point.
2156
2542
  * Your overrides are merged over the framework defaults through the 4-level config
2157
2543
  * cascade, every plugin's lifecycle runs, and a fully-typed, frozen app is returned.
2158
2544
  *
2545
+ * The defaults are the isomorphic plugin set (`site`, `i18n`, `router`, `head`,
2546
+ * `spa` + `log`/`env` core). Add the Node-only plugins for an SSG build:
2547
+ * `createApp({ plugins: [contentPlugin, buildPlugin, deployPlugin] })`.
2548
+ *
2159
2549
  * @param options - Optional configuration:
2160
- * - `pluginConfigs` — per-plugin overrides, keyed by plugin name
2161
- * (`site`, `i18n`, `router`, `content`, `head`, `build`, `spa`, `deploy`, `env`).
2550
+ * - `pluginConfigs` — per-plugin overrides, keyed by plugin name.
2162
2551
  * - `config` — global framework config (e.g. `{ mode: "development" }`).
2163
- * - `plugins` — extra consumer plugins, merged into the app and its return type.
2552
+ * - `plugins` — extra plugins (Node-only built-ins or your own) merged into the app and its type.
2164
2553
  * - `onReady` / `onError` / `onStart` / `onStop` — lifecycle callbacks.
2165
2554
  * @returns The initialized app: `start()`, `stop()`, every plugin's API, and `log`.
2166
2555
  * @example
2167
2556
  * ```ts
2557
+ * // Node SSG build — add the node-only plugins:
2168
2558
  * const app = createApp({
2559
+ * plugins: [contentPlugin, buildPlugin, deployPlugin],
2169
2560
  * pluginConfigs: {
2170
2561
  * site: { name: "My Blog", url: "https://blog.dev", author: "Ada", description: "Notes" },
2171
2562
  * router: { routes: defineRoutes({ home: route("/"), post: route("/blog/{slug}/") }) }
2172
2563
  * }
2173
2564
  * });
2174
2565
  * await app.start();
2566
+ * await app.build.run();
2175
2567
  * ```
2176
2568
  */
2177
2569
  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, {}> & {
2178
2570
  route: typeof route;
2179
2571
  defineRoutes: typeof defineRoutes;
2180
- }) | (import("@moku-labs/core").PluginInstance<"content", Config$3, State$3, Api$3, {
2181
- "content:ready": {
2182
- locales: readonly string[];
2183
- articleCount: number;
2184
- };
2185
- "content:invalidated": {
2186
- paths: readonly string[];
2187
- };
2188
- }> & Record<never, never>) | (import("@moku-labs/core").PluginInstance<"head", Config$2, State$2, Api$2, {}> & {
2572
+ }) | (import("@moku-labs/core").PluginInstance<"head", Config$3, State$3, Api$3, {}> & {
2189
2573
  meta: typeof meta;
2190
2574
  og: typeof og;
2191
2575
  twitter: typeof twitter;
@@ -2194,18 +2578,7 @@ declare const createApp: <const ExtraPlugins extends readonly import("@moku-labs
2194
2578
  hreflang: typeof hreflang;
2195
2579
  feedLink: typeof feedLink;
2196
2580
  buildArticleHead: typeof buildArticleHead;
2197
- }) | (import("@moku-labs/core").PluginInstance<"build", Config$1, State$1, Api$1, {
2198
- "build:phase": {
2199
- phase: PhaseName;
2200
- status: "start" | "done";
2201
- durationMs?: number;
2202
- };
2203
- "build:complete": {
2204
- outDir: string;
2205
- pageCount: number;
2206
- durationMs: number;
2207
- };
2208
- }> & Record<never, never>) | (import("@moku-labs/core").PluginInstance<"spa", SpaConfig, SpaState, SpaApi, {
2581
+ }) | (import("@moku-labs/core").PluginInstance<"spa", SpaConfig, SpaState, SpaApi, {
2209
2582
  "spa:navigate": {
2210
2583
  from: string;
2211
2584
  to: string;
@@ -2221,25 +2594,10 @@ declare const createApp: <const ExtraPlugins extends readonly import("@moku-labs
2221
2594
  name: string;
2222
2595
  el: Element;
2223
2596
  };
2224
- }> & Record<never, never>) | (import("@moku-labs/core").PluginInstance<"deploy", Config, State, Api, {
2225
- "deploy:complete": {
2226
- url: string;
2227
- deploymentId: string;
2228
- branch: string;
2229
- durationMs: number;
2230
- };
2231
2597
  }> & 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, {}> & {
2232
2598
  route: typeof route;
2233
2599
  defineRoutes: typeof defineRoutes;
2234
- }) | (import("@moku-labs/core").PluginInstance<"content", Config$3, State$3, Api$3, {
2235
- "content:ready": {
2236
- locales: readonly string[];
2237
- articleCount: number;
2238
- };
2239
- "content:invalidated": {
2240
- paths: readonly string[];
2241
- };
2242
- }> & Record<never, never>) | (import("@moku-labs/core").PluginInstance<"head", Config$2, State$2, Api$2, {}> & {
2600
+ }) | (import("@moku-labs/core").PluginInstance<"head", Config$3, State$3, Api$3, {}> & {
2243
2601
  meta: typeof meta;
2244
2602
  og: typeof og;
2245
2603
  twitter: typeof twitter;
@@ -2248,18 +2606,7 @@ declare const createApp: <const ExtraPlugins extends readonly import("@moku-labs
2248
2606
  hreflang: typeof hreflang;
2249
2607
  feedLink: typeof feedLink;
2250
2608
  buildArticleHead: typeof buildArticleHead;
2251
- }) | (import("@moku-labs/core").PluginInstance<"build", Config$1, State$1, Api$1, {
2252
- "build:phase": {
2253
- phase: PhaseName;
2254
- status: "start" | "done";
2255
- durationMs?: number;
2256
- };
2257
- "build:complete": {
2258
- outDir: string;
2259
- pageCount: number;
2260
- durationMs: number;
2261
- };
2262
- }> & Record<never, never>) | (import("@moku-labs/core").PluginInstance<"spa", SpaConfig, SpaState, SpaApi, {
2609
+ }) | (import("@moku-labs/core").PluginInstance<"spa", SpaConfig, SpaState, SpaApi, {
2263
2610
  "spa:navigate": {
2264
2611
  from: string;
2265
2612
  to: string;
@@ -2275,13 +2622,6 @@ declare const createApp: <const ExtraPlugins extends readonly import("@moku-labs
2275
2622
  name: string;
2276
2623
  el: Element;
2277
2624
  };
2278
- }> & Record<never, never>) | (import("@moku-labs/core").PluginInstance<"deploy", Config, State, Api, {
2279
- "deploy:complete": {
2280
- url: string;
2281
- deploymentId: string;
2282
- branch: string;
2283
- durationMs: number;
2284
- };
2285
2625
  }> & 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>]>>;
2286
2626
  /**
2287
2627
  * Create a custom plugin bound to this framework's `Config`/`Events` and core
@@ -2300,4 +2640,4 @@ declare const createApp: <const ExtraPlugins extends readonly import("@moku-labs
2300
2640
  */
2301
2641
  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>]>>;
2302
2642
  //#endregion
2303
- 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 };
2643
+ 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 };