@mokup/cli 1.0.4 → 1.0.6

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
@@ -9,8 +9,8 @@ const node_url = require('node:url');
9
9
  const esbuild = require('@mokup/shared/esbuild');
10
10
  const runtime = require('@mokup/runtime');
11
11
  const jsoncParser = require('@mokup/shared/jsonc-parser');
12
- const server = require('@mokup/server');
13
12
  const node = require('@mokup/server/node');
13
+ const logger$1 = require('@mokup/shared/logger');
14
14
  const commander = require('commander');
15
15
 
16
16
  var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
@@ -75,6 +75,7 @@ async function writeManifestModule(outDir, manifest) {
75
75
  }
76
76
 
77
77
  const configExtensions = [".ts", ".js", ".mjs", ".cjs"];
78
+ const middlewareSymbol$1 = Symbol.for("mokup.config.middlewares");
78
79
  async function loadModule$1(file) {
79
80
  const ext = configExtensions.find((extension) => file.endsWith(extension));
80
81
  if (ext === ".cjs") {
@@ -135,7 +136,7 @@ async function loadConfig(file) {
135
136
  }
136
137
  return value;
137
138
  }
138
- function normalizeMiddlewares(value, source, log) {
139
+ function normalizeMiddlewares(value, source, log, position) {
139
140
  if (!value) {
140
141
  return [];
141
142
  }
@@ -146,10 +147,22 @@ function normalizeMiddlewares(value, source, log) {
146
147
  log?.(`Invalid middleware in ${source}`);
147
148
  return;
148
149
  }
149
- middlewares.push({ file: source, index });
150
+ middlewares.push({ file: source, index, position });
150
151
  });
151
152
  return middlewares;
152
153
  }
154
+ function readMiddlewareMeta(config) {
155
+ const value = config[middlewareSymbol$1];
156
+ if (!value || typeof value !== "object") {
157
+ return null;
158
+ }
159
+ const meta = value;
160
+ return {
161
+ pre: Array.isArray(meta.pre) ? meta.pre : [],
162
+ normal: Array.isArray(meta.normal) ? meta.normal : [],
163
+ post: Array.isArray(meta.post) ? meta.post : []
164
+ };
165
+ }
153
166
  async function resolveDirectoryConfig(params) {
154
167
  const { file, rootDir, log, configCache, fileCache } = params;
155
168
  const resolvedRoot = pathe.normalize(rootDir);
@@ -168,7 +181,10 @@ async function resolveDirectoryConfig(params) {
168
181
  current = parent;
169
182
  }
170
183
  chain.reverse();
171
- const merged = { middlewares: [] };
184
+ const merged = {};
185
+ const preMiddlewares = [];
186
+ const normalMiddlewares = [];
187
+ const postMiddlewares = [];
172
188
  for (const dir of chain) {
173
189
  const configPath = await findConfigFile(dir, fileCache);
174
190
  if (!configPath) {
@@ -204,12 +220,48 @@ async function resolveDirectoryConfig(params) {
204
220
  if (typeof config.exclude !== "undefined") {
205
221
  merged.exclude = config.exclude;
206
222
  }
207
- const normalized = normalizeMiddlewares(config.middleware, configPath, log);
208
- if (normalized.length > 0) {
209
- merged.middlewares.push(...normalized);
223
+ const meta = readMiddlewareMeta(config);
224
+ const normalizedPre = normalizeMiddlewares(
225
+ meta?.pre,
226
+ configPath,
227
+ log,
228
+ "pre"
229
+ );
230
+ const normalizedNormal = normalizeMiddlewares(
231
+ meta?.normal,
232
+ configPath,
233
+ log,
234
+ "normal"
235
+ );
236
+ const normalizedLegacy = normalizeMiddlewares(
237
+ config.middleware,
238
+ configPath,
239
+ log,
240
+ "normal"
241
+ );
242
+ const normalizedPost = normalizeMiddlewares(
243
+ meta?.post,
244
+ configPath,
245
+ log,
246
+ "post"
247
+ );
248
+ if (normalizedPre.length > 0) {
249
+ preMiddlewares.push(...normalizedPre);
250
+ }
251
+ if (normalizedNormal.length > 0) {
252
+ normalMiddlewares.push(...normalizedNormal);
253
+ }
254
+ if (normalizedLegacy.length > 0) {
255
+ normalMiddlewares.push(...normalizedLegacy);
256
+ }
257
+ if (normalizedPost.length > 0) {
258
+ postMiddlewares.push(...normalizedPost);
210
259
  }
211
260
  }
212
- return merged;
261
+ return {
262
+ ...merged,
263
+ middlewares: [...preMiddlewares, ...normalMiddlewares, ...postMiddlewares]
264
+ };
213
265
  }
214
266
 
215
267
  function toPosix(value) {
@@ -764,6 +816,40 @@ async function buildManifest(options = {}) {
764
816
  };
765
817
  }
766
818
 
819
+ const middlewareSymbol = Symbol.for("mokup.config.middlewares");
820
+ function createRegistry(list) {
821
+ return {
822
+ use: (...handlers) => {
823
+ list.push(...handlers);
824
+ }
825
+ };
826
+ }
827
+ function attachMetadata(config, meta) {
828
+ Object.defineProperty(config, middlewareSymbol, {
829
+ value: meta,
830
+ enumerable: false
831
+ });
832
+ return config;
833
+ }
834
+ function defineConfig(input) {
835
+ if (typeof input === "function") {
836
+ const pre = [];
837
+ const normal = [];
838
+ const post = [];
839
+ const context = {
840
+ pre: createRegistry(pre),
841
+ normal: createRegistry(normal),
842
+ post: createRegistry(post)
843
+ };
844
+ const result = input(context);
845
+ const config2 = result && typeof result === "object" ? result : {};
846
+ return attachMetadata(config2, { pre, normal, post });
847
+ }
848
+ const config = input && typeof input === "object" ? input : {};
849
+ return attachMetadata(config, { pre: [], normal: [], post: [] });
850
+ }
851
+
852
+ const logger = logger$1.createLogger();
767
853
  function collectValues(value, previous) {
768
854
  return [...previous ?? [], value];
769
855
  }
@@ -776,7 +862,7 @@ function toBuildOptions(options) {
776
862
  const buildOptions = {
777
863
  handlers: options.handlers !== false,
778
864
  log: (message) => {
779
- console.log(message);
865
+ logger.info(message);
780
866
  }
781
867
  };
782
868
  if (options.dir && options.dir.length > 0) {
@@ -829,10 +915,10 @@ function toServeOptions(options) {
829
915
  }
830
916
  serveOptions.port = parsed;
831
917
  }
832
- if (typeof options.playground !== "undefined") {
833
- serveOptions.playground = options.playground;
834
- }
835
- return serveOptions;
918
+ return {
919
+ entry: serveOptions,
920
+ playground: options.playground
921
+ };
836
922
  }
837
923
  function createCli() {
838
924
  const program = new commander.Command();
@@ -843,31 +929,35 @@ function createCli() {
843
929
  });
844
930
  program.command("serve").description("Start a Node.js mock server").option("-d, --dir <dir>", "Mock directory (repeatable)", collectValues).option("--prefix <prefix>", "URL prefix").option("--include <pattern>", "Include regex (repeatable)", collectRegex).option("--exclude <pattern>", "Exclude regex (repeatable)", collectRegex).option("--ignore-prefix <prefix>", "Ignore path segment prefix (repeatable)", collectValues).option("--host <host>", "Hostname (default: localhost)").option("--port <port>", "Port (default: 8080)").option("--no-watch", "Disable file watching").option("--no-playground", "Disable Playground").option("--no-log", "Disable logging").action(async (options) => {
845
931
  const serveOptions = toServeOptions(options);
846
- const host = serveOptions.host ?? "localhost";
847
- const port = serveOptions.port ?? 8080;
848
- const playgroundEnabled = serveOptions.playground !== false;
849
- const playgroundPath = "/_mokup";
850
- const server$1 = await server.createFetchServer(serveOptions);
932
+ const { entry, playground } = serveOptions;
933
+ const host = entry.host ?? "localhost";
934
+ const port = entry.port ?? 8080;
935
+ const playgroundEnabled = playground !== false;
936
+ const playgroundPath = "/__mokup";
937
+ const server = await node.createFetchServer({
938
+ entries: entry,
939
+ playground
940
+ });
851
941
  const nodeServer = node.serve(
852
942
  {
853
- fetch: server$1.fetch,
943
+ fetch: server.fetch,
854
944
  hostname: host,
855
945
  port
856
946
  },
857
947
  (info) => {
858
948
  const resolvedHost = typeof info === "string" ? host : info?.address ?? host;
859
949
  const resolvedPort = typeof info === "string" ? port : info?.port ?? port;
860
- console.log(`Mock server ready at http://${resolvedHost}:${resolvedPort}`);
950
+ logger.info(`Mock server ready at http://${resolvedHost}:${resolvedPort}`);
861
951
  if (playgroundEnabled) {
862
- console.log(`Playground at http://${resolvedHost}:${resolvedPort}${playgroundPath}`);
952
+ logger.info(`Playground at http://${resolvedHost}:${resolvedPort}${playgroundPath}`);
863
953
  }
864
954
  }
865
955
  );
866
- server$1.injectWebSocket?.(nodeServer);
956
+ server.injectWebSocket?.(nodeServer);
867
957
  const shutdown = async () => {
868
958
  try {
869
- if (server$1.close) {
870
- await server$1.close();
959
+ if (server.close) {
960
+ await server.close();
871
961
  }
872
962
  await new Promise((resolve, reject) => {
873
963
  nodeServer.close((error) => {
@@ -901,4 +991,5 @@ async function runCli(argv = process__default.argv) {
901
991
 
902
992
  exports.buildManifest = buildManifest;
903
993
  exports.createCli = createCli;
994
+ exports.defineConfig = defineConfig;
904
995
  exports.runCli = runCli;
package/dist/index.d.cts CHANGED
@@ -1,25 +1,264 @@
1
1
  import { Manifest } from '@mokup/runtime';
2
+ import { MiddlewareHandler } from '@mokup/shared/hono';
2
3
  import { Command } from 'commander';
3
4
 
5
+ /**
6
+ * Options for building a mokup manifest.
7
+ *
8
+ * @example
9
+ * import type { BuildOptions } from '@mokup/cli'
10
+ *
11
+ * const options: BuildOptions = {
12
+ * dir: 'mock',
13
+ * outDir: '.mokup',
14
+ * }
15
+ */
4
16
  interface BuildOptions {
17
+ /**
18
+ * Directory or directories to scan for mock files.
19
+ *
20
+ * @default "mock"
21
+ */
5
22
  dir?: string | string[];
23
+ /**
24
+ * Output directory for manifest artifacts.
25
+ *
26
+ * @default ".mokup"
27
+ */
6
28
  outDir?: string;
29
+ /**
30
+ * URL prefix to apply to generated routes.
31
+ *
32
+ * @default ""
33
+ */
7
34
  prefix?: string;
35
+ /**
36
+ * Include filter for files.
37
+ *
38
+ * @default undefined
39
+ */
8
40
  include?: RegExp | RegExp[];
41
+ /**
42
+ * Exclude filter for files.
43
+ *
44
+ * @default undefined
45
+ */
9
46
  exclude?: RegExp | RegExp[];
47
+ /**
48
+ * Ignore file or folder prefixes.
49
+ *
50
+ * @default ["."]
51
+ */
10
52
  ignorePrefix?: string | string[];
53
+ /**
54
+ * Emit handler bundles for module-based responses.
55
+ *
56
+ * @default true
57
+ */
11
58
  handlers?: boolean;
59
+ /**
60
+ * Project root used to resolve paths.
61
+ *
62
+ * @default process.cwd()
63
+ */
12
64
  root?: string;
65
+ /**
66
+ * Optional logger for build messages.
67
+ *
68
+ * @default undefined
69
+ */
13
70
  log?: (message: string) => void;
14
71
  }
72
+ /**
73
+ * Route rule shape used in build-time resolution.
74
+ *
75
+ * @example
76
+ * import type { RouteRule } from '@mokup/cli'
77
+ *
78
+ * const rule: RouteRule = { handler: { ok: true } }
79
+ */
80
+ interface RouteRule {
81
+ /** Route handler or static value. */
82
+ handler: unknown;
83
+ /**
84
+ * Enable or disable this rule.
85
+ *
86
+ * @default true
87
+ */
88
+ enabled?: boolean;
89
+ /**
90
+ * Override response status code.
91
+ *
92
+ * @default 200
93
+ */
94
+ status?: number;
95
+ /**
96
+ * Additional response headers.
97
+ *
98
+ * @default {}
99
+ */
100
+ headers?: Record<string, string>;
101
+ /**
102
+ * Delay in milliseconds before responding.
103
+ *
104
+ * @default 0
105
+ */
106
+ delay?: number;
107
+ }
108
+ /**
109
+ * Directory-level config used during manifest build.
110
+ *
111
+ * @example
112
+ * import type { RouteDirectoryConfig } from '@mokup/cli'
113
+ *
114
+ * const config: RouteDirectoryConfig = {
115
+ * headers: { 'x-mokup': 'dir' },
116
+ * }
117
+ */
118
+ interface RouteDirectoryConfig {
119
+ /**
120
+ * Headers applied to routes in this directory.
121
+ *
122
+ * @default {}
123
+ */
124
+ headers?: Record<string, string>;
125
+ /**
126
+ * Default status code override.
127
+ *
128
+ * @default 200
129
+ */
130
+ status?: number;
131
+ /**
132
+ * Default delay in milliseconds.
133
+ *
134
+ * @default 0
135
+ */
136
+ delay?: number;
137
+ /**
138
+ * Enable or disable this directory.
139
+ *
140
+ * @default true
141
+ */
142
+ enabled?: boolean;
143
+ /**
144
+ * Ignore prefixes applied to files in this directory.
145
+ *
146
+ * @default ["."]
147
+ */
148
+ ignorePrefix?: string | string[];
149
+ /**
150
+ * Include filter for files.
151
+ *
152
+ * @default undefined
153
+ */
154
+ include?: RegExp | RegExp[];
155
+ /**
156
+ * Exclude filter for files.
157
+ *
158
+ * @default undefined
159
+ */
160
+ exclude?: RegExp | RegExp[];
161
+ /**
162
+ * Middleware for the directory.
163
+ *
164
+ * @default undefined
165
+ */
166
+ middleware?: MiddlewareHandler | MiddlewareHandler[];
167
+ }
168
+ /**
169
+ * Middleware execution position.
170
+ *
171
+ * @example
172
+ * import type { MiddlewarePosition } from '@mokup/cli'
173
+ *
174
+ * const position: MiddlewarePosition = 'pre'
175
+ */
176
+ type MiddlewarePosition = 'pre' | 'normal' | 'post';
177
+ /**
178
+ * Middleware registry used by defineConfig.
179
+ *
180
+ * @example
181
+ * import type { MiddlewareRegistry } from '@mokup/cli'
182
+ *
183
+ * const registry: MiddlewareRegistry = { use: () => {} }
184
+ */
185
+ interface MiddlewareRegistry {
186
+ use: (...handlers: MiddlewareHandler[]) => void;
187
+ }
15
188
 
189
+ /**
190
+ * Build and write a mokup manifest to disk.
191
+ *
192
+ * @param options - Build options.
193
+ * @returns Build output metadata.
194
+ *
195
+ * @example
196
+ * import { buildManifest } from '@mokup/cli'
197
+ *
198
+ * const result = await buildManifest({ dir: 'mock', outDir: '.mokup' })
199
+ */
16
200
  declare function buildManifest(options?: BuildOptions): Promise<{
17
201
  manifest: Manifest;
18
202
  manifestPath: string;
19
203
  }>;
20
204
 
205
+ type DefineConfigFactory = (context: {
206
+ pre: MiddlewareRegistry;
207
+ normal: MiddlewareRegistry;
208
+ post: MiddlewareRegistry;
209
+ }) => RouteDirectoryConfig | void;
210
+ /**
211
+ * Define a directory config with Hono-style middleware registration.
212
+ *
213
+ * @param input - Config object or factory callback.
214
+ * @returns Route directory config with middleware metadata.
215
+ *
216
+ * @example
217
+ * import { defineConfig } from '@mokup/cli'
218
+ *
219
+ * export default defineConfig(({ pre, normal, post }) => {
220
+ * pre.use(async (c, next) => {
221
+ * c.header('x-before', '1')
222
+ * await next()
223
+ * })
224
+ *
225
+ * normal.use(async (_c, next) => {
226
+ * await next()
227
+ * })
228
+ *
229
+ * post.use(async (c, next) => {
230
+ * await next()
231
+ * c.header('x-after', '1')
232
+ * })
233
+ *
234
+ * return { delay: 120 }
235
+ * })
236
+ */
237
+ declare function defineConfig(input: RouteDirectoryConfig | DefineConfigFactory): RouteDirectoryConfig;
238
+
239
+ /**
240
+ * Create the mokup CLI program instance.
241
+ *
242
+ * @returns A configured CLI program.
243
+ *
244
+ * @example
245
+ * import { createCli } from '@mokup/cli'
246
+ *
247
+ * const cli = createCli()
248
+ */
21
249
  declare function createCli(): Command;
250
+ /**
251
+ * Run the mokup CLI with the provided argv.
252
+ *
253
+ * @param argv - CLI arguments.
254
+ * @returns Exit code or void.
255
+ *
256
+ * @example
257
+ * import { runCli } from '@mokup/cli'
258
+ *
259
+ * await runCli(['node', 'mokup', 'build', '--dir', 'mock'])
260
+ */
22
261
  declare function runCli(argv?: string[]): Promise<void>;
23
262
 
24
- export { buildManifest, createCli, runCli };
25
- export type { BuildOptions };
263
+ export { buildManifest, createCli, defineConfig, runCli };
264
+ export type { BuildOptions, MiddlewarePosition, MiddlewareRegistry, RouteDirectoryConfig, RouteRule };
package/dist/index.d.mts CHANGED
@@ -1,25 +1,264 @@
1
1
  import { Manifest } from '@mokup/runtime';
2
+ import { MiddlewareHandler } from '@mokup/shared/hono';
2
3
  import { Command } from 'commander';
3
4
 
5
+ /**
6
+ * Options for building a mokup manifest.
7
+ *
8
+ * @example
9
+ * import type { BuildOptions } from '@mokup/cli'
10
+ *
11
+ * const options: BuildOptions = {
12
+ * dir: 'mock',
13
+ * outDir: '.mokup',
14
+ * }
15
+ */
4
16
  interface BuildOptions {
17
+ /**
18
+ * Directory or directories to scan for mock files.
19
+ *
20
+ * @default "mock"
21
+ */
5
22
  dir?: string | string[];
23
+ /**
24
+ * Output directory for manifest artifacts.
25
+ *
26
+ * @default ".mokup"
27
+ */
6
28
  outDir?: string;
29
+ /**
30
+ * URL prefix to apply to generated routes.
31
+ *
32
+ * @default ""
33
+ */
7
34
  prefix?: string;
35
+ /**
36
+ * Include filter for files.
37
+ *
38
+ * @default undefined
39
+ */
8
40
  include?: RegExp | RegExp[];
41
+ /**
42
+ * Exclude filter for files.
43
+ *
44
+ * @default undefined
45
+ */
9
46
  exclude?: RegExp | RegExp[];
47
+ /**
48
+ * Ignore file or folder prefixes.
49
+ *
50
+ * @default ["."]
51
+ */
10
52
  ignorePrefix?: string | string[];
53
+ /**
54
+ * Emit handler bundles for module-based responses.
55
+ *
56
+ * @default true
57
+ */
11
58
  handlers?: boolean;
59
+ /**
60
+ * Project root used to resolve paths.
61
+ *
62
+ * @default process.cwd()
63
+ */
12
64
  root?: string;
65
+ /**
66
+ * Optional logger for build messages.
67
+ *
68
+ * @default undefined
69
+ */
13
70
  log?: (message: string) => void;
14
71
  }
72
+ /**
73
+ * Route rule shape used in build-time resolution.
74
+ *
75
+ * @example
76
+ * import type { RouteRule } from '@mokup/cli'
77
+ *
78
+ * const rule: RouteRule = { handler: { ok: true } }
79
+ */
80
+ interface RouteRule {
81
+ /** Route handler or static value. */
82
+ handler: unknown;
83
+ /**
84
+ * Enable or disable this rule.
85
+ *
86
+ * @default true
87
+ */
88
+ enabled?: boolean;
89
+ /**
90
+ * Override response status code.
91
+ *
92
+ * @default 200
93
+ */
94
+ status?: number;
95
+ /**
96
+ * Additional response headers.
97
+ *
98
+ * @default {}
99
+ */
100
+ headers?: Record<string, string>;
101
+ /**
102
+ * Delay in milliseconds before responding.
103
+ *
104
+ * @default 0
105
+ */
106
+ delay?: number;
107
+ }
108
+ /**
109
+ * Directory-level config used during manifest build.
110
+ *
111
+ * @example
112
+ * import type { RouteDirectoryConfig } from '@mokup/cli'
113
+ *
114
+ * const config: RouteDirectoryConfig = {
115
+ * headers: { 'x-mokup': 'dir' },
116
+ * }
117
+ */
118
+ interface RouteDirectoryConfig {
119
+ /**
120
+ * Headers applied to routes in this directory.
121
+ *
122
+ * @default {}
123
+ */
124
+ headers?: Record<string, string>;
125
+ /**
126
+ * Default status code override.
127
+ *
128
+ * @default 200
129
+ */
130
+ status?: number;
131
+ /**
132
+ * Default delay in milliseconds.
133
+ *
134
+ * @default 0
135
+ */
136
+ delay?: number;
137
+ /**
138
+ * Enable or disable this directory.
139
+ *
140
+ * @default true
141
+ */
142
+ enabled?: boolean;
143
+ /**
144
+ * Ignore prefixes applied to files in this directory.
145
+ *
146
+ * @default ["."]
147
+ */
148
+ ignorePrefix?: string | string[];
149
+ /**
150
+ * Include filter for files.
151
+ *
152
+ * @default undefined
153
+ */
154
+ include?: RegExp | RegExp[];
155
+ /**
156
+ * Exclude filter for files.
157
+ *
158
+ * @default undefined
159
+ */
160
+ exclude?: RegExp | RegExp[];
161
+ /**
162
+ * Middleware for the directory.
163
+ *
164
+ * @default undefined
165
+ */
166
+ middleware?: MiddlewareHandler | MiddlewareHandler[];
167
+ }
168
+ /**
169
+ * Middleware execution position.
170
+ *
171
+ * @example
172
+ * import type { MiddlewarePosition } from '@mokup/cli'
173
+ *
174
+ * const position: MiddlewarePosition = 'pre'
175
+ */
176
+ type MiddlewarePosition = 'pre' | 'normal' | 'post';
177
+ /**
178
+ * Middleware registry used by defineConfig.
179
+ *
180
+ * @example
181
+ * import type { MiddlewareRegistry } from '@mokup/cli'
182
+ *
183
+ * const registry: MiddlewareRegistry = { use: () => {} }
184
+ */
185
+ interface MiddlewareRegistry {
186
+ use: (...handlers: MiddlewareHandler[]) => void;
187
+ }
15
188
 
189
+ /**
190
+ * Build and write a mokup manifest to disk.
191
+ *
192
+ * @param options - Build options.
193
+ * @returns Build output metadata.
194
+ *
195
+ * @example
196
+ * import { buildManifest } from '@mokup/cli'
197
+ *
198
+ * const result = await buildManifest({ dir: 'mock', outDir: '.mokup' })
199
+ */
16
200
  declare function buildManifest(options?: BuildOptions): Promise<{
17
201
  manifest: Manifest;
18
202
  manifestPath: string;
19
203
  }>;
20
204
 
205
+ type DefineConfigFactory = (context: {
206
+ pre: MiddlewareRegistry;
207
+ normal: MiddlewareRegistry;
208
+ post: MiddlewareRegistry;
209
+ }) => RouteDirectoryConfig | void;
210
+ /**
211
+ * Define a directory config with Hono-style middleware registration.
212
+ *
213
+ * @param input - Config object or factory callback.
214
+ * @returns Route directory config with middleware metadata.
215
+ *
216
+ * @example
217
+ * import { defineConfig } from '@mokup/cli'
218
+ *
219
+ * export default defineConfig(({ pre, normal, post }) => {
220
+ * pre.use(async (c, next) => {
221
+ * c.header('x-before', '1')
222
+ * await next()
223
+ * })
224
+ *
225
+ * normal.use(async (_c, next) => {
226
+ * await next()
227
+ * })
228
+ *
229
+ * post.use(async (c, next) => {
230
+ * await next()
231
+ * c.header('x-after', '1')
232
+ * })
233
+ *
234
+ * return { delay: 120 }
235
+ * })
236
+ */
237
+ declare function defineConfig(input: RouteDirectoryConfig | DefineConfigFactory): RouteDirectoryConfig;
238
+
239
+ /**
240
+ * Create the mokup CLI program instance.
241
+ *
242
+ * @returns A configured CLI program.
243
+ *
244
+ * @example
245
+ * import { createCli } from '@mokup/cli'
246
+ *
247
+ * const cli = createCli()
248
+ */
21
249
  declare function createCli(): Command;
250
+ /**
251
+ * Run the mokup CLI with the provided argv.
252
+ *
253
+ * @param argv - CLI arguments.
254
+ * @returns Exit code or void.
255
+ *
256
+ * @example
257
+ * import { runCli } from '@mokup/cli'
258
+ *
259
+ * await runCli(['node', 'mokup', 'build', '--dir', 'mock'])
260
+ */
22
261
  declare function runCli(argv?: string[]): Promise<void>;
23
262
 
24
- export { buildManifest, createCli, runCli };
25
- export type { BuildOptions };
263
+ export { buildManifest, createCli, defineConfig, runCli };
264
+ export type { BuildOptions, MiddlewarePosition, MiddlewareRegistry, RouteDirectoryConfig, RouteRule };
package/dist/index.d.ts CHANGED
@@ -1,25 +1,264 @@
1
1
  import { Manifest } from '@mokup/runtime';
2
+ import { MiddlewareHandler } from '@mokup/shared/hono';
2
3
  import { Command } from 'commander';
3
4
 
5
+ /**
6
+ * Options for building a mokup manifest.
7
+ *
8
+ * @example
9
+ * import type { BuildOptions } from '@mokup/cli'
10
+ *
11
+ * const options: BuildOptions = {
12
+ * dir: 'mock',
13
+ * outDir: '.mokup',
14
+ * }
15
+ */
4
16
  interface BuildOptions {
17
+ /**
18
+ * Directory or directories to scan for mock files.
19
+ *
20
+ * @default "mock"
21
+ */
5
22
  dir?: string | string[];
23
+ /**
24
+ * Output directory for manifest artifacts.
25
+ *
26
+ * @default ".mokup"
27
+ */
6
28
  outDir?: string;
29
+ /**
30
+ * URL prefix to apply to generated routes.
31
+ *
32
+ * @default ""
33
+ */
7
34
  prefix?: string;
35
+ /**
36
+ * Include filter for files.
37
+ *
38
+ * @default undefined
39
+ */
8
40
  include?: RegExp | RegExp[];
41
+ /**
42
+ * Exclude filter for files.
43
+ *
44
+ * @default undefined
45
+ */
9
46
  exclude?: RegExp | RegExp[];
47
+ /**
48
+ * Ignore file or folder prefixes.
49
+ *
50
+ * @default ["."]
51
+ */
10
52
  ignorePrefix?: string | string[];
53
+ /**
54
+ * Emit handler bundles for module-based responses.
55
+ *
56
+ * @default true
57
+ */
11
58
  handlers?: boolean;
59
+ /**
60
+ * Project root used to resolve paths.
61
+ *
62
+ * @default process.cwd()
63
+ */
12
64
  root?: string;
65
+ /**
66
+ * Optional logger for build messages.
67
+ *
68
+ * @default undefined
69
+ */
13
70
  log?: (message: string) => void;
14
71
  }
72
+ /**
73
+ * Route rule shape used in build-time resolution.
74
+ *
75
+ * @example
76
+ * import type { RouteRule } from '@mokup/cli'
77
+ *
78
+ * const rule: RouteRule = { handler: { ok: true } }
79
+ */
80
+ interface RouteRule {
81
+ /** Route handler or static value. */
82
+ handler: unknown;
83
+ /**
84
+ * Enable or disable this rule.
85
+ *
86
+ * @default true
87
+ */
88
+ enabled?: boolean;
89
+ /**
90
+ * Override response status code.
91
+ *
92
+ * @default 200
93
+ */
94
+ status?: number;
95
+ /**
96
+ * Additional response headers.
97
+ *
98
+ * @default {}
99
+ */
100
+ headers?: Record<string, string>;
101
+ /**
102
+ * Delay in milliseconds before responding.
103
+ *
104
+ * @default 0
105
+ */
106
+ delay?: number;
107
+ }
108
+ /**
109
+ * Directory-level config used during manifest build.
110
+ *
111
+ * @example
112
+ * import type { RouteDirectoryConfig } from '@mokup/cli'
113
+ *
114
+ * const config: RouteDirectoryConfig = {
115
+ * headers: { 'x-mokup': 'dir' },
116
+ * }
117
+ */
118
+ interface RouteDirectoryConfig {
119
+ /**
120
+ * Headers applied to routes in this directory.
121
+ *
122
+ * @default {}
123
+ */
124
+ headers?: Record<string, string>;
125
+ /**
126
+ * Default status code override.
127
+ *
128
+ * @default 200
129
+ */
130
+ status?: number;
131
+ /**
132
+ * Default delay in milliseconds.
133
+ *
134
+ * @default 0
135
+ */
136
+ delay?: number;
137
+ /**
138
+ * Enable or disable this directory.
139
+ *
140
+ * @default true
141
+ */
142
+ enabled?: boolean;
143
+ /**
144
+ * Ignore prefixes applied to files in this directory.
145
+ *
146
+ * @default ["."]
147
+ */
148
+ ignorePrefix?: string | string[];
149
+ /**
150
+ * Include filter for files.
151
+ *
152
+ * @default undefined
153
+ */
154
+ include?: RegExp | RegExp[];
155
+ /**
156
+ * Exclude filter for files.
157
+ *
158
+ * @default undefined
159
+ */
160
+ exclude?: RegExp | RegExp[];
161
+ /**
162
+ * Middleware for the directory.
163
+ *
164
+ * @default undefined
165
+ */
166
+ middleware?: MiddlewareHandler | MiddlewareHandler[];
167
+ }
168
+ /**
169
+ * Middleware execution position.
170
+ *
171
+ * @example
172
+ * import type { MiddlewarePosition } from '@mokup/cli'
173
+ *
174
+ * const position: MiddlewarePosition = 'pre'
175
+ */
176
+ type MiddlewarePosition = 'pre' | 'normal' | 'post';
177
+ /**
178
+ * Middleware registry used by defineConfig.
179
+ *
180
+ * @example
181
+ * import type { MiddlewareRegistry } from '@mokup/cli'
182
+ *
183
+ * const registry: MiddlewareRegistry = { use: () => {} }
184
+ */
185
+ interface MiddlewareRegistry {
186
+ use: (...handlers: MiddlewareHandler[]) => void;
187
+ }
15
188
 
189
+ /**
190
+ * Build and write a mokup manifest to disk.
191
+ *
192
+ * @param options - Build options.
193
+ * @returns Build output metadata.
194
+ *
195
+ * @example
196
+ * import { buildManifest } from '@mokup/cli'
197
+ *
198
+ * const result = await buildManifest({ dir: 'mock', outDir: '.mokup' })
199
+ */
16
200
  declare function buildManifest(options?: BuildOptions): Promise<{
17
201
  manifest: Manifest;
18
202
  manifestPath: string;
19
203
  }>;
20
204
 
205
+ type DefineConfigFactory = (context: {
206
+ pre: MiddlewareRegistry;
207
+ normal: MiddlewareRegistry;
208
+ post: MiddlewareRegistry;
209
+ }) => RouteDirectoryConfig | void;
210
+ /**
211
+ * Define a directory config with Hono-style middleware registration.
212
+ *
213
+ * @param input - Config object or factory callback.
214
+ * @returns Route directory config with middleware metadata.
215
+ *
216
+ * @example
217
+ * import { defineConfig } from '@mokup/cli'
218
+ *
219
+ * export default defineConfig(({ pre, normal, post }) => {
220
+ * pre.use(async (c, next) => {
221
+ * c.header('x-before', '1')
222
+ * await next()
223
+ * })
224
+ *
225
+ * normal.use(async (_c, next) => {
226
+ * await next()
227
+ * })
228
+ *
229
+ * post.use(async (c, next) => {
230
+ * await next()
231
+ * c.header('x-after', '1')
232
+ * })
233
+ *
234
+ * return { delay: 120 }
235
+ * })
236
+ */
237
+ declare function defineConfig(input: RouteDirectoryConfig | DefineConfigFactory): RouteDirectoryConfig;
238
+
239
+ /**
240
+ * Create the mokup CLI program instance.
241
+ *
242
+ * @returns A configured CLI program.
243
+ *
244
+ * @example
245
+ * import { createCli } from '@mokup/cli'
246
+ *
247
+ * const cli = createCli()
248
+ */
21
249
  declare function createCli(): Command;
250
+ /**
251
+ * Run the mokup CLI with the provided argv.
252
+ *
253
+ * @param argv - CLI arguments.
254
+ * @returns Exit code or void.
255
+ *
256
+ * @example
257
+ * import { runCli } from '@mokup/cli'
258
+ *
259
+ * await runCli(['node', 'mokup', 'build', '--dir', 'mock'])
260
+ */
22
261
  declare function runCli(argv?: string[]): Promise<void>;
23
262
 
24
- export { buildManifest, createCli, runCli };
25
- export type { BuildOptions };
263
+ export { buildManifest, createCli, defineConfig, runCli };
264
+ export type { BuildOptions, MiddlewarePosition, MiddlewareRegistry, RouteDirectoryConfig, RouteRule };
package/dist/index.mjs CHANGED
@@ -7,8 +7,8 @@ import { pathToFileURL } from 'node:url';
7
7
  import { build } from '@mokup/shared/esbuild';
8
8
  import { parseRouteTemplate, compareRouteScore } from '@mokup/runtime';
9
9
  import { parse } from '@mokup/shared/jsonc-parser';
10
- import { createFetchServer } from '@mokup/server';
11
- import { serve } from '@mokup/server/node';
10
+ import { createFetchServer, serve } from '@mokup/server/node';
11
+ import { createLogger } from '@mokup/shared/logger';
12
12
  import { Command } from 'commander';
13
13
 
14
14
  async function writeBundle(outDir, hasHandlers) {
@@ -68,6 +68,7 @@ async function writeManifestModule(outDir, manifest) {
68
68
  }
69
69
 
70
70
  const configExtensions = [".ts", ".js", ".mjs", ".cjs"];
71
+ const middlewareSymbol$1 = Symbol.for("mokup.config.middlewares");
71
72
  async function loadModule$1(file) {
72
73
  const ext = configExtensions.find((extension) => file.endsWith(extension));
73
74
  if (ext === ".cjs") {
@@ -128,7 +129,7 @@ async function loadConfig(file) {
128
129
  }
129
130
  return value;
130
131
  }
131
- function normalizeMiddlewares(value, source, log) {
132
+ function normalizeMiddlewares(value, source, log, position) {
132
133
  if (!value) {
133
134
  return [];
134
135
  }
@@ -139,10 +140,22 @@ function normalizeMiddlewares(value, source, log) {
139
140
  log?.(`Invalid middleware in ${source}`);
140
141
  return;
141
142
  }
142
- middlewares.push({ file: source, index });
143
+ middlewares.push({ file: source, index, position });
143
144
  });
144
145
  return middlewares;
145
146
  }
147
+ function readMiddlewareMeta(config) {
148
+ const value = config[middlewareSymbol$1];
149
+ if (!value || typeof value !== "object") {
150
+ return null;
151
+ }
152
+ const meta = value;
153
+ return {
154
+ pre: Array.isArray(meta.pre) ? meta.pre : [],
155
+ normal: Array.isArray(meta.normal) ? meta.normal : [],
156
+ post: Array.isArray(meta.post) ? meta.post : []
157
+ };
158
+ }
146
159
  async function resolveDirectoryConfig(params) {
147
160
  const { file, rootDir, log, configCache, fileCache } = params;
148
161
  const resolvedRoot = normalize(rootDir);
@@ -161,7 +174,10 @@ async function resolveDirectoryConfig(params) {
161
174
  current = parent;
162
175
  }
163
176
  chain.reverse();
164
- const merged = { middlewares: [] };
177
+ const merged = {};
178
+ const preMiddlewares = [];
179
+ const normalMiddlewares = [];
180
+ const postMiddlewares = [];
165
181
  for (const dir of chain) {
166
182
  const configPath = await findConfigFile(dir, fileCache);
167
183
  if (!configPath) {
@@ -197,12 +213,48 @@ async function resolveDirectoryConfig(params) {
197
213
  if (typeof config.exclude !== "undefined") {
198
214
  merged.exclude = config.exclude;
199
215
  }
200
- const normalized = normalizeMiddlewares(config.middleware, configPath, log);
201
- if (normalized.length > 0) {
202
- merged.middlewares.push(...normalized);
216
+ const meta = readMiddlewareMeta(config);
217
+ const normalizedPre = normalizeMiddlewares(
218
+ meta?.pre,
219
+ configPath,
220
+ log,
221
+ "pre"
222
+ );
223
+ const normalizedNormal = normalizeMiddlewares(
224
+ meta?.normal,
225
+ configPath,
226
+ log,
227
+ "normal"
228
+ );
229
+ const normalizedLegacy = normalizeMiddlewares(
230
+ config.middleware,
231
+ configPath,
232
+ log,
233
+ "normal"
234
+ );
235
+ const normalizedPost = normalizeMiddlewares(
236
+ meta?.post,
237
+ configPath,
238
+ log,
239
+ "post"
240
+ );
241
+ if (normalizedPre.length > 0) {
242
+ preMiddlewares.push(...normalizedPre);
243
+ }
244
+ if (normalizedNormal.length > 0) {
245
+ normalMiddlewares.push(...normalizedNormal);
246
+ }
247
+ if (normalizedLegacy.length > 0) {
248
+ normalMiddlewares.push(...normalizedLegacy);
249
+ }
250
+ if (normalizedPost.length > 0) {
251
+ postMiddlewares.push(...normalizedPost);
203
252
  }
204
253
  }
205
- return merged;
254
+ return {
255
+ ...merged,
256
+ middlewares: [...preMiddlewares, ...normalMiddlewares, ...postMiddlewares]
257
+ };
206
258
  }
207
259
 
208
260
  function toPosix(value) {
@@ -757,6 +809,40 @@ async function buildManifest(options = {}) {
757
809
  };
758
810
  }
759
811
 
812
+ const middlewareSymbol = Symbol.for("mokup.config.middlewares");
813
+ function createRegistry(list) {
814
+ return {
815
+ use: (...handlers) => {
816
+ list.push(...handlers);
817
+ }
818
+ };
819
+ }
820
+ function attachMetadata(config, meta) {
821
+ Object.defineProperty(config, middlewareSymbol, {
822
+ value: meta,
823
+ enumerable: false
824
+ });
825
+ return config;
826
+ }
827
+ function defineConfig(input) {
828
+ if (typeof input === "function") {
829
+ const pre = [];
830
+ const normal = [];
831
+ const post = [];
832
+ const context = {
833
+ pre: createRegistry(pre),
834
+ normal: createRegistry(normal),
835
+ post: createRegistry(post)
836
+ };
837
+ const result = input(context);
838
+ const config2 = result && typeof result === "object" ? result : {};
839
+ return attachMetadata(config2, { pre, normal, post });
840
+ }
841
+ const config = input && typeof input === "object" ? input : {};
842
+ return attachMetadata(config, { pre: [], normal: [], post: [] });
843
+ }
844
+
845
+ const logger = createLogger();
760
846
  function collectValues(value, previous) {
761
847
  return [...previous ?? [], value];
762
848
  }
@@ -769,7 +855,7 @@ function toBuildOptions(options) {
769
855
  const buildOptions = {
770
856
  handlers: options.handlers !== false,
771
857
  log: (message) => {
772
- console.log(message);
858
+ logger.info(message);
773
859
  }
774
860
  };
775
861
  if (options.dir && options.dir.length > 0) {
@@ -822,10 +908,10 @@ function toServeOptions(options) {
822
908
  }
823
909
  serveOptions.port = parsed;
824
910
  }
825
- if (typeof options.playground !== "undefined") {
826
- serveOptions.playground = options.playground;
827
- }
828
- return serveOptions;
911
+ return {
912
+ entry: serveOptions,
913
+ playground: options.playground
914
+ };
829
915
  }
830
916
  function createCli() {
831
917
  const program = new Command();
@@ -836,11 +922,15 @@ function createCli() {
836
922
  });
837
923
  program.command("serve").description("Start a Node.js mock server").option("-d, --dir <dir>", "Mock directory (repeatable)", collectValues).option("--prefix <prefix>", "URL prefix").option("--include <pattern>", "Include regex (repeatable)", collectRegex).option("--exclude <pattern>", "Exclude regex (repeatable)", collectRegex).option("--ignore-prefix <prefix>", "Ignore path segment prefix (repeatable)", collectValues).option("--host <host>", "Hostname (default: localhost)").option("--port <port>", "Port (default: 8080)").option("--no-watch", "Disable file watching").option("--no-playground", "Disable Playground").option("--no-log", "Disable logging").action(async (options) => {
838
924
  const serveOptions = toServeOptions(options);
839
- const host = serveOptions.host ?? "localhost";
840
- const port = serveOptions.port ?? 8080;
841
- const playgroundEnabled = serveOptions.playground !== false;
842
- const playgroundPath = "/_mokup";
843
- const server = await createFetchServer(serveOptions);
925
+ const { entry, playground } = serveOptions;
926
+ const host = entry.host ?? "localhost";
927
+ const port = entry.port ?? 8080;
928
+ const playgroundEnabled = playground !== false;
929
+ const playgroundPath = "/__mokup";
930
+ const server = await createFetchServer({
931
+ entries: entry,
932
+ playground
933
+ });
844
934
  const nodeServer = serve(
845
935
  {
846
936
  fetch: server.fetch,
@@ -850,9 +940,9 @@ function createCli() {
850
940
  (info) => {
851
941
  const resolvedHost = typeof info === "string" ? host : info?.address ?? host;
852
942
  const resolvedPort = typeof info === "string" ? port : info?.port ?? port;
853
- console.log(`Mock server ready at http://${resolvedHost}:${resolvedPort}`);
943
+ logger.info(`Mock server ready at http://${resolvedHost}:${resolvedPort}`);
854
944
  if (playgroundEnabled) {
855
- console.log(`Playground at http://${resolvedHost}:${resolvedPort}${playgroundPath}`);
945
+ logger.info(`Playground at http://${resolvedHost}:${resolvedPort}${playgroundPath}`);
856
946
  }
857
947
  }
858
948
  );
@@ -892,4 +982,4 @@ async function runCli(argv = process.argv) {
892
982
  await program.parseAsync(argv);
893
983
  }
894
984
 
895
- export { buildManifest, createCli, runCli };
985
+ export { buildManifest, createCli, defineConfig, runCli };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mokup/cli",
3
3
  "type": "module",
4
- "version": "1.0.4",
4
+ "version": "1.0.6",
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.1",
32
- "@mokup/server": "1.1.1",
33
- "@mokup/shared": "1.0.0"
31
+ "@mokup/runtime": "1.0.3",
32
+ "@mokup/server": "1.1.3",
33
+ "@mokup/shared": "1.0.2"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@types/node": "^25.0.10",