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