@moku-labs/core 0.1.0-alpha.2 → 0.1.0-alpha.4

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/README.md CHANGED
@@ -230,7 +230,7 @@ Every field is optional. A plugin with only `api` works. A plugin with only `hoo
230
230
 
231
231
  ## Design Principles
232
232
 
233
- **Brutal simplicity.** No classes. No decorators. No dependency injection. No inheritance. Every function is a pure factory: input output.
233
+ **Brutal simplicity.** No classes. No decorators. No dependency injection. No inheritance. `createCoreConfig`, `createCore`, and `createPlugin` are pure factories. `createApp()` performs synchronous init. `app.start()` / `app.stop()` run the runtime lifecycle.
234
234
 
235
235
  **Types over runtime.** Most of the codebase is type definitions and JSDoc. The type system provides autocomplete, compile-time validation, and documentation simultaneously.
236
236
 
@@ -268,13 +268,13 @@ Creates a plugin instance. Zero explicit generics — everything inferred from t
268
268
 
269
269
  ### createApp(options?)
270
270
 
271
- Merges framework defaults with consumer options. Validates, resolves config, runs `onInit`. Returns `App` — a frozen object with plugin APIs mounted as properties.
271
+ Merges framework defaults with consumer options. Validates, resolves config, runs `onInit`, and returns `App` — a frozen object with plugin APIs mounted as properties. `createApp()` is synchronous and side-effectful within the init phase; it is not a lazy builder. `start()` / `stop()` are optional and mainly useful for apps with a distinct runtime phase. Lifecycle execution is non-transactional: start/stop errors propagate, they are not rolled back by the kernel.
272
272
 
273
273
  ### App
274
274
 
275
275
  ```typescript
276
- await app.start(); // onStart (forward order)
277
- await app.stop(); // onStop (reverse order)
276
+ await app.start(); // optional: onStart (forward order)
277
+ await app.stop(); // optional: onStop (reverse order)
278
278
  app.emit('event', payload); // strictly typed, fire-and-forget
279
279
  app.require(plugin); // returns typed API or throws
280
280
  app.has('name'); // boolean, never throws
