@moku-labs/web 0.4.1 → 0.5.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.
@@ -0,0 +1,1852 @@
1
+ import { EmitFn } from "@moku-labs/core";
2
+ import { ComponentChildren, VNode } from "preact";
3
+
4
+ //#region src/plugins/log/types.d.ts
5
+ declare namespace types_d_exports$3 {
6
+ export { ExpectChain, LogApi, LogConfig, LogEntry, LogLevel, LogSink, LogState };
7
+ }
8
+ /**
9
+ * @file log plugin — type definitions skeleton.
10
+ *
11
+ * Core-plugin type surface: config, state, public API, and the supporting
12
+ * value/sink/assertion-chain types. These types are inferred onto the plugin
13
+ * via state.ts / api.ts; index.ts passes NO explicit generics.
14
+ */
15
+ /**
16
+ * Runtime mode for the log plugin. Selects which default sinks are installed at
17
+ * onInit. The in-memory trace sink is ALWAYS installed regardless of mode.
18
+ *
19
+ * - "test" — no console sink (keeps test output clean); trace only.
20
+ * - "silent" — no console sink (explicit quiet); trace only.
21
+ * - "dev" — console sink + trace.
22
+ * - "production" — console sink + trace.
23
+ */
24
+ type LogConfig = {
25
+ /** Sink-selection mode. Defaults to `production`. */mode: "test" | "dev" | "production" | "silent";
26
+ };
27
+ /** Severity level for a log entry. */
28
+ type LogLevel = "debug" | "info" | "warn" | "error";
29
+ /**
30
+ * A single recorded log entry.
31
+ */
32
+ type LogEntry = {
33
+ /** Severity level. */level: LogLevel; /** Event identifier (free-form string; convention: `domain:action`). */
34
+ event: string; /** Optional structured payload associated with the event. */
35
+ data?: unknown; /** Capture timestamp in epoch milliseconds (`Date.now()` at append time). */
36
+ ts: number; /** Optional originating plugin name. Reserved for future enrichment. */
37
+ plugin?: string;
38
+ };
39
+ /**
40
+ * Pluggable output target. Implement this to add console/file/JSON/etc. sinks
41
+ * WITHOUT changing the log API. Each logged entry is passed to `write` once,
42
+ * in registration order.
43
+ */
44
+ type LogSink = {
45
+ /**
46
+ * Write a single entry to this sink.
47
+ *
48
+ * @param entry - The entry to emit.
49
+ */
50
+ write(entry: LogEntry): void;
51
+ };
52
+ /**
53
+ * Fluent event-trace assertion chain. Reads the live entries array on each call,
54
+ * so assertions reflect the trace state at call time (not chain-creation time).
55
+ * Every method returns the same chain for fluent chaining; assertion failures throw.
56
+ */
57
+ type ExpectChain = {
58
+ /**
59
+ * Assert at least one entry has `event`, optionally matching `partial` (subset match).
60
+ *
61
+ * @param event - Event name to find.
62
+ * @param partial - Optional partial data shape (subset-matched against `entry.data`).
63
+ * @returns The same chain for chaining.
64
+ * @throws {Error} `LogExpectAssertionError` when no matching entry exists.
65
+ */
66
+ toHaveEvent(event: string, partial?: Record<string, unknown>): ExpectChain;
67
+ /**
68
+ * Assert all of `events` appear in the trace in the given relative order
69
+ * (gaps allowed; later events must occur after earlier ones).
70
+ *
71
+ * @param events - Ordered list of event names.
72
+ * @returns The same chain for chaining.
73
+ * @throws {Error} `LogExpectAssertionError` when the ordering cannot be satisfied.
74
+ */
75
+ toHaveEventInOrder(events: string[]): ExpectChain;
76
+ /**
77
+ * Assert NO entry has `event` (optionally narrowed by `partial`).
78
+ *
79
+ * @param event - Event name that must be absent.
80
+ * @param partial - Optional partial data shape; only matching entries violate the assertion.
81
+ * @returns The same chain for chaining.
82
+ * @throws {Error} `LogExpectAssertionError` when a matching entry exists.
83
+ */
84
+ toNotHaveEvent(event: string, partial?: Record<string, unknown>): ExpectChain;
85
+ };
86
+ /**
87
+ * Internal mutable state for the log plugin. Created fresh per createApp construction.
88
+ */
89
+ type LogState = {
90
+ /** Append-only ordered trace of every logged entry (the in-memory trace sink's backing store). */entries: LogEntry[]; /** Registered output sinks. Each entry is written to every sink in order. */
91
+ sinks: LogSink[];
92
+ };
93
+ /** Public log API injected as `ctx.log` on every regular plugin and exposed as `app.log`. */
94
+ type LogApi = {
95
+ /**
96
+ * Append an `info` entry and fan it out to every sink.
97
+ *
98
+ * @param event - Event identifier (convention: `domain:action`).
99
+ * @param data - Optional structured payload.
100
+ */
101
+ info(event: string, data?: unknown): void;
102
+ /**
103
+ * Append a `debug` entry and fan it out to every sink.
104
+ *
105
+ * @param event - Event identifier (convention: `domain:action`).
106
+ * @param data - Optional structured payload.
107
+ */
108
+ debug(event: string, data?: unknown): void;
109
+ /**
110
+ * Append a `warn` entry and fan it out to every sink.
111
+ *
112
+ * @param event - Event identifier (convention: `domain:action`).
113
+ * @param data - Optional structured payload.
114
+ */
115
+ warn(event: string, data?: unknown): void;
116
+ /**
117
+ * Append an `error` entry. When `error` is provided, its `message`/`stack` are
118
+ * merged into `data` under an `error` key; otherwise `data` is recorded as-is.
119
+ *
120
+ * @param event - Event identifier (convention: `domain:action`).
121
+ * @param data - Optional structured payload.
122
+ * @param error - Optional originating Error to merge into `data`.
123
+ */
124
+ error(event: string, data?: unknown, error?: Error): void;
125
+ /**
126
+ * Return a frozen snapshot of the entries recorded so far (a fresh copy).
127
+ *
128
+ * @returns A readonly, frozen copy of the recorded entries.
129
+ */
130
+ trace(): readonly LogEntry[];
131
+ /**
132
+ * Return a fluent assertion chain bound to the live entries array.
133
+ *
134
+ * @returns A fresh {@link ExpectChain}.
135
+ */
136
+ expect(): ExpectChain;
137
+ /**
138
+ * Register an additional output sink at runtime.
139
+ *
140
+ * @param sink - The sink to add to the fan-out list.
141
+ */
142
+ addSink(sink: LogSink): void; /** Clear all recorded entries while keeping registered sinks. */
143
+ reset(): void;
144
+ };
145
+ declare namespace types_d_exports$1 {
146
+ export { EnvApi, EnvConfig, EnvProvider, EnvState, EnvVarSpec };
147
+ }
148
+ /**
149
+ * @file env plugin — public + boundary type definitions.
150
+ */
151
+ /**
152
+ * A source of raw environment values.
153
+ *
154
+ * Providers are walked in array order during resolution; the first provider to
155
+ * return a non-`undefined` (and non-empty-string) value for a key wins. `load()`
156
+ * is called once per resolution at `onInit` time — except for live, per-request
157
+ * sources (e.g. {@link cloudflareBindings}) which read fresh on every call.
158
+ *
159
+ * @example
160
+ * ```ts
161
+ * const custom: EnvProvider = {
162
+ * name: "vault",
163
+ * load: () => ({ DB_URL: readVaultSecret("db") })
164
+ * };
165
+ * ```
166
+ */
167
+ interface EnvProvider {
168
+ /** Human-readable provider name, used in diagnostics and error messages. */
169
+ name: string;
170
+ /**
171
+ * Reads this provider's current view of the environment.
172
+ *
173
+ * @returns A flat record of variable names to string values. Keys the
174
+ * provider cannot supply must be omitted or set to `undefined`.
175
+ */
176
+ load(): Record<string, string | undefined>;
177
+ }
178
+ /**
179
+ * Declares how a single environment variable is validated and exposed.
180
+ *
181
+ * @example
182
+ * ```ts
183
+ * const port: EnvVarSpec = { public: false, required: false, default: "3000" };
184
+ * const apiBase: EnvVarSpec = { public: true }; // key must start with PUBLIC_
185
+ * const token: EnvVarSpec = { public: false, required: true, secret: true };
186
+ * ```
187
+ */
188
+ interface EnvVarSpec {
189
+ /**
190
+ * Whether the variable is safe to ship to the browser. When `true`, the key
191
+ * **must** start with {@link EnvConfig.publicPrefix} (cross-checked at
192
+ * `onInit`), and the variable is included in {@link EnvApi.getPublicMap}.
193
+ */
194
+ public: boolean;
195
+ /** Whether resolution fails if the variable is still undefined after defaults. */
196
+ required?: boolean;
197
+ /** Value applied when no provider supplies the variable. */
198
+ default?: string;
199
+ /**
200
+ * Marks the variable as a secret for documentation / tooling. Has no runtime
201
+ * effect on resolution, but secrets are never permitted to be `public`.
202
+ */
203
+ secret?: boolean;
204
+ }
205
+ /**
206
+ * Configuration for the {@link envPlugin} core plugin.
207
+ *
208
+ * @example
209
+ * ```ts
210
+ * createCoreConfig("web", {
211
+ * plugins: [envPlugin],
212
+ * pluginConfigs: {
213
+ * env: {
214
+ * schema: {
215
+ * PUBLIC_API_URL: { public: true, default: "/api" },
216
+ * SESSION_SECRET: { public: false, required: true, secret: true }
217
+ * }
218
+ * }
219
+ * }
220
+ * });
221
+ * ```
222
+ */
223
+ type EnvConfig = {
224
+ /** Per-variable validation + exposure rules, keyed by variable name. */schema: Record<string, EnvVarSpec>;
225
+ /**
226
+ * Ordered list of value sources. The first provider yielding a non-`undefined`
227
+ * (and non-empty-string) value for a key wins. The plugin's own spec default is
228
+ * `[]`; the framework layer supplies the working default `[dotenv(), processEnv()]`.
229
+ */
230
+ providers: EnvProvider[];
231
+ /**
232
+ * Prefix that public variable names must carry. Bidirectionally enforced at
233
+ * `onInit`. Framework default is `"PUBLIC_"`.
234
+ */
235
+ publicPrefix: string;
236
+ };
237
+ /**
238
+ * Internal env plugin state: the resolved variable table and its public subset.
239
+ * Both maps are populated and frozen (via `freezeMap`) during `onInit`.
240
+ *
241
+ * Exported only to type the `createState` / `api` / `validate` boundary —
242
+ * consumers use {@link EnvApi}, never `EnvState`.
243
+ */
244
+ interface EnvState {
245
+ /** All validated variables that resolved to a defined value (incl. defaults). */
246
+ resolved: Map<string, string>;
247
+ /** Subset of `resolved` where `schema[key].public === true`. */
248
+ publicMap: Map<string, string>;
249
+ }
250
+ /**
251
+ * The resolved-environment accessor mounted at `ctx.env`. Built by the plugin's
252
+ * `api` factory over `ctx.state` ({@link EnvState}).
253
+ *
254
+ * Available after `onInit` (i.e. inside any plugin's lifecycle and in consumer
255
+ * code). All accessors read from the frozen `resolved` / `publicMap` maps;
256
+ * mutation is impossible.
257
+ *
258
+ * @example
259
+ * ```ts
260
+ * const url = ctx.env.get("PUBLIC_API_URL"); // string | undefined
261
+ * const token = ctx.env.require("DEPLOY_TOKEN"); // string, or throws
262
+ * ```
263
+ */
264
+ type EnvApi = {
265
+ /**
266
+ * Reads a resolved variable.
267
+ *
268
+ * @param key - Variable name.
269
+ * @returns The value, or `undefined` if not present / not in schema.
270
+ */
271
+ get(key: string): string | undefined;
272
+ /**
273
+ * Reads a variable that must exist.
274
+ *
275
+ * @param key - Variable name.
276
+ * @returns The value.
277
+ * @throws {Error} If the variable is undefined.
278
+ */
279
+ require(key: string): string;
280
+ /**
281
+ * Tests presence of a resolved variable.
282
+ *
283
+ * @param key - Variable name.
284
+ * @returns `true` if a value is present.
285
+ */
286
+ has(key: string): boolean;
287
+ /**
288
+ * Returns all public variables as a frozen plain object — convenient for
289
+ * spreading into a serializable payload.
290
+ *
291
+ * @returns A frozen `Record` of public variable names to values.
292
+ */
293
+ getPublic(): Readonly<Record<string, string>>;
294
+ /**
295
+ * Returns the frozen map of public variables. This is the **sole** intended
296
+ * input to a build-time `define` injection: every entry is safe to inline
297
+ * into the browser bundle.
298
+ *
299
+ * @returns The frozen public map.
300
+ */
301
+ getPublicMap(): ReadonlyMap<string, string>;
302
+ };
303
+ //#endregion
304
+ //#region src/plugins/env/providers.browser.d.ts
305
+ /**
306
+ * A browser-safe {@link EnvProvider} that reads `import.meta.env` and an optional
307
+ * `globalThis[globalKey]` snapshot, merging them with the runtime global winning.
308
+ * Contains zero `node:*` imports, so it is safe to include in the client bundle.
309
+ * Never throws on missing sources — each absent source resolves to `{}`.
310
+ *
311
+ * @param options - Optional settings.
312
+ * @param options.globalKey - `globalThis` key to read a public-env snapshot from. Defaults to `"__ENV__"`.
313
+ * @returns An {@link EnvProvider} named `browser-env`.
314
+ * @example
315
+ * ```ts
316
+ * const provider = browserEnv();
317
+ * provider.load(); // { PUBLIC_API_URL: "/api", ... }
318
+ * ```
319
+ */
320
+ declare function browserEnv(options?: {
321
+ globalKey?: string;
322
+ }): EnvProvider;
323
+ //#endregion
324
+ //#region src/plugins/env/index.d.ts
325
+ /**
326
+ * Core plugin that resolves, validates, and freezes the environment at `onInit`,
327
+ * exposing a read-only accessor at `ctx.env`. No `onStart`/`onStop` — holds no resource.
328
+ *
329
+ * @example
330
+ * ```ts
331
+ * createApp({ pluginConfigs: { env: { schema: { PUBLIC_API_URL: { public: true } } } } });
332
+ * ```
333
+ */
334
+ declare const envPlugin: import("@moku-labs/core").CorePluginInstance<"env", EnvConfig, EnvState, EnvApi>;
335
+ //#endregion
336
+ //#region src/config.d.ts
337
+ /**
338
+ * Global framework configuration. Minimal by design — per-plugin config is
339
+ * resolved via `pluginConfigs`, not merged here.
340
+ */
341
+ type Config$4 = {
342
+ /** Runtime mode. Drives log sink defaults, content draft filtering, build minify. */mode: "production" | "development";
343
+ };
344
+ /**
345
+ * Framework event contract. Empty base — each plugin declares its own events
346
+ * via the `events` register callback (spec/14 §2).
347
+ */
348
+ type Events = {};
349
+ //#endregion
350
+ //#region src/plugins/site/types.d.ts
351
+ /**
352
+ * @file site plugin — public type definitions (Config + Api).
353
+ */
354
+ /**
355
+ * Configuration for the site plugin — global, frozen site metadata.
356
+ *
357
+ * All four fields are required at runtime. The framework ships empty-string
358
+ * defaults and `onInit` fails fast (at `createApp`) if `name` is blank or
359
+ * `url` is not a valid absolute URL. Consumers MUST supply real values via
360
+ * `pluginConfigs.site`.
361
+ *
362
+ * @example
363
+ * ```ts
364
+ * createApp({
365
+ * pluginConfigs: {
366
+ * site: {
367
+ * name: "My Blog",
368
+ * url: "https://blog.dev",
369
+ * author: "Alex",
370
+ * description: "A personal blog about web frameworks."
371
+ * }
372
+ * }
373
+ * });
374
+ * ```
375
+ */
376
+ type Config$3 = {
377
+ /** 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). */
378
+ url: string; /** Default author/byline for the site. Used in feeds and article author meta. */
379
+ author: string; /** Short site description. Used in feeds, the default meta description, and og:description fallbacks. */
380
+ description: string;
381
+ };
382
+ /**
383
+ * Public API of the site plugin — read-only accessors over frozen global
384
+ * site metadata, plus canonical URL construction.
385
+ */
386
+ type Api$3 = {
387
+ /**
388
+ * Returns the configured site name.
389
+ *
390
+ * @returns {string} The human-readable site name from `config.name`.
391
+ * @example
392
+ * ```ts
393
+ * app.site.name(); // "My Blog"
394
+ * ```
395
+ */
396
+ name: () => string;
397
+ /**
398
+ * Returns the configured absolute base URL of the site.
399
+ *
400
+ * @returns {string} The base URL from `config.url`, e.g. "https://blog.dev".
401
+ * @example
402
+ * ```ts
403
+ * app.site.url(); // "https://blog.dev"
404
+ * ```
405
+ */
406
+ url: () => string;
407
+ /**
408
+ * Returns the configured site author/byline.
409
+ *
410
+ * @returns {string} The author from `config.author`.
411
+ * @example
412
+ * ```ts
413
+ * app.site.author(); // "Alex"
414
+ * ```
415
+ */
416
+ author: () => string;
417
+ /**
418
+ * Returns the configured site description.
419
+ *
420
+ * @returns {string} The description from `config.description`.
421
+ * @example
422
+ * ```ts
423
+ * app.site.description(); // "A personal blog about web frameworks."
424
+ * ```
425
+ */
426
+ description: () => string;
427
+ /**
428
+ * Joins a path against the configured base `url` to produce an absolute
429
+ * canonical URL. An empty path (or "/") returns the base URL unchanged.
430
+ *
431
+ * @param {string} path - Relative path for the page, e.g. "/about/" or "blog/post/".
432
+ * @returns {string} The absolute canonical URL, e.g. "https://blog.dev/about/".
433
+ * @example
434
+ * ```ts
435
+ * app.site.canonical("/about/"); // "https://blog.dev/about/"
436
+ * app.site.canonical("/"); // "https://blog.dev"
437
+ * ```
438
+ */
439
+ canonical: (path: string) => string;
440
+ };
441
+ //#endregion
442
+ //#region src/plugins/i18n/types.d.ts
443
+ /**
444
+ * @file i18n plugin — public type definitions (Config + Api).
445
+ */
446
+ /**
447
+ * i18n plugin configuration. Mirrors the legacy `I18nConfig` shape.
448
+ *
449
+ * `locales` and `defaultLocale` are required and validated in `onInit`. The
450
+ * optional maps default to empty objects so every lookup method is total —
451
+ * lookups return `undefined` on a miss and `t()` falls back to the key.
452
+ *
453
+ * @example
454
+ * ```ts
455
+ * {
456
+ * locales: ["en", "uk"],
457
+ * defaultLocale: "en",
458
+ * localeNames: { en: "English", uk: "Українська" },
459
+ * ogLocaleMap: { en: "en_US", uk: "uk_UA" },
460
+ * translations: { en: { "nav.home": "Home" }, uk: { "nav.home": "Головна" } }
461
+ * }
462
+ * ```
463
+ */
464
+ type Config$2 = {
465
+ readonly locales: readonly string[];
466
+ readonly defaultLocale: string;
467
+ readonly localeNames?: Record<string, string>;
468
+ readonly ogLocaleMap?: Record<string, string>;
469
+ readonly translations?: Record<string, Record<string, string>>;
470
+ };
471
+ /**
472
+ * Public API of the i18n plugin. Injected as `app.i18n` and reachable from
473
+ * other plugins via `ctx.require(i18nPlugin)`.
474
+ */
475
+ type Api$2 = {
476
+ /**
477
+ * Returns the configured supported locales in declared order.
478
+ *
479
+ * @returns The configured `locales` list (priority/display order).
480
+ * @example
481
+ * ```ts
482
+ * app.i18n.locales(); // ["en", "uk"]
483
+ * ```
484
+ */
485
+ locales(): readonly string[];
486
+ /**
487
+ * Returns the fallback locale used when a requested locale is absent.
488
+ *
489
+ * @returns The configured `defaultLocale`.
490
+ * @example
491
+ * ```ts
492
+ * app.i18n.defaultLocale(); // "en"
493
+ * ```
494
+ */
495
+ defaultLocale(): string;
496
+ /**
497
+ * Membership guard: whether `x` is one of the supported locales.
498
+ *
499
+ * @param x - Candidate locale code.
500
+ * @returns `true` if `x ∈ locales`, else `false`.
501
+ * @example
502
+ * ```ts
503
+ * app.i18n.isLocale("uk"); // true
504
+ * ```
505
+ */
506
+ isLocale(x: string): boolean;
507
+ /**
508
+ * Human-readable display name for a locale.
509
+ *
510
+ * @param locale - Locale code to look up.
511
+ * @returns The display name, or `undefined` if unmapped.
512
+ * @example
513
+ * ```ts
514
+ * app.i18n.localeName("uk"); // "Українська"
515
+ * ```
516
+ */
517
+ localeName(locale: string): string | undefined;
518
+ /**
519
+ * Open Graph `og:locale` value for a locale.
520
+ *
521
+ * @param locale - Locale code to look up.
522
+ * @returns The `og:locale` value (e.g. `"en_US"`), or `undefined` if unmapped.
523
+ * @example
524
+ * ```ts
525
+ * app.i18n.ogLocale("en"); // "en_US"
526
+ * ```
527
+ */
528
+ ogLocale(locale: string): string | undefined;
529
+ /**
530
+ * Translate `key` for `locale` with a deterministic fallback chain
531
+ * (requested locale → default locale → the key itself).
532
+ *
533
+ * @param locale - Requested locale code.
534
+ * @param key - Translation key (e.g. `"nav.home"`).
535
+ * @returns The translated value, the default-locale value, or `key`.
536
+ * @example
537
+ * ```ts
538
+ * app.i18n.t("uk", "nav.home"); // "Головна"
539
+ * ```
540
+ */
541
+ t(locale: string, key: string): string;
542
+ };
543
+ declare namespace types_d_exports$4 {
544
+ export { Api$1 as Api, ClientRoute, CompileInput, CompiledRoute, Config$1 as Config, ExtractRouteParams, ExtractSegmentParameter, HeadConfig$1 as HeadConfig, LayoutContext, MatcherTable, Prettify, RouteBuilder, RouteContext, RouteDefinition, RouteHandlers, RouteMap, RouteState, RouterApi, RouterConfig, RouterState, State$1 as State, TypedRoute };
545
+ }
546
+ /**
547
+ * Param contribution of a single path segment. `{name:?}` / `:name?` → optional;
548
+ * `{name}` / `:name` → required; static segments contribute nothing.
549
+ *
550
+ * @example
551
+ * type S = ExtractSegmentParameter<"{slug}">; // { slug: string }
552
+ */
553
+ type ExtractSegmentParameter<Segment extends string> = Segment extends `{${infer Name}:?}` ? { [K in Name]?: string } : Segment extends `{${infer Name}}` ? { [K in Name]: string } : Segment extends `:${infer Name}?` ? { [K in Name]?: string } : Segment extends `:${infer Name}` ? { [K in Name]: string } : Record<never, never>;
554
+ /**
555
+ * Template-literal type that extracts path params from a URL pattern by walking
556
+ * one `/`-delimited segment at a time (so mixed required/optional patterns infer
557
+ * correctly). `{name}` / `:name` become required; `{name:?}` becomes optional.
558
+ *
559
+ * @example
560
+ * type P = ExtractRouteParams<"/{lang:?}/{slug}/">; // { lang?: string; slug: string }
561
+ */
562
+ type ExtractRouteParams<P extends string> = P extends `${infer Head}/${infer Tail}` ? ExtractSegmentParameter<Head> & ExtractRouteParams<Tail> : ExtractSegmentParameter<P>;
563
+ /** Flattens an intersection type into a single object literal for readable IntelliSense. */
564
+ type Prettify<T> = { [K in keyof T]: T[K] };
565
+ /**
566
+ * Accumulating generic carried by `RouteBuilder` as the fluent chain grows.
567
+ * `P` is the pattern's extracted params; `D` is the data type produced by `.load()`.
568
+ */
569
+ interface RouteState<P extends string = string, D = unknown> {
570
+ /** Path params inferred from the pattern. */
571
+ readonly params: Prettify<ExtractRouteParams<P>>;
572
+ /** Loaded data type produced by `.load()` (widened only by `.load()`). */
573
+ readonly data: D;
574
+ }
575
+ /** Render-time context handed to `.render()` / `.head()`; `data` is `.load()`'s return. */
576
+ interface RouteContext<S extends RouteState> {
577
+ /** Resolved path params. */
578
+ readonly params: S["params"];
579
+ /** Loaded data (the return value of this route's `.load()`). */
580
+ readonly data: S["data"];
581
+ /** Active locale for this render. */
582
+ readonly locale: string;
583
+ }
584
+ /**
585
+ * Context handed to a route's `.layout()` wrapper: the render-time
586
+ * {@link RouteContext} plus the route's `.meta()` bag, so persistent chrome (e.g. a
587
+ * TopBar/TabNav) can read `locale` and `meta.activeTab`. Distinct from
588
+ * `RouteContext` because the layout is the only handler that needs `meta`; keeping
589
+ * it on its own type leaves `.render()`/`.head()` contexts unchanged.
590
+ *
591
+ * @remarks
592
+ * The layout is applied in the SSG render path ONLY. On client (SPA) navigation the
593
+ * chrome is persistent and the layout is intentionally NOT re-applied — only the
594
+ * inner swap region is replaced. See `build`'s pages phase and `spa`'s kernel.
595
+ */
596
+ interface LayoutContext<S extends RouteState> extends RouteContext<S> {
597
+ /** The route's `.meta()` bag (e.g. `{ activeTab: "home" }`). */
598
+ readonly meta: Record<string, unknown>;
599
+ }
600
+ /** Head metadata produced by a route's `.head()` handler. */
601
+ interface HeadConfig$1 {
602
+ /** Document title. */
603
+ readonly title?: string;
604
+ /** Meta description. */
605
+ readonly description?: string;
606
+ /** Arbitrary extra head fields. */
607
+ readonly [key: string]: unknown;
608
+ }
609
+ /**
610
+ * Fluent route builder. Each chain method returns the same builder with a
611
+ * (possibly widened) state generic. Only `.load()` widens the data type `D`.
612
+ */
613
+ interface RouteBuilder<S extends RouteState> extends RouteDefinition {
614
+ /**
615
+ * Attach a data loader; widens the data generic (and ONLY the data generic) so
616
+ * `.render()`/`.head()` see its return. Path params are preserved unchanged.
617
+ */
618
+ load<D>(loader: (params: S["params"], locale: string) => D | Promise<D>): RouteBuilder<{
619
+ readonly params: S["params"];
620
+ readonly data: Awaited<D>;
621
+ }>;
622
+ /**
623
+ * Attach a ctx-aware layout wrapper that frames this route's rendered page in
624
+ * persistent chrome. Receives the route's {@link LayoutContext} (render context +
625
+ * `meta`) and the page `children`. Applied in the SSG render path ONLY — on client
626
+ * navigation the chrome persists and only the inner swap region is replaced, so the
627
+ * layout is not re-run.
628
+ */
629
+ layout(component: (ctx: LayoutContext<S>, children: ComponentChildren) => VNode): RouteBuilder<S>;
630
+ /** Attach the page render handler. */
631
+ render(handler: (ctx: RouteContext<S>) => VNode): RouteBuilder<S>;
632
+ /**
633
+ * Attach the client-side validation gate: parse the raw `unknown` fetched from
634
+ * the persisted data file back into this route's data type `S["data"]`. Runs at
635
+ * the trust boundary before `render` on the client (and MUST return `S["data"]`,
636
+ * so a mismatched schema is a compile error). Throw inside it to reject malformed
637
+ * data — `spa` then falls back to HTML-over-fetch. Use a hand guard or any
638
+ * Standard-Schema validator (zod/valibot/arktype).
639
+ */
640
+ parse(handler: (raw: unknown) => S["data"]): RouteBuilder<S>;
641
+ /** Attach the head/SEO handler. */
642
+ head(handler: (ctx: RouteContext<S>) => HeadConfig$1): RouteBuilder<S>;
643
+ /** Attach a static-generation param producer. */
644
+ generate(handler: (locale: string) => S["params"][] | Promise<S["params"][]>): RouteBuilder<S>;
645
+ /**
646
+ * Attach an arbitrary metadata bag. The bag MUST be JSON-serializable: it is
647
+ * projected verbatim into `clientManifest()` and shipped to the browser.
648
+ */
649
+ meta(meta: Record<string, unknown>): RouteBuilder<S>;
650
+ /** Attach a JSON serializer for the route's data. */
651
+ toJson(handler: (ctx: RouteContext<S>) => unknown): RouteBuilder<S>;
652
+ /** Override the output file-path producer. */
653
+ toFile(handler: (params: S["params"]) => string): RouteBuilder<S>;
654
+ }
655
+ /** Build-only handler bag captured by a `RouteBuilder` (consumed by `build` via `manifest()`). */
656
+ interface RouteHandlers {
657
+ /** Data loader. */
658
+ readonly load?: (params: Record<string, string>, locale: string) => unknown;
659
+ /** Layout wrapper (ctx-aware): frames the page in persistent chrome. SSG-only. */
660
+ readonly layout?: (ctx: LayoutContext<RouteState>, children: ComponentChildren) => VNode;
661
+ /** Page renderer. */
662
+ readonly render?: (ctx: RouteContext<RouteState>) => VNode;
663
+ /** Client-side validation gate: `unknown` (fetched JSON) → the route's data type, or throw. */
664
+ readonly parse?: (raw: unknown) => unknown;
665
+ /** Head/SEO producer. */
666
+ readonly head?: (ctx: RouteContext<RouteState>) => HeadConfig$1;
667
+ /** Static-generation param producer. */
668
+ readonly generate?: (locale: string) => unknown[] | Promise<unknown[]>;
669
+ /** JSON serializer. */
670
+ readonly toJson?: (ctx: RouteContext<RouteState>) => unknown;
671
+ /** Output file-path producer. */
672
+ readonly toFile?: (params: Record<string, string>) => string;
673
+ }
674
+ /**
675
+ * A single route definition: the (erased) carrier produced by `route(...)`.
676
+ * Build consumes `_handlers` via `manifest()`; per-route param/data integrity
677
+ * is a call-site property established before config erasure.
678
+ */
679
+ interface RouteDefinition {
680
+ /** URL pattern string, e.g. `/{lang:?}/{slug}/`. */
681
+ readonly pattern: string;
682
+ /** Metadata bag accumulated from `.meta()` (named `_meta` to avoid clashing with the `.meta()` builder method). */
683
+ readonly _meta: Record<string, unknown>;
684
+ /** Build-time handler bag (load/render/head/generate/toJson/toFile). */
685
+ readonly _handlers: RouteHandlers;
686
+ }
687
+ /**
688
+ * Map of route name → route definition. The element type is intentionally the
689
+ * base (erased) `RouteDefinition`; this is the documented generic-erasure boundary.
690
+ */
691
+ type RouteMap = Record<string, RouteDefinition>;
692
+ /**
693
+ * Configuration for the router plugin.
694
+ *
695
+ * @remarks
696
+ * `routes` is an OPAQUE carrier at the config boundary — the framework `Config`
697
+ * generic erases the per-route element types (spec/05 §8, spec/09 §4). Downstream
698
+ * plugins read the typed route set via `ctx.require(routerPlugin).manifest()`.
699
+ */
700
+ type RouterConfig = {
701
+ /**
702
+ * Named route definitions. Element type erases to the base `RouteDefinition`
703
+ * at this config boundary; per-route call-site types are preserved only through
704
+ * `defineRoutes()` + `route()` at the consumer and re-exposed via `manifest()`.
705
+ */
706
+ routes: RouteMap;
707
+ /**
708
+ * Render mode for URL/file resolution. Defaults to `"hybrid"`.
709
+ * - `"ssg"` static generation only (no client router emitted).
710
+ * - `"spa"` client-side routing only.
711
+ * - `"hybrid"` static HTML + client navigation overlay.
712
+ */
713
+ mode?: "ssg" | "spa" | "hybrid";
714
+ };
715
+ /** A resolved route exposing URL utilities with typed params (port of legacy TypedRoute). */
716
+ interface TypedRoute<TParams = Record<string, string>> {
717
+ /** URL pattern string, e.g. `/{lang:?}/{slug}/`. */
718
+ readonly pattern: string;
719
+ /** Route name key. */
720
+ readonly name: string;
721
+ /** Metadata bag from `.meta()`. */
722
+ readonly meta: Record<string, unknown>;
723
+ /** Build a URL from typed params. */
724
+ toUrl(params: TParams): string;
725
+ /** Build an output file path from typed params. */
726
+ toFile(params: TParams): string;
727
+ /** Match a pathname into typed params, or `null`. */
728
+ match(pathname: string): TParams | null;
729
+ }
730
+ /** A single compiled route entry: name, pattern, specificity, matchers, URL utilities. */
731
+ interface CompiledRoute {
732
+ /** Route name key from the route map. */
733
+ readonly name: string;
734
+ /** Original user pattern, e.g. `/{lang:?}/{slug}/`. */
735
+ readonly pattern: string;
736
+ /** Dynamic-segment count (lower = more specific = matched first). */
737
+ readonly dynamicSegmentCount: number;
738
+ /** Pre-built URLPattern matchers (lang-aware + bare fallback). */
739
+ readonly matchers: {
740
+ readonly withLang: URLPattern;
741
+ readonly bare: URLPattern;
742
+ };
743
+ /** Resolve pathname into params (withLang first, then bare with defaultLocale injected). */
744
+ readonly matchFn: (pathname: string) => Record<string, string> | null;
745
+ /** Build a URL from params. */
746
+ readonly toUrl: (params: Record<string, string>) => string;
747
+ /** Build an output file path from params. */
748
+ readonly toFile: (params: Record<string, string>) => string;
749
+ /** The original (opaque) RouteDefinition — preserved for `manifest()`. */
750
+ readonly definition: RouteDefinition;
751
+ /** Route metadata bag from `.meta()`. */
752
+ readonly meta: Record<string, unknown>;
753
+ }
754
+ /** The compiled matcher table (immutable once `onInit` assigns it). */
755
+ interface MatcherTable {
756
+ /** All compiled routes, sorted by specificity (fewest dynamic segments first). */
757
+ readonly compiled: readonly CompiledRoute[];
758
+ /** Name → CompiledRoute index for O(1) `toUrl(name, ...)` lookups. */
759
+ readonly byName: ReadonlyMap<string, CompiledRoute>;
760
+ }
761
+ /**
762
+ * Router plugin state. `createState` runs with minimal context and returns a
763
+ * mutable holder whose `table` is `null` until `onInit` (which has full context)
764
+ * compiles and assigns it. Keeps all mutable state in `ctx.state` (no singletons).
765
+ */
766
+ interface RouterState {
767
+ /** Compiled matcher table; `null` until `onInit` assigns it. */
768
+ table: MatcherTable | null;
769
+ /** Resolved render mode (single source of truth; set in `onInit`). Defaults `"hybrid"`. */
770
+ mode: "ssg" | "spa" | "hybrid";
771
+ }
772
+ /** Plain-data input to `compileRoutes` — resolved DATA only, never the plugin ctx. */
773
+ interface CompileInput {
774
+ /** The opaque route map from config. */
775
+ readonly routes: RouteMap;
776
+ /** Resolved render mode. */
777
+ readonly mode: "ssg" | "spa" | "hybrid";
778
+ /** Site base URL (from `ctx.require(sitePlugin).url()`). */
779
+ readonly baseUrl: string;
780
+ /** Available locales (from `ctx.require(i18nPlugin).locales()`). */
781
+ readonly locales: readonly string[];
782
+ /** Default locale used for bare-pattern fallback. */
783
+ readonly defaultLocale: string;
784
+ }
785
+ /**
786
+ * Serializable route entry for the client route-index — a projection of the
787
+ * compiled route table with NO `_handlers` closures, safe to ship to the browser
788
+ * (the SPA recompiles matchers lazily from `pattern`).
789
+ *
790
+ * @remarks
791
+ * `meta` MUST be JSON-serializable: `clientManifest()` is intended to survive a
792
+ * `JSON.stringify`/`JSON.parse` round-trip, so a route's `.meta()` bag must contain
793
+ * only JSON-safe values (no functions, symbols, or class instances).
794
+ */
795
+ interface ClientRoute {
796
+ /** URL pattern string, e.g. `/{lang:?}/{slug}/`. */
797
+ readonly pattern: string;
798
+ /** Route name key from the route map. */
799
+ readonly name: string;
800
+ /** Route metadata bag from `.meta()`. MUST be JSON-serializable. */
801
+ readonly meta: Record<string, unknown>;
802
+ }
803
+ /** Public API exposed via `ctx.require(routerPlugin)`. */
804
+ type RouterApi = {
805
+ /**
806
+ * Match a pathname against the compiled route table (specificity-sorted).
807
+ *
808
+ * @param pathname - URL pathname, e.g. `/en/hello/`.
809
+ * @returns `{ params, route }` for the most specific match, or `null` if none.
810
+ * @example
811
+ * const hit = ctx.require(routerPlugin).match("/en/hello/");
812
+ */
813
+ match(pathname: string): {
814
+ params: Record<string, string>;
815
+ route: RouteDefinition;
816
+ } | null;
817
+ /**
818
+ * Build a URL for a named route from params.
819
+ *
820
+ * @param routeName - Route name key from the route map.
821
+ * @param params - Param values to substitute into the pattern.
822
+ * @returns The resolved URL string (e.g. `/en/hello/`).
823
+ * @throws {Error} If `routeName` is unknown.
824
+ * @example
825
+ * ctx.require(routerPlugin).toUrl("article", { lang: "en", slug: "hello" });
826
+ */
827
+ toUrl(routeName: string, params: Record<string, string>): string;
828
+ /**
829
+ * All resolved routes as typed URL utilities, in specificity order.
830
+ *
831
+ * @returns Read-only array of resolved typed routes.
832
+ * @example
833
+ * for (const r of ctx.require(routerPlugin).entries()) { r.toUrl({ slug: "x" }); }
834
+ */
835
+ entries(): readonly TypedRoute[];
836
+ /**
837
+ * The typed route set for build-time consumption (the KEY mechanism). An API
838
+ * return, NOT a config readback — preserves per-route types despite config erasure.
839
+ *
840
+ * @returns Read-only array of the typed route definitions, in declaration order.
841
+ * @example
842
+ * for (const def of ctx.require(routerPlugin).manifest()) { def._handlers.load?.({}, "en"); }
843
+ */
844
+ manifest(): readonly RouteDefinition[];
845
+ /**
846
+ * Serializable, specificity-sorted projection of the route table for client
847
+ * shipping. Maps the compiled table to `{ pattern, name, meta }` entries with NO
848
+ * `_handlers` closures, returned as a fresh frozen array. JSON-serializable so the
849
+ * SPA can embed it and recompile matchers lazily in the browser.
850
+ *
851
+ * @returns A fresh, frozen, specificity-sorted read-only array of {@link ClientRoute}.
852
+ * @example
853
+ * const json = JSON.stringify(ctx.require(routerPlugin).clientManifest());
854
+ */
855
+ clientManifest(): readonly ClientRoute[];
856
+ /**
857
+ * The resolved render mode — the single source of truth for static/hybrid/spa
858
+ * behavior. `build` reads it to decide whether to emit client data sidecars;
859
+ * `spa` reads it to decide whether to attempt client DATA navigation.
860
+ *
861
+ * @returns `"ssg" | "spa" | "hybrid"`.
862
+ * @example
863
+ * if (ctx.require(routerPlugin).mode() !== "ssg") { ... }
864
+ */
865
+ mode(): "ssg" | "spa" | "hybrid";
866
+ };
867
+ /** Re-export under the canonical `Config` name for the plugin-types barrel. */
868
+ type Config$1 = RouterConfig;
869
+ /** Re-export under the canonical `State` name for the plugin-types barrel. */
870
+ type State$1 = RouterState;
871
+ /** Re-export under the canonical `Api` name for the plugin-types barrel. */
872
+ type Api$1 = RouterApi;
873
+ declare namespace types_d_exports$2 {
874
+ export { Api, ArticleMeta, Config, HeadConfig, HeadDefaults, HeadElement, ResolvedRoute, State };
875
+ }
876
+ /**
877
+ * @file head plugin — type definitions skeleton
878
+ */
879
+ /**
880
+ * Configuration for the `head` plugin.
881
+ *
882
+ * All fields are optional; sensible empty/identity defaults apply. Site-level values
883
+ * (title, description, base URL) are owned by the `site` plugin and read at render time.
884
+ *
885
+ * @example
886
+ * ```ts
887
+ * const config: Config = { titleTemplate: "%s — Moku" };
888
+ * ```
889
+ */
890
+ type Config = {
891
+ /** 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. */
892
+ defaultOgImage?: string; /** Default Twitter card type emitted when og/twitter content is present. */
893
+ twitterCard?: "summary" | "summary_large_image"; /** Default Twitter site handle (e.g. `"@moku_labs"`) emitted as `twitter:site`. */
894
+ twitterHandle?: string;
895
+ };
896
+ /**
897
+ * Internal head state: a single `defaults` slot holding the normalized head defaults.
898
+ *
899
+ * `createState` initializes `defaults` to `null`; `onInit` assigns the normalized snapshot
900
+ * exactly once. The field is mutable (assigned in `onInit`) and nullable (initial value
901
+ * before `onInit`).
902
+ *
903
+ * @example
904
+ * ```ts
905
+ * const state: State = { defaults: null };
906
+ * ```
907
+ */
908
+ type State = {
909
+ /** Normalized head defaults, assigned once in `onInit` (initially `null`). */defaults: HeadDefaults | null;
910
+ };
911
+ /**
912
+ * The normalized, resolved head defaults snapshot built from `Config` in `onInit` and
913
+ * read by `render`.
914
+ *
915
+ * @example
916
+ * ```ts
917
+ * const defaults: HeadDefaults = { twitterCard: "summary_large_image" };
918
+ * ```
919
+ */
920
+ type HeadDefaults = {
921
+ /** Title template carried over from config (validated to contain `%s`). */readonly titleTemplate?: string; /** Default Open Graph image URL. */
922
+ readonly defaultOgImage?: string; /** Resolved Twitter card type (defaulted to `"summary_large_image"`). */
923
+ readonly twitterCard: "summary" | "summary_large_image"; /** Default Twitter site handle. */
924
+ readonly twitterHandle?: string;
925
+ };
926
+ /**
927
+ * A serializable descriptor for a single `<head>` tag.
928
+ *
929
+ * Deliberately a PLAIN serializable object (NOT a Preact `VNode`) so `head` stays
930
+ * decoupled from any renderer and can be produced/consumed in build (string) and in the
931
+ * SPA (DOM) without pulling in `preact`.
932
+ *
933
+ * @example
934
+ * ```ts
935
+ * const el: HeadElement = { tag: "meta", attrs: { name: "robots", content: "index" } };
936
+ * ```
937
+ */
938
+ type HeadElement = {
939
+ /** The tag name to emit. */tag: "meta" | "link" | "title" | "script"; /** Attribute map (already-unescaped values; escaping happens at serialization). */
940
+ attrs?: Record<string, string>; /** Inner text content (for `<title>` and JSON-LD `<script>`). */
941
+ children?: string; /** Stable identity used for de-duplication during `render` (e.g. `"meta:description"`). */
942
+ key?: string;
943
+ };
944
+ /**
945
+ * The shape returned by a route's `.head(data)` callback. All fields optional so routes
946
+ * supply only what they override.
947
+ *
948
+ * @example
949
+ * ```ts
950
+ * const head: HeadConfig = { title: "Home", description: "Welcome" };
951
+ * ```
952
+ */
953
+ type HeadConfig = {
954
+ /** Page title (before `titleTemplate` is applied). */title?: string; /** Page description (`<meta name=description>` + og/twitter fallback). */
955
+ description?: string; /** Canonical URL override (otherwise derived from `router.toUrl`). */
956
+ canonical?: string; /** Open Graph image override for this page. */
957
+ image?: string; /** Arbitrary extra head elements (use the SEO primitive helpers to build these). */
958
+ elements?: HeadElement[];
959
+ };
960
+ /**
961
+ * Metadata describing an article page, consumed by `buildArticleHead`.
962
+ *
963
+ * @example
964
+ * ```ts
965
+ * const meta: ArticleMeta = { title: "Hi", author: "A", published: "2026-01-01" };
966
+ * ```
967
+ */
968
+ type ArticleMeta = {
969
+ /** Article title. */title: string; /** Article description. */
970
+ description?: string; /** Article author. */
971
+ author?: string; /** ISO 8601 publish date. */
972
+ published?: string; /** ISO 8601 last-modified date. */
973
+ modified?: string; /** Section/category. */
974
+ section?: string; /** Article tags. */
975
+ tags?: string[]; /** Article image URL. */
976
+ image?: string;
977
+ };
978
+ /**
979
+ * A resolved route descriptor passed to `render` (path, params, locale, and its `.head()`
980
+ * result as a `HeadConfig`).
981
+ *
982
+ * @example
983
+ * ```ts
984
+ * const route: ResolvedRoute = { path: "/about", params: {}, name: "about" };
985
+ * ```
986
+ */
987
+ type ResolvedRoute = {
988
+ /** The resolved path of the route. */path: string; /** The route name key (used by `router.toUrl` for canonical/alternate hrefs). */
989
+ name: string; /** Resolved route params. */
990
+ params: Record<string, string>; /** The active locale for this route render. */
991
+ locale?: string; /** The route's `.head()` result. */
992
+ head?: HeadConfig;
993
+ };
994
+ /**
995
+ * The public API surface of the `head` plugin.
996
+ *
997
+ * @example
998
+ * ```ts
999
+ * const html: string = api.render(route, data);
1000
+ * ```
1001
+ */
1002
+ type Api = {
1003
+ /**
1004
+ * Compose the final `<head>` inner HTML for a route. Pulled synchronously by `build`.
1005
+ *
1006
+ * @param route - The resolved route descriptor (incl. its `.head()` HeadConfig).
1007
+ * @param data - The page data object passed to the route's loader/render.
1008
+ * @returns The serialized inner HTML of `<head>` (no surrounding `<head>` tags).
1009
+ * @example
1010
+ * ```ts
1011
+ * api.render(route, data);
1012
+ * ```
1013
+ */
1014
+ render(route: ResolvedRoute, data: unknown): string;
1015
+ };
1016
+ declare namespace types_d_exports$5 {
1017
+ export { COMPONENT_HOOK_NAMES, ComponentContext, ComponentDef, ComponentHooks, ComponentInstance, ExtractApi, PageData, ResolvedSpaConfig, SpaApi, SpaConfig, SpaContext, SpaDataReader, SpaEmitFunction, SpaEvents, SpaKernel, SpaKernelDeps, SpaRequire, SpaState };
1018
+ }
1019
+ /** Payload map for the events `spa` emits, used to type the kernel's `emit` closure. */
1020
+ type SpaEvents = {
1021
+ /** A navigation has been intercepted and is starting. */"spa:navigate": {
1022
+ from: string;
1023
+ to: string;
1024
+ }; /** The swap completed and the new URL is active. */
1025
+ "spa:navigated": {
1026
+ url: string;
1027
+ }; /** A component instance attached to an element. */
1028
+ "spa:component-mount": {
1029
+ name: string;
1030
+ el: Element;
1031
+ }; /** A component instance detached from an element. */
1032
+ "spa:component-unmount": {
1033
+ name: string;
1034
+ el: Element;
1035
+ };
1036
+ };
1037
+ /** Strictly-typed emit closure for the spa events (kernel overload form). */
1038
+ type SpaEmitFunction = EmitFn<SpaEvents>;
1039
+ /**
1040
+ * Structural extraction of a plugin instance's public API from its `_phantom`
1041
+ * carrier (mirrors the kernel's non-exported `ExtractPluginApi`).
1042
+ *
1043
+ * @example
1044
+ * type RApi = ExtractApi<typeof routerPlugin>;
1045
+ */
1046
+ type ExtractApi<PluginCandidate> = PluginCandidate extends {
1047
+ readonly _phantom: {
1048
+ readonly api: infer PluginApi;
1049
+ };
1050
+ } ? PluginApi : never;
1051
+ /** Generic `require` closure for pulling dependency plugin APIs at init time. */
1052
+ type SpaRequire = <PluginCandidate extends {
1053
+ readonly name: string;
1054
+ readonly spec: unknown;
1055
+ readonly _phantom: {
1056
+ readonly config: unknown;
1057
+ readonly state: unknown;
1058
+ readonly api: unknown;
1059
+ readonly events: Record<string, unknown>;
1060
+ };
1061
+ }>(plugin: PluginCandidate) => ExtractApi<PluginCandidate>;
1062
+ /**
1063
+ * The plugin-context slice the spa wiring consumes in `onInit`/`onStart`:
1064
+ * mutable `state`, resolved `config`, `require`/`emit`/`log`. Structurally
1065
+ * assignable from the framework's generic execution context.
1066
+ */
1067
+ interface SpaContext {
1068
+ /** Mutable spa state (all kernel data lives here). */
1069
+ state: SpaState;
1070
+ /** Resolved, frozen spa config. */
1071
+ readonly config: Readonly<SpaConfig>;
1072
+ /** Resolve a depended-upon plugin instance to its public API. */
1073
+ require: SpaRequire;
1074
+ /**
1075
+ * Whether a plugin is registered (by name). Used to detect the OPTIONAL `data`
1076
+ * plugin at init — `spa` enables client DATA navigation only when `has("data")`.
1077
+ */
1078
+ has: (name: string) => boolean;
1079
+ /** Emit a spa lifecycle event (notification-only). */
1080
+ emit: SpaEmitFunction;
1081
+ /** Structured logger (core `log` API). */
1082
+ readonly log: LogApi;
1083
+ }
1084
+ /** Configuration for the SPA runtime plugin. All fields optional; defaults applied in onInit. */
1085
+ type SpaConfig = {
1086
+ /**
1087
+ * CSS selector for the page region swapped on navigation. Defaults to
1088
+ * `"main > section"`.
1089
+ */
1090
+ swapSelector?: string;
1091
+ /**
1092
+ * Use the View Transitions API for cross-fade swaps when available.
1093
+ * Falls back to an instant swap when unsupported. Defaults to `false`.
1094
+ */
1095
+ viewTransitions?: boolean;
1096
+ /**
1097
+ * Show the in-house top progress bar during navigation. Defaults to `true`.
1098
+ */
1099
+ progressBar?: boolean;
1100
+ /**
1101
+ * Components to auto-register at init (in addition to runtime `register`).
1102
+ * Defaults to an empty array.
1103
+ */
1104
+ components?: ComponentDef[];
1105
+ };
1106
+ /** Resolved SPA config after defaults are applied. */
1107
+ interface ResolvedSpaConfig {
1108
+ /** CSS selector for the swapped page region. */
1109
+ swapSelector: string;
1110
+ /** Whether View Transitions are enabled. */
1111
+ viewTransitions: boolean;
1112
+ /** Whether the progress bar is enabled. */
1113
+ progressBar: boolean;
1114
+ /** Pre-registered components. */
1115
+ components: ComponentDef[];
1116
+ }
1117
+ /** Context handed to every component lifecycle hook. */
1118
+ interface ComponentContext {
1119
+ /** The element the component instance is bound to. */
1120
+ el: Element;
1121
+ /** Page data extracted from the `script#__DATA__` payload. */
1122
+ data: PageData;
1123
+ }
1124
+ /** Lifecycle hooks a component may implement. */
1125
+ interface ComponentHooks {
1126
+ /**
1127
+ * Called once when the instance is created (before DOM attach).
1128
+ *
1129
+ * @param ctx - The component context for this instance.
1130
+ * @returns void
1131
+ * @example
1132
+ * onCreate({ el }) { el.dataset.ready = "1"; }
1133
+ */
1134
+ onCreate?(ctx: ComponentContext): void;
1135
+ /**
1136
+ * Called after the instance is attached to its element.
1137
+ *
1138
+ * @param ctx - The component context for this instance.
1139
+ * @returns void
1140
+ * @example
1141
+ * onMount({ el }) { el.textContent = "0"; }
1142
+ */
1143
+ onMount?(ctx: ComponentContext): void;
1144
+ /**
1145
+ * Called when a navigation begins while this instance is mounted.
1146
+ *
1147
+ * @param ctx - The component context for this instance.
1148
+ * @returns void
1149
+ * @example
1150
+ * onNavStart({ el }) { el.dataset.loading = ""; }
1151
+ */
1152
+ onNavStart?(ctx: ComponentContext): void;
1153
+ /**
1154
+ * Called when a navigation completes while this instance is mounted.
1155
+ *
1156
+ * @param ctx - The component context for this instance.
1157
+ * @returns void
1158
+ * @example
1159
+ * onNavEnd({ el }) { delete el.dataset.loading; }
1160
+ */
1161
+ onNavEnd?(ctx: ComponentContext): void;
1162
+ /**
1163
+ * Called before the instance is detached from its element.
1164
+ *
1165
+ * @param ctx - The component context for this instance.
1166
+ * @returns void
1167
+ * @example
1168
+ * onUnMount({ el }) { el.replaceChildren(); }
1169
+ */
1170
+ onUnMount?(ctx: ComponentContext): void;
1171
+ /**
1172
+ * Called once when the instance is destroyed (after detach).
1173
+ *
1174
+ * @param ctx - The component context for this instance.
1175
+ * @returns void
1176
+ * @example
1177
+ * onDestroy({ el }) { delete el.dataset.ready; }
1178
+ */
1179
+ onDestroy?(ctx: ComponentContext): void;
1180
+ }
1181
+ /** Allowed hook names — single source of truth for fail-fast validation. */
1182
+ declare const COMPONENT_HOOK_NAMES: readonly ["onCreate", "onMount", "onNavStart", "onNavEnd", "onUnMount", "onDestroy"];
1183
+ /** A registered component definition. */
1184
+ interface ComponentDef {
1185
+ /** Unique component name (matched against `data-component`). */
1186
+ name: string;
1187
+ /** Lifecycle hooks. */
1188
+ hooks: ComponentHooks;
1189
+ }
1190
+ /** A live, mounted component instance. */
1191
+ interface ComponentInstance {
1192
+ /** The definition this instance was created from. */
1193
+ def: ComponentDef;
1194
+ /** The element this instance is bound to. */
1195
+ el: Element;
1196
+ /**
1197
+ * True if the element is OUTSIDE the swap area — persists across navigations
1198
+ * and receives onNavStart/onNavEnd (never onUnMount on nav). False =
1199
+ * page-specific: full unmount/destroy on every navigation.
1200
+ */
1201
+ persistent: boolean;
1202
+ }
1203
+ /** Page data payload parsed from the inline `script#__DATA__` element. */
1204
+ type PageData = Record<string, unknown>;
1205
+ /**
1206
+ * The OPTIONAL `data` provider reader the kernel uses for client DATA navigation —
1207
+ * a structural slice of the `data` plugin's API (fetch the persisted JSON for a
1208
+ * page path). Captured at init via `ctx.has("data")`/`ctx.require` so `spa` never
1209
+ * imports the `data` plugin or its types.
1210
+ */
1211
+ type SpaDataReader = (path: string) => Promise<unknown | null>;
1212
+ /** Resolved dependency APIs the kernel reuses (router match/mode, head compose, optional data). */
1213
+ interface SpaKernelDeps {
1214
+ /** Router plugin API — used for client-side route matching (`match`) + the resolved `mode`. */
1215
+ router: RouterApi;
1216
+ /** Head plugin API — its pure compose is reused for client head-sync. */
1217
+ head: Api;
1218
+ /**
1219
+ * The OPTIONAL `data` reader. Present only when the `data` plugin is composed.
1220
+ * When present (and `router.mode() !== "ssg"`), navigation first tries the client
1221
+ * DATA path (match → `dataAt(path)` → `route.parse` → `route.render`); otherwise
1222
+ * it always uses HTML-over-fetch.
1223
+ */
1224
+ dataAt?: SpaDataReader;
1225
+ }
1226
+ /** The single shared SPA kernel — pure factory over state/config/emit/deps. */
1227
+ interface SpaKernel {
1228
+ /**
1229
+ * Validate config, register config.components, seed currentUrl.
1230
+ *
1231
+ * @returns void
1232
+ * @example
1233
+ * kernel.init();
1234
+ */
1235
+ init(): void;
1236
+ /**
1237
+ * Boot the browser runtime (router listeners + initial scan). Throws if started.
1238
+ *
1239
+ * @returns void
1240
+ * @example
1241
+ * kernel.boot();
1242
+ */
1243
+ boot(): void;
1244
+ /**
1245
+ * Register a component definition (last-registered-wins).
1246
+ *
1247
+ * @param component - The component definition to register.
1248
+ * @returns void
1249
+ * @example
1250
+ * kernel.register(counter);
1251
+ */
1252
+ register(component: ComponentDef): void;
1253
+ /**
1254
+ * Process a navigation to `path`: fetch then swap then head-sync then emit.
1255
+ *
1256
+ * @param path - The target path to navigate to.
1257
+ * @returns void
1258
+ * @example
1259
+ * kernel.processNav("/about");
1260
+ */
1261
+ processNav(path: string): void;
1262
+ /**
1263
+ * Query the swap region and mount components for matching elements.
1264
+ *
1265
+ * @returns void
1266
+ * @example
1267
+ * kernel.scan();
1268
+ */
1269
+ scan(): void;
1270
+ /**
1271
+ * Tear down router listeners, run unmount/destroy, clear instances.
1272
+ *
1273
+ * @returns void
1274
+ * @example
1275
+ * kernel.dispose();
1276
+ */
1277
+ dispose(): void;
1278
+ }
1279
+ /** Internal mutable state for the spa plugin (all kernel data lives here). */
1280
+ interface SpaState {
1281
+ /** Components registered by name (last-registered-wins). */
1282
+ registeredComponents: Map<string, ComponentDef>;
1283
+ /** Live component instances keyed by their bound element. */
1284
+ instances: Map<Element, ComponentInstance>;
1285
+ /** The current resolved URL (pathname + search). */
1286
+ currentUrl: string;
1287
+ /** Teardown handle for the attached router listeners (null when detached). */
1288
+ destroyRouter: (() => void) | null;
1289
+ /** Whether the browser runtime has been booted. */
1290
+ started: boolean;
1291
+ /** The single shared SPA kernel instance (null until onInit builds it). */
1292
+ kernel: SpaKernel | null;
1293
+ }
1294
+ /** Public API of the spa plugin (registration / control surface). */
1295
+ type SpaApi = {
1296
+ /**
1297
+ * Register a component definition for client mounting.
1298
+ *
1299
+ * @param component - The component definition created via `createComponent`.
1300
+ * @returns void
1301
+ * @example
1302
+ * app.spa.register(counter);
1303
+ */
1304
+ register(component: ComponentDef): void;
1305
+ /**
1306
+ * Programmatically navigate to a path (client runtime; no-op without a DOM).
1307
+ *
1308
+ * @param path - Target path (pathname, optionally with search/hash).
1309
+ * @returns void
1310
+ * @example
1311
+ * app.spa.navigate("/about");
1312
+ */
1313
+ navigate(path: string): void;
1314
+ /**
1315
+ * Read the current resolved URL.
1316
+ *
1317
+ * @returns The current pathname + search.
1318
+ * @example
1319
+ * const url = app.spa.current(); // "/about"
1320
+ */
1321
+ current(): string;
1322
+ };
1323
+ //#endregion
1324
+ //#region src/plugins/site/index.d.ts
1325
+ /**
1326
+ * Site plugin — holds global, frozen site metadata (name, url, author,
1327
+ * description) and builds canonical URLs. Consumed by router, head, and build.
1328
+ * `name` and `url` must be non-empty (validated at `onInit`).
1329
+ *
1330
+ * @example Set your site identity
1331
+ * ```ts
1332
+ * const app = createApp({
1333
+ * pluginConfigs: {
1334
+ * site: {
1335
+ * name: "My Blog",
1336
+ * url: "https://blog.dev",
1337
+ * author: "Ada Lovelace",
1338
+ * description: "Notes on computing"
1339
+ * }
1340
+ * }
1341
+ * });
1342
+ * ```
1343
+ */
1344
+ declare const sitePlugin: import("@moku-labs/core").PluginInstance<"site", Config$3, Record<string, never>, Api$3, {}> & Record<never, never>;
1345
+ //#endregion
1346
+ //#region src/plugins/i18n/index.d.ts
1347
+ /**
1348
+ * Internationalization plugin — locale registry plus a flat translation helper
1349
+ * with default-locale fallback. Pure config-as-data (no state or events);
1350
+ * consumed read-only by content, router, head, and build.
1351
+ *
1352
+ * @example Register locales and translations
1353
+ * ```ts
1354
+ * const app = createApp({
1355
+ * pluginConfigs: {
1356
+ * i18n: {
1357
+ * locales: ["en", "uk"],
1358
+ * defaultLocale: "en",
1359
+ * localeNames: { en: "English", uk: "Українська" },
1360
+ * translations: { uk: { "nav.home": "Головна" } }
1361
+ * }
1362
+ * }
1363
+ * });
1364
+ * ```
1365
+ */
1366
+ declare const i18nPlugin: import("@moku-labs/core").PluginInstance<"i18n", Config$2, Record<string, never>, Api$2, {}> & Record<never, never>;
1367
+ //#endregion
1368
+ //#region src/plugins/router/builders/route-builder.d.ts
1369
+ /**
1370
+ * Create a fluent route builder from a URL pattern string. Captures the pattern
1371
+ * as a literal type for compile-time param inference; `.load()` is the only method
1372
+ * that widens the data generic, so `ctx.data` in `.render()`/`.head()` is typed by
1373
+ * `.load()`'s return at the CALL SITE. The returned object is itself the route
1374
+ * definition (`pattern` / `_meta` / `_handlers`), so it slots straight into a route map.
1375
+ *
1376
+ * @param pattern - URL pattern with `{param}` / `{param:?}` placeholders.
1377
+ * @returns A `RouteBuilder<RouteState<P>>` carrying the typed fluent chain.
1378
+ * @example
1379
+ * ```ts
1380
+ * route("/{lang:?}/{slug}/")
1381
+ * .load(({ slug }) => loadArticle(slug))
1382
+ * .render((ctx) => <Article a={ctx.data} />)
1383
+ * .head((ctx) => ({ title: ctx.data.title }));
1384
+ * ```
1385
+ */
1386
+ declare function route<P extends string>(pattern: P): RouteBuilder<RouteState<P>>;
1387
+ /**
1388
+ * Typed identity helper for route maps. Preserves the precise literal type of the
1389
+ * route object for IntelliSense at the consumer call site (before config erasure).
1390
+ *
1391
+ * @param routes - The route map object.
1392
+ * @returns The same object, with its precise type preserved.
1393
+ * @example
1394
+ * ```ts
1395
+ * const routes = defineRoutes({ home: route("/"), article: route("/{slug}/") });
1396
+ * ```
1397
+ */
1398
+ declare function defineRoutes<T extends RouteMap>(routes: T): T;
1399
+ //#endregion
1400
+ //#region src/plugins/router/index.d.ts
1401
+ /**
1402
+ * Router plugin — typed, named route definitions with locale-aware URL generation
1403
+ * and matching. Author routes with {@link route} + {@link defineRoutes}. Depends
1404
+ * on site (base URL) and i18n (locales).
1405
+ *
1406
+ * @example Define routes and choose a render mode
1407
+ * ```ts
1408
+ * const app = createApp({
1409
+ * pluginConfigs: {
1410
+ * router: {
1411
+ * routes: defineRoutes({
1412
+ * home: route("/"),
1413
+ * article: route("/blog/{slug}/")
1414
+ * }),
1415
+ * mode: "hybrid" // "ssg" | "spa" | "hybrid" (default)
1416
+ * }
1417
+ * }
1418
+ * });
1419
+ * ```
1420
+ */
1421
+ declare const routerPlugin: import("@moku-labs/core").PluginInstance<"router", RouterConfig, RouterState, RouterApi, {}> & {
1422
+ route: typeof route;
1423
+ defineRoutes: typeof defineRoutes;
1424
+ };
1425
+ //#endregion
1426
+ //#region src/plugins/head/primitives.d.ts
1427
+ /**
1428
+ * Build a `<meta name=… content=…>` descriptor.
1429
+ *
1430
+ * @param name - The meta `name` attribute (e.g. `"description"`, `"robots"`).
1431
+ * @param content - The meta `content` value.
1432
+ * @returns A serializable head element keyed `meta:<name>`.
1433
+ * @example meta("description", "A web framework built on @moku-labs/core")
1434
+ */
1435
+ declare function meta(name: string, content: string): HeadElement;
1436
+ /**
1437
+ * Build an Open Graph `<meta property=… content=…>` descriptor.
1438
+ *
1439
+ * @param property - The OG property, used verbatim (e.g. `"og:title"`, `"og:image"`).
1440
+ * @param content - The property value.
1441
+ * @returns A serializable head element keyed `meta:<property>`.
1442
+ * @example og("og:title", "Home")
1443
+ */
1444
+ declare function og(property: string, content: string): HeadElement;
1445
+ /**
1446
+ * Build a Twitter-card `<meta name=… content=…>` descriptor.
1447
+ *
1448
+ * @param name - The Twitter meta name, used verbatim (e.g. `"twitter:title"`).
1449
+ * @param content - The value.
1450
+ * @returns A serializable head element keyed `meta:<name>`.
1451
+ * @example twitter("twitter:card", "summary_large_image")
1452
+ */
1453
+ declare function twitter(name: string, content: string): HeadElement;
1454
+ /**
1455
+ * Build a JSON-LD `<script type="application/ld+json">` descriptor.
1456
+ *
1457
+ * XSS-SAFE: the serialized JSON has `<`, `>`, and `&` unicode-escaped (`<`,
1458
+ * `>`, `&`) so the payload can never break out of the `<script>` element
1459
+ * or inject markup, while still round-tripping via `JSON.parse`.
1460
+ *
1461
+ * @param data - Any JSON-serializable structured-data object.
1462
+ * @returns A serializable head element carrying the escaped JSON-LD script.
1463
+ * @example jsonLd({ "@context": "https://schema.org", "@type": "Article", headline: "Hi" })
1464
+ */
1465
+ declare function jsonLd(data: unknown): HeadElement;
1466
+ /**
1467
+ * Build a canonical `<link rel="canonical" href=…>` descriptor.
1468
+ *
1469
+ * @param url - The canonical absolute URL.
1470
+ * @returns A serializable head element keyed `link:canonical`.
1471
+ * @example canonical("https://example.com/post")
1472
+ */
1473
+ declare function canonical(url: string): HeadElement;
1474
+ /**
1475
+ * Build an alternate-language `<link rel="alternate" hreflang=… href=…>` descriptor.
1476
+ *
1477
+ * @param locale - The BCP-47 locale tag (e.g. `"en"`, `"uk"`, `"x-default"`).
1478
+ * @param url - The absolute URL of the localized page.
1479
+ * @returns A serializable head element keyed `link:alternate:<locale>`.
1480
+ * @example hreflang("uk", "https://example.com/uk/post")
1481
+ */
1482
+ declare function hreflang(locale: string, url: string): HeadElement;
1483
+ /**
1484
+ * Build a feed `<link rel="alternate" type=… title=… href=…>` descriptor.
1485
+ *
1486
+ * @param title - Human-readable feed title.
1487
+ * @param url - The feed URL.
1488
+ * @param type - The feed MIME type. Defaults to `"application/rss+xml"`.
1489
+ * @returns A serializable head element keyed `link:feed:<url>`.
1490
+ * @example feedLink("My Blog", "/feed.xml", "application/atom+xml")
1491
+ */
1492
+ declare function feedLink(title: string, url: string, type?: string): HeadElement;
1493
+ /**
1494
+ * Compose the full head element set for an article page: og:type=article, published/
1495
+ * modified times, author, section, tags, plus a JSON-LD `Article` block and canonical.
1496
+ *
1497
+ * @param articleMeta - Article metadata (title, description, author, dates, tags, image…).
1498
+ * @param canonicalUrl - The article's canonical absolute URL.
1499
+ * @returns An ordered array of serializable head elements.
1500
+ * @example buildArticleHead({ title: "Hi", author: "A", published: "2026-01-01" }, "https://x/p")
1501
+ */
1502
+ declare function buildArticleHead(articleMeta: ArticleMeta, canonicalUrl: string): HeadElement[];
1503
+ //#endregion
1504
+ //#region src/plugins/head/index.d.ts
1505
+ /**
1506
+ * Head plugin — composes per-route `<head>` metadata (title template, Open Graph,
1507
+ * Twitter cards, canonical, hreflang). Use the re-exported SEO primitives
1508
+ * ({@link meta}, {@link og}, {@link twitter}, …) inside a route's `.head()`.
1509
+ * Depends on site, i18n, and router.
1510
+ *
1511
+ * @example Set global head defaults
1512
+ * ```ts
1513
+ * const app = createApp({
1514
+ * pluginConfigs: {
1515
+ * head: {
1516
+ * titleTemplate: "%s — My Blog",
1517
+ * twitterCard: "summary_large_image",
1518
+ * twitterHandle: "@moku_labs"
1519
+ * }
1520
+ * }
1521
+ * });
1522
+ * ```
1523
+ */
1524
+ declare const headPlugin: import("@moku-labs/core").PluginInstance<"head", Config, State, Api, {}> & {
1525
+ meta: typeof meta;
1526
+ og: typeof og;
1527
+ twitter: typeof twitter;
1528
+ jsonLd: typeof jsonLd;
1529
+ canonical: typeof canonical;
1530
+ hreflang: typeof hreflang;
1531
+ feedLink: typeof feedLink;
1532
+ buildArticleHead: typeof buildArticleHead;
1533
+ };
1534
+ //#endregion
1535
+ //#region src/plugins/spa/components.d.ts
1536
+ /**
1537
+ * Create a validated component definition. Validates hook names at registration
1538
+ * for fail-fast typo detection (e.g. `onMout` throws immediately) and asserts
1539
+ * each provided hook is a function.
1540
+ *
1541
+ * @param name - Unique component name.
1542
+ * @param hooks - Lifecycle hook implementations.
1543
+ * @returns A `ComponentDef` ready to `register`.
1544
+ * @throws {Error} If `name` is empty, any hook key is not in
1545
+ * `COMPONENT_HOOK_NAMES`, or any provided hook value is not a function.
1546
+ * @example
1547
+ * const counter = createComponent("counter", {
1548
+ * onMount({ el }) { el.textContent = "0"; }
1549
+ * });
1550
+ */
1551
+ declare function createComponent(name: string, hooks: ComponentHooks): ComponentDef;
1552
+ //#endregion
1553
+ //#region src/plugins/spa/index.d.ts
1554
+ /**
1555
+ * SPA plugin — progressive client-side navigation layered over the static site:
1556
+ * swaps a page region on navigation, with an optional progress bar and View
1557
+ * Transitions. Register interactive islands with {@link createComponent}. Depends
1558
+ * on router and head; emits `spa:navigate`, `spa:navigated`, `spa:component-mount`,
1559
+ * and `spa:component-unmount`.
1560
+ *
1561
+ * @example Enable view transitions and a custom swap region
1562
+ * ```ts
1563
+ * const app = createApp({
1564
+ * pluginConfigs: {
1565
+ * spa: {
1566
+ * swapSelector: "main > section",
1567
+ * viewTransitions: true,
1568
+ * progressBar: true
1569
+ * }
1570
+ * }
1571
+ * });
1572
+ * ```
1573
+ */
1574
+ declare const spaPlugin: import("@moku-labs/core").PluginInstance<"spa", SpaConfig, SpaState, SpaApi, {
1575
+ "spa:navigate": {
1576
+ from: string;
1577
+ to: string;
1578
+ };
1579
+ "spa:navigated": {
1580
+ url: string;
1581
+ };
1582
+ "spa:component-mount": {
1583
+ name: string;
1584
+ el: Element;
1585
+ };
1586
+ "spa:component-unmount": {
1587
+ name: string;
1588
+ el: Element;
1589
+ };
1590
+ }> & Record<never, never>;
1591
+ declare namespace types_d_exports {
1592
+ export { DataConfig, DataEntry, DataProvider, DataState, DataWriteSummary };
1593
+ }
1594
+ /**
1595
+ * @file data plugin — type definitions (Standard tier).
1596
+ *
1597
+ * The `data` plugin is the **agnostic data provider** for the SSG→DATA→SPA pattern.
1598
+ * It owns ONE thing: the contract `page path → persisted JSON file`. It knows
1599
+ * NOTHING about what the data *is* — no domain types appear here. A route decides
1600
+ * its own data shape (`load`'s return) and its own validation (`route.parse`).
1601
+ *
1602
+ * - **Node (build):** `write(entries)` persists one JSON file per page, keyed by
1603
+ * the page's URL via {@link DataProvider.fileFor}. `build` supplies the entries
1604
+ * (it already expanded the routes), so there is no duplicate expansion here.
1605
+ * - **Browser (runtime):** `at(path)` fetches + caches that file as `unknown`; the
1606
+ * route's `parse` validates it into the route's data type before `render`.
1607
+ *
1608
+ * The Node-only file-writing code (`node:fs`) is isolated behind a lazy `import()`
1609
+ * inside `write()`, so composing `data` in a browser app keeps the bundle free of
1610
+ * `node:*`.
1611
+ */
1612
+ /**
1613
+ * Configuration for {@link dataPlugin}. All fields have defaults (see `./config`).
1614
+ *
1615
+ * @example
1616
+ * ```ts
1617
+ * const cfg: DataConfig = { outputDir: "_data", baseUrl: "/_data/" };
1618
+ * ```
1619
+ */
1620
+ type DataConfig = {
1621
+ /**
1622
+ * WRITE side (Node): output subdir relative to the build `outDir`, a filesystem
1623
+ * path where `write()` persists the per-page JSON. Default `"_data"`.
1624
+ */
1625
+ outputDir: string;
1626
+ /**
1627
+ * READ side (browser): site-root-relative URL the client fetches the per-page
1628
+ * JSON from. A different domain from {@link DataConfig.outputDir} (a filesystem
1629
+ * path); keep consistent (`"/" + trim(outputDir) + "/"`). Default `"/_data/"`.
1630
+ */
1631
+ baseUrl: string;
1632
+ };
1633
+ /** One page's data to persist — `build` produces these from its route expansion. */
1634
+ interface DataEntry {
1635
+ /** The page's URL path (e.g. `/en/hello/`); maps to a file via {@link DataProvider.fileFor}. */
1636
+ path: string;
1637
+ /** The serializable data for this page (the route's `load`/projection output). */
1638
+ data: unknown;
1639
+ }
1640
+ /** Summary returned by {@link DataProvider.write} and cached in state. */
1641
+ interface DataWriteSummary {
1642
+ /** Number of per-page JSON files written. */
1643
+ fileCount: number;
1644
+ /** Total bytes written across all files. */
1645
+ bytes: number;
1646
+ /** The written file paths, relative to the build `outDir`. */
1647
+ files: string[];
1648
+ }
1649
+ /**
1650
+ * Internal data state. `lastWrite` records the most recent `write()` (Node);
1651
+ * `cache` memoizes fetched per-path data (browser, lazy). Both empty until their
1652
+ * respective side first runs.
1653
+ */
1654
+ interface DataState {
1655
+ /** Result of the last `write()`, or `null` if it has not run yet (Node). */
1656
+ lastWrite: DataWriteSummary | null;
1657
+ /** Per-path fetched data, cached after the first `at(path)` (browser). */
1658
+ cache: Map<string, unknown>;
1659
+ }
1660
+ /**
1661
+ * Public API mounted at `app.data` — the agnostic data provider. `write()` is the
1662
+ * Node persist side; `at()` is the browser read side; `urlFor`/`fileFor` are the
1663
+ * pure URL convention shared by both so the written file and fetched URL can never
1664
+ * drift.
1665
+ *
1666
+ * @example
1667
+ * ```ts
1668
+ * // Node build (build supplies the entries it already expanded):
1669
+ * await app.data.write([{ path: "/en/hello/", data: article }]);
1670
+ *
1671
+ * // Browser (inside spa nav): fetch the page's data, then route.parse validates it:
1672
+ * const raw = await app.data.at("/en/hello/"); // unknown | null
1673
+ * ```
1674
+ */
1675
+ type DataProvider = {
1676
+ /**
1677
+ * READ (browser) — fetch (and cache) the persisted data for a page path from
1678
+ * `config.baseUrl`. Returns the raw parsed JSON as `unknown` (the caller's
1679
+ * `route.parse` validates it), or `null` if the fetch/parse fails.
1680
+ *
1681
+ * @param path - The page URL path (e.g. `/en/hello/`).
1682
+ * @returns The page's raw data, or `null` on failure.
1683
+ */
1684
+ at(path: string): Promise<unknown | null>;
1685
+ /**
1686
+ * WRITE (Node) — persist one JSON file per entry, keyed by page path via
1687
+ * {@link DataProvider.fileFor}. Called by `build` after it expands routes (no
1688
+ * duplicate expansion). Lazily loads its `node:fs` writer, so it never
1689
+ * contaminates a browser bundle.
1690
+ *
1691
+ * @param entries - The per-page data to persist.
1692
+ * @param options - Optional overrides.
1693
+ * @param options.outDir - Build output directory to write under (default `./dist`).
1694
+ * @returns A summary of the written files.
1695
+ */
1696
+ write(entries: readonly DataEntry[], options?: {
1697
+ outDir?: string;
1698
+ }): Promise<DataWriteSummary>;
1699
+ /**
1700
+ * PURE — the browser fetch URL for a page path (e.g. `/en/hello/` →
1701
+ * `/_data/en/hello/index.json`). Shared with {@link DataProvider.fileFor}.
1702
+ *
1703
+ * @param path - The page URL path.
1704
+ * @returns The site-root-relative data URL.
1705
+ */
1706
+ urlFor(path: string): string;
1707
+ /**
1708
+ * PURE — the `outDir`-relative file path for a page path (e.g. `/en/hello/` →
1709
+ * `_data/en/hello/index.json`). Shared with {@link DataProvider.urlFor}.
1710
+ *
1711
+ * @param path - The page URL path.
1712
+ * @returns The output-relative file path.
1713
+ */
1714
+ fileFor(path: string): string;
1715
+ };
1716
+ //#endregion
1717
+ //#region src/plugins/data/index.d.ts
1718
+ /**
1719
+ * Data plugin — the agnostic data provider. Mounts `write(entries)` (Node persist),
1720
+ * `at(path)` (browser read), and the pure `urlFor`/`fileFor` convention at `app.data`.
1721
+ *
1722
+ * @example
1723
+ * ```ts
1724
+ * // Node build: `build` calls app.data.write(...) during its pages phase when
1725
+ * // router.mode !== "ssg". Just compose the plugin:
1726
+ * const app = createApp({
1727
+ * plugins: [dataPlugin, contentPlugin, buildPlugin],
1728
+ * pluginConfigs: { content: { contentDir: "./content" }, router: { routes, mode: "hybrid" } }
1729
+ * });
1730
+ * await app.start();
1731
+ * await app.build.run(); // writes HTML + per-page data sidecars
1732
+ *
1733
+ * // Browser app: compose `dataPlugin` too; spa fetches via app.data.at(path) on nav.
1734
+ * ```
1735
+ */
1736
+ declare const dataPlugin: import("@moku-labs/core").PluginInstance<"data", DataConfig, DataState, DataProvider, {}> & Record<never, never>;
1737
+ //#endregion
1738
+ //#region src/plugins/log/index.d.ts
1739
+ /**
1740
+ * Core logging plugin — always-on in-memory trace + `expect()` event-trace DSL.
1741
+ * API injected as `ctx.log` on every regular plugin and surfaced as `app.log`.
1742
+ * No depends / events / hooks (core plugin per spec/03 §5).
1743
+ *
1744
+ * @see README.md
1745
+ */
1746
+ declare const logPlugin: import("@moku-labs/core").CorePluginInstance<"log", LogConfig, LogState, LogApi>;
1747
+ //#endregion
1748
+ //#region src/browser.d.ts
1749
+ /**
1750
+ * Create and initialize a browser-safe `@moku-labs/web` application — the Layer-3
1751
+ * entry point for client bundles. Identical to the main entry's `createApp`, but
1752
+ * this module's import graph contains zero node/native code, and `env` defaults to
1753
+ * the `browserEnv()` provider (reads `import.meta.env` / `globalThis.__ENV__`).
1754
+ *
1755
+ * The defaults are the isomorphic plugin set (`site`, `i18n`, `router`, `head`,
1756
+ * `spa` + `log`/`env` core). For client-data navigation (`router.mode("spa"|"hybrid")`)
1757
+ * compose the `data` plugin — its consume-half (`at()`) is browser-safe.
1758
+ *
1759
+ * @param options - Optional configuration:
1760
+ * - `pluginConfigs` — per-plugin overrides, keyed by plugin name.
1761
+ * - `config` — global framework config (e.g. `{ mode: "development" }`).
1762
+ * - `plugins` — extra plugins (e.g. `dataPlugin` or your own) merged into the app and its type.
1763
+ * - `onReady` / `onError` / `onStart` / `onStop` — lifecycle callbacks.
1764
+ * @returns The initialized app: `start()`, `stop()`, every plugin's API, and `log`.
1765
+ * @example
1766
+ * ```ts
1767
+ * // Client SPA — env works with no wiring (browserEnv is the default provider):
1768
+ * const app = createApp({
1769
+ * plugins: [dataPlugin],
1770
+ * pluginConfigs: {
1771
+ * router: { mode: "spa", routes: defineRoutes({ home: route("/"), post: route("/blog/{slug}/") }) }
1772
+ * }
1773
+ * });
1774
+ * await app.start();
1775
+ * app.env.get("PUBLIC_API_URL"); // resolved from import.meta.env
1776
+ * ```
1777
+ */
1778
+ declare const createApp: <const ExtraPlugins extends readonly import("@moku-labs/core").AnyPluginInstance[] = readonly []>(options?: import("@moku-labs/core").CreateAppOptions<Config$4, Events, (import("@moku-labs/core").PluginInstance<"site", Config$3, Record<string, never>, Api$3, {}> & Record<never, never>) | (import("@moku-labs/core").PluginInstance<"i18n", Config$2, Record<string, never>, Api$2, {}> & Record<never, never>) | (import("@moku-labs/core").PluginInstance<"router", RouterConfig, RouterState, RouterApi, {}> & {
1779
+ route: typeof route;
1780
+ defineRoutes: typeof defineRoutes;
1781
+ }) | (import("@moku-labs/core").PluginInstance<"head", Config, State, Api, {}> & {
1782
+ meta: typeof meta;
1783
+ og: typeof og;
1784
+ twitter: typeof twitter;
1785
+ jsonLd: typeof jsonLd;
1786
+ canonical: typeof canonical;
1787
+ hreflang: typeof hreflang;
1788
+ feedLink: typeof feedLink;
1789
+ buildArticleHead: typeof buildArticleHead;
1790
+ }) | (import("@moku-labs/core").PluginInstance<"spa", SpaConfig, SpaState, SpaApi, {
1791
+ "spa:navigate": {
1792
+ from: string;
1793
+ to: string;
1794
+ };
1795
+ "spa:navigated": {
1796
+ url: string;
1797
+ };
1798
+ "spa:component-mount": {
1799
+ name: string;
1800
+ el: Element;
1801
+ };
1802
+ "spa:component-unmount": {
1803
+ name: string;
1804
+ el: Element;
1805
+ };
1806
+ }> & 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$4, Events, (import("@moku-labs/core").PluginInstance<"site", Config$3, Record<string, never>, Api$3, {}> & Record<never, never>) | (import("@moku-labs/core").PluginInstance<"i18n", Config$2, Record<string, never>, Api$2, {}> & Record<never, never>) | (import("@moku-labs/core").PluginInstance<"router", RouterConfig, RouterState, RouterApi, {}> & {
1807
+ route: typeof route;
1808
+ defineRoutes: typeof defineRoutes;
1809
+ }) | (import("@moku-labs/core").PluginInstance<"head", Config, State, Api, {}> & {
1810
+ meta: typeof meta;
1811
+ og: typeof og;
1812
+ twitter: typeof twitter;
1813
+ jsonLd: typeof jsonLd;
1814
+ canonical: typeof canonical;
1815
+ hreflang: typeof hreflang;
1816
+ feedLink: typeof feedLink;
1817
+ buildArticleHead: typeof buildArticleHead;
1818
+ }) | (import("@moku-labs/core").PluginInstance<"spa", SpaConfig, SpaState, SpaApi, {
1819
+ "spa:navigate": {
1820
+ from: string;
1821
+ to: string;
1822
+ };
1823
+ "spa:navigated": {
1824
+ url: string;
1825
+ };
1826
+ "spa:component-mount": {
1827
+ name: string;
1828
+ el: Element;
1829
+ };
1830
+ "spa:component-unmount": {
1831
+ name: string;
1832
+ el: Element;
1833
+ };
1834
+ }> & Record<never, never>) | ExtraPlugins[number], import("@moku-labs/core").CoreApisFromTuple<[import("@moku-labs/core").CorePluginInstance<"log", LogConfig, LogState, LogApi>, import("@moku-labs/core").CorePluginInstance<"env", EnvConfig, EnvState, EnvApi>]>>;
1835
+ /**
1836
+ * Create a custom plugin bound to this framework's `Config`/`Events` and core
1837
+ * APIs. Plugin types are inferred from the spec object — never written explicitly.
1838
+ * Pass the result to {@link createApp} via `plugins`.
1839
+ *
1840
+ * @example
1841
+ * ```ts
1842
+ * const analytics = createPlugin("analytics", {
1843
+ * config: { writeKey: "" },
1844
+ * api: (ctx) => ({ track: (event: string) => ctx.log.info("analytics:track", { event }) })
1845
+ * });
1846
+ *
1847
+ * const app = createApp({ plugins: [analytics] });
1848
+ * ```
1849
+ */
1850
+ declare const createPlugin: import("@moku-labs/core").BoundCreatePluginFunction<Config$4, Events, import("@moku-labs/core").CoreApisFromTuple<[import("@moku-labs/core").CorePluginInstance<"log", LogConfig, LogState, LogApi>, import("@moku-labs/core").CorePluginInstance<"env", EnvConfig, EnvState, EnvApi>]>>;
1851
+ //#endregion
1852
+ export { types_d_exports as Data, types_d_exports$1 as Env, types_d_exports$2 as Head, types_d_exports$3 as Log, types_d_exports$4 as Router, types_d_exports$5 as Spa, browserEnv, buildArticleHead, canonical, createApp, createComponent, createPlugin, dataPlugin, defineRoutes, envPlugin, feedLink, headPlugin, hreflang, i18nPlugin, jsonLd, logPlugin, meta, og, route, routerPlugin, sitePlugin, spaPlugin, twitter };