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