@kimesh/kit 0.2.20 → 0.2.21

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.d.mts CHANGED
@@ -116,6 +116,26 @@ interface KimeshRouteMiddleware {
116
116
  //#endregion
117
117
  //#region src/types/hooks.d.ts
118
118
  type HookResult = void | Promise<void>;
119
+ /**
120
+ * Normalized component directory for shadcn extension hook.
121
+ * Defined inline to avoid circular dependency with @kimesh/shadcn.
122
+ */
123
+ interface ShadcnComponentDir {
124
+ /** Absolute or relative path to component directory */
125
+ path: string;
126
+ /** Component name prefix (e.g., 'Ui' for UiButton) */
127
+ prefix: string;
128
+ }
129
+ /**
130
+ * Tailwind source entry for extension hook.
131
+ * Defined inline to avoid circular dependency with @kimesh/tailwindcss.
132
+ */
133
+ interface TailwindSourceEntry {
134
+ /** Absolute path to source directory */
135
+ path: string;
136
+ /** Optional comment in generated CSS */
137
+ comment?: string;
138
+ }
119
139
  interface KimeshHooks {
120
140
  /** Called after config is loaded, before layers */
121
141
  'config:loaded': (config: KimeshConfig) => HookResult;
@@ -179,6 +199,44 @@ interface KimeshHooks {
179
199
  'hmr:after': (file: string, kimesh: Kimesh) => HookResult;
180
200
  /** Called when Kimesh is closing */
181
201
  close: (kimesh: Kimesh) => HookResult;
202
+ /**
203
+ * Called by @kimesh/shadcn to allow extending modules to add component directories.
204
+ * Hook handlers can push entries to the componentDirs array.
205
+ *
206
+ * @example
207
+ * ```ts
208
+ * defineKimeshModule({
209
+ * hooks: {
210
+ * 'shadcn:componentDirs:extend': (componentDirs) => {
211
+ * componentDirs.push({
212
+ * path: '/path/to/uikit/components',
213
+ * prefix: 'Ui'
214
+ * })
215
+ * }
216
+ * }
217
+ * })
218
+ * ```
219
+ */
220
+ 'shadcn:componentDirs:extend': (componentDirs: ShadcnComponentDir[], kimesh: Kimesh) => HookResult;
221
+ /**
222
+ * Called by @kimesh/tailwindcss to allow extending modules to add source directories.
223
+ * Hook handlers can push entries to the sources array.
224
+ *
225
+ * @example
226
+ * ```ts
227
+ * defineKimeshModule({
228
+ * hooks: {
229
+ * 'tailwindcss:sources:extend': (sources) => {
230
+ * sources.push({
231
+ * path: '/path/to/uikit/src',
232
+ * comment: '@onesystem/uikit'
233
+ * })
234
+ * }
235
+ * }
236
+ * })
237
+ * ```
238
+ */
239
+ 'tailwindcss:sources:extend': (sources: TailwindSourceEntry[], kimesh: Kimesh) => HookResult;
182
240
  }
183
241
  //#endregion
184
242
  //#region src/types/runtime-plugin.d.ts
@@ -399,6 +457,23 @@ interface KimeshModuleMeta {
399
457
  /** Minimum Kimesh version required */kimesh?: string; /** Minimum Vite version required */
400
458
  vite?: string;
401
459
  };
460
+ /**
461
+ * Modules to auto-load before this module.
462
+ * Extended modules will be resolved and their hooks registered
463
+ * before this module's setup() is called.
464
+ *
465
+ * @example
466
+ * ```ts
467
+ * defineKimeshModule({
468
+ * meta: {
469
+ * name: '@onesystem/uikit',
470
+ * extends: ['@kimesh/shadcn', '@kimesh/tailwindcss'],
471
+ * },
472
+ * // ...
473
+ * })
474
+ * ```
475
+ */
476
+ extends?: string[];
402
477
  }
403
478
  /**
404
479
  * Module defaults can be:
@@ -1473,11 +1548,33 @@ declare function normalizeModuleInput<TOptions>(input: KimeshModuleInput<TOption
1473
1548
  options: Partial<TOptions>;
1474
1549
  }>;
1475
1550
  /**
1476
- * Execute a single module
1551
+ * Execute a single module (legacy API - kept for backward compatibility)
1552
+ * @deprecated Use executeModules for proper extends support
1477
1553
  */
1478
1554
  declare function executeModule<TOptions>(moduleInput: KimeshModuleInput<TOptions>, kimesh: Kimesh): Promise<void>;
1479
1555
  /**
1480
- * Execute all modules in order
1556
+ * Execute all modules using two-phase execution
1557
+ *
1558
+ * Two-phase execution ensures proper hook timing for module extends:
1559
+ *
1560
+ * **Phase 1: Resolve & Register Hooks**
1561
+ * - Resolves all modules (including those in `extends`)
1562
+ * - Registers hooks from all modules BEFORE any setup() runs
1563
+ * - This allows extending modules to inject config via hooks
1564
+ *
1565
+ * **Phase 2: Execute Setup**
1566
+ * - Calls setup() on all modules in dependency order
1567
+ * - Extended modules run first, then extending modules
1568
+ *
1569
+ * @example
1570
+ * ```ts
1571
+ * // @onesystem/uikit extends @kimesh/shadcn
1572
+ * // Execution order:
1573
+ * // 1. shadcn hooks registered
1574
+ * // 2. uikit hooks registered (can hook into shadcn)
1575
+ * // 3. shadcn.setup() runs (calls shadcn:componentDirs:extend hook)
1576
+ * // 4. uikit.setup() runs
1577
+ * ```
1481
1578
  */
1482
1579
  declare function executeModules(modules: KimeshModuleInput[], kimesh: Kimesh): Promise<void>;
1483
1580
  //#endregion
package/dist/index.mjs CHANGED
@@ -316,7 +316,8 @@ function defineKimeshModule(definition) {
316
316
  name: definition.meta?.name ?? "anonymous-module",
317
317
  version: definition.meta?.version ?? "0.0.0",
318
318
  configKey: definition.meta?.configKey ?? "",
319
- compatibility: definition.meta?.compatibility ?? {}
319
+ compatibility: definition.meta?.compatibility ?? {},
320
+ extends: definition.meta?.extends ?? []
320
321
  },
321
322
  async getDefaults(kimesh) {
322
323
  if (!definition.defaults) return {};
@@ -324,9 +325,6 @@ function defineKimeshModule(definition) {
324
325
  return definition.defaults;
325
326
  },
326
327
  async setup(options, kimesh) {
327
- if (definition.hooks) {
328
- for (const [hookName, handler] of Object.entries(definition.hooks)) if (handler) kimesh.hook(hookName, handler);
329
- }
330
328
  if (definition.setup) await definition.setup(options, kimesh);
331
329
  }
332
330
  };
@@ -335,6 +333,7 @@ function defineKimeshModule(definition) {
335
333
  * Resolve a string module name to an actual module
336
334
  */
337
335
  async function resolveStringModule(moduleName, kimesh) {
336
+ if (moduleName.includes("..") || moduleName.startsWith("/") || moduleName.includes("\\")) throw new Error(`Invalid module name: "${moduleName}". Module names cannot contain path traversal characters.`);
338
337
  try {
339
338
  const requirePath = kimesh.root + "/package.json";
340
339
  const require = createRequire(pathToFileURL(requirePath).href);
@@ -381,7 +380,82 @@ async function normalizeModuleInput(input, kimesh) {
381
380
  };
382
381
  }
383
382
  /**
384
- * Execute a single module
383
+ * Create a fresh execution context
384
+ */
385
+ function createExecutionContext() {
386
+ return {
387
+ loadedModules: /* @__PURE__ */ new Set(),
388
+ loadingStack: [],
389
+ resolvedModules: /* @__PURE__ */ new Map()
390
+ };
391
+ }
392
+ /**
393
+ * PHASE 1: Resolve module and its dependencies, register hooks
394
+ *
395
+ * This function:
396
+ * 1. Resolves the module (from string or object)
397
+ * 2. Recursively resolves all modules in `extends` first
398
+ * 3. Registers the module's hooks
399
+ * 4. Tracks loaded modules to prevent duplicates
400
+ * 5. Detects circular dependencies
401
+ */
402
+ async function resolveAndRegisterHooks(moduleInput, kimesh, ctx, userOptions = {}) {
403
+ const { module, options: inputOptions } = await normalizeModuleInput(moduleInput, kimesh);
404
+ const moduleName = module.meta.name;
405
+ if (ctx.loadedModules.has(moduleName)) return;
406
+ if (ctx.loadingStack.includes(moduleName)) {
407
+ const cycle = [...ctx.loadingStack, moduleName].join(" -> ");
408
+ throw new Error(`Circular dependency detected: ${cycle}`);
409
+ }
410
+ ctx.loadingStack.push(moduleName);
411
+ try {
412
+ const extendsModules = module.meta.extends || [];
413
+ for (const extendedName of extendsModules) await resolveAndRegisterHooks(extendedName, kimesh, ctx);
414
+ ctx.loadedModules.add(moduleName);
415
+ const mergedOptions = {
416
+ ...inputOptions,
417
+ ...userOptions
418
+ };
419
+ ctx.resolvedModules.set(moduleName, {
420
+ module,
421
+ options: mergedOptions
422
+ });
423
+ if (module._def.hooks) {
424
+ for (const [hookName, handler] of Object.entries(module._def.hooks)) if (handler) kimesh.hook(hookName, handler);
425
+ }
426
+ } finally {
427
+ ctx.loadingStack.pop();
428
+ }
429
+ }
430
+ /**
431
+ * PHASE 2: Execute all setup() functions in dependency order
432
+ *
433
+ * Called after all modules are resolved and hooks registered.
434
+ * Modules execute in the order they were resolved (dependencies first).
435
+ */
436
+ async function executeAllSetups(kimesh, ctx) {
437
+ for (const [moduleName, { module, options: userOptions }] of ctx.resolvedModules) {
438
+ _setKimeshContext(kimesh);
439
+ try {
440
+ const defaults = await module.getDefaults(kimesh);
441
+ const configKey = module.meta.configKey;
442
+ const configOptions = configKey ? kimesh.options.config[configKey] ?? {} : {};
443
+ const mergedOptions = {
444
+ ...defaults,
445
+ ...configOptions,
446
+ ...userOptions
447
+ };
448
+ await module.setup(mergedOptions, kimesh);
449
+ } catch (error) {
450
+ consola.error(`[Kimesh] Failed to execute module "${moduleName}":`, error);
451
+ } finally {
452
+ _setKimeshContext(void 0);
453
+ }
454
+ }
455
+ }
456
+ /**
457
+ * Execute a single module (legacy API - kept for backward compatibility)
458
+ * @deprecated Use executeModules for proper extends support
385
459
  */
386
460
  async function executeModule(moduleInput, kimesh) {
387
461
  const { module, options: userOptions } = await normalizeModuleInput(moduleInput, kimesh);
@@ -393,6 +467,9 @@ async function executeModule(moduleInput, kimesh) {
393
467
  ...configOptions,
394
468
  ...userOptions
395
469
  };
470
+ if (module._def.hooks) {
471
+ for (const [hookName, handler] of Object.entries(module._def.hooks)) if (handler) kimesh.hook(hookName, handler);
472
+ }
396
473
  _setKimeshContext(kimesh);
397
474
  try {
398
475
  await module.setup(mergedOptions, kimesh);
@@ -413,16 +490,39 @@ function getModuleName(input) {
413
490
  return input.meta?.name ?? "unknown";
414
491
  }
415
492
  /**
416
- * Execute all modules in order
493
+ * Execute all modules using two-phase execution
494
+ *
495
+ * Two-phase execution ensures proper hook timing for module extends:
496
+ *
497
+ * **Phase 1: Resolve & Register Hooks**
498
+ * - Resolves all modules (including those in `extends`)
499
+ * - Registers hooks from all modules BEFORE any setup() runs
500
+ * - This allows extending modules to inject config via hooks
501
+ *
502
+ * **Phase 2: Execute Setup**
503
+ * - Calls setup() on all modules in dependency order
504
+ * - Extended modules run first, then extending modules
505
+ *
506
+ * @example
507
+ * ```ts
508
+ * // @onesystem/uikit extends @kimesh/shadcn
509
+ * // Execution order:
510
+ * // 1. shadcn hooks registered
511
+ * // 2. uikit hooks registered (can hook into shadcn)
512
+ * // 3. shadcn.setup() runs (calls shadcn:componentDirs:extend hook)
513
+ * // 4. uikit.setup() runs
514
+ * ```
417
515
  */
418
516
  async function executeModules(modules, kimesh) {
419
517
  await kimesh.callHook("modules:before", kimesh);
518
+ const ctx = createExecutionContext();
420
519
  for (const moduleInput of modules) try {
421
- await executeModule(moduleInput, kimesh);
520
+ await resolveAndRegisterHooks(moduleInput, kimesh, ctx, Array.isArray(moduleInput) ? moduleInput[1] : {});
422
521
  } catch (error) {
423
522
  const moduleName = getModuleName(moduleInput);
424
- consola.error(`[Kimesh] Failed to execute module "${moduleName}":`, error);
523
+ consola.error(`[Kimesh] Failed to resolve module "${moduleName}":`, error);
425
524
  }
525
+ await executeAllSetups(kimesh, ctx);
426
526
  await kimesh.callHook("modules:done", kimesh);
427
527
  }
428
528
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kimesh/kit",
3
- "version": "0.2.20",
3
+ "version": "0.2.21",
4
4
  "description": "Build-time engine for Kimesh framework",
5
5
  "repository": {
6
6
  "type": "git",
@@ -27,9 +27,9 @@
27
27
  "test:watch": "vitest"
28
28
  },
29
29
  "dependencies": {
30
- "@kimesh/auto-import": "0.2.20",
31
- "@kimesh/layers": "0.2.20",
32
- "@kimesh/router-generator": "0.2.20",
30
+ "@kimesh/auto-import": "0.2.21",
31
+ "@kimesh/layers": "0.2.21",
32
+ "@kimesh/router-generator": "0.2.21",
33
33
  "@vitejs/plugin-vue": "^6.0.3",
34
34
  "c12": "^3.3.3",
35
35
  "consola": "^3.4.2",