@moku-labs/web 0.5.6 → 1.0.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.mts CHANGED
@@ -1,10 +1,10 @@
1
1
  import { EmitFn } from "@moku-labs/core";
2
- import { Pluggable, Processor } from "unified";
3
2
  import { ComponentChildren, VNode } from "preact";
3
+ import { Pluggable, Processor } from "unified";
4
4
  import { BundledTheme, ThemeRegistrationAny } from "shiki";
5
5
 
6
6
  //#region src/plugins/log/types.d.ts
7
- declare namespace types_d_exports$6 {
7
+ declare namespace types_d_exports$7 {
8
8
  export { ExpectChain, LogApi, LogConfig, LogEntry, LogLevel, LogSink, LogState };
9
9
  }
10
10
  /**
@@ -144,7 +144,7 @@ type LogApi = {
144
144
  addSink(sink: LogSink): void; /** Clear all recorded entries while keeping registered sinks. */
145
145
  reset(): void;
146
146
  };
147
- declare namespace types_d_exports$4 {
147
+ declare namespace types_d_exports$5 {
148
148
  export { EnvApi, EnvConfig, EnvProvider, EnvState, EnvVarSpec };
149
149
  }
150
150
  /**
@@ -155,8 +155,9 @@ declare namespace types_d_exports$4 {
155
155
  *
156
156
  * Providers are walked in array order during resolution; the first provider to
157
157
  * return a non-`undefined` (and non-empty-string) value for a key wins. `load()`
158
- * is called once per resolution at `onInit` time except for live, per-request
159
- * sources (e.g. {@link cloudflareBindings}) which read fresh on every call.
158
+ * is called exactly once per resolution at `onInit` time, after which both env
159
+ * maps are frozen. A provider like {@link cloudflareBindings} reads `globalThis`
160
+ * at that single `onInit` call (not per request).
160
161
  *
161
162
  * @example
162
163
  * ```ts
@@ -227,7 +228,8 @@ type EnvConfig = {
227
228
  /**
228
229
  * Ordered list of value sources. The first provider yielding a non-`undefined`
229
230
  * (and non-empty-string) value for a key wins. The plugin's own spec default is
230
- * `[]`; the framework layer supplies the working default `[dotenv(), processEnv()]`.
231
+ * `[]`; the consumer supplies the providers per target (`[dotenv(), processEnv()]`
232
+ * on Node) — only the `/browser` entry pre-wires `browserEnv()` out of the box.
231
233
  */
232
234
  providers: EnvProvider[];
233
235
  /**
@@ -336,12 +338,25 @@ declare function browserEnv(options?: {
336
338
  declare const envPlugin: import("@moku-labs/core").CorePluginInstance<"env", EnvConfig, EnvState, EnvApi>;
337
339
  //#endregion
338
340
  //#region src/config.d.ts
341
+ /**
342
+ * Deployment stage. Drives content draft visibility — drafts are suppressed
343
+ * only in `"production"`; `"development"` and `"test"` both surface them.
344
+ */
345
+ type Stage = "production" | "development" | "test";
339
346
  /**
340
347
  * Global framework configuration. Minimal by design — per-plugin config is
341
348
  * resolved via `pluginConfigs`, not merged here.
342
349
  */
343
- type Config$7 = {
344
- /** Runtime mode. Drives log sink defaults, content draft filtering, build minify. */mode: "production" | "development";
350
+ type Config$8 = {
351
+ /** Deployment stage. Drives content draft visibility (drafts hidden only in `"production"`). */stage: Stage;
352
+ /**
353
+ * Render mode — the single SSG/DATA/SPA switch, read by the router (`ctx.global`)
354
+ * and consumed by `build`/`spa` via `router.mode()`.
355
+ * - `"ssg"` static generation only (no client router emitted).
356
+ * - `"spa"` client-side routing only.
357
+ * - `"hybrid"` static HTML + client navigation overlay (default).
358
+ */
359
+ mode: "ssg" | "spa" | "hybrid";
345
360
  };
346
361
  /**
347
362
  * Framework event contract. Empty base — each plugin declares its own events
@@ -375,7 +390,7 @@ type Events = {};
375
390
  * });
376
391
  * ```
377
392
  */
378
- type Config$6 = {
393
+ type Config$7 = {
379
394
  /** Human-readable site name. Used in feeds, og:site_name, and titles. MUST be non-empty. */name: string; /** Absolute base URL of the site, e.g. "https://blog.dev". MUST be a valid absolute URL (http/https). */
380
395
  url: string; /** Default author/byline for the site. Used in feeds and article author meta. */
381
396
  author: string; /** Short site description. Used in feeds, the default meta description, and og:description fallbacks. */
@@ -385,7 +400,7 @@ type Config$6 = {
385
400
  * Public API of the site plugin — read-only accessors over frozen global
386
401
  * site metadata, plus canonical URL construction.
387
402
  */
388
- type Api$6 = {
403
+ type Api$7 = {
389
404
  /**
390
405
  * Returns the configured site name.
391
406
  *
@@ -446,7 +461,7 @@ type Api$6 = {
446
461
  * @file i18n plugin — public type definitions (Config + Api).
447
462
  */
448
463
  /**
449
- * i18n plugin configuration. Mirrors the legacy `I18nConfig` shape.
464
+ * i18n plugin configuration.
450
465
  *
451
466
  * `locales` and `defaultLocale` are required and validated in `onInit`. The
452
467
  * optional maps default to empty objects so every lookup method is total —
@@ -463,7 +478,7 @@ type Api$6 = {
463
478
  * }
464
479
  * ```
465
480
  */
466
- type Config$5 = {
481
+ type Config$6 = {
467
482
  readonly locales: readonly string[];
468
483
  readonly defaultLocale: string;
469
484
  readonly localeNames?: Record<string, string>;
@@ -474,7 +489,7 @@ type Config$5 = {
474
489
  * Public API of the i18n plugin. Injected as `app.i18n` and reachable from
475
490
  * other plugins via `ctx.require(i18nPlugin)`.
476
491
  */
477
- type Api$5 = {
492
+ type Api$6 = {
478
493
  /**
479
494
  * Returns the configured supported locales in declared order.
480
495
  *
@@ -542,8 +557,8 @@ type Api$5 = {
542
557
  */
543
558
  t(locale: string, key: string): string;
544
559
  };
545
- declare namespace types_d_exports$7 {
546
- 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 };
560
+ declare namespace types_d_exports$8 {
561
+ export { Api$5 as Api, ClientRoute, CompileInput, CompiledRoute, Config$5 as Config, ExtractApi$2 as ExtractApi, ExtractRouteParams, ExtractSegmentParameter, GenerateContext, HeadConfig$1 as HeadConfig, LayoutContext, LoadContext, MatcherTable, Prettify, RouteBuilder, RouteContext, RouteDefinition, RouteHandlers, RouteMap, RouteRequire, RouteState, RouterApi, RouterConfig, RouterState, State$5 as State, TypedRoute, Urls };
547
562
  }
548
563
  /**
549
564
  * Param contribution of a single path segment. `{name:?}` / `:name?` → optional;
@@ -582,6 +597,84 @@ interface RouteContext<S extends RouteState> {
582
597
  readonly data: S["data"];
583
598
  /** Active locale for this render. */
584
599
  readonly locale: string;
600
+ /**
601
+ * Build a link to a named route by pattern substitution — the framework delivers
602
+ * this on the context (same output as `app.router.toUrl`), so render/head build
603
+ * links with no `app`/`createUrls` reference. Works identically at build and on
604
+ * client navigation.
605
+ */
606
+ readonly url: (name: string, params?: Record<string, string>) => string;
607
+ }
608
+ /**
609
+ * Structural extraction of a plugin instance's public API from its `_phantom`
610
+ * carrier (mirrors the kernel's `ExtractApi` / spec/09 §3). Lets the loader/generator
611
+ * `require` resolve a plugin instance to its typed public API.
612
+ *
613
+ * @example
614
+ * type ContentApi = ExtractApi<typeof contentPlugin>;
615
+ */
616
+ type ExtractApi$2<PluginCandidate> = PluginCandidate extends {
617
+ readonly _phantom: {
618
+ readonly api: infer PluginApi;
619
+ };
620
+ } ? PluginApi : never;
621
+ /**
622
+ * Generic, instance-only `require` handed to a route's `.load()` / `.generate()` —
623
+ * the SAME shape as the kernel's `RequireFunction` (spec/08 §7) and the build's
624
+ * `PhaseRequire`, so the build forwards its own `ctx.require` straight through.
625
+ * Resolves a plugin INSTANCE to its public API; the consumer supplies the instance
626
+ * (e.g. `ctx.require(contentPlugin)`), so the router never names a sibling plugin.
627
+ *
628
+ * @example
629
+ * const content = ctx.require(contentPlugin); // ContentApi
630
+ */
631
+ type RouteRequire = <PluginCandidate extends {
632
+ readonly name: string;
633
+ readonly spec: unknown;
634
+ readonly _phantom: {
635
+ readonly config: unknown;
636
+ readonly state: unknown;
637
+ readonly api: unknown;
638
+ readonly events: Record<string, unknown>;
639
+ };
640
+ }>(plugin: PluginCandidate) => ExtractApi$2<PluginCandidate>;
641
+ /**
642
+ * Build-time context handed to a route's `.load()`. Carries the resolved path
643
+ * `params` and active `locale`, plus the spec's `require`/`has` so a loader pulls
644
+ * sibling plugin APIs the canonical way — `ctx.require(contentPlugin)` — with no
645
+ * module global and no router→content coupling. Loaders run ONLY at build time
646
+ * (never on the client), inside the build plugin's context, so `require`/`has` are
647
+ * always live here.
648
+ *
649
+ * @example
650
+ * route("/{slug}/").load((ctx) => ctx.require(contentPlugin).load(ctx.params.slug, ctx.locale));
651
+ */
652
+ interface LoadContext<S extends RouteState> {
653
+ /** Resolved path params for this page instance. */
654
+ readonly params: S["params"];
655
+ /** Active locale this page instance is built for. */
656
+ readonly locale: string;
657
+ /** Resolve a sibling plugin instance to its public API (spec/08 §7). */
658
+ readonly require: RouteRequire;
659
+ /** Whether a plugin is registered (by name) — branch on OPTIONAL plugins. */
660
+ readonly has: (name: string) => boolean;
661
+ }
662
+ /**
663
+ * Build-time context handed to a route's `.generate()` — the static-param producer.
664
+ * Carries the active `locale` plus `require`/`has` (no `params` yet — `.generate()`
665
+ * PRODUCES the param sets). Same build-only guarantee as {@link LoadContext}.
666
+ *
667
+ * @example
668
+ * route("/{slug}/").generate(async (ctx) =>
669
+ * [...(await ctx.require(contentPlugin).loadAll()).get(ctx.locale) ?? []].map((a) => ({ slug: a.computed.slug })));
670
+ */
671
+ interface GenerateContext {
672
+ /** Active locale to enumerate param sets for. */
673
+ readonly locale: string;
674
+ /** Resolve a sibling plugin instance to its public API (spec/08 §7). */
675
+ readonly require: RouteRequire;
676
+ /** Whether a plugin is registered (by name). */
677
+ readonly has: (name: string) => boolean;
585
678
  }
586
679
  /**
587
680
  * Context handed to a route's `.layout()` wrapper: the render-time
@@ -617,7 +710,7 @@ interface RouteBuilder<S extends RouteState> extends RouteDefinition {
617
710
  * Attach a data loader; widens the data generic (and ONLY the data generic) so
618
711
  * `.render()`/`.head()` see its return. Path params are preserved unchanged.
619
712
  */
620
- load<D>(loader: (params: S["params"], locale: string) => D | Promise<D>): RouteBuilder<{
713
+ load<D>(loader: (ctx: LoadContext<S>) => D | Promise<D>): RouteBuilder<{
621
714
  readonly params: S["params"];
622
715
  readonly data: Awaited<D>;
623
716
  }>;
@@ -631,19 +724,10 @@ interface RouteBuilder<S extends RouteState> extends RouteDefinition {
631
724
  layout(component: (ctx: LayoutContext<S>, children: ComponentChildren) => VNode): RouteBuilder<S>;
632
725
  /** Attach the page render handler. */
633
726
  render(handler: (ctx: RouteContext<S>) => VNode): RouteBuilder<S>;
634
- /**
635
- * Attach the client-side validation gate: parse the raw `unknown` fetched from
636
- * the persisted data file back into this route's data type `S["data"]`. Runs at
637
- * the trust boundary before `render` on the client (and MUST return `S["data"]`,
638
- * so a mismatched schema is a compile error). Throw inside it to reject malformed
639
- * data — `spa` then falls back to HTML-over-fetch. Use a hand guard or any
640
- * Standard-Schema validator (zod/valibot/arktype).
641
- */
642
- parse(handler: (raw: unknown) => S["data"]): RouteBuilder<S>;
643
727
  /** Attach the head/SEO handler. */
644
728
  head(handler: (ctx: RouteContext<S>) => HeadConfig$1): RouteBuilder<S>;
645
- /** Attach a static-generation param producer. */
646
- generate(handler: (locale: string) => S["params"][] | Promise<S["params"][]>): RouteBuilder<S>;
729
+ /** Attach a static-generation param producer (receives a {@link GenerateContext}). */
730
+ generate(handler: (ctx: GenerateContext) => S["params"][] | Promise<S["params"][]>): RouteBuilder<S>;
647
731
  /**
648
732
  * Attach an arbitrary metadata bag. The bag MUST be JSON-serializable: it is
649
733
  * projected verbatim into `clientManifest()` and shipped to the browser.
@@ -656,18 +740,16 @@ interface RouteBuilder<S extends RouteState> extends RouteDefinition {
656
740
  }
657
741
  /** Build-only handler bag captured by a `RouteBuilder` (consumed by `build` via `manifest()`). */
658
742
  interface RouteHandlers {
659
- /** Data loader. */
660
- readonly load?: (params: Record<string, string>, locale: string) => unknown;
743
+ /** Data loader (receives a {@link LoadContext}: params + locale + require/has). */
744
+ readonly load?: (ctx: LoadContext<RouteState>) => unknown;
661
745
  /** Layout wrapper (ctx-aware): frames the page in persistent chrome. SSG-only. */
662
746
  readonly layout?: (ctx: LayoutContext<RouteState>, children: ComponentChildren) => VNode;
663
747
  /** Page renderer. */
664
748
  readonly render?: (ctx: RouteContext<RouteState>) => VNode;
665
- /** Client-side validation gate: `unknown` (fetched JSON) → the route's data type, or throw. */
666
- readonly parse?: (raw: unknown) => unknown;
667
749
  /** Head/SEO producer. */
668
750
  readonly head?: (ctx: RouteContext<RouteState>) => HeadConfig$1;
669
- /** Static-generation param producer. */
670
- readonly generate?: (locale: string) => unknown[] | Promise<unknown[]>;
751
+ /** Static-generation param producer (receives a {@link GenerateContext}). */
752
+ readonly generate?: (ctx: GenerateContext) => unknown[] | Promise<unknown[]>;
671
753
  /** JSON serializer. */
672
754
  readonly toJson?: (ctx: RouteContext<RouteState>) => unknown;
673
755
  /** Output file-path producer. */
@@ -691,28 +773,45 @@ interface RouteDefinition {
691
773
  * base (erased) `RouteDefinition`; this is the documented generic-erasure boundary.
692
774
  */
693
775
  type RouteMap = Record<string, RouteDefinition>;
776
+ /**
777
+ * A pure, app-free URL builder over a route map (the return type of `createUrls`).
778
+ * `toUrl` builds a route's path by name + params via pattern substitution — it needs
779
+ * NO running app, router instance, base URL, or i18n: just the route map the consumer
780
+ * already holds at module scope. Works identically at build, on client navigation,
781
+ * and inside hydrated islands. Reuses the SAME `buildUrl` as the runtime `RouterApi`,
782
+ * so the helper and the API can never diverge.
783
+ *
784
+ * @example
785
+ * const url = createUrls(routes);
786
+ * url.toUrl("article", { lang: "en", slug: "hello" }); // "/en/hello/"
787
+ */
788
+ interface Urls<T extends RouteMap> {
789
+ /**
790
+ * Build a route's URL path from its name and params. The name is typed to the
791
+ * route map's keys — only declared routes are accepted.
792
+ *
793
+ * @param name - Route name key from the map (e.g. `"home"`, `"article"`).
794
+ * @param params - Path params to substitute into the pattern. Defaults to `{}`.
795
+ * @returns The resolved relative URL path.
796
+ * @throws {Error} If `name` is not present in the route map.
797
+ * @example
798
+ * url.toUrl("home", { lang: "en" }); // "/en/"
799
+ */
800
+ toUrl<K extends keyof T & string>(name: K, params?: Record<string, string>): string;
801
+ }
694
802
  /**
695
803
  * Configuration for the router plugin.
696
804
  *
697
805
  * @remarks
698
- * `routes` is an OPAQUE carrier at the config boundary the framework `Config`
699
- * generic erases the per-route element types (spec/05 §8, spec/09 §4). Downstream
700
- * plugins read the typed route set via `ctx.require(routerPlugin).manifest()`.
806
+ * `routes` is the declarative route map registered the normal config way via
807
+ * `createApp({ pluginConfigs: { router: { routes } } })` and compiled into the matcher
808
+ * table in the router's `onInit`. An `import * as routes` namespace is a valid value. It is
809
+ * the SOLE registration path: omitting it leaves the matcher table empty, so every read
810
+ * (`match`/`toUrl`/`entries`/…) throws. The render `mode` is NOT here — it is a GLOBAL
811
+ * framework option (`createApp({ config: { mode } })`), read by the router via `ctx.global`.
701
812
  */
702
813
  type RouterConfig = {
703
- /**
704
- * Named route definitions. Element type erases to the base `RouteDefinition`
705
- * at this config boundary; per-route call-site types are preserved only through
706
- * `defineRoutes()` + `route()` at the consumer and re-exposed via `manifest()`.
707
- */
708
- routes: RouteMap;
709
- /**
710
- * Render mode for URL/file resolution. Defaults to `"hybrid"`.
711
- * - `"ssg"` static generation only (no client router emitted).
712
- * - `"spa"` client-side routing only.
713
- * - `"hybrid"` static HTML + client navigation overlay.
714
- */
715
- mode?: "ssg" | "spa" | "hybrid";
814
+ /** Declarative route map (route name → `route(...)`); compiled at init. An `import * as` namespace works. */readonly routes?: RouteMap;
716
815
  };
717
816
  /** A resolved route exposing URL utilities with typed params (port of legacy TypedRoute). */
718
817
  interface TypedRoute<TParams = Record<string, string>> {
@@ -761,15 +860,14 @@ interface MatcherTable {
761
860
  readonly byName: ReadonlyMap<string, CompiledRoute>;
762
861
  }
763
862
  /**
764
- * Router plugin state. `createState` runs with minimal context and returns a
765
- * mutable holder whose `table` is `null` until `onInit` (which has full context)
766
- * compiles and assigns it. Keeps all mutable state in `ctx.state` (no singletons).
863
+ * Router plugin state — a mutable holder whose `table` is `null` until the router's
864
+ * `onInit` compiles `config.routes`. The render `mode` is NOT stored here; it is read
865
+ * from the global framework config via the API context. Keeps all mutable state in
866
+ * `ctx.state` (no singletons).
767
867
  */
768
868
  interface RouterState {
769
- /** Compiled matcher table; `null` until `onInit` assigns it. */
869
+ /** Compiled matcher table; `null` until `onInit` compiles `config.routes`. */
770
870
  table: MatcherTable | null;
771
- /** Resolved render mode (single source of truth; set in `onInit`). Defaults `"hybrid"`. */
772
- mode: "ssg" | "spa" | "hybrid";
773
871
  }
774
872
  /** Plain-data input to `compileRoutes` — resolved DATA only, never the plugin ctx. */
775
873
  interface CompileInput {
@@ -802,7 +900,7 @@ interface ClientRoute {
802
900
  /** Route metadata bag from `.meta()`. MUST be JSON-serializable. */
803
901
  readonly meta: Record<string, unknown>;
804
902
  }
805
- /** Public API exposed via `ctx.require(routerPlugin)`. */
903
+ /** Public API exposed via `ctx.require(routerPlugin)` and `app.router`. */
806
904
  type RouterApi = {
807
905
  /**
808
906
  * Match a pathname against the compiled route table (specificity-sorted).
@@ -841,7 +939,7 @@ type RouterApi = {
841
939
  *
842
940
  * @returns Read-only array of the typed route definitions, in declaration order.
843
941
  * @example
844
- * for (const def of ctx.require(routerPlugin).manifest()) { def._handlers.load?.({}, "en"); }
942
+ * for (const def of ctx.require(routerPlugin).manifest()) def._handlers.render?.(routeContext);
845
943
  */
846
944
  manifest(): readonly RouteDefinition[];
847
945
  /**
@@ -867,13 +965,13 @@ type RouterApi = {
867
965
  mode(): "ssg" | "spa" | "hybrid";
868
966
  };
869
967
  /** Re-export under the canonical `Config` name for the plugin-types barrel. */
870
- type Config$4 = RouterConfig;
968
+ type Config$5 = RouterConfig;
871
969
  /** Re-export under the canonical `State` name for the plugin-types barrel. */
872
- type State$4 = RouterState;
970
+ type State$5 = RouterState;
873
971
  /** Re-export under the canonical `Api` name for the plugin-types barrel. */
874
- type Api$4 = RouterApi;
875
- declare namespace types_d_exports$5 {
876
- export { Api$3 as Api, ArticleMeta, Config$3 as Config, HeadConfig, HeadDefaults, HeadElement, ResolvedRoute, State$3 as State };
972
+ type Api$5 = RouterApi;
973
+ declare namespace types_d_exports$6 {
974
+ export { Api$4 as Api, ArticleMeta, Config$4 as Config, HeadConfig, HeadDefaults, HeadElement, ResolvedRoute, State$4 as State };
877
975
  }
878
976
  /**
879
977
  * @file head plugin — type definitions skeleton
@@ -889,7 +987,7 @@ declare namespace types_d_exports$5 {
889
987
  * const config: Config = { titleTemplate: "%s — Moku" };
890
988
  * ```
891
989
  */
892
- type Config$3 = {
990
+ type Config$4 = {
893
991
  /** 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. */
894
992
  defaultOgImage?: string; /** Default Twitter card type emitted when og/twitter content is present. */
895
993
  twitterCard?: "summary" | "summary_large_image"; /** Default Twitter site handle (e.g. `"@moku_labs"`) emitted as `twitter:site`. */
@@ -907,7 +1005,7 @@ type Config$3 = {
907
1005
  * const state: State = { defaults: null };
908
1006
  * ```
909
1007
  */
910
- type State$3 = {
1008
+ type State$4 = {
911
1009
  /** Normalized head defaults, assigned once in `onInit` (initially `null`). */defaults: HeadDefaults | null;
912
1010
  };
913
1011
  /**
@@ -1001,7 +1099,7 @@ type ResolvedRoute = {
1001
1099
  * const html: string = api.render(route, data);
1002
1100
  * ```
1003
1101
  */
1004
- type Api$3 = {
1102
+ type Api$4 = {
1005
1103
  /**
1006
1104
  * Compose the final `<head>` inner HTML for a route. Pulled synchronously by `build`.
1007
1105
  *
@@ -1015,7 +1113,7 @@ type Api$3 = {
1015
1113
  */
1016
1114
  render(route: ResolvedRoute, data: unknown): string;
1017
1115
  };
1018
- declare namespace types_d_exports$8 {
1116
+ declare namespace types_d_exports$9 {
1019
1117
  export { COMPONENT_HOOK_NAMES, ComponentContext, ComponentDef, ComponentHooks, ComponentInstance, ExtractApi$1 as ExtractApi, PageData, ResolvedSpaConfig, SpaApi, SpaConfig, SpaContext, SpaDataReader, SpaEmitFunction, SpaEvents, SpaKernel, SpaKernelDeps, SpaRequire, SpaState };
1020
1118
  }
1021
1119
  /** Payload map for the events `spa` emits, used to type the kernel's `emit` closure. */
@@ -1216,11 +1314,11 @@ interface SpaKernelDeps {
1216
1314
  /** Router plugin API — used for client-side route matching (`match`) + the resolved `mode`. */
1217
1315
  router: RouterApi;
1218
1316
  /** Head plugin API — its pure compose is reused for client head-sync. */
1219
- head: Api$3;
1317
+ head: Api$4;
1220
1318
  /**
1221
1319
  * The OPTIONAL `data` reader. Present only when the `data` plugin is composed.
1222
1320
  * When present (and `router.mode() !== "ssg"`), navigation first tries the client
1223
- * DATA path (match → `dataAt(path)` → `route.parse` → `route.render`); otherwise
1321
+ * DATA path (match → `dataAt(path)` → `route.render`); otherwise
1224
1322
  * it always uses HTML-over-fetch.
1225
1323
  */
1226
1324
  dataAt?: SpaDataReader;
@@ -1323,7 +1421,7 @@ type SpaApi = {
1323
1421
  current(): string;
1324
1422
  };
1325
1423
  declare namespace types_d_exports {
1326
- 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 };
1424
+ export { Api$3 as Api, BuildCacheEntry, BuildEvents, BuildResult, Config$3 as Config, ExtractApi, OgFont, OgImageConfig, PhaseContext, PhaseEmit, PhaseLog, PhaseName, PhaseRequire, RichOgInput, State$3 as State };
1327
1425
  }
1328
1426
  /**
1329
1427
  * Structural extraction of a plugin instance's public API from its `_phantom`
@@ -1400,27 +1498,16 @@ type PhaseRequire = <PluginCandidate extends {
1400
1498
  * ```
1401
1499
  */
1402
1500
  type PhaseContext = {
1403
- /** Mutable per-run build state (caches + runId). */state: State$2; /** Resolved, frozen build config. */
1404
- readonly config: Readonly<Config$2>; /** Global framework config (mode, etc.). */
1501
+ /** Mutable per-run build state (caches + runId). */state: State$3; /** Resolved, frozen build config. */
1502
+ readonly config: Readonly<Config$3>; /** Global framework config (deployment stage; render mode is read via `router.mode()`). */
1405
1503
  readonly global: Readonly<{
1406
- mode: "production" | "development";
1504
+ stage: Stage;
1407
1505
  }>; /** Resolve a depended-upon plugin instance to its public API. */
1408
1506
  require: PhaseRequire; /** Whether a plugin is registered (by name) — used to detect the OPTIONAL `data` plugin. */
1409
1507
  has: (name: string) => boolean; /** Emit a build event (notification-only). */
1410
1508
  emit: PhaseEmit; /** Structured logger (core `log` API). */
1411
1509
  readonly log: PhaseLog;
1412
1510
  };
1413
- /**
1414
- * Injectable PNG renderer for the og-images phase. Defaults to the real
1415
- * Satori → resvg pipeline; unit tests inject a fake to assert hash-cache skip
1416
- * and the `p-limit` bound without rasterizing real images.
1417
- *
1418
- * @example
1419
- * ```ts
1420
- * const render: OgPngRenderer = async () => new Uint8Array();
1421
- * ```
1422
- */
1423
- type OgPngRenderer = (input: RichOgInput) => Promise<Uint8Array>;
1424
1511
  /**
1425
1512
  * Rich input handed to a custom OG `render` hook for a single article card. Carries
1426
1513
  * the full article + site metadata so a consumer can compose any layout. Returned by
@@ -1510,7 +1597,7 @@ interface OgImageConfig {
1510
1597
  * const config: Config = { outDir: "./dist", minify: true, feeds: true, sitemap: true, images: true, ogImage: false };
1511
1598
  * ```
1512
1599
  */
1513
- type Config$2 = {
1600
+ type Config$3 = {
1514
1601
  /** Output directory for the built site. */outDir: string; /** Minify bundled CSS/JS. */
1515
1602
  minify: boolean; /** Generate RSS/Atom/JSON feeds. */
1516
1603
  feeds: boolean; /** Generate sitemap.xml + robots.txt. */
@@ -1521,11 +1608,11 @@ type Config$2 = {
1521
1608
  publicDir?: string;
1522
1609
  /**
1523
1610
  * Emit `outDir/404.html`. `true` for the built-in default page, or
1524
- * `{ route }` to supply the page's literal HTML body content (NOT a route
1525
- * path/slug — the string is written into the 404 page verbatim). Default `false`.
1611
+ * `{ body }` to supply the page's literal HTML body content (written into the
1612
+ * 404 page verbatim). Default `false`.
1526
1613
  */
1527
1614
  notFound?: boolean | {
1528
- route?: string;
1615
+ body?: string;
1529
1616
  }; /** Emit per-path i18n bare-path redirect HTML pages. Default `false`. */
1530
1617
  localeRedirects?: boolean; /** Authoritative client bundle entry path (overrides the conventional scan). */
1531
1618
  clientEntry?: string; /** HTML shell template with `<!--moku:head-->`/`<!--moku:body-->`/`<!--moku:assets-->` placeholders. */
@@ -1551,9 +1638,9 @@ type BuildCacheEntry = Record<string, string>;
1551
1638
  * const state: State = { config, manifest: null, buildCache: new Map(), runId: null, ogImageHashCache: new Map() };
1552
1639
  * ```
1553
1640
  */
1554
- interface State$2 {
1641
+ interface State$3 {
1555
1642
  /** Resolved, frozen config snapshot. */
1556
- config: Config$2;
1643
+ config: Config$3;
1557
1644
  /** Cached route manifest for the current run (populated in Phase 3 from router). */
1558
1645
  manifest: RouteDefinition[] | null;
1559
1646
  /** Per-run build artifacts (e.g. hashed CSS/JS asset paths from Phase 1). */
@@ -1600,7 +1687,7 @@ interface BuildResult {
1600
1687
  * const result = await app.build.run();
1601
1688
  * ```
1602
1689
  */
1603
- type Api$2 = {
1690
+ type Api$3 = {
1604
1691
  /**
1605
1692
  * Run the full SSG pipeline and write the site to disk.
1606
1693
  *
@@ -1649,7 +1736,7 @@ type Api$2 = {
1649
1736
  * });
1650
1737
  * ```
1651
1738
  */
1652
- declare const buildPlugin: import("@moku-labs/core").PluginInstance<"build", Config$2, State$2, Api$2, {
1739
+ declare const buildPlugin: import("@moku-labs/core").PluginInstance<"build", Config$3, State$3, Api$3, {
1653
1740
  "build:phase": {
1654
1741
  phase: PhaseName;
1655
1742
  status: "start" | "done";
@@ -1661,117 +1748,780 @@ declare const buildPlugin: import("@moku-labs/core").PluginInstance<"build", Con
1661
1748
  durationMs: number;
1662
1749
  };
1663
1750
  }> & Record<never, never>;
1664
- declare namespace types_d_exports$1 {
1665
- export { Api$1 as Api, Article, ArticleCard, ComputedFields, Config$1 as Config, ContentApiContext, ContentEvents, Frontmatter, State$1 as State };
1751
+ declare namespace types_d_exports$4 {
1752
+ export { Api$2 as Api, Config$2 as Config, DeployErrorCode, DeployInitOptions, DeployResult, DeployRunOptions, InitResult, SpawnFunction, SpawnOptions, SpawnedProcess, State$2 as State, WranglerErrorKind };
1666
1753
  }
1667
1754
  /**
1668
- * Configuration for the content plugin.
1669
- *
1670
- * @example
1671
- * ```ts
1672
- * { contentDir: "./src/content", trustedContent: false, shikiTheme: "github-dark" }
1673
- * ```
1755
+ * @file deploy plugin type definitions.
1674
1756
  */
1675
- type Config$1 = {
1676
- /** Absolute or project-relative path to the content directory. Validated in onInit. */contentDir: string;
1757
+ /**
1758
+ * Options passed to the injected spawner the subset of Bun.spawn's options the
1759
+ * plugin uses (piped stdout/stderr plus an env carrying the API token).
1760
+ */
1761
+ interface SpawnOptions {
1762
+ /** Capture stdout as a readable stream. */
1763
+ readonly stdout: "pipe";
1764
+ /** Capture stderr as a readable stream. */
1765
+ readonly stderr: "pipe";
1766
+ /** Subprocess environment — the API token is injected here, never via argv. */
1767
+ readonly env?: Record<string, string | undefined>;
1768
+ }
1769
+ /**
1770
+ * The structural subprocess handle the plugin reads back: stdout/stderr streams
1771
+ * plus the exit-code promise. Streams are typed `unknown` and narrowed at the read
1772
+ * site so this carries no Bun namespace types.
1773
+ */
1774
+ interface SpawnedProcess {
1775
+ /** Standard output stream (narrowed to a ReadableStream at the read site). */
1776
+ readonly stdout: unknown;
1777
+ /** Standard error stream (narrowed to a ReadableStream at the read site). */
1778
+ readonly stderr: unknown;
1779
+ /** Resolves with the subprocess exit code. */
1780
+ readonly exited: Promise<number>;
1781
+ }
1782
+ /**
1783
+ * The subset of Bun.spawn's signature the plugin relies on (argv array + options).
1784
+ * Declared structurally — with NO `import("bun")` namespace types — so it survives
1785
+ * `.d.ts` bundling intact and tests can supply a fake spawn without importing Bun.
1786
+ */
1787
+ type SpawnFunction = (cmd: string[], options: SpawnOptions) => SpawnedProcess;
1788
+ /**
1789
+ * A deploy error `code` from the wrangler error taxonomy and preflight validators.
1790
+ */
1791
+ type DeployErrorCode = "ERR_DEPLOY_NO_WRANGLER_CONFIG" | "ERR_DEPLOY_EMPTY_OUTDIR" | "ERR_DEPLOY_TOO_MANY_FILES" | "ERR_DEPLOY_FILE_TOO_LARGE" | "ERR_DEPLOY_PATH_TRAVERSAL" | "ERR_DEPLOY_INVALID_BRANCH" | "ERR_DEPLOY_NO_TOKEN" | "ERR_DEPLOY_PROJECT_NOT_FOUND" | "ERR_DEPLOY_AUTH_EXPIRED" | "ERR_DEPLOY_AUTH" | "ERR_DEPLOY_NETWORK" | "ERR_DEPLOY_WRANGLER_FAILED" | "ERR_DEPLOY_CONFIG";
1792
+ /**
1793
+ * The subset of wrangler error `code`s classifyWranglerError can produce from a
1794
+ * non-zero wrangler exit.
1795
+ */
1796
+ type WranglerErrorKind = Extract<DeployErrorCode, "ERR_DEPLOY_PROJECT_NOT_FOUND" | "ERR_DEPLOY_AUTH_EXPIRED" | "ERR_DEPLOY_AUTH" | "ERR_DEPLOY_NETWORK" | "ERR_DEPLOY_WRANGLER_FAILED">;
1797
+ /**
1798
+ * Configuration for the deploy plugin.
1799
+ */
1800
+ type Config$2 = {
1677
1801
  /**
1678
- * SECURITY GATE. When false (the default), rehype-sanitize runs as the final
1679
- * pipeline step. Set true ONLY for fully author-controlled Markdown — true
1680
- * disables sanitize and trusts all raw HTML.
1802
+ * Deploy target. Only Cloudflare Pages is supported in this version.
1803
+ * Defaults to `cloudflare-pages`.
1681
1804
  */
1682
- trustedContent: boolean; /** Additional remark plugins, concatenated AFTER framework defaults. Defaults to []. */
1683
- extraRemarkPlugins?: readonly Pluggable[]; /** Additional rehype plugins, concatenated after custom transforms, before Shiki + sanitize. Defaults to []. */
1684
- extraRehypePlugins?: readonly Pluggable[];
1805
+ target: "cloudflare-pages";
1806
+ /**
1807
+ * Directory (relative to project root) containing the built site to deploy.
1808
+ * Re-validated against cwd at run() time to block path traversal.
1809
+ * Defaults to `dist`.
1810
+ */
1811
+ outDir: string;
1685
1812
  /**
1686
- * Shiki theme for syntax highlighting: a bundled theme NAME — typed as Shiki's
1687
- * `BundledTheme` union so editors autocomplete the ~60 built-ins (default
1688
- * "github-dark") — or a custom `ThemeRegistration` object. Passed straight through
1689
- * to `@shikijs/rehype`'s `theme`. (Like Shiki's own theme type, an arbitrary string
1690
- * still compiles via the object arm, so this is autocomplete, not typo-rejection.)
1813
+ * Branch treated as the Cloudflare Pages production branch.
1814
+ * Defaults to `main`.
1691
1815
  */
1692
- shikiTheme?: BundledTheme | ThemeRegistrationAny; /** Author applied to articles whose frontmatter omits author. Defaults to undefined. */
1693
- defaultAuthor?: string;
1816
+ productionBranch?: string;
1817
+ /**
1818
+ * Substrings exempt from entropy-gated secret scrubbing in logged output.
1819
+ * Defaults to `["CLOUDFLARE_ACCOUNT_ID"]`.
1820
+ */
1821
+ scrubAllowlist: string[];
1822
+ /**
1823
+ * Cloudflare compatibility date written into generated wrangler.jsonc.
1824
+ * Defaults to `2024-01-01`.
1825
+ */
1826
+ compatibilityDate?: string;
1827
+ /**
1828
+ * Whether init() also generates a GitHub Actions workflow.
1829
+ * Defaults to `false`.
1830
+ */
1831
+ ci?: boolean;
1694
1832
  };
1695
1833
  /**
1696
- * Internal mutable state for the content plugin.
1697
- *
1698
- * @example
1699
- * ```ts
1700
- * { processor: null, articles: new Map(), slugs: null, dirtyPaths: new Set() }
1701
- * ```
1834
+ * Result of a successful deploy.
1702
1835
  */
1703
- type State$1 = {
1704
- /** Lazily-created unified processor singleton. null until first render()/loadAll(). */processor: Processor | null; /** Article cache keyed locale -> (slug -> Article). Starts empty. */
1705
- articles: Map<string, Map<string, Article>>; /** Discovered, sorted slug list cached after first disk scan. null until first discovery. */
1706
- slugs: string[] | null; /** Paths marked stale by invalidate(); next loadAll() re-reads only these. Starts empty. */
1707
- dirtyPaths: Set<string>;
1836
+ type DeployResult = {
1837
+ /** The public deployment URL (e.g. https://my-site.pages.dev). */url: string; /** Cloudflare deployment ID parsed from wrangler output. */
1838
+ deploymentId: string; /** The branch that was deployed. */
1839
+ branch: string; /** Wall-clock duration of the deploy in milliseconds. */
1840
+ durationMs: number;
1708
1841
  };
1709
1842
  /**
1710
- * YAML frontmatter parsed from each article file.
1711
- *
1712
- * @example
1713
- * ```ts
1714
- * { title: "Hello", date: "2026-01-15", description: "Intro", tags: [], language: "en" }
1715
- * ```
1843
+ * Runtime state for the deploy plugin. Created in createState() and accessed via
1844
+ * ctx.state. deploy declares no onStop because nothing here is a long-lived resource.
1716
1845
  */
1717
- type Frontmatter = {
1718
- /** Article title. Required. */title: string; /** ISO 8601 date string, e.g. "2026-01-15". Required. */
1719
- date: string; /** Short summary used in cards, feeds, and meta description. Required. */
1720
- description: string; /** Topic tags. Required (may be empty array). */
1721
- tags: string[]; /** Source language code of this file. Required. */
1722
- language: string; /** Draft flag. Excluded from output in production mode. Defaults to false. */
1723
- draft?: boolean; /** Author name. Falls back to config.defaultAuthor when omitted. */
1724
- author?: string;
1846
+ type State$2 = {
1847
+ /** Result of the most recent successful deploy, or null before the first run. */lastDeployment: DeployResult | null;
1848
+ /**
1849
+ * Injectable subprocess spawner. Defaults to Bun.spawn. Swapped for a mock in
1850
+ * unit tests so wrangler is never actually invoked. Never reassigned at runtime.
1851
+ */
1852
+ spawn: SpawnFunction;
1725
1853
  };
1726
1854
  /**
1727
- * Fields computed by the pipeline (not authored in frontmatter).
1728
- *
1729
- * @example
1730
- * ```ts
1731
- * { slug: "hello", readingTime: 1, contentId: "hello", status: "published", wordCount: 42 }
1732
- * ```
1855
+ * Options for DeployApi.run.
1733
1856
  */
1734
- type ComputedFields = {
1735
- /** Article directory name. */slug: string; /** Reading time in minutes (ceiling, minimum 1). */
1736
- readingTime: number; /** Stable content identifier (slug by default). */
1737
- contentId: string; /** Derived publication status. */
1738
- status: "published" | "draft"; /** Word count from the source body. */
1739
- wordCount: number;
1857
+ type DeployRunOptions = {
1858
+ /**
1859
+ * Branch to deploy. Defaults to config.productionBranch (or "main"). Must match
1860
+ * /^[a-zA-Z0-9/_.-]+$/ otherwise rejected with ERR_DEPLOY_INVALID_BRANCH.
1861
+ */
1862
+ branch?: string;
1740
1863
  };
1741
1864
  /**
1742
- * A fully processed, render-ready article.
1743
- *
1744
- * @example
1745
- * ```ts
1746
- * { frontmatter, computed, html: "<p>…</p>", locale: "en", isFallback: false, url: "/en/hello/" }
1747
- * ```
1865
+ * Options for DeployApi.init.
1748
1866
  */
1749
- type Article = {
1750
- /** Parsed frontmatter. */frontmatter: Frontmatter; /** Pipeline-computed metadata. */
1751
- computed: ComputedFields; /** Sanitized rendered HTML body. */
1752
- html: string; /** Locale this Article instance represents (the requested locale, even on fallback). */
1753
- locale: string; /** True when the default-locale file was used because the requested locale was missing. */
1754
- isFallback: boolean; /** Canonical URL for this article in this locale. */
1755
- url: string;
1867
+ type DeployInitOptions = {
1868
+ /** Also generate the GitHub Actions workflow. Defaults to config.ci. */ci?: boolean; /** Drift-only mode: report differences without writing any files. Defaults to `false`. */
1869
+ check?: boolean;
1756
1870
  };
1757
1871
  /**
1758
- * Lightweight projection of Article for cards/lists.
1759
- *
1760
- * @example
1761
- * ```ts
1762
- * { contentId: "hello", status: "published", title: "Hello", date: "2026-01-15", description: "Intro", tags: [], readingTime: 1, url: "/en/hello/" }
1763
- * ```
1872
+ * Result of an init/scaffold operation.
1764
1873
  */
1765
- type ArticleCard = {
1766
- /** Stable content identifier. */contentId: string; /** Derived publication status. */
1767
- status: "published" | "draft"; /** Article title. */
1768
- title: string; /** ISO 8601 date string. */
1769
- date: string; /** Short summary. */
1874
+ type InitResult = {
1875
+ /** Paths written this invocation. */written: string[]; /** Paths skipped because they already exist. */
1876
+ skipped: string[]; /** In check mode: paths whose on-disk content differs from what would be generated. */
1877
+ drifted: string[];
1878
+ };
1879
+ /**
1880
+ * Public API of the deploy plugin (returned from the api factory).
1881
+ */
1882
+ type Api$2 = {
1883
+ /**
1884
+ * Deploy the built outDir to Cloudflare Pages via the wrangler subprocess.
1885
+ * Runs preflight validators, re-validates the resolved outdir against cwd, guards
1886
+ * the branch argument, spawns wrangler (no shell), scrubs all subprocess output
1887
+ * before logging, records lastDeployment, and emits deploy:complete.
1888
+ *
1889
+ * @param options - Optional branch override.
1890
+ * @returns The deploy result (url, deploymentId, branch, durationMs).
1891
+ * @throws {Error} With a `code` from the deploy error taxonomy on any failure.
1892
+ * @example
1893
+ * const result = await app.deploy.run();
1894
+ * console.log(result.url); // https://my-site.pages.dev
1895
+ * @example
1896
+ * await app.deploy.run({ branch: "preview/landing" });
1897
+ */
1898
+ run(options?: DeployRunOptions): Promise<DeployResult>;
1899
+ /**
1900
+ * Return the most recent successful deploy result, or null if none has occurred.
1901
+ * The returned object is read-only (a defensive snapshot).
1902
+ *
1903
+ * @returns The last DeployResult, or null.
1904
+ * @example
1905
+ * const last = app.deploy.getLastDeployment();
1906
+ * if (last) console.log(`Last deployed to ${last.url}`);
1907
+ */
1908
+ getLastDeployment(): Readonly<DeployResult> | null;
1909
+ /**
1910
+ * Generate deploy scaffolding: wrangler.jsonc (slug from site.name() + outDir +
1911
+ * compatibilityDate) and, when ci is enabled, .github/workflows/deploy.yml. Never
1912
+ * overwrites an existing wrangler.jsonc. In check mode, reports drift instead of writing.
1913
+ *
1914
+ * @param options - Optional ci toggle and check (drift-only) mode.
1915
+ * @returns Which files were written, skipped, or would drift.
1916
+ * @example
1917
+ * const out = await app.deploy.init({ ci: true });
1918
+ * // out.written -> ["wrangler.jsonc", ".github/workflows/deploy.yml"]
1919
+ * @example
1920
+ * const drift = await app.deploy.init({ check: true });
1921
+ * if (drift.drifted.length) process.exit(1);
1922
+ */
1923
+ init(options?: DeployInitOptions): Promise<InitResult>;
1924
+ };
1925
+ declare namespace types_d_exports$1 {
1926
+ export { Api$1 as Api, BuildOptions, BuildSummary, CliErrorCode, CliRenderer, Command, Config$1 as Config, DeployOptions, DeployOutcome, FileResponseFunction, PreviewOptions, ReloadInfo, ServeOptions, ServeStaticFunction, ServeStaticOptions, ServerHandle, ServerInfo, State$1 as State, WatchHandle };
1927
+ }
1928
+ /**
1929
+ * A cli error `code` from the config-validation and runtime taxonomy. Mirrors the
1930
+ * deploy plugin's coded-error pattern so every thrown value carries a stable `code`.
1931
+ *
1932
+ * @example
1933
+ * const code: CliErrorCode = "ERR_CLI_CONFIG";
1934
+ */
1935
+ type CliErrorCode = "ERR_CLI_CONFIG" | "ERR_CLI_NOT_FOUND";
1936
+ /**
1937
+ * The four commands a single cli process can run. Each maps to one consumer script
1938
+ * (`scripts/{build,serve,preview,deploy}.ts`) and is rendered as the Panel header.
1939
+ *
1940
+ * @example
1941
+ * const command: Command = "build";
1942
+ */
1943
+ type Command = "build" | "serve" | "preview" | "deploy";
1944
+ /**
1945
+ * Information rendered into the bordered server-ready panel by `serve()`/`preview()`.
1946
+ *
1947
+ * @example
1948
+ * const info: ServerInfo = { local: "http://localhost:4173", network: null };
1949
+ */
1950
+ type ServerInfo = {
1951
+ /** The loopback URL the server is reachable at (e.g. `http://localhost:4173`). */local: string; /** The LAN URL derived from the first non-internal IPv4, or `null` when offline. */
1952
+ network: string | null; /** Directories `serve()` is watching for changes (omitted by `preview()`). */
1953
+ watching?: string[];
1954
+ };
1955
+ /**
1956
+ * Information rendered after a single `serve()` rebuild: the watched directory whose
1957
+ * subtree changed plus the fresh build summary used to print the "rebuilt N pages"
1958
+ * line.
1959
+ *
1960
+ * @example
1961
+ * const info: ReloadInfo = { file: "content", pageCount: 12, durationMs: 84 };
1962
+ */
1963
+ type ReloadInfo = {
1964
+ /** The watched directory whose subtree changed (the rebuild is per-watchDir, not per-file). */file: string; /** Number of route pages rendered by the rebuild. */
1965
+ pageCount: number; /** Wall-clock duration of the rebuild in milliseconds. */
1966
+ durationMs: number;
1967
+ };
1968
+ /**
1969
+ * The Panel renderer surface — every line of terminal output flows through this so
1970
+ * tests can inject a line-capturing fake. Implemented by `createPanelRenderer` and
1971
+ * is TTY/`NO_COLOR`-aware (box-drawing + color on a TTY, plain lines otherwise).
1972
+ *
1973
+ * @example
1974
+ * const render: CliRenderer = createPanelRenderer();
1975
+ * render.header("build");
1976
+ */
1977
+ type CliRenderer = {
1978
+ /**
1979
+ * Render the boxed `MOKU WEB` logo + command label. Called once per command (one
1980
+ * command = one process), so it never repeats within a run.
1981
+ *
1982
+ * @param command - The command being run, shown beside the logo.
1983
+ * @returns Nothing.
1984
+ * @example
1985
+ * render.header("serve");
1986
+ */
1987
+ header(command: Command): void;
1988
+ /**
1989
+ * Render a live per-phase row from a `build:phase` event.
1990
+ *
1991
+ * @param phase - The `build:phase` payload (`{ phase, status, durationMs? }`).
1992
+ * @returns Nothing.
1993
+ * @example
1994
+ * render.phase({ phase: "pages", status: "done", durationMs: 12 });
1995
+ */
1996
+ phase(phase: BuildEvents["build:phase"]): void;
1997
+ /**
1998
+ * Render the BUILD summary block from a `build:complete` event.
1999
+ *
2000
+ * @param summary - The `build:complete` payload (`{ outDir, pageCount, durationMs }`).
2001
+ * @returns Nothing.
2002
+ * @example
2003
+ * render.built({ outDir: "dist", pageCount: 12, durationMs: 840 });
2004
+ */
2005
+ built(summary: BuildEvents["build:complete"]): void;
2006
+ /**
2007
+ * Render the bordered server-ready panel (Local / Network URLs + watched dirs).
2008
+ *
2009
+ * @param info - Local/Network URLs and optionally the watched directories.
2010
+ * @returns Nothing.
2011
+ * @example
2012
+ * render.serverReady({ local: "http://localhost:4173", network: null });
2013
+ */
2014
+ serverReady(info: ServerInfo): void;
2015
+ /**
2016
+ * Render the post-rebuild line ("~ dir" + "✓ rebuilt N pages · Xms · reloaded"),
2017
+ * where the label is the watched directory whose subtree changed (see {@link ReloadInfo.file}).
2018
+ *
2019
+ * @param info - The changed watched directory plus the rebuild's page count and duration.
2020
+ * @returns Nothing.
2021
+ * @example
2022
+ * render.reload({ file: "content", pageCount: 12, durationMs: 84 });
2023
+ */
2024
+ reload(info: ReloadInfo): void;
2025
+ /**
2026
+ * Render the deploy result panel from a `deploy:complete` event.
2027
+ *
2028
+ * @param result - The `deploy:complete` payload (`{ url, deploymentId, branch, durationMs }`).
2029
+ * @returns Nothing.
2030
+ * @example
2031
+ * render.deployed({ url: "https://x.pages.dev", deploymentId: "id", branch: "main", durationMs: 1200 });
2032
+ */
2033
+ deployed(result: DeployResult): void;
2034
+ /**
2035
+ * Render a neutral informational line (e.g. the non-interactive deploy note, watch notice).
2036
+ *
2037
+ * @param message - The line to print.
2038
+ * @returns Nothing.
2039
+ * @example
2040
+ * render.info("watching for changes…");
2041
+ */
2042
+ info(message: string): void;
2043
+ /**
2044
+ * Render a warning line (written to stderr).
2045
+ *
2046
+ * @param message - The warning to print.
2047
+ * @returns Nothing.
2048
+ * @example
2049
+ * render.warn("deploy skipped");
2050
+ */
2051
+ warn(message: string): void;
2052
+ /**
2053
+ * Render an error line (written to stderr), optionally with a cause.
2054
+ *
2055
+ * @param message - The error summary to print.
2056
+ * @param cause - Optional underlying error/value to print beneath the summary.
2057
+ * @returns Nothing.
2058
+ * @example
2059
+ * render.error("build failed", err);
2060
+ */
2061
+ error(message: string, cause?: unknown): void;
2062
+ };
2063
+ /**
2064
+ * A live directory watcher handle returned by the injectable `watch` seam. Closing
2065
+ * it detaches the underlying `node:fs.watch` listener.
2066
+ *
2067
+ * @example
2068
+ * const handle: WatchHandle = state.watch("content", onChange);
2069
+ * handle.close();
2070
+ */
2071
+ type WatchHandle = {
2072
+ /**
2073
+ * Stop watching and release the underlying listener.
2074
+ *
2075
+ * @returns Nothing.
2076
+ * @example
2077
+ * handle.close();
2078
+ */
2079
+ close(): void;
2080
+ };
2081
+ /**
2082
+ * A running static server handle the cli stops on teardown. Declared structurally
2083
+ * (no Bun namespace types) so it survives `.d.ts` bundling and tests can supply a
2084
+ * fake without importing Bun.
2085
+ *
2086
+ * @example
2087
+ * const handle: ServerHandle = state.serveStatic({ port, fetch });
2088
+ * handle.stop();
2089
+ */
2090
+ type ServerHandle = {
2091
+ /**
2092
+ * Stop the server and release its socket.
2093
+ *
2094
+ * @returns Nothing.
2095
+ * @example
2096
+ * handle.stop();
2097
+ */
2098
+ stop(): void;
2099
+ };
2100
+ /**
2101
+ * The subset of `Bun.serve`'s options the cli uses: a port plus a `fetch` handler.
2102
+ * Declared structurally so no Bun namespace type reaches the public surface.
2103
+ *
2104
+ * @example
2105
+ * const opts: ServeStaticOptions = { port: 4173, fetch: () => new Response("ok") };
2106
+ */
2107
+ type ServeStaticOptions = {
2108
+ /** Port to bind the server to. */port: number;
2109
+ /**
2110
+ * Per-request handler returning the response (sync or async).
2111
+ *
2112
+ * @param request - The incoming request.
2113
+ * @returns The response (or a promise of it).
2114
+ * @example
2115
+ * fetch(req) { return new Response("ok"); }
2116
+ */
2117
+ fetch(request: Request): Response | Promise<Response>;
2118
+ };
2119
+ /**
2120
+ * An injectable static-server factory (defaults to `Bun.serve`). Keeps the Bun
2121
+ * runtime dependency behind a structural seam so `serve()`/`preview()` never open a
2122
+ * real socket in tests.
2123
+ *
2124
+ * @example
2125
+ * const serveStatic: ServeStaticFunction = options => Bun.serve(options);
2126
+ */
2127
+ type ServeStaticFunction = (options: ServeStaticOptions) => ServerHandle;
2128
+ /**
2129
+ * An injectable file-response factory (defaults to `new Response(Bun.file(path))`).
2130
+ * Maps a resolved on-disk path + status to the response body the server returns.
2131
+ *
2132
+ * @example
2133
+ * const fileResponse: FileResponseFunction = (path, status) => new Response(Bun.file(path), { status });
2134
+ */
2135
+ type FileResponseFunction = (path: string, status: number) => Response;
2136
+ /**
2137
+ * Configuration for the cli plugin — the complete resolved `Config` (not a partial).
2138
+ * Consumers override individual fields via `pluginConfigs.cli`.
2139
+ *
2140
+ * @example
2141
+ * const config: Config = {
2142
+ * outDir: "dist", port: 4173, watchDirs: ["content", "src"],
2143
+ * debounceMs: 150, notFoundFile: "404.html", liveReload: true
2144
+ * };
2145
+ */
2146
+ type Config$1 = {
2147
+ /** Build output directory; served by preview, asserted by build, rebuilt by serve. Default `"dist"`. */outDir: string; /** Default port for serve()/preview() (overridable per-call via options.port). Default `4173`. */
2148
+ port: number; /** Directories serve() watches for changes (recursive). Default `["content", "src"]`. */
2149
+ watchDirs: string[]; /** Debounce window (ms) coalescing FS-event bursts into one rebuild. Default `150`. */
2150
+ debounceMs: number; /** Filename build() asserts exists at outDir root (CF Pages flips to SPA mode without it). Default `"404.html"`. */
2151
+ notFoundFile: string; /** Inject the live-reload SSE client into HTML during serve() (never during preview()). Default `true`. */
2152
+ liveReload: boolean;
2153
+ };
2154
+ /**
2155
+ * Runtime state for the cli plugin — injectable seams so every command is testable
2156
+ * without real sockets/FS-watch/TTY (mirrors deploy's injectable `spawn`).
2157
+ *
2158
+ * @example
2159
+ * const state: State = createState();
2160
+ */
2161
+ type State$1 = {
2162
+ /** Panel renderer — all terminal output flows through this. Tests inject a line-capturing fake. */render: CliRenderer;
2163
+ /**
2164
+ * Interactive y/N prompt used by deploy(). Default reads stdin (TTY); tests inject a canned answer.
2165
+ *
2166
+ * @param question - The yes/no question to display.
2167
+ * @returns Resolves `true` when the user answered yes.
2168
+ * @example
2169
+ * const ok = await state.confirm("Deploy dist/?");
2170
+ */
2171
+ confirm: (question: string) => Promise<boolean>;
2172
+ /**
2173
+ * Monotonic clock for durations. Default `Date.now`; tests inject for deterministic timing.
2174
+ *
2175
+ * @returns The current time in milliseconds.
2176
+ * @example
2177
+ * const t = state.clock();
2178
+ */
2179
+ clock: () => number;
2180
+ /**
2181
+ * Recursive directory watcher factory used by serve(). Default wraps `node:fs.watch`;
2182
+ * tests inject a fake emitter.
2183
+ *
2184
+ * @param dir - The directory to watch recursively.
2185
+ * @param onChange - Invoked on any change beneath `dir`.
2186
+ * @returns A handle whose `close()` detaches the watcher.
2187
+ * @example
2188
+ * const handle = state.watch("content", () => rebuild());
2189
+ */
2190
+ watch: (dir: string, onChange: () => void) => WatchHandle; /** Static-server factory used by serve()/preview(). Default `Bun.serve`; tests inject a fake. */
2191
+ serveStatic: ServeStaticFunction; /** File-response factory mapping a resolved path + status to a `Response`. Default `Bun.file`. */
2192
+ fileResponse: FileResponseFunction;
2193
+ /**
2194
+ * LAN network-URL deriver for the server-ready panel. Default reads `node:os`
2195
+ * interfaces; tests inject a deterministic value.
2196
+ *
2197
+ * @param port - The port the server is bound to.
2198
+ * @returns The `http://<ip>:<port>` URL, or `null` when offline.
2199
+ * @example
2200
+ * const url = state.networkUrl(4173);
2201
+ */
2202
+ networkUrl: (port: number) => string | null;
2203
+ };
2204
+ /**
2205
+ * Summary returned by `cli.build()` — the awaited `build.run()` result.
2206
+ *
2207
+ * @example
2208
+ * const summary: BuildSummary = { outDir: "dist", pageCount: 12, durationMs: 840 };
2209
+ */
2210
+ type BuildSummary = {
2211
+ /** Resolved output directory the site was written to. */outDir: string; /** Number of route pages rendered. */
2212
+ pageCount: number; /** Total wall-clock duration of the run, in milliseconds. */
2213
+ durationMs: number;
2214
+ };
2215
+ /**
2216
+ * Outcome returned by `cli.deploy()` — either a completed deploy (with details) or a
2217
+ * skipped one. A skip happens only when an interactive TTY user answers "no" at the
2218
+ * confirm prompt (`reason: "declined"`). Non-interactive runs (CI / non-TTY) never
2219
+ * prompt and always proceed, so they never skip — the scripts are CI-safe.
2220
+ *
2221
+ * @example
2222
+ * const outcome: DeployOutcome = { deployed: false, reason: "declined" };
2223
+ */
2224
+ type DeployOutcome = {
2225
+ deployed: true;
2226
+ url: string;
2227
+ deploymentId: string;
2228
+ branch: string;
2229
+ durationMs: number;
2230
+ } | {
2231
+ deployed: false;
2232
+ reason: "declined";
2233
+ };
2234
+ /**
2235
+ * Options for `cli.build()`.
2236
+ *
2237
+ * @example
2238
+ * await app.cli.build({ assertNotFound: false });
2239
+ */
2240
+ type BuildOptions = {
2241
+ /** Assert `outDir/notFoundFile` exists after the build. Defaults to `true`. */assertNotFound?: boolean;
2242
+ };
2243
+ /**
2244
+ * Options for `cli.serve()`.
2245
+ *
2246
+ * @example
2247
+ * await app.cli.serve({ port: 3000 });
2248
+ */
2249
+ type ServeOptions = {
2250
+ /** Port to bind the dev server to. Defaults to `config.port`. */port?: number; /** Reserved for opening the browser on start (not yet implemented). Defaults to `false`. */
2251
+ open?: boolean;
2252
+ };
2253
+ /**
2254
+ * Options for `cli.preview()`.
2255
+ *
2256
+ * @example
2257
+ * await app.cli.preview({ port: 8080 });
2258
+ */
2259
+ type PreviewOptions = {
2260
+ /** Port to bind the preview server to. Defaults to `config.port`. */port?: number;
2261
+ };
2262
+ /**
2263
+ * Options for `cli.deploy()`.
2264
+ *
2265
+ * @example
2266
+ * await app.cli.deploy({ branch: "preview/x", yes: true });
2267
+ */
2268
+ type DeployOptions = {
2269
+ /** Branch to deploy. Defaults to the deploy plugin's production branch. */branch?: string; /** Skip the y/N confirm and deploy immediately. Defaults to `false`. */
2270
+ yes?: boolean;
2271
+ };
2272
+ /**
2273
+ * Public API of the cli plugin (mounted at `app.cli`) — exactly four methods.
2274
+ *
2275
+ * @example
2276
+ * await app.cli.build();
2277
+ */
2278
+ type Api$1 = {
2279
+ /**
2280
+ * Run the SSG build and assert the not-found page exists.
2281
+ *
2282
+ * @param options - Optional `assertNotFound` toggle (default `true`).
2283
+ * @returns The build summary (`outDir`, `pageCount`, `durationMs`).
2284
+ * @throws {Error} `ERR_CLI_NOT_FOUND` when the not-found page is missing and asserted.
2285
+ * @example
2286
+ * const summary = await app.cli.build();
2287
+ */
2288
+ build(options?: BuildOptions): Promise<BuildSummary>;
2289
+ /**
2290
+ * Dev loop: build once, serve `dist/` in-process (live-reload injected), watch
2291
+ * `watchDirs`, debounced rebuild + reload. Resolves when SIGINT/SIGTERM tears down.
2292
+ *
2293
+ * @param options - Optional port override.
2294
+ * @returns Resolves once the server has been torn down.
2295
+ * @example
2296
+ * await app.cli.serve({ port: 3000 });
2297
+ */
2298
+ serve(options?: ServeOptions): Promise<void>;
2299
+ /**
2300
+ * Static preview of the built `dist/` with CF-Pages clean-URL resolution. No
2301
+ * reload injection (mirrors production). Resolves on SIGINT/SIGTERM.
2302
+ *
2303
+ * @param options - Optional port override.
2304
+ * @returns Resolves once the server has been torn down.
2305
+ * @example
2306
+ * await app.cli.preview();
2307
+ */
2308
+ preview(options?: PreviewOptions): Promise<void>;
2309
+ /**
2310
+ * Scaffold, then deploy. A y/N confirm is shown only on an interactive TTY;
2311
+ * non-interactive runs (CI, or any non-TTY) skip the prompt and deploy, so the
2312
+ * consumer scripts never block a pipeline. `options.yes` forces the skip anywhere.
2313
+ *
2314
+ * @param options - Optional branch override and `yes` flag.
2315
+ * @returns The deploy outcome (completed details, or `declined` if a TTY user says no).
2316
+ * @example
2317
+ * await app.cli.deploy({ branch: "preview/x", yes: true });
2318
+ */
2319
+ deploy(options?: DeployOptions): Promise<DeployOutcome>;
2320
+ };
2321
+ //#endregion
2322
+ //#region src/plugins/cli/index.d.ts
2323
+ /**
2324
+ * cli plugin — the node-only developer CLI for `@moku-labs/web`. Mounts exactly four
2325
+ * methods at `app.cli` (`build`/`serve`/`preview`/`deploy`), each rendering through
2326
+ * the boxed Panel UI. Live build/deploy progress rides on hooks over the `build` and
2327
+ * `deploy` plugins' events; there is no argv parser and no `run()` dispatcher — the
2328
+ * consumer drives it from one thin script per command.
2329
+ *
2330
+ * @example Compose the CLI in a consumer app (node-only)
2331
+ * ```ts
2332
+ * import { buildPlugin, cliPlugin, createApp, deployPlugin } from "@moku-labs/web";
2333
+ *
2334
+ * const app = createApp({
2335
+ * plugins: [buildPlugin, deployPlugin, cliPlugin],
2336
+ * pluginConfigs: { cli: { outDir: "dist", port: 4173, watchDirs: ["content", "src"] } }
2337
+ * });
2338
+ * await app.start();
2339
+ * await app.cli.build();
2340
+ * ```
2341
+ */
2342
+ declare const cliPlugin: import("@moku-labs/core").PluginInstance<"cli", Config$1, State$1, Api$1, {}> & Record<never, never>;
2343
+ declare namespace types_d_exports$2 {
2344
+ export { Api, Article, ArticleCard, ComputedFields, Config, ContentApiContext, ContentEvents, ContentProvider, ContentProviderState, FileSystemContentOptions, Frontmatter, State };
2345
+ }
2346
+ /**
2347
+ * YAML frontmatter parsed from each article file.
2348
+ *
2349
+ * @example
2350
+ * ```ts
2351
+ * { title: "Hello", date: "2026-01-15", description: "Intro", tags: [], language: "en" }
2352
+ * ```
2353
+ */
2354
+ type Frontmatter = {
2355
+ /** Article title. Required. */title: string; /** ISO 8601 date string, e.g. "2026-01-15". Required. */
2356
+ date: string; /** Short summary used in cards, feeds, and meta description. Required. */
2357
+ description: string; /** Topic tags. Required (may be empty array). */
2358
+ tags: string[]; /** Source language code of this file. Required. */
2359
+ language: string; /** Draft flag. Excluded from output in production mode. Defaults to false. */
2360
+ draft?: boolean; /** Author name. Falls back to the provider's defaultAuthor when omitted. */
2361
+ author?: string;
2362
+ };
2363
+ /**
2364
+ * Fields computed by the pipeline (not authored in frontmatter).
2365
+ *
2366
+ * @example
2367
+ * ```ts
2368
+ * { slug: "hello", readingTime: 1, contentId: "hello", status: "published", wordCount: 42 }
2369
+ * ```
2370
+ */
2371
+ type ComputedFields = {
2372
+ /** Article directory name. */slug: string; /** Reading time in minutes (ceiling, minimum 1). */
2373
+ readingTime: number; /** Stable content identifier (slug by default). */
2374
+ contentId: string; /** Derived publication status. */
2375
+ status: "published" | "draft"; /** Word count from the source body. */
2376
+ wordCount: number;
2377
+ };
2378
+ /**
2379
+ * A fully processed, render-ready article.
2380
+ *
2381
+ * @example
2382
+ * ```ts
2383
+ * { frontmatter, computed, html: "<p>…</p>", locale: "en", isFallback: false, url: "/en/hello/" }
2384
+ * ```
2385
+ */
2386
+ type Article = {
2387
+ /** Parsed frontmatter. */frontmatter: Frontmatter; /** Pipeline-computed metadata. */
2388
+ computed: ComputedFields; /** Sanitized rendered HTML body. */
2389
+ html: string; /** Locale this Article instance represents (the requested locale, even on fallback). */
2390
+ locale: string; /** True when the default-locale file was used because the requested locale was missing. */
2391
+ isFallback: boolean; /** Canonical URL for this article in this locale. */
2392
+ url: string;
2393
+ };
2394
+ /**
2395
+ * Lightweight projection of Article for cards/lists.
2396
+ *
2397
+ * @example
2398
+ * ```ts
2399
+ * { contentId: "hello", status: "published", title: "Hello", date: "2026-01-15", description: "Intro", tags: [], readingTime: 1, url: "/en/hello/" }
2400
+ * ```
2401
+ */
2402
+ type ArticleCard = {
2403
+ /** Stable content identifier. */contentId: string; /** Derived publication status. */
2404
+ status: "published" | "draft"; /** Article title. */
2405
+ title: string; /** ISO 8601 date string. */
2406
+ date: string; /** Short summary. */
1770
2407
  description: string; /** Topic tags. */
1771
2408
  tags: string[]; /** Reading time in minutes. */
1772
2409
  readingTime: number; /** Canonical URL for this article in this locale. */
1773
2410
  url: string;
1774
2411
  };
2412
+ /**
2413
+ * A pluggable content SOURCE. The shell calls these to read articles; whether content
2414
+ * is read from the filesystem (Node) or some other source is chosen by which provider
2415
+ * you compose — exactly like `env` providers (`dotenv`/`processEnv` vs `browserEnv`).
2416
+ * The shell adds locale fallback, draft filtering, sorting, caching, and events on top.
2417
+ *
2418
+ * @example
2419
+ * ```ts
2420
+ * const provider = fileSystemContent({ contentDir: "./content" });
2421
+ * ```
2422
+ */
2423
+ interface ContentProvider {
2424
+ /** Human-readable provider name, used in diagnostics. */
2425
+ readonly name: string;
2426
+ /** Source directory surfaced via `api.contentDir()` (filesystem providers; "" otherwise). */
2427
+ readonly contentDir: string;
2428
+ /**
2429
+ * Discover the article slugs this provider can supply.
2430
+ *
2431
+ * @returns The provider's slug list.
2432
+ */
2433
+ slugs(): Promise<readonly string[]>;
2434
+ /**
2435
+ * Read + render ONE article for a file-locale; `null` if this provider has no such file.
2436
+ *
2437
+ * @param slug - Article directory name.
2438
+ * @param fileLocale - Locale whose source file is read.
2439
+ * @param outLocale - Locale the resulting Article represents (the requested locale).
2440
+ * @param isFallback - Whether this resolution used the default-locale fallback.
2441
+ * @returns The constructed Article, or `null` when absent.
2442
+ */
2443
+ readArticle(slug: string, fileLocale: string, outLocale: string, isFallback: boolean): Promise<Article | null>;
2444
+ /**
2445
+ * Render a standalone Markdown string to HTML through the provider's pipeline.
2446
+ *
2447
+ * @param markdown - Raw Markdown source.
2448
+ * @returns The rendered HTML.
2449
+ */
2450
+ render(markdown: string): Promise<string>;
2451
+ /**
2452
+ * Optional dev hook: drop cached discovery so stale paths are re-read next time.
2453
+ *
2454
+ * @param paths - Stale file paths.
2455
+ */
2456
+ invalidate?(paths: readonly string[]): void;
2457
+ }
2458
+ /**
2459
+ * Options for the node filesystem provider {@link ContentProvider} `fileSystemContent`.
2460
+ * These are the markdown-pipeline + source concerns that used to live on the content
2461
+ * plugin config; they now belong to the provider you compose.
2462
+ *
2463
+ * @example
2464
+ * ```ts
2465
+ * fileSystemContent({ contentDir: "./content", shikiTheme: "github-dark", defaultAuthor: "Ada" });
2466
+ * ```
2467
+ */
2468
+ type FileSystemContentOptions = {
2469
+ /** Absolute or project-relative path to the content directory. */contentDir: string;
2470
+ /**
2471
+ * SECURITY GATE. When false (the default), rehype-sanitize runs as the final
2472
+ * pipeline step. Set true ONLY for fully author-controlled Markdown.
2473
+ */
2474
+ trustedContent?: boolean; /** Additional remark plugins, concatenated AFTER framework defaults. Defaults to []. */
2475
+ extraRemarkPlugins?: readonly Pluggable[]; /** Additional rehype plugins, concatenated after custom transforms, before Shiki + sanitize. Defaults to []. */
2476
+ extraRehypePlugins?: readonly Pluggable[];
2477
+ /**
2478
+ * Shiki theme for syntax highlighting: a bundled theme NAME (default "github-dark")
2479
+ * or a custom `ThemeRegistration` object. Passed straight through to `@shikijs/rehype`.
2480
+ */
2481
+ shikiTheme?: BundledTheme | ThemeRegistrationAny; /** Author applied to articles whose frontmatter omits author. Defaults to undefined. */
2482
+ defaultAuthor?: string;
2483
+ };
2484
+ /**
2485
+ * Internal mutable state of the filesystem provider: the lazy unified processor and
2486
+ * the discovery caches. Owned by the provider closure, never by the plugin shell.
2487
+ *
2488
+ * @example
2489
+ * ```ts
2490
+ * { processor: null, slugs: null, dirtyPaths: new Set() }
2491
+ * ```
2492
+ */
2493
+ type ContentProviderState = {
2494
+ /** Lazily-created unified processor singleton. null until first render()/readArticle(). */processor: Processor | null; /** Discovered, sorted slug list cached after first disk scan. null until first discovery. */
2495
+ slugs: string[] | null; /** Paths marked stale by invalidate(); next discovery re-reads only these. Starts empty. */
2496
+ dirtyPaths: Set<string>;
2497
+ };
2498
+ /**
2499
+ * Configuration for the content plugin (shell).
2500
+ *
2501
+ * @example
2502
+ * ```ts
2503
+ * { providers: [fileSystemContent({ contentDir: "./content" })] }
2504
+ * ```
2505
+ */
2506
+ type Config = {
2507
+ /**
2508
+ * Ordered content sources. Compose at least one (e.g. `fileSystemContent(...)` on
2509
+ * Node). The first provider that supplies an article for a slug+locale wins;
2510
+ * `slugs()` are unioned. The plugin's own spec default is `[]` (a build must supply one).
2511
+ */
2512
+ providers: ContentProvider[];
2513
+ };
2514
+ /**
2515
+ * Internal mutable state for the content plugin shell: the locale-keyed article cache.
2516
+ *
2517
+ * @example
2518
+ * ```ts
2519
+ * { articles: new Map() }
2520
+ * ```
2521
+ */
2522
+ type State = {
2523
+ /** Article cache keyed locale -> (slug -> Article). Starts empty. */articles: Map<string, Map<string, Article>>;
2524
+ };
1775
2525
  /**
1776
2526
  * Notification-only events emitted by the content plugin.
1777
2527
  *
@@ -1791,24 +2541,24 @@ type ContentEvents = {
1791
2541
  };
1792
2542
  /**
1793
2543
  * Kernel-free domain context handed to createContentApi by the wiring harness.
1794
- * Carries ctx.state (mutable escape hatch), config, global, emit, and the
1795
- * i18n-derived locale/url helpers — so api.ts stays free of createPlugin/ctx.
2544
+ * Carries the shell state (article cache), global flag, emit, the i18n-derived
2545
+ * locale helpers, and the resolved content {@link ContentProvider} — so api.ts stays
2546
+ * free of createPlugin/ctx AND of any node/pipeline import.
1796
2547
  *
1797
2548
  * @example
1798
2549
  * ```ts
1799
- * const apiContext: ContentApiContext = { state, config, global, emit, locales, defaultLocale, articleToUrl };
2550
+ * const apiContext: ContentApiContext = { state, global, emit, locales, defaultLocale, provider };
1800
2551
  * ```
1801
2552
  */
1802
2553
  type ContentApiContext = {
1803
- /** Mutable plugin state (article cache + lazy processor). */state: State$1; /** Resolved plugin configuration. */
1804
- config: Config$1; /** Global framework configuration (mode, etc.). */
2554
+ /** Mutable shell state (article cache). */state: State; /** Global framework configuration (deployment stage). */
1805
2555
  global: {
1806
- mode: "production" | "development";
2556
+ stage: Stage;
1807
2557
  }; /** Emit a registered content event. */
1808
2558
  emit: <K extends keyof ContentEvents>(event: K, payload: ContentEvents[K]) => void; /** Active locale codes from i18n. */
1809
2559
  locales: () => readonly string[]; /** Default locale code from i18n (fallback source). */
1810
- defaultLocale: () => string; /** Build a canonical article URL for a locale + slug. */
1811
- articleToUrl: (locale: string, slug: string) => string;
2560
+ defaultLocale: () => string; /** The resolved content source (merged from `config.providers`). */
2561
+ provider: ContentProvider;
1812
2562
  };
1813
2563
  /**
1814
2564
  * Public API for the content plugin.
@@ -1818,7 +2568,7 @@ type ContentApiContext = {
1818
2568
  * const map = await app.content.loadAll();
1819
2569
  * ```
1820
2570
  */
1821
- type Api$1 = {
2571
+ type Api = {
1822
2572
  /**
1823
2573
  * Load every article across every active locale, returning a locale-keyed
1824
2574
  * map of date-descending Article arrays. Emits content:ready.
@@ -1850,35 +2600,35 @@ type Api$1 = {
1850
2600
  */
1851
2601
  articleToCard(article: Article): ArticleCard;
1852
2602
  /**
1853
- * The configured content source directory (e.g. `"./content"`). Lets the build copy each
1854
- * article's co-located assets (`<contentDir>/<slug>/images/`) into the output so the absolute
1855
- * image URLs the renderer emits resolve.
2603
+ * The configured content source directory (e.g. `"./content"`), from the first
2604
+ * provider. Lets the build copy each article's co-located assets
2605
+ * (`<contentDir>/<slug>/images/`) into the output.
1856
2606
  */
1857
2607
  contentDir(): string;
1858
2608
  };
1859
2609
  //#endregion
1860
2610
  //#region src/plugins/content/index.d.ts
1861
2611
  /**
1862
- * Content plugin — Markdown pipeline: discovers files, parses frontmatter, renders
1863
- * to sanitized HTML (rehype-sanitize unless `trustedContent`), and exposes a
1864
- * locale-keyed Article model. Depends on i18n; emits `content:ready` and
1865
- * `content:invalidated`.
2612
+ * Content plugin (shell) provider-driven locale-keyed Article model. Orchestration
2613
+ * (locale fallback, draft filtering, sort, caching, events) lives here; source I/O +
2614
+ * the Markdown pipeline live in a {@link ContentProvider} you compose (like `env`
2615
+ * providers). The shell imports zero node code, so `contentPlugin` is browser-safe.
2616
+ * Depends on i18n; emits `content:ready` and `content:invalidated`.
1866
2617
  *
1867
- * @example Point at a content directory and pick a Shiki theme
2618
+ * @example Compose the node filesystem provider with a content dir + Shiki theme
1868
2619
  * ```ts
2620
+ * import { contentPlugin, fileSystemContent } from "@moku-labs/web";
1869
2621
  * const app = createApp({
2622
+ * plugins: [contentPlugin],
1870
2623
  * pluginConfigs: {
1871
2624
  * content: {
1872
- * contentDir: "./content",
1873
- * shikiTheme: "github-dark",
1874
- * defaultAuthor: "Ada Lovelace"
1875
- * // trustedContent: true // ONLY for fully author-controlled Markdown — disables sanitize
2625
+ * providers: [fileSystemContent({ contentDir: "./content", shikiTheme: "github-dark", defaultAuthor: "Ada" })]
1876
2626
  * }
1877
2627
  * }
1878
2628
  * });
1879
2629
  * ```
1880
2630
  */
1881
- declare const contentPlugin: import("@moku-labs/core").PluginInstance<"content", Config$1, State$1, Api$1, {
2631
+ declare const contentPlugin: import("@moku-labs/core").PluginInstance<"content", Config, State, Api, {
1882
2632
  "content:ready": {
1883
2633
  locales: readonly string[];
1884
2634
  articleCount: number;
@@ -1887,7 +2637,7 @@ declare const contentPlugin: import("@moku-labs/core").PluginInstance<"content",
1887
2637
  paths: readonly string[];
1888
2638
  };
1889
2639
  }> & Record<never, never>;
1890
- declare namespace types_d_exports$2 {
2640
+ declare namespace types_d_exports$3 {
1891
2641
  export { DataConfig, DataEntry, DataProvider, DataState, DataWriteSummary };
1892
2642
  }
1893
2643
  /**
@@ -1896,13 +2646,13 @@ declare namespace types_d_exports$2 {
1896
2646
  * The `data` plugin is the **agnostic data provider** for the SSG→DATA→SPA pattern.
1897
2647
  * It owns ONE thing: the contract `page path → persisted JSON file`. It knows
1898
2648
  * NOTHING about what the data *is* — no domain types appear here. A route decides
1899
- * its own data shape (`load`'s return) and its own validation (`route.parse`).
2649
+ * its own data shape (`load`'s return).
1900
2650
  *
1901
2651
  * - **Node (build):** `write(entries)` persists one JSON file per page, keyed by
1902
2652
  * the page's URL via {@link DataProvider.fileFor}. `build` supplies the entries
1903
2653
  * (it already expanded the routes), so there is no duplicate expansion here.
1904
- * - **Browser (runtime):** `at(path)` fetches + caches that file as `unknown`; the
1905
- * route's `parse` validates it into the route's data type before `render`.
2654
+ * - **Browser (runtime):** `at(path)` fetches + caches that file as `unknown`, which
2655
+ * the route uses directly as `ctx.data` in `render`.
1906
2656
  *
1907
2657
  * The Node-only file-writing code (`node:fs`) is isolated behind a lazy `import()`
1908
2658
  * inside `write()`, so composing `data` in a browser app keeps the bundle free of
@@ -1924,8 +2674,8 @@ type DataConfig = {
1924
2674
  outputDir: string;
1925
2675
  /**
1926
2676
  * READ side (browser): site-root-relative URL the client fetches the per-page
1927
- * JSON from. A different domain from {@link DataConfig.outputDir} (a filesystem
1928
- * path); keep consistent (`"/" + trim(outputDir) + "/"`). Default `"/_data/"`.
2677
+ * JSON from. The URL-space mirror of {@link DataConfig.outputDir} (a filesystem
2678
+ * path); keep them consistent (`"/" + trim(outputDir) + "/"`). Default `"/_data/"`.
1929
2679
  */
1930
2680
  baseUrl: string;
1931
2681
  };
@@ -1967,15 +2717,15 @@ interface DataState {
1967
2717
  * // Node build (build supplies the entries it already expanded):
1968
2718
  * await app.data.write([{ path: "/en/hello/", data: article }]);
1969
2719
  *
1970
- * // Browser (inside spa nav): fetch the page's data, then route.parse validates it:
1971
- * const raw = await app.data.at("/en/hello/"); // unknown | null
2720
+ * // Browser (inside spa nav): fetch the page's data, used directly as ctx.data:
2721
+ * const raw = await app.data.at("/en/hello/"); // unknown | null (null on failure)
1972
2722
  * ```
1973
2723
  */
1974
2724
  type DataProvider = {
1975
2725
  /**
1976
2726
  * READ (browser) — fetch (and cache) the persisted data for a page path from
1977
- * `config.baseUrl`. Returns the raw parsed JSON as `unknown` (the caller's
1978
- * `route.parse` validates it), or `null` if the fetch/parse fails.
2727
+ * `config.baseUrl`. Returns the raw parsed JSON as `unknown`, used directly as
2728
+ * the route's `ctx.data`; returns `null` if the fetch or JSON parse fails.
1979
2729
  *
1980
2730
  * @param path - The page URL path (e.g. `/en/hello/`).
1981
2731
  * @returns The page's raw data, or `null` on failure.
@@ -2021,197 +2771,19 @@ type DataProvider = {
2021
2771
  * @example
2022
2772
  * ```ts
2023
2773
  * // Node build: `build` calls app.data.write(...) during its pages phase when
2024
- * // router.mode !== "ssg". Just compose the plugin:
2774
+ * // router.mode() !== "ssg". Compose the plugin + set the global render mode:
2775
+ * import * as routes from "./routes";
2025
2776
  * const app = createApp({
2026
2777
  * plugins: [dataPlugin, contentPlugin, buildPlugin],
2027
- * pluginConfigs: { content: { contentDir: "./content" }, router: { routes, mode: "hybrid" } }
2778
+ * config: { mode: "hybrid" },
2779
+ * pluginConfigs: { content: { providers: [fileSystemContent({ contentDir: "./content" })] }, router: { routes } }
2028
2780
  * });
2029
- * await app.start();
2030
- * await app.build.run(); // writes HTML + per-page data sidecars
2781
+ * await app.build.run(); // writes HTML + per-page data sidecars (routes compiled at init)
2031
2782
  *
2032
2783
  * // Browser app: compose `dataPlugin` too; spa fetches via app.data.at(path) on nav.
2033
2784
  * ```
2034
2785
  */
2035
2786
  declare const dataPlugin: import("@moku-labs/core").PluginInstance<"data", DataConfig, DataState, DataProvider, {}> & Record<never, never>;
2036
- declare namespace types_d_exports$3 {
2037
- export { Api, Config, DeployErrorCode, DeployInitOptions, DeployResult, DeployRunOptions, InitResult, SpawnFunction, SpawnOptions, SpawnedProcess, State, WranglerErrorKind };
2038
- }
2039
- /**
2040
- * @file deploy plugin — type definitions.
2041
- */
2042
- /**
2043
- * Options passed to the injected spawner — the subset of Bun.spawn's options the
2044
- * plugin uses (piped stdout/stderr plus an env carrying the API token).
2045
- */
2046
- interface SpawnOptions {
2047
- /** Capture stdout as a readable stream. */
2048
- readonly stdout: "pipe";
2049
- /** Capture stderr as a readable stream. */
2050
- readonly stderr: "pipe";
2051
- /** Subprocess environment — the API token is injected here, never via argv. */
2052
- readonly env?: Record<string, string | undefined>;
2053
- }
2054
- /**
2055
- * The structural subprocess handle the plugin reads back: stdout/stderr streams
2056
- * plus the exit-code promise. Streams are typed `unknown` and narrowed at the read
2057
- * site so this carries no Bun namespace types.
2058
- */
2059
- interface SpawnedProcess {
2060
- /** Standard output stream (narrowed to a ReadableStream at the read site). */
2061
- readonly stdout: unknown;
2062
- /** Standard error stream (narrowed to a ReadableStream at the read site). */
2063
- readonly stderr: unknown;
2064
- /** Resolves with the subprocess exit code. */
2065
- readonly exited: Promise<number>;
2066
- }
2067
- /**
2068
- * The subset of Bun.spawn's signature the plugin relies on (argv array + options).
2069
- * Declared structurally — with NO `import("bun")` namespace types — so it survives
2070
- * `.d.ts` bundling intact and tests can supply a fake spawn without importing Bun.
2071
- */
2072
- type SpawnFunction = (cmd: string[], options: SpawnOptions) => SpawnedProcess;
2073
- /**
2074
- * A deploy error `code` from the wrangler error taxonomy and preflight validators.
2075
- */
2076
- type DeployErrorCode = "ERR_DEPLOY_NO_WRANGLER_CONFIG" | "ERR_DEPLOY_EMPTY_OUTDIR" | "ERR_DEPLOY_TOO_MANY_FILES" | "ERR_DEPLOY_FILE_TOO_LARGE" | "ERR_DEPLOY_PATH_TRAVERSAL" | "ERR_DEPLOY_INVALID_BRANCH" | "ERR_DEPLOY_NO_TOKEN" | "ERR_DEPLOY_PROJECT_NOT_FOUND" | "ERR_DEPLOY_AUTH_EXPIRED" | "ERR_DEPLOY_AUTH" | "ERR_DEPLOY_NETWORK" | "ERR_DEPLOY_WRANGLER_FAILED" | "ERR_DEPLOY_CONFIG";
2077
- /**
2078
- * The subset of wrangler error `code`s classifyWranglerError can produce from a
2079
- * non-zero wrangler exit.
2080
- */
2081
- type WranglerErrorKind = Extract<DeployErrorCode, "ERR_DEPLOY_PROJECT_NOT_FOUND" | "ERR_DEPLOY_AUTH_EXPIRED" | "ERR_DEPLOY_AUTH" | "ERR_DEPLOY_NETWORK" | "ERR_DEPLOY_WRANGLER_FAILED">;
2082
- /**
2083
- * Configuration for the deploy plugin.
2084
- */
2085
- type Config = {
2086
- /**
2087
- * Deploy target. Only Cloudflare Pages is supported in this version.
2088
- * Defaults to `cloudflare-pages`.
2089
- */
2090
- target: "cloudflare-pages";
2091
- /**
2092
- * Directory (relative to project root) containing the built site to deploy.
2093
- * Re-validated against cwd at run() time to block path traversal.
2094
- * Defaults to `dist`.
2095
- */
2096
- outDir: string;
2097
- /**
2098
- * Branch treated as the Cloudflare Pages production branch.
2099
- * Defaults to `main`.
2100
- */
2101
- productionBranch?: string;
2102
- /**
2103
- * Substrings exempt from entropy-gated secret scrubbing in logged output.
2104
- * Defaults to `["CLOUDFLARE_ACCOUNT_ID"]`.
2105
- */
2106
- scrubAllowlist: string[];
2107
- /**
2108
- * Cloudflare compatibility date written into generated wrangler.jsonc.
2109
- * Defaults to `2024-01-01`.
2110
- */
2111
- compatibilityDate?: string;
2112
- /**
2113
- * Whether init() also generates a GitHub Actions workflow.
2114
- * Defaults to `false`.
2115
- */
2116
- ci?: boolean;
2117
- };
2118
- /**
2119
- * Result of a successful deploy.
2120
- */
2121
- type DeployResult = {
2122
- /** The public deployment URL (e.g. https://my-site.pages.dev). */url: string; /** Cloudflare deployment ID parsed from wrangler output. */
2123
- deploymentId: string; /** The branch that was deployed. */
2124
- branch: string; /** Wall-clock duration of the deploy in milliseconds. */
2125
- durationMs: number;
2126
- };
2127
- /**
2128
- * Runtime state for the deploy plugin. Created in createState() and accessed via
2129
- * ctx.state. deploy declares no onStop because nothing here is a long-lived resource.
2130
- */
2131
- type State = {
2132
- /** Result of the most recent successful deploy, or null before the first run. */lastDeployment: DeployResult | null;
2133
- /**
2134
- * Injectable subprocess spawner. Defaults to Bun.spawn. Swapped for a mock in
2135
- * unit tests so wrangler is never actually invoked. Never reassigned at runtime.
2136
- */
2137
- spawn: SpawnFunction;
2138
- };
2139
- /**
2140
- * Options for DeployApi.run.
2141
- */
2142
- type DeployRunOptions = {
2143
- /**
2144
- * Branch to deploy. Defaults to config.productionBranch (or "main"). Must match
2145
- * /^[a-zA-Z0-9/_.-]+$/ — otherwise rejected with ERR_DEPLOY_INVALID_BRANCH.
2146
- */
2147
- branch?: string;
2148
- /**
2149
- * Whether to run the build before deploying. When false, deploys the existing outDir.
2150
- * Defaults to `true`.
2151
- */
2152
- build?: boolean;
2153
- };
2154
- /**
2155
- * Options for DeployApi.init.
2156
- */
2157
- type DeployInitOptions = {
2158
- /** Also generate the GitHub Actions workflow. Defaults to config.ci. */ci?: boolean; /** Drift-only mode: report differences without writing any files. Defaults to `false`. */
2159
- check?: boolean;
2160
- };
2161
- /**
2162
- * Result of an init/scaffold operation.
2163
- */
2164
- type InitResult = {
2165
- /** Paths written this invocation. */written: string[]; /** Paths skipped because they already exist. */
2166
- skipped: string[]; /** In check mode: paths whose on-disk content differs from what would be generated. */
2167
- drifted: string[];
2168
- };
2169
- /**
2170
- * Public API of the deploy plugin (returned from the api factory).
2171
- */
2172
- type Api = {
2173
- /**
2174
- * Deploy the built outDir to Cloudflare Pages via the wrangler subprocess.
2175
- * Runs preflight validators, re-validates the resolved outdir against cwd, guards
2176
- * the branch argument, spawns wrangler (no shell), scrubs all subprocess output
2177
- * before logging, records lastDeployment, and emits deploy:complete.
2178
- *
2179
- * @param options - Optional branch override and build toggle.
2180
- * @returns The deploy result (url, deploymentId, branch, durationMs).
2181
- * @throws {Error} With a `code` from the deploy error taxonomy on any failure.
2182
- * @example
2183
- * const result = await app.deploy.run();
2184
- * console.log(result.url); // https://my-site.pages.dev
2185
- * @example
2186
- * await app.deploy.run({ branch: "preview/landing", build: false });
2187
- */
2188
- run(options?: DeployRunOptions): Promise<DeployResult>;
2189
- /**
2190
- * Return the most recent successful deploy result, or null if none has occurred.
2191
- * The returned object is read-only (a defensive snapshot).
2192
- *
2193
- * @returns The last DeployResult, or null.
2194
- * @example
2195
- * const last = app.deploy.getLastDeployment();
2196
- * if (last) console.log(`Last deployed to ${last.url}`);
2197
- */
2198
- getLastDeployment(): Readonly<DeployResult> | null;
2199
- /**
2200
- * Generate deploy scaffolding: wrangler.jsonc (slug from site.name() + outDir +
2201
- * compatibilityDate) and, when ci is enabled, .github/workflows/deploy.yml. Never
2202
- * overwrites an existing wrangler.jsonc. In check mode, reports drift instead of writing.
2203
- *
2204
- * @param options - Optional ci toggle and check (drift-only) mode.
2205
- * @returns Which files were written, skipped, or would drift.
2206
- * @example
2207
- * const out = await app.deploy.init({ ci: true });
2208
- * // out.written -> ["wrangler.jsonc", ".github/workflows/deploy.yml"]
2209
- * @example
2210
- * const drift = await app.deploy.init({ check: true });
2211
- * if (drift.drifted.length) process.exit(1);
2212
- */
2213
- init(options?: DeployInitOptions): Promise<InitResult>;
2214
- };
2215
2787
  //#endregion
2216
2788
  //#region src/plugins/deploy/index.d.ts
2217
2789
  /**
@@ -2239,7 +2811,7 @@ type Api = {
2239
2811
  * });
2240
2812
  * ```
2241
2813
  */
2242
- declare const deployPlugin: import("@moku-labs/core").PluginInstance<"deploy", Config, State, Api, {
2814
+ declare const deployPlugin: import("@moku-labs/core").PluginInstance<"deploy", Config$2, State$2, Api$2, {
2243
2815
  "deploy:complete": {
2244
2816
  url: string;
2245
2817
  deploymentId: string;
@@ -2320,6 +2892,8 @@ declare function feedLink(title: string, url: string, type?: string): HeadElemen
2320
2892
  * modified times, author, section, tags, plus a JSON-LD `Article` block and canonical.
2321
2893
  *
2322
2894
  * @param articleMeta - Article metadata (title, description, author, dates, tags, image…).
2895
+ * `image`, when present, is pushed to `og:image` verbatim and must therefore be
2896
+ * an absolute URL (this helper does not resolve relative paths against the site).
2323
2897
  * @param canonicalUrl - The article's canonical absolute URL.
2324
2898
  * @returns An ordered array of serializable head elements.
2325
2899
  * @example buildArticleHead({ title: "Hi", author: "A", published: "2026-01-01" }, "https://x/p")
@@ -2346,7 +2920,7 @@ declare function buildArticleHead(articleMeta: ArticleMeta, canonicalUrl: string
2346
2920
  * });
2347
2921
  * ```
2348
2922
  */
2349
- declare const headPlugin: import("@moku-labs/core").PluginInstance<"head", Config$3, State$3, Api$3, {}> & {
2923
+ declare const headPlugin: import("@moku-labs/core").PluginInstance<"head", Config$4, State$4, Api$4, {}> & {
2350
2924
  meta: typeof meta;
2351
2925
  og: typeof og;
2352
2926
  twitter: typeof twitter;
@@ -2377,7 +2951,7 @@ declare const headPlugin: import("@moku-labs/core").PluginInstance<"head", Confi
2377
2951
  * });
2378
2952
  * ```
2379
2953
  */
2380
- declare const i18nPlugin: import("@moku-labs/core").PluginInstance<"i18n", Config$5, Record<string, never>, Api$5, {}> & Record<never, never>;
2954
+ declare const i18nPlugin: import("@moku-labs/core").PluginInstance<"i18n", Config$6, Record<string, never>, Api$6, {}> & Record<never, never>;
2381
2955
  //#endregion
2382
2956
  //#region src/plugins/log/index.d.ts
2383
2957
  /**
@@ -2402,7 +2976,7 @@ declare const logPlugin: import("@moku-labs/core").CorePluginInstance<"log", Log
2402
2976
  * @example
2403
2977
  * ```ts
2404
2978
  * route("/{lang:?}/{slug}/")
2405
- * .load(({ slug }) => loadArticle(slug))
2979
+ * .load((ctx) => loadArticle(ctx.params.slug))
2406
2980
  * .render((ctx) => <Article a={ctx.data} />)
2407
2981
  * .head((ctx) => ({ title: ctx.data.title }));
2408
2982
  * ```
@@ -2420,31 +2994,46 @@ declare function route<P extends string>(pattern: P): RouteBuilder<RouteState<P>
2420
2994
  * ```
2421
2995
  */
2422
2996
  declare function defineRoutes<T extends RouteMap>(routes: T): T;
2997
+ /**
2998
+ * Build a pure, app-free URL builder from a route map. `toUrl(name, params)` resolves
2999
+ * a route's path by pattern substitution using the SAME `buildUrl` as the runtime
3000
+ * `RouterApi.toUrl`, so the helper and the API can never diverge. It needs no running
3001
+ * app, router instance, base URL, or i18n — just the route map the consumer already
3002
+ * holds at module scope. So components, layouts, and hydrated islands import it
3003
+ * directly: no `app.router` reference, no manual "bind", no module global, no
3004
+ * "not bound" guard, and no createApp ↔ routes cycle.
3005
+ *
3006
+ * @param routes - The route map (typically the value returned by {@link defineRoutes}).
3007
+ * @returns A {@link Urls} builder whose `toUrl` accepts only this map's route names.
3008
+ * @example
3009
+ * ```ts
3010
+ * const url = createUrls(routes);
3011
+ * url.toUrl("article", { lang: "en", slug: "hello" }); // "/en/hello/"
3012
+ * ```
3013
+ */
3014
+ declare function createUrls<T extends RouteMap>(routes: T): Urls<T>;
2423
3015
  //#endregion
2424
3016
  //#region src/plugins/router/index.d.ts
2425
3017
  /**
2426
3018
  * Router plugin — typed, named route definitions with locale-aware URL generation
2427
- * and matching. Author routes with {@link route} + {@link defineRoutes}. Depends
2428
- * on site (base URL) and i18n (locales).
3019
+ * and matching. Author routes with {@link route}, then register them the normal config
3020
+ * way via `pluginConfigs.router.routes` (compiled at init). Depends on site (base URL)
3021
+ * and i18n (locales).
2429
3022
  *
2430
- * @example Define routes and choose a render mode
3023
+ * @example Register routes via config, then start/build
2431
3024
  * ```ts
3025
+ * import * as routes from "./routes";
2432
3026
  * const app = createApp({
2433
- * pluginConfigs: {
2434
- * router: {
2435
- * routes: defineRoutes({
2436
- * home: route("/"),
2437
- * article: route("/blog/{slug}/")
2438
- * }),
2439
- * mode: "hybrid" // "ssg" | "spa" | "hybrid" (default)
2440
- * }
2441
- * }
3027
+ * config: { mode: "hybrid" }, // render mode is GLOBAL config
3028
+ * pluginConfigs: { router: { routes } } // declarative route map (a namespace works)
2442
3029
  * });
3030
+ * await app.build.run(); // or: await app.start(); — routes compiled at init
2443
3031
  * ```
2444
3032
  */
2445
3033
  declare const routerPlugin: import("@moku-labs/core").PluginInstance<"router", RouterConfig, RouterState, RouterApi, {}> & {
2446
3034
  route: typeof route;
2447
3035
  defineRoutes: typeof defineRoutes;
3036
+ createUrls: typeof createUrls;
2448
3037
  };
2449
3038
  //#endregion
2450
3039
  //#region src/plugins/site/index.d.ts
@@ -2467,7 +3056,7 @@ declare const routerPlugin: import("@moku-labs/core").PluginInstance<"router", R
2467
3056
  * });
2468
3057
  * ```
2469
3058
  */
2470
- declare const sitePlugin: import("@moku-labs/core").PluginInstance<"site", Config$6, Record<string, never>, Api$6, {}> & Record<never, never>;
3059
+ declare const sitePlugin: import("@moku-labs/core").PluginInstance<"site", Config$7, Record<string, never>, Api$7, {}> & Record<never, never>;
2471
3060
  //#endregion
2472
3061
  //#region src/plugins/spa/components.d.ts
2473
3062
  /**
@@ -2568,6 +3157,20 @@ declare function processEnv(): EnvProvider;
2568
3157
  */
2569
3158
  declare function cloudflareBindings(): EnvProvider;
2570
3159
  //#endregion
3160
+ //#region src/plugins/content/providers.d.ts
3161
+ /**
3162
+ * The node filesystem content provider: reads + renders Markdown from `contentDir`
3163
+ * through the full pipeline. Caches discovery + the unified processor internally.
3164
+ *
3165
+ * @param options - Filesystem + pipeline options (`contentDir`, `shikiTheme`, `trustedContent`, …).
3166
+ * @returns A {@link ContentProvider} backed by the local filesystem.
3167
+ * @example
3168
+ * ```ts
3169
+ * createApp({ pluginConfigs: { content: { providers: [fileSystemContent({ contentDir: "./content" })] } } });
3170
+ * ```
3171
+ */
3172
+ declare function fileSystemContent(options: FileSystemContentOptions): ContentProvider;
3173
+ //#endregion
2571
3174
  //#region src/index.d.ts
2572
3175
  /**
2573
3176
  * Create and initialize a `@moku-labs/web` application — the Layer-3 entry point.
@@ -2580,28 +3183,29 @@ declare function cloudflareBindings(): EnvProvider;
2580
3183
  *
2581
3184
  * @param options - Optional configuration:
2582
3185
  * - `pluginConfigs` — per-plugin overrides, keyed by plugin name.
2583
- * - `config` — global framework config (e.g. `{ mode: "development" }`).
3186
+ * - `config` — global framework config (e.g. `{ mode: "spa" }`).
2584
3187
  * - `plugins` — extra plugins (Node-only built-ins or your own) merged into the app and its type.
2585
3188
  * - `onReady` / `onError` / `onStart` / `onStop` — lifecycle callbacks.
2586
3189
  * @returns The initialized app: `start()`, `stop()`, every plugin's API, and `log`.
2587
3190
  * @example
2588
3191
  * ```ts
2589
- * // Node SSG build — add the node-only plugins:
3192
+ * // Node SSG build — add the node-only plugins, register routes via config, then build:
3193
+ * import * as routes from "./routes";
2590
3194
  * const app = createApp({
2591
3195
  * plugins: [contentPlugin, buildPlugin, deployPlugin],
2592
3196
  * pluginConfigs: {
2593
3197
  * site: { name: "My Blog", url: "https://blog.dev", author: "Ada", description: "Notes" },
2594
- * router: { routes: defineRoutes({ home: route("/"), post: route("/blog/{slug}/") }) }
3198
+ * router: { routes }
2595
3199
  * }
2596
3200
  * });
2597
- * await app.start();
2598
- * await app.build.run();
3201
+ * await app.build.run(); // routes compiled at init
2599
3202
  * ```
2600
3203
  */
2601
- 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, {}> & {
3204
+ declare const createApp: <const ExtraPlugins extends readonly import("@moku-labs/core").AnyPluginInstance[] = readonly []>(options?: import("@moku-labs/core").CreateAppOptions<Config$8, Events, (import("@moku-labs/core").PluginInstance<"site", Config$7, Record<string, never>, Api$7, {}> & Record<never, never>) | (import("@moku-labs/core").PluginInstance<"i18n", Config$6, Record<string, never>, Api$6, {}> & Record<never, never>) | (import("@moku-labs/core").PluginInstance<"router", RouterConfig, RouterState, RouterApi, {}> & {
2602
3205
  route: typeof route;
2603
3206
  defineRoutes: typeof defineRoutes;
2604
- }) | (import("@moku-labs/core").PluginInstance<"head", Config$3, State$3, Api$3, {}> & {
3207
+ createUrls: typeof createUrls;
3208
+ }) | (import("@moku-labs/core").PluginInstance<"head", Config$4, State$4, Api$4, {}> & {
2605
3209
  meta: typeof meta;
2606
3210
  og: typeof og;
2607
3211
  twitter: typeof twitter;
@@ -2626,10 +3230,11 @@ declare const createApp: <const ExtraPlugins extends readonly import("@moku-labs
2626
3230
  name: string;
2627
3231
  el: Element;
2628
3232
  };
2629
- }> & 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, {}> & {
3233
+ }> & 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$8, Events, (import("@moku-labs/core").PluginInstance<"site", Config$7, Record<string, never>, Api$7, {}> & Record<never, never>) | (import("@moku-labs/core").PluginInstance<"i18n", Config$6, Record<string, never>, Api$6, {}> & Record<never, never>) | (import("@moku-labs/core").PluginInstance<"router", RouterConfig, RouterState, RouterApi, {}> & {
2630
3234
  route: typeof route;
2631
3235
  defineRoutes: typeof defineRoutes;
2632
- }) | (import("@moku-labs/core").PluginInstance<"head", Config$3, State$3, Api$3, {}> & {
3236
+ createUrls: typeof createUrls;
3237
+ }) | (import("@moku-labs/core").PluginInstance<"head", Config$4, State$4, Api$4, {}> & {
2633
3238
  meta: typeof meta;
2634
3239
  og: typeof og;
2635
3240
  twitter: typeof twitter;
@@ -2670,6 +3275,6 @@ declare const createApp: <const ExtraPlugins extends readonly import("@moku-labs
2670
3275
  * const app = createApp({ plugins: [analytics] });
2671
3276
  * ```
2672
3277
  */
2673
- 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>]>>;
3278
+ declare const createPlugin: import("@moku-labs/core").BoundCreatePluginFunction<Config$8, Events, import("@moku-labs/core").CoreApisFromTuple<[import("@moku-labs/core").CorePluginInstance<"log", LogConfig, LogState, LogApi>, import("@moku-labs/core").CorePluginInstance<"env", EnvConfig, EnvState, EnvApi>]>>;
2674
3279
  //#endregion
2675
- 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, createComponent, createPlugin, dataPlugin, defineRoutes, deployPlugin, dotenv, envPlugin, feedLink, headPlugin, hreflang, i18nPlugin, jsonLd, logPlugin, meta, og, processEnv, route, routerPlugin, sitePlugin, spaPlugin, twitter };
3280
+ export { types_d_exports as Build, types_d_exports$1 as Cli, types_d_exports$2 as Content, types_d_exports$3 as Data, types_d_exports$4 as Deploy, types_d_exports$5 as Env, types_d_exports$6 as Head, types_d_exports$7 as Log, types_d_exports$8 as Router, types_d_exports$9 as Spa, browserEnv, buildArticleHead, buildPlugin, canonical, cliPlugin, cloudflareBindings, contentPlugin, createApp, createComponent, createPlugin, createUrls, dataPlugin, defineRoutes, deployPlugin, dotenv, envPlugin, feedLink, fileSystemContent, headPlugin, hreflang, i18nPlugin, jsonLd, logPlugin, meta, og, processEnv, route, routerPlugin, sitePlugin, spaPlugin, twitter };