@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/LICENSE +21 -0
- package/README.md +315 -0
- package/dist/index.cjs +715 -0
- package/dist/index.d.cts +900 -0
- package/dist/index.d.mts +900 -0
- package/dist/index.mjs +713 -0
- package/package.json +85 -0
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 };
|