@moku-labs/core 0.1.0-alpha.3 → 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/dist/index.mjs CHANGED
@@ -30,6 +30,8 @@ const RESERVED_NAMES = new Set([
30
30
  "require",
31
31
  "has",
32
32
  "config",
33
+ "global",
34
+ "state",
33
35
  "__proto__",
34
36
  "constructor",
35
37
  "prototype"
@@ -106,6 +108,37 @@ function validatePlugins(id, plugins) {
106
108
  checkDuplicateNames(id, names);
107
109
  checkDependencyOrder(id, plugins, names);
108
110
  }
111
+ /**
112
+ * Validate core plugins: no reserved names, no duplicates.
113
+ *
114
+ * @param id - Framework identifier for error messages.
115
+ * @param corePlugins - The core plugin list to validate.
116
+ * @throws {TypeError} If validation fails.
117
+ * @example
118
+ * ```ts
119
+ * validateCorePlugins("my-site", [logPlugin, envPlugin]); // throws if invalid
120
+ * ```
121
+ */
122
+ function validateCorePlugins(id, corePlugins) {
123
+ const names = corePlugins.map((p) => p.name);
124
+ checkReservedNames(id, names);
125
+ checkDuplicateNames(id, names);
126
+ }
127
+ /**
128
+ * Validate that no regular plugin name conflicts with a core plugin name.
129
+ *
130
+ * @param id - Framework identifier for error messages.
131
+ * @param plugins - The regular plugin list.
132
+ * @param corePluginNames - Set of core plugin names.
133
+ * @throws {TypeError} If a name conflict is found.
134
+ * @example
135
+ * ```ts
136
+ * checkCorePluginConflicts("my-site", [routerPlugin], new Set(["log", "env"]));
137
+ * ```
138
+ */
139
+ function checkCorePluginConflicts(id, plugins, corePluginNames) {
140
+ 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.`);
141
+ }
109
142
 
110
143
  //#endregion
111
144
  //#region src/app.ts
@@ -300,6 +333,7 @@ function registerPluginHooks(flatPlugins, buildPluginContext, registerHook) {
300
333
  * @param runtime - The kernel runtime with shared state.
301
334
  * @param resolvedConfigs - The resolved per-plugin config map.
302
335
  * @param states - The plugin state map.
336
+ * @param coreApis - Core plugin APIs to spread onto every plugin context.
303
337
  * @returns A factory function that produces a PluginContext for any plugin.
304
338
  * @example
305
339
  * ```ts
@@ -307,7 +341,7 @@ function registerPluginHooks(flatPlugins, buildPluginContext, registerHook) {
307
341
  * const ctx = factory(routerPlugin);
308
342
  * ```
309
343
  */
310
- function createContextFactory(runtime, resolvedConfigs, states) {
344
+ function createContextFactory(runtime, resolvedConfigs, states, coreApis) {
311
345
  const has = createHas(runtime);
312
346
  return (plugin) => ({
313
347
  global: runtime.globalConfig,
@@ -315,7 +349,8 @@ function createContextFactory(runtime, resolvedConfigs, states) {
315
349
  state: states.get(plugin.name),
316
350
  emit: runtime.emit,
317
351
  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
352
+ has,
353
+ ...coreApis
319
354
  });
320
355
  }
321
356
  /**
@@ -362,6 +397,10 @@ async function executeStop(flatPlugins, globalConfig) {
362
397
  * @param runtime - The kernel runtime with shared state and APIs.
363
398
  * @param flatPlugins - The flattened plugin list.
364
399
  * @param buildPluginContext - Factory that builds context for a plugin.
400
+ * @param corePluginData - Core plugin instances, resolved configs, and states.
401
+ * @param corePluginData.plugins - The core plugin instances.
402
+ * @param corePluginData.configs - Resolved core plugin config map.
403
+ * @param corePluginData.states - Core plugin state map.
365
404
  * @param consumer - Optional consumer lifecycle callbacks.
366
405
  * @returns A frozen app object with lifecycle methods and plugin APIs.
367
406
  * @example
@@ -370,13 +409,17 @@ async function executeStop(flatPlugins, globalConfig) {
370
409
  * await app.start();
371
410
  * ```
372
411
  */
373
- function buildApp(runtime, flatPlugins, buildPluginContext, consumer) {
412
+ function buildApp(runtime, flatPlugins, buildPluginContext, corePluginData, consumer) {
374
413
  let started = false;
375
414
  const appRequire = createRequire(runtime, (name) => `[${runtime.id}] app.require("${name}") failed: "${name}" is not registered.\n Check your plugin list.`);
376
415
  const appHas = createHas(runtime);
377
416
  const app = {
378
417
  start: async () => {
379
418
  if (started) throw new Error(`[${runtime.id}] App already started.\n start() can only be called once.`);
419
+ for (const plugin of corePluginData.plugins) if (plugin.spec.onStart) await plugin.spec.onStart({
420
+ config: corePluginData.configs.get(plugin.name) ?? {},
421
+ state: corePluginData.states.get(plugin.name)
422
+ });
380
423
  for (const plugin of flatPlugins) if (plugin.spec.onStart) await plugin.spec.onStart(buildPluginContext(plugin));
381
424
  if (consumer?.onStart) await consumer.onStart(buildCallbackContext(runtime));
382
425
  started = true;
@@ -384,6 +427,10 @@ function buildApp(runtime, flatPlugins, buildPluginContext, consumer) {
384
427
  stop: async () => {
385
428
  if (!started) throw new Error(`[${runtime.id}] App not started.\n Call start() before stop().`);
386
429
  await executeStop(flatPlugins, runtime.globalConfig);
430
+ for (const plugin of corePluginData.plugins.toReversed()) if (plugin.spec.onStop) await plugin.spec.onStop({
431
+ config: corePluginData.configs.get(plugin.name) ?? {},
432
+ state: corePluginData.states.get(plugin.name)
433
+ });
387
434
  if (consumer?.onStop) await consumer.onStop(buildCallbackContext(runtime));
388
435
  },
389
436
  emit: (eventName, payload) => {
@@ -396,6 +443,59 @@ function buildApp(runtime, flatPlugins, buildPluginContext, consumer) {
396
443
  return Object.freeze(app);
397
444
  }
398
445
  /**
446
+ * Initialize core plugins: resolve configs, create states, build APIs, run onInit.
447
+ *
448
+ * @param corePlugins - Core plugin instances.
449
+ * @param corePluginConfigs - Config overrides from createCoreConfig.
450
+ * @param frameworkPluginConfigs - Config overrides from createCore.
451
+ * @param consumerPluginConfigs - Config overrides from createApp.
452
+ * @returns Resolved configs, states, core APIs map, and shared apis Map.
453
+ * @example
454
+ * ```ts
455
+ * const { coreResolvedConfigs, coreStates, coreApis, apis } = initCorePlugins(
456
+ * corePlugins, corePluginConfigs, frameworkPluginConfigs, consumerPluginConfigs
457
+ * );
458
+ * ```
459
+ */
460
+ function initCorePlugins(corePlugins, corePluginConfigs, frameworkPluginConfigs, consumerPluginConfigs) {
461
+ const coreResolvedConfigs = /* @__PURE__ */ new Map();
462
+ for (const plugin of corePlugins) {
463
+ const merged = Object.freeze({
464
+ ...plugin.spec.config,
465
+ ...asRecord(corePluginConfigs[plugin.name]),
466
+ ...asRecord(frameworkPluginConfigs[plugin.name]),
467
+ ...asRecord(consumerPluginConfigs[plugin.name])
468
+ });
469
+ coreResolvedConfigs.set(plugin.name, merged);
470
+ }
471
+ const coreStates = /* @__PURE__ */ new Map();
472
+ for (const plugin of corePlugins) if (plugin.spec.createState) {
473
+ const pluginConfig = coreResolvedConfigs.get(plugin.name) ?? {};
474
+ coreStates.set(plugin.name, plugin.spec.createState({ config: pluginConfig }));
475
+ } else coreStates.set(plugin.name, {});
476
+ const coreApis = {};
477
+ const apis = /* @__PURE__ */ new Map();
478
+ for (const plugin of corePlugins) if (plugin.spec.api) {
479
+ const context = {
480
+ config: coreResolvedConfigs.get(plugin.name) ?? {},
481
+ state: coreStates.get(plugin.name)
482
+ };
483
+ const api = plugin.spec.api(context);
484
+ coreApis[plugin.name] = api;
485
+ apis.set(plugin.name, api);
486
+ }
487
+ for (const plugin of corePlugins) if (plugin.spec.onInit) plugin.spec.onInit({
488
+ config: coreResolvedConfigs.get(plugin.name) ?? {},
489
+ state: coreStates.get(plugin.name)
490
+ });
491
+ return {
492
+ coreResolvedConfigs,
493
+ coreStates,
494
+ coreApis,
495
+ apis
496
+ };
497
+ }
498
+ /**
399
499
  * The kernel — creates and initializes the application.
400
500
  *
401
501
  * Receives pre-flattened, pre-validated plugins and all captured context from
@@ -410,15 +510,15 @@ function buildApp(runtime, flatPlugins, buildPluginContext, consumer) {
410
510
  * ```
411
511
  */
412
512
  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));
513
+ const { id, configDefaults, frameworkPluginConfigs, flatPlugins, corePlugins, corePluginConfigs, configOverrides, consumerPluginConfigs, onReady, onError, consumer } = parameters;
514
+ const pluginNameSet = new Set([...corePlugins.map((plugin) => plugin.name), ...flatPlugins.map((plugin) => plugin.name)]);
415
515
  const globalConfig = Object.freeze({
416
516
  ...configDefaults,
417
517
  ...configOverrides
418
518
  });
519
+ const { coreResolvedConfigs, coreStates, coreApis, apis } = initCorePlugins(corePlugins, corePluginConfigs, frameworkPluginConfigs, consumerPluginConfigs);
419
520
  const resolvedConfigs = resolvePluginConfigs(flatPlugins, frameworkPluginConfigs, consumerPluginConfigs);
420
521
  const states = createPluginStates(flatPlugins, globalConfig, resolvedConfigs);
421
- const apis = /* @__PURE__ */ new Map();
422
522
  const { emit, registerHook } = buildEventBus(onError || consumer?.onError ? (error) => {
423
523
  if (onError) onError(error);
424
524
  if (consumer?.onError) consumer.onError(error, buildCallbackContext(runtime));
@@ -430,13 +530,17 @@ function kernel(parameters) {
430
530
  apis,
431
531
  pluginNameSet
432
532
  };
433
- const buildPluginContext = createContextFactory(runtime, resolvedConfigs, states);
533
+ const buildPluginContext = createContextFactory(runtime, resolvedConfigs, states, coreApis);
434
534
  registerPluginHooks(flatPlugins, buildPluginContext, registerHook);
435
535
  for (const plugin of flatPlugins) if (plugin.spec.api) apis.set(plugin.name, plugin.spec.api(buildPluginContext(plugin)));
436
536
  for (const plugin of flatPlugins) if (plugin.spec.onInit) plugin.spec.onInit(buildPluginContext(plugin));
437
537
  if (onReady) onReady({ config: globalConfig });
438
538
  if (consumer?.onReady) consumer.onReady(buildCallbackContext(runtime));
439
- return buildApp(runtime, flatPlugins, buildPluginContext, consumer);
539
+ return buildApp(runtime, flatPlugins, buildPluginContext, {
540
+ plugins: corePlugins,
541
+ configs: coreResolvedConfigs,
542
+ states: coreStates
543
+ }, consumer);
440
544
  }
441
545
 
442
546
  //#endregion
@@ -451,6 +555,8 @@ function kernel(parameters) {
451
555
  * @param frameworkId - The framework identifier for error messages.
452
556
  * @param configDefaults - Default config values captured from Step 1.
453
557
  * @param createPlugin - Bound createPlugin function from Step 1.
558
+ * @param corePlugins - Core plugin instances from createCoreConfig.
559
+ * @param corePluginConfigs - Core plugin config overrides from createCoreConfig.
454
560
  * @returns A createCore function bound to the framework's Config and Events types.
455
561
  * @example
456
562
  * ```ts
@@ -459,7 +565,7 @@ function kernel(parameters) {
459
565
  * );
460
566
  * ```
461
567
  */
462
- function createCoreFactory(frameworkId, configDefaults, createPlugin) {
568
+ function createCoreFactory(frameworkId, configDefaults, createPlugin, corePlugins, corePluginConfigs) {
463
569
  /**
464
570
  * Step 2: Captures framework default plugins and returns createApp.
465
571
  *
@@ -493,11 +599,14 @@ function createCoreFactory(frameworkId, configDefaults, createPlugin) {
493
599
  const appOptions = consumerOptions ?? {};
494
600
  const allPlugins = [...defaultPlugins, ...appOptions.plugins ?? []];
495
601
  validatePlugins(frameworkId, allPlugins);
602
+ checkCorePluginConflicts(frameworkId, allPlugins, new Set(corePlugins.map((p) => p.name)));
496
603
  return kernel({
497
604
  id: frameworkId,
498
605
  configDefaults,
499
606
  frameworkPluginConfigs,
500
607
  flatPlugins: allPlugins,
608
+ corePlugins,
609
+ corePluginConfigs,
501
610
  configOverrides: appOptions.config ?? {},
502
611
  consumerPluginConfigs: appOptions.pluginConfigs ?? {},
503
612
  onReady,
@@ -690,6 +799,8 @@ function createPluginFactory(frameworkId) {
690
799
  * @param id - Framework identifier used in error messages.
691
800
  * @param options - Configuration options containing the default config values.
692
801
  * @param options.config - Default configuration values for the framework.
802
+ * @param options.plugins - Optional core plugin instances to register.
803
+ * @param options.pluginConfigs - Optional config overrides for core plugins.
693
804
  * @returns An object with createPlugin (bound to Config/Events) and createCore.
694
805
  * @example
695
806
  * ```ts
@@ -702,12 +813,136 @@ function createPluginFactory(frameworkId) {
702
813
  function createCoreConfig(id, options) {
703
814
  const configDefaults = options.config;
704
815
  const frameworkId = id;
816
+ const corePlugins = options.plugins ?? [];
817
+ const corePluginConfigs = options.pluginConfigs ?? {};
818
+ validateCorePlugins(frameworkId, corePlugins);
705
819
  const createPlugin = createPluginFactory(frameworkId);
706
820
  return {
707
821
  createPlugin,
708
- createCore: createCoreFactory(frameworkId, configDefaults, createPlugin)
822
+ createCore: createCoreFactory(frameworkId, configDefaults, createPlugin, corePlugins, corePluginConfigs)
823
+ };
824
+ }
825
+
826
+ //#endregion
827
+ //#region src/core-plugin.ts
828
+ /**
829
+ * Reserved names that cannot be used for core plugins.
830
+ * Includes regular reserved names plus context property names that would collide.
831
+ */
832
+ const CORE_PLUGIN_RESERVED_NAMES = new Set([
833
+ "start",
834
+ "stop",
835
+ "emit",
836
+ "require",
837
+ "has",
838
+ "config",
839
+ "global",
840
+ "state",
841
+ "__proto__",
842
+ "constructor",
843
+ "prototype"
844
+ ]);
845
+ /** Fields that core plugins must not have. */
846
+ const CORE_PLUGIN_FORBIDDEN_FIELDS = [
847
+ "depends",
848
+ "events",
849
+ "hooks"
850
+ ];
851
+ /**
852
+ * Asserts that a core plugin name is a non-empty string and not reserved.
853
+ *
854
+ * @param name - Candidate core plugin name.
855
+ * @example
856
+ * ```ts
857
+ * assertValidCorePluginName("log"); // ok
858
+ * assertValidCorePluginName("config"); // throws
859
+ * ```
860
+ */
861
+ function assertValidCorePluginName(name) {
862
+ 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.");
863
+ 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.`);
864
+ }
865
+ /**
866
+ * Asserts that the core plugin spec is a non-null object.
867
+ *
868
+ * @param name - Validated core plugin name.
869
+ * @param spec - Candidate core plugin spec.
870
+ * @example
871
+ * ```ts
872
+ * assertValidCorePluginSpec("log", { api: () => ({}) }); // ok
873
+ * ```
874
+ */
875
+ function assertValidCorePluginSpec(name, spec) {
876
+ if (isRecord(spec)) return;
877
+ throw new TypeError(`Core plugin "${name}" has invalid spec: expected an object.\n Provide a plugin specification object as the second argument.`);
878
+ }
879
+ /**
880
+ * Asserts that the core plugin spec does not contain forbidden fields.
881
+ *
882
+ * @param name - Validated core plugin name.
883
+ * @param spec - Validated core plugin spec.
884
+ * @example
885
+ * ```ts
886
+ * assertNoCorePluginForbiddenFields("log", { api: () => ({}) }); // ok
887
+ * assertNoCorePluginForbiddenFields("log", { depends: [] }); // throws
888
+ * ```
889
+ */
890
+ function assertNoCorePluginForbiddenFields(name, spec) {
891
+ 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.`);
892
+ }
893
+ /**
894
+ * Validates lifecycle handlers and factories on a core plugin spec.
895
+ *
896
+ * @param name - Validated core plugin name.
897
+ * @param spec - Validated core plugin spec.
898
+ * @example
899
+ * ```ts
900
+ * assertValidCorePluginCallbacks("log", { api: () => ({}) }); // ok
901
+ * ```
902
+ */
903
+ function assertValidCorePluginCallbacks(name, spec) {
904
+ for (const field of [
905
+ "api",
906
+ "createState",
907
+ "onInit",
908
+ "onStart",
909
+ "onStop"
910
+ ]) {
911
+ const value = spec[field];
912
+ 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.`);
913
+ }
914
+ }
915
+ /**
916
+ * Creates a core plugin instance. Core plugins are standalone, self-contained
917
+ * infrastructure plugins (log, storage, env) whose APIs are injected directly
918
+ * onto every regular plugin's context.
919
+ *
920
+ * @param name - Unique core plugin name (inferred as literal string type).
921
+ * @param spec - Core plugin specification: config, createState, api, lifecycle.
922
+ * @returns A CorePluginInstance carrying phantom types for compile-time inference.
923
+ * @example
924
+ * ```ts
925
+ * const logPlugin = createCorePlugin("log", {
926
+ * config: { level: "info" },
927
+ * createState: () => ({ entries: [] as string[] }),
928
+ * api: ctx => ({
929
+ * info: (msg: string) => { ctx.state.entries.push(msg); console.log(msg); },
930
+ * }),
931
+ * });
932
+ * ```
933
+ */
934
+ function createCorePlugin(name, spec) {
935
+ assertValidCorePluginName(name);
936
+ assertValidCorePluginSpec(name, spec);
937
+ assertNoCorePluginForbiddenFields(name, spec);
938
+ assertValidCorePluginCallbacks(name, spec);
939
+ return {
940
+ name,
941
+ spec,
942
+ _corePlugin: true,
943
+ _phantom: {}
709
944
  };
710
945
  }
711
946
 
712
947
  //#endregion
713
- export { createCoreConfig };
948
+ export { createCoreConfig, createCorePlugin };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moku-labs/core",
3
- "version": "0.1.0-alpha.3",
3
+ "version": "0.1.0-alpha.4",
4
4
  "author": "Alex Kucherenko",
5
5
  "repository": {
6
6
  "type": "git",