@moku-labs/core 0.1.0-alpha.3 → 0.1.0-alpha.5

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.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,
@@ -633,6 +742,32 @@ function assertValidApi(frameworkId, pluginName, api) {
633
742
  function assertValidCreateState(frameworkId, pluginName, createState) {
634
743
  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 })`);
635
744
  }
745
+ /** PluginInstance field names that cannot be used as helper names. */
746
+ const PLUGIN_INSTANCE_RESERVED_KEYS = new Set([
747
+ "name",
748
+ "spec",
749
+ "_phantom"
750
+ ]);
751
+ /**
752
+ * Validates that `helpers` is a plain object of functions if provided.
753
+ * Also rejects helper names that collide with PluginInstance properties.
754
+ *
755
+ * @param frameworkId - Framework identifier used in error messages.
756
+ * @param pluginName - Validated plugin name.
757
+ * @param helpers - Candidate helpers value from plugin spec.
758
+ * @example
759
+ * ```ts
760
+ * assertValidHelpers("my-app", "router", { route: () => ({}) });
761
+ * ```
762
+ */
763
+ function assertValidHelpers(frameworkId, pluginName, helpers) {
764
+ if (helpers === void 0) return;
765
+ if (!isRecord(helpers)) throw new TypeError(`[${frameworkId}] Plugin "${pluginName}" has invalid helpers: expected an object.\n Provide an object of functions: helpers: { myHelper: (...args) => result }`);
766
+ for (const [key, value] of Object.entries(helpers)) {
767
+ if (typeof value !== "function") throw new TypeError(`[${frameworkId}] Plugin "${pluginName}" has invalid helper "${key}": expected a function.\n Each helper must be a function.`);
768
+ if (PLUGIN_INSTANCE_RESERVED_KEYS.has(key)) throw new TypeError(`[${frameworkId}] Plugin "${pluginName}" helper "${key}" conflicts with a PluginInstance property.\n Choose a different helper name.`);
769
+ }
770
+ }
636
771
  /**
637
772
  * Creates a bound `createPlugin` function that captures framework generics.
638
773
  *
@@ -671,10 +806,12 @@ function createPluginFactory(frameworkId) {
671
806
  assertValidHooks(frameworkId, name, spec.hooks);
672
807
  assertValidApi(frameworkId, name, spec.api);
673
808
  assertValidCreateState(frameworkId, name, spec.createState);
809
+ assertValidHelpers(frameworkId, name, spec.helpers);
674
810
  return {
675
811
  name,
676
812
  spec,
677
- _phantom: {}
813
+ _phantom: {},
814
+ ...isRecord(spec.helpers) ? spec.helpers : {}
678
815
  };
679
816
  };
680
817
  return createPlugin;
@@ -692,6 +829,8 @@ function createPluginFactory(frameworkId) {
692
829
  * @param id - Framework identifier used in error messages.
693
830
  * @param options - Configuration options containing the default config values.
694
831
  * @param options.config - Default configuration values for the framework.
832
+ * @param options.plugins - Optional core plugin instances to register.
833
+ * @param options.pluginConfigs - Optional config overrides for core plugins.
695
834
  * @returns An object with createPlugin (bound to Config/Events) and createCore.
696
835
  * @example
697
836
  * ```ts
@@ -704,12 +843,137 @@ function createPluginFactory(frameworkId) {
704
843
  function createCoreConfig(id, options) {
705
844
  const configDefaults = options.config;
706
845
  const frameworkId = id;
846
+ const corePlugins = options.plugins ?? [];
847
+ const corePluginConfigs = options.pluginConfigs ?? {};
848
+ validateCorePlugins(frameworkId, corePlugins);
707
849
  const createPlugin = createPluginFactory(frameworkId);
708
850
  return {
709
851
  createPlugin,
710
- createCore: createCoreFactory(frameworkId, configDefaults, createPlugin)
852
+ createCore: createCoreFactory(frameworkId, configDefaults, createPlugin, corePlugins, corePluginConfigs)
853
+ };
854
+ }
855
+
856
+ //#endregion
857
+ //#region src/core-plugin.ts
858
+ /**
859
+ * Reserved names that cannot be used for core plugins.
860
+ * Includes regular reserved names plus context property names that would collide.
861
+ */
862
+ const CORE_PLUGIN_RESERVED_NAMES = new Set([
863
+ "start",
864
+ "stop",
865
+ "emit",
866
+ "require",
867
+ "has",
868
+ "config",
869
+ "global",
870
+ "state",
871
+ "__proto__",
872
+ "constructor",
873
+ "prototype"
874
+ ]);
875
+ /** Fields that core plugins must not have. */
876
+ const CORE_PLUGIN_FORBIDDEN_FIELDS = [
877
+ "depends",
878
+ "events",
879
+ "hooks"
880
+ ];
881
+ /**
882
+ * Asserts that a core plugin name is a non-empty string and not reserved.
883
+ *
884
+ * @param name - Candidate core plugin name.
885
+ * @example
886
+ * ```ts
887
+ * assertValidCorePluginName("log"); // ok
888
+ * assertValidCorePluginName("config"); // throws
889
+ * ```
890
+ */
891
+ function assertValidCorePluginName(name) {
892
+ 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.");
893
+ 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.`);
894
+ }
895
+ /**
896
+ * Asserts that the core plugin spec is a non-null object.
897
+ *
898
+ * @param name - Validated core plugin name.
899
+ * @param spec - Candidate core plugin spec.
900
+ * @example
901
+ * ```ts
902
+ * assertValidCorePluginSpec("log", { api: () => ({}) }); // ok
903
+ * ```
904
+ */
905
+ function assertValidCorePluginSpec(name, spec) {
906
+ if (isRecord(spec)) return;
907
+ throw new TypeError(`Core plugin "${name}" has invalid spec: expected an object.\n Provide a plugin specification object as the second argument.`);
908
+ }
909
+ /**
910
+ * Asserts that the core plugin spec does not contain forbidden fields.
911
+ *
912
+ * @param name - Validated core plugin name.
913
+ * @param spec - Validated core plugin spec.
914
+ * @example
915
+ * ```ts
916
+ * assertNoCorePluginForbiddenFields("log", { api: () => ({}) }); // ok
917
+ * assertNoCorePluginForbiddenFields("log", { depends: [] }); // throws
918
+ * ```
919
+ */
920
+ function assertNoCorePluginForbiddenFields(name, spec) {
921
+ 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.`);
922
+ }
923
+ /**
924
+ * Validates lifecycle handlers and factories on a core plugin spec.
925
+ *
926
+ * @param name - Validated core plugin name.
927
+ * @param spec - Validated core plugin spec.
928
+ * @example
929
+ * ```ts
930
+ * assertValidCorePluginCallbacks("log", { api: () => ({}) }); // ok
931
+ * ```
932
+ */
933
+ function assertValidCorePluginCallbacks(name, spec) {
934
+ for (const field of [
935
+ "api",
936
+ "createState",
937
+ "onInit",
938
+ "onStart",
939
+ "onStop"
940
+ ]) {
941
+ const value = spec[field];
942
+ 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.`);
943
+ }
944
+ }
945
+ /**
946
+ * Creates a core plugin instance. Core plugins are standalone, self-contained
947
+ * infrastructure plugins (log, storage, env) whose APIs are injected directly
948
+ * onto every regular plugin's context.
949
+ *
950
+ * @param name - Unique core plugin name (inferred as literal string type).
951
+ * @param spec - Core plugin specification: config, createState, api, lifecycle.
952
+ * @returns A CorePluginInstance carrying phantom types for compile-time inference.
953
+ * @example
954
+ * ```ts
955
+ * const logPlugin = createCorePlugin("log", {
956
+ * config: { level: "info" },
957
+ * createState: () => ({ entries: [] as string[] }),
958
+ * api: ctx => ({
959
+ * info: (msg: string) => { ctx.state.entries.push(msg); console.log(msg); },
960
+ * }),
961
+ * });
962
+ * ```
963
+ */
964
+ function createCorePlugin(name, spec) {
965
+ assertValidCorePluginName(name);
966
+ assertValidCorePluginSpec(name, spec);
967
+ assertNoCorePluginForbiddenFields(name, spec);
968
+ assertValidCorePluginCallbacks(name, spec);
969
+ return {
970
+ name,
971
+ spec,
972
+ _corePlugin: true,
973
+ _phantom: {}
711
974
  };
712
975
  }
713
976
 
714
977
  //#endregion
715
- exports.createCoreConfig = createCoreConfig;
978
+ exports.createCoreConfig = createCoreConfig;
979
+ exports.createCorePlugin = createCorePlugin;