@mokup/cli 1.0.9 → 1.1.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/dist/index.cjs CHANGED
@@ -7,6 +7,7 @@ const node_buffer = require('node:buffer');
7
7
  const node_module = require('node:module');
8
8
  const node_url = require('node:url');
9
9
  const esbuild = require('@mokup/shared/esbuild');
10
+ const pathUtils = require('@mokup/shared/path-utils');
10
11
  const runtime = require('@mokup/runtime');
11
12
  const jsoncParser = require('@mokup/shared/jsonc-parser');
12
13
  const node = require('@mokup/server/node');
@@ -130,12 +131,16 @@ async function loadConfig(file) {
130
131
  if (!mod) {
131
132
  return null;
132
133
  }
133
- const value = mod?.default ?? mod;
134
+ const raw = mod?.default ?? mod;
135
+ const value = isPromise$1(raw) ? await raw : raw;
134
136
  if (!value || typeof value !== "object") {
135
137
  return null;
136
138
  }
137
139
  return value;
138
140
  }
141
+ function isPromise$1(value) {
142
+ return !!value && typeof value.then === "function";
143
+ }
139
144
  function normalizeMiddlewares(value, source, log, position) {
140
145
  if (!value) {
141
146
  return [];
@@ -264,10 +269,6 @@ async function resolveDirectoryConfig(params) {
264
269
  };
265
270
  }
266
271
 
267
- function toPosix(value) {
268
- return value.replace(/\\/g, "/");
269
- }
270
-
271
272
  const supportedExtensions = /* @__PURE__ */ new Set([
272
273
  ".json",
273
274
  ".jsonc",
@@ -318,34 +319,10 @@ function resolveDirs(dir, root) {
318
319
  );
319
320
  return Array.from(new Set(normalized));
320
321
  }
321
- function testPatterns(patterns, value) {
322
- const list = Array.isArray(patterns) ? patterns : [patterns];
323
- return list.some((pattern) => pattern.test(value));
324
- }
325
- function matchesFilter(file, include, exclude) {
326
- const normalized = toPosix(file);
327
- if (exclude && testPatterns(exclude, normalized)) {
328
- return false;
329
- }
330
- if (include) {
331
- return testPatterns(include, normalized);
332
- }
333
- return true;
334
- }
335
322
  function normalizeIgnorePrefix(value, fallback = ["."]) {
336
323
  const list = typeof value === "undefined" ? fallback : Array.isArray(value) ? value : [value];
337
324
  return list.filter((entry) => typeof entry === "string" && entry.length > 0);
338
325
  }
339
- function hasIgnoredPrefix(file, rootDir, prefixes) {
340
- if (prefixes.length === 0) {
341
- return false;
342
- }
343
- const relativePath = toPosix(pathe.relative(rootDir, file));
344
- const segments = relativePath.split("/");
345
- return segments.some(
346
- (segment) => prefixes.some((prefix) => segment.startsWith(prefix))
347
- );
348
- }
349
326
  function isSupportedFile(file) {
350
327
  if (file.endsWith(".d.ts")) {
351
328
  return false;
@@ -363,7 +340,7 @@ function getHandlerModulePath(file, handlersDir, root) {
363
340
  const relNoExt = `${relFromRoot.slice(0, relFromRoot.length - ext.length)}.mjs`;
364
341
  const outputPath = pathe.join(handlersDir, relNoExt);
365
342
  const relFromOutDir = pathe.relative(pathe.dirname(handlersDir), outputPath);
366
- const normalized = toPosix(relFromOutDir);
343
+ const normalized = pathUtils.toPosix(relFromOutDir);
367
344
  return normalized.startsWith(".") ? normalized : `./${normalized}`;
368
345
  }
369
346
  async function writeHandlerIndex(handlerModuleMap, handlersDir, outDir) {
@@ -375,7 +352,7 @@ async function writeHandlerIndex(handlerModuleMap, handlersDir, outDir) {
375
352
  const entries = [];
376
353
  modulePaths.forEach((modulePath, index) => {
377
354
  const absolutePath = pathe.resolve(outDir, modulePath);
378
- const relImport = toPosix(pathe.relative(handlersDir, absolutePath));
355
+ const relImport = pathUtils.toPosix(pathe.relative(handlersDir, absolutePath));
379
356
  const importPath = relImport.startsWith(".") ? relImport : `./${relImport}`;
380
357
  const name = `module${index}`;
381
358
  imports.push(`import * as ${name} from '${importPath}'`);
@@ -509,7 +486,7 @@ function stripMethodSuffix(base) {
509
486
  };
510
487
  }
511
488
  function deriveRouteFromFile(file, rootDir, log) {
512
- const rel = toPosix(pathe.relative(rootDir, file));
489
+ const rel = pathUtils.toPosix(pathe.relative(rootDir, file));
513
490
  const ext = pathe.extname(rel);
514
491
  const withoutExt = rel.slice(0, rel.length - ext.length);
515
492
  const dir = pathe.dirname(withoutExt);
@@ -525,7 +502,7 @@ function deriveRouteFromFile(file, rootDir, log) {
525
502
  return null;
526
503
  }
527
504
  const joined = dir === "." ? name : pathe.join(dir, name);
528
- const segments = toPosix(joined).split("/");
505
+ const segments = pathUtils.toPosix(joined).split("/");
529
506
  if (segments.at(-1) === "index") {
530
507
  segments.pop();
531
508
  }
@@ -683,7 +660,7 @@ async function buildManifest(options = {}) {
683
660
  continue;
684
661
  }
685
662
  const effectiveIgnorePrefix = typeof config.ignorePrefix !== "undefined" ? normalizeIgnorePrefix(config.ignorePrefix, []) : globalIgnorePrefix;
686
- if (hasIgnoredPrefix(fileInfo.file, fileInfo.rootDir, effectiveIgnorePrefix)) {
663
+ if (pathUtils.hasIgnoredPrefix(fileInfo.file, fileInfo.rootDir, effectiveIgnorePrefix)) {
687
664
  continue;
688
665
  }
689
666
  if (!isSupportedFile(fileInfo.file)) {
@@ -691,7 +668,7 @@ async function buildManifest(options = {}) {
691
668
  }
692
669
  const effectiveInclude = typeof config.include !== "undefined" ? config.include : options.include;
693
670
  const effectiveExclude = typeof config.exclude !== "undefined" ? config.exclude : options.exclude;
694
- if (!matchesFilter(fileInfo.file, effectiveInclude, effectiveExclude)) {
671
+ if (!pathUtils.matchesFilter(fileInfo.file, effectiveInclude, effectiveExclude)) {
695
672
  continue;
696
673
  }
697
674
  const derived = deriveRouteFromFile(fileInfo.file, fileInfo.rootDir, options.log);
@@ -753,7 +730,7 @@ async function buildManifest(options = {}) {
753
730
  if (!response) {
754
731
  continue;
755
732
  }
756
- const source = toPosix(pathe.relative(root, fileInfo.file));
733
+ const source = pathUtils.toPosix(pathe.relative(root, fileInfo.file));
757
734
  const middlewareRefs = options.handlers === false ? [] : config.middlewares.map((entry) => {
758
735
  handlerSources.add(entry.file);
759
736
  const modulePath = getHandlerModulePath(entry.file, handlersDir, root);
@@ -817,12 +794,90 @@ async function buildManifest(options = {}) {
817
794
  }
818
795
 
819
796
  const middlewareSymbol = Symbol.for("mokup.config.middlewares");
820
- function createRegistry(list) {
821
- return {
822
- use: (...handlers) => {
823
- list.push(...handlers);
797
+ const contextStack = [];
798
+ function getActiveContext() {
799
+ const context = contextStack[contextStack.length - 1];
800
+ if (!context) {
801
+ throw new Error("onBeforeAll/onAfterAll must be called inside defineConfig()");
802
+ }
803
+ return context;
804
+ }
805
+ function runWithContext(context, fn) {
806
+ contextStack.push(context);
807
+ try {
808
+ const result = fn();
809
+ if (isPromise(result)) {
810
+ return result.finally(() => {
811
+ contextStack.pop();
812
+ });
813
+ }
814
+ contextStack.pop();
815
+ return result;
816
+ } catch (error) {
817
+ contextStack.pop();
818
+ throw error;
819
+ }
820
+ }
821
+ function isPromise(value) {
822
+ return !!value && typeof value.then === "function";
823
+ }
824
+ function normalizeHookError(policy) {
825
+ if (policy === "throw" || policy === "silent") {
826
+ return policy;
827
+ }
828
+ return "warn";
829
+ }
830
+ function reportHookError(error, policy) {
831
+ if (policy === "silent") {
832
+ return;
833
+ }
834
+ if (policy === "warn") {
835
+ console.warn("[@mokup/cli] defineConfig hook failed:", error);
836
+ }
837
+ }
838
+ function runHookSequence(stage, hooks, policy, setStage) {
839
+ if (hooks.length === 0) {
840
+ return;
841
+ }
842
+ setStage(stage);
843
+ let chain = null;
844
+ const runHook = (hook) => {
845
+ try {
846
+ const result = hook();
847
+ if (isPromise(result)) {
848
+ return result.catch((error) => {
849
+ if (policy === "throw") {
850
+ throw error;
851
+ }
852
+ reportHookError(error, policy);
853
+ });
854
+ }
855
+ return void 0;
856
+ } catch (error) {
857
+ if (policy === "throw") {
858
+ throw error;
859
+ }
860
+ reportHookError(error, policy);
861
+ return void 0;
824
862
  }
825
863
  };
864
+ for (const hook of hooks) {
865
+ if (chain) {
866
+ chain = chain.then(() => runHook(hook));
867
+ continue;
868
+ }
869
+ const result = runHook(hook);
870
+ if (isPromise(result)) {
871
+ chain = result;
872
+ }
873
+ }
874
+ if (!chain) {
875
+ setStage("normal");
876
+ return;
877
+ }
878
+ return chain.finally(() => {
879
+ setStage("normal");
880
+ });
826
881
  }
827
882
  function attachMetadata(config, meta) {
828
883
  Object.defineProperty(config, middlewareSymbol, {
@@ -831,21 +886,70 @@ function attachMetadata(config, meta) {
831
886
  });
832
887
  return config;
833
888
  }
889
+ function normalizeConfig(value) {
890
+ return value && typeof value === "object" ? value : {};
891
+ }
892
+ function onBeforeAll(handler) {
893
+ if (typeof handler !== "function") {
894
+ throw new TypeError("onBeforeAll expects a function");
895
+ }
896
+ const context = getActiveContext();
897
+ context.hooks.pre.push(handler);
898
+ }
899
+ function onAfterAll(handler) {
900
+ if (typeof handler !== "function") {
901
+ throw new TypeError("onAfterAll expects a function");
902
+ }
903
+ const context = getActiveContext();
904
+ context.hooks.post.push(handler);
905
+ }
834
906
  function defineConfig(input) {
835
907
  if (typeof input === "function") {
836
908
  const pre = [];
837
909
  const normal = [];
838
910
  const post = [];
911
+ let stage = "normal";
912
+ const app = {
913
+ use: (...handlers) => {
914
+ if (stage === "pre") {
915
+ pre.push(...handlers);
916
+ return;
917
+ }
918
+ if (stage === "post") {
919
+ post.push(...handlers);
920
+ return;
921
+ }
922
+ normal.push(...handlers);
923
+ }
924
+ };
839
925
  const context = {
840
- pre: createRegistry(pre),
841
- normal: createRegistry(normal),
842
- post: createRegistry(post)
926
+ app,
927
+ hooks: { pre: [], post: [] },
928
+ setStage: (next) => {
929
+ stage = next;
930
+ }
843
931
  };
844
- const result = input(context);
845
- const config2 = result && typeof result === "object" ? result : {};
846
- return attachMetadata(config2, { pre, normal, post });
932
+ const result = runWithContext(context, () => input({ app }));
933
+ const finalize = (value) => {
934
+ const config2 = normalizeConfig(value);
935
+ const policy = normalizeHookError(config2.hookError);
936
+ const preResult = runHookSequence("pre", context.hooks.pre, policy, context.setStage);
937
+ const runPost = () => runHookSequence("post", context.hooks.post, policy, context.setStage);
938
+ if (isPromise(preResult)) {
939
+ return preResult.then(runPost).then(() => attachMetadata(config2, { pre, normal, post }));
940
+ }
941
+ const postResult = runPost();
942
+ if (isPromise(postResult)) {
943
+ return postResult.then(() => attachMetadata(config2, { pre, normal, post }));
944
+ }
945
+ return attachMetadata(config2, { pre, normal, post });
946
+ };
947
+ if (isPromise(result)) {
948
+ return result.then(finalize);
949
+ }
950
+ return finalize(result);
847
951
  }
848
- const config = input && typeof input === "object" ? input : {};
952
+ const config = normalizeConfig(input);
849
953
  return attachMetadata(config, { pre: [], normal: [], post: [] });
850
954
  }
851
955
 
@@ -992,4 +1096,6 @@ async function runCli(argv = process__default.argv) {
992
1096
  exports.buildManifest = buildManifest;
993
1097
  exports.createCli = createCli;
994
1098
  exports.defineConfig = defineConfig;
1099
+ exports.onAfterAll = onAfterAll;
1100
+ exports.onBeforeAll = onBeforeAll;
995
1101
  exports.runCli = runCli;
package/dist/index.d.cts CHANGED
@@ -164,6 +164,12 @@ interface RouteDirectoryConfig {
164
164
  * @default undefined
165
165
  */
166
166
  middleware?: MiddlewareHandler | MiddlewareHandler[];
167
+ /**
168
+ * Error handling policy for defineConfig hooks.
169
+ *
170
+ * @default "warn"
171
+ */
172
+ hookError?: HookErrorPolicy;
167
173
  }
168
174
  /**
169
175
  * Middleware execution position.
@@ -174,6 +180,10 @@ interface RouteDirectoryConfig {
174
180
  * const position: MiddlewarePosition = 'pre'
175
181
  */
176
182
  type MiddlewarePosition = 'pre' | 'normal' | 'post';
183
+ /**
184
+ * Error handling policy for config hooks.
185
+ */
186
+ type HookErrorPolicy = 'throw' | 'warn' | 'silent';
177
187
  /**
178
188
  * Middleware registry used by defineConfig.
179
189
  *
@@ -202,39 +212,47 @@ declare function buildManifest(options?: BuildOptions): Promise<{
202
212
  manifestPath: string;
203
213
  }>;
204
214
 
215
+ type HookHandler = () => void | Promise<void>;
216
+ interface ConfigApp {
217
+ use: (...handlers: MiddlewareHandler[]) => void;
218
+ }
205
219
  type DefineConfigFactory = (context: {
206
- pre: MiddlewareRegistry;
207
- normal: MiddlewareRegistry;
208
- post: MiddlewareRegistry;
209
- }) => RouteDirectoryConfig | void;
220
+ app: ConfigApp;
221
+ }) => RouteDirectoryConfig | void | Promise<RouteDirectoryConfig | void>;
222
+ declare function onBeforeAll(handler: HookHandler): void;
223
+ declare function onAfterAll(handler: HookHandler): void;
210
224
  /**
211
- * Define a directory config with Hono-style middleware registration.
225
+ * Define a directory config with hook-based middleware registration.
212
226
  *
213
227
  * @param input - Config object or factory callback.
214
228
  * @returns Route directory config with middleware metadata.
215
229
  *
216
230
  * @example
217
- * import { defineConfig } from '@mokup/cli'
231
+ * import { defineConfig, onBeforeAll, onAfterAll } from '@mokup/cli'
218
232
  *
219
- * export default defineConfig(({ pre, normal, post }) => {
220
- * pre.use(async (c, next) => {
221
- * c.header('x-before', '1')
222
- * await next()
233
+ * export default defineConfig(({ app }) => {
234
+ * onBeforeAll(() => {
235
+ * app.use(async (c, next) => {
236
+ * c.header('x-before', '1')
237
+ * await next()
238
+ * })
223
239
  * })
224
240
  *
225
- * normal.use(async (_c, next) => {
241
+ * app.use(async (_c, next) => {
226
242
  * await next()
227
243
  * })
228
244
  *
229
- * post.use(async (c, next) => {
230
- * await next()
231
- * c.header('x-after', '1')
245
+ * onAfterAll(() => {
246
+ * app.use(async (c, next) => {
247
+ * await next()
248
+ * c.header('x-after', '1')
249
+ * })
232
250
  * })
233
251
  *
234
252
  * return { delay: 120 }
235
253
  * })
236
254
  */
237
- declare function defineConfig(input: RouteDirectoryConfig | DefineConfigFactory): RouteDirectoryConfig;
255
+ declare function defineConfig(input: RouteDirectoryConfig | DefineConfigFactory): RouteDirectoryConfig | Promise<RouteDirectoryConfig>;
238
256
 
239
257
  /**
240
258
  * Create the mokup CLI program instance.
@@ -260,5 +278,5 @@ declare function createCli(): Command;
260
278
  */
261
279
  declare function runCli(argv?: string[]): Promise<void>;
262
280
 
263
- export { buildManifest, createCli, defineConfig, runCli };
264
- export type { BuildOptions, MiddlewarePosition, MiddlewareRegistry, RouteDirectoryConfig, RouteRule };
281
+ export { buildManifest, createCli, defineConfig, onAfterAll, onBeforeAll, runCli };
282
+ export type { BuildOptions, HookErrorPolicy, MiddlewarePosition, MiddlewareRegistry, RouteDirectoryConfig, RouteRule };
package/dist/index.d.mts CHANGED
@@ -164,6 +164,12 @@ interface RouteDirectoryConfig {
164
164
  * @default undefined
165
165
  */
166
166
  middleware?: MiddlewareHandler | MiddlewareHandler[];
167
+ /**
168
+ * Error handling policy for defineConfig hooks.
169
+ *
170
+ * @default "warn"
171
+ */
172
+ hookError?: HookErrorPolicy;
167
173
  }
168
174
  /**
169
175
  * Middleware execution position.
@@ -174,6 +180,10 @@ interface RouteDirectoryConfig {
174
180
  * const position: MiddlewarePosition = 'pre'
175
181
  */
176
182
  type MiddlewarePosition = 'pre' | 'normal' | 'post';
183
+ /**
184
+ * Error handling policy for config hooks.
185
+ */
186
+ type HookErrorPolicy = 'throw' | 'warn' | 'silent';
177
187
  /**
178
188
  * Middleware registry used by defineConfig.
179
189
  *
@@ -202,39 +212,47 @@ declare function buildManifest(options?: BuildOptions): Promise<{
202
212
  manifestPath: string;
203
213
  }>;
204
214
 
215
+ type HookHandler = () => void | Promise<void>;
216
+ interface ConfigApp {
217
+ use: (...handlers: MiddlewareHandler[]) => void;
218
+ }
205
219
  type DefineConfigFactory = (context: {
206
- pre: MiddlewareRegistry;
207
- normal: MiddlewareRegistry;
208
- post: MiddlewareRegistry;
209
- }) => RouteDirectoryConfig | void;
220
+ app: ConfigApp;
221
+ }) => RouteDirectoryConfig | void | Promise<RouteDirectoryConfig | void>;
222
+ declare function onBeforeAll(handler: HookHandler): void;
223
+ declare function onAfterAll(handler: HookHandler): void;
210
224
  /**
211
- * Define a directory config with Hono-style middleware registration.
225
+ * Define a directory config with hook-based middleware registration.
212
226
  *
213
227
  * @param input - Config object or factory callback.
214
228
  * @returns Route directory config with middleware metadata.
215
229
  *
216
230
  * @example
217
- * import { defineConfig } from '@mokup/cli'
231
+ * import { defineConfig, onBeforeAll, onAfterAll } from '@mokup/cli'
218
232
  *
219
- * export default defineConfig(({ pre, normal, post }) => {
220
- * pre.use(async (c, next) => {
221
- * c.header('x-before', '1')
222
- * await next()
233
+ * export default defineConfig(({ app }) => {
234
+ * onBeforeAll(() => {
235
+ * app.use(async (c, next) => {
236
+ * c.header('x-before', '1')
237
+ * await next()
238
+ * })
223
239
  * })
224
240
  *
225
- * normal.use(async (_c, next) => {
241
+ * app.use(async (_c, next) => {
226
242
  * await next()
227
243
  * })
228
244
  *
229
- * post.use(async (c, next) => {
230
- * await next()
231
- * c.header('x-after', '1')
245
+ * onAfterAll(() => {
246
+ * app.use(async (c, next) => {
247
+ * await next()
248
+ * c.header('x-after', '1')
249
+ * })
232
250
  * })
233
251
  *
234
252
  * return { delay: 120 }
235
253
  * })
236
254
  */
237
- declare function defineConfig(input: RouteDirectoryConfig | DefineConfigFactory): RouteDirectoryConfig;
255
+ declare function defineConfig(input: RouteDirectoryConfig | DefineConfigFactory): RouteDirectoryConfig | Promise<RouteDirectoryConfig>;
238
256
 
239
257
  /**
240
258
  * Create the mokup CLI program instance.
@@ -260,5 +278,5 @@ declare function createCli(): Command;
260
278
  */
261
279
  declare function runCli(argv?: string[]): Promise<void>;
262
280
 
263
- export { buildManifest, createCli, defineConfig, runCli };
264
- export type { BuildOptions, MiddlewarePosition, MiddlewareRegistry, RouteDirectoryConfig, RouteRule };
281
+ export { buildManifest, createCli, defineConfig, onAfterAll, onBeforeAll, runCli };
282
+ export type { BuildOptions, HookErrorPolicy, MiddlewarePosition, MiddlewareRegistry, RouteDirectoryConfig, RouteRule };
package/dist/index.d.ts CHANGED
@@ -164,6 +164,12 @@ interface RouteDirectoryConfig {
164
164
  * @default undefined
165
165
  */
166
166
  middleware?: MiddlewareHandler | MiddlewareHandler[];
167
+ /**
168
+ * Error handling policy for defineConfig hooks.
169
+ *
170
+ * @default "warn"
171
+ */
172
+ hookError?: HookErrorPolicy;
167
173
  }
168
174
  /**
169
175
  * Middleware execution position.
@@ -174,6 +180,10 @@ interface RouteDirectoryConfig {
174
180
  * const position: MiddlewarePosition = 'pre'
175
181
  */
176
182
  type MiddlewarePosition = 'pre' | 'normal' | 'post';
183
+ /**
184
+ * Error handling policy for config hooks.
185
+ */
186
+ type HookErrorPolicy = 'throw' | 'warn' | 'silent';
177
187
  /**
178
188
  * Middleware registry used by defineConfig.
179
189
  *
@@ -202,39 +212,47 @@ declare function buildManifest(options?: BuildOptions): Promise<{
202
212
  manifestPath: string;
203
213
  }>;
204
214
 
215
+ type HookHandler = () => void | Promise<void>;
216
+ interface ConfigApp {
217
+ use: (...handlers: MiddlewareHandler[]) => void;
218
+ }
205
219
  type DefineConfigFactory = (context: {
206
- pre: MiddlewareRegistry;
207
- normal: MiddlewareRegistry;
208
- post: MiddlewareRegistry;
209
- }) => RouteDirectoryConfig | void;
220
+ app: ConfigApp;
221
+ }) => RouteDirectoryConfig | void | Promise<RouteDirectoryConfig | void>;
222
+ declare function onBeforeAll(handler: HookHandler): void;
223
+ declare function onAfterAll(handler: HookHandler): void;
210
224
  /**
211
- * Define a directory config with Hono-style middleware registration.
225
+ * Define a directory config with hook-based middleware registration.
212
226
  *
213
227
  * @param input - Config object or factory callback.
214
228
  * @returns Route directory config with middleware metadata.
215
229
  *
216
230
  * @example
217
- * import { defineConfig } from '@mokup/cli'
231
+ * import { defineConfig, onBeforeAll, onAfterAll } from '@mokup/cli'
218
232
  *
219
- * export default defineConfig(({ pre, normal, post }) => {
220
- * pre.use(async (c, next) => {
221
- * c.header('x-before', '1')
222
- * await next()
233
+ * export default defineConfig(({ app }) => {
234
+ * onBeforeAll(() => {
235
+ * app.use(async (c, next) => {
236
+ * c.header('x-before', '1')
237
+ * await next()
238
+ * })
223
239
  * })
224
240
  *
225
- * normal.use(async (_c, next) => {
241
+ * app.use(async (_c, next) => {
226
242
  * await next()
227
243
  * })
228
244
  *
229
- * post.use(async (c, next) => {
230
- * await next()
231
- * c.header('x-after', '1')
245
+ * onAfterAll(() => {
246
+ * app.use(async (c, next) => {
247
+ * await next()
248
+ * c.header('x-after', '1')
249
+ * })
232
250
  * })
233
251
  *
234
252
  * return { delay: 120 }
235
253
  * })
236
254
  */
237
- declare function defineConfig(input: RouteDirectoryConfig | DefineConfigFactory): RouteDirectoryConfig;
255
+ declare function defineConfig(input: RouteDirectoryConfig | DefineConfigFactory): RouteDirectoryConfig | Promise<RouteDirectoryConfig>;
238
256
 
239
257
  /**
240
258
  * Create the mokup CLI program instance.
@@ -260,5 +278,5 @@ declare function createCli(): Command;
260
278
  */
261
279
  declare function runCli(argv?: string[]): Promise<void>;
262
280
 
263
- export { buildManifest, createCli, defineConfig, runCli };
264
- export type { BuildOptions, MiddlewarePosition, MiddlewareRegistry, RouteDirectoryConfig, RouteRule };
281
+ export { buildManifest, createCli, defineConfig, onAfterAll, onBeforeAll, runCli };
282
+ export type { BuildOptions, HookErrorPolicy, MiddlewarePosition, MiddlewareRegistry, RouteDirectoryConfig, RouteRule };
package/dist/index.mjs CHANGED
@@ -1,10 +1,11 @@
1
1
  import { promises } from 'node:fs';
2
2
  import process, { cwd } from 'node:process';
3
- import { join, normalize, dirname, resolve, isAbsolute, relative, basename, extname } from '@mokup/shared/pathe';
3
+ import { join, normalize, dirname, resolve, isAbsolute, basename, extname, relative } from '@mokup/shared/pathe';
4
4
  import { Buffer } from 'node:buffer';
5
5
  import { createRequire } from 'node:module';
6
6
  import { pathToFileURL } from 'node:url';
7
7
  import { build } from '@mokup/shared/esbuild';
8
+ import { toPosix, hasIgnoredPrefix, matchesFilter } from '@mokup/shared/path-utils';
8
9
  import { parseRouteTemplate, compareRouteScore } from '@mokup/runtime';
9
10
  import { parse } from '@mokup/shared/jsonc-parser';
10
11
  import { createFetchServer, serve } from '@mokup/server/node';
@@ -123,12 +124,16 @@ async function loadConfig(file) {
123
124
  if (!mod) {
124
125
  return null;
125
126
  }
126
- const value = mod?.default ?? mod;
127
+ const raw = mod?.default ?? mod;
128
+ const value = isPromise$1(raw) ? await raw : raw;
127
129
  if (!value || typeof value !== "object") {
128
130
  return null;
129
131
  }
130
132
  return value;
131
133
  }
134
+ function isPromise$1(value) {
135
+ return !!value && typeof value.then === "function";
136
+ }
132
137
  function normalizeMiddlewares(value, source, log, position) {
133
138
  if (!value) {
134
139
  return [];
@@ -257,10 +262,6 @@ async function resolveDirectoryConfig(params) {
257
262
  };
258
263
  }
259
264
 
260
- function toPosix(value) {
261
- return value.replace(/\\/g, "/");
262
- }
263
-
264
265
  const supportedExtensions = /* @__PURE__ */ new Set([
265
266
  ".json",
266
267
  ".jsonc",
@@ -311,34 +312,10 @@ function resolveDirs(dir, root) {
311
312
  );
312
313
  return Array.from(new Set(normalized));
313
314
  }
314
- function testPatterns(patterns, value) {
315
- const list = Array.isArray(patterns) ? patterns : [patterns];
316
- return list.some((pattern) => pattern.test(value));
317
- }
318
- function matchesFilter(file, include, exclude) {
319
- const normalized = toPosix(file);
320
- if (exclude && testPatterns(exclude, normalized)) {
321
- return false;
322
- }
323
- if (include) {
324
- return testPatterns(include, normalized);
325
- }
326
- return true;
327
- }
328
315
  function normalizeIgnorePrefix(value, fallback = ["."]) {
329
316
  const list = typeof value === "undefined" ? fallback : Array.isArray(value) ? value : [value];
330
317
  return list.filter((entry) => typeof entry === "string" && entry.length > 0);
331
318
  }
332
- function hasIgnoredPrefix(file, rootDir, prefixes) {
333
- if (prefixes.length === 0) {
334
- return false;
335
- }
336
- const relativePath = toPosix(relative(rootDir, file));
337
- const segments = relativePath.split("/");
338
- return segments.some(
339
- (segment) => prefixes.some((prefix) => segment.startsWith(prefix))
340
- );
341
- }
342
319
  function isSupportedFile(file) {
343
320
  if (file.endsWith(".d.ts")) {
344
321
  return false;
@@ -810,12 +787,90 @@ async function buildManifest(options = {}) {
810
787
  }
811
788
 
812
789
  const middlewareSymbol = Symbol.for("mokup.config.middlewares");
813
- function createRegistry(list) {
814
- return {
815
- use: (...handlers) => {
816
- list.push(...handlers);
790
+ const contextStack = [];
791
+ function getActiveContext() {
792
+ const context = contextStack[contextStack.length - 1];
793
+ if (!context) {
794
+ throw new Error("onBeforeAll/onAfterAll must be called inside defineConfig()");
795
+ }
796
+ return context;
797
+ }
798
+ function runWithContext(context, fn) {
799
+ contextStack.push(context);
800
+ try {
801
+ const result = fn();
802
+ if (isPromise(result)) {
803
+ return result.finally(() => {
804
+ contextStack.pop();
805
+ });
806
+ }
807
+ contextStack.pop();
808
+ return result;
809
+ } catch (error) {
810
+ contextStack.pop();
811
+ throw error;
812
+ }
813
+ }
814
+ function isPromise(value) {
815
+ return !!value && typeof value.then === "function";
816
+ }
817
+ function normalizeHookError(policy) {
818
+ if (policy === "throw" || policy === "silent") {
819
+ return policy;
820
+ }
821
+ return "warn";
822
+ }
823
+ function reportHookError(error, policy) {
824
+ if (policy === "silent") {
825
+ return;
826
+ }
827
+ if (policy === "warn") {
828
+ console.warn("[@mokup/cli] defineConfig hook failed:", error);
829
+ }
830
+ }
831
+ function runHookSequence(stage, hooks, policy, setStage) {
832
+ if (hooks.length === 0) {
833
+ return;
834
+ }
835
+ setStage(stage);
836
+ let chain = null;
837
+ const runHook = (hook) => {
838
+ try {
839
+ const result = hook();
840
+ if (isPromise(result)) {
841
+ return result.catch((error) => {
842
+ if (policy === "throw") {
843
+ throw error;
844
+ }
845
+ reportHookError(error, policy);
846
+ });
847
+ }
848
+ return void 0;
849
+ } catch (error) {
850
+ if (policy === "throw") {
851
+ throw error;
852
+ }
853
+ reportHookError(error, policy);
854
+ return void 0;
817
855
  }
818
856
  };
857
+ for (const hook of hooks) {
858
+ if (chain) {
859
+ chain = chain.then(() => runHook(hook));
860
+ continue;
861
+ }
862
+ const result = runHook(hook);
863
+ if (isPromise(result)) {
864
+ chain = result;
865
+ }
866
+ }
867
+ if (!chain) {
868
+ setStage("normal");
869
+ return;
870
+ }
871
+ return chain.finally(() => {
872
+ setStage("normal");
873
+ });
819
874
  }
820
875
  function attachMetadata(config, meta) {
821
876
  Object.defineProperty(config, middlewareSymbol, {
@@ -824,21 +879,70 @@ function attachMetadata(config, meta) {
824
879
  });
825
880
  return config;
826
881
  }
882
+ function normalizeConfig(value) {
883
+ return value && typeof value === "object" ? value : {};
884
+ }
885
+ function onBeforeAll(handler) {
886
+ if (typeof handler !== "function") {
887
+ throw new TypeError("onBeforeAll expects a function");
888
+ }
889
+ const context = getActiveContext();
890
+ context.hooks.pre.push(handler);
891
+ }
892
+ function onAfterAll(handler) {
893
+ if (typeof handler !== "function") {
894
+ throw new TypeError("onAfterAll expects a function");
895
+ }
896
+ const context = getActiveContext();
897
+ context.hooks.post.push(handler);
898
+ }
827
899
  function defineConfig(input) {
828
900
  if (typeof input === "function") {
829
901
  const pre = [];
830
902
  const normal = [];
831
903
  const post = [];
904
+ let stage = "normal";
905
+ const app = {
906
+ use: (...handlers) => {
907
+ if (stage === "pre") {
908
+ pre.push(...handlers);
909
+ return;
910
+ }
911
+ if (stage === "post") {
912
+ post.push(...handlers);
913
+ return;
914
+ }
915
+ normal.push(...handlers);
916
+ }
917
+ };
832
918
  const context = {
833
- pre: createRegistry(pre),
834
- normal: createRegistry(normal),
835
- post: createRegistry(post)
919
+ app,
920
+ hooks: { pre: [], post: [] },
921
+ setStage: (next) => {
922
+ stage = next;
923
+ }
836
924
  };
837
- const result = input(context);
838
- const config2 = result && typeof result === "object" ? result : {};
839
- return attachMetadata(config2, { pre, normal, post });
925
+ const result = runWithContext(context, () => input({ app }));
926
+ const finalize = (value) => {
927
+ const config2 = normalizeConfig(value);
928
+ const policy = normalizeHookError(config2.hookError);
929
+ const preResult = runHookSequence("pre", context.hooks.pre, policy, context.setStage);
930
+ const runPost = () => runHookSequence("post", context.hooks.post, policy, context.setStage);
931
+ if (isPromise(preResult)) {
932
+ return preResult.then(runPost).then(() => attachMetadata(config2, { pre, normal, post }));
933
+ }
934
+ const postResult = runPost();
935
+ if (isPromise(postResult)) {
936
+ return postResult.then(() => attachMetadata(config2, { pre, normal, post }));
937
+ }
938
+ return attachMetadata(config2, { pre, normal, post });
939
+ };
940
+ if (isPromise(result)) {
941
+ return result.then(finalize);
942
+ }
943
+ return finalize(result);
840
944
  }
841
- const config = input && typeof input === "object" ? input : {};
945
+ const config = normalizeConfig(input);
842
946
  return attachMetadata(config, { pre: [], normal: [], post: [] });
843
947
  }
844
948
 
@@ -982,4 +1086,4 @@ async function runCli(argv = process.argv) {
982
1086
  await program.parseAsync(argv);
983
1087
  }
984
1088
 
985
- export { buildManifest, createCli, defineConfig, runCli };
1089
+ export { buildManifest, createCli, defineConfig, onAfterAll, onBeforeAll, runCli };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mokup/cli",
3
3
  "type": "module",
4
- "version": "1.0.9",
4
+ "version": "1.1.0",
5
5
  "description": "CLI for building mokup manifests and handlers.",
6
6
  "license": "MIT",
7
7
  "homepage": "https://mokup.icebreaker.top",
@@ -28,9 +28,9 @@
28
28
  ],
29
29
  "dependencies": {
30
30
  "commander": "^14.0.0",
31
- "@mokup/runtime": "1.0.5",
32
- "@mokup/server": "1.1.6",
33
- "@mokup/shared": "1.1.0"
31
+ "@mokup/runtime": "1.0.6",
32
+ "@mokup/server": "1.2.0",
33
+ "@mokup/shared": "1.1.1"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@types/node": "^25.0.10",