package/dist/index.cjs CHANGED
@@ -32,6 +32,8 @@ const RESERVED_NAMES = new Set([
32
32
  "require",
33
33
  "has",
34
34
  "config",
35
+ "global",
36
+ "state",
35
37
  "__proto__",
36
38
  "constructor",
37
39
  "prototype"
@@ -108,6 +110,37 @@ function validatePlugins(id, plugins) {
108
110
  checkDuplicateNames(id, names);
109
111
  checkDependencyOrder(id, plugins, names);
110
112
  }
113
+ /**
114
+ * Validate core plugins: no reserved names, no duplicates.
115
+ *
116
+ * @param id - Framework identifier for error messages.
117
+ * @param corePlugins - The core plugin list to validate.
118
+ * @throws {TypeError} If validation fails.
119
+ * @example
120
+ * ```ts
121
+ * validateCorePlugins("my-site", [logPlugin, envPlugin]); // throws if invalid
122
+ * ```
123
+ */
124
+ function validateCorePlugins(id, corePlugins) {
125
+ const names = corePlugins.map((p) => p.name);
126
+ checkReservedNames(id, names);
127
+ checkDuplicateNames(id, names);
128
+ }
129
+ /**
130
+ * Validate that no regular plugin name conflicts with a core plugin name.
131
+ *
132
+ * @param id - Framework identifier for error messages.
133
+ * @param plugins - The regular plugin list.
134
+ * @param corePluginNames - Set of core plugin names.
135
+ * @throws {TypeError} If a name conflict is found.
136
+ * @example
137
+ * ```ts
138
+ * checkCorePluginConflicts("my-site", [routerPlugin], new Set(["log", "env"]));
139
+ * ```
140
+ */
141
+ function checkCorePluginConflicts(id, plugins, corePluginNames) {
142
+ for (const plugin of plugins) if (corePluginNames.has(plugin.name)) throw new TypeError(`[${id}] Plugin name "${plugin.name}" conflicts with core plugin "${plugin.name}".\n Choose a different plugin name.`);
143
+ }
111
144
 
112
145
  //#endregion
113
146
  //#region src/app.ts
@@ -302,6 +335,7 @@ function registerPluginHooks(flatPlugins, buildPluginContext, registerHook) {
302
335
  * @param runtime - The kernel runtime with shared state.
303
336
  * @param resolvedConfigs - The resolved per-plugin config map.
304
337
  * @param states - The plugin state map.
338
+ * @param coreApis - Core plugin APIs to spread onto every plugin context.
305
339
  * @returns A factory function that produces a PluginContext for any plugin.
306
340
  * @example
307
341
  * ```ts
@@ -309,7 +343,7 @@ function registerPluginHooks(flatPlugins, buildPluginContext, registerHook) {
309
343
  * const ctx = factory(routerPlugin);
310
344
  * ```
311
345
  */
312
- function createContextFactory(runtime, resolvedConfigs, states) {
346
+ function createContextFactory(runtime, resolvedConfigs, states, coreApis) {
313
347
  const has = createHas(runtime);
314
348
  return (plugin) => ({
315
349
  global: runtime.globalConfig,
@@ -317,7 +351,8 @@ function createContextFactory(runtime, resolvedConfigs, states) {
317
351
  state: states.get(plugin.name),
318
352
  emit: runtime.emit,
319
353
  require: createRequire(runtime, (name) => `[${runtime.id}] Plugin "${plugin.name}" requires "${name}", but "${name}" is not registered.\n Add "${name}" to your plugin list.`),
320
- has
354
+ has,
355
+ ...coreApis
321
356
  });
322
357
  }
323
358
  /**
@@ -364,6 +399,10 @@ async function executeStop(flatPlugins, globalConfig) {
364
399
  * @param runtime - The kernel runtime with shared state and APIs.
365
400
  * @param flatPlugins - The flattened plugin list.
366
401
  * @param buildPluginContext - Factory that builds context for a plugin.
402
+ * @param corePluginData - Core plugin instances, resolved configs, and states.
403
+ * @param corePluginData.plugins - The core plugin instances.
404
+ * @param corePluginData.configs - Resolved core plugin config map.
405
+ * @param corePluginData.states - Core plugin state map.
367
406
  * @param consumer - Optional consumer lifecycle callbacks.
368
407
  * @returns A frozen app object with lifecycle methods and plugin APIs.
369
408
  * @example
@@ -372,13 +411,17 @@ async function executeStop(flatPlugins, globalConfig) {
372
411
  * await app.start();
373
412
  * ```
374
413
  */
375
- function buildApp(runtime, flatPlugins, buildPluginContext, consumer) {
414
+ function buildApp(runtime, flatPlugins, buildPluginContext, corePluginData, consumer) {
376
415
  let started = false;
377
416
  const appRequire = createRequire(runtime, (name) => `[${runtime.id}] app.require("${name}") failed: "${name}" is not registered.\n Check your plugin list.`);
378
417
  const appHas = createHas(runtime);
379
418
  const app = {
380
419
  start: async () => {
381
420
  if (started) throw new Error(`[${runtime.id}] App already started.\n start() can only be called once.`);
421
+ for (const plugin of corePluginData.plugins) if (plugin.spec.onStart) await plugin.spec.onStart({
422
+ config: corePluginData.configs.get(plugin.name) ?? {},
423
+ state: corePluginData.states.get(plugin.name)
424
+ });
382
425
  for (const plugin of flatPlugins) if (plugin.spec.onStart) await plugin.spec.onStart(buildPluginContext(plugin));
383
426
  if (consumer?.onStart) await consumer.onStart(buildCallbackContext(runtime));
384
427
  started = true;
@@ -386,6 +429,10 @@ function buildApp(runtime, flatPlugins, buildPluginContext, consumer) {
386
429
  stop: async () => {
387
430
  if (!started) throw new Error(`[${runtime.id}] App not started.\n Call start() before stop().`);
388
431
  await executeStop(flatPlugins, runtime.globalConfig);
432
+ for (const plugin of corePluginData.plugins.toReversed()) if (plugin.spec.onStop) await plugin.spec.onStop({
433
+ config: corePluginData.configs.get(plugin.name) ?? {},
434
+ state: corePluginData.states.get(plugin.name)
435
+ });
389
436
  if (consumer?.onStop) await consumer.onStop(buildCallbackContext(runtime));
390
437
  },
391
438
  emit: (eventName, payload) => {
@@ -398,6 +445,59 @@ function buildApp(runtime, flatPlugins, buildPluginContext, consumer) {
398
445
  return Object.freeze(app);
399
446
  }
400
447
  /**
448
+ * Initialize core plugins: resolve configs, create states, build APIs, run onInit.
449
+ *
450
+ * @param corePlugins - Core plugin instances.
451
+ * @param corePluginConfigs - Config overrides from createCoreConfig.
452
+ * @param frameworkPluginConfigs - Config overrides from createCore.
453
+ * @param consumerPluginConfigs - Config overrides from createApp.
454
+ * @returns Resolved configs, states, core APIs map, and shared apis Map.
455
+ * @example
456
+ * ```ts
457
+ * const { coreResolvedConfigs, coreStates, coreApis, apis } = initCorePlugins(
458
+ * corePlugins, corePluginConfigs, frameworkPluginConfigs, consumerPluginConfigs
459
+ * );
460
+ * ```
461
+ */
462
+ function initCorePlugins(corePlugins, corePluginConfigs, frameworkPluginConfigs, consumerPluginConfigs) {
463
+ const coreResolvedConfigs = /* @__PURE__ */ new Map();
464
+ for (const plugin of corePlugins) {
465
+ const merged = Object.freeze({
466
+ ...plugin.spec.config,
467
+ ...asRecord(corePluginConfigs[plugin.name]),
468
+ ...asRecord(frameworkPluginConfigs[plugin.name]),
469
+ ...asRecord(consumerPluginConfigs[plugin.name])
470
+ });
471
+ coreResolvedConfigs.set(plugin.name, merged);
472
+ }
473
+ const coreStates = /* @__PURE__ */ new Map();
474
+ for (const plugin of corePlugins) if (plugin.spec.createState) {
475
+ const pluginConfig = coreResolvedConfigs.get(plugin.name) ?? {};
476
+ coreStates.set(plugin.name, plugin.spec.createState({ config: pluginConfig }));
477
+ } else coreStates.set(plugin.name, {});
478
+ const coreApis = {};
479
+ const apis = /* @__PURE__ */ new Map();
480
+ for (const plugin of corePlugins) if (plugin.spec.api) {
481
+ const context = {
482
+ config: coreResolvedConfigs.get(plugin.name) ?? {},
483
+ state: coreStates.get(plugin.name)
484
+ };
485
+ const api = plugin.spec.api(context);
486
+ coreApis[plugin.name] = api;
487
+ apis.set(plugin.name, api);
488
+ }
489
+ for (const plugin of corePlugins) if (plugin.spec.onInit) plugin.spec.onInit({
490
+ config: coreResolvedConfigs.get(plugin.name) ?? {},
491
+ state: coreStates.get(plugin.name)
492
+ });
493
+ return {
494
+ coreResolvedConfigs,
495
+ coreStates,
496
+ coreApis,
497
+ apis
498
+ };
499
+ }
500
+ /**
401
501
  * The kernel — creates and initializes the application.
402
502
  *
403
503
  * Receives pre-flattened, pre-validated plugins and all captured context from
@@ -412,15 +512,15 @@ function buildApp(runtime, flatPlugins, buildPluginContext, consumer) {
412
512
  * ```
413
513
  */
414
514
  function kernel(parameters) {
415
- const { id, configDefaults, frameworkPluginConfigs, flatPlugins, configOverrides, consumerPluginConfigs, onReady, onError, consumer } = parameters;
416
- const pluginNameSet = new Set(flatPlugins.map((plugin) => plugin.name));
515
+ const { id, configDefaults, frameworkPluginConfigs, flatPlugins, corePlugins, corePluginConfigs, configOverrides, consumerPluginConfigs, onReady, onError, consumer } = parameters;
516
+ const pluginNameSet = new Set([...corePlugins.map((plugin) => plugin.name), ...flatPlugins.map((plugin) => plugin.name)]);
417
517
  const globalConfig = Object.freeze({
418
518
  ...configDefaults,
419
519
  ...configOverrides
420
520
  });
521
+ const { coreResolvedConfigs, coreStates, coreApis, apis } = initCorePlugins(corePlugins, corePluginConfigs, frameworkPluginConfigs, consumerPluginConfigs);
421
522
  const resolvedConfigs = resolvePluginConfigs(flatPlugins, frameworkPluginConfigs, consumerPluginConfigs);
422
523
  const states = createPluginStates(flatPlugins, globalConfig, resolvedConfigs);
423
- const apis = /* @__PURE__ */ new Map();
424
524
  const { emit, registerHook } = buildEventBus(onError || consumer?.onError ? (error) => {
425
525
  if (onError) onError(error);
426
526
  if (consumer?.onError) consumer.onError(error, buildCallbackContext(runtime));
@@ -432,13 +532,17 @@ function kernel(parameters) {
432
532
  apis,
433
533
  pluginNameSet
434
534
  };
435
- const buildPluginContext = createContextFactory(runtime, resolvedConfigs, states);
535
+ const buildPluginContext = createContextFactory(runtime, resolvedConfigs, states, coreApis);
436
536
  registerPluginHooks(flatPlugins, buildPluginContext, registerHook);
437
537
  for (const plugin of flatPlugins) if (plugin.spec.api) apis.set(plugin.name, plugin.spec.api(buildPluginContext(plugin)));
438
538
  for (const plugin of flatPlugins) if (plugin.spec.onInit) plugin.spec.onInit(buildPluginContext(plugin));
439
539
  if (onReady) onReady({ config: globalConfig });
440
540
  if (consumer?.onReady) consumer.onReady(buildCallbackContext(runtime));
441
- return buildApp(runtime, flatPlugins, buildPluginContext, consumer);
541
+ return buildApp(runtime, flatPlugins, buildPluginContext, {
542
+ plugins: corePlugins,
543
+ configs: coreResolvedConfigs,
544
+ states: coreStates
545
+ }, consumer);
442
546
  }
443
547
 
444
548
  //#endregion
@@ -453,6 +557,8 @@ function kernel(parameters) {
453
557
  * @param frameworkId - The framework identifier for error messages.
454
558
  * @param configDefaults - Default config values captured from Step 1.
455
559
  * @param createPlugin - Bound createPlugin function from Step 1.
560
+ * @param corePlugins - Core plugin instances from createCoreConfig.
561
+ * @param corePluginConfigs - Core plugin config overrides from createCoreConfig.
456
562
  * @returns A createCore function bound to the framework's Config and Events types.
457
563
  * @example
458
564
  * ```ts
@@ -461,7 +567,7 @@ function kernel(parameters) {
461
567
  * );
462
568
  * ```
463
569
  */
464
- function createCoreFactory(frameworkId, configDefaults, createPlugin) {
570
+ function createCoreFactory(frameworkId, configDefaults, createPlugin, corePlugins, corePluginConfigs) {
465
571
  /**
466
572
  * Step 2: Captures framework default plugins and returns createApp.
467
573
  *
@@ -495,11 +601,14 @@ function createCoreFactory(frameworkId, configDefaults, createPlugin) {
495
601
  const appOptions = consumerOptions ?? {};
496
602
  const allPlugins = [...defaultPlugins, ...appOptions.plugins ?? []];
497
603
  validatePlugins(frameworkId, allPlugins);
604
+ checkCorePluginConflicts(frameworkId, allPlugins, new Set(corePlugins.map((p) => p.name)));
498
605
  return kernel({
499
606
  id: frameworkId,
500
607
  configDefaults,
501
608
  frameworkPluginConfigs,
502
609
  flatPlugins: allPlugins,
610
+ corePlugins,
611
+ corePluginConfigs,
503
612
  configOverrides: appOptions.config ?? {},
504
613
  consumerPluginConfigs: appOptions.pluginConfigs ?? {},
505
614
  onReady,
@@ -692,6 +801,8 @@ function createPluginFactory(frameworkId) {
692
801
  * @param id - Framework identifier used in error messages.
693
802
  * @param options - Configuration options containing the default config values.
694
803
  * @param options.config - Default configuration values for the framework.
804
+ * @param options.plugins - Optional core plugin instances to register.
805
+ * @param options.pluginConfigs - Optional config overrides for core plugins.
695
806
  * @returns An object with createPlugin (bound to Config/Events) and createCore.
696
807
  * @example
697
808
  * ```ts
@@ -704,12 +815,137 @@ function createPluginFactory(frameworkId) {
704
815
  function createCoreConfig(id, options) {
705
816
  const configDefaults = options.config;
706
817
  const frameworkId = id;
818
+ const corePlugins = options.plugins ?? [];
819
+ const corePluginConfigs = options.pluginConfigs ?? {};
820
+ validateCorePlugins(frameworkId, corePlugins);
707
821
  const createPlugin = createPluginFactory(frameworkId);
708
822
  return {
709
823
  createPlugin,
710
- createCore: createCoreFactory(frameworkId, configDefaults, createPlugin)
824
+ createCore: createCoreFactory(frameworkId, configDefaults, createPlugin, corePlugins, corePluginConfigs)
825
+ };
826
+ }
827
+
828
+ //#endregion
829
+ //#region src/core-plugin.ts
830
+ /**
831
+ * Reserved names that cannot be used for core plugins.
832
+ * Includes regular reserved names plus context property names that would collide.
833
+ */
834
+ const CORE_PLUGIN_RESERVED_NAMES = new Set([
835
+ "start",
836
+ "stop",
837
+ "emit",
838
+ "require",
839
+ "has",
840
+ "config",
841
+ "global",
842
+ "state",
843
+ "__proto__",
844
+ "constructor",
845
+ "prototype"
846
+ ]);
847
+ /** Fields that core plugins must not have. */
848
+ const CORE_PLUGIN_FORBIDDEN_FIELDS = [
849
+ "depends",
850
+ "events",
851
+ "hooks"
852
+ ];
853
+ /**
854
+ * Asserts that a core plugin name is a non-empty string and not reserved.
855
+ *
856
+ * @param name - Candidate core plugin name.
857
+ * @example
858
+ * ```ts
859
+ * assertValidCorePluginName("log"); // ok
860
+ * assertValidCorePluginName("config"); // throws
861
+ * ```
862
+ */
863
+ function assertValidCorePluginName(name) {
864
+ if (typeof name !== "string" || name.length === 0) throw new TypeError("Core plugin name must be a non-empty string.\n Pass a non-empty string as the first argument to createCorePlugin.");
865
+ if (CORE_PLUGIN_RESERVED_NAMES.has(name)) throw new TypeError(`Core plugin name "${name}" conflicts with a reserved name.\n Choose a different core plugin name.`);
866
+ }
867
+ /**
868
+ * Asserts that the core plugin spec is a non-null object.
869
+ *
870
+ * @param name - Validated core plugin name.
871
+ * @param spec - Candidate core plugin spec.
872
+ * @example
873
+ * ```ts
874
+ * assertValidCorePluginSpec("log", { api: () => ({}) }); // ok
875
+ * ```
876
+ */
877
+ function assertValidCorePluginSpec(name, spec) {
878
+ if (isRecord(spec)) return;
879
+ throw new TypeError(`Core plugin "${name}" has invalid spec: expected an object.\n Provide a plugin specification object as the second argument.`);
880
+ }
881
+ /**
882
+ * Asserts that the core plugin spec does not contain forbidden fields.
883
+ *
884
+ * @param name - Validated core plugin name.
885
+ * @param spec - Validated core plugin spec.
886
+ * @example
887
+ * ```ts
888
+ * assertNoCorePluginForbiddenFields("log", { api: () => ({}) }); // ok
889
+ * assertNoCorePluginForbiddenFields("log", { depends: [] }); // throws
890
+ * ```
891
+ */
892
+ function assertNoCorePluginForbiddenFields(name, spec) {
893
+ for (const field of CORE_PLUGIN_FORBIDDEN_FIELDS) if (field in spec) throw new TypeError(`Core plugin "${name}" cannot have "${field}".\n Core plugins are self-contained — remove the forbidden field.`);
894
+ }
895
+ /**
896
+ * Validates lifecycle handlers and factories on a core plugin spec.
897
+ *
898
+ * @param name - Validated core plugin name.
899
+ * @param spec - Validated core plugin spec.
900
+ * @example
901
+ * ```ts
902
+ * assertValidCorePluginCallbacks("log", { api: () => ({}) }); // ok
903
+ * ```
904
+ */
905
+ function assertValidCorePluginCallbacks(name, spec) {
906
+ for (const field of [
907
+ "api",
908
+ "createState",
909
+ "onInit",
910
+ "onStart",
911
+ "onStop"
912
+ ]) {
913
+ const value = spec[field];
914
+ if (value !== void 0 && typeof value !== "function") throw new TypeError(`Core plugin "${name}" has invalid ${field}: expected a function.\n Provide a function for ${field} or remove it from the spec.`);
915
+ }
916
+ }
917
+ /**
918
+ * Creates a core plugin instance. Core plugins are standalone, self-contained
919
+ * infrastructure plugins (log, storage, env) whose APIs are injected directly
920
+ * onto every regular plugin's context.
921
+ *
922
+ * @param name - Unique core plugin name (inferred as literal string type).
923
+ * @param spec - Core plugin specification: config, createState, api, lifecycle.
924
+ * @returns A CorePluginInstance carrying phantom types for compile-time inference.
925
+ * @example
926
+ * ```ts
927
+ * const logPlugin = createCorePlugin("log", {
928
+ * config: { level: "info" },
929
+ * createState: () => ({ entries: [] as string[] }),
930
+ * api: ctx => ({
931
+ * info: (msg: string) => { ctx.state.entries.push(msg); console.log(msg); },
932
+ * }),
933
+ * });
934
+ * ```
935
+ */
936
+ function createCorePlugin(name, spec) {
937
+ assertValidCorePluginName(name);
938
+ assertValidCorePluginSpec(name, spec);
939
+ assertNoCorePluginForbiddenFields(name, spec);
940
+ assertValidCorePluginCallbacks(name, spec);
941
+ return {
942
+ name,
943
+ spec,
944
+ _corePlugin: true,
945
+ _phantom: {}
711
946
  };
712
947
  }
713
948
 
714
949
  //#endregion
715
- exports.createCoreConfig = createCoreConfig;
950
+ exports.createCoreConfig = createCoreConfig;
951
+ exports.createCorePlugin = createCorePlugin;