mokup 2.0.2 → 2.1.1

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.
@@ -1,34 +1,14 @@
1
- import { Buffer } from 'node:buffer';
2
- import { Hono, PatternRouter } from '@mokup/shared/hono';
3
- import { promises } from 'node:fs';
1
+ import { promises, existsSync } from 'node:fs';
2
+ import { resolve, isAbsolute, relative, join, dirname, normalize, extname, basename } from '@mokup/shared/pathe';
4
3
  import { createRequire } from 'node:module';
5
4
  import { cwd } from 'node:process';
6
- import { resolve, isAbsolute, relative, join, normalize, extname, dirname, basename } from '@mokup/shared/pathe';
7
- import { pathToFileURL } from 'node:url';
5
+ import { Buffer } from 'node:buffer';
6
+ import { Hono, PatternRouter } from '@mokup/shared/hono';
7
+ import { fileURLToPath, pathToFileURL } from 'node:url';
8
8
  import { build } from '@mokup/shared/esbuild';
9
9
  import { parse } from '@mokup/shared/jsonc-parser';
10
10
  import { compareRouteScore, parseRouteTemplate } from '@mokup/runtime';
11
11
 
12
- function createLogger(enabled) {
13
- return {
14
- info: (...args) => {
15
- if (enabled) {
16
- console.info("[mokup]", ...args);
17
- }
18
- },
19
- warn: (...args) => {
20
- if (enabled) {
21
- console.warn("[mokup]", ...args);
22
- }
23
- },
24
- error: (...args) => {
25
- if (enabled) {
26
- console.error("[mokup]", ...args);
27
- }
28
- }
29
- };
30
- }
31
-
32
12
  const methodSet = /* @__PURE__ */ new Set([
33
13
  "GET",
34
14
  "POST",
@@ -98,20 +78,6 @@ function isInDirs(file, dirs) {
98
78
  return normalized === normalizedDir || normalized.startsWith(`${normalizedDir}/`);
99
79
  });
100
80
  }
101
- function testPatterns(patterns, value) {
102
- const list = Array.isArray(patterns) ? patterns : [patterns];
103
- return list.some((pattern) => pattern.test(value));
104
- }
105
- function matchesFilter(file, include, exclude) {
106
- const normalized = toPosix(file);
107
- if (exclude && testPatterns(exclude, normalized)) {
108
- return false;
109
- }
110
- if (include) {
111
- return testPatterns(include, normalized);
112
- }
113
- return true;
114
- }
115
81
  function normalizeIgnorePrefix(value, fallback = ["."]) {
116
82
  const list = typeof value === "undefined" ? fallback : Array.isArray(value) ? value : [value];
117
83
  return list.filter((entry) => typeof entry === "string" && entry.length > 0);
@@ -130,222 +96,188 @@ function delay(ms) {
130
96
  return new Promise((resolve2) => setTimeout(resolve2, ms));
131
97
  }
132
98
 
133
- function toHonoPath(route) {
134
- if (!route.tokens || route.tokens.length === 0) {
135
- return "/";
99
+ function toViteImportPath(file, root) {
100
+ const absolute = isAbsolute(file) ? file : resolve(root, file);
101
+ const rel = relative(root, absolute);
102
+ if (!rel.startsWith("..") && !isAbsolute(rel)) {
103
+ return `/${toPosix(rel)}`;
136
104
  }
137
- const segments = route.tokens.map((token) => {
138
- if (token.type === "static") {
139
- return token.value;
140
- }
141
- if (token.type === "param") {
142
- return `:${token.name}`;
143
- }
144
- if (token.type === "catchall") {
145
- return `:${token.name}{.+}`;
146
- }
147
- return `:${token.name}{.+}?`;
148
- });
149
- return `/${segments.join("/")}`;
150
- }
151
- function isValidStatus(status) {
152
- return typeof status === "number" && Number.isFinite(status) && status >= 200 && status <= 599;
105
+ return `/@fs/${toPosix(absolute)}`;
153
106
  }
154
- function resolveStatus(routeStatus, responseStatus) {
155
- if (isValidStatus(routeStatus)) {
156
- return routeStatus;
107
+ function shouldModuleize(handler) {
108
+ if (typeof handler === "function") {
109
+ return true;
157
110
  }
158
- if (isValidStatus(responseStatus)) {
159
- return responseStatus;
111
+ if (typeof Response !== "undefined" && handler instanceof Response) {
112
+ return true;
160
113
  }
161
- return 200;
114
+ return false;
162
115
  }
163
- function applyRouteOverrides(response, route) {
164
- const headers = new Headers(response.headers);
165
- const hasHeaders = !!route.headers && Object.keys(route.headers).length > 0;
166
- if (route.headers) {
167
- for (const [key, value] of Object.entries(route.headers)) {
168
- headers.set(key, value);
169
- }
116
+ const BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
117
+ function getNodeBuffer() {
118
+ if (typeof globalThis === "undefined") {
119
+ return null;
170
120
  }
171
- const status = resolveStatus(route.status, response.status);
172
- if (status === response.status && !hasHeaders) {
173
- return response;
121
+ const buffer = globalThis.Buffer;
122
+ return buffer ?? null;
123
+ }
124
+ function getBtoa() {
125
+ if (typeof globalThis === "undefined") {
126
+ return null;
174
127
  }
175
- return new Response(response.body, { status, headers });
128
+ const btoaFn = globalThis.btoa;
129
+ return typeof btoaFn === "function" ? btoaFn : null;
130
+ }
131
+ function encodeBase64(bytes) {
132
+ const buffer = getNodeBuffer();
133
+ if (buffer) {
134
+ return buffer.from(bytes).toString("base64");
135
+ }
136
+ const btoaFn = getBtoa();
137
+ if (btoaFn) {
138
+ let binary = "";
139
+ const chunkSize = 32768;
140
+ for (let i = 0; i < bytes.length; i += chunkSize) {
141
+ const chunk = bytes.subarray(i, i + chunkSize);
142
+ binary += String.fromCharCode(...chunk);
143
+ }
144
+ return btoaFn(binary);
145
+ }
146
+ let output = "";
147
+ for (let i = 0; i < bytes.length; i += 3) {
148
+ const a = bytes[i] ?? 0;
149
+ const b = i + 1 < bytes.length ? bytes[i + 1] ?? 0 : 0;
150
+ const c = i + 2 < bytes.length ? bytes[i + 2] ?? 0 : 0;
151
+ const triple = a << 16 | b << 8 | c;
152
+ output += BASE64_ALPHABET[triple >> 18 & 63];
153
+ output += BASE64_ALPHABET[triple >> 12 & 63];
154
+ output += i + 1 < bytes.length ? BASE64_ALPHABET[triple >> 6 & 63] : "=";
155
+ output += i + 2 < bytes.length ? BASE64_ALPHABET[triple & 63] : "=";
156
+ }
157
+ return output;
176
158
  }
177
- function resolveResponse(value, fallback) {
178
- if (value instanceof Response) {
179
- return value;
159
+ function toBinaryBody(handler) {
160
+ if (handler instanceof ArrayBuffer) {
161
+ return encodeBase64(new Uint8Array(handler));
180
162
  }
181
- if (value && typeof value === "object" && "res" in value) {
182
- const resolved = value.res;
183
- if (resolved instanceof Response) {
184
- return resolved;
185
- }
163
+ if (handler instanceof Uint8Array) {
164
+ return encodeBase64(handler);
186
165
  }
187
- return fallback;
166
+ return null;
188
167
  }
189
- function normalizeHandlerValue(c, value) {
190
- if (value instanceof Response) {
191
- return value;
192
- }
193
- if (typeof value === "undefined") {
194
- const response = c.body(null);
195
- if (response.status === 200) {
196
- return new Response(response.body, {
197
- status: 204,
198
- headers: response.headers
199
- });
168
+ function buildManifestResponse(route, moduleId) {
169
+ if (moduleId) {
170
+ const response = {
171
+ type: "module",
172
+ module: moduleId
173
+ };
174
+ if (typeof route.ruleIndex === "number") {
175
+ response.ruleIndex = route.ruleIndex;
200
176
  }
201
177
  return response;
202
178
  }
203
- if (typeof value === "string") {
204
- return c.text(value);
179
+ const handler = route.handler;
180
+ if (typeof handler === "string") {
181
+ return {
182
+ type: "text",
183
+ body: handler
184
+ };
205
185
  }
206
- if (value instanceof Uint8Array || value instanceof ArrayBuffer) {
207
- if (!c.res.headers.get("content-type")) {
208
- c.header("content-type", "application/octet-stream");
209
- }
210
- const data = value instanceof ArrayBuffer ? new Uint8Array(value) : new Uint8Array(value);
211
- return c.body(data);
186
+ const binary = toBinaryBody(handler);
187
+ if (binary) {
188
+ return {
189
+ type: "binary",
190
+ body: binary,
191
+ encoding: "base64"
192
+ };
212
193
  }
213
- return c.json(value);
214
- }
215
- function createRouteHandler(route) {
216
- return async (c) => {
217
- const value = typeof route.handler === "function" ? await route.handler(c) : route.handler;
218
- return normalizeHandlerValue(c, value);
194
+ return {
195
+ type: "json",
196
+ body: handler
219
197
  };
220
198
  }
221
- function createFinalizeMiddleware(route) {
222
- return async (c, next) => {
223
- const response = await next();
224
- const resolved = resolveResponse(response, c.res);
225
- if (route.delay && route.delay > 0) {
226
- await delay(route.delay);
199
+ function buildManifestData(params) {
200
+ const { routes, root } = params;
201
+ const resolveModulePath = params.resolveModulePath ?? toViteImportPath;
202
+ const ruleModules = /* @__PURE__ */ new Map();
203
+ const middlewareModules = /* @__PURE__ */ new Map();
204
+ const manifestRoutes = routes.map((route) => {
205
+ const moduleId = shouldModuleize(route.handler) ? resolveModulePath(route.file, root) : null;
206
+ if (moduleId && !ruleModules.has(moduleId)) {
207
+ ruleModules.set(moduleId, { id: moduleId, kind: "rule" });
227
208
  }
228
- const overridden = applyRouteOverrides(resolved, route);
229
- c.res = overridden;
230
- return overridden;
209
+ const middleware = route.middlewares?.map((entry) => {
210
+ const modulePath = resolveModulePath(entry.source, root);
211
+ if (!middlewareModules.has(modulePath)) {
212
+ middlewareModules.set(modulePath, { id: modulePath, kind: "middleware" });
213
+ }
214
+ return {
215
+ module: modulePath,
216
+ ruleIndex: entry.index
217
+ };
218
+ });
219
+ const response = buildManifestResponse(route, moduleId);
220
+ const manifestRoute = {
221
+ method: route.method,
222
+ url: route.template,
223
+ ...route.tokens ? { tokens: route.tokens } : {},
224
+ ...route.score ? { score: route.score } : {},
225
+ ...route.status ? { status: route.status } : {},
226
+ ...route.headers ? { headers: route.headers } : {},
227
+ ...route.delay ? { delay: route.delay } : {},
228
+ ...middleware && middleware.length > 0 ? { middleware } : {},
229
+ response
230
+ };
231
+ return manifestRoute;
232
+ });
233
+ const manifest = {
234
+ version: 1,
235
+ routes: manifestRoutes
231
236
  };
232
- }
233
- function wrapMiddleware(handler) {
234
- return async (c, next) => {
235
- const response = await handler(c, next);
236
- return resolveResponse(response, c.res);
237
+ return {
238
+ manifest,
239
+ modules: [
240
+ ...ruleModules.values(),
241
+ ...middlewareModules.values()
242
+ ]
237
243
  };
238
244
  }
239
- function createHonoApp(routes) {
240
- const app = new Hono({ router: new PatternRouter(), strict: false });
241
- for (const route of routes) {
242
- const middlewares = route.middlewares?.map((entry) => wrapMiddleware(entry.handle)) ?? [];
243
- app.on(
244
- route.method,
245
- toHonoPath(route),
246
- createFinalizeMiddleware(route),
247
- ...middlewares,
248
- createRouteHandler(route)
249
- );
245
+
246
+ function normalizePlaygroundPath(value) {
247
+ if (!value) {
248
+ return "/__mokup";
250
249
  }
251
- return app;
252
- }
253
- async function readRawBody(req) {
254
- return await new Promise((resolve, reject) => {
255
- const chunks = [];
256
- req.on("data", (chunk) => {
257
- if (typeof chunk === "string") {
258
- chunks.push(Buffer.from(chunk));
259
- return;
260
- }
261
- if (chunk instanceof Uint8Array) {
262
- chunks.push(chunk);
263
- return;
264
- }
265
- chunks.push(Buffer.from(String(chunk)));
266
- });
267
- req.on("end", () => {
268
- if (chunks.length === 0) {
269
- resolve(null);
270
- return;
271
- }
272
- resolve(Buffer.concat(chunks));
273
- });
274
- req.on("error", reject);
275
- });
250
+ const normalized = value.startsWith("/") ? value : `/${value}`;
251
+ return normalized.length > 1 && normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
276
252
  }
277
- function buildHeaders(headers) {
278
- const result = new Headers();
279
- for (const [key, value] of Object.entries(headers)) {
280
- if (typeof value === "undefined") {
281
- continue;
282
- }
283
- if (Array.isArray(value)) {
284
- result.set(key, value.join(","));
285
- } else {
286
- result.set(key, value);
287
- }
253
+ function normalizeBase(base) {
254
+ if (!base || base === "/") {
255
+ return "";
288
256
  }
289
- return result;
257
+ return base.endsWith("/") ? base.slice(0, -1) : base;
290
258
  }
291
- async function toRequest(req) {
292
- const url = new URL(req.url ?? "/", "http://mokup.local");
293
- const method = req.method ?? "GET";
294
- const headers = buildHeaders(req.headers);
295
- const init = { method, headers };
296
- const rawBody = await readRawBody(req);
297
- if (rawBody && method !== "GET" && method !== "HEAD") {
298
- init.body = rawBody;
259
+ function resolvePlaygroundRequestPath(base, playgroundPath) {
260
+ const normalizedBase = normalizeBase(base);
261
+ const normalizedPath = normalizePlaygroundPath(playgroundPath);
262
+ if (!normalizedBase) {
263
+ return normalizedPath;
299
264
  }
300
- return new Request(url.toString(), init);
265
+ if (normalizedPath.startsWith(normalizedBase)) {
266
+ return normalizedPath;
267
+ }
268
+ return `${normalizedBase}${normalizedPath}`;
301
269
  }
302
- async function sendResponse(res, response) {
303
- res.statusCode = response.status;
304
- response.headers.forEach((value, key) => {
305
- res.setHeader(key, value);
306
- });
307
- if (!response.body) {
308
- res.end();
309
- return;
270
+ function resolvePlaygroundOptions(playground) {
271
+ if (playground === false) {
272
+ return { enabled: false, path: "/__mokup" };
310
273
  }
311
- const buffer = new Uint8Array(await response.arrayBuffer());
312
- res.end(buffer);
313
- }
314
- function hasMatch(app, method, pathname) {
315
- const matchMethod = method === "HEAD" ? "GET" : method;
316
- const match = app.router.match(matchMethod, pathname);
317
- return !!match && match[0].length > 0;
318
- }
319
- function createMiddleware(getApp, logger) {
320
- return async (req, res, next) => {
321
- const app = getApp();
322
- if (!app) {
323
- return next();
324
- }
325
- const url = req.url ?? "/";
326
- const parsedUrl = new URL(url, "http://mokup.local");
327
- const pathname = parsedUrl.pathname;
328
- const method = normalizeMethod(req.method) ?? "GET";
329
- if (!hasMatch(app, method, pathname)) {
330
- return next();
331
- }
332
- const startedAt = Date.now();
333
- try {
334
- const response = await app.fetch(await toRequest(req));
335
- if (res.writableEnded) {
336
- return;
337
- }
338
- await sendResponse(res, response);
339
- logger.info(`${method} ${pathname} ${Date.now() - startedAt}ms`);
340
- } catch (error) {
341
- if (!res.headersSent) {
342
- res.statusCode = 500;
343
- res.setHeader("Content-Type", "text/plain; charset=utf-8");
344
- }
345
- res.end("Mock handler error");
346
- logger.error("Mock handler failed:", error);
347
- }
348
- };
274
+ if (playground && typeof playground === "object") {
275
+ return {
276
+ enabled: playground.enabled !== false,
277
+ path: normalizePlaygroundPath(playground.path)
278
+ };
279
+ }
280
+ return { enabled: true, path: "/__mokup" };
349
281
  }
350
282
 
351
283
  const require$1 = createRequire(import.meta.url);
@@ -361,96 +293,6 @@ const mimeTypes = {
361
293
  ".jpeg": "image/jpeg",
362
294
  ".ico": "image/x-icon"
363
295
  };
364
- function normalizePlaygroundPath(value) {
365
- if (!value) {
366
- return "/_mokup";
367
- }
368
- const normalized = value.startsWith("/") ? value : `/${value}`;
369
- return normalized.length > 1 && normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
370
- }
371
- function normalizeBase(base) {
372
- if (!base || base === "/") {
373
- return "";
374
- }
375
- return base.endsWith("/") ? base.slice(0, -1) : base;
376
- }
377
- function resolvePlaygroundRequestPath(base, playgroundPath) {
378
- const normalizedBase = normalizeBase(base);
379
- const normalizedPath = normalizePlaygroundPath(playgroundPath);
380
- if (!normalizedBase) {
381
- return normalizedPath;
382
- }
383
- if (normalizedPath.startsWith(normalizedBase)) {
384
- return normalizedPath;
385
- }
386
- return `${normalizedBase}${normalizedPath}`;
387
- }
388
- function injectPlaygroundHmr(html, base) {
389
- if (html.includes("mokup-playground-hmr")) {
390
- return html;
391
- }
392
- const normalizedBase = normalizeBase(base);
393
- const clientPath = `${normalizedBase}/@vite/client`;
394
- const snippet = [
395
- '<script type="module" id="mokup-playground-hmr">',
396
- `import('${clientPath}').then(({ createHotContext }) => {`,
397
- " const hot = createHotContext('/@mokup/playground')",
398
- " hot.on('mokup:routes-changed', () => {",
399
- " const api = window.__MOKUP_PLAYGROUND__",
400
- " if (api && typeof api.reloadRoutes === 'function') {",
401
- " api.reloadRoutes()",
402
- " return",
403
- " }",
404
- " window.location.reload()",
405
- " })",
406
- "}).catch(() => {})",
407
- "<\/script>"
408
- ].join("\n");
409
- if (html.includes("</body>")) {
410
- return html.replace("</body>", `${snippet}
411
- </body>`);
412
- }
413
- return `${html}
414
- ${snippet}`;
415
- }
416
- function injectPlaygroundSw(html, script) {
417
- if (!script) {
418
- return html;
419
- }
420
- if (html.includes("mokup-playground-sw")) {
421
- return html;
422
- }
423
- const snippet = [
424
- '<script type="module" id="mokup-playground-sw">',
425
- script,
426
- "<\/script>"
427
- ].join("\n");
428
- if (html.includes("</head>")) {
429
- return html.replace("</head>", `${snippet}
430
- </head>`);
431
- }
432
- if (html.includes("</body>")) {
433
- return html.replace("</body>", `${snippet}
434
- </body>`);
435
- }
436
- return `${html}
437
- ${snippet}`;
438
- }
439
- function isViteDevServer(server) {
440
- return !!server && "ws" in server;
441
- }
442
- function resolvePlaygroundOptions(playground) {
443
- if (playground === false) {
444
- return { enabled: false, path: "/_mokup" };
445
- }
446
- if (playground && typeof playground === "object") {
447
- return {
448
- enabled: playground.enabled !== false,
449
- path: normalizePlaygroundPath(playground.path)
450
- };
451
- }
452
- return { enabled: true, path: "/_mokup" };
453
- }
454
296
  function resolvePlaygroundDist() {
455
297
  const pkgPath = require$1.resolve("@mokup/playground/package.json");
456
298
  return join(pkgPath, "..", "dist");
@@ -465,6 +307,7 @@ function sendFile(res, content, contentType) {
465
307
  res.setHeader("Content-Type", contentType);
466
308
  res.end(content);
467
309
  }
310
+
468
311
  function toPosixPath(value) {
469
312
  return value.replace(/\\/g, "/");
470
313
  }
@@ -506,41 +349,6 @@ function resolveGroupRoot(dirs, serverRoot) {
506
349
  }
507
350
  return common;
508
351
  }
509
- const disabledReasonSet = /* @__PURE__ */ new Set([
510
- "disabled",
511
- "disabled-dir",
512
- "exclude",
513
- "ignore-prefix",
514
- "include",
515
- "unknown"
516
- ]);
517
- const ignoredReasonSet = /* @__PURE__ */ new Set([
518
- "unsupported",
519
- "invalid-route",
520
- "unknown"
521
- ]);
522
- function normalizeDisabledReason(reason) {
523
- if (reason && disabledReasonSet.has(reason)) {
524
- return reason;
525
- }
526
- return "unknown";
527
- }
528
- function normalizeIgnoredReason(reason) {
529
- if (reason && ignoredReasonSet.has(reason)) {
530
- return reason;
531
- }
532
- return "unknown";
533
- }
534
- function formatRouteFile(file, root) {
535
- if (!root) {
536
- return toPosixPath(file);
537
- }
538
- const rel = toPosixPath(relative(root, file));
539
- if (!rel || rel.startsWith("..")) {
540
- return toPosixPath(file);
541
- }
542
- return rel;
543
- }
544
352
  function resolveGroups(dirs, root) {
545
353
  const groups = [];
546
354
  const seen = /* @__PURE__ */ new Set();
@@ -575,64 +383,205 @@ function resolveRouteGroup(routeFile, groups) {
575
383
  }
576
384
  return matched;
577
385
  }
578
- function toPlaygroundRoute(route, root, groups) {
579
- const matchedGroup = resolveRouteGroup(route.file, groups);
580
- const middlewareSources = route.middlewares?.map((entry) => formatRouteFile(entry.source, root));
581
- return {
582
- method: route.method,
583
- url: route.template,
584
- file: formatRouteFile(route.file, root),
585
- type: typeof route.handler === "function" ? "handler" : "static",
586
- status: route.status,
587
- delay: route.delay,
588
- middlewareCount: middlewareSources?.length ?? 0,
589
- middlewares: middlewareSources,
590
- groupKey: matchedGroup?.key,
591
- group: matchedGroup?.label
592
- };
593
- }
594
- function toPlaygroundDisabledRoute(route, root, groups) {
595
- const matchedGroup = resolveRouteGroup(route.file, groups);
596
- const disabled = {
597
- file: formatRouteFile(route.file, root),
598
- reason: normalizeDisabledReason(route.reason)
599
- };
600
- if (typeof route.method !== "undefined") {
601
- disabled.method = route.method;
602
- }
603
- if (typeof route.url !== "undefined") {
604
- disabled.url = route.url;
386
+ function formatRouteFile(file, root) {
387
+ if (!root) {
388
+ return toPosixPath(file);
605
389
  }
606
- if (matchedGroup) {
607
- disabled.groupKey = matchedGroup.key;
608
- disabled.group = matchedGroup.label;
390
+ const rel = toPosixPath(relative(root, file));
391
+ if (!rel || rel.startsWith("..")) {
392
+ return toPosixPath(file);
609
393
  }
610
- return disabled;
394
+ return rel;
611
395
  }
612
- function toPlaygroundIgnoredRoute(route, root, groups) {
613
- const matchedGroup = resolveRouteGroup(route.file, groups);
614
- const ignored = {
615
- file: formatRouteFile(route.file, root),
616
- reason: normalizeIgnoredReason(route.reason)
617
- };
618
- if (matchedGroup) {
619
- ignored.groupKey = matchedGroup.key;
620
- ignored.group = matchedGroup.label;
396
+
397
+ function injectPlaygroundHmr(html, base) {
398
+ if (html.includes("mokup-playground-hmr")) {
399
+ return html;
621
400
  }
622
- return ignored;
623
- }
624
- function toPlaygroundConfigFile(entry, root, groups) {
625
- const matchedGroup = resolveRouteGroup(entry.file, groups);
626
- const configFile = {
627
- file: formatRouteFile(entry.file, root)
628
- };
629
- if (matchedGroup) {
630
- configFile.groupKey = matchedGroup.key;
631
- configFile.group = matchedGroup.label;
401
+ const normalizedBase = normalizeBase(base);
402
+ const clientPath = `${normalizedBase}/@vite/client`;
403
+ const snippet = [
404
+ '<script type="module" id="mokup-playground-hmr">',
405
+ `import('${clientPath}').then(({ createHotContext }) => {`,
406
+ " const hot = createHotContext('/@mokup/playground')",
407
+ " hot.on('mokup:routes-changed', () => {",
408
+ " const api = window.__MOKUP_PLAYGROUND__",
409
+ " if (api && typeof api.reloadRoutes === 'function') {",
410
+ " api.reloadRoutes()",
411
+ " return",
412
+ " }",
413
+ " window.location.reload()",
414
+ " })",
415
+ "}).catch(() => {})",
416
+ "<\/script>"
417
+ ].join("\n");
418
+ if (html.includes("</body>")) {
419
+ return html.replace("</body>", `${snippet}
420
+ </body>`);
632
421
  }
633
- return configFile;
422
+ return `${html}
423
+ ${snippet}`;
634
424
  }
635
- function createPlaygroundMiddleware(params) {
425
+ function injectPlaygroundSw(html, script) {
426
+ if (!script) {
427
+ return html;
428
+ }
429
+ if (html.includes("mokup-playground-sw")) {
430
+ return html;
431
+ }
432
+ const snippet = [
433
+ '<script type="module" id="mokup-playground-sw">',
434
+ script,
435
+ "<\/script>"
436
+ ].join("\n");
437
+ if (html.includes("</head>")) {
438
+ return html.replace("</head>", `${snippet}
439
+ </head>`);
440
+ }
441
+ if (html.includes("</body>")) {
442
+ return html.replace("</body>", `${snippet}
443
+ </body>`);
444
+ }
445
+ return `${html}
446
+ ${snippet}`;
447
+ }
448
+ function isViteDevServer(server) {
449
+ return !!server && "ws" in server;
450
+ }
451
+
452
+ const disabledReasonSet = /* @__PURE__ */ new Set([
453
+ "disabled",
454
+ "disabled-dir",
455
+ "exclude",
456
+ "ignore-prefix",
457
+ "include",
458
+ "unknown"
459
+ ]);
460
+ const ignoredReasonSet = /* @__PURE__ */ new Set([
461
+ "unsupported",
462
+ "invalid-route",
463
+ "unknown"
464
+ ]);
465
+ function normalizeDisabledReason(reason) {
466
+ if (reason && disabledReasonSet.has(reason)) {
467
+ return reason;
468
+ }
469
+ return "unknown";
470
+ }
471
+ function normalizeIgnoredReason(reason) {
472
+ if (reason && ignoredReasonSet.has(reason)) {
473
+ return reason;
474
+ }
475
+ return "unknown";
476
+ }
477
+ function toPlaygroundRoute(route, root, groups) {
478
+ const matchedGroup = resolveRouteGroup(route.file, groups);
479
+ const preSources = route.middlewares?.filter((entry) => entry.position === "pre").map((entry) => formatRouteFile(entry.source, root)) ?? [];
480
+ const postSources = route.middlewares?.filter((entry) => entry.position === "post").map((entry) => formatRouteFile(entry.source, root)) ?? [];
481
+ const normalSources = route.middlewares?.filter((entry) => entry.position !== "pre" && entry.position !== "post").map((entry) => formatRouteFile(entry.source, root)) ?? [];
482
+ const combinedSources = [
483
+ ...preSources,
484
+ ...normalSources,
485
+ ...postSources
486
+ ];
487
+ const configChain = route.configChain?.map((entry) => formatRouteFile(entry, root)) ?? [];
488
+ return {
489
+ method: route.method,
490
+ url: route.template,
491
+ file: formatRouteFile(route.file, root),
492
+ type: typeof route.handler === "function" ? "handler" : "static",
493
+ status: route.status,
494
+ delay: route.delay,
495
+ middlewareCount: combinedSources.length,
496
+ middlewares: combinedSources,
497
+ preMiddlewareCount: preSources.length,
498
+ normalMiddlewareCount: normalSources.length,
499
+ postMiddlewareCount: postSources.length,
500
+ preMiddlewares: preSources,
501
+ normalMiddlewares: normalSources,
502
+ postMiddlewares: postSources,
503
+ configChain: configChain.length > 0 ? configChain : void 0,
504
+ groupKey: matchedGroup?.key,
505
+ group: matchedGroup?.label
506
+ };
507
+ }
508
+ function toPlaygroundDisabledRoute(route, root, groups) {
509
+ const matchedGroup = resolveRouteGroup(route.file, groups);
510
+ const disabled = {
511
+ file: formatRouteFile(route.file, root),
512
+ reason: normalizeDisabledReason(route.reason)
513
+ };
514
+ if (typeof route.method !== "undefined") {
515
+ disabled.method = route.method;
516
+ }
517
+ if (typeof route.url !== "undefined") {
518
+ disabled.url = route.url;
519
+ }
520
+ if (route.configChain && route.configChain.length > 0) {
521
+ disabled.configChain = route.configChain.map((entry) => formatRouteFile(entry, root));
522
+ }
523
+ if (route.decisionChain && route.decisionChain.length > 0) {
524
+ disabled.decisionChain = formatDecisionChain(route.decisionChain, root);
525
+ }
526
+ if (route.effectiveConfig && Object.keys(route.effectiveConfig).length > 0) {
527
+ disabled.effectiveConfig = route.effectiveConfig;
528
+ }
529
+ if (matchedGroup) {
530
+ disabled.groupKey = matchedGroup.key;
531
+ disabled.group = matchedGroup.label;
532
+ }
533
+ return disabled;
534
+ }
535
+ function toPlaygroundIgnoredRoute(route, root, groups) {
536
+ const matchedGroup = resolveRouteGroup(route.file, groups);
537
+ const ignored = {
538
+ file: formatRouteFile(route.file, root),
539
+ reason: normalizeIgnoredReason(route.reason)
540
+ };
541
+ if (matchedGroup) {
542
+ ignored.groupKey = matchedGroup.key;
543
+ ignored.group = matchedGroup.label;
544
+ }
545
+ if (route.configChain && route.configChain.length > 0) {
546
+ ignored.configChain = route.configChain.map((entry) => formatRouteFile(entry, root));
547
+ }
548
+ if (route.decisionChain && route.decisionChain.length > 0) {
549
+ ignored.decisionChain = formatDecisionChain(route.decisionChain, root);
550
+ }
551
+ if (route.effectiveConfig && Object.keys(route.effectiveConfig).length > 0) {
552
+ ignored.effectiveConfig = route.effectiveConfig;
553
+ }
554
+ return ignored;
555
+ }
556
+ function toPlaygroundConfigFile(entry, root, groups) {
557
+ const matchedGroup = resolveRouteGroup(entry.file, groups);
558
+ const configFile = {
559
+ file: formatRouteFile(entry.file, root)
560
+ };
561
+ if (matchedGroup) {
562
+ configFile.groupKey = matchedGroup.key;
563
+ configFile.group = matchedGroup.label;
564
+ }
565
+ return configFile;
566
+ }
567
+ function formatDecisionChain(chain, root) {
568
+ return chain.map((entry) => {
569
+ const formatted = {
570
+ step: entry.step,
571
+ result: entry.result
572
+ };
573
+ if (typeof entry.detail !== "undefined") {
574
+ formatted.detail = entry.detail;
575
+ }
576
+ if (typeof entry.source !== "undefined") {
577
+ const source = entry.source;
578
+ formatted.source = source && isAbsolute(source) ? formatRouteFile(source, root) : source;
579
+ }
580
+ return formatted;
581
+ });
582
+ }
583
+
584
+ function createPlaygroundMiddleware(params) {
636
585
  const distDir = resolvePlaygroundDist();
637
586
  const playgroundPath = params.config.path;
638
587
  const indexPath = join(distDir, "index.html");
@@ -715,932 +664,1394 @@ function createPlaygroundMiddleware(params) {
715
664
  };
716
665
  }
717
666
 
718
- const jsonExtensions = /* @__PURE__ */ new Set([".json", ".jsonc"]);
719
- function resolveTemplate(template, prefix) {
720
- const normalized = template.startsWith("/") ? template : `/${template}`;
721
- if (!prefix) {
722
- return normalized;
667
+ const defaultSwPath = "/mokup-sw.js";
668
+ const defaultSwScope = "/";
669
+ function normalizeSwPath(path) {
670
+ if (!path) {
671
+ return defaultSwPath;
723
672
  }
724
- const normalizedPrefix = normalizePrefix(prefix);
725
- if (!normalizedPrefix) {
726
- return normalized;
673
+ return path.startsWith("/") ? path : `/${path}`;
674
+ }
675
+ function normalizeSwScope(scope) {
676
+ if (!scope) {
677
+ return defaultSwScope;
727
678
  }
728
- if (normalized === normalizedPrefix || normalized.startsWith(`${normalizedPrefix}/`)) {
729
- return normalized;
679
+ return scope.startsWith("/") ? scope : `/${scope}`;
680
+ }
681
+ function normalizeBasePath(value) {
682
+ if (!value) {
683
+ return "/";
730
684
  }
731
- if (normalized === "/") {
732
- return `${normalizedPrefix}/`;
685
+ const normalized = value.startsWith("/") ? value : `/${value}`;
686
+ if (normalized.length > 1 && normalized.endsWith("/")) {
687
+ return normalized.slice(0, -1);
733
688
  }
734
- return `${normalizedPrefix}${normalized}`;
689
+ return normalized;
735
690
  }
736
- function stripMethodSuffix(base) {
737
- const segments = base.split(".");
738
- const last = segments.at(-1);
739
- if (last && methodSuffixSet.has(last.toLowerCase())) {
740
- segments.pop();
741
- return {
742
- name: segments.join("."),
743
- method: last.toUpperCase()
744
- };
691
+ function resolveSwConfigFromEntries(entries, logger) {
692
+ let path = defaultSwPath;
693
+ let scope = defaultSwScope;
694
+ let register = true;
695
+ let unregister = false;
696
+ const basePaths = [];
697
+ let hasPath = false;
698
+ let hasScope = false;
699
+ let hasRegister = false;
700
+ let hasUnregister = false;
701
+ for (const entry of entries) {
702
+ const config = entry.sw;
703
+ if (config?.path) {
704
+ const next = normalizeSwPath(config.path);
705
+ if (!hasPath) {
706
+ path = next;
707
+ hasPath = true;
708
+ } else if (path !== next) {
709
+ logger.warn(`SW path "${next}" ignored; using "${path}".`);
710
+ }
711
+ }
712
+ if (config?.scope) {
713
+ const next = normalizeSwScope(config.scope);
714
+ if (!hasScope) {
715
+ scope = next;
716
+ hasScope = true;
717
+ } else if (scope !== next) {
718
+ logger.warn(`SW scope "${next}" ignored; using "${scope}".`);
719
+ }
720
+ }
721
+ if (typeof config?.register === "boolean") {
722
+ if (!hasRegister) {
723
+ register = config.register;
724
+ hasRegister = true;
725
+ } else if (register !== config.register) {
726
+ logger.warn(
727
+ `SW register="${String(config.register)}" ignored; using "${String(register)}".`
728
+ );
729
+ }
730
+ }
731
+ if (typeof config?.unregister === "boolean") {
732
+ if (!hasUnregister) {
733
+ unregister = config.unregister;
734
+ hasUnregister = true;
735
+ } else if (unregister !== config.unregister) {
736
+ logger.warn(
737
+ `SW unregister="${String(config.unregister)}" ignored; using "${String(unregister)}".`
738
+ );
739
+ }
740
+ }
741
+ if (typeof config?.basePath !== "undefined") {
742
+ const values = Array.isArray(config.basePath) ? config.basePath : [config.basePath];
743
+ for (const value of values) {
744
+ basePaths.push(normalizeBasePath(value));
745
+ }
746
+ continue;
747
+ }
748
+ const normalizedPrefix = normalizePrefix(entry.prefix ?? "");
749
+ if (normalizedPrefix) {
750
+ basePaths.push(normalizedPrefix);
751
+ }
745
752
  }
746
753
  return {
747
- name: base,
748
- method: void 0
754
+ path,
755
+ scope,
756
+ register,
757
+ unregister,
758
+ basePaths: Array.from(new Set(basePaths))
749
759
  };
750
760
  }
751
- function deriveRouteFromFile(file, rootDir, logger) {
752
- const rel = toPosix(relative(rootDir, file));
753
- const ext = extname(rel);
754
- const withoutExt = rel.slice(0, rel.length - ext.length);
755
- const dir = dirname(withoutExt);
756
- const base = basename(withoutExt);
757
- const { name, method } = stripMethodSuffix(base);
758
- const resolvedMethod = method ?? (jsonExtensions.has(ext) ? "GET" : void 0);
759
- if (!resolvedMethod) {
760
- logger.warn(`Skip mock without method suffix: ${file}`);
761
- return null;
762
- }
763
- if (!name) {
764
- logger.warn(`Skip mock with empty route name: ${file}`);
761
+ function resolveSwConfig(options, logger) {
762
+ const swEntries = options.filter((entry) => entry.mode === "sw");
763
+ if (swEntries.length === 0) {
765
764
  return null;
766
765
  }
767
- const joined = dir === "." ? name : join(dir, name);
768
- const segments = toPosix(joined).split("/");
769
- if (segments.at(-1) === "index") {
770
- segments.pop();
771
- }
772
- const template = segments.length === 0 ? "/" : `/${segments.join("/")}`;
773
- const parsed = parseRouteTemplate(template);
774
- if (parsed.errors.length > 0) {
775
- for (const error of parsed.errors) {
776
- logger.warn(`${error} in ${file}`);
777
- }
778
- return null;
779
- }
780
- for (const warning of parsed.warnings) {
781
- logger.warn(`${warning} in ${file}`);
782
- }
783
- return {
784
- template: parsed.template,
785
- method: resolvedMethod,
786
- tokens: parsed.tokens,
787
- score: parsed.score
788
- };
766
+ return resolveSwConfigFromEntries(swEntries, logger);
789
767
  }
790
- function resolveRule(params) {
791
- const method = params.derivedMethod;
792
- if (!method) {
793
- params.logger.warn(`Skip mock without method suffix: ${params.file}`);
794
- return null;
768
+ function resolveSwUnregisterConfig(options, logger) {
769
+ return resolveSwConfigFromEntries(options, logger);
770
+ }
771
+ function buildSwScript(params) {
772
+ const { routes, root } = params;
773
+ const runtimeImportPath = params.runtimeImportPath ?? "mokup/runtime";
774
+ const loggerImportPath = params.loggerImportPath ?? "@mokup/shared/logger";
775
+ const basePaths = params.basePaths ?? [];
776
+ const resolveModulePath = params.resolveModulePath ?? toViteImportPath;
777
+ const { manifest, modules } = buildManifestData({
778
+ routes,
779
+ root,
780
+ resolveModulePath
781
+ });
782
+ const imports = [
783
+ `import { createLogger } from ${JSON.stringify(loggerImportPath)}`,
784
+ `import { createRuntimeApp, handle } from ${JSON.stringify(runtimeImportPath)}`
785
+ ];
786
+ const moduleEntries = [];
787
+ let moduleIndex = 0;
788
+ for (const entry of modules) {
789
+ const name = `module${moduleIndex++}`;
790
+ imports.push(`import * as ${name} from '${entry.id}'`);
791
+ moduleEntries.push({ id: entry.id, name, kind: entry.kind });
795
792
  }
796
- const template = resolveTemplate(params.derivedTemplate, params.prefix);
797
- const parsed = parseRouteTemplate(template);
798
- if (parsed.errors.length > 0) {
799
- for (const error of parsed.errors) {
800
- params.logger.warn(`${error} in ${params.file}`);
793
+ const lines = [];
794
+ lines.push(...imports, "");
795
+ lines.push(
796
+ "const logger = createLogger()",
797
+ "",
798
+ "const resolveModuleExport = (mod) => mod?.default ?? mod",
799
+ "",
800
+ "const toRuntimeRule = (value) => {",
801
+ " if (typeof value === 'undefined') {",
802
+ " return null",
803
+ " }",
804
+ " if (typeof value === 'function') {",
805
+ " return { response: value }",
806
+ " }",
807
+ " if (value === null) {",
808
+ " return { response: null }",
809
+ " }",
810
+ " if (typeof value === 'object') {",
811
+ " if ('response' in value) {",
812
+ " return value",
813
+ " }",
814
+ " if ('handler' in value) {",
815
+ " const handlerRule = value",
816
+ " return {",
817
+ " response: handlerRule.handler,",
818
+ " ...(typeof handlerRule.status === 'number' ? { status: handlerRule.status } : {}),",
819
+ " ...(handlerRule.headers ? { headers: handlerRule.headers } : {}),",
820
+ " ...(typeof handlerRule.delay === 'number' ? { delay: handlerRule.delay } : {}),",
821
+ " }",
822
+ " }",
823
+ " return { response: value }",
824
+ " }",
825
+ " return { response: value }",
826
+ "}",
827
+ "",
828
+ "const toRuntimeRules = (value) => {",
829
+ " if (typeof value === 'undefined') {",
830
+ " return []",
831
+ " }",
832
+ " if (Array.isArray(value)) {",
833
+ " return value.map(toRuntimeRule).filter(Boolean)",
834
+ " }",
835
+ " const rule = toRuntimeRule(value)",
836
+ " return rule ? [rule] : []",
837
+ "}",
838
+ ""
839
+ );
840
+ lines.push(
841
+ `const manifest = ${JSON.stringify(manifest, null, 2)}`,
842
+ ""
843
+ );
844
+ if (moduleEntries.length > 0) {
845
+ lines.push("const moduleMap = {");
846
+ for (const entry of moduleEntries) {
847
+ if (entry.kind === "rule") {
848
+ lines.push(
849
+ ` ${JSON.stringify(entry.id)}: { default: toRuntimeRules(resolveModuleExport(${entry.name})) },`
850
+ );
851
+ continue;
852
+ }
853
+ lines.push(
854
+ ` ${JSON.stringify(entry.id)}: ${entry.name},`
855
+ );
801
856
  }
802
- return null;
803
- }
804
- for (const warning of parsed.warnings) {
805
- params.logger.warn(`${warning} in ${params.file}`);
806
- }
807
- const route = {
808
- file: params.file,
809
- template: parsed.template,
810
- method,
811
- tokens: parsed.tokens,
812
- score: parsed.score,
813
- handler: params.rule.handler
814
- };
815
- if (typeof params.rule.status === "number") {
816
- route.status = params.rule.status;
817
- }
818
- if (params.rule.headers) {
819
- route.headers = params.rule.headers;
820
- }
821
- if (typeof params.rule.delay === "number") {
822
- route.delay = params.rule.delay;
857
+ lines.push("}", "");
823
858
  }
824
- return route;
859
+ const runtimeOptions = moduleEntries.length > 0 ? "{ manifest, moduleMap }" : "{ manifest }";
860
+ lines.push(
861
+ `const basePaths = ${JSON.stringify(basePaths)}`,
862
+ "",
863
+ "self.addEventListener('install', () => {",
864
+ " self.skipWaiting()",
865
+ "})",
866
+ "",
867
+ "self.addEventListener('activate', (event) => {",
868
+ " event.waitUntil(self.clients.claim())",
869
+ "})",
870
+ "",
871
+ "const shouldHandle = (request) => {",
872
+ " if (!basePaths || basePaths.length === 0) {",
873
+ " return true",
874
+ " }",
875
+ " const pathname = new URL(request.url).pathname",
876
+ " return basePaths.some((basePath) => {",
877
+ " if (basePath === '/') {",
878
+ " return true",
879
+ " }",
880
+ " return pathname === basePath || pathname.startsWith(basePath + '/')",
881
+ " })",
882
+ "}",
883
+ "",
884
+ "const registerHandler = async () => {",
885
+ ` const app = await createRuntimeApp(${runtimeOptions})`,
886
+ " const handler = handle(app)",
887
+ " self.addEventListener('fetch', (event) => {",
888
+ " if (!shouldHandle(event.request)) {",
889
+ " return",
890
+ " }",
891
+ " handler(event)",
892
+ " })",
893
+ "}",
894
+ "",
895
+ "registerHandler().catch((error) => {",
896
+ " logger.error('Failed to build service worker app:', error)",
897
+ "})",
898
+ ""
899
+ );
900
+ return lines.join("\n");
825
901
  }
826
- function sortRoutes(routes) {
827
- return routes.sort((a, b) => {
828
- if (a.method !== b.method) {
829
- return a.method.localeCompare(b.method);
902
+
903
+ function toHonoPath(route) {
904
+ if (!route.tokens || route.tokens.length === 0) {
905
+ return "/";
906
+ }
907
+ const segments = route.tokens.map((token) => {
908
+ if (token.type === "static") {
909
+ return token.value;
830
910
  }
831
- const scoreCompare = compareRouteScore(a.score, b.score);
832
- if (scoreCompare !== 0) {
833
- return scoreCompare;
911
+ if (token.type === "param") {
912
+ return `:${token.name}`;
834
913
  }
835
- return a.template.localeCompare(b.template);
914
+ if (token.type === "catchall") {
915
+ return `:${token.name}{.+}`;
916
+ }
917
+ return `:${token.name}{.+}?`;
836
918
  });
919
+ return `/${segments.join("/")}`;
837
920
  }
838
-
839
- async function loadModule$1(file) {
840
- const ext = configExtensions.find((extension) => file.endsWith(extension));
841
- if (ext === ".cjs") {
842
- const require = createRequire(import.meta.url);
843
- delete require.cache[file];
844
- return require(file);
845
- }
846
- if (ext === ".js" || ext === ".mjs") {
847
- return import(`${pathToFileURL(file).href}?t=${Date.now()}`);
921
+ function isValidStatus(status) {
922
+ return typeof status === "number" && Number.isFinite(status) && status >= 200 && status <= 599;
923
+ }
924
+ function resolveStatus(routeStatus, responseStatus) {
925
+ if (isValidStatus(routeStatus)) {
926
+ return routeStatus;
848
927
  }
849
- if (ext === ".ts") {
850
- const result = await build({
851
- entryPoints: [file],
852
- bundle: true,
853
- format: "esm",
854
- platform: "node",
855
- sourcemap: "inline",
856
- target: "es2020",
857
- write: false
858
- });
859
- const output = result.outputFiles[0];
860
- const code = output?.text ?? "";
861
- const dataUrl = `data:text/javascript;base64,${Buffer.from(code).toString(
862
- "base64"
863
- )}`;
864
- return import(`${dataUrl}#${Date.now()}`);
928
+ if (isValidStatus(responseStatus)) {
929
+ return responseStatus;
865
930
  }
866
- return null;
931
+ return 200;
867
932
  }
868
- async function loadModuleWithVite$1(server, file) {
869
- const asDevServer = server;
870
- if ("ssrLoadModule" in asDevServer) {
871
- const moduleNode = asDevServer.moduleGraph.getModuleById(file);
872
- if (moduleNode) {
873
- asDevServer.moduleGraph.invalidateModule(moduleNode);
933
+ function applyRouteOverrides(response, route) {
934
+ const headers = new Headers(response.headers);
935
+ const hasHeaders = !!route.headers && Object.keys(route.headers).length > 0;
936
+ if (route.headers) {
937
+ for (const [key, value] of Object.entries(route.headers)) {
938
+ headers.set(key, value);
874
939
  }
875
- return asDevServer.ssrLoadModule(file);
876
- }
877
- return loadModule$1(file);
878
- }
879
- function getConfigFileCandidates(dir) {
880
- return configExtensions.map((extension) => join(dir, `index.config${extension}`));
881
- }
882
- async function findConfigFile(dir, cache) {
883
- const cached = cache.get(dir);
884
- if (cached !== void 0) {
885
- return cached;
886
940
  }
887
- for (const candidate of getConfigFileCandidates(dir)) {
888
- try {
889
- await promises.stat(candidate);
890
- cache.set(dir, candidate);
891
- return candidate;
892
- } catch {
893
- continue;
894
- }
941
+ const status = resolveStatus(route.status, response.status);
942
+ if (status === response.status && !hasHeaders) {
943
+ return response;
895
944
  }
896
- cache.set(dir, null);
897
- return null;
945
+ return new Response(response.body, { status, headers });
898
946
  }
899
- async function loadConfig(file, server) {
900
- const mod = server ? await loadModuleWithVite$1(server, file) : await loadModule$1(file);
901
- if (!mod) {
902
- return null;
947
+ function resolveResponse(value, fallback) {
948
+ if (value instanceof Response) {
949
+ return value;
903
950
  }
904
- const value = mod?.default ?? mod;
905
- if (!value || typeof value !== "object") {
906
- return null;
951
+ if (value && typeof value === "object" && "res" in value) {
952
+ const resolved = value.res;
953
+ if (resolved instanceof Response) {
954
+ return resolved;
955
+ }
907
956
  }
908
- return value;
957
+ return fallback;
909
958
  }
910
- function normalizeMiddlewares(value, source, logger) {
911
- if (!value) {
912
- return [];
959
+ function normalizeHandlerValue(c, value) {
960
+ if (value instanceof Response) {
961
+ return value;
913
962
  }
914
- const list = Array.isArray(value) ? value : [value];
915
- const middlewares = [];
916
- for (const [index, entry] of list.entries()) {
917
- if (typeof entry !== "function") {
918
- logger.warn(`Invalid middleware in ${source}`);
919
- continue;
963
+ if (typeof value === "undefined") {
964
+ const response = c.body(null);
965
+ if (response.status === 200) {
966
+ return new Response(response.body, {
967
+ status: 204,
968
+ headers: response.headers
969
+ });
920
970
  }
921
- middlewares.push({ handle: entry, source, index });
971
+ return response;
922
972
  }
923
- return middlewares;
924
- }
925
- async function resolveDirectoryConfig(params) {
926
- const { file, rootDir, server, logger, configCache, fileCache } = params;
927
- const resolvedRoot = normalize(rootDir);
928
- const resolvedFileDir = normalize(dirname(file));
929
- const chain = [];
930
- let current = resolvedFileDir;
931
- while (true) {
932
- chain.push(current);
933
- if (current === resolvedRoot) {
934
- break;
935
- }
936
- const parent = dirname(current);
937
- if (parent === current) {
938
- break;
939
- }
940
- current = parent;
973
+ if (typeof value === "string") {
974
+ return c.text(value);
941
975
  }
942
- chain.reverse();
943
- const merged = { middlewares: [] };
944
- for (const dir of chain) {
945
- const configPath = await findConfigFile(dir, fileCache);
946
- if (!configPath) {
947
- continue;
948
- }
949
- let config = configCache.get(configPath);
950
- if (config === void 0) {
951
- config = await loadConfig(configPath, server);
952
- configCache.set(configPath, config);
953
- }
954
- if (!config) {
955
- logger.warn(`Invalid config in ${configPath}`);
956
- continue;
957
- }
958
- if (config.headers) {
959
- merged.headers = { ...merged.headers ?? {}, ...config.headers };
960
- }
961
- if (typeof config.status === "number") {
962
- merged.status = config.status;
963
- }
964
- if (typeof config.delay === "number") {
965
- merged.delay = config.delay;
966
- }
967
- if (typeof config.enabled === "boolean") {
968
- merged.enabled = config.enabled;
969
- }
970
- if (typeof config.ignorePrefix !== "undefined") {
971
- merged.ignorePrefix = config.ignorePrefix;
972
- }
973
- if (typeof config.include !== "undefined") {
974
- merged.include = config.include;
975
- }
976
- if (typeof config.exclude !== "undefined") {
977
- merged.exclude = config.exclude;
978
- }
979
- const normalized = normalizeMiddlewares(config.middleware, configPath, logger);
980
- if (normalized.length > 0) {
981
- merged.middlewares.push(...normalized);
976
+ if (value instanceof Uint8Array || value instanceof ArrayBuffer) {
977
+ if (!c.res.headers.get("content-type")) {
978
+ c.header("content-type", "application/octet-stream");
982
979
  }
980
+ const data = value instanceof ArrayBuffer ? new Uint8Array(value) : new Uint8Array(value);
981
+ return c.body(data);
983
982
  }
984
- return merged;
983
+ return c.json(value);
985
984
  }
986
-
987
- async function walkDir(dir, rootDir, files) {
988
- const entries = await promises.readdir(dir, { withFileTypes: true });
989
- for (const entry of entries) {
990
- if (entry.name === "node_modules" || entry.name === ".git") {
991
- continue;
992
- }
993
- const fullPath = join(dir, entry.name);
994
- if (entry.isDirectory()) {
995
- await walkDir(fullPath, rootDir, files);
996
- continue;
985
+ function createRouteHandler(route) {
986
+ return async (c) => {
987
+ const value = typeof route.handler === "function" ? await route.handler(c) : route.handler;
988
+ return normalizeHandlerValue(c, value);
989
+ };
990
+ }
991
+ function createFinalizeMiddleware(route) {
992
+ return async (c, next) => {
993
+ const response = await next();
994
+ const resolved = resolveResponse(response, c.res);
995
+ if (route.delay && route.delay > 0) {
996
+ await delay(route.delay);
997
997
  }
998
- if (entry.isFile()) {
999
- files.push({ file: fullPath, rootDir });
998
+ const overridden = applyRouteOverrides(resolved, route);
999
+ c.res = overridden;
1000
+ return overridden;
1001
+ };
1002
+ }
1003
+ function wrapMiddleware(handler) {
1004
+ return async (c, next) => {
1005
+ const response = await handler(c, next);
1006
+ return resolveResponse(response, c.res);
1007
+ };
1008
+ }
1009
+ function splitRouteMiddlewares(route) {
1010
+ const before = [];
1011
+ const normal = [];
1012
+ const after = [];
1013
+ for (const entry of route.middlewares ?? []) {
1014
+ const wrapped = wrapMiddleware(entry.handle);
1015
+ if (entry.position === "post") {
1016
+ after.push(wrapped);
1017
+ } else if (entry.position === "pre") {
1018
+ before.push(wrapped);
1019
+ } else {
1020
+ normal.push(wrapped);
1000
1021
  }
1001
1022
  }
1023
+ return { before, normal, after };
1002
1024
  }
1003
- async function exists(path) {
1004
- try {
1005
- await promises.stat(path);
1006
- return true;
1007
- } catch {
1008
- return false;
1025
+ function createHonoApp(routes) {
1026
+ const app = new Hono({ router: new PatternRouter(), strict: false });
1027
+ for (const route of routes) {
1028
+ const { before, normal, after } = splitRouteMiddlewares(route);
1029
+ app.on(
1030
+ route.method,
1031
+ toHonoPath(route),
1032
+ createFinalizeMiddleware(route),
1033
+ ...before,
1034
+ ...normal,
1035
+ ...after,
1036
+ createRouteHandler(route)
1037
+ );
1009
1038
  }
1039
+ return app;
1010
1040
  }
1011
- async function collectFiles(dirs) {
1012
- const files = [];
1013
- for (const dir of dirs) {
1014
- if (!await exists(dir)) {
1041
+ async function readRawBody(req) {
1042
+ return await new Promise((resolve, reject) => {
1043
+ const chunks = [];
1044
+ req.on("data", (chunk) => {
1045
+ if (typeof chunk === "string") {
1046
+ chunks.push(Buffer.from(chunk));
1047
+ return;
1048
+ }
1049
+ if (chunk instanceof Uint8Array) {
1050
+ chunks.push(chunk);
1051
+ return;
1052
+ }
1053
+ chunks.push(Buffer.from(String(chunk)));
1054
+ });
1055
+ req.on("end", () => {
1056
+ if (chunks.length === 0) {
1057
+ resolve(null);
1058
+ return;
1059
+ }
1060
+ resolve(Buffer.concat(chunks));
1061
+ });
1062
+ req.on("error", reject);
1063
+ });
1064
+ }
1065
+ function buildHeaders(headers) {
1066
+ const result = new Headers();
1067
+ for (const [key, value] of Object.entries(headers)) {
1068
+ if (typeof value === "undefined") {
1015
1069
  continue;
1016
1070
  }
1017
- await walkDir(dir, dir, files);
1018
- }
1019
- return files;
1020
- }
1021
- function isSupportedFile(file) {
1022
- if (file.endsWith(".d.ts")) {
1023
- return false;
1024
- }
1025
- if (isConfigFile(file)) {
1026
- return false;
1071
+ if (Array.isArray(value)) {
1072
+ result.set(key, value.join(","));
1073
+ } else {
1074
+ result.set(key, value);
1075
+ }
1027
1076
  }
1028
- const ext = extname(file).toLowerCase();
1029
- return supportedExtensions.has(ext);
1077
+ return result;
1030
1078
  }
1031
- function isConfigFile(file) {
1032
- if (file.endsWith(".d.ts")) {
1033
- return false;
1034
- }
1035
- const base = basename(file);
1036
- if (!base.startsWith("index.config.")) {
1037
- return false;
1079
+ async function toRequest(req) {
1080
+ const url = new URL(req.url ?? "/", "http://mokup.local");
1081
+ const method = req.method ?? "GET";
1082
+ const headers = buildHeaders(req.headers);
1083
+ const init = { method, headers };
1084
+ const rawBody = await readRawBody(req);
1085
+ if (rawBody && method !== "GET" && method !== "HEAD") {
1086
+ init.body = rawBody;
1038
1087
  }
1039
- const ext = extname(file).toLowerCase();
1040
- return configExtensions.includes(ext);
1088
+ return new Request(url.toString(), init);
1041
1089
  }
1042
-
1043
- async function loadModule(file) {
1044
- const ext = extname(file).toLowerCase();
1045
- if (ext === ".cjs") {
1046
- const require = createRequire(import.meta.url);
1047
- delete require.cache[file];
1048
- return require(file);
1049
- }
1050
- if (ext === ".js" || ext === ".mjs") {
1051
- return import(`${pathToFileURL(file).href}?t=${Date.now()}`);
1052
- }
1053
- if (ext === ".ts") {
1054
- const result = await build({
1055
- entryPoints: [file],
1056
- bundle: true,
1057
- format: "esm",
1058
- platform: "node",
1059
- sourcemap: "inline",
1060
- target: "es2020",
1061
- write: false
1062
- });
1063
- const output = result.outputFiles[0];
1064
- const code = output?.text ?? "";
1065
- const dataUrl = `data:text/javascript;base64,${Buffer.from(code).toString(
1066
- "base64"
1067
- )}`;
1068
- return import(`${dataUrl}#${Date.now()}`);
1090
+ async function sendResponse(res, response) {
1091
+ res.statusCode = response.status;
1092
+ response.headers.forEach((value, key) => {
1093
+ res.setHeader(key, value);
1094
+ });
1095
+ if (!response.body) {
1096
+ res.end();
1097
+ return;
1069
1098
  }
1070
- return null;
1099
+ const buffer = new Uint8Array(await response.arrayBuffer());
1100
+ res.end(buffer);
1071
1101
  }
1072
- async function loadModuleWithVite(server, file) {
1073
- const asDevServer = server;
1074
- if ("ssrLoadModule" in asDevServer) {
1075
- const moduleNode = asDevServer.moduleGraph.getModuleById(file);
1076
- if (moduleNode) {
1077
- asDevServer.moduleGraph.invalidateModule(moduleNode);
1078
- }
1079
- return asDevServer.ssrLoadModule(file);
1080
- }
1081
- return loadModule(file);
1102
+ function hasMatch(app, method, pathname) {
1103
+ const matchMethod = method === "HEAD" ? "GET" : method;
1104
+ const match = app.router.match(matchMethod, pathname);
1105
+ return !!match && match[0].length > 0;
1082
1106
  }
1083
- async function readJsonFile(file, logger) {
1084
- try {
1085
- const content = await promises.readFile(file, "utf8");
1086
- const errors = [];
1087
- const data = parse(content, errors, {
1088
- allowTrailingComma: true,
1089
- disallowComments: false
1090
- });
1091
- if (errors.length > 0) {
1092
- logger.warn(`Invalid JSONC in ${file}`);
1093
- return void 0;
1107
+ function createMiddleware(getApp, logger) {
1108
+ return async (req, res, next) => {
1109
+ const app = getApp();
1110
+ if (!app) {
1111
+ return next();
1094
1112
  }
1095
- return data;
1096
- } catch (error) {
1097
- logger.warn(`Failed to read ${file}: ${String(error)}`);
1098
- return void 0;
1099
- }
1100
- }
1101
- async function loadRules(file, server, logger) {
1102
- const ext = extname(file).toLowerCase();
1103
- if (ext === ".json" || ext === ".jsonc") {
1104
- const json = await readJsonFile(file, logger);
1105
- if (typeof json === "undefined") {
1106
- return [];
1113
+ const url = req.url ?? "/";
1114
+ const parsedUrl = new URL(url, "http://mokup.local");
1115
+ const pathname = parsedUrl.pathname;
1116
+ const method = normalizeMethod(req.method) ?? "GET";
1117
+ if (!hasMatch(app, method, pathname)) {
1118
+ return next();
1107
1119
  }
1108
- return [
1109
- {
1110
- handler: json
1120
+ const startedAt = Date.now();
1121
+ try {
1122
+ const response = await app.fetch(await toRequest(req));
1123
+ if (res.writableEnded) {
1124
+ return;
1111
1125
  }
1112
- ];
1126
+ await sendResponse(res, response);
1127
+ logger.info(`${method} ${pathname} ${Date.now() - startedAt}ms`);
1128
+ } catch (error) {
1129
+ if (!res.headersSent) {
1130
+ res.statusCode = 500;
1131
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
1132
+ }
1133
+ res.end("Mock handler error");
1134
+ logger.error("Mock handler failed:", error);
1135
+ }
1136
+ };
1137
+ }
1138
+
1139
+ const jsonExtensions = /* @__PURE__ */ new Set([".json", ".jsonc"]);
1140
+ function resolveTemplate(template, prefix) {
1141
+ const normalized = template.startsWith("/") ? template : `/${template}`;
1142
+ if (!prefix) {
1143
+ return normalized;
1113
1144
  }
1114
- const mod = server ? await loadModuleWithVite(server, file) : await loadModule(file);
1115
- const value = mod?.default ?? mod;
1116
- if (!value) {
1117
- return [];
1145
+ const normalizedPrefix = normalizePrefix(prefix);
1146
+ if (!normalizedPrefix) {
1147
+ return normalized;
1118
1148
  }
1119
- if (Array.isArray(value)) {
1120
- return value;
1149
+ if (normalized === normalizedPrefix || normalized.startsWith(`${normalizedPrefix}/`)) {
1150
+ return normalized;
1121
1151
  }
1122
- if (typeof value === "function") {
1123
- return [
1124
- {
1125
- handler: value
1126
- }
1127
- ];
1152
+ if (normalized === "/") {
1153
+ return `${normalizedPrefix}/`;
1128
1154
  }
1129
- return [value];
1155
+ return `${normalizedPrefix}${normalized}`;
1130
1156
  }
1131
-
1132
- const silentLogger = {
1133
- info: () => {
1134
- },
1135
- warn: () => {
1136
- },
1137
- error: () => {
1157
+ function stripMethodSuffix(base) {
1158
+ const segments = base.split(".");
1159
+ const last = segments.at(-1);
1160
+ if (last && methodSuffixSet.has(last.toLowerCase())) {
1161
+ segments.pop();
1162
+ return {
1163
+ name: segments.join("."),
1164
+ method: last.toUpperCase()
1165
+ };
1138
1166
  }
1139
- };
1140
- function resolveSkipRoute(params) {
1141
- const derived = params.derived ?? deriveRouteFromFile(params.file, params.rootDir, silentLogger);
1142
- if (!derived?.method) {
1167
+ return {
1168
+ name: base,
1169
+ method: void 0
1170
+ };
1171
+ }
1172
+ function deriveRouteFromFile(file, rootDir, logger) {
1173
+ const rel = toPosix(relative(rootDir, file));
1174
+ const ext = extname(rel);
1175
+ const withoutExt = rel.slice(0, rel.length - ext.length);
1176
+ const dir = dirname(withoutExt);
1177
+ const base = basename(withoutExt);
1178
+ const { name, method } = stripMethodSuffix(base);
1179
+ const resolvedMethod = method ?? (jsonExtensions.has(ext) ? "GET" : void 0);
1180
+ if (!resolvedMethod) {
1181
+ logger.warn(`Skip mock without method suffix: ${file}`);
1143
1182
  return null;
1144
1183
  }
1145
- const resolved = resolveRule({
1146
- rule: { handler: null },
1147
- derivedTemplate: derived.template,
1148
- derivedMethod: derived.method,
1149
- prefix: params.prefix,
1150
- file: params.file,
1151
- logger: silentLogger
1152
- });
1153
- if (!resolved) {
1184
+ if (!name) {
1185
+ logger.warn(`Skip mock with empty route name: ${file}`);
1186
+ return null;
1187
+ }
1188
+ const joined = dir === "." ? name : join(dir, name);
1189
+ const segments = toPosix(joined).split("/");
1190
+ if (segments.at(-1) === "index") {
1191
+ segments.pop();
1192
+ }
1193
+ const template = segments.length === 0 ? "/" : `/${segments.join("/")}`;
1194
+ const parsed = parseRouteTemplate(template);
1195
+ if (parsed.errors.length > 0) {
1196
+ for (const error of parsed.errors) {
1197
+ logger.warn(`${error} in ${file}`);
1198
+ }
1154
1199
  return null;
1155
1200
  }
1201
+ for (const warning of parsed.warnings) {
1202
+ logger.warn(`${warning} in ${file}`);
1203
+ }
1156
1204
  return {
1157
- method: resolved.method,
1158
- url: resolved.template
1205
+ template: parsed.template,
1206
+ method: resolvedMethod,
1207
+ tokens: parsed.tokens,
1208
+ score: parsed.score
1159
1209
  };
1160
1210
  }
1161
- function buildSkipInfo(file, reason, resolved) {
1162
- const info = { file, reason };
1163
- if (resolved) {
1164
- info.method = resolved.method;
1165
- info.url = resolved.url;
1211
+ function resolveRule(params) {
1212
+ const method = params.derivedMethod;
1213
+ if (!method) {
1214
+ params.logger.warn(`Skip mock without method suffix: ${params.file}`);
1215
+ return null;
1166
1216
  }
1167
- return info;
1217
+ const template = resolveTemplate(params.derivedTemplate, params.prefix);
1218
+ const parsed = parseRouteTemplate(template);
1219
+ if (parsed.errors.length > 0) {
1220
+ for (const error of parsed.errors) {
1221
+ params.logger.warn(`${error} in ${params.file}`);
1222
+ }
1223
+ return null;
1224
+ }
1225
+ for (const warning of parsed.warnings) {
1226
+ params.logger.warn(`${warning} in ${params.file}`);
1227
+ }
1228
+ const route = {
1229
+ file: params.file,
1230
+ template: parsed.template,
1231
+ method,
1232
+ tokens: parsed.tokens,
1233
+ score: parsed.score,
1234
+ handler: params.rule.handler
1235
+ };
1236
+ if (typeof params.rule.status === "number") {
1237
+ route.status = params.rule.status;
1238
+ }
1239
+ if (params.rule.headers) {
1240
+ route.headers = params.rule.headers;
1241
+ }
1242
+ if (typeof params.rule.delay === "number") {
1243
+ route.delay = params.rule.delay;
1244
+ }
1245
+ return route;
1168
1246
  }
1169
- async function scanRoutes(params) {
1170
- const routes = [];
1171
- const seen = /* @__PURE__ */ new Set();
1172
- const files = await collectFiles(params.dirs);
1173
- const globalIgnorePrefix = normalizeIgnorePrefix(params.ignorePrefix);
1174
- const configCache = /* @__PURE__ */ new Map();
1175
- const fileCache = /* @__PURE__ */ new Map();
1176
- const shouldCollectSkip = typeof params.onSkip === "function";
1177
- const shouldCollectIgnore = typeof params.onIgnore === "function";
1178
- const shouldCollectConfig = typeof params.onConfig === "function";
1179
- for (const fileInfo of files) {
1180
- if (isConfigFile(fileInfo.file)) {
1181
- if (shouldCollectConfig) {
1182
- const config2 = await resolveDirectoryConfig({
1183
- file: fileInfo.file,
1184
- rootDir: fileInfo.rootDir,
1185
- server: params.server,
1186
- logger: params.logger,
1187
- configCache,
1188
- fileCache
1189
- });
1190
- params.onConfig?.({ file: fileInfo.file, enabled: config2.enabled !== false });
1191
- }
1192
- continue;
1247
+ function sortRoutes(routes) {
1248
+ return routes.sort((a, b) => {
1249
+ if (a.method !== b.method) {
1250
+ return a.method.localeCompare(b.method);
1193
1251
  }
1194
- const configParams = {
1195
- file: fileInfo.file,
1196
- rootDir: fileInfo.rootDir,
1197
- logger: params.logger,
1198
- configCache,
1199
- fileCache
1200
- };
1201
- if (params.server) {
1202
- configParams.server = params.server;
1252
+ const scoreCompare = compareRouteScore(a.score, b.score);
1253
+ if (scoreCompare !== 0) {
1254
+ return scoreCompare;
1203
1255
  }
1204
- const config = await resolveDirectoryConfig(configParams);
1205
- if (config.enabled === false) {
1206
- if (shouldCollectSkip && isSupportedFile(fileInfo.file)) {
1207
- const resolved = resolveSkipRoute({
1208
- file: fileInfo.file,
1209
- rootDir: fileInfo.rootDir,
1210
- prefix: params.prefix
1211
- });
1212
- params.onSkip?.(buildSkipInfo(fileInfo.file, "disabled-dir", resolved));
1213
- }
1256
+ return a.template.localeCompare(b.template);
1257
+ });
1258
+ }
1259
+
1260
+ async function walkDir(dir, rootDir, files) {
1261
+ const entries = await promises.readdir(dir, { withFileTypes: true });
1262
+ for (const entry of entries) {
1263
+ if (entry.name === "node_modules" || entry.name === ".git") {
1214
1264
  continue;
1215
1265
  }
1216
- const effectiveIgnorePrefix = typeof config.ignorePrefix !== "undefined" ? normalizeIgnorePrefix(config.ignorePrefix, []) : globalIgnorePrefix;
1217
- if (hasIgnoredPrefix(fileInfo.file, fileInfo.rootDir, effectiveIgnorePrefix)) {
1218
- if (shouldCollectSkip && isSupportedFile(fileInfo.file)) {
1219
- const resolved = resolveSkipRoute({
1220
- file: fileInfo.file,
1221
- rootDir: fileInfo.rootDir,
1222
- prefix: params.prefix
1223
- });
1224
- params.onSkip?.(buildSkipInfo(fileInfo.file, "ignore-prefix", resolved));
1225
- }
1226
- continue;
1227
- }
1228
- if (!isSupportedFile(fileInfo.file)) {
1229
- if (shouldCollectIgnore) {
1230
- params.onIgnore?.({ file: fileInfo.file, reason: "unsupported" });
1231
- }
1266
+ const fullPath = join(dir, entry.name);
1267
+ if (entry.isDirectory()) {
1268
+ await walkDir(fullPath, rootDir, files);
1232
1269
  continue;
1233
1270
  }
1234
- const effectiveInclude = typeof config.include !== "undefined" ? config.include : params.include;
1235
- const effectiveExclude = typeof config.exclude !== "undefined" ? config.exclude : params.exclude;
1236
- if (!matchesFilter(fileInfo.file, effectiveInclude, effectiveExclude)) {
1237
- if (shouldCollectSkip) {
1238
- const resolved = resolveSkipRoute({
1239
- file: fileInfo.file,
1240
- rootDir: fileInfo.rootDir,
1241
- prefix: params.prefix
1242
- });
1243
- const reason = effectiveExclude && matchesFilter(fileInfo.file, void 0, effectiveExclude) ? "exclude" : "include";
1244
- params.onSkip?.(buildSkipInfo(fileInfo.file, reason, resolved));
1245
- }
1246
- continue;
1271
+ if (entry.isFile()) {
1272
+ files.push({ file: fullPath, rootDir });
1247
1273
  }
1248
- const derived = deriveRouteFromFile(fileInfo.file, fileInfo.rootDir, params.logger);
1249
- if (!derived) {
1250
- if (shouldCollectIgnore) {
1251
- params.onIgnore?.({ file: fileInfo.file, reason: "invalid-route" });
1252
- }
1274
+ }
1275
+ }
1276
+ async function exists(path) {
1277
+ try {
1278
+ await promises.stat(path);
1279
+ return true;
1280
+ } catch {
1281
+ return false;
1282
+ }
1283
+ }
1284
+ async function collectFiles(dirs) {
1285
+ const files = [];
1286
+ for (const dir of dirs) {
1287
+ if (!await exists(dir)) {
1253
1288
  continue;
1254
1289
  }
1255
- const rules = await loadRules(fileInfo.file, params.server, params.logger);
1256
- for (const [index, rule] of rules.entries()) {
1257
- if (!rule || typeof rule !== "object") {
1258
- continue;
1259
- }
1260
- if (rule.enabled === false) {
1261
- if (shouldCollectSkip) {
1262
- const resolved2 = resolveSkipRoute({
1263
- file: fileInfo.file,
1264
- rootDir: fileInfo.rootDir,
1265
- prefix: params.prefix,
1266
- derived
1267
- });
1268
- params.onSkip?.(buildSkipInfo(fileInfo.file, "disabled", resolved2));
1269
- }
1270
- continue;
1271
- }
1272
- const ruleValue = rule;
1273
- const unsupportedKeys = ["response", "url", "method"].filter(
1274
- (key2) => key2 in ruleValue
1275
- );
1276
- if (unsupportedKeys.length > 0) {
1277
- params.logger.warn(
1278
- `Skip mock with unsupported fields (${unsupportedKeys.join(", ")}): ${fileInfo.file}`
1279
- );
1280
- continue;
1281
- }
1282
- if (typeof rule.handler === "undefined") {
1283
- params.logger.warn(`Skip mock without handler: ${fileInfo.file}`);
1284
- continue;
1285
- }
1286
- const resolved = resolveRule({
1287
- rule,
1288
- derivedTemplate: derived.template,
1289
- derivedMethod: derived.method,
1290
- prefix: params.prefix,
1291
- file: fileInfo.file,
1292
- logger: params.logger
1293
- });
1294
- if (!resolved) {
1295
- continue;
1296
- }
1297
- resolved.ruleIndex = index;
1298
- if (config.headers) {
1299
- resolved.headers = { ...config.headers, ...resolved.headers ?? {} };
1300
- }
1301
- if (typeof resolved.status === "undefined" && typeof config.status === "number") {
1302
- resolved.status = config.status;
1303
- }
1304
- if (typeof resolved.delay === "undefined" && typeof config.delay === "number") {
1305
- resolved.delay = config.delay;
1306
- }
1307
- if (config.middlewares.length > 0) {
1308
- resolved.middlewares = config.middlewares;
1309
- }
1310
- const key = `${resolved.method} ${resolved.template}`;
1311
- if (seen.has(key)) {
1312
- params.logger.warn(`Duplicate mock route ${key} from ${fileInfo.file}`);
1313
- }
1314
- seen.add(key);
1315
- routes.push(resolved);
1316
- }
1290
+ await walkDir(dir, dir, files);
1317
1291
  }
1318
- return sortRoutes(routes);
1292
+ return files;
1319
1293
  }
1320
-
1321
- const defaultSwPath = "/mokup-sw.js";
1322
- const defaultSwScope = "/";
1323
- function normalizeSwPath(path) {
1324
- if (!path) {
1325
- return defaultSwPath;
1294
+ function isSupportedFile(file) {
1295
+ if (file.endsWith(".d.ts")) {
1296
+ return false;
1326
1297
  }
1327
- return path.startsWith("/") ? path : `/${path}`;
1328
- }
1329
- function normalizeSwScope(scope) {
1330
- if (!scope) {
1331
- return defaultSwScope;
1298
+ if (isConfigFile(file)) {
1299
+ return false;
1332
1300
  }
1333
- return scope.startsWith("/") ? scope : `/${scope}`;
1301
+ const ext = extname(file).toLowerCase();
1302
+ return supportedExtensions.has(ext);
1334
1303
  }
1335
- function normalizeBasePath(value) {
1336
- if (!value) {
1337
- return "/";
1304
+ function isConfigFile(file) {
1305
+ if (file.endsWith(".d.ts")) {
1306
+ return false;
1338
1307
  }
1339
- const normalized = value.startsWith("/") ? value : `/${value}`;
1340
- if (normalized.length > 1 && normalized.endsWith("/")) {
1341
- return normalized.slice(0, -1);
1308
+ const base = basename(file);
1309
+ if (!base.startsWith("index.config.")) {
1310
+ return false;
1342
1311
  }
1343
- return normalized;
1312
+ const ext = extname(file).toLowerCase();
1313
+ return configExtensions.includes(ext);
1344
1314
  }
1345
- function resolveSwConfigFromEntries(entries, logger) {
1346
- let path = defaultSwPath;
1347
- let scope = defaultSwScope;
1348
- let register = true;
1349
- let unregister = false;
1350
- const basePaths = [];
1351
- let hasPath = false;
1352
- let hasScope = false;
1353
- let hasRegister = false;
1354
- let hasUnregister = false;
1355
- for (const entry of entries) {
1356
- const config = entry.sw;
1357
- if (config?.path) {
1358
- const next = normalizeSwPath(config.path);
1359
- if (!hasPath) {
1360
- path = next;
1361
- hasPath = true;
1362
- } else if (path !== next) {
1363
- logger.warn(`SW path "${next}" ignored; using "${path}".`);
1364
- }
1365
- }
1366
- if (config?.scope) {
1367
- const next = normalizeSwScope(config.scope);
1368
- if (!hasScope) {
1369
- scope = next;
1370
- hasScope = true;
1371
- } else if (scope !== next) {
1372
- logger.warn(`SW scope "${next}" ignored; using "${scope}".`);
1373
- }
1374
- }
1375
- if (typeof config?.register === "boolean") {
1376
- if (!hasRegister) {
1377
- register = config.register;
1378
- hasRegister = true;
1379
- } else if (register !== config.register) {
1380
- logger.warn(
1381
- `SW register="${String(config.register)}" ignored; using "${String(register)}".`
1382
- );
1383
- }
1384
- }
1385
- if (typeof config?.unregister === "boolean") {
1386
- if (!hasUnregister) {
1387
- unregister = config.unregister;
1388
- hasUnregister = true;
1389
- } else if (unregister !== config.unregister) {
1390
- logger.warn(
1391
- `SW unregister="${String(config.unregister)}" ignored; using "${String(unregister)}".`
1392
- );
1315
+
1316
+ const sourceRoot = dirname(fileURLToPath(import.meta.url));
1317
+ const mokupSourceEntry = resolve(sourceRoot, "../index.ts");
1318
+ const mokupViteSourceEntry = resolve(sourceRoot, "../vite.ts");
1319
+ const hasMokupSourceEntry = existsSync(mokupSourceEntry);
1320
+ const hasMokupViteSourceEntry = existsSync(mokupViteSourceEntry);
1321
+ function createWorkspaceResolvePlugin() {
1322
+ if (!hasMokupSourceEntry && !hasMokupViteSourceEntry) {
1323
+ return null;
1324
+ }
1325
+ return {
1326
+ name: "mokup:resolve-workspace",
1327
+ setup(build) {
1328
+ if (hasMokupSourceEntry) {
1329
+ build.onResolve({ filter: /^mokup$/ }, () => ({ path: mokupSourceEntry }));
1393
1330
  }
1394
- }
1395
- if (typeof config?.basePath !== "undefined") {
1396
- const values = Array.isArray(config.basePath) ? config.basePath : [config.basePath];
1397
- for (const value of values) {
1398
- basePaths.push(normalizeBasePath(value));
1331
+ if (hasMokupViteSourceEntry) {
1332
+ build.onResolve({ filter: /^mokup\/vite$/ }, () => ({ path: mokupViteSourceEntry }));
1399
1333
  }
1400
- continue;
1401
- }
1402
- const normalizedPrefix = normalizePrefix(entry.prefix ?? "");
1403
- if (normalizedPrefix) {
1404
- basePaths.push(normalizedPrefix);
1405
1334
  }
1406
- }
1407
- return {
1408
- path,
1409
- scope,
1410
- register,
1411
- unregister,
1412
- basePaths: Array.from(new Set(basePaths))
1413
1335
  };
1414
1336
  }
1415
- function resolveSwConfig(options, logger) {
1416
- const swEntries = options.filter((entry) => entry.mode === "sw");
1417
- if (swEntries.length === 0) {
1418
- return null;
1419
- }
1420
- return resolveSwConfigFromEntries(swEntries, logger);
1421
- }
1422
- function resolveSwUnregisterConfig(options, logger) {
1423
- return resolveSwConfigFromEntries(options, logger);
1424
- }
1425
- function toViteImportPath(file, root) {
1426
- const absolute = isAbsolute(file) ? file : resolve(root, file);
1427
- const rel = relative(root, absolute);
1428
- if (!rel.startsWith("..") && !isAbsolute(rel)) {
1429
- return `/${toPosix(rel)}`;
1337
+ const workspaceResolvePlugin = createWorkspaceResolvePlugin();
1338
+ async function loadModule(file) {
1339
+ const ext = extname(file).toLowerCase();
1340
+ if (ext === ".cjs") {
1341
+ const require = createRequire(import.meta.url);
1342
+ delete require.cache[file];
1343
+ return require(file);
1430
1344
  }
1431
- return `/@fs/${toPosix(absolute)}`;
1432
- }
1433
- function shouldModuleize(handler) {
1434
- if (typeof handler === "function") {
1435
- return true;
1345
+ if (ext === ".js" || ext === ".mjs") {
1346
+ return import(`${pathToFileURL(file).href}?t=${Date.now()}`);
1436
1347
  }
1437
- if (typeof Response !== "undefined" && handler instanceof Response) {
1438
- return true;
1348
+ if (ext === ".ts") {
1349
+ const result = await build({
1350
+ entryPoints: [file],
1351
+ bundle: true,
1352
+ format: "esm",
1353
+ platform: "node",
1354
+ sourcemap: "inline",
1355
+ target: "es2020",
1356
+ write: false,
1357
+ ...workspaceResolvePlugin ? { plugins: [workspaceResolvePlugin] } : {}
1358
+ });
1359
+ const output = result.outputFiles[0];
1360
+ const code = output?.text ?? "";
1361
+ const dataUrl = `data:text/javascript;base64,${Buffer.from(code).toString(
1362
+ "base64"
1363
+ )}`;
1364
+ return import(`${dataUrl}#${Date.now()}`);
1439
1365
  }
1440
- return false;
1366
+ return null;
1441
1367
  }
1442
- function toBinaryBody(handler) {
1443
- if (handler instanceof ArrayBuffer) {
1444
- return Buffer.from(new Uint8Array(handler)).toString("base64");
1445
- }
1446
- if (handler instanceof Uint8Array) {
1447
- return Buffer.from(handler).toString("base64");
1368
+ async function loadModuleWithVite(server, file) {
1369
+ const asDevServer = server;
1370
+ if ("ssrLoadModule" in asDevServer) {
1371
+ const moduleNode = asDevServer.moduleGraph.getModuleById(file);
1372
+ if (moduleNode) {
1373
+ asDevServer.moduleGraph.invalidateModule(moduleNode);
1374
+ }
1375
+ return asDevServer.ssrLoadModule(file);
1376
+ }
1377
+ return loadModule(file);
1378
+ }
1379
+
1380
+ const middlewareSymbol = Symbol.for("mokup.config.middlewares");
1381
+ function getConfigFileCandidates(dir) {
1382
+ return configExtensions.map((extension) => join(dir, `index.config${extension}`));
1383
+ }
1384
+ async function findConfigFile(dir, cache) {
1385
+ const cached = cache.get(dir);
1386
+ if (cached !== void 0) {
1387
+ return cached;
1448
1388
  }
1449
- if (Buffer.isBuffer(handler)) {
1450
- return handler.toString("base64");
1389
+ for (const candidate of getConfigFileCandidates(dir)) {
1390
+ try {
1391
+ await promises.stat(candidate);
1392
+ cache.set(dir, candidate);
1393
+ return candidate;
1394
+ } catch {
1395
+ continue;
1396
+ }
1451
1397
  }
1398
+ cache.set(dir, null);
1452
1399
  return null;
1453
1400
  }
1454
- function buildManifestResponse(route, moduleId) {
1455
- if (moduleId) {
1456
- const response = {
1457
- type: "module",
1458
- module: moduleId
1459
- };
1460
- if (typeof route.ruleIndex === "number") {
1461
- response.ruleIndex = route.ruleIndex;
1462
- }
1463
- return response;
1401
+ async function loadConfig(file, server) {
1402
+ const mod = server ? await loadModuleWithVite(server, file) : await loadModule(file);
1403
+ if (!mod) {
1404
+ return null;
1464
1405
  }
1465
- const handler = route.handler;
1466
- if (typeof handler === "string") {
1467
- return {
1468
- type: "text",
1469
- body: handler
1470
- };
1406
+ const value = mod?.default ?? mod;
1407
+ if (!value || typeof value !== "object") {
1408
+ return null;
1471
1409
  }
1472
- const binary = toBinaryBody(handler);
1473
- if (binary) {
1474
- return {
1475
- type: "binary",
1476
- body: binary,
1477
- encoding: "base64"
1478
- };
1410
+ return value;
1411
+ }
1412
+ function normalizeMiddlewares(value, source, logger, position) {
1413
+ if (!value) {
1414
+ return [];
1415
+ }
1416
+ const list = Array.isArray(value) ? value : [value];
1417
+ const middlewares = [];
1418
+ for (const [index, entry] of list.entries()) {
1419
+ if (typeof entry !== "function") {
1420
+ logger.warn(`Invalid middleware in ${source}`);
1421
+ continue;
1422
+ }
1423
+ middlewares.push({
1424
+ handle: entry,
1425
+ source,
1426
+ index,
1427
+ position
1428
+ });
1429
+ }
1430
+ return middlewares;
1431
+ }
1432
+ function readMiddlewareMeta(config) {
1433
+ const value = config[middlewareSymbol];
1434
+ if (!value || typeof value !== "object") {
1435
+ return null;
1479
1436
  }
1437
+ const meta = value;
1480
1438
  return {
1481
- type: "json",
1482
- body: handler
1439
+ pre: Array.isArray(meta.pre) ? meta.pre : [],
1440
+ normal: Array.isArray(meta.normal) ? meta.normal : [],
1441
+ post: Array.isArray(meta.post) ? meta.post : []
1483
1442
  };
1484
1443
  }
1485
- function buildSwScript(params) {
1486
- const { routes, root } = params;
1487
- const runtimeImportPath = params.runtimeImportPath ?? "mokup/runtime";
1488
- const basePaths = params.basePaths ?? [];
1489
- const resolveModulePath = params.resolveModulePath ?? toViteImportPath;
1490
- const ruleModules = /* @__PURE__ */ new Map();
1491
- const middlewareModules = /* @__PURE__ */ new Map();
1492
- const manifestRoutes = routes.map((route) => {
1493
- const moduleId = shouldModuleize(route.handler) ? resolveModulePath(route.file, root) : null;
1494
- if (moduleId) {
1495
- ruleModules.set(moduleId, moduleId);
1444
+ async function resolveDirectoryConfig(params) {
1445
+ const { file, rootDir, server, logger, configCache, fileCache } = params;
1446
+ const resolvedRoot = normalize(rootDir);
1447
+ const resolvedFileDir = normalize(dirname(file));
1448
+ const chain = [];
1449
+ let current = resolvedFileDir;
1450
+ while (true) {
1451
+ chain.push(current);
1452
+ if (current === resolvedRoot) {
1453
+ break;
1496
1454
  }
1497
- const middleware = route.middlewares?.map((entry) => {
1498
- const modulePath = resolveModulePath(entry.source, root);
1499
- middlewareModules.set(modulePath, modulePath);
1500
- return {
1501
- module: modulePath,
1502
- ruleIndex: entry.index
1503
- };
1504
- });
1505
- const response = buildManifestResponse(route, moduleId);
1506
- const manifestRoute = {
1507
- method: route.method,
1508
- url: route.template,
1509
- ...route.tokens ? { tokens: route.tokens } : {},
1510
- ...route.score ? { score: route.score } : {},
1511
- ...route.status ? { status: route.status } : {},
1512
- ...route.headers ? { headers: route.headers } : {},
1513
- ...route.delay ? { delay: route.delay } : {},
1514
- ...middleware && middleware.length > 0 ? { middleware } : {},
1515
- response
1516
- };
1517
- return manifestRoute;
1518
- });
1519
- const manifest = {
1520
- version: 1,
1521
- routes: manifestRoutes
1522
- };
1523
- const imports = [
1524
- `import { createRuntimeApp, handle } from ${JSON.stringify(runtimeImportPath)}`
1525
- ];
1526
- const moduleEntries = [];
1527
- let moduleIndex = 0;
1528
- for (const id of ruleModules.keys()) {
1529
- const name = `module${moduleIndex++}`;
1530
- imports.push(`import * as ${name} from '${id}'`);
1531
- moduleEntries.push({ id, name, kind: "rule" });
1532
- }
1533
- for (const id of middlewareModules.keys()) {
1534
- const name = `module${moduleIndex++}`;
1535
- imports.push(`import * as ${name} from '${id}'`);
1536
- moduleEntries.push({ id, name, kind: "middleware" });
1455
+ const parent = dirname(current);
1456
+ if (parent === current) {
1457
+ break;
1458
+ }
1459
+ current = parent;
1537
1460
  }
1538
- const lines = [];
1539
- lines.push(...imports, "");
1540
- lines.push(
1541
- "const resolveModuleExport = (mod) => mod?.default ?? mod",
1542
- "",
1543
- "const toRuntimeRule = (value) => {",
1544
- " if (typeof value === 'undefined') {",
1545
- " return null",
1546
- " }",
1547
- " if (typeof value === 'function') {",
1548
- " return { response: value }",
1549
- " }",
1550
- " if (value === null) {",
1551
- " return { response: null }",
1552
- " }",
1553
- " if (typeof value === 'object') {",
1554
- " if ('response' in value) {",
1555
- " return value",
1556
- " }",
1557
- " if ('handler' in value) {",
1558
- " const handlerRule = value",
1559
- " return {",
1560
- " response: handlerRule.handler,",
1561
- " ...(typeof handlerRule.status === 'number' ? { status: handlerRule.status } : {}),",
1562
- " ...(handlerRule.headers ? { headers: handlerRule.headers } : {}),",
1563
- " ...(typeof handlerRule.delay === 'number' ? { delay: handlerRule.delay } : {}),",
1564
- " }",
1565
- " }",
1566
- " return { response: value }",
1567
- " }",
1568
- " return { response: value }",
1569
- "}",
1570
- "",
1571
- "const toRuntimeRules = (value) => {",
1572
- " if (typeof value === 'undefined') {",
1573
- " return []",
1574
- " }",
1575
- " if (Array.isArray(value)) {",
1576
- " return value.map(toRuntimeRule).filter(Boolean)",
1577
- " }",
1578
- " const rule = toRuntimeRule(value)",
1579
- " return rule ? [rule] : []",
1580
- "}",
1581
- ""
1582
- );
1583
- lines.push(
1584
- `const manifest = ${JSON.stringify(manifest, null, 2)}`,
1585
- ""
1586
- );
1587
- if (moduleEntries.length > 0) {
1588
- lines.push("const moduleMap = {");
1589
- for (const entry of moduleEntries) {
1590
- if (entry.kind === "rule") {
1591
- lines.push(
1592
- ` ${JSON.stringify(entry.id)}: { default: toRuntimeRules(resolveModuleExport(${entry.name})) },`
1593
- );
1594
- continue;
1595
- }
1596
- lines.push(
1597
- ` ${JSON.stringify(entry.id)}: ${entry.name},`
1598
- );
1461
+ chain.reverse();
1462
+ const merged = {};
1463
+ const preMiddlewares = [];
1464
+ const normalMiddlewares = [];
1465
+ const postMiddlewares = [];
1466
+ const configChain = [];
1467
+ const configSources = {};
1468
+ for (const dir of chain) {
1469
+ const configPath = await findConfigFile(dir, fileCache);
1470
+ if (!configPath) {
1471
+ continue;
1472
+ }
1473
+ let config = configCache.get(configPath);
1474
+ if (config === void 0) {
1475
+ config = await loadConfig(configPath, server);
1476
+ configCache.set(configPath, config);
1477
+ }
1478
+ if (!config) {
1479
+ logger.warn(`Invalid config in ${configPath}`);
1480
+ continue;
1481
+ }
1482
+ configChain.push(configPath);
1483
+ if (config.headers) {
1484
+ merged.headers = { ...merged.headers ?? {}, ...config.headers };
1485
+ configSources.headers = configPath;
1486
+ }
1487
+ if (typeof config.status === "number") {
1488
+ merged.status = config.status;
1489
+ configSources.status = configPath;
1490
+ }
1491
+ if (typeof config.delay === "number") {
1492
+ merged.delay = config.delay;
1493
+ configSources.delay = configPath;
1494
+ }
1495
+ if (typeof config.enabled === "boolean") {
1496
+ merged.enabled = config.enabled;
1497
+ configSources.enabled = configPath;
1498
+ }
1499
+ if (typeof config.ignorePrefix !== "undefined") {
1500
+ merged.ignorePrefix = config.ignorePrefix;
1501
+ configSources.ignorePrefix = configPath;
1502
+ }
1503
+ if (typeof config.include !== "undefined") {
1504
+ merged.include = config.include;
1505
+ configSources.include = configPath;
1506
+ }
1507
+ if (typeof config.exclude !== "undefined") {
1508
+ merged.exclude = config.exclude;
1509
+ configSources.exclude = configPath;
1510
+ }
1511
+ const meta = readMiddlewareMeta(config);
1512
+ const normalizedPre = normalizeMiddlewares(
1513
+ meta?.pre,
1514
+ configPath,
1515
+ logger,
1516
+ "pre"
1517
+ );
1518
+ const normalizedNormal = normalizeMiddlewares(
1519
+ meta?.normal,
1520
+ configPath,
1521
+ logger,
1522
+ "normal"
1523
+ );
1524
+ const normalizedLegacy = normalizeMiddlewares(
1525
+ config.middleware,
1526
+ configPath,
1527
+ logger,
1528
+ "normal"
1529
+ );
1530
+ const normalizedPost = normalizeMiddlewares(
1531
+ meta?.post,
1532
+ configPath,
1533
+ logger,
1534
+ "post"
1535
+ );
1536
+ if (normalizedPre.length > 0) {
1537
+ preMiddlewares.push(...normalizedPre);
1538
+ }
1539
+ if (normalizedNormal.length > 0) {
1540
+ normalMiddlewares.push(...normalizedNormal);
1541
+ }
1542
+ if (normalizedLegacy.length > 0) {
1543
+ normalMiddlewares.push(...normalizedLegacy);
1544
+ }
1545
+ if (normalizedPost.length > 0) {
1546
+ postMiddlewares.push(...normalizedPost);
1599
1547
  }
1600
- lines.push("}", "");
1601
1548
  }
1602
- const runtimeOptions = moduleEntries.length > 0 ? "{ manifest, moduleMap }" : "{ manifest }";
1603
- lines.push(
1604
- `const basePaths = ${JSON.stringify(basePaths)}`,
1605
- "",
1606
- "self.addEventListener('install', () => {",
1607
- " self.skipWaiting()",
1608
- "})",
1609
- "",
1610
- "self.addEventListener('activate', (event) => {",
1611
- " event.waitUntil(self.clients.claim())",
1612
- "})",
1613
- "",
1614
- "const shouldHandle = (request) => {",
1615
- " if (!basePaths || basePaths.length === 0) {",
1616
- " return true",
1617
- " }",
1618
- " const pathname = new URL(request.url).pathname",
1619
- " return basePaths.some((basePath) => {",
1620
- " if (basePath === '/') {",
1621
- " return true",
1622
- " }",
1623
- " return pathname === basePath || pathname.startsWith(basePath + '/')",
1624
- " })",
1625
- "}",
1626
- "",
1627
- "const registerHandler = async () => {",
1628
- ` const app = await createRuntimeApp(${runtimeOptions})`,
1629
- " const handler = handle(app)",
1630
- " self.addEventListener('fetch', (event) => {",
1631
- " if (!shouldHandle(event.request)) {",
1632
- " return",
1633
- " }",
1634
- " handler(event)",
1635
- " })",
1636
- "}",
1637
- "",
1638
- "registerHandler().catch((error) => {",
1639
- " console.error('[mokup] Failed to build service worker app:', error)",
1640
- "})",
1641
- ""
1642
- );
1643
- return lines.join("\n");
1549
+ return {
1550
+ ...merged,
1551
+ middlewares: [...preMiddlewares, ...normalMiddlewares, ...postMiddlewares],
1552
+ configChain,
1553
+ configSources
1554
+ };
1555
+ }
1556
+
1557
+ async function readJsonFile(file, logger) {
1558
+ try {
1559
+ const content = await promises.readFile(file, "utf8");
1560
+ const errors = [];
1561
+ const data = parse(content, errors, {
1562
+ allowTrailingComma: true,
1563
+ disallowComments: false
1564
+ });
1565
+ if (errors.length > 0) {
1566
+ logger.warn(`Invalid JSONC in ${file}`);
1567
+ return void 0;
1568
+ }
1569
+ return data;
1570
+ } catch (error) {
1571
+ logger.warn(`Failed to read ${file}: ${String(error)}`);
1572
+ return void 0;
1573
+ }
1574
+ }
1575
+ async function loadRules(file, server, logger) {
1576
+ const ext = extname(file).toLowerCase();
1577
+ if (ext === ".json" || ext === ".jsonc") {
1578
+ const json = await readJsonFile(file, logger);
1579
+ if (typeof json === "undefined") {
1580
+ return [];
1581
+ }
1582
+ return [
1583
+ {
1584
+ handler: json
1585
+ }
1586
+ ];
1587
+ }
1588
+ const mod = server ? await loadModuleWithVite(server, file) : await loadModule(file);
1589
+ const value = mod?.default ?? mod;
1590
+ if (!value) {
1591
+ return [];
1592
+ }
1593
+ if (Array.isArray(value)) {
1594
+ return value;
1595
+ }
1596
+ if (typeof value === "function") {
1597
+ return [
1598
+ {
1599
+ handler: value
1600
+ }
1601
+ ];
1602
+ }
1603
+ return [value];
1604
+ }
1605
+
1606
+ const silentLogger = {
1607
+ info: () => {
1608
+ },
1609
+ warn: () => {
1610
+ },
1611
+ error: () => {
1612
+ },
1613
+ log: () => {
1614
+ }
1615
+ };
1616
+ function resolveSkipRoute(params) {
1617
+ const derived = params.derived ?? deriveRouteFromFile(params.file, params.rootDir, silentLogger);
1618
+ if (!derived?.method) {
1619
+ return null;
1620
+ }
1621
+ const resolved = resolveRule({
1622
+ rule: { handler: null },
1623
+ derivedTemplate: derived.template,
1624
+ derivedMethod: derived.method,
1625
+ prefix: params.prefix,
1626
+ file: params.file,
1627
+ logger: silentLogger
1628
+ });
1629
+ if (!resolved) {
1630
+ return null;
1631
+ }
1632
+ return {
1633
+ method: resolved.method,
1634
+ url: resolved.template
1635
+ };
1636
+ }
1637
+ function buildSkipInfo(file, reason, resolved, configChain, decisionChain, effectiveConfig) {
1638
+ const info = { file, reason };
1639
+ if (resolved) {
1640
+ info.method = resolved.method;
1641
+ info.url = resolved.url;
1642
+ }
1643
+ if (configChain && configChain.length > 0) {
1644
+ info.configChain = configChain;
1645
+ }
1646
+ if (decisionChain && decisionChain.length > 0) {
1647
+ info.decisionChain = decisionChain;
1648
+ }
1649
+ if (effectiveConfig && Object.keys(effectiveConfig).length > 0) {
1650
+ info.effectiveConfig = effectiveConfig;
1651
+ }
1652
+ return info;
1653
+ }
1654
+ function toFilterStrings(value) {
1655
+ if (!value) {
1656
+ return [];
1657
+ }
1658
+ const list = Array.isArray(value) ? value : [value];
1659
+ return list.filter((entry) => entry instanceof RegExp).map((entry) => entry.toString());
1660
+ }
1661
+ function toStringList(value) {
1662
+ return value.length === 1 ? value[0] : [...value];
1663
+ }
1664
+ function formatList(value) {
1665
+ return value.join(", ");
1666
+ }
1667
+ function testPatterns(patterns, value) {
1668
+ const list = Array.isArray(patterns) ? patterns : [patterns];
1669
+ return list.some((pattern) => pattern.test(value));
1670
+ }
1671
+ function buildEffectiveConfig(params) {
1672
+ const { config, effectiveInclude, effectiveExclude, effectiveIgnorePrefix } = params;
1673
+ const includeList = toFilterStrings(effectiveInclude);
1674
+ const excludeList = toFilterStrings(effectiveExclude);
1675
+ const effectiveConfig = {};
1676
+ if (config.headers && Object.keys(config.headers).length > 0) {
1677
+ effectiveConfig.headers = config.headers;
1678
+ }
1679
+ if (typeof config.status === "number") {
1680
+ effectiveConfig.status = config.status;
1681
+ }
1682
+ if (typeof config.delay === "number") {
1683
+ effectiveConfig.delay = config.delay;
1684
+ }
1685
+ if (typeof config.enabled !== "undefined") {
1686
+ effectiveConfig.enabled = config.enabled;
1687
+ }
1688
+ if (effectiveIgnorePrefix.length > 0) {
1689
+ effectiveConfig.ignorePrefix = toStringList(effectiveIgnorePrefix);
1690
+ }
1691
+ if (includeList.length > 0) {
1692
+ effectiveConfig.include = toStringList(includeList);
1693
+ }
1694
+ if (excludeList.length > 0) {
1695
+ effectiveConfig.exclude = toStringList(excludeList);
1696
+ }
1697
+ return effectiveConfig;
1698
+ }
1699
+
1700
+ function pushDecisionStep(chain, entry) {
1701
+ const step = {
1702
+ step: entry.step,
1703
+ result: entry.result
1704
+ };
1705
+ if (typeof entry.source !== "undefined") {
1706
+ step.source = entry.source;
1707
+ }
1708
+ if (typeof entry.detail !== "undefined") {
1709
+ step.detail = entry.detail;
1710
+ }
1711
+ chain.push(step);
1712
+ }
1713
+ function runRoutePrechecks(params) {
1714
+ const {
1715
+ fileInfo,
1716
+ prefix,
1717
+ config,
1718
+ configChain,
1719
+ globalIgnorePrefix,
1720
+ include,
1721
+ exclude,
1722
+ shouldCollectSkip,
1723
+ shouldCollectIgnore,
1724
+ onSkip,
1725
+ onIgnore
1726
+ } = params;
1727
+ const configSources = config.configSources ?? {};
1728
+ const decisionChain = [];
1729
+ const isConfigEnabled = config.enabled !== false;
1730
+ pushDecisionStep(decisionChain, {
1731
+ step: "config.enabled",
1732
+ result: isConfigEnabled ? "pass" : "fail",
1733
+ source: configSources.enabled,
1734
+ detail: config.enabled === false ? "enabled=false" : typeof config.enabled === "boolean" ? "enabled=true" : "enabled=true (default)"
1735
+ });
1736
+ const effectiveIgnorePrefix = typeof config.ignorePrefix !== "undefined" ? normalizeIgnorePrefix(config.ignorePrefix, []) : globalIgnorePrefix;
1737
+ const effectiveInclude = typeof config.include !== "undefined" ? config.include : include;
1738
+ const effectiveExclude = typeof config.exclude !== "undefined" ? config.exclude : exclude;
1739
+ const effectiveConfigParams = {
1740
+ config,
1741
+ effectiveIgnorePrefix
1742
+ };
1743
+ if (typeof effectiveInclude !== "undefined") {
1744
+ effectiveConfigParams.effectiveInclude = effectiveInclude;
1745
+ }
1746
+ if (typeof effectiveExclude !== "undefined") {
1747
+ effectiveConfigParams.effectiveExclude = effectiveExclude;
1748
+ }
1749
+ const effectiveConfig = buildEffectiveConfig(effectiveConfigParams);
1750
+ const effectiveConfigValue = Object.keys(effectiveConfig).length > 0 ? effectiveConfig : void 0;
1751
+ if (!isConfigEnabled) {
1752
+ if (shouldCollectSkip && isSupportedFile(fileInfo.file)) {
1753
+ const resolved = resolveSkipRoute({
1754
+ file: fileInfo.file,
1755
+ rootDir: fileInfo.rootDir,
1756
+ prefix
1757
+ });
1758
+ onSkip?.(buildSkipInfo(
1759
+ fileInfo.file,
1760
+ "disabled-dir",
1761
+ resolved,
1762
+ configChain,
1763
+ decisionChain,
1764
+ effectiveConfigValue
1765
+ ));
1766
+ }
1767
+ return null;
1768
+ }
1769
+ if (effectiveIgnorePrefix.length > 0) {
1770
+ const ignoredByPrefix = hasIgnoredPrefix(fileInfo.file, fileInfo.rootDir, effectiveIgnorePrefix);
1771
+ pushDecisionStep(decisionChain, {
1772
+ step: "ignore-prefix",
1773
+ result: ignoredByPrefix ? "fail" : "pass",
1774
+ source: configSources.ignorePrefix,
1775
+ detail: `prefixes: ${formatList(effectiveIgnorePrefix)}`
1776
+ });
1777
+ if (ignoredByPrefix) {
1778
+ if (shouldCollectSkip && isSupportedFile(fileInfo.file)) {
1779
+ const resolved = resolveSkipRoute({
1780
+ file: fileInfo.file,
1781
+ rootDir: fileInfo.rootDir,
1782
+ prefix
1783
+ });
1784
+ onSkip?.(buildSkipInfo(
1785
+ fileInfo.file,
1786
+ "ignore-prefix",
1787
+ resolved,
1788
+ configChain,
1789
+ decisionChain,
1790
+ effectiveConfigValue
1791
+ ));
1792
+ }
1793
+ return null;
1794
+ }
1795
+ }
1796
+ const supportedFile = isSupportedFile(fileInfo.file);
1797
+ pushDecisionStep(decisionChain, {
1798
+ step: "file.supported",
1799
+ result: supportedFile ? "pass" : "fail",
1800
+ detail: supportedFile ? void 0 : "unsupported file type"
1801
+ });
1802
+ if (!supportedFile) {
1803
+ if (shouldCollectIgnore) {
1804
+ const ignoreInfo = {
1805
+ file: fileInfo.file,
1806
+ reason: "unsupported",
1807
+ configChain,
1808
+ decisionChain
1809
+ };
1810
+ if (effectiveConfigValue) {
1811
+ ignoreInfo.effectiveConfig = effectiveConfigValue;
1812
+ }
1813
+ onIgnore?.(ignoreInfo);
1814
+ }
1815
+ return null;
1816
+ }
1817
+ const normalizedFile = toPosix(fileInfo.file);
1818
+ if (typeof effectiveExclude !== "undefined") {
1819
+ const excluded = testPatterns(effectiveExclude, normalizedFile);
1820
+ const patterns = toFilterStrings(effectiveExclude);
1821
+ pushDecisionStep(decisionChain, {
1822
+ step: "filter.exclude",
1823
+ result: excluded ? "fail" : "pass",
1824
+ source: configSources.exclude,
1825
+ detail: patterns.length > 0 ? `${excluded ? "matched" : "no match"}: ${patterns.join(", ")}` : void 0
1826
+ });
1827
+ if (excluded) {
1828
+ if (shouldCollectSkip) {
1829
+ const resolved = resolveSkipRoute({
1830
+ file: fileInfo.file,
1831
+ rootDir: fileInfo.rootDir,
1832
+ prefix
1833
+ });
1834
+ onSkip?.(buildSkipInfo(
1835
+ fileInfo.file,
1836
+ "exclude",
1837
+ resolved,
1838
+ configChain,
1839
+ decisionChain,
1840
+ effectiveConfigValue
1841
+ ));
1842
+ }
1843
+ return null;
1844
+ }
1845
+ }
1846
+ if (typeof effectiveInclude !== "undefined") {
1847
+ const included = testPatterns(effectiveInclude, normalizedFile);
1848
+ const patterns = toFilterStrings(effectiveInclude);
1849
+ pushDecisionStep(decisionChain, {
1850
+ step: "filter.include",
1851
+ result: included ? "pass" : "fail",
1852
+ source: configSources.include,
1853
+ detail: patterns.length > 0 ? `${included ? "matched" : "no match"}: ${patterns.join(", ")}` : void 0
1854
+ });
1855
+ if (!included) {
1856
+ if (shouldCollectSkip) {
1857
+ const resolved = resolveSkipRoute({
1858
+ file: fileInfo.file,
1859
+ rootDir: fileInfo.rootDir,
1860
+ prefix
1861
+ });
1862
+ onSkip?.(buildSkipInfo(
1863
+ fileInfo.file,
1864
+ "include",
1865
+ resolved,
1866
+ configChain,
1867
+ decisionChain,
1868
+ effectiveConfigValue
1869
+ ));
1870
+ }
1871
+ return null;
1872
+ }
1873
+ }
1874
+ const result = { decisionChain };
1875
+ if (effectiveConfigValue) {
1876
+ result.effectiveConfigValue = effectiveConfigValue;
1877
+ }
1878
+ return result;
1879
+ }
1880
+
1881
+ async function scanRoutes(params) {
1882
+ const routes = [];
1883
+ const seen = /* @__PURE__ */ new Set();
1884
+ const files = await collectFiles(params.dirs);
1885
+ const globalIgnorePrefix = normalizeIgnorePrefix(params.ignorePrefix);
1886
+ const configCache = /* @__PURE__ */ new Map();
1887
+ const fileCache = /* @__PURE__ */ new Map();
1888
+ const shouldCollectSkip = typeof params.onSkip === "function";
1889
+ const shouldCollectIgnore = typeof params.onIgnore === "function";
1890
+ const shouldCollectConfig = typeof params.onConfig === "function";
1891
+ for (const fileInfo of files) {
1892
+ if (isConfigFile(fileInfo.file)) {
1893
+ if (shouldCollectConfig) {
1894
+ const configParams2 = {
1895
+ file: fileInfo.file,
1896
+ rootDir: fileInfo.rootDir,
1897
+ logger: params.logger,
1898
+ configCache,
1899
+ fileCache
1900
+ };
1901
+ if (params.server) {
1902
+ configParams2.server = params.server;
1903
+ }
1904
+ const config2 = await resolveDirectoryConfig(configParams2);
1905
+ params.onConfig?.({ file: fileInfo.file, enabled: config2.enabled !== false });
1906
+ }
1907
+ continue;
1908
+ }
1909
+ const configParams = {
1910
+ file: fileInfo.file,
1911
+ rootDir: fileInfo.rootDir,
1912
+ logger: params.logger,
1913
+ configCache,
1914
+ fileCache
1915
+ };
1916
+ if (params.server) {
1917
+ configParams.server = params.server;
1918
+ }
1919
+ const config = await resolveDirectoryConfig(configParams);
1920
+ const configChain = config.configChain ?? [];
1921
+ const precheckParams = {
1922
+ fileInfo,
1923
+ prefix: params.prefix,
1924
+ config,
1925
+ configChain,
1926
+ globalIgnorePrefix,
1927
+ shouldCollectSkip,
1928
+ shouldCollectIgnore
1929
+ };
1930
+ if (params.onSkip) {
1931
+ precheckParams.onSkip = params.onSkip;
1932
+ }
1933
+ if (params.onIgnore) {
1934
+ precheckParams.onIgnore = params.onIgnore;
1935
+ }
1936
+ if (params.include) {
1937
+ precheckParams.include = params.include;
1938
+ }
1939
+ if (params.exclude) {
1940
+ precheckParams.exclude = params.exclude;
1941
+ }
1942
+ const precheck = runRoutePrechecks(precheckParams);
1943
+ if (!precheck) {
1944
+ continue;
1945
+ }
1946
+ const { decisionChain, effectiveConfigValue } = precheck;
1947
+ const derived = deriveRouteFromFile(fileInfo.file, fileInfo.rootDir, params.logger);
1948
+ if (!derived) {
1949
+ if (shouldCollectIgnore) {
1950
+ decisionChain.push({
1951
+ step: "route.derived",
1952
+ result: "fail",
1953
+ source: fileInfo.file,
1954
+ detail: "invalid route name"
1955
+ });
1956
+ const ignoreInfo = {
1957
+ file: fileInfo.file,
1958
+ reason: "invalid-route",
1959
+ configChain,
1960
+ decisionChain
1961
+ };
1962
+ if (effectiveConfigValue) {
1963
+ ignoreInfo.effectiveConfig = effectiveConfigValue;
1964
+ }
1965
+ params.onIgnore?.(ignoreInfo);
1966
+ }
1967
+ continue;
1968
+ }
1969
+ decisionChain.push({
1970
+ step: "route.derived",
1971
+ result: "pass",
1972
+ source: fileInfo.file
1973
+ });
1974
+ const rules = await loadRules(fileInfo.file, params.server, params.logger);
1975
+ for (const [index, rule] of rules.entries()) {
1976
+ if (!rule || typeof rule !== "object") {
1977
+ continue;
1978
+ }
1979
+ if (rule.enabled === false) {
1980
+ if (shouldCollectSkip) {
1981
+ const resolved2 = resolveSkipRoute({
1982
+ file: fileInfo.file,
1983
+ rootDir: fileInfo.rootDir,
1984
+ prefix: params.prefix,
1985
+ derived
1986
+ });
1987
+ const ruleDecisionStep = {
1988
+ step: "rule.enabled",
1989
+ result: "fail",
1990
+ source: fileInfo.file,
1991
+ detail: "enabled=false"
1992
+ };
1993
+ const ruleDecisionChain = [...decisionChain, ruleDecisionStep];
1994
+ params.onSkip?.(buildSkipInfo(
1995
+ fileInfo.file,
1996
+ "disabled",
1997
+ resolved2,
1998
+ configChain,
1999
+ ruleDecisionChain,
2000
+ effectiveConfigValue
2001
+ ));
2002
+ }
2003
+ continue;
2004
+ }
2005
+ const ruleValue = rule;
2006
+ const unsupportedKeys = ["response", "url", "method"].filter(
2007
+ (key2) => key2 in ruleValue
2008
+ );
2009
+ if (unsupportedKeys.length > 0) {
2010
+ params.logger.warn(
2011
+ `Skip mock with unsupported fields (${unsupportedKeys.join(", ")}): ${fileInfo.file}`
2012
+ );
2013
+ continue;
2014
+ }
2015
+ if (typeof rule.handler === "undefined") {
2016
+ params.logger.warn(`Skip mock without handler: ${fileInfo.file}`);
2017
+ continue;
2018
+ }
2019
+ const resolved = resolveRule({
2020
+ rule,
2021
+ derivedTemplate: derived.template,
2022
+ derivedMethod: derived.method,
2023
+ prefix: params.prefix,
2024
+ file: fileInfo.file,
2025
+ logger: params.logger
2026
+ });
2027
+ if (!resolved) {
2028
+ continue;
2029
+ }
2030
+ resolved.ruleIndex = index;
2031
+ if (configChain.length > 0) {
2032
+ resolved.configChain = configChain;
2033
+ }
2034
+ if (config.headers) {
2035
+ resolved.headers = { ...config.headers, ...resolved.headers ?? {} };
2036
+ }
2037
+ if (typeof resolved.status === "undefined" && typeof config.status === "number") {
2038
+ resolved.status = config.status;
2039
+ }
2040
+ if (typeof resolved.delay === "undefined" && typeof config.delay === "number") {
2041
+ resolved.delay = config.delay;
2042
+ }
2043
+ if (config.middlewares.length > 0) {
2044
+ resolved.middlewares = config.middlewares;
2045
+ }
2046
+ const key = `${resolved.method} ${resolved.template}`;
2047
+ if (seen.has(key)) {
2048
+ params.logger.warn(`Duplicate mock route ${key} from ${fileInfo.file}`);
2049
+ }
2050
+ seen.add(key);
2051
+ routes.push(resolved);
2052
+ }
2053
+ }
2054
+ return sortRoutes(routes);
1644
2055
  }
1645
2056
 
1646
- export { resolveSwConfig as a, resolveSwUnregisterConfig as b, createPlaygroundMiddleware as c, buildSwScript as d, createLogger as e, createMiddleware as f, createDebouncer as g, resolveDirs as h, isInDirs as i, sortRoutes as j, createHonoApp as k, resolvePlaygroundOptions as r, scanRoutes as s, toPosix as t };
2057
+ export { sortRoutes as a, buildSwScript as b, createHonoApp as c, createDebouncer as d, resolvePlaygroundOptions as e, resolveSwConfig as f, resolveSwUnregisterConfig as g, createPlaygroundMiddleware as h, isInDirs as i, createMiddleware as j, buildManifestData as k, resolveDirs as r, scanRoutes as s, toPosix as t };