@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.
package/dist/index.mjs ADDED
@@ -0,0 +1,713 @@
1
+ //#region src/utilities.ts
2
+ /**
3
+ * Checks whether a value is a non-null, non-array object record.
4
+ *
5
+ * @param value - Value to inspect.
6
+ * @returns `true` when value is an object record.
7
+ * @example
8
+ * ```ts
9
+ * isRecord({ key: "value" }); // => true
10
+ * isRecord([1, 2, 3]); // => false
11
+ * isRecord(null); // => false
12
+ * ```
13
+ */
14
+ function isRecord(value) {
15
+ return typeof value === "object" && value !== null && !Array.isArray(value);
16
+ }
17
+ /**
18
+ * Reserved app method names that cannot be used as plugin names.
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * RESERVED_NAMES.has("start"); // true
23
+ * RESERVED_NAMES.has("router"); // false
24
+ * ```
25
+ */
26
+ const RESERVED_NAMES = new Set([
27
+ "start",
28
+ "stop",
29
+ "emit",
30
+ "require",
31
+ "has",
32
+ "config",
33
+ "__proto__",
34
+ "constructor",
35
+ "prototype"
36
+ ]);
37
+ /**
38
+ * Check that no plugin name collides with reserved app method names.
39
+ *
40
+ * @param id - Framework identifier for error messages.
41
+ * @param names - Array of plugin names to check.
42
+ * @example
43
+ * ```ts
44
+ * checkReservedNames("my-site", ["router", "seo"]); // ok
45
+ * checkReservedNames("my-site", ["start"]); // throws TypeError
46
+ * ```
47
+ */
48
+ function checkReservedNames(id, names) {
49
+ for (const name of names) if (RESERVED_NAMES.has(name)) throw new TypeError(`[${id}] Plugin name "${name}" conflicts with a reserved app method.\n Choose a different plugin name.`);
50
+ }
51
+ /**
52
+ * Check that no duplicate plugin names exist.
53
+ *
54
+ * @param id - Framework identifier for error messages.
55
+ * @param names - Array of plugin names to check.
56
+ * @example
57
+ * ```ts
58
+ * checkDuplicateNames("my-site", ["router", "seo"]); // ok
59
+ * checkDuplicateNames("my-site", ["router", "router"]); // throws TypeError
60
+ * ```
61
+ */
62
+ function checkDuplicateNames(id, names) {
63
+ const seen = /* @__PURE__ */ new Set();
64
+ for (const name of names) {
65
+ if (seen.has(name)) throw new TypeError(`[${id}] Duplicate plugin name: "${name}".\n Each plugin must have a unique name.`);
66
+ seen.add(name);
67
+ }
68
+ }
69
+ /**
70
+ * Check that all dependencies exist and appear before the dependent plugin.
71
+ *
72
+ * @param id - Framework identifier for error messages.
73
+ * @param plugins - The plugin list.
74
+ * @param names - Array of plugin names (same order as plugins).
75
+ * @example
76
+ * ```ts
77
+ * checkDependencyOrder("my-site", [routerPlugin, loggerPlugin], ["router", "logger"]);
78
+ * ```
79
+ */
80
+ function checkDependencyOrder(id, plugins, names) {
81
+ for (const [index, plugin] of plugins.entries()) {
82
+ if (!plugin.spec.depends) continue;
83
+ for (const dependency of plugin.spec.depends) {
84
+ const depName = dependency.name;
85
+ const depIndex = names.indexOf(depName);
86
+ if (depIndex === -1) throw new TypeError(`[${id}] Plugin "${plugin.name}" depends on "${depName}", but "${depName}" is not registered.\n Add "${depName}" to your plugin list before "${plugin.name}".`);
87
+ if (depIndex >= index) throw new TypeError(`[${id}] Plugin "${plugin.name}" depends on "${depName}", but "${depName}" appears after "${plugin.name}".\n Move "${depName}" before "${plugin.name}" in your plugin list.`);
88
+ }
89
+ }
90
+ }
91
+ /**
92
+ * Validate a plugin list for correctness.
93
+ * Checks: no reserved names, no duplicates, dependencies exist and are ordered.
94
+ *
95
+ * @param id - Framework identifier for error messages.
96
+ * @param plugins - The plugin list to validate.
97
+ * @throws {TypeError} If validation fails.
98
+ * @example
99
+ * ```ts
100
+ * validatePlugins("my-site", plugins); // throws if invalid
101
+ * ```
102
+ */
103
+ function validatePlugins(id, plugins) {
104
+ const names = plugins.map((p) => p.name);
105
+ checkReservedNames(id, names);
106
+ checkDuplicateNames(id, names);
107
+ checkDependencyOrder(id, plugins, names);
108
+ }
109
+
110
+ //#endregion
111
+ //#region src/app.ts
112
+ /**
113
+ * Cast a value to Record if it is a non-null object, or return empty object.
114
+ *
115
+ * @param value - The value to cast.
116
+ * @returns The value as a Record, or an empty object if not a non-null object.
117
+ * @example
118
+ * ```ts
119
+ * asRecord({ a: 1 }); // => { a: 1 }
120
+ * asRecord(undefined); // => {}
121
+ * ```
122
+ */
123
+ function asRecord(value) {
124
+ return isRecord(value) ? value : {};
125
+ }
126
+ /**
127
+ * Create a require function that looks up a plugin API by instance reference.
128
+ *
129
+ * @param runtime - The kernel runtime containing the API map.
130
+ * @param formatError - Formats the error message using the plugin instance name.
131
+ * @returns A function that returns the API for a given plugin instance or throws.
132
+ * @example
133
+ * ```ts
134
+ * const require = createRequire(runtime, name => `Plugin "${name}" not found.`);
135
+ * const api = require(routerPlugin);
136
+ * ```
137
+ */
138
+ function createRequire(runtime, formatError) {
139
+ return (instance) => {
140
+ const api = runtime.apis.get(instance.name);
141
+ if (!api) throw new Error(formatError(instance.name));
142
+ return api;
143
+ };
144
+ }
145
+ /**
146
+ * Create a has function from the runtime's plugin name set.
147
+ *
148
+ * @param runtime - The kernel runtime containing the plugin name set.
149
+ * @returns A function that checks if a plugin name is registered.
150
+ * @example
151
+ * ```ts
152
+ * const has = createHas(runtime);
153
+ * has("router"); // => true or false
154
+ * ```
155
+ */
156
+ function createHas(runtime) {
157
+ return (name) => runtime.pluginNameSet.has(name);
158
+ }
159
+ /**
160
+ * Resolve per-plugin configs: 3-level merge (plugin defaults, framework, consumer), freeze.
161
+ *
162
+ * @param flatPlugins - The flattened plugin list.
163
+ * @param frameworkPluginConfigs - Framework-level plugin config overrides.
164
+ * @param consumerPluginConfigs - Consumer-level plugin config overrides.
165
+ * @returns A map of plugin names to their frozen resolved configs.
166
+ * @example
167
+ * ```ts
168
+ * const configs = resolvePluginConfigs(plugins, frameworkConfigs, consumerConfigs);
169
+ * configs.get("router"); // => { basePath: "/" }
170
+ * ```
171
+ */
172
+ function resolvePluginConfigs(flatPlugins, frameworkPluginConfigs, consumerPluginConfigs) {
173
+ const resolvedConfigs = /* @__PURE__ */ new Map();
174
+ for (const plugin of flatPlugins) {
175
+ const merged = Object.freeze({
176
+ ...plugin.spec.config,
177
+ ...asRecord(frameworkPluginConfigs[plugin.name]),
178
+ ...asRecord(consumerPluginConfigs[plugin.name])
179
+ });
180
+ resolvedConfigs.set(plugin.name, merged);
181
+ }
182
+ return resolvedConfigs;
183
+ }
184
+ /**
185
+ * Create plugin state using MinimalContext (global + config only).
186
+ *
187
+ * @param flatPlugins - The flattened plugin list.
188
+ * @param globalConfig - The frozen global config object.
189
+ * @param resolvedConfigs - The resolved per-plugin config map.
190
+ * @returns A map of plugin names to their initial state objects.
191
+ * @example
192
+ * ```ts
193
+ * const states = createPluginStates(plugins, globalConfig, resolvedConfigs);
194
+ * states.get("counter"); // => { count: 0 }
195
+ * ```
196
+ */
197
+ function createPluginStates(flatPlugins, globalConfig, resolvedConfigs) {
198
+ const states = /* @__PURE__ */ new Map();
199
+ for (const plugin of flatPlugins) if (plugin.spec.createState) {
200
+ const minimalContext = {
201
+ global: globalConfig,
202
+ config: resolvedConfigs.get(plugin.name) ?? {}
203
+ };
204
+ states.set(plugin.name, plugin.spec.createState(minimalContext));
205
+ } else states.set(plugin.name, {});
206
+ return states;
207
+ }
208
+ /**
209
+ * Build event bus: hookMap with async dispatch, fire-and-forget emit, and registerHook.
210
+ *
211
+ * @param onError - Optional error handler for hook execution failures.
212
+ * @returns An object with emit and registerHook functions.
213
+ * @example
214
+ * ```ts
215
+ * const { emit, registerHook } = buildEventBus(error => console.error(error));
216
+ * registerHook("page:view", payload => console.log(payload));
217
+ * emit("page:view", { path: "/" });
218
+ * ```
219
+ */
220
+ function buildEventBus(onError) {
221
+ const hookMap = /* @__PURE__ */ new Map();
222
+ /**
223
+ * Dispatch an event to all registered handlers sequentially.
224
+ *
225
+ * @param eventName - The event name to dispatch.
226
+ * @param payload - The event payload.
227
+ * @example
228
+ * ```ts
229
+ * await dispatch("page:view", { path: "/" });
230
+ * ```
231
+ */
232
+ async function dispatch(eventName, payload) {
233
+ const handlers = hookMap.get(eventName);
234
+ if (!handlers) return;
235
+ for (const handler of handlers) try {
236
+ await handler(payload);
237
+ } catch (error) {
238
+ if (onError) onError(error);
239
+ }
240
+ }
241
+ /**
242
+ * Fire-and-forget emit that dispatches without awaiting.
243
+ *
244
+ * @param eventName - The event name to emit.
245
+ * @param payload - The optional event payload.
246
+ * @example
247
+ * ```ts
248
+ * emit("page:view", { path: "/" });
249
+ * ```
250
+ */
251
+ const emit = (eventName, payload) => {
252
+ dispatch(eventName, payload);
253
+ };
254
+ /**
255
+ * Register a hook handler for a given event name.
256
+ *
257
+ * @param eventName - The event name to listen for.
258
+ * @param handler - The handler to invoke when the event fires.
259
+ * @example
260
+ * ```ts
261
+ * registerHook("page:view", payload => console.log(payload));
262
+ * ```
263
+ */
264
+ const registerHook = (eventName, handler) => {
265
+ let list = hookMap.get(eventName);
266
+ if (!list) {
267
+ list = [];
268
+ hookMap.set(eventName, list);
269
+ }
270
+ list.push(handler);
271
+ };
272
+ return {
273
+ emit,
274
+ registerHook
275
+ };
276
+ }
277
+ /**
278
+ * Register hooks from all plugins. Each plugin's hooks(ctx) produces a handler map.
279
+ *
280
+ * @param flatPlugins - The flattened plugin list.
281
+ * @param buildPluginContext - Factory that builds context for a plugin.
282
+ * @param registerHook - Function to register a hook handler for an event.
283
+ * @example
284
+ * ```ts
285
+ * registerPluginHooks(plugins, contextFactory, registerHook);
286
+ * ```
287
+ */
288
+ function registerPluginHooks(flatPlugins, buildPluginContext, registerHook) {
289
+ for (const plugin of flatPlugins) if (plugin.spec.hooks) {
290
+ const hookHandlers = plugin.spec.hooks(buildPluginContext(plugin));
291
+ for (const [eventName, handler] of Object.entries(hookHandlers)) {
292
+ if (!handler) continue;
293
+ registerHook(eventName, handler);
294
+ }
295
+ }
296
+ }
297
+ /**
298
+ * Create a factory that builds PluginContext for a given plugin.
299
+ *
300
+ * @param runtime - The kernel runtime with shared state.
301
+ * @param resolvedConfigs - The resolved per-plugin config map.
302
+ * @param states - The plugin state map.
303
+ * @returns A factory function that produces a PluginContext for any plugin.
304
+ * @example
305
+ * ```ts
306
+ * const factory = createContextFactory(runtime, configs, states);
307
+ * const ctx = factory(routerPlugin);
308
+ * ```
309
+ */
310
+ function createContextFactory(runtime, resolvedConfigs, states) {
311
+ const has = createHas(runtime);
312
+ return (plugin) => ({
313
+ global: runtime.globalConfig,
314
+ config: resolvedConfigs.get(plugin.name),
315
+ state: states.get(plugin.name),
316
+ emit: runtime.emit,
317
+ require: createRequire(runtime, (name) => `[${runtime.id}] Plugin "${plugin.name}" requires "${name}", but "${name}" is not registered.\n Add "${name}" to your plugin list.`),
318
+ has
319
+ });
320
+ }
321
+ /**
322
+ * Build callback context for consumer lifecycle callbacks (onReady, onStart, onStop).
323
+ *
324
+ * @param runtime - The kernel runtime with shared state and APIs.
325
+ * @returns A dynamic object with config, emit, require, has, and all plugin APIs.
326
+ * @example
327
+ * ```ts
328
+ * const ctx = buildCallbackContext(runtime);
329
+ * ctx.config; // frozen global config
330
+ * ctx.router; // router plugin API (if registered)
331
+ * ```
332
+ */
333
+ function buildCallbackContext(runtime) {
334
+ const context = {
335
+ config: runtime.globalConfig,
336
+ emit: runtime.emit,
337
+ require: createRequire(runtime, (name) => `[${runtime.id}] Plugin "${name}" is not registered.\n Add "${name}" to your plugin list.`),
338
+ has: createHas(runtime)
339
+ };
340
+ for (const [name, api] of runtime.apis) context[name] = api;
341
+ return context;
342
+ }
343
+ /**
344
+ * Run onStop for all plugins in reverse order.
345
+ *
346
+ * @param flatPlugins - The flattened plugin list.
347
+ * @param globalConfig - The frozen global config object.
348
+ * @example
349
+ * ```ts
350
+ * await executeStop(plugins, globalConfig);
351
+ * ```
352
+ */
353
+ async function executeStop(flatPlugins, globalConfig) {
354
+ for (const plugin of flatPlugins.toReversed()) {
355
+ if (!plugin.spec.onStop) continue;
356
+ await plugin.spec.onStop({ global: globalConfig });
357
+ }
358
+ }
359
+ /**
360
+ * Build the frozen app object with start, stop, emit, require, has and mounted plugin APIs.
361
+ *
362
+ * @param runtime - The kernel runtime with shared state and APIs.
363
+ * @param flatPlugins - The flattened plugin list.
364
+ * @param buildPluginContext - Factory that builds context for a plugin.
365
+ * @param consumer - Optional consumer lifecycle callbacks.
366
+ * @returns A frozen app object with lifecycle methods and plugin APIs.
367
+ * @example
368
+ * ```ts
369
+ * const app = buildApp(runtime, plugins, contextFactory, onError, consumer);
370
+ * await app.start();
371
+ * ```
372
+ */
373
+ function buildApp(runtime, flatPlugins, buildPluginContext, consumer) {
374
+ let started = false;
375
+ const appRequire = createRequire(runtime, (name) => `[${runtime.id}] app.require("${name}") failed: "${name}" is not registered.\n Check your plugin list.`);
376
+ const appHas = createHas(runtime);
377
+ const app = {
378
+ start: async () => {
379
+ if (started) throw new Error(`[${runtime.id}] App already started.\n start() can only be called once.`);
380
+ for (const plugin of flatPlugins) if (plugin.spec.onStart) await plugin.spec.onStart(buildPluginContext(plugin));
381
+ if (consumer?.onStart) await consumer.onStart(buildCallbackContext(runtime));
382
+ started = true;
383
+ },
384
+ stop: async () => {
385
+ if (!started) throw new Error(`[${runtime.id}] App not started.\n Call start() before stop().`);
386
+ await executeStop(flatPlugins, runtime.globalConfig);
387
+ if (consumer?.onStop) await consumer.onStop(buildCallbackContext(runtime));
388
+ },
389
+ emit: (eventName, payload) => {
390
+ runtime.emit(eventName, payload);
391
+ },
392
+ require: (instance) => appRequire(instance),
393
+ has: (name) => appHas(name)
394
+ };
395
+ for (const [name, api] of runtime.apis) app[name] = api;
396
+ return Object.freeze(app);
397
+ }
398
+ /**
399
+ * The kernel — creates and initializes the application.
400
+ *
401
+ * Receives pre-flattened, pre-validated plugins and all captured context from
402
+ * createCore. Performs: config resolution, state creation, event bus setup,
403
+ * API building, lifecycle execution, returns frozen app object.
404
+ *
405
+ * @param parameters - All kernel inputs captured from the factory chain.
406
+ * @returns A promise that resolves to the frozen app object.
407
+ * @example
408
+ * ```ts
409
+ * const app = await kernel({ id: "my-app", configDefaults: {}, ... });
410
+ * ```
411
+ */
412
+ async function kernel(parameters) {
413
+ const { id, configDefaults, frameworkPluginConfigs, flatPlugins, configOverrides, consumerPluginConfigs, onReady, onError, consumer } = parameters;
414
+ const pluginNameSet = new Set(flatPlugins.map((plugin) => plugin.name));
415
+ const globalConfig = Object.freeze({
416
+ ...configDefaults,
417
+ ...configOverrides
418
+ });
419
+ const resolvedConfigs = resolvePluginConfigs(flatPlugins, frameworkPluginConfigs, consumerPluginConfigs);
420
+ const states = createPluginStates(flatPlugins, globalConfig, resolvedConfigs);
421
+ const apis = /* @__PURE__ */ new Map();
422
+ const { emit, registerHook } = buildEventBus(onError || consumer?.onError ? (error) => {
423
+ if (onError) onError(error);
424
+ if (consumer?.onError) consumer.onError(error, buildCallbackContext(runtime));
425
+ } : void 0);
426
+ const runtime = {
427
+ id,
428
+ globalConfig,
429
+ emit,
430
+ apis,
431
+ pluginNameSet
432
+ };
433
+ const buildPluginContext = createContextFactory(runtime, resolvedConfigs, states);
434
+ registerPluginHooks(flatPlugins, buildPluginContext, registerHook);
435
+ for (const plugin of flatPlugins) if (plugin.spec.api) apis.set(plugin.name, plugin.spec.api(buildPluginContext(plugin)));
436
+ for (const plugin of flatPlugins) if (plugin.spec.onInit) await plugin.spec.onInit(buildPluginContext(plugin));
437
+ if (onReady) await onReady({ config: globalConfig });
438
+ if (consumer?.onReady) await consumer.onReady(buildCallbackContext(runtime));
439
+ return buildApp(runtime, flatPlugins, buildPluginContext, consumer);
440
+ }
441
+
442
+ //#endregion
443
+ //#region src/core.ts
444
+ /**
445
+ * Creates a bound `createCore` function that captures framework context.
446
+ *
447
+ * Generic parameters:
448
+ * - `Config`: app-wide config from `createCoreConfig`
449
+ * - `Events`: app-wide events from `createCoreConfig`
450
+ *
451
+ * @param frameworkId - The framework identifier for error messages.
452
+ * @param configDefaults - Default config values captured from Step 1.
453
+ * @param createPlugin - Bound createPlugin function from Step 1.
454
+ * @returns A createCore function bound to the framework's Config and Events types.
455
+ * @example
456
+ * ```ts
457
+ * const createCore = createCoreFactory<MyConfig, MyEvents>(
458
+ * "my-app", configDefaults, createPlugin
459
+ * );
460
+ * ```
461
+ */
462
+ function createCoreFactory(frameworkId, configDefaults, createPlugin) {
463
+ /**
464
+ * Step 2: Captures framework default plugins and returns createApp.
465
+ *
466
+ * @param _coreConfig - The CoreConfigResult object (for type flow only).
467
+ * @param coreOptions - Framework-level defaults: plugins, pluginConfigs, onReady, onError.
468
+ * @returns An object with createApp (async function) and createPlugin (same reference).
469
+ * @example
470
+ * ```ts
471
+ * const { createApp, createPlugin } = createCore(coreConfig, { plugins: [routerPlugin] });
472
+ * ```
473
+ */
474
+ const createCore = (_coreConfig, coreOptions) => {
475
+ const options = coreOptions;
476
+ const defaultPlugins = options.plugins;
477
+ const frameworkPluginConfigs = options.pluginConfigs ?? {};
478
+ const onReady = options.onReady;
479
+ const onError = options.onError;
480
+ /**
481
+ * Step 3: Creates and initializes the application.
482
+ * Merges consumer options with framework defaults, flattens and validates
483
+ * plugins, then delegates to the kernel for lifecycle execution.
484
+ *
485
+ * @param consumerOptions - Consumer-level config, plugins, and callbacks.
486
+ * @returns A promise that resolves to the frozen App object.
487
+ * @example
488
+ * ```ts
489
+ * const app = await createApp({ config: { siteName: "Blog" } });
490
+ * ```
491
+ */
492
+ const createApp = async (consumerOptions) => {
493
+ const appOptions = consumerOptions ?? {};
494
+ const allPlugins = [...defaultPlugins, ...appOptions.plugins ?? []];
495
+ validatePlugins(frameworkId, allPlugins);
496
+ return kernel({
497
+ id: frameworkId,
498
+ configDefaults,
499
+ frameworkPluginConfigs,
500
+ flatPlugins: allPlugins,
501
+ configOverrides: appOptions.config ?? {},
502
+ consumerPluginConfigs: appOptions.pluginConfigs ?? {},
503
+ onReady,
504
+ onError,
505
+ consumer: {
506
+ onReady: appOptions.onReady,
507
+ onError: appOptions.onError,
508
+ onStart: appOptions.onStart,
509
+ onStop: appOptions.onStop
510
+ }
511
+ });
512
+ };
513
+ return {
514
+ createApp,
515
+ createPlugin
516
+ };
517
+ };
518
+ return createCore;
519
+ }
520
+
521
+ //#endregion
522
+ //#region src/plugin.ts
523
+ /**
524
+ * Asserts that a plugin name is a non-empty string.
525
+ *
526
+ * @param frameworkId - Framework identifier used in error messages.
527
+ * @param name - Candidate plugin name.
528
+ * @example
529
+ * ```ts
530
+ * assertValidPluginName("my-app", "router");
531
+ * ```
532
+ */
533
+ function assertValidPluginName(frameworkId, name) {
534
+ if (typeof name === "string" && name.length > 0) return;
535
+ throw new TypeError(`[${frameworkId}] Plugin name must be a non-empty string.\n Pass a non-empty string as the first argument.`);
536
+ }
537
+ /**
538
+ * Asserts that the plugin spec is a non-null object.
539
+ *
540
+ * @param frameworkId - Framework identifier used in error messages.
541
+ * @param pluginName - Validated plugin name.
542
+ * @param spec - Candidate plugin spec.
543
+ * @example
544
+ * ```ts
545
+ * assertValidPluginSpec("my-app", "router", { onInit: () => {} });
546
+ * ```
547
+ */
548
+ function assertValidPluginSpec(frameworkId, pluginName, spec) {
549
+ if (isRecord(spec)) return;
550
+ throw new TypeError(`[${frameworkId}] Plugin "${pluginName}" has invalid spec: expected an object.\n Provide a plugin specification object as the second argument.`);
551
+ }
552
+ /**
553
+ * Validates lifecycle handlers (`onInit`, `onStart`, `onStop`) if provided.
554
+ *
555
+ * @param frameworkId - Framework identifier used in error messages.
556
+ * @param pluginName - Validated plugin name.
557
+ * @param spec - Runtime plugin spec.
558
+ * @example
559
+ * ```ts
560
+ * assertValidLifecycleHandlers("my-app", "router", { onStart: () => {} });
561
+ * ```
562
+ */
563
+ function assertValidLifecycleHandlers(frameworkId, pluginName, spec) {
564
+ for (const methodName of [
565
+ "onInit",
566
+ "onStart",
567
+ "onStop"
568
+ ]) {
569
+ const methodValue = spec[methodName];
570
+ if (methodValue !== void 0 && typeof methodValue !== "function") throw new TypeError(`[${frameworkId}] Plugin "${pluginName}" has invalid ${methodName}: expected a function.\n Provide a function for ${methodName} or remove it from the spec.`);
571
+ }
572
+ }
573
+ /**
574
+ * Validates that events is a function (the register callback factory) if provided.
575
+ * The kernel does not call events at runtime — it exists for compile-time type inference.
576
+ * This validation catches typos like `events: { ... }` instead of `events: register => ({ ... })`.
577
+ *
578
+ * @param frameworkId - Framework identifier used in error messages.
579
+ * @param pluginName - Validated plugin name.
580
+ * @param events - Candidate events value from plugin spec.
581
+ * @example
582
+ * ```ts
583
+ * assertValidEvents("my-app", "auth", register => ({ "auth:login": register<{ userId: string }>() }));
584
+ * ```
585
+ */
586
+ function assertValidEvents(frameworkId, pluginName, events) {
587
+ if (events === void 0) return;
588
+ if (typeof events !== "function") throw new TypeError(`[${frameworkId}] Plugin "${pluginName}" has invalid events: expected a function.\n Provide a function like: events: register => ({ "event:name": register<PayloadType>() })`);
589
+ }
590
+ /**
591
+ * Validates that hooks is a function (the context-receiving factory).
592
+ * The return value (handler map) is validated at kernel time when hooks(ctx) is called.
593
+ *
594
+ * @param frameworkId - Framework identifier used in error messages.
595
+ * @param pluginName - Validated plugin name.
596
+ * @param hooks - Candidate hooks value from plugin spec.
597
+ * @example
598
+ * ```ts
599
+ * assertValidHooks("my-app", "router", ctx => ({ "route:change": () => {} }));
600
+ * ```
601
+ */
602
+ function assertValidHooks(frameworkId, pluginName, hooks) {
603
+ if (hooks === void 0) return;
604
+ if (typeof hooks !== "function") throw new TypeError(`[${frameworkId}] Plugin "${pluginName}" has invalid hooks: expected a function.\n Provide a function like: hooks: ctx => ({ "event:name": payload => { ... } })`);
605
+ }
606
+ /**
607
+ * Validates that `api` is a function if provided.
608
+ *
609
+ * @param frameworkId - Framework identifier used in error messages.
610
+ * @param pluginName - Validated plugin name.
611
+ * @param api - Candidate api value from plugin spec.
612
+ * @example
613
+ * ```ts
614
+ * assertValidApi("my-app", "router", ctx => ({ navigate: () => {} }));
615
+ * ```
616
+ */
617
+ function assertValidApi(frameworkId, pluginName, api) {
618
+ if (api !== void 0 && typeof api !== "function") throw new TypeError(`[${frameworkId}] Plugin "${pluginName}" has invalid api: expected a function.\n Provide a function like: api: ctx => ({ methodName: () => { ... } })`);
619
+ }
620
+ /**
621
+ * Validates that `createState` is a function if provided.
622
+ *
623
+ * @param frameworkId - Framework identifier used in error messages.
624
+ * @param pluginName - Validated plugin name.
625
+ * @param createState - Candidate createState value from plugin spec.
626
+ * @example
627
+ * ```ts
628
+ * assertValidCreateState("my-app", "router", ctx => ({ count: 0 }));
629
+ * ```
630
+ */
631
+ function assertValidCreateState(frameworkId, pluginName, createState) {
632
+ if (createState !== void 0 && typeof createState !== "function") throw new TypeError(`[${frameworkId}] Plugin "${pluginName}" has invalid createState: expected a function.\n Provide a function like: createState: ctx => ({ key: initialValue })`);
633
+ }
634
+ /**
635
+ * Creates a bound `createPlugin` function that captures framework generics.
636
+ *
637
+ * Generic parameters:
638
+ * - `GlobalConfig`: app-wide config from `createCoreConfig`
639
+ * - `GlobalEventMap`: app-wide events from `createCoreConfig`
640
+ *
641
+ * @param frameworkId - The framework identifier for error messages.
642
+ * @returns A createPlugin function bound to the framework's Config and Events types.
643
+ * @example
644
+ * ```ts
645
+ * const createPlugin = createPluginFactory<MyConfig, MyEvents>("my-app");
646
+ * const plugin = createPlugin("router", { config: { basePath: "/" } });
647
+ * ```
648
+ */
649
+ function createPluginFactory(frameworkId) {
650
+ /**
651
+ * Creates a plugin instance with inferred types from the spec object.
652
+ *
653
+ * @param name - Unique plugin name (inferred as literal string type).
654
+ * @param spec - Plugin specification with config, state, api, lifecycle, hooks.
655
+ * @returns A PluginInstance carrying phantom types for compile-time inference.
656
+ * @example
657
+ * ```ts
658
+ * const router = createPlugin("router", {
659
+ * config: { basePath: "/" },
660
+ * api: (ctx) => ({ navigate: (path: string) => path }),
661
+ * });
662
+ * ```
663
+ */
664
+ const createPlugin = (name, spec) => {
665
+ assertValidPluginName(frameworkId, name);
666
+ assertValidPluginSpec(frameworkId, name, spec);
667
+ assertValidLifecycleHandlers(frameworkId, name, spec);
668
+ assertValidEvents(frameworkId, name, spec.events);
669
+ assertValidHooks(frameworkId, name, spec.hooks);
670
+ assertValidApi(frameworkId, name, spec.api);
671
+ assertValidCreateState(frameworkId, name, spec.createState);
672
+ return {
673
+ name,
674
+ spec,
675
+ _phantom: {}
676
+ };
677
+ };
678
+ return createPlugin;
679
+ }
680
+
681
+ //#endregion
682
+ //#region src/config.ts
683
+ /**
684
+ * Step 1 of the 3-step factory chain. Captures Config and Events generics
685
+ * in a closure and returns { createPlugin, createCore }.
686
+ *
687
+ * This is the ONLY export from `@moku-labs/core`. All downstream types flow from
688
+ * the generics captured here.
689
+ *
690
+ * @param id - Framework identifier used in error messages.
691
+ * @param options - Configuration options containing the default config values.
692
+ * @param options.config - Default configuration values for the framework.
693
+ * @returns An object with createPlugin (bound to Config/Events) and createCore.
694
+ * @example
695
+ * ```ts
696
+ * const coreConfig = createCoreConfig<SiteConfig, SiteEvents>("my-site", {
697
+ * config: { siteName: "Untitled", mode: "development" }
698
+ * });
699
+ * const { createPlugin, createCore } = coreConfig;
700
+ * ```
701
+ */
702
+ function createCoreConfig(id, options) {
703
+ const configDefaults = options.config;
704
+ const frameworkId = id;
705
+ const createPlugin = createPluginFactory(frameworkId);
706
+ return {
707
+ createPlugin,
708
+ createCore: createCoreFactory(frameworkId, configDefaults, createPlugin)
709
+ };
710
+ }
711
+
712
+ //#endregion
713
+ export { createCoreConfig };