@mokup/cli 0.1.0 → 0.3.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.mjs CHANGED
@@ -1,10 +1,855 @@
1
- export { b as buildManifest } from './shared/cli.DqkcmI0-.mjs';
2
- import 'node:fs';
3
- import 'node:process';
4
- import 'pathe';
5
- import 'node:buffer';
6
- import 'node:module';
7
- import 'node:url';
8
- import 'esbuild';
9
- import '@mokup/runtime';
10
- import 'jsonc-parser';
1
+ import { promises } from 'node:fs';
2
+ import process, { cwd } from 'node:process';
3
+ import { join, normalize, dirname, resolve, isAbsolute, basename, extname, relative } from '@mokup/shared/pathe';
4
+ import { Buffer } from 'node:buffer';
5
+ import { createRequire } from 'node:module';
6
+ import { pathToFileURL } from 'node:url';
7
+ import { build } from '@mokup/shared/esbuild';
8
+ import { parseRouteTemplate, compareRouteScore } from '@mokup/runtime';
9
+ import { parse } from '@mokup/shared/jsonc-parser';
10
+ import { serve } from '@hono/node-server';
11
+ import { createFetchServer } from '@mokup/server';
12
+ import { Command } from 'commander';
13
+
14
+ async function writeBundle(outDir, hasHandlers) {
15
+ const lines = [
16
+ "import manifest from './mokup.manifest.mjs'"
17
+ ];
18
+ if (hasHandlers) {
19
+ lines.push(
20
+ "import { mokupModuleMap } from './mokup-handlers/index.mjs'",
21
+ "",
22
+ "const mokupBundle = {",
23
+ " manifest,",
24
+ " moduleMap: mokupModuleMap,",
25
+ " moduleBase: './',",
26
+ "}"
27
+ );
28
+ } else {
29
+ lines.push(
30
+ "",
31
+ "const mokupBundle = {",
32
+ " manifest,",
33
+ "}"
34
+ );
35
+ }
36
+ lines.push("", "export default mokupBundle", "export { mokupBundle }", "");
37
+ const dts = [
38
+ "import type { Manifest, ModuleMap } from '@mokup/runtime'",
39
+ "export interface MokupBundle {",
40
+ " manifest: Manifest",
41
+ " moduleMap?: ModuleMap",
42
+ " moduleBase?: string | URL",
43
+ "}",
44
+ "declare const mokupBundle: MokupBundle",
45
+ "export default mokupBundle",
46
+ "export { mokupBundle }",
47
+ ""
48
+ ];
49
+ await promises.writeFile(join(outDir, "mokup.bundle.mjs"), lines.join("\n"), "utf8");
50
+ await promises.writeFile(join(outDir, "mokup.bundle.d.ts"), dts.join("\n"), "utf8");
51
+ await promises.writeFile(join(outDir, "mokup.bundle.d.mts"), dts.join("\n"), "utf8");
52
+ }
53
+ async function writeManifestModule(outDir, manifest) {
54
+ const lines = [
55
+ `const manifest = ${JSON.stringify(manifest, null, 2)}`,
56
+ "",
57
+ "export default manifest",
58
+ ""
59
+ ];
60
+ const dts = [
61
+ "import type { Manifest } from '@mokup/runtime'",
62
+ "declare const manifest: Manifest",
63
+ "export default manifest",
64
+ ""
65
+ ];
66
+ await promises.writeFile(join(outDir, "mokup.manifest.mjs"), lines.join("\n"), "utf8");
67
+ await promises.writeFile(join(outDir, "mokup.manifest.d.mts"), dts.join("\n"), "utf8");
68
+ }
69
+
70
+ const configExtensions = [".ts", ".js", ".mjs", ".cjs"];
71
+ async function loadModule$1(file) {
72
+ const ext = configExtensions.find((extension) => file.endsWith(extension));
73
+ if (ext === ".cjs") {
74
+ const require = createRequire(import.meta.url);
75
+ delete require.cache[file];
76
+ return require(file);
77
+ }
78
+ if (ext === ".js" || ext === ".mjs") {
79
+ return import(`${pathToFileURL(file).href}?t=${Date.now()}`);
80
+ }
81
+ if (ext === ".ts") {
82
+ const result = await build({
83
+ entryPoints: [file],
84
+ bundle: true,
85
+ format: "esm",
86
+ platform: "node",
87
+ sourcemap: "inline",
88
+ target: "es2020",
89
+ write: false
90
+ });
91
+ const output = result.outputFiles[0];
92
+ const code = output?.text ?? "";
93
+ const dataUrl = `data:text/javascript;base64,${Buffer.from(code).toString(
94
+ "base64"
95
+ )}`;
96
+ return import(`${dataUrl}#${Date.now()}`);
97
+ }
98
+ return null;
99
+ }
100
+ function getConfigFileCandidates(dir) {
101
+ return configExtensions.map((extension) => join(dir, `index.config${extension}`));
102
+ }
103
+ async function findConfigFile(dir, cache) {
104
+ const cached = cache.get(dir);
105
+ if (cached !== void 0) {
106
+ return cached;
107
+ }
108
+ for (const candidate of getConfigFileCandidates(dir)) {
109
+ try {
110
+ await promises.stat(candidate);
111
+ cache.set(dir, candidate);
112
+ return candidate;
113
+ } catch {
114
+ continue;
115
+ }
116
+ }
117
+ cache.set(dir, null);
118
+ return null;
119
+ }
120
+ async function loadConfig(file) {
121
+ const mod = await loadModule$1(file);
122
+ if (!mod) {
123
+ return null;
124
+ }
125
+ const value = mod?.default ?? mod;
126
+ if (!value || typeof value !== "object") {
127
+ return null;
128
+ }
129
+ return value;
130
+ }
131
+ function normalizeMiddlewares(value, source, log) {
132
+ if (!value) {
133
+ return [];
134
+ }
135
+ const list = Array.isArray(value) ? value : [value];
136
+ const middlewares = [];
137
+ list.forEach((entry, index) => {
138
+ if (typeof entry !== "function") {
139
+ log?.(`Invalid middleware in ${source}`);
140
+ return;
141
+ }
142
+ middlewares.push({ file: source, index });
143
+ });
144
+ return middlewares;
145
+ }
146
+ async function resolveDirectoryConfig(params) {
147
+ const { file, rootDir, log, configCache, fileCache } = params;
148
+ const resolvedRoot = normalize(rootDir);
149
+ const resolvedFileDir = normalize(dirname(file));
150
+ const chain = [];
151
+ let current = resolvedFileDir;
152
+ while (true) {
153
+ chain.push(current);
154
+ if (current === resolvedRoot) {
155
+ break;
156
+ }
157
+ const parent = dirname(current);
158
+ if (parent === current) {
159
+ break;
160
+ }
161
+ current = parent;
162
+ }
163
+ chain.reverse();
164
+ const merged = { middlewares: [] };
165
+ for (const dir of chain) {
166
+ const configPath = await findConfigFile(dir, fileCache);
167
+ if (!configPath) {
168
+ continue;
169
+ }
170
+ let config = configCache.get(configPath);
171
+ if (config === void 0) {
172
+ config = await loadConfig(configPath);
173
+ configCache.set(configPath, config);
174
+ }
175
+ if (!config) {
176
+ log?.(`Invalid config in ${configPath}`);
177
+ continue;
178
+ }
179
+ if (config.headers) {
180
+ merged.headers = { ...merged.headers ?? {}, ...config.headers };
181
+ }
182
+ if (typeof config.status === "number") {
183
+ merged.status = config.status;
184
+ }
185
+ if (typeof config.delay === "number") {
186
+ merged.delay = config.delay;
187
+ }
188
+ if (typeof config.enabled === "boolean") {
189
+ merged.enabled = config.enabled;
190
+ }
191
+ const normalized = normalizeMiddlewares(config.middleware, configPath, log);
192
+ if (normalized.length > 0) {
193
+ merged.middlewares.push(...normalized);
194
+ }
195
+ }
196
+ return merged;
197
+ }
198
+
199
+ function toPosix(value) {
200
+ return value.replace(/\\/g, "/");
201
+ }
202
+
203
+ const supportedExtensions = /* @__PURE__ */ new Set([
204
+ ".json",
205
+ ".jsonc",
206
+ ".ts",
207
+ ".js",
208
+ ".mjs",
209
+ ".cjs"
210
+ ]);
211
+ async function exists(path) {
212
+ try {
213
+ await promises.stat(path);
214
+ return true;
215
+ } catch {
216
+ return false;
217
+ }
218
+ }
219
+ async function walkDir(dir, rootDir, files) {
220
+ const entries = await promises.readdir(dir, { withFileTypes: true });
221
+ for (const entry of entries) {
222
+ if (entry.name === "node_modules" || entry.name === ".git") {
223
+ continue;
224
+ }
225
+ const fullPath = join(dir, entry.name);
226
+ if (entry.isDirectory()) {
227
+ await walkDir(fullPath, rootDir, files);
228
+ continue;
229
+ }
230
+ if (entry.isFile()) {
231
+ files.push({ file: fullPath, rootDir });
232
+ }
233
+ }
234
+ }
235
+ async function collectFiles(dirs) {
236
+ const files = [];
237
+ for (const dir of dirs) {
238
+ if (!await exists(dir)) {
239
+ continue;
240
+ }
241
+ await walkDir(dir, dir, files);
242
+ }
243
+ return files;
244
+ }
245
+ function resolveDirs(dir, root) {
246
+ const raw = dir;
247
+ const resolved = Array.isArray(raw) ? raw : raw ? [raw] : ["mock"];
248
+ const normalized = resolved.map(
249
+ (entry) => isAbsolute(entry) ? entry : resolve(root, entry)
250
+ );
251
+ return Array.from(new Set(normalized));
252
+ }
253
+ function testPatterns(patterns, value) {
254
+ const list = Array.isArray(patterns) ? patterns : [patterns];
255
+ return list.some((pattern) => pattern.test(value));
256
+ }
257
+ function matchesFilter(file, include, exclude) {
258
+ const normalized = toPosix(file);
259
+ if (exclude && testPatterns(exclude, normalized)) {
260
+ return false;
261
+ }
262
+ if (include) {
263
+ return testPatterns(include, normalized);
264
+ }
265
+ return true;
266
+ }
267
+ function isSupportedFile(file) {
268
+ if (file.endsWith(".d.ts")) {
269
+ return false;
270
+ }
271
+ if (basename(file).startsWith("index.config.")) {
272
+ return false;
273
+ }
274
+ const ext = extname(file).toLowerCase();
275
+ return supportedExtensions.has(ext);
276
+ }
277
+
278
+ function getHandlerModulePath(file, handlersDir, root) {
279
+ const relFromRoot = relative(root, file);
280
+ const ext = extname(relFromRoot);
281
+ const relNoExt = `${relFromRoot.slice(0, relFromRoot.length - ext.length)}.mjs`;
282
+ const outputPath = join(handlersDir, relNoExt);
283
+ const relFromOutDir = relative(dirname(handlersDir), outputPath);
284
+ const normalized = toPosix(relFromOutDir);
285
+ return normalized.startsWith(".") ? normalized : `./${normalized}`;
286
+ }
287
+ async function writeHandlerIndex(handlerModuleMap, handlersDir, outDir) {
288
+ const modulePaths = Array.from(new Set(handlerModuleMap.values()));
289
+ if (modulePaths.length === 0) {
290
+ return;
291
+ }
292
+ const imports = [];
293
+ const entries = [];
294
+ modulePaths.forEach((modulePath, index) => {
295
+ const absolutePath = resolve(outDir, modulePath);
296
+ const relImport = toPosix(relative(handlersDir, absolutePath));
297
+ const importPath = relImport.startsWith(".") ? relImport : `./${relImport}`;
298
+ const name = `module${index}`;
299
+ imports.push(`import * as ${name} from '${importPath}'`);
300
+ entries.push({ key: modulePath, name });
301
+ });
302
+ const lines = [
303
+ ...imports,
304
+ "",
305
+ "export const mokupModuleMap = {",
306
+ ...entries.map((entry) => ` '${entry.key}': ${entry.name},`),
307
+ "}",
308
+ ""
309
+ ];
310
+ await promises.writeFile(join(handlersDir, "index.mjs"), lines.join("\n"), "utf8");
311
+ const dts = [
312
+ "export type MokupModuleMap = Record<string, Record<string, unknown>>",
313
+ "export declare const mokupModuleMap: MokupModuleMap",
314
+ ""
315
+ ];
316
+ await promises.writeFile(join(handlersDir, "index.d.ts"), dts.join("\n"), "utf8");
317
+ await promises.writeFile(join(handlersDir, "index.d.mts"), dts.join("\n"), "utf8");
318
+ }
319
+ function buildResponse(handler, options) {
320
+ if (typeof handler === "function") {
321
+ if (!options.handlers) {
322
+ return null;
323
+ }
324
+ const moduleRel = getHandlerModulePath(
325
+ options.file,
326
+ options.handlersDir,
327
+ options.root
328
+ );
329
+ options.handlerSources.add(options.file);
330
+ options.handlerModuleMap.set(options.file, moduleRel);
331
+ return {
332
+ type: "module",
333
+ module: moduleRel,
334
+ ruleIndex: options.ruleIndex
335
+ };
336
+ }
337
+ if (typeof handler === "string") {
338
+ return {
339
+ type: "text",
340
+ body: handler
341
+ };
342
+ }
343
+ if (handler instanceof Uint8Array || handler instanceof ArrayBuffer) {
344
+ return {
345
+ type: "binary",
346
+ body: Buffer.from(handler).toString("base64"),
347
+ encoding: "base64"
348
+ };
349
+ }
350
+ if (Buffer.isBuffer(handler)) {
351
+ return {
352
+ type: "binary",
353
+ body: handler.toString("base64"),
354
+ encoding: "base64"
355
+ };
356
+ }
357
+ return {
358
+ type: "json",
359
+ body: handler
360
+ };
361
+ }
362
+ async function bundleHandlers(files, root, handlersDir) {
363
+ await build({
364
+ entryPoints: files,
365
+ bundle: true,
366
+ format: "esm",
367
+ platform: "neutral",
368
+ target: "es2020",
369
+ outdir: handlersDir,
370
+ outbase: root,
371
+ entryNames: "[dir]/[name]",
372
+ outExtension: { ".js": ".mjs" },
373
+ logLevel: "silent"
374
+ });
375
+ }
376
+
377
+ const methodSet = /* @__PURE__ */ new Set([
378
+ "GET",
379
+ "POST",
380
+ "PUT",
381
+ "PATCH",
382
+ "DELETE",
383
+ "OPTIONS",
384
+ "HEAD"
385
+ ]);
386
+ const methodSuffixSet = new Set(
387
+ Array.from(methodSet, (method) => method.toLowerCase())
388
+ );
389
+ const jsonExtensions = /* @__PURE__ */ new Set([".json", ".jsonc"]);
390
+ function normalizePrefix(prefix) {
391
+ if (!prefix) {
392
+ return "";
393
+ }
394
+ const normalized = prefix.startsWith("/") ? prefix : `/${prefix}`;
395
+ return normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
396
+ }
397
+ function resolveTemplate(template, prefix) {
398
+ const normalized = template.startsWith("/") ? template : `/${template}`;
399
+ if (!prefix) {
400
+ return normalized;
401
+ }
402
+ const normalizedPrefix = normalizePrefix(prefix);
403
+ if (!normalizedPrefix) {
404
+ return normalized;
405
+ }
406
+ if (normalized === normalizedPrefix || normalized.startsWith(`${normalizedPrefix}/`)) {
407
+ return normalized;
408
+ }
409
+ if (normalized === "/") {
410
+ return `${normalizedPrefix}/`;
411
+ }
412
+ return `${normalizedPrefix}${normalized}`;
413
+ }
414
+ function stripMethodSuffix(base) {
415
+ const segments = base.split(".");
416
+ const last = segments.at(-1);
417
+ if (last && methodSuffixSet.has(last.toLowerCase())) {
418
+ segments.pop();
419
+ return {
420
+ name: segments.join("."),
421
+ method: last.toUpperCase()
422
+ };
423
+ }
424
+ return {
425
+ name: base,
426
+ method: void 0
427
+ };
428
+ }
429
+ function deriveRouteFromFile(file, rootDir, log) {
430
+ const rel = toPosix(relative(rootDir, file));
431
+ const ext = extname(rel);
432
+ const withoutExt = rel.slice(0, rel.length - ext.length);
433
+ const dir = dirname(withoutExt);
434
+ const base = basename(withoutExt);
435
+ const { name, method } = stripMethodSuffix(base);
436
+ const resolvedMethod = method ?? (jsonExtensions.has(ext) ? "GET" : void 0);
437
+ if (!resolvedMethod) {
438
+ log?.(`Skip mock without method suffix: ${file}`);
439
+ return null;
440
+ }
441
+ if (!name) {
442
+ log?.(`Skip mock with empty route name: ${file}`);
443
+ return null;
444
+ }
445
+ const joined = dir === "." ? name : join(dir, name);
446
+ const segments = toPosix(joined).split("/");
447
+ if (segments.at(-1) === "index") {
448
+ segments.pop();
449
+ }
450
+ const template = segments.length === 0 ? "/" : `/${segments.join("/")}`;
451
+ const parsed = parseRouteTemplate(template);
452
+ if (parsed.errors.length > 0) {
453
+ for (const error of parsed.errors) {
454
+ log?.(`${error} in ${file}`);
455
+ }
456
+ return null;
457
+ }
458
+ for (const warning of parsed.warnings) {
459
+ log?.(`${warning} in ${file}`);
460
+ }
461
+ return {
462
+ template: parsed.template,
463
+ method: resolvedMethod,
464
+ tokens: parsed.tokens,
465
+ score: parsed.score
466
+ };
467
+ }
468
+ function resolveRule(params) {
469
+ const method = params.derivedMethod;
470
+ if (!method) {
471
+ return null;
472
+ }
473
+ const template = resolveTemplate(params.derivedTemplate, params.prefix);
474
+ const parsed = parseRouteTemplate(template);
475
+ if (parsed.errors.length > 0) {
476
+ for (const error of parsed.errors) {
477
+ params.log?.(`${error} in ${params.file}`);
478
+ }
479
+ return null;
480
+ }
481
+ for (const warning of parsed.warnings) {
482
+ params.log?.(`${warning} in ${params.file}`);
483
+ }
484
+ return {
485
+ method,
486
+ template: parsed.template,
487
+ tokens: parsed.tokens,
488
+ score: parsed.score
489
+ };
490
+ }
491
+ function sortRoutes(routes) {
492
+ return routes.sort((a, b) => {
493
+ if (a.method !== b.method) {
494
+ return a.method.localeCompare(b.method);
495
+ }
496
+ return compareRouteScore(a.score ?? [], b.score ?? []);
497
+ });
498
+ }
499
+
500
+ async function readJsonFile(file) {
501
+ try {
502
+ const content = await promises.readFile(file, "utf8");
503
+ const errors = [];
504
+ const data = parse(content, errors, {
505
+ allowTrailingComma: true,
506
+ disallowComments: false
507
+ });
508
+ if (errors.length > 0) {
509
+ return void 0;
510
+ }
511
+ return data;
512
+ } catch {
513
+ return void 0;
514
+ }
515
+ }
516
+ async function loadModule(file) {
517
+ const ext = extname(file).toLowerCase();
518
+ if (ext === ".cjs") {
519
+ const require = createRequire(import.meta.url);
520
+ delete require.cache[file];
521
+ return require(file);
522
+ }
523
+ if (ext === ".js" || ext === ".mjs") {
524
+ return import(`${pathToFileURL(file).href}?t=${Date.now()}`);
525
+ }
526
+ if (ext === ".ts") {
527
+ const result = await build({
528
+ entryPoints: [file],
529
+ bundle: true,
530
+ format: "esm",
531
+ platform: "node",
532
+ sourcemap: "inline",
533
+ target: "es2020",
534
+ write: false
535
+ });
536
+ const output = result.outputFiles[0];
537
+ const code = output?.text ?? "";
538
+ const dataUrl = `data:text/javascript;base64,${Buffer.from(code).toString(
539
+ "base64"
540
+ )}`;
541
+ return import(`${dataUrl}#${Date.now()}`);
542
+ }
543
+ return null;
544
+ }
545
+ async function loadRules(file) {
546
+ const ext = extname(file).toLowerCase();
547
+ if (ext === ".json" || ext === ".jsonc") {
548
+ const json = await readJsonFile(file);
549
+ if (typeof json === "undefined") {
550
+ return [];
551
+ }
552
+ return [
553
+ {
554
+ handler: json
555
+ }
556
+ ];
557
+ }
558
+ const mod = await loadModule(file);
559
+ const value = mod?.default ?? mod;
560
+ if (!value) {
561
+ return [];
562
+ }
563
+ if (Array.isArray(value)) {
564
+ return value;
565
+ }
566
+ if (typeof value === "function") {
567
+ return [
568
+ {
569
+ handler: value
570
+ }
571
+ ];
572
+ }
573
+ return [value];
574
+ }
575
+
576
+ async function buildManifest(options = {}) {
577
+ const root = options.root ?? cwd();
578
+ const outDir = resolve(root, options.outDir ?? ".mokup");
579
+ const handlersDir = join(outDir, "mokup-handlers");
580
+ const dirs = resolveDirs(options.dir, root);
581
+ const files = await collectFiles(dirs);
582
+ const routes = [];
583
+ const seen = /* @__PURE__ */ new Set();
584
+ const handlerSources = /* @__PURE__ */ new Set();
585
+ const handlerModuleMap = /* @__PURE__ */ new Map();
586
+ const configCache = /* @__PURE__ */ new Map();
587
+ const configFileCache = /* @__PURE__ */ new Map();
588
+ for (const fileInfo of files) {
589
+ if (!isSupportedFile(fileInfo.file)) {
590
+ continue;
591
+ }
592
+ if (!matchesFilter(fileInfo.file, options.include, options.exclude)) {
593
+ continue;
594
+ }
595
+ const configParams = {
596
+ file: fileInfo.file,
597
+ rootDir: fileInfo.rootDir,
598
+ configCache,
599
+ fileCache: configFileCache
600
+ };
601
+ if (options.log) {
602
+ configParams.log = options.log;
603
+ }
604
+ const config = await resolveDirectoryConfig(configParams);
605
+ if (config.enabled === false) {
606
+ continue;
607
+ }
608
+ const derived = deriveRouteFromFile(fileInfo.file, fileInfo.rootDir, options.log);
609
+ if (!derived) {
610
+ continue;
611
+ }
612
+ const rules = await loadRules(fileInfo.file);
613
+ for (const [index, rule] of rules.entries()) {
614
+ if (!rule || typeof rule !== "object") {
615
+ continue;
616
+ }
617
+ const ruleValue = rule;
618
+ const unsupportedKeys = ["response", "url", "method"].filter(
619
+ (key2) => key2 in ruleValue
620
+ );
621
+ if (unsupportedKeys.length > 0) {
622
+ options.log?.(
623
+ `Skip mock with unsupported fields (${unsupportedKeys.join(", ")}): ${fileInfo.file}`
624
+ );
625
+ continue;
626
+ }
627
+ if (typeof rule.handler === "undefined") {
628
+ continue;
629
+ }
630
+ const resolveParams = {
631
+ rule,
632
+ derivedTemplate: derived.template,
633
+ derivedMethod: derived.method,
634
+ prefix: options.prefix ?? "",
635
+ file: fileInfo.file
636
+ };
637
+ if (options.log) {
638
+ resolveParams.log = options.log;
639
+ }
640
+ const resolved = resolveRule(resolveParams);
641
+ if (!resolved) {
642
+ continue;
643
+ }
644
+ const key = `${resolved.method} ${resolved.template}`;
645
+ if (seen.has(key)) {
646
+ options.log?.(`Duplicate mock route ${key} from ${fileInfo.file}`);
647
+ }
648
+ seen.add(key);
649
+ const response = buildResponse(
650
+ rule.handler,
651
+ {
652
+ file: fileInfo.file,
653
+ handlers: options.handlers !== false,
654
+ handlerSources,
655
+ handlerModuleMap,
656
+ handlersDir,
657
+ root,
658
+ ruleIndex: index
659
+ }
660
+ );
661
+ if (!response) {
662
+ continue;
663
+ }
664
+ const source = toPosix(relative(root, fileInfo.file));
665
+ const middlewareRefs = options.handlers === false ? [] : config.middlewares.map((entry) => {
666
+ handlerSources.add(entry.file);
667
+ const modulePath = getHandlerModulePath(entry.file, handlersDir, root);
668
+ handlerModuleMap.set(entry.file, modulePath);
669
+ return {
670
+ module: modulePath,
671
+ ruleIndex: entry.index
672
+ };
673
+ });
674
+ const route = {
675
+ method: resolved.method,
676
+ url: resolved.template,
677
+ tokens: resolved.tokens,
678
+ score: resolved.score,
679
+ source,
680
+ response
681
+ };
682
+ if (typeof rule.status === "number") {
683
+ route.status = rule.status;
684
+ }
685
+ if (config.headers) {
686
+ route.headers = { ...config.headers };
687
+ }
688
+ if (rule.headers) {
689
+ route.headers = { ...route.headers ?? {}, ...rule.headers };
690
+ }
691
+ if (typeof rule.delay === "number") {
692
+ route.delay = rule.delay;
693
+ }
694
+ if (typeof route.status === "undefined" && typeof config.status === "number") {
695
+ route.status = config.status;
696
+ }
697
+ if (typeof route.delay === "undefined" && typeof config.delay === "number") {
698
+ route.delay = config.delay;
699
+ }
700
+ if (middlewareRefs.length > 0) {
701
+ route.middleware = middlewareRefs;
702
+ }
703
+ routes.push(route);
704
+ }
705
+ }
706
+ const manifest = {
707
+ version: 1,
708
+ routes: sortRoutes(routes)
709
+ };
710
+ await promises.mkdir(outDir, { recursive: true });
711
+ if (handlerSources.size > 0) {
712
+ await promises.mkdir(handlersDir, { recursive: true });
713
+ await bundleHandlers(Array.from(handlerSources), root, handlersDir);
714
+ await writeHandlerIndex(handlerModuleMap, handlersDir, outDir);
715
+ }
716
+ const manifestPath = join(outDir, "mokup.manifest.json");
717
+ await promises.writeFile(manifestPath, JSON.stringify(manifest, null, 2), "utf8");
718
+ await writeManifestModule(outDir, manifest);
719
+ await writeBundle(outDir, handlerSources.size > 0);
720
+ options.log?.(`Manifest written to ${manifestPath}`);
721
+ return {
722
+ manifest,
723
+ manifestPath
724
+ };
725
+ }
726
+
727
+ function collectValues(value, previous) {
728
+ return [...previous ?? [], value];
729
+ }
730
+ function collectRegex(value, previous) {
731
+ const next = previous ?? [];
732
+ next.push(new RegExp(value));
733
+ return next;
734
+ }
735
+ function toBuildOptions(options) {
736
+ const buildOptions = {
737
+ handlers: options.handlers !== false,
738
+ log: (message) => {
739
+ console.log(message);
740
+ }
741
+ };
742
+ if (options.dir && options.dir.length > 0) {
743
+ buildOptions.dir = options.dir;
744
+ }
745
+ if (options.out) {
746
+ buildOptions.outDir = options.out;
747
+ }
748
+ if (options.prefix) {
749
+ buildOptions.prefix = options.prefix;
750
+ }
751
+ if (options.include && options.include.length > 0) {
752
+ buildOptions.include = options.include;
753
+ }
754
+ if (options.exclude && options.exclude.length > 0) {
755
+ buildOptions.exclude = options.exclude;
756
+ }
757
+ return buildOptions;
758
+ }
759
+ function toServeOptions(options) {
760
+ const serveOptions = {
761
+ watch: options.watch !== false,
762
+ log: options.log !== false
763
+ };
764
+ if (options.dir && options.dir.length > 0) {
765
+ serveOptions.dir = options.dir;
766
+ }
767
+ if (options.prefix) {
768
+ serveOptions.prefix = options.prefix;
769
+ }
770
+ if (options.include && options.include.length > 0) {
771
+ serveOptions.include = options.include;
772
+ }
773
+ if (options.exclude && options.exclude.length > 0) {
774
+ serveOptions.exclude = options.exclude;
775
+ }
776
+ if (options.host) {
777
+ serveOptions.host = options.host;
778
+ }
779
+ if (typeof options.port === "string" && options.port.length > 0) {
780
+ const parsed = Number(options.port);
781
+ if (!Number.isFinite(parsed)) {
782
+ throw new TypeError(`Invalid port: ${options.port}`);
783
+ }
784
+ serveOptions.port = parsed;
785
+ }
786
+ if (typeof options.playground !== "undefined") {
787
+ serveOptions.playground = options.playground;
788
+ }
789
+ return serveOptions;
790
+ }
791
+ function createCli() {
792
+ const program = new Command();
793
+ program.name("mokup").description("Mock utilities for file-based routes.").showHelpAfterError();
794
+ program.command("build").description("Generate .mokup build output").option("-d, --dir <dir>", "Mock directory (repeatable)", collectValues).option("-o, --out <dir>", "Output directory (default: .mokup)").option("--prefix <prefix>", "URL prefix").option("--include <pattern>", "Include regex (repeatable)", collectRegex).option("--exclude <pattern>", "Exclude regex (repeatable)", collectRegex).option("--no-handlers", "Skip function handler output").action(async (options) => {
795
+ const buildOptions = toBuildOptions(options);
796
+ await buildManifest(buildOptions);
797
+ });
798
+ 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("--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) => {
799
+ const serveOptions = toServeOptions(options);
800
+ const host = serveOptions.host ?? "localhost";
801
+ const port = serveOptions.port ?? 8080;
802
+ const playgroundEnabled = serveOptions.playground !== false;
803
+ const playgroundPath = "/_mokup";
804
+ const server = await createFetchServer(serveOptions);
805
+ const nodeServer = serve(
806
+ {
807
+ fetch: server.fetch,
808
+ hostname: host,
809
+ port
810
+ },
811
+ (info) => {
812
+ const resolvedHost = typeof info === "string" ? host : info?.address ?? host;
813
+ const resolvedPort = typeof info === "string" ? port : info?.port ?? port;
814
+ console.log(`Mock server ready at http://${resolvedHost}:${resolvedPort}`);
815
+ if (playgroundEnabled) {
816
+ console.log(`Playground at http://${resolvedHost}:${resolvedPort}${playgroundPath}`);
817
+ }
818
+ }
819
+ );
820
+ const shutdown = async () => {
821
+ try {
822
+ if (server.close) {
823
+ await server.close();
824
+ }
825
+ await new Promise((resolve, reject) => {
826
+ nodeServer.close((error) => {
827
+ if (error) {
828
+ reject(error);
829
+ return;
830
+ }
831
+ resolve();
832
+ });
833
+ });
834
+ } finally {
835
+ process.exit(0);
836
+ }
837
+ };
838
+ process.on("SIGINT", shutdown);
839
+ process.on("SIGTERM", shutdown);
840
+ });
841
+ program.command("help").description("Show help").action(() => {
842
+ program.help();
843
+ });
844
+ return program;
845
+ }
846
+ async function runCli(argv = process.argv) {
847
+ const program = createCli();
848
+ if (argv.length <= 2) {
849
+ program.help();
850
+ return;
851
+ }
852
+ await program.parseAsync(argv);
853
+ }
854
+
855
+ export { buildManifest, createCli, runCli };