@moku-labs/core 0.1.0-alpha.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,900 @@
1
+ //#region src/utilities.d.ts
2
+ /**
3
+ * Base constraint for framework configuration objects.
4
+ *
5
+ * @example
6
+ * ```ts
7
+ * function createConfig<C extends FrameworkConfig>(defaults: C): C { return defaults; }
8
+ * ```
9
+ */
10
+ type FrameworkConfig = Record<string, unknown>;
11
+ /**
12
+ * Base constraint for framework event maps.
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * function createEvents<E extends FrameworkEventMap>(events: E): E { return events; }
17
+ * ```
18
+ */
19
+ type FrameworkEventMap = Record<string, unknown>;
20
+ /**
21
+ * Empty event map used as the default when a plugin declares no custom events.
22
+ * `Record<never, never>` is the identity element for intersection (`T & {} = T`).
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * type PluginEvents = EmptyPluginEventMap; // default when no events declared
27
+ * ```
28
+ */
29
+ type EmptyPluginEventMap = Record<never, never>;
30
+ /**
31
+ * Convert a union to an intersection via distributive conditional + contra-variance.
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * type Result = UnionToIntersection<{ a: 1 } | { b: 2 }>; // { a: 1 } & { b: 2 }
36
+ * ```
37
+ */
38
+ type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
39
+ /**
40
+ * Detect if a string type is a literal (e.g. "router") vs the general `string` type.
41
+ * Used by BuildPluginApis to exclude plugins with non-literal names from the mapped type.
42
+ *
43
+ * @example
44
+ * ```ts
45
+ * type Yes = IsLiteralString<"router">; // true
46
+ * type No = IsLiteralString<string>; // false
47
+ * ```
48
+ */
49
+ type IsLiteralString<S extends string> = string extends S ? false : true;
50
+ /**
51
+ * Auto-generate overloaded emit call signatures from an event map type.
52
+ *
53
+ * Converts `{ "a": X; "b": Y }` into `((name: "a", payload: X) => void) & ((name: "b", payload: Y) => void)`.
54
+ * Uses `UnionToIntersection` to transform a union of per-event functions into
55
+ * an intersection (overloaded call signatures).
56
+ *
57
+ * When `E` has no keys, produces an uncallable but constructible function type
58
+ * so `vi.fn()` and `() => {}` remain assignable while calling emit is a compile error.
59
+ *
60
+ * Exported from the package entry point for plugin authors at Standard+ tier.
61
+ *
62
+ * @example
63
+ * ```ts
64
+ * type CmsEmit = EmitFn<{
65
+ * "cms:publish": { contentId: string; path: string };
66
+ * "cms:draft": { contentId: string };
67
+ * }>;
68
+ * // Equivalent to:
69
+ * // ((name: "cms:publish", payload: { contentId: string; path: string }) => void)
70
+ * // & ((name: "cms:draft", payload: { contentId: string }) => void)
71
+ * ```
72
+ */
73
+ type EmitFunction<E extends Record<string, unknown>> = [keyof E] extends [never] ? (...arguments_: never[]) => void : UnionToIntersection<{ [K in string & keyof E]: (name: K, payload: E[K]) => void }[string & keyof E]>;
74
+ /**
75
+ * Checks whether a value is a non-null, non-array object record.
76
+ *
77
+ * @param value - Value to inspect.
78
+ * @returns `true` when value is an object record.
79
+ * @example
80
+ * ```ts
81
+ * isRecord({ key: "value" }); // => true
82
+ * isRecord([1, 2, 3]); // => false
83
+ * isRecord(null); // => false
84
+ * ```
85
+ */
86
+ //#endregion
87
+ //#region src/types.d.ts
88
+ /**
89
+ * Teardown context — the most minimal context tier.
90
+ * Used by: onStop
91
+ *
92
+ * During teardown, plugins may be partially or fully stopped. Only the frozen
93
+ * global config is available.
94
+ *
95
+ * @example
96
+ * ```ts
97
+ * type StopCtx = TeardownContext<{ siteName: string }>;
98
+ * // => { readonly global: Readonly<{ siteName: string }> }
99
+ *
100
+ * // Used in plugin spec:
101
+ * onStop: (ctx: StopCtx) => { console.log(`Stopping ${ctx.global.siteName}`); }
102
+ * ```
103
+ */
104
+ type TeardownContext<Config> = {
105
+ readonly global: Readonly<Config>;
106
+ };
107
+ /**
108
+ * Minimal context — teardown context plus plugin config.
109
+ * Used by: createState
110
+ *
111
+ * At this stage, not all plugins have been created yet. Communication methods
112
+ * (emit, require, has) are intentionally unavailable.
113
+ *
114
+ * @example
115
+ * ```ts
116
+ * type StateCtx = MinimalContext<{ siteName: string }, { basePath: string }>;
117
+ * // => { readonly global: ...; readonly config: Readonly<{ basePath: string }> }
118
+ *
119
+ * // Used in plugin spec:
120
+ * createState: (ctx: StateCtx) => ({ currentPath: ctx.config.basePath })
121
+ * ```
122
+ */
123
+ type MinimalContext<Config, C> = {
124
+ readonly global: Readonly<Config>;
125
+ readonly config: Readonly<C>;
126
+ };
127
+ /**
128
+ * Full plugin context — everything is live.
129
+ * Used by: api, onInit, onStart
130
+ *
131
+ * Provides global config, plugin config, mutable state, event emission,
132
+ * and inter-plugin communication.
133
+ *
134
+ * @example
135
+ * ```ts
136
+ * type Ctx = PluginContext<
137
+ * { siteName: string },
138
+ * { "page:view": { path: string } },
139
+ * { basePath: string },
140
+ * { count: number }
141
+ * >;
142
+ * // => { global, config, state, emit, require, has }
143
+ *
144
+ * // Used in plugin spec:
145
+ * api: (ctx: Ctx) => ({
146
+ * navigate: (path: string) => { ctx.state.count += 1; ctx.emit("page:view", { path }); }
147
+ * })
148
+ * ```
149
+ */
150
+ type PluginContext<Config, Events extends Record<string, unknown>, C, S> = {
151
+ readonly global: Readonly<Config>;
152
+ readonly config: Readonly<C>;
153
+ state: S;
154
+ emit: EmitFunction$1<Events>;
155
+ require: RequireFunction;
156
+ has: HasFunction;
157
+ };
158
+ /**
159
+ * Strictly typed emit function (kernel-layer generic signature).
160
+ * Only known event names are accepted, with matching payload required.
161
+ *
162
+ * This is the compile-time generic signature used by PluginContext and App.
163
+ * app.ts defines a separate runtime-layer `EmitFunction` alias without generics
164
+ * for dynamically typed dispatch — they are intentionally different.
165
+ *
166
+ * @example
167
+ * ```ts
168
+ * type Emit = EmitFunction<{ "page:view": { path: string }; "auth:login": { userId: string } }>;
169
+ *
170
+ * declare const emit: Emit;
171
+ * emit("page:view", { path: "/" }); // OK — known event with correct payload
172
+ * // emit("page:view", { url: "/" }); // Error — wrong payload shape
173
+ * // emit("unknown", {}); // Error — unknown event name
174
+ * ```
175
+ */
176
+ type EmitFunction$1<Events extends Record<string, unknown>> = <K extends string & keyof Events>(name: K, payload: Events[K]) => void;
177
+ /**
178
+ * Get a dependency plugin's API by instance reference.
179
+ * Accepts only PluginInstance values (not strings). Returns the fully typed API
180
+ * extracted from the phantom type, or throws at runtime if not registered.
181
+ *
182
+ * @example
183
+ * ```ts
184
+ * declare const require: RequireFunction;
185
+ * const api = require(routerPlugin); // => typeof routerPlugin's API
186
+ * // api.navigate("/about"); // fully typed — method comes from phantom type
187
+ * ```
188
+ */
189
+ type RequireFunction = <P extends PluginInstance<string, any, any, any, any>>(plugin: P) => ExtractApi<P>;
190
+ /**
191
+ * Check if a plugin is registered by name. String-based boolean check.
192
+ * Unlike require, this accepts a plain string and returns a boolean instead of throwing.
193
+ *
194
+ * @example
195
+ * ```ts
196
+ * declare const has: HasFunction;
197
+ * if (has("analytics")) {
198
+ * // analytics plugin is registered — safe to require
199
+ * }
200
+ * ```
201
+ */
202
+ type HasFunction = (name: string) => boolean;
203
+ /**
204
+ * Plugin specification — the shape passed to createPlugin.
205
+ *
206
+ * All generics (N, C, S, A) are inferred from the spec object values.
207
+ * Config and Events flow from the createCoreConfig closure.
208
+ * PluginEvents is the only explicit generic (defines new events).
209
+ *
210
+ * @example
211
+ * ```ts
212
+ * // Rarely written explicitly — inferred from createPlugin spec object.
213
+ * // The kernel uses this type internally to constrain spec shapes:
214
+ * type RouterSpec = PluginSpec<SiteConfig, SiteEvents, RouterEvents,
215
+ * { basePath: string }, { currentPath: string },
216
+ * { navigate(path: string): void }, readonly []>;
217
+ * ```
218
+ */
219
+ type PluginSpec<Config, Events extends Record<string, unknown>, PluginEvents extends Record<string, unknown>, C, S, A extends Record<string, any>, Deps extends ReadonlyArray<PluginInstance<string, any, any, any, any>>> = {
220
+ config?: C;
221
+ depends?: Deps;
222
+ createState?: (context: MinimalContext<Config, C>) => S;
223
+ api?: (context: PluginContext<Config, Events & PluginEvents & DepsEvents<Deps>, C, S>) => A;
224
+ onInit?: (context: PluginContext<Config, Events & PluginEvents & DepsEvents<Deps>, C, S>) => void | Promise<void>;
225
+ onStart?: (context: PluginContext<Config, Events & PluginEvents & DepsEvents<Deps>, C, S>) => void | Promise<void>;
226
+ onStop?: (context: TeardownContext<Config>) => void | Promise<void>;
227
+ hooks?: (context: PluginContext<Config, Events & PluginEvents & DepsEvents<Deps>, C, S>) => { [K in string & keyof (Events & PluginEvents & DepsEvents<Deps>)]?: (payload: (Events & PluginEvents & DepsEvents<Deps>)[K]) => void | Promise<void> };
228
+ };
229
+ /**
230
+ * Plugin instance — the return value of createPlugin.
231
+ *
232
+ * Carries phantom types for compile-time type inference. The _phantom field
233
+ * is never read at runtime (it is `{} as { ... }`).
234
+ *
235
+ * @example
236
+ * ```ts
237
+ * // Created via createPlugin — types are inferred, not written:
238
+ * const router = createPlugin("router", {
239
+ * config: { basePath: "/" },
240
+ * api: ctx => ({ navigate: (path: string) => path }),
241
+ * });
242
+ * // router: PluginInstance<"router", { basePath: string }, ..., { navigate(path: string): string }>
243
+ * ```
244
+ */
245
+ interface PluginInstance<N extends string = string, C = void, S = void, A extends Record<string, any> = Record<string, never>, PluginEvents extends Record<string, unknown> = {}> {
246
+ readonly name: N;
247
+ readonly spec: PluginSpec<any, any, any, C, S, A, any>;
248
+ readonly _phantom: {
249
+ config: C;
250
+ state: S;
251
+ api: A;
252
+ events: PluginEvents;
253
+ };
254
+ }
255
+ /**
256
+ * Widened PluginInstance type for generic constraints on arrays.
257
+ * Used across multiple modules (utilities, core, app) for plugin list parameters.
258
+ *
259
+ * @example
260
+ * ```ts
261
+ * function processPlugins(plugins: AnyPluginInstance[]): void { ... }
262
+ * ```
263
+ */
264
+ type AnyPluginInstance = PluginInstance<string, any, any, any, any>;
265
+ /**
266
+ * Extract the API phantom type from a PluginInstance.
267
+ * Used by RequireFunction to return the correct API type from require(plugin).
268
+ *
269
+ * @example
270
+ * ```ts
271
+ * type RouterApi = ExtractApi<typeof routerPlugin>; // { navigate(path: string): void }
272
+ * ```
273
+ */
274
+ type ExtractApi<P> = P extends PluginInstance<string, any, any, infer A, any> ? A : never;
275
+ /**
276
+ * Extract the events phantom type from a PluginInstance.
277
+ * Used by DepsEvents to merge event maps from dependency plugins.
278
+ *
279
+ * @example
280
+ * ```ts
281
+ * type RouterEvents = ExtractEvents<typeof routerPlugin>;
282
+ * // { "router:navigate": { from: string; to: string } }
283
+ * ```
284
+ */
285
+ type ExtractEvents<P> = P extends PluginInstance<string, any, any, any, infer E> ? E : never;
286
+ /**
287
+ * Extract the name literal type from a PluginInstance.
288
+ * Used by BuildPluginApis to key the app surface by plugin name.
289
+ *
290
+ * @example
291
+ * ```ts
292
+ * type Name = ExtractName<typeof routerPlugin>; // "router"
293
+ * ```
294
+ */
295
+ type ExtractName<P> = P extends PluginInstance<infer N, any, any, any, any> ? N : never;
296
+ /**
297
+ * Extract the config phantom type from a PluginInstance.
298
+ * Used by CreateAppOptions to type pluginConfigs keys.
299
+ *
300
+ * @example
301
+ * ```ts
302
+ * type RouterConfig = ExtractConfig<typeof routerPlugin>; // { basePath: string }
303
+ * ```
304
+ */
305
+ type ExtractConfig<P> = P extends PluginInstance<string, infer C, any, any, any> ? C : never;
306
+ /**
307
+ * Intersection of all PluginEvents from a depends tuple.
308
+ * Merges events from [authPlugin, routerPlugin] into AuthEvents & RouterEvents.
309
+ * Falls back to `{}` (identity element) when the tuple is empty.
310
+ *
311
+ * @example
312
+ * ```ts
313
+ * type Combined = DepsEvents<readonly [typeof authPlugin, typeof routerPlugin]>;
314
+ * // => { "auth:login": { userId: string } } & { "router:navigate": { path: string } }
315
+ * ```
316
+ */
317
+ type DepsEvents<Deps extends ReadonlyArray<PluginInstance<string, any, any, any, any>>> = Deps[number] extends never ? {} : UnionToIntersection<ExtractEvents<Deps[number]>>;
318
+ /**
319
+ * Map a plugin tuple to `{ [Name]: Api }` for the app surface.
320
+ * Plugins with empty API (Record<string, never>) are excluded.
321
+ * Plugins with non-literal name type (string) are excluded to prevent
322
+ * index signature pollution on the App type.
323
+ *
324
+ * @example
325
+ * ```ts
326
+ * type Apis = BuildPluginApis<typeof routerPlugin | typeof authPlugin>;
327
+ * // => { router: { navigate(path: string): void }; auth: { login(): void } }
328
+ * ```
329
+ */
330
+ type BuildPluginApis<P extends PluginInstance<string, any, any, any, any>> = { [K in P as ExtractApi<K> extends Record<string, never> ? never : IsLiteralString<ExtractName<K>> extends true ? ExtractName<K> : never]: ExtractApi<K> };
331
+ /**
332
+ * Typed App object returned by createApp.
333
+ * Combines base methods (start, stop, emit, require, has) with plugin APIs
334
+ * mapped by name via BuildPluginApis.
335
+ *
336
+ * @example
337
+ * ```ts
338
+ * type MyApp = App<SiteConfig, SiteEvents, typeof routerPlugin | typeof authPlugin>;
339
+ * // => { start, stop, emit, require, has, router: RouterApi, auth: AuthApi }
340
+ *
341
+ * declare const app: MyApp;
342
+ * await app.start();
343
+ * app.router.navigate("/about");
344
+ * ```
345
+ */
346
+ type App<_Config extends Record<string, unknown>, Events extends Record<string, unknown>, P extends PluginInstance<string, any, any, any, any>> = {
347
+ readonly start: () => Promise<void>;
348
+ readonly stop: () => Promise<void>;
349
+ readonly emit: EmitFunction$1<Events>;
350
+ readonly require: RequireFunction;
351
+ readonly has: HasFunction;
352
+ } & BuildPluginApis<P>;
353
+ /**
354
+ * Context passed to consumer lifecycle callbacks (onReady, onStart, onStop).
355
+ * Includes frozen config, event emission, plugin lookup, and mounted plugin APIs.
356
+ *
357
+ * @example
358
+ * ```ts
359
+ * type Ctx = AppCallbackContext<SiteConfig, SiteEvents, typeof routerPlugin>;
360
+ * // => { config, emit, require, has, router: RouterApi }
361
+ *
362
+ * // Used in createApp options:
363
+ * onReady: (ctx: Ctx) => { ctx.router.navigate("/home"); }
364
+ * ```
365
+ */
366
+ type AppCallbackContext<Config extends Record<string, unknown>, Events extends Record<string, unknown>, P extends PluginInstance<string, any, any, any, any>> = {
367
+ readonly config: Readonly<Config>;
368
+ readonly emit: EmitFunction$1<Events>;
369
+ readonly require: RequireFunction;
370
+ readonly has: HasFunction;
371
+ } & BuildPluginApis<P>;
372
+ /**
373
+ * Options for createApp (Step 3). Structured namespaces replace flat key discrimination:
374
+ * - `plugins`: extra consumer plugins
375
+ * - `config`: global config overrides (shallow-merged with framework defaults)
376
+ * - `pluginConfigs`: per-plugin config overrides keyed by plugin name
377
+ * - `onReady/onError/onStart/onStop`: consumer lifecycle callbacks
378
+ *
379
+ * @example
380
+ * ```ts
381
+ * const app = await createApp({
382
+ * config: { siteName: "My Blog" },
383
+ * pluginConfigs: { router: { basePath: "/blog" } },
384
+ * onReady: ctx => { console.log("App ready:", ctx.config.siteName); },
385
+ * });
386
+ * ```
387
+ */
388
+ type CreateAppOptions<Config extends Record<string, unknown>, Events extends Record<string, unknown>, P extends PluginInstance<string, any, any, any, any>, ExtraPlugins extends readonly PluginInstance<string, any, any, any, any>[]> = {
389
+ plugins?: ExtraPlugins;
390
+ config?: { [K in keyof Config]?: Config[K] };
391
+ pluginConfigs?: { [K in P as ExtractConfig<K> extends Record<string, never> ? never : IsLiteralString<ExtractName<K>> extends true ? ExtractName<K> : never]?: Partial<ExtractConfig<K>> };
392
+ onReady?: (context: AppCallbackContext<Config, Events, P>) => void | Promise<void>;
393
+ onError?: (error: Error, context: AppCallbackContext<Config, Events, P>) => void;
394
+ onStart?: (context: AppCallbackContext<Config, Events, P>) => void | Promise<void>;
395
+ onStop?: (context: AppCallbackContext<Config, Events, P>) => void | Promise<void>;
396
+ };
397
+ /**
398
+ * Domain context type for extracted plugin files (api.ts, handlers.ts, etc.).
399
+ *
400
+ * Provides `config`, `state`, and strictly typed `emit` with overloaded call
401
+ * signatures auto-generated from the event map `E`. Replaces manual overload
402
+ * declarations that must be kept in sync with the event type.
403
+ *
404
+ * For plugins without events, omit `E` — emit becomes uncallable.
405
+ * For advanced composition (e.g., adding `require`), use `EmitFn<E>` directly.
406
+ *
407
+ * @example
408
+ * ```ts
409
+ * import type { PluginCtx } from "@moku-labs/core";
410
+ *
411
+ * type CmsEvents = {
412
+ * "cms:publish": { contentId: string; path: string };
413
+ * "cms:draft": { contentId: string };
414
+ * };
415
+ *
416
+ * // One line replaces manual emit overloads:
417
+ * type CmsCtx = PluginCtx<CmsConfig, CmsState, CmsEvents>;
418
+ *
419
+ * // Use in domain factories:
420
+ * export const createCmsApi = (ctx: CmsCtx) => ({
421
+ * publish: (id: string, path: string) => {
422
+ * ctx.emit("cms:publish", { contentId: id, path }); // strictly typed
423
+ * },
424
+ * });
425
+ * ```
426
+ */
427
+ type PluginContext_<C, S, E extends Record<string, unknown> = Record<never, never>> = {
428
+ readonly config: Readonly<C>;
429
+ state: S;
430
+ emit: EmitFunction<E>;
431
+ };
432
+ //#endregion
433
+ //#region src/plugin.d.ts
434
+ /**
435
+ * Structural plugin shape used for generic constraints without variance issues.
436
+ *
437
+ * @example
438
+ * ```ts
439
+ * const pluginRef: PluginLike = somePlugin;
440
+ * ```
441
+ */
442
+ type PluginLike = {
443
+ readonly name: string;
444
+ readonly spec: unknown;
445
+ readonly _phantom: {
446
+ readonly config: unknown;
447
+ readonly state: unknown;
448
+ readonly api: unknown;
449
+ readonly events: Record<string, unknown>;
450
+ };
451
+ };
452
+ /**
453
+ * Readonly dependency tuple accepted by `depends`.
454
+ *
455
+ * @example
456
+ * ```ts
457
+ * const deps: DependencyPluginTuple = [];
458
+ * ```
459
+ */
460
+ type DependencyPluginTuple = readonly PluginLike[];
461
+ /**
462
+ * Extracts API type from a plugin-like value.
463
+ *
464
+ * @example
465
+ * ```ts
466
+ * type Api = ExtractPluginApi<typeof somePlugin>;
467
+ * ```
468
+ */
469
+ type ExtractPluginApi<PluginCandidate> = PluginCandidate extends {
470
+ readonly _phantom: {
471
+ readonly api: infer PluginApi;
472
+ };
473
+ } ? PluginApi : never;
474
+ /**
475
+ * Extracts event map type from a plugin-like value.
476
+ *
477
+ * @example
478
+ * ```ts
479
+ * type Events = ExtractPluginEvents<typeof somePlugin>;
480
+ * ```
481
+ */
482
+ type ExtractPluginEvents<PluginCandidate> = PluginCandidate extends {
483
+ readonly _phantom: {
484
+ readonly events: infer PluginEvents;
485
+ };
486
+ } ? PluginEvents extends Record<string, unknown> ? PluginEvents : EmptyPluginEventMap : EmptyPluginEventMap;
487
+ /**
488
+ * Event map contributed by all dependency plugins.
489
+ *
490
+ * @example
491
+ * ```ts
492
+ * type DepEvents = DependencyEvents<readonly [authPlugin, routerPlugin]>;
493
+ * ```
494
+ */
495
+ type DependencyEvents<DependencyPlugins extends DependencyPluginTuple> = DependencyPlugins[number] extends never ? EmptyPluginEventMap : UnionToIntersection<ExtractPluginEvents<DependencyPlugins[number]>>;
496
+ /**
497
+ * Intersection of framework events, plugin events, and dependency events.
498
+ *
499
+ * @example
500
+ * ```ts
501
+ * type AllEvents = MergedPluginEvents<SiteConfigEvents, RouterEvents, readonly [authPlugin]>;
502
+ * ```
503
+ */
504
+ type MergedPluginEvents<GlobalEventMap extends FrameworkEventMap, PluginEventMap extends Record<string, unknown>, DependencyPlugins extends DependencyPluginTuple> = GlobalEventMap & PluginEventMap & DependencyEvents<DependencyPlugins>;
505
+ /**
506
+ * Runtime context for `api`, `onInit`, and `onStart`.
507
+ *
508
+ * Generic parameters:
509
+ * - `GlobalConfig`: frozen app-wide config from `createCoreConfig`
510
+ * - `AllEvents`: merged events the plugin can emit
511
+ * - `PluginConfig`: this plugin's config slice
512
+ * - `PluginState`: mutable plugin state
513
+ *
514
+ * @example
515
+ * ```ts
516
+ * type Ctx = PluginExecutionContext<
517
+ * SiteConfig,
518
+ * SiteEvents,
519
+ * { basePath: string },
520
+ * { currentPath: string }
521
+ * >;
522
+ * ```
523
+ */
524
+ type PluginExecutionContext<GlobalConfig extends FrameworkConfig, AllEvents extends Record<string, unknown>, PluginConfig, PluginState> = {
525
+ /**
526
+ * Frozen app-wide config from `createCoreConfig`. Shared by all plugins.
527
+ *
528
+ * @example
529
+ * ```ts
530
+ * const siteUrl = ctx.global.siteUrl;
531
+ * ```
532
+ */
533
+ readonly global: Readonly<GlobalConfig>;
534
+ /**
535
+ * Frozen plugin-specific config. Defaults from the plugin spec, shallow-merged
536
+ * with consumer overrides.
537
+ *
538
+ * @example
539
+ * ```ts
540
+ * const base = ctx.config.basePath; // from plugin defaults or consumer override
541
+ * ```
542
+ */
543
+ readonly config: Readonly<PluginConfig>;
544
+ /**
545
+ * Mutable plugin state created by `createState`. The only mutable store in the system.
546
+ *
547
+ * @example
548
+ * ```ts
549
+ * ctx.state.count += 1;
550
+ * ctx.state.cache.set(key, value);
551
+ * ```
552
+ */
553
+ state: PluginState;
554
+ /**
555
+ * Dispatch a typed event. Only known event names are accepted (no escape hatch).
556
+ *
557
+ * @example
558
+ * ```ts
559
+ * ctx.emit("auth:login", { userId: "123" });
560
+ * ```
561
+ */
562
+ emit: EmitFunction$1<AllEvents>;
563
+ /**
564
+ * Get a registered plugin's API by instance reference. Throws if the plugin is not registered.
565
+ * Accepts any plugin instance, not just declared dependencies (not strings).
566
+ *
567
+ * @example
568
+ * ```ts
569
+ * const http = ctx.require(httpPlugin);
570
+ * http.use(authMiddleware);
571
+ * ```
572
+ */
573
+ require: <PluginCandidate extends PluginLike>(plugin: PluginCandidate) => ExtractPluginApi<PluginCandidate>;
574
+ /**
575
+ * Check if a plugin is registered by name. String-based (boolean check only).
576
+ *
577
+ * @example
578
+ * ```ts
579
+ * if (ctx.has("analytics")) { ... }
580
+ * ```
581
+ */
582
+ has: (name: string) => boolean;
583
+ };
584
+ /**
585
+ * Descriptor returned by register(). Carries the payload type.
586
+ * and an optional description string for runtime event catalogs.
587
+ */
588
+ type EventDescriptor<PayloadType = unknown> = {
589
+ readonly description: string; /** Phantom field — carries `PayloadType` for inference. Never set at runtime. */
590
+ readonly _type?: PayloadType;
591
+ };
592
+ /**
593
+ * The register function passed to the events callback.
594
+ * Two usage modes:
595
+ * - `register<T>(description?)` — register a single event with payload type `T`.
596
+ * - `register.map<EventMap>(descriptions?)` — bulk-register all events from a
597
+ * pre-declared event map type. Eliminates per-event `register<Events["name"]>()` calls.
598
+ *
599
+ * @example
600
+ * ```ts
601
+ * // Individual registration (inline event types):
602
+ * events: register => ({
603
+ * "auth:login": register<{ userId: string }>("Triggered after login"),
604
+ * })
605
+ *
606
+ * // Bulk registration (Standard+ plugins with separate XxxEvents type):
607
+ * events: register => register.map<AuthEvents>({
608
+ * "auth:login": "Triggered after login",
609
+ * "auth:logout": "Triggered after logout",
610
+ * })
611
+ * ```
612
+ */
613
+ type RegisterFunction = {
614
+ <PayloadType>(description?: string): EventDescriptor<PayloadType>;
615
+ map: <EventMap extends Record<string, unknown>>(descriptions?: { [K in keyof EventMap]?: string }) => { [K in keyof EventMap]: EventDescriptor<EventMap[K]> };
616
+ };
617
+ /**
618
+ * Specification object passed to `createPlugin`.
619
+ *
620
+ * Generic parameters:
621
+ * - `GlobalConfig`: app-wide config object
622
+ * - `GlobalEventMap`: app-wide events from `createCoreConfig`
623
+ * - `PluginEventMap`: plugin-specific events declared by `events`
624
+ * - `PluginConfig`: plugin config shape from `config`
625
+ * - `PluginState`: mutable state returned by `createState`
626
+ * - `PluginApi`: API returned by `api`
627
+ * - `DependencyPlugins`: tuple from `depends`
628
+ *
629
+ * @example
630
+ * ```ts
631
+ * type RouterSpec = CreatePluginSpec<
632
+ * SiteConfig,
633
+ * SiteEvents,
634
+ * Record<never, never>,
635
+ * { basePath: string },
636
+ * { currentPath: string },
637
+ * { navigate(path: string): void },
638
+ * readonly []
639
+ * >;
640
+ * ```
641
+ */
642
+ type CreatePluginSpec<GlobalConfig extends FrameworkConfig, GlobalEventMap extends FrameworkEventMap, PluginEventMap extends Record<string, unknown>, PluginConfig extends Record<string, unknown>, PluginState, PluginApi extends Record<string, unknown>, DependencyPlugins extends DependencyPluginTuple> = {
643
+ /**
644
+ * Declare plugin-specific events via a register callback.
645
+ * Used for compile-time type inference only — the kernel does not call this at runtime.
646
+ *
647
+ * @example
648
+ * ```ts
649
+ * events: (register) => ({
650
+ * "auth:login": register<{ userId: string }>("Triggered after user login"),
651
+ * "auth:logout": register<{ userId: string }>("Triggered after user logout"),
652
+ * })
653
+ * ```
654
+ */
655
+ events?: (register: RegisterFunction) => { [EventName in keyof PluginEventMap]: EventDescriptor<PluginEventMap[EventName]> };
656
+ /**
657
+ * Default config values for this plugin. Consumers can override via `pluginName: { ... }`.
658
+ * Shallow-merged at startup, then frozen.
659
+ *
660
+ * @example
661
+ * ```ts
662
+ * config: { basePath: "/", trailing: false }
663
+ * ```
664
+ */
665
+ config?: PluginConfig;
666
+ /**
667
+ * Plugins this plugin depends on. Dependency APIs are available via `ctx.require()`.
668
+ * Dependencies must appear earlier in the plugins array (no topological sort).
669
+ *
670
+ * @example
671
+ * ```ts
672
+ * depends: [authPlugin, httpPlugin]
673
+ * ```
674
+ */
675
+ depends?: DependencyPlugins;
676
+ /**
677
+ * Factory for mutable plugin state. Called once at startup with a minimal context
678
+ * (global config + plugin config). The returned object is the only mutable store.
679
+ *
680
+ * @example
681
+ * ```ts
682
+ * createState: (ctx) => ({ count: 0, cache: new Map() })
683
+ * ```
684
+ */
685
+ createState?: (context: MinimalContext<GlobalConfig, PluginConfig>) => PluginState;
686
+ /**
687
+ * Public API factory. Receives full plugin context; returns the API object
688
+ * other plugins access via `ctx.require(thisPlugin)`.
689
+ * Must be synchronous and side-effect-free — do not call `ctx.emit()` from this factory.
690
+ *
691
+ * @example
692
+ * ```ts
693
+ * api: (ctx) => ({
694
+ * navigate: (path: string) => { ctx.state.currentPath = path; },
695
+ * getCurrentPath: () => ctx.state.currentPath,
696
+ * })
697
+ * ```
698
+ */
699
+ api?: (context: PluginExecutionContext<GlobalConfig, MergedPluginEvents<GlobalEventMap, PluginEventMap, DependencyPlugins>, PluginConfig, PluginState>) => PluginApi;
700
+ /**
701
+ * Called after all plugins are registered and APIs are built. Runs in forward
702
+ * plugin order, sequentially awaited. Use for setup that depends on other plugins.
703
+ *
704
+ * @example
705
+ * ```ts
706
+ * onInit: (ctx) => {
707
+ * const http = ctx.require(httpPlugin);
708
+ * http.use(authMiddleware);
709
+ * }
710
+ * ```
711
+ */
712
+ onInit?: (context: PluginExecutionContext<GlobalConfig, MergedPluginEvents<GlobalEventMap, PluginEventMap, DependencyPlugins>, PluginConfig, PluginState>) => void | Promise<void>;
713
+ /**
714
+ * Called when the app starts. Runs in forward plugin order, sequentially awaited.
715
+ * Use for runtime startup (open connections, start listeners).
716
+ *
717
+ * @example
718
+ * ```ts
719
+ * onStart: async (ctx) => {
720
+ * await ctx.state.db.connect(ctx.config.connectionString);
721
+ * }
722
+ * ```
723
+ */
724
+ onStart?: (context: PluginExecutionContext<GlobalConfig, MergedPluginEvents<GlobalEventMap, PluginEventMap, DependencyPlugins>, PluginConfig, PluginState>) => void | Promise<void>;
725
+ /**
726
+ * Called when the app stops. Runs in **reverse** plugin order, sequentially awaited.
727
+ * Use for teardown (close connections, flush buffers). Receives only global config.
728
+ *
729
+ * @example
730
+ * ```ts
731
+ * onStop: async (ctx) => {
732
+ * await db.disconnect();
733
+ * }
734
+ * ```
735
+ */
736
+ onStop?: (context: TeardownContext<GlobalConfig>) => void | Promise<void>;
737
+ /**
738
+ * Event subscription factory. Receives full plugin context; returns a map of
739
+ * event handlers. Same closure pattern as `api`. Handlers can access `ctx.state`,
740
+ * `ctx.emit`, `ctx.require`, etc.
741
+ *
742
+ * @example
743
+ * ```ts
744
+ * hooks: (ctx) => ({
745
+ * "auth:login": ({ userId }) => {
746
+ * ctx.state.lastLogin = userId;
747
+ * ctx.emit("page:render", { path: "/dashboard", html: "<div>Welcome</div>" });
748
+ * },
749
+ * })
750
+ * ```
751
+ */
752
+ hooks?: (context: PluginExecutionContext<GlobalConfig, MergedPluginEvents<GlobalEventMap, PluginEventMap, DependencyPlugins>, PluginConfig, PluginState>) => { [EventName in string & keyof MergedPluginEvents<GlobalEventMap, PluginEventMap, DependencyPlugins>]?: (payload: MergedPluginEvents<GlobalEventMap, PluginEventMap, DependencyPlugins>[EventName]) => void | Promise<void> };
753
+ };
754
+ /**
755
+ * Bound createPlugin function type, parameterized by the framework's Config and Events.
756
+ *
757
+ * All type parameters are inferred from the spec object — no explicit generics needed.
758
+ * Per-plugin events use the register callback pattern instead of explicit type arguments.
759
+ *
760
+ * @example
761
+ * ```ts
762
+ * const { createPlugin } = createCoreConfig<MyConfig, MyEvents>("my-app", { config: defaults });
763
+ * const router = createPlugin("router", { config: { basePath: "/" } });
764
+ * ```
765
+ */
766
+ type BoundCreatePluginFunction<GlobalConfig extends FrameworkConfig, GlobalEventMap extends FrameworkEventMap> = {
767
+ <const PluginName extends string = string, PluginConfig extends Record<string, unknown> = Record<string, never>, PluginState = Record<string, never>, PluginApi extends Record<string, unknown> = Record<string, never>, DependencyPlugins extends DependencyPluginTuple = readonly [], PluginEventMap extends Record<string, unknown> = EmptyPluginEventMap, HookHandlerMap extends Record<string, any> = Record<never, never>>(name: PluginName, spec: Omit<CreatePluginSpec<GlobalConfig, GlobalEventMap, PluginEventMap, PluginConfig, PluginState, PluginApi, DependencyPlugins>, "hooks"> & {
768
+ hooks?: (context: PluginExecutionContext<GlobalConfig, MergedPluginEvents<GlobalEventMap, PluginEventMap, DependencyPlugins>, PluginConfig, PluginState>) => { [K in keyof HookHandlerMap]: K extends string & keyof MergedPluginEvents<GlobalEventMap, PluginEventMap, DependencyPlugins> ? (payload: MergedPluginEvents<GlobalEventMap, PluginEventMap, DependencyPlugins>[K & keyof MergedPluginEvents<GlobalEventMap, PluginEventMap, DependencyPlugins>]) => void | Promise<void> : never };
769
+ }): PluginInstance<PluginName, PluginConfig, PluginState, PluginApi, PluginEventMap>;
770
+ };
771
+ /**
772
+ * Creates a bound `createPlugin` function that captures framework generics.
773
+ *
774
+ * Generic parameters:
775
+ * - `GlobalConfig`: app-wide config from `createCoreConfig`
776
+ * - `GlobalEventMap`: app-wide events from `createCoreConfig`
777
+ *
778
+ * @param frameworkId - The framework identifier for error messages.
779
+ * @returns A createPlugin function bound to the framework's Config and Events types.
780
+ * @example
781
+ * ```ts
782
+ * const createPlugin = createPluginFactory<MyConfig, MyEvents>("my-app");
783
+ * const plugin = createPlugin("router", { config: { basePath: "/" } });
784
+ * ```
785
+ */
786
+ //#endregion
787
+ //#region src/core.d.ts
788
+ /**
789
+ * Options for createCore (Step 2).
790
+ *
791
+ * @example
792
+ * ```ts
793
+ * createCore(coreConfig, { plugins: [routerPlugin], onReady: (ctx) => console.log(ctx.config) });
794
+ * ```
795
+ */
796
+ interface CreateCoreOptions<Config> {
797
+ /** Framework default plugins. */
798
+ readonly plugins: AnyPluginInstance[];
799
+ /** Framework-level plugin config overrides keyed by plugin name. */
800
+ readonly pluginConfigs?: Record<string, unknown>;
801
+ /** Called after all plugins are initialized. */
802
+ readonly onReady?: (context: {
803
+ config: Readonly<Config>;
804
+ }) => void | Promise<void>;
805
+ /** Error handler for hook dispatch and teardown failures. */
806
+ readonly onError?: (error: Error) => void;
807
+ }
808
+ /**
809
+ * Return type of createCore (Step 2).
810
+ * Carries the Plugins type parameter to thread type information through createApp.
811
+ *
812
+ * @example
813
+ * ```ts
814
+ * const { createApp, createPlugin } = createCore(coreConfig, { plugins: [...] });
815
+ * ```
816
+ */
817
+ interface CreateCoreResult<Config extends Record<string, unknown>, Events extends Record<string, unknown>, Plugins extends readonly AnyPluginInstance[] = readonly AnyPluginInstance[]> {
818
+ /**
819
+ * Step 3: Creates and initializes the application.
820
+ * Generic over ExtraPlugins to merge consumer plugins into the return type.
821
+ *
822
+ * @param options - Consumer-level config overrides, plugin configs, extra plugins.
823
+ * @returns A promise that resolves to the frozen, fully typed App object.
824
+ */
825
+ readonly createApp: <const ExtraPlugins extends readonly AnyPluginInstance[] = readonly []>(options?: CreateAppOptions<Config, Events, Plugins[number] | ExtraPlugins[number], [...ExtraPlugins]>) => Promise<App<Config, Events, Plugins[number] | ExtraPlugins[number]>>;
826
+ /** Re-exported createPlugin for consumer convenience. */
827
+ readonly createPlugin: BoundCreatePluginFunction<Config, Events>;
828
+ }
829
+ /**
830
+ * Bound createCore function type. Generic method captures the Plugins tuple
831
+ * from options.plugins to thread type information into CreateCoreResult.
832
+ *
833
+ * @example
834
+ * ```ts
835
+ * const framework = createCore(coreConfig, { plugins: [routerPlugin] });
836
+ * ```
837
+ */
838
+ type BoundCreateCoreFunction<Config extends Record<string, unknown>, Events extends Record<string, unknown>> = <const Plugins extends readonly AnyPluginInstance[]>(coreConfig: {
839
+ readonly createPlugin: BoundCreatePluginFunction<Config, Events>;
840
+ }, options: CreateCoreOptions<Config> & {
841
+ readonly plugins: [...Plugins];
842
+ }) => CreateCoreResult<Config, Events, Plugins>;
843
+ /**
844
+ * Creates a bound `createCore` function that captures framework context.
845
+ *
846
+ * Generic parameters:
847
+ * - `Config`: app-wide config from `createCoreConfig`
848
+ * - `Events`: app-wide events from `createCoreConfig`
849
+ *
850
+ * @param frameworkId - The framework identifier for error messages.
851
+ * @param configDefaults - Default config values captured from Step 1.
852
+ * @param createPlugin - Bound createPlugin function from Step 1.
853
+ * @returns A createCore function bound to the framework's Config and Events types.
854
+ * @example
855
+ * ```ts
856
+ * const createCore = createCoreFactory<MyConfig, MyEvents>(
857
+ * "my-app", configDefaults, createPlugin
858
+ * );
859
+ * ```
860
+ */
861
+ //#endregion
862
+ //#region src/config.d.ts
863
+ /**
864
+ * Return type of createCoreConfig. Named type required for isolatedDeclarations.
865
+ *
866
+ * @example
867
+ * ```ts
868
+ * const result: CoreConfigResult<SiteConfig, SiteEvents> = createCoreConfig("app", { config: defaults });
869
+ * ```
870
+ */
871
+ interface CoreConfigResult<Config extends Record<string, unknown>, Events extends Record<string, unknown>> {
872
+ /** Creates a plugin instance with all types inferred from the spec object. */
873
+ readonly createPlugin: BoundCreatePluginFunction<Config, Events>;
874
+ /** Step 2 factory: captures default plugins and returns createApp + createPlugin. */
875
+ readonly createCore: BoundCreateCoreFunction<Config, Events>;
876
+ }
877
+ /**
878
+ * Step 1 of the 3-step factory chain. Captures Config and Events generics
879
+ * in a closure and returns { createPlugin, createCore }.
880
+ *
881
+ * This is the ONLY export from `@moku-labs/core`. All downstream types flow from
882
+ * the generics captured here.
883
+ *
884
+ * @param id - Framework identifier used in error messages.
885
+ * @param options - Configuration options containing the default config values.
886
+ * @param options.config - Default configuration values for the framework.
887
+ * @returns An object with createPlugin (bound to Config/Events) and createCore.
888
+ * @example
889
+ * ```ts
890
+ * const coreConfig = createCoreConfig<SiteConfig, SiteEvents>("my-site", {
891
+ * config: { siteName: "Untitled", mode: "development" }
892
+ * });
893
+ * const { createPlugin, createCore } = coreConfig;
894
+ * ```
895
+ */
896
+ declare function createCoreConfig<Config extends Record<string, unknown>, Events extends Record<string, unknown> = Record<string, never>>(id: string, options: {
897
+ config: Config;
898
+ }): CoreConfigResult<Config, Events>;
899
+ //#endregion
900
+ export { type EmitFunction as EmitFn, type PluginContext_ as PluginCtx, createCoreConfig };