@mokup/server 1.1.0 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/README.zh-CN.md +9 -0
- package/dist/connect.cjs +25 -0
- package/dist/connect.d.cts +8 -0
- package/dist/connect.d.mts +8 -0
- package/dist/connect.d.ts +8 -0
- package/dist/connect.mjs +23 -0
- package/dist/express.cjs +11 -0
- package/dist/express.d.cts +7 -0
- package/dist/express.d.mts +7 -0
- package/dist/express.d.ts +7 -0
- package/dist/express.mjs +9 -0
- package/dist/fastify.cjs +39 -0
- package/dist/fastify.d.cts +18 -0
- package/dist/fastify.d.mts +18 -0
- package/dist/fastify.d.ts +18 -0
- package/dist/fastify.mjs +37 -0
- package/dist/fetch-server.cjs +1507 -0
- package/dist/fetch-server.d.cts +54 -0
- package/dist/fetch-server.d.mts +54 -0
- package/dist/fetch-server.d.ts +54 -0
- package/dist/fetch-server.mjs +1500 -0
- package/dist/fetch.cjs +26 -0
- package/dist/fetch.d.cts +6 -0
- package/dist/fetch.d.mts +6 -0
- package/dist/fetch.d.ts +6 -0
- package/dist/fetch.mjs +24 -0
- package/dist/hono.cjs +27 -0
- package/dist/hono.d.cts +21 -0
- package/dist/hono.d.mts +21 -0
- package/dist/hono.d.ts +21 -0
- package/dist/hono.mjs +25 -0
- package/dist/index.cjs +5 -1593
- package/dist/index.d.cts +3 -134
- package/dist/index.d.mts +3 -134
- package/dist/index.d.ts +3 -134
- package/dist/index.mjs +4 -1585
- package/dist/koa.cjs +38 -0
- package/dist/koa.d.cts +18 -0
- package/dist/koa.d.mts +18 -0
- package/dist/koa.d.ts +18 -0
- package/dist/koa.mjs +36 -0
- package/dist/node.cjs +26 -0
- package/dist/node.d.cts +12 -0
- package/dist/node.d.mts +12 -0
- package/dist/node.d.ts +12 -0
- package/dist/node.mjs +19 -0
- package/dist/shared/{server.BdTl0qJd.cjs → server.3GcmR3Ev.cjs} +2 -23
- package/dist/shared/{server.DNITwCtQ.d.cts → server.B82hrXoo.d.cts} +2 -2
- package/dist/shared/{server.DNITwCtQ.d.mts → server.B82hrXoo.d.mts} +2 -2
- package/dist/shared/{server.DNITwCtQ.d.ts → server.B82hrXoo.d.ts} +2 -2
- package/dist/shared/server.Cb2eiCU2.d.cts +17 -0
- package/dist/shared/server.Cb2eiCU2.d.mts +17 -0
- package/dist/shared/server.Cb2eiCU2.d.ts +17 -0
- package/dist/shared/{server.Dje1y79O.mjs → server.tZ4R8aB2.mjs} +1 -23
- package/dist/worker-node.cjs +74 -0
- package/dist/worker-node.d.cts +12 -0
- package/dist/worker-node.d.mts +12 -0
- package/dist/worker-node.d.ts +12 -0
- package/dist/worker-node.mjs +72 -0
- package/dist/worker.cjs +6 -2
- package/dist/worker.d.cts +2 -2
- package/dist/worker.d.mts +2 -2
- package/dist/worker.d.ts +2 -2
- package/dist/worker.mjs +6 -2
- package/package.json +44 -4
|
@@ -0,0 +1,1507 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const process = require('node:process');
|
|
4
|
+
const hono = require('@mokup/shared/hono');
|
|
5
|
+
const pathe = require('@mokup/shared/pathe');
|
|
6
|
+
const node_fs = require('node:fs');
|
|
7
|
+
const node_module = require('node:module');
|
|
8
|
+
const runtime = require('@mokup/runtime');
|
|
9
|
+
const node_buffer = require('node:buffer');
|
|
10
|
+
const node_url = require('node:url');
|
|
11
|
+
const esbuild = require('@mokup/shared/esbuild');
|
|
12
|
+
const jsoncParser = require('@mokup/shared/jsonc-parser');
|
|
13
|
+
|
|
14
|
+
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
15
|
+
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; }
|
|
16
|
+
|
|
17
|
+
const process__default = /*#__PURE__*/_interopDefaultCompat(process);
|
|
18
|
+
|
|
19
|
+
const methodSet = /* @__PURE__ */ new Set([
|
|
20
|
+
"GET",
|
|
21
|
+
"POST",
|
|
22
|
+
"PUT",
|
|
23
|
+
"PATCH",
|
|
24
|
+
"DELETE",
|
|
25
|
+
"OPTIONS",
|
|
26
|
+
"HEAD"
|
|
27
|
+
]);
|
|
28
|
+
const methodSuffixSet = new Set(
|
|
29
|
+
Array.from(methodSet, (method) => method.toLowerCase())
|
|
30
|
+
);
|
|
31
|
+
const supportedExtensions = /* @__PURE__ */ new Set([
|
|
32
|
+
".json",
|
|
33
|
+
".jsonc",
|
|
34
|
+
".ts",
|
|
35
|
+
".js",
|
|
36
|
+
".mjs",
|
|
37
|
+
".cjs"
|
|
38
|
+
]);
|
|
39
|
+
const configExtensions = [".ts", ".js", ".mjs", ".cjs"];
|
|
40
|
+
|
|
41
|
+
function normalizePrefix(prefix) {
|
|
42
|
+
if (!prefix) {
|
|
43
|
+
return "";
|
|
44
|
+
}
|
|
45
|
+
const normalized = prefix.startsWith("/") ? prefix : `/${prefix}`;
|
|
46
|
+
return normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
|
|
47
|
+
}
|
|
48
|
+
function resolveDirs(dir, root) {
|
|
49
|
+
const raw = typeof dir === "function" ? dir(root) : dir;
|
|
50
|
+
const resolved = Array.isArray(raw) ? raw : raw ? [raw] : ["mock"];
|
|
51
|
+
const normalized = resolved.map(
|
|
52
|
+
(entry) => pathe.isAbsolute(entry) ? entry : pathe.resolve(root, entry)
|
|
53
|
+
);
|
|
54
|
+
return Array.from(new Set(normalized));
|
|
55
|
+
}
|
|
56
|
+
function createDebouncer(delayMs, fn) {
|
|
57
|
+
let timer = null;
|
|
58
|
+
return () => {
|
|
59
|
+
if (timer) {
|
|
60
|
+
clearTimeout(timer);
|
|
61
|
+
}
|
|
62
|
+
timer = setTimeout(() => {
|
|
63
|
+
timer = null;
|
|
64
|
+
fn();
|
|
65
|
+
}, delayMs);
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function toPosix(value) {
|
|
69
|
+
return value.replace(/\\/g, "/");
|
|
70
|
+
}
|
|
71
|
+
function isInDirs(file, dirs) {
|
|
72
|
+
const normalized = toPosix(file);
|
|
73
|
+
return dirs.some((dir) => {
|
|
74
|
+
const normalizedDir = toPosix(dir).replace(/\/$/, "");
|
|
75
|
+
return normalized === normalizedDir || normalized.startsWith(`${normalizedDir}/`);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
function testPatterns(patterns, value) {
|
|
79
|
+
const list = Array.isArray(patterns) ? patterns : [patterns];
|
|
80
|
+
return list.some((pattern) => pattern.test(value));
|
|
81
|
+
}
|
|
82
|
+
function matchesFilter(file, include, exclude) {
|
|
83
|
+
const normalized = toPosix(file);
|
|
84
|
+
if (exclude && testPatterns(exclude, normalized)) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
if (include) {
|
|
88
|
+
return testPatterns(include, normalized);
|
|
89
|
+
}
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
function normalizeIgnorePrefix(value, fallback = ["."]) {
|
|
93
|
+
const list = typeof value === "undefined" ? fallback : Array.isArray(value) ? value : [value];
|
|
94
|
+
return list.filter((entry) => typeof entry === "string" && entry.length > 0);
|
|
95
|
+
}
|
|
96
|
+
function hasIgnoredPrefix(file, rootDir, prefixes) {
|
|
97
|
+
if (prefixes.length === 0) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
const relativePath = toPosix(pathe.relative(rootDir, file));
|
|
101
|
+
const segments = relativePath.split("/");
|
|
102
|
+
return segments.some(
|
|
103
|
+
(segment) => prefixes.some((prefix) => segment.startsWith(prefix))
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
function delay(ms) {
|
|
107
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function toHonoPath(route) {
|
|
111
|
+
if (!route.tokens || route.tokens.length === 0) {
|
|
112
|
+
return "/";
|
|
113
|
+
}
|
|
114
|
+
const segments = route.tokens.map((token) => {
|
|
115
|
+
if (token.type === "static") {
|
|
116
|
+
return token.value;
|
|
117
|
+
}
|
|
118
|
+
if (token.type === "param") {
|
|
119
|
+
return `:${token.name}`;
|
|
120
|
+
}
|
|
121
|
+
if (token.type === "catchall") {
|
|
122
|
+
return `:${token.name}{.+}`;
|
|
123
|
+
}
|
|
124
|
+
return `:${token.name}{.+}?`;
|
|
125
|
+
});
|
|
126
|
+
return `/${segments.join("/")}`;
|
|
127
|
+
}
|
|
128
|
+
function isValidStatus(status) {
|
|
129
|
+
return typeof status === "number" && Number.isFinite(status) && status >= 200 && status <= 599;
|
|
130
|
+
}
|
|
131
|
+
function resolveStatus(routeStatus, responseStatus) {
|
|
132
|
+
if (isValidStatus(routeStatus)) {
|
|
133
|
+
return routeStatus;
|
|
134
|
+
}
|
|
135
|
+
if (isValidStatus(responseStatus)) {
|
|
136
|
+
return responseStatus;
|
|
137
|
+
}
|
|
138
|
+
return 200;
|
|
139
|
+
}
|
|
140
|
+
function applyRouteOverrides(response, route) {
|
|
141
|
+
const headers = new Headers(response.headers);
|
|
142
|
+
const hasHeaders = !!route.headers && Object.keys(route.headers).length > 0;
|
|
143
|
+
if (route.headers) {
|
|
144
|
+
for (const [key, value] of Object.entries(route.headers)) {
|
|
145
|
+
headers.set(key, value);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const status = resolveStatus(route.status, response.status);
|
|
149
|
+
if (status === response.status && !hasHeaders) {
|
|
150
|
+
return response;
|
|
151
|
+
}
|
|
152
|
+
return new Response(response.body, { status, headers });
|
|
153
|
+
}
|
|
154
|
+
function resolveResponse(value, fallback) {
|
|
155
|
+
if (value instanceof Response) {
|
|
156
|
+
return value;
|
|
157
|
+
}
|
|
158
|
+
if (value && typeof value === "object" && "res" in value) {
|
|
159
|
+
const resolved = value.res;
|
|
160
|
+
if (resolved instanceof Response) {
|
|
161
|
+
return resolved;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return fallback;
|
|
165
|
+
}
|
|
166
|
+
function normalizeHandlerValue(c, value) {
|
|
167
|
+
if (value instanceof Response) {
|
|
168
|
+
return value;
|
|
169
|
+
}
|
|
170
|
+
if (typeof value === "undefined") {
|
|
171
|
+
const response = c.body(null);
|
|
172
|
+
if (response.status === 200) {
|
|
173
|
+
return new Response(response.body, {
|
|
174
|
+
status: 204,
|
|
175
|
+
headers: response.headers
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
return response;
|
|
179
|
+
}
|
|
180
|
+
if (typeof value === "string") {
|
|
181
|
+
return c.text(value);
|
|
182
|
+
}
|
|
183
|
+
if (value instanceof Uint8Array || value instanceof ArrayBuffer) {
|
|
184
|
+
if (!c.res.headers.get("content-type")) {
|
|
185
|
+
c.header("content-type", "application/octet-stream");
|
|
186
|
+
}
|
|
187
|
+
const data = value instanceof ArrayBuffer ? new Uint8Array(value) : new Uint8Array(value);
|
|
188
|
+
return c.body(data);
|
|
189
|
+
}
|
|
190
|
+
return c.json(value);
|
|
191
|
+
}
|
|
192
|
+
function createRouteHandler(route) {
|
|
193
|
+
return async (c) => {
|
|
194
|
+
const value = typeof route.handler === "function" ? await route.handler(c) : route.handler;
|
|
195
|
+
return normalizeHandlerValue(c, value);
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function createFinalizeMiddleware(route, onResponse) {
|
|
199
|
+
return async (c, next) => {
|
|
200
|
+
const response = await next();
|
|
201
|
+
const resolved = resolveResponse(response, c.res);
|
|
202
|
+
if (route.delay && route.delay > 0) {
|
|
203
|
+
await delay(route.delay);
|
|
204
|
+
}
|
|
205
|
+
const overridden = applyRouteOverrides(resolved, route);
|
|
206
|
+
c.res = overridden;
|
|
207
|
+
if (onResponse) {
|
|
208
|
+
try {
|
|
209
|
+
const result = onResponse(route, overridden);
|
|
210
|
+
if (result instanceof Promise) {
|
|
211
|
+
result.catch(() => void 0);
|
|
212
|
+
}
|
|
213
|
+
} catch {
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return overridden;
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
function wrapMiddleware(handler) {
|
|
220
|
+
return async (c, next) => {
|
|
221
|
+
const response = await handler(c, next);
|
|
222
|
+
return resolveResponse(response, c.res);
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
function createHonoApp(routes, options = {}) {
|
|
226
|
+
const app = new hono.Hono({ router: new hono.PatternRouter(), strict: false });
|
|
227
|
+
for (const route of routes) {
|
|
228
|
+
const middlewares = route.middlewares?.map((entry) => wrapMiddleware(entry.handle)) ?? [];
|
|
229
|
+
app.on(
|
|
230
|
+
route.method,
|
|
231
|
+
toHonoPath(route),
|
|
232
|
+
createFinalizeMiddleware(route, options.onResponse),
|
|
233
|
+
...middlewares,
|
|
234
|
+
createRouteHandler(route)
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
return app;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function createLogger(enabled) {
|
|
241
|
+
return {
|
|
242
|
+
info: (...args) => {
|
|
243
|
+
if (enabled) {
|
|
244
|
+
console.info("[mokup]", ...args);
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
warn: (...args) => {
|
|
248
|
+
if (enabled) {
|
|
249
|
+
console.warn("[mokup]", ...args);
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
error: (...args) => {
|
|
253
|
+
if (enabled) {
|
|
254
|
+
console.error("[mokup]", ...args);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const mimeTypes = {
|
|
261
|
+
".html": "text/html; charset=utf-8",
|
|
262
|
+
".css": "text/css; charset=utf-8",
|
|
263
|
+
".js": "text/javascript; charset=utf-8",
|
|
264
|
+
".map": "application/json; charset=utf-8",
|
|
265
|
+
".json": "application/json; charset=utf-8",
|
|
266
|
+
".svg": "image/svg+xml",
|
|
267
|
+
".png": "image/png",
|
|
268
|
+
".jpg": "image/jpeg",
|
|
269
|
+
".jpeg": "image/jpeg",
|
|
270
|
+
".ico": "image/x-icon"
|
|
271
|
+
};
|
|
272
|
+
function normalizePlaygroundPath(value) {
|
|
273
|
+
if (!value) {
|
|
274
|
+
return "/_mokup";
|
|
275
|
+
}
|
|
276
|
+
const normalized = value.startsWith("/") ? value : `/${value}`;
|
|
277
|
+
return normalized.length > 1 && normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
|
|
278
|
+
}
|
|
279
|
+
function resolvePlaygroundOptions(playground) {
|
|
280
|
+
if (playground === false) {
|
|
281
|
+
return { enabled: false, path: "/_mokup" };
|
|
282
|
+
}
|
|
283
|
+
if (playground && typeof playground === "object") {
|
|
284
|
+
return {
|
|
285
|
+
enabled: playground.enabled !== false,
|
|
286
|
+
path: normalizePlaygroundPath(playground.path)
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
return { enabled: true, path: "/_mokup" };
|
|
290
|
+
}
|
|
291
|
+
function resolvePlaygroundDist() {
|
|
292
|
+
const require$1 = node_module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('fetch-server.cjs', document.baseURI).href)));
|
|
293
|
+
const pkgPath = require$1.resolve("@mokup/playground/package.json");
|
|
294
|
+
return pathe.join(pkgPath, "..", "dist");
|
|
295
|
+
}
|
|
296
|
+
function toPosixPath(value) {
|
|
297
|
+
return value.replace(/\\/g, "/");
|
|
298
|
+
}
|
|
299
|
+
function normalizePath(value) {
|
|
300
|
+
return toPosixPath(pathe.normalize(value));
|
|
301
|
+
}
|
|
302
|
+
function isAncestor(parent, child) {
|
|
303
|
+
const normalizedParent = normalizePath(parent).replace(/\/$/, "");
|
|
304
|
+
const normalizedChild = normalizePath(child);
|
|
305
|
+
return normalizedChild === normalizedParent || normalizedChild.startsWith(`${normalizedParent}/`);
|
|
306
|
+
}
|
|
307
|
+
function resolveGroupRoot(dirs, serverRoot) {
|
|
308
|
+
if (!dirs || dirs.length === 0) {
|
|
309
|
+
return serverRoot ?? process.cwd();
|
|
310
|
+
}
|
|
311
|
+
if (serverRoot) {
|
|
312
|
+
const normalizedRoot = normalizePath(serverRoot);
|
|
313
|
+
const canUseRoot = dirs.every((dir) => isAncestor(normalizedRoot, dir));
|
|
314
|
+
if (canUseRoot) {
|
|
315
|
+
return normalizedRoot;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (dirs.length === 1) {
|
|
319
|
+
return normalizePath(pathe.dirname(dirs[0]));
|
|
320
|
+
}
|
|
321
|
+
let common = normalizePath(dirs[0]);
|
|
322
|
+
for (const dir of dirs.slice(1)) {
|
|
323
|
+
const normalizedDir = normalizePath(dir);
|
|
324
|
+
while (common && !isAncestor(common, normalizedDir)) {
|
|
325
|
+
const parent = normalizePath(pathe.dirname(common));
|
|
326
|
+
if (parent === common) {
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
common = parent;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
if (!common || common === "/") {
|
|
333
|
+
return serverRoot ?? process.cwd();
|
|
334
|
+
}
|
|
335
|
+
return common;
|
|
336
|
+
}
|
|
337
|
+
const disabledReasonSet = /* @__PURE__ */ new Set([
|
|
338
|
+
"disabled",
|
|
339
|
+
"disabled-dir",
|
|
340
|
+
"exclude",
|
|
341
|
+
"ignore-prefix",
|
|
342
|
+
"include",
|
|
343
|
+
"unknown"
|
|
344
|
+
]);
|
|
345
|
+
const ignoredReasonSet = /* @__PURE__ */ new Set([
|
|
346
|
+
"unsupported",
|
|
347
|
+
"invalid-route",
|
|
348
|
+
"unknown"
|
|
349
|
+
]);
|
|
350
|
+
function normalizeDisabledReason(reason) {
|
|
351
|
+
if (reason && disabledReasonSet.has(reason)) {
|
|
352
|
+
return reason;
|
|
353
|
+
}
|
|
354
|
+
return "unknown";
|
|
355
|
+
}
|
|
356
|
+
function normalizeIgnoredReason(reason) {
|
|
357
|
+
if (reason && ignoredReasonSet.has(reason)) {
|
|
358
|
+
return reason;
|
|
359
|
+
}
|
|
360
|
+
return "unknown";
|
|
361
|
+
}
|
|
362
|
+
function formatRouteFile(file, root) {
|
|
363
|
+
if (!root) {
|
|
364
|
+
return toPosixPath(file);
|
|
365
|
+
}
|
|
366
|
+
const rel = toPosixPath(pathe.relative(root, file));
|
|
367
|
+
if (!rel || rel.startsWith("..")) {
|
|
368
|
+
return toPosixPath(file);
|
|
369
|
+
}
|
|
370
|
+
return rel;
|
|
371
|
+
}
|
|
372
|
+
function resolveGroups(dirs, root) {
|
|
373
|
+
const groups = [];
|
|
374
|
+
const seen = /* @__PURE__ */ new Set();
|
|
375
|
+
for (const dir of dirs) {
|
|
376
|
+
const normalized = normalizePath(dir);
|
|
377
|
+
if (seen.has(normalized)) {
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
seen.add(normalized);
|
|
381
|
+
const rel = toPosixPath(pathe.relative(root, normalized));
|
|
382
|
+
const label = rel && !rel.startsWith("..") ? rel : normalized;
|
|
383
|
+
groups.push({
|
|
384
|
+
key: normalized,
|
|
385
|
+
label,
|
|
386
|
+
path: normalized
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
return groups;
|
|
390
|
+
}
|
|
391
|
+
function resolveRouteGroup(routeFile, groups) {
|
|
392
|
+
if (groups.length === 0) {
|
|
393
|
+
return void 0;
|
|
394
|
+
}
|
|
395
|
+
const normalizedFile = toPosixPath(pathe.normalize(routeFile));
|
|
396
|
+
let matched;
|
|
397
|
+
for (const group of groups) {
|
|
398
|
+
if (normalizedFile === group.path || normalizedFile.startsWith(`${group.path}/`)) {
|
|
399
|
+
if (!matched || group.path.length > matched.path.length) {
|
|
400
|
+
matched = group;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return matched;
|
|
405
|
+
}
|
|
406
|
+
function toPlaygroundRoute(route, root, groups) {
|
|
407
|
+
const matchedGroup = resolveRouteGroup(route.file, groups);
|
|
408
|
+
const middlewareSources = route.middlewares?.map((entry) => formatRouteFile(entry.source, root));
|
|
409
|
+
return {
|
|
410
|
+
method: route.method,
|
|
411
|
+
url: route.template,
|
|
412
|
+
file: formatRouteFile(route.file, root),
|
|
413
|
+
type: typeof route.handler === "function" ? "handler" : "static",
|
|
414
|
+
status: route.status,
|
|
415
|
+
delay: route.delay,
|
|
416
|
+
middlewareCount: middlewareSources?.length ?? 0,
|
|
417
|
+
middlewares: middlewareSources,
|
|
418
|
+
groupKey: matchedGroup?.key,
|
|
419
|
+
group: matchedGroup?.label
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
function toPlaygroundDisabledRoute(route, root, groups) {
|
|
423
|
+
const matchedGroup = resolveRouteGroup(route.file, groups);
|
|
424
|
+
const disabled = {
|
|
425
|
+
file: formatRouteFile(route.file, root),
|
|
426
|
+
reason: normalizeDisabledReason(route.reason)
|
|
427
|
+
};
|
|
428
|
+
if (typeof route.method !== "undefined") {
|
|
429
|
+
disabled.method = route.method;
|
|
430
|
+
}
|
|
431
|
+
if (typeof route.url !== "undefined") {
|
|
432
|
+
disabled.url = route.url;
|
|
433
|
+
}
|
|
434
|
+
if (matchedGroup) {
|
|
435
|
+
disabled.groupKey = matchedGroup.key;
|
|
436
|
+
disabled.group = matchedGroup.label;
|
|
437
|
+
}
|
|
438
|
+
return disabled;
|
|
439
|
+
}
|
|
440
|
+
function toPlaygroundIgnoredRoute(route, root, groups) {
|
|
441
|
+
const matchedGroup = resolveRouteGroup(route.file, groups);
|
|
442
|
+
const ignored = {
|
|
443
|
+
file: formatRouteFile(route.file, root),
|
|
444
|
+
reason: normalizeIgnoredReason(route.reason)
|
|
445
|
+
};
|
|
446
|
+
if (matchedGroup) {
|
|
447
|
+
ignored.groupKey = matchedGroup.key;
|
|
448
|
+
ignored.group = matchedGroup.label;
|
|
449
|
+
}
|
|
450
|
+
return ignored;
|
|
451
|
+
}
|
|
452
|
+
function toPlaygroundConfigFile(entry, root, groups) {
|
|
453
|
+
const matchedGroup = resolveRouteGroup(entry.file, groups);
|
|
454
|
+
const configFile = {
|
|
455
|
+
file: formatRouteFile(entry.file, root)
|
|
456
|
+
};
|
|
457
|
+
if (matchedGroup) {
|
|
458
|
+
configFile.groupKey = matchedGroup.key;
|
|
459
|
+
configFile.group = matchedGroup.label;
|
|
460
|
+
}
|
|
461
|
+
return configFile;
|
|
462
|
+
}
|
|
463
|
+
function registerPlaygroundRoutes(params) {
|
|
464
|
+
if (!params.config.enabled) {
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
const playgroundPath = normalizePlaygroundPath(params.config.path);
|
|
468
|
+
const distDir = resolvePlaygroundDist();
|
|
469
|
+
const indexPath = pathe.join(distDir, "index.html");
|
|
470
|
+
const serveIndex = async () => {
|
|
471
|
+
try {
|
|
472
|
+
const html = await node_fs.promises.readFile(indexPath, "utf8");
|
|
473
|
+
const contentType = mimeTypes[".html"] ?? "text/html; charset=utf-8";
|
|
474
|
+
return new Response(html, { headers: { "Content-Type": contentType } });
|
|
475
|
+
} catch (error) {
|
|
476
|
+
params.logger.error("Failed to load playground index:", error);
|
|
477
|
+
return new Response("Playground is not available.", { status: 500 });
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
params.app.get(playgroundPath, (c) => {
|
|
481
|
+
try {
|
|
482
|
+
const pathname = new URL(c.req.raw.url, "http://localhost").pathname;
|
|
483
|
+
if (pathname.endsWith("/")) {
|
|
484
|
+
return serveIndex();
|
|
485
|
+
}
|
|
486
|
+
} catch {
|
|
487
|
+
}
|
|
488
|
+
return c.redirect(`${playgroundPath}/`);
|
|
489
|
+
});
|
|
490
|
+
params.app.get(`${playgroundPath}/`, () => serveIndex());
|
|
491
|
+
params.app.get(`${playgroundPath}/index.html`, () => serveIndex());
|
|
492
|
+
params.app.get(`${playgroundPath}/routes`, (c) => {
|
|
493
|
+
const baseRoot = resolveGroupRoot(params.dirs, params.root);
|
|
494
|
+
const groups = resolveGroups(params.dirs, baseRoot);
|
|
495
|
+
return c.json({
|
|
496
|
+
basePath: playgroundPath,
|
|
497
|
+
root: baseRoot,
|
|
498
|
+
count: params.routes.length,
|
|
499
|
+
groups: groups.map((group) => ({ key: group.key, label: group.label })),
|
|
500
|
+
routes: params.routes.map((route) => toPlaygroundRoute(route, baseRoot, groups)),
|
|
501
|
+
disabled: (params.disabledRoutes ?? []).map((route) => toPlaygroundDisabledRoute(route, baseRoot, groups)),
|
|
502
|
+
ignored: (params.ignoredRoutes ?? []).map((route) => toPlaygroundIgnoredRoute(route, baseRoot, groups)),
|
|
503
|
+
configs: (params.configFiles ?? []).map((entry) => toPlaygroundConfigFile(entry, baseRoot, groups)),
|
|
504
|
+
disabledConfigs: (params.disabledConfigFiles ?? []).map(
|
|
505
|
+
(entry) => toPlaygroundConfigFile(entry, baseRoot, groups)
|
|
506
|
+
)
|
|
507
|
+
});
|
|
508
|
+
});
|
|
509
|
+
params.app.get(`${playgroundPath}/*`, async (c) => {
|
|
510
|
+
const pathname = c.req.path;
|
|
511
|
+
const relPath = pathname.slice(playgroundPath.length).replace(/^\/+/, "");
|
|
512
|
+
if (!relPath || relPath === "/") {
|
|
513
|
+
return serveIndex();
|
|
514
|
+
}
|
|
515
|
+
if (relPath.includes("..")) {
|
|
516
|
+
return new Response("Invalid path.", { status: 400 });
|
|
517
|
+
}
|
|
518
|
+
const normalizedPath = pathe.normalize(relPath);
|
|
519
|
+
const filePath = pathe.join(distDir, normalizedPath);
|
|
520
|
+
try {
|
|
521
|
+
const content = await node_fs.promises.readFile(filePath);
|
|
522
|
+
const ext = pathe.extname(filePath);
|
|
523
|
+
const contentType = mimeTypes[ext] ?? "application/octet-stream";
|
|
524
|
+
return new Response(content, { headers: { "Content-Type": contentType } });
|
|
525
|
+
} catch {
|
|
526
|
+
return c.notFound();
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const jsonExtensions = /* @__PURE__ */ new Set([".json", ".jsonc"]);
|
|
532
|
+
function resolveTemplate(template, prefix) {
|
|
533
|
+
const normalized = template.startsWith("/") ? template : `/${template}`;
|
|
534
|
+
if (!prefix) {
|
|
535
|
+
return normalized;
|
|
536
|
+
}
|
|
537
|
+
const normalizedPrefix = normalizePrefix(prefix);
|
|
538
|
+
if (!normalizedPrefix) {
|
|
539
|
+
return normalized;
|
|
540
|
+
}
|
|
541
|
+
if (normalized === normalizedPrefix || normalized.startsWith(`${normalizedPrefix}/`)) {
|
|
542
|
+
return normalized;
|
|
543
|
+
}
|
|
544
|
+
if (normalized === "/") {
|
|
545
|
+
return `${normalizedPrefix}/`;
|
|
546
|
+
}
|
|
547
|
+
return `${normalizedPrefix}${normalized}`;
|
|
548
|
+
}
|
|
549
|
+
function stripMethodSuffix(base) {
|
|
550
|
+
const segments = base.split(".");
|
|
551
|
+
const last = segments.at(-1);
|
|
552
|
+
if (last && methodSuffixSet.has(last.toLowerCase())) {
|
|
553
|
+
segments.pop();
|
|
554
|
+
return {
|
|
555
|
+
name: segments.join("."),
|
|
556
|
+
method: last.toUpperCase()
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
return {
|
|
560
|
+
name: base,
|
|
561
|
+
method: void 0
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
function deriveRouteFromFile(file, rootDir, logger) {
|
|
565
|
+
const rel = toPosix(pathe.relative(rootDir, file));
|
|
566
|
+
const ext = pathe.extname(rel);
|
|
567
|
+
const withoutExt = rel.slice(0, rel.length - ext.length);
|
|
568
|
+
const dir = pathe.dirname(withoutExt);
|
|
569
|
+
const base = pathe.basename(withoutExt);
|
|
570
|
+
const { name, method } = stripMethodSuffix(base);
|
|
571
|
+
const resolvedMethod = method ?? (jsonExtensions.has(ext) ? "GET" : void 0);
|
|
572
|
+
if (!resolvedMethod) {
|
|
573
|
+
logger.warn(`Skip mock without method suffix: ${file}`);
|
|
574
|
+
return null;
|
|
575
|
+
}
|
|
576
|
+
if (!name) {
|
|
577
|
+
logger.warn(`Skip mock with empty route name: ${file}`);
|
|
578
|
+
return null;
|
|
579
|
+
}
|
|
580
|
+
const joined = dir === "." ? name : pathe.join(dir, name);
|
|
581
|
+
const segments = toPosix(joined).split("/");
|
|
582
|
+
if (segments.at(-1) === "index") {
|
|
583
|
+
segments.pop();
|
|
584
|
+
}
|
|
585
|
+
const template = segments.length === 0 ? "/" : `/${segments.join("/")}`;
|
|
586
|
+
const parsed = runtime.parseRouteTemplate(template);
|
|
587
|
+
if (parsed.errors.length > 0) {
|
|
588
|
+
for (const error of parsed.errors) {
|
|
589
|
+
logger.warn(`${error} in ${file}`);
|
|
590
|
+
}
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
for (const warning of parsed.warnings) {
|
|
594
|
+
logger.warn(`${warning} in ${file}`);
|
|
595
|
+
}
|
|
596
|
+
return {
|
|
597
|
+
template: parsed.template,
|
|
598
|
+
method: resolvedMethod,
|
|
599
|
+
tokens: parsed.tokens,
|
|
600
|
+
score: parsed.score
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
function resolveRule(params) {
|
|
604
|
+
const method = params.derivedMethod;
|
|
605
|
+
if (!method) {
|
|
606
|
+
params.logger.warn(`Skip mock without method suffix: ${params.file}`);
|
|
607
|
+
return null;
|
|
608
|
+
}
|
|
609
|
+
const template = resolveTemplate(params.derivedTemplate, params.prefix);
|
|
610
|
+
const parsed = runtime.parseRouteTemplate(template);
|
|
611
|
+
if (parsed.errors.length > 0) {
|
|
612
|
+
for (const error of parsed.errors) {
|
|
613
|
+
params.logger.warn(`${error} in ${params.file}`);
|
|
614
|
+
}
|
|
615
|
+
return null;
|
|
616
|
+
}
|
|
617
|
+
for (const warning of parsed.warnings) {
|
|
618
|
+
params.logger.warn(`${warning} in ${params.file}`);
|
|
619
|
+
}
|
|
620
|
+
const route = {
|
|
621
|
+
file: params.file,
|
|
622
|
+
template: parsed.template,
|
|
623
|
+
method,
|
|
624
|
+
tokens: parsed.tokens,
|
|
625
|
+
score: parsed.score,
|
|
626
|
+
handler: params.rule.handler
|
|
627
|
+
};
|
|
628
|
+
if (typeof params.rule.status === "number") {
|
|
629
|
+
route.status = params.rule.status;
|
|
630
|
+
}
|
|
631
|
+
if (params.rule.headers) {
|
|
632
|
+
route.headers = params.rule.headers;
|
|
633
|
+
}
|
|
634
|
+
if (typeof params.rule.delay === "number") {
|
|
635
|
+
route.delay = params.rule.delay;
|
|
636
|
+
}
|
|
637
|
+
return route;
|
|
638
|
+
}
|
|
639
|
+
function sortRoutes(routes) {
|
|
640
|
+
return routes.sort((a, b) => {
|
|
641
|
+
if (a.method !== b.method) {
|
|
642
|
+
return a.method.localeCompare(b.method);
|
|
643
|
+
}
|
|
644
|
+
const scoreCompare = runtime.compareRouteScore(a.score, b.score);
|
|
645
|
+
if (scoreCompare !== 0) {
|
|
646
|
+
return scoreCompare;
|
|
647
|
+
}
|
|
648
|
+
return a.template.localeCompare(b.template);
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
let registerPromise = null;
|
|
653
|
+
let hasLoggedFailure = false;
|
|
654
|
+
async function ensureTsxRegister(logger) {
|
|
655
|
+
if (registerPromise) {
|
|
656
|
+
return registerPromise;
|
|
657
|
+
}
|
|
658
|
+
registerPromise = (async () => {
|
|
659
|
+
try {
|
|
660
|
+
const mod = await import('tsx/esm/api');
|
|
661
|
+
const setSourceMapsEnabled = process__default.setSourceMapsEnabled;
|
|
662
|
+
if (typeof setSourceMapsEnabled === "function") {
|
|
663
|
+
setSourceMapsEnabled(true);
|
|
664
|
+
}
|
|
665
|
+
if (typeof mod.register === "function") {
|
|
666
|
+
mod.register();
|
|
667
|
+
}
|
|
668
|
+
return true;
|
|
669
|
+
} catch (error) {
|
|
670
|
+
if (!hasLoggedFailure && logger) {
|
|
671
|
+
logger.warn(
|
|
672
|
+
"Failed to register tsx loader; falling back to bundled TS loader.",
|
|
673
|
+
error
|
|
674
|
+
);
|
|
675
|
+
hasLoggedFailure = true;
|
|
676
|
+
}
|
|
677
|
+
return false;
|
|
678
|
+
}
|
|
679
|
+
})();
|
|
680
|
+
return registerPromise;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function isUnknownFileExtensionError$1(error) {
|
|
684
|
+
if (!error || typeof error !== "object") {
|
|
685
|
+
return false;
|
|
686
|
+
}
|
|
687
|
+
const code = error.code;
|
|
688
|
+
if (code === "ERR_UNKNOWN_FILE_EXTENSION") {
|
|
689
|
+
return true;
|
|
690
|
+
}
|
|
691
|
+
const message = error.message;
|
|
692
|
+
return typeof message === "string" && message.includes("Unknown file extension");
|
|
693
|
+
}
|
|
694
|
+
async function loadTsModule$1(file, logger) {
|
|
695
|
+
const cacheBust = Date.now();
|
|
696
|
+
const fileUrl = `${node_url.pathToFileURL(file).href}?t=${cacheBust}`;
|
|
697
|
+
const registered = await ensureTsxRegister(logger);
|
|
698
|
+
if (registered) {
|
|
699
|
+
try {
|
|
700
|
+
return await import(fileUrl);
|
|
701
|
+
} catch (error) {
|
|
702
|
+
if (!isUnknownFileExtensionError$1(error)) {
|
|
703
|
+
throw error;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
const result = await esbuild.build({
|
|
708
|
+
entryPoints: [file],
|
|
709
|
+
bundle: true,
|
|
710
|
+
format: "esm",
|
|
711
|
+
platform: "node",
|
|
712
|
+
sourcemap: "inline",
|
|
713
|
+
target: "es2020",
|
|
714
|
+
write: false
|
|
715
|
+
});
|
|
716
|
+
const output = result.outputFiles[0];
|
|
717
|
+
const code = output?.text ?? "";
|
|
718
|
+
const dataUrl = `data:text/javascript;base64,${node_buffer.Buffer.from(code).toString(
|
|
719
|
+
"base64"
|
|
720
|
+
)}`;
|
|
721
|
+
return import(`${dataUrl}#${cacheBust}`);
|
|
722
|
+
}
|
|
723
|
+
async function loadModule$1(file, logger) {
|
|
724
|
+
const ext = configExtensions.find((extension) => file.endsWith(extension));
|
|
725
|
+
if (ext === ".cjs") {
|
|
726
|
+
const require$1 = node_module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('fetch-server.cjs', document.baseURI).href)));
|
|
727
|
+
delete require$1.cache[file];
|
|
728
|
+
return require$1(file);
|
|
729
|
+
}
|
|
730
|
+
if (ext === ".js" || ext === ".mjs") {
|
|
731
|
+
return import(`${node_url.pathToFileURL(file).href}?t=${Date.now()}`);
|
|
732
|
+
}
|
|
733
|
+
if (ext === ".ts") {
|
|
734
|
+
return loadTsModule$1(file, logger);
|
|
735
|
+
}
|
|
736
|
+
return null;
|
|
737
|
+
}
|
|
738
|
+
function getConfigFileCandidates(dir) {
|
|
739
|
+
return configExtensions.map((extension) => pathe.join(dir, `index.config${extension}`));
|
|
740
|
+
}
|
|
741
|
+
async function findConfigFile(dir, cache) {
|
|
742
|
+
const cached = cache.get(dir);
|
|
743
|
+
if (cached !== void 0) {
|
|
744
|
+
return cached;
|
|
745
|
+
}
|
|
746
|
+
for (const candidate of getConfigFileCandidates(dir)) {
|
|
747
|
+
try {
|
|
748
|
+
await node_fs.promises.stat(candidate);
|
|
749
|
+
cache.set(dir, candidate);
|
|
750
|
+
return candidate;
|
|
751
|
+
} catch {
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
cache.set(dir, null);
|
|
756
|
+
return null;
|
|
757
|
+
}
|
|
758
|
+
async function loadConfig(file, logger) {
|
|
759
|
+
const mod = await loadModule$1(file, logger);
|
|
760
|
+
if (!mod) {
|
|
761
|
+
return null;
|
|
762
|
+
}
|
|
763
|
+
const value = mod?.default ?? mod;
|
|
764
|
+
if (!value || typeof value !== "object") {
|
|
765
|
+
return null;
|
|
766
|
+
}
|
|
767
|
+
return value;
|
|
768
|
+
}
|
|
769
|
+
function normalizeMiddlewares(value, source, logger) {
|
|
770
|
+
if (!value) {
|
|
771
|
+
return [];
|
|
772
|
+
}
|
|
773
|
+
const list = Array.isArray(value) ? value : [value];
|
|
774
|
+
const middlewares = [];
|
|
775
|
+
for (const [index, entry] of list.entries()) {
|
|
776
|
+
if (typeof entry !== "function") {
|
|
777
|
+
logger.warn(`Invalid middleware in ${source}`);
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
780
|
+
middlewares.push({ handle: entry, source, index });
|
|
781
|
+
}
|
|
782
|
+
return middlewares;
|
|
783
|
+
}
|
|
784
|
+
async function resolveDirectoryConfig(params) {
|
|
785
|
+
const { file, rootDir, logger, configCache, fileCache } = params;
|
|
786
|
+
const resolvedRoot = pathe.normalize(rootDir);
|
|
787
|
+
const resolvedFileDir = pathe.normalize(pathe.dirname(file));
|
|
788
|
+
const chain = [];
|
|
789
|
+
let current = resolvedFileDir;
|
|
790
|
+
while (true) {
|
|
791
|
+
chain.push(current);
|
|
792
|
+
if (current === resolvedRoot) {
|
|
793
|
+
break;
|
|
794
|
+
}
|
|
795
|
+
const parent = pathe.dirname(current);
|
|
796
|
+
if (parent === current) {
|
|
797
|
+
break;
|
|
798
|
+
}
|
|
799
|
+
current = parent;
|
|
800
|
+
}
|
|
801
|
+
chain.reverse();
|
|
802
|
+
const merged = { middlewares: [] };
|
|
803
|
+
for (const dir of chain) {
|
|
804
|
+
const configPath = await findConfigFile(dir, fileCache);
|
|
805
|
+
if (!configPath) {
|
|
806
|
+
continue;
|
|
807
|
+
}
|
|
808
|
+
let config = configCache.get(configPath);
|
|
809
|
+
if (config === void 0) {
|
|
810
|
+
config = await loadConfig(configPath, logger);
|
|
811
|
+
configCache.set(configPath, config);
|
|
812
|
+
}
|
|
813
|
+
if (!config) {
|
|
814
|
+
logger.warn(`Invalid config in ${configPath}`);
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
817
|
+
if (config.headers) {
|
|
818
|
+
merged.headers = { ...merged.headers ?? {}, ...config.headers };
|
|
819
|
+
}
|
|
820
|
+
if (typeof config.status === "number") {
|
|
821
|
+
merged.status = config.status;
|
|
822
|
+
}
|
|
823
|
+
if (typeof config.delay === "number") {
|
|
824
|
+
merged.delay = config.delay;
|
|
825
|
+
}
|
|
826
|
+
if (typeof config.enabled === "boolean") {
|
|
827
|
+
merged.enabled = config.enabled;
|
|
828
|
+
}
|
|
829
|
+
if (typeof config.ignorePrefix !== "undefined") {
|
|
830
|
+
merged.ignorePrefix = config.ignorePrefix;
|
|
831
|
+
}
|
|
832
|
+
if (typeof config.include !== "undefined") {
|
|
833
|
+
merged.include = config.include;
|
|
834
|
+
}
|
|
835
|
+
if (typeof config.exclude !== "undefined") {
|
|
836
|
+
merged.exclude = config.exclude;
|
|
837
|
+
}
|
|
838
|
+
const normalized = normalizeMiddlewares(config.middleware, configPath, logger);
|
|
839
|
+
if (normalized.length > 0) {
|
|
840
|
+
merged.middlewares.push(...normalized);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
return merged;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
async function walkDir(dir, rootDir, files) {
|
|
847
|
+
const entries = await node_fs.promises.readdir(dir, { withFileTypes: true });
|
|
848
|
+
for (const entry of entries) {
|
|
849
|
+
if (entry.name === "node_modules" || entry.name === ".git") {
|
|
850
|
+
continue;
|
|
851
|
+
}
|
|
852
|
+
const fullPath = pathe.join(dir, entry.name);
|
|
853
|
+
if (entry.isDirectory()) {
|
|
854
|
+
await walkDir(fullPath, rootDir, files);
|
|
855
|
+
continue;
|
|
856
|
+
}
|
|
857
|
+
if (entry.isFile()) {
|
|
858
|
+
files.push({ file: fullPath, rootDir });
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
async function exists(path) {
|
|
863
|
+
try {
|
|
864
|
+
await node_fs.promises.stat(path);
|
|
865
|
+
return true;
|
|
866
|
+
} catch {
|
|
867
|
+
return false;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
async function collectFiles(dirs) {
|
|
871
|
+
const files = [];
|
|
872
|
+
for (const dir of dirs) {
|
|
873
|
+
if (!await exists(dir)) {
|
|
874
|
+
continue;
|
|
875
|
+
}
|
|
876
|
+
await walkDir(dir, dir, files);
|
|
877
|
+
}
|
|
878
|
+
return files;
|
|
879
|
+
}
|
|
880
|
+
function isSupportedFile(file) {
|
|
881
|
+
if (file.endsWith(".d.ts")) {
|
|
882
|
+
return false;
|
|
883
|
+
}
|
|
884
|
+
if (isConfigFile(file)) {
|
|
885
|
+
return false;
|
|
886
|
+
}
|
|
887
|
+
const ext = pathe.extname(file).toLowerCase();
|
|
888
|
+
return supportedExtensions.has(ext);
|
|
889
|
+
}
|
|
890
|
+
function isConfigFile(file) {
|
|
891
|
+
if (file.endsWith(".d.ts")) {
|
|
892
|
+
return false;
|
|
893
|
+
}
|
|
894
|
+
const base = pathe.basename(file);
|
|
895
|
+
if (!base.startsWith("index.config.")) {
|
|
896
|
+
return false;
|
|
897
|
+
}
|
|
898
|
+
const ext = pathe.extname(file).toLowerCase();
|
|
899
|
+
return configExtensions.includes(ext);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
function isUnknownFileExtensionError(error) {
|
|
903
|
+
if (!error || typeof error !== "object") {
|
|
904
|
+
return false;
|
|
905
|
+
}
|
|
906
|
+
const code = error.code;
|
|
907
|
+
if (code === "ERR_UNKNOWN_FILE_EXTENSION") {
|
|
908
|
+
return true;
|
|
909
|
+
}
|
|
910
|
+
const message = error.message;
|
|
911
|
+
return typeof message === "string" && message.includes("Unknown file extension");
|
|
912
|
+
}
|
|
913
|
+
async function loadTsModule(file, logger) {
|
|
914
|
+
const cacheBust = Date.now();
|
|
915
|
+
const fileUrl = `${node_url.pathToFileURL(file).href}?t=${cacheBust}`;
|
|
916
|
+
const registered = await ensureTsxRegister(logger);
|
|
917
|
+
if (registered) {
|
|
918
|
+
try {
|
|
919
|
+
return await import(fileUrl);
|
|
920
|
+
} catch (error) {
|
|
921
|
+
if (!isUnknownFileExtensionError(error)) {
|
|
922
|
+
throw error;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
const result = await esbuild.build({
|
|
927
|
+
entryPoints: [file],
|
|
928
|
+
bundle: true,
|
|
929
|
+
format: "esm",
|
|
930
|
+
platform: "node",
|
|
931
|
+
sourcemap: "inline",
|
|
932
|
+
target: "es2020",
|
|
933
|
+
write: false
|
|
934
|
+
});
|
|
935
|
+
const output = result.outputFiles[0];
|
|
936
|
+
const code = output?.text ?? "";
|
|
937
|
+
const dataUrl = `data:text/javascript;base64,${node_buffer.Buffer.from(code).toString(
|
|
938
|
+
"base64"
|
|
939
|
+
)}`;
|
|
940
|
+
return import(`${dataUrl}#${cacheBust}`);
|
|
941
|
+
}
|
|
942
|
+
async function loadModule(file, logger) {
|
|
943
|
+
const ext = pathe.extname(file).toLowerCase();
|
|
944
|
+
if (ext === ".cjs") {
|
|
945
|
+
const require$1 = node_module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('fetch-server.cjs', document.baseURI).href)));
|
|
946
|
+
delete require$1.cache[file];
|
|
947
|
+
return require$1(file);
|
|
948
|
+
}
|
|
949
|
+
if (ext === ".js" || ext === ".mjs") {
|
|
950
|
+
return import(`${node_url.pathToFileURL(file).href}?t=${Date.now()}`);
|
|
951
|
+
}
|
|
952
|
+
if (ext === ".ts") {
|
|
953
|
+
return loadTsModule(file, logger);
|
|
954
|
+
}
|
|
955
|
+
return null;
|
|
956
|
+
}
|
|
957
|
+
async function readJsonFile(file, logger) {
|
|
958
|
+
try {
|
|
959
|
+
const content = await node_fs.promises.readFile(file, "utf8");
|
|
960
|
+
const errors = [];
|
|
961
|
+
const data = jsoncParser.parse(content, errors, {
|
|
962
|
+
allowTrailingComma: true,
|
|
963
|
+
disallowComments: false
|
|
964
|
+
});
|
|
965
|
+
if (errors.length > 0) {
|
|
966
|
+
logger.warn(`Invalid JSONC in ${file}`);
|
|
967
|
+
return void 0;
|
|
968
|
+
}
|
|
969
|
+
return data;
|
|
970
|
+
} catch (error) {
|
|
971
|
+
logger.warn(`Failed to read ${file}: ${String(error)}`);
|
|
972
|
+
return void 0;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
async function loadRules(file, logger) {
|
|
976
|
+
const ext = pathe.extname(file).toLowerCase();
|
|
977
|
+
if (ext === ".json" || ext === ".jsonc") {
|
|
978
|
+
const json = await readJsonFile(file, logger);
|
|
979
|
+
if (typeof json === "undefined") {
|
|
980
|
+
return [];
|
|
981
|
+
}
|
|
982
|
+
return [
|
|
983
|
+
{
|
|
984
|
+
handler: json
|
|
985
|
+
}
|
|
986
|
+
];
|
|
987
|
+
}
|
|
988
|
+
const mod = await loadModule(file, logger);
|
|
989
|
+
const value = mod?.default ?? mod;
|
|
990
|
+
if (!value) {
|
|
991
|
+
return [];
|
|
992
|
+
}
|
|
993
|
+
if (Array.isArray(value)) {
|
|
994
|
+
return value;
|
|
995
|
+
}
|
|
996
|
+
if (typeof value === "function") {
|
|
997
|
+
return [
|
|
998
|
+
{
|
|
999
|
+
handler: value
|
|
1000
|
+
}
|
|
1001
|
+
];
|
|
1002
|
+
}
|
|
1003
|
+
return [value];
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
const silentLogger = {
|
|
1007
|
+
info: () => {
|
|
1008
|
+
},
|
|
1009
|
+
warn: () => {
|
|
1010
|
+
},
|
|
1011
|
+
error: () => {
|
|
1012
|
+
}
|
|
1013
|
+
};
|
|
1014
|
+
function resolveSkipRoute(params) {
|
|
1015
|
+
const derived = params.derived ?? deriveRouteFromFile(params.file, params.rootDir, silentLogger);
|
|
1016
|
+
if (!derived?.method) {
|
|
1017
|
+
return null;
|
|
1018
|
+
}
|
|
1019
|
+
const resolved = resolveRule({
|
|
1020
|
+
rule: { handler: null },
|
|
1021
|
+
derivedTemplate: derived.template,
|
|
1022
|
+
derivedMethod: derived.method,
|
|
1023
|
+
prefix: params.prefix,
|
|
1024
|
+
file: params.file,
|
|
1025
|
+
logger: silentLogger
|
|
1026
|
+
});
|
|
1027
|
+
if (!resolved) {
|
|
1028
|
+
return null;
|
|
1029
|
+
}
|
|
1030
|
+
return {
|
|
1031
|
+
method: resolved.method,
|
|
1032
|
+
url: resolved.template
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
function buildSkipInfo(file, reason, resolved) {
|
|
1036
|
+
const info = { file, reason };
|
|
1037
|
+
if (resolved) {
|
|
1038
|
+
info.method = resolved.method;
|
|
1039
|
+
info.url = resolved.url;
|
|
1040
|
+
}
|
|
1041
|
+
return info;
|
|
1042
|
+
}
|
|
1043
|
+
async function scanRoutes(params) {
|
|
1044
|
+
const routes = [];
|
|
1045
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1046
|
+
const files = await collectFiles(params.dirs);
|
|
1047
|
+
const globalIgnorePrefix = normalizeIgnorePrefix(params.ignorePrefix);
|
|
1048
|
+
const configCache = /* @__PURE__ */ new Map();
|
|
1049
|
+
const fileCache = /* @__PURE__ */ new Map();
|
|
1050
|
+
const shouldCollectSkip = typeof params.onSkip === "function";
|
|
1051
|
+
const shouldCollectIgnore = typeof params.onIgnore === "function";
|
|
1052
|
+
const shouldCollectConfig = typeof params.onConfig === "function";
|
|
1053
|
+
for (const fileInfo of files) {
|
|
1054
|
+
if (isConfigFile(fileInfo.file)) {
|
|
1055
|
+
if (shouldCollectConfig) {
|
|
1056
|
+
const config2 = await resolveDirectoryConfig({
|
|
1057
|
+
file: fileInfo.file,
|
|
1058
|
+
rootDir: fileInfo.rootDir,
|
|
1059
|
+
logger: params.logger,
|
|
1060
|
+
configCache,
|
|
1061
|
+
fileCache
|
|
1062
|
+
});
|
|
1063
|
+
params.onConfig?.({ file: fileInfo.file, enabled: config2.enabled !== false });
|
|
1064
|
+
}
|
|
1065
|
+
continue;
|
|
1066
|
+
}
|
|
1067
|
+
const config = await resolveDirectoryConfig({
|
|
1068
|
+
file: fileInfo.file,
|
|
1069
|
+
rootDir: fileInfo.rootDir,
|
|
1070
|
+
logger: params.logger,
|
|
1071
|
+
configCache,
|
|
1072
|
+
fileCache
|
|
1073
|
+
});
|
|
1074
|
+
if (config.enabled === false) {
|
|
1075
|
+
if (shouldCollectSkip && isSupportedFile(fileInfo.file)) {
|
|
1076
|
+
const resolved = resolveSkipRoute({
|
|
1077
|
+
file: fileInfo.file,
|
|
1078
|
+
rootDir: fileInfo.rootDir,
|
|
1079
|
+
prefix: params.prefix
|
|
1080
|
+
});
|
|
1081
|
+
params.onSkip?.(buildSkipInfo(fileInfo.file, "disabled-dir", resolved));
|
|
1082
|
+
}
|
|
1083
|
+
continue;
|
|
1084
|
+
}
|
|
1085
|
+
const effectiveIgnorePrefix = typeof config.ignorePrefix !== "undefined" ? normalizeIgnorePrefix(config.ignorePrefix, []) : globalIgnorePrefix;
|
|
1086
|
+
if (hasIgnoredPrefix(fileInfo.file, fileInfo.rootDir, effectiveIgnorePrefix)) {
|
|
1087
|
+
if (shouldCollectSkip && isSupportedFile(fileInfo.file)) {
|
|
1088
|
+
const resolved = resolveSkipRoute({
|
|
1089
|
+
file: fileInfo.file,
|
|
1090
|
+
rootDir: fileInfo.rootDir,
|
|
1091
|
+
prefix: params.prefix
|
|
1092
|
+
});
|
|
1093
|
+
params.onSkip?.(buildSkipInfo(fileInfo.file, "ignore-prefix", resolved));
|
|
1094
|
+
}
|
|
1095
|
+
continue;
|
|
1096
|
+
}
|
|
1097
|
+
if (!isSupportedFile(fileInfo.file)) {
|
|
1098
|
+
if (shouldCollectIgnore) {
|
|
1099
|
+
params.onIgnore?.({ file: fileInfo.file, reason: "unsupported" });
|
|
1100
|
+
}
|
|
1101
|
+
continue;
|
|
1102
|
+
}
|
|
1103
|
+
const effectiveInclude = typeof config.include !== "undefined" ? config.include : params.include;
|
|
1104
|
+
const effectiveExclude = typeof config.exclude !== "undefined" ? config.exclude : params.exclude;
|
|
1105
|
+
if (!matchesFilter(fileInfo.file, effectiveInclude, effectiveExclude)) {
|
|
1106
|
+
if (shouldCollectSkip) {
|
|
1107
|
+
const resolved = resolveSkipRoute({
|
|
1108
|
+
file: fileInfo.file,
|
|
1109
|
+
rootDir: fileInfo.rootDir,
|
|
1110
|
+
prefix: params.prefix
|
|
1111
|
+
});
|
|
1112
|
+
const reason = effectiveExclude && matchesFilter(fileInfo.file, void 0, effectiveExclude) ? "exclude" : "include";
|
|
1113
|
+
params.onSkip?.(buildSkipInfo(fileInfo.file, reason, resolved));
|
|
1114
|
+
}
|
|
1115
|
+
continue;
|
|
1116
|
+
}
|
|
1117
|
+
const derived = deriveRouteFromFile(fileInfo.file, fileInfo.rootDir, params.logger);
|
|
1118
|
+
if (!derived) {
|
|
1119
|
+
if (shouldCollectIgnore) {
|
|
1120
|
+
params.onIgnore?.({ file: fileInfo.file, reason: "invalid-route" });
|
|
1121
|
+
}
|
|
1122
|
+
continue;
|
|
1123
|
+
}
|
|
1124
|
+
const rules = await loadRules(fileInfo.file, params.logger);
|
|
1125
|
+
for (const [index, rule] of rules.entries()) {
|
|
1126
|
+
if (!rule || typeof rule !== "object") {
|
|
1127
|
+
continue;
|
|
1128
|
+
}
|
|
1129
|
+
if (rule.enabled === false) {
|
|
1130
|
+
if (shouldCollectSkip) {
|
|
1131
|
+
const resolved2 = resolveSkipRoute({
|
|
1132
|
+
file: fileInfo.file,
|
|
1133
|
+
rootDir: fileInfo.rootDir,
|
|
1134
|
+
prefix: params.prefix,
|
|
1135
|
+
derived
|
|
1136
|
+
});
|
|
1137
|
+
params.onSkip?.(buildSkipInfo(fileInfo.file, "disabled", resolved2));
|
|
1138
|
+
}
|
|
1139
|
+
continue;
|
|
1140
|
+
}
|
|
1141
|
+
const ruleValue = rule;
|
|
1142
|
+
const unsupportedKeys = ["response", "url", "method"].filter(
|
|
1143
|
+
(key2) => key2 in ruleValue
|
|
1144
|
+
);
|
|
1145
|
+
if (unsupportedKeys.length > 0) {
|
|
1146
|
+
params.logger.warn(
|
|
1147
|
+
`Skip mock with unsupported fields (${unsupportedKeys.join(", ")}): ${fileInfo.file}`
|
|
1148
|
+
);
|
|
1149
|
+
continue;
|
|
1150
|
+
}
|
|
1151
|
+
if (typeof rule.handler === "undefined") {
|
|
1152
|
+
params.logger.warn(`Skip mock without handler: ${fileInfo.file}`);
|
|
1153
|
+
continue;
|
|
1154
|
+
}
|
|
1155
|
+
const resolved = resolveRule({
|
|
1156
|
+
rule,
|
|
1157
|
+
derivedTemplate: derived.template,
|
|
1158
|
+
derivedMethod: derived.method,
|
|
1159
|
+
prefix: params.prefix,
|
|
1160
|
+
file: fileInfo.file,
|
|
1161
|
+
logger: params.logger
|
|
1162
|
+
});
|
|
1163
|
+
if (!resolved) {
|
|
1164
|
+
continue;
|
|
1165
|
+
}
|
|
1166
|
+
resolved.ruleIndex = index;
|
|
1167
|
+
if (config.headers) {
|
|
1168
|
+
resolved.headers = { ...config.headers, ...resolved.headers ?? {} };
|
|
1169
|
+
}
|
|
1170
|
+
if (typeof resolved.status === "undefined" && typeof config.status === "number") {
|
|
1171
|
+
resolved.status = config.status;
|
|
1172
|
+
}
|
|
1173
|
+
if (typeof resolved.delay === "undefined" && typeof config.delay === "number") {
|
|
1174
|
+
resolved.delay = config.delay;
|
|
1175
|
+
}
|
|
1176
|
+
if (config.middlewares.length > 0) {
|
|
1177
|
+
resolved.middlewares = config.middlewares;
|
|
1178
|
+
}
|
|
1179
|
+
const key = `${resolved.method} ${resolved.template}`;
|
|
1180
|
+
if (seen.has(key)) {
|
|
1181
|
+
params.logger.warn(`Duplicate mock route ${key} from ${fileInfo.file}`);
|
|
1182
|
+
}
|
|
1183
|
+
seen.add(key);
|
|
1184
|
+
routes.push(resolved);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
return sortRoutes(routes);
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
function normalizeEntries(entries) {
|
|
1191
|
+
const list = Array.isArray(entries) ? entries : entries ? [entries] : [{}];
|
|
1192
|
+
return list.length > 0 ? list : [{}];
|
|
1193
|
+
}
|
|
1194
|
+
function normalizeOptions(options) {
|
|
1195
|
+
return {
|
|
1196
|
+
entries: normalizeEntries(options.entries),
|
|
1197
|
+
playground: options.playground
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
function resolveFirst(list, getter) {
|
|
1201
|
+
for (const entry of list) {
|
|
1202
|
+
const value = getter(entry);
|
|
1203
|
+
if (typeof value !== "undefined") {
|
|
1204
|
+
return value;
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
return void 0;
|
|
1208
|
+
}
|
|
1209
|
+
function resolveRoot(list) {
|
|
1210
|
+
const explicit = resolveFirst(list, (entry) => entry.root);
|
|
1211
|
+
if (explicit) {
|
|
1212
|
+
return explicit;
|
|
1213
|
+
}
|
|
1214
|
+
const deno = globalThis.Deno;
|
|
1215
|
+
if (deno?.cwd) {
|
|
1216
|
+
return deno.cwd();
|
|
1217
|
+
}
|
|
1218
|
+
return process.cwd();
|
|
1219
|
+
}
|
|
1220
|
+
function resolveAllDirs(list, root) {
|
|
1221
|
+
const dirs = [];
|
|
1222
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1223
|
+
for (const entry of list) {
|
|
1224
|
+
for (const dir of resolveDirs(entry.dir, root)) {
|
|
1225
|
+
if (seen.has(dir)) {
|
|
1226
|
+
continue;
|
|
1227
|
+
}
|
|
1228
|
+
seen.add(dir);
|
|
1229
|
+
dirs.push(dir);
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
return dirs;
|
|
1233
|
+
}
|
|
1234
|
+
function buildApp(params) {
|
|
1235
|
+
const app = new hono.Hono({ strict: false });
|
|
1236
|
+
registerPlaygroundRoutes({
|
|
1237
|
+
app,
|
|
1238
|
+
routes: params.routes,
|
|
1239
|
+
disabledRoutes: params.disabledRoutes,
|
|
1240
|
+
ignoredRoutes: params.ignoredRoutes,
|
|
1241
|
+
configFiles: params.configFiles,
|
|
1242
|
+
disabledConfigFiles: params.disabledConfigFiles,
|
|
1243
|
+
dirs: params.dirs,
|
|
1244
|
+
logger: params.logger,
|
|
1245
|
+
config: params.playground,
|
|
1246
|
+
root: params.root
|
|
1247
|
+
});
|
|
1248
|
+
if (params.wsHandler && params.playground.enabled) {
|
|
1249
|
+
app.get(`${params.playground.path}/ws`, params.wsHandler);
|
|
1250
|
+
}
|
|
1251
|
+
if (params.routes.length > 0) {
|
|
1252
|
+
const mockAppOptions = params.onResponse ? { onResponse: params.onResponse } : {};
|
|
1253
|
+
const mockApp = createHonoApp(params.routes, mockAppOptions);
|
|
1254
|
+
app.route("/", mockApp);
|
|
1255
|
+
}
|
|
1256
|
+
return app;
|
|
1257
|
+
}
|
|
1258
|
+
async function createDenoWatcher(params) {
|
|
1259
|
+
const deno = globalThis.Deno;
|
|
1260
|
+
if (!deno?.watchFs) {
|
|
1261
|
+
return null;
|
|
1262
|
+
}
|
|
1263
|
+
const watcher = deno.watchFs(params.dirs, { recursive: true });
|
|
1264
|
+
let closed = false;
|
|
1265
|
+
(async () => {
|
|
1266
|
+
try {
|
|
1267
|
+
for await (const event of watcher) {
|
|
1268
|
+
if (closed) {
|
|
1269
|
+
break;
|
|
1270
|
+
}
|
|
1271
|
+
if (event.kind === "access") {
|
|
1272
|
+
continue;
|
|
1273
|
+
}
|
|
1274
|
+
params.onChange();
|
|
1275
|
+
}
|
|
1276
|
+
} catch (error) {
|
|
1277
|
+
if (!closed) {
|
|
1278
|
+
params.logger.warn("Watcher failed:", error);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
})();
|
|
1282
|
+
return {
|
|
1283
|
+
close: async () => {
|
|
1284
|
+
closed = true;
|
|
1285
|
+
watcher.close();
|
|
1286
|
+
}
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
async function createChokidarWatcher(params) {
|
|
1290
|
+
try {
|
|
1291
|
+
const { default: chokidar } = await import('@mokup/shared/chokidar');
|
|
1292
|
+
const watcher = chokidar.watch(params.dirs, { ignoreInitial: true });
|
|
1293
|
+
watcher.on("add", (file) => {
|
|
1294
|
+
if (isInDirs(file, params.dirs)) {
|
|
1295
|
+
params.onChange();
|
|
1296
|
+
}
|
|
1297
|
+
});
|
|
1298
|
+
watcher.on("change", (file) => {
|
|
1299
|
+
if (isInDirs(file, params.dirs)) {
|
|
1300
|
+
params.onChange();
|
|
1301
|
+
}
|
|
1302
|
+
});
|
|
1303
|
+
watcher.on("unlink", (file) => {
|
|
1304
|
+
if (isInDirs(file, params.dirs)) {
|
|
1305
|
+
params.onChange();
|
|
1306
|
+
}
|
|
1307
|
+
});
|
|
1308
|
+
return {
|
|
1309
|
+
close: async () => {
|
|
1310
|
+
await watcher.close();
|
|
1311
|
+
}
|
|
1312
|
+
};
|
|
1313
|
+
} catch {
|
|
1314
|
+
return null;
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
async function createWatcher(params) {
|
|
1318
|
+
if (!params.enabled || params.dirs.length === 0) {
|
|
1319
|
+
return null;
|
|
1320
|
+
}
|
|
1321
|
+
const denoWatcher = await createDenoWatcher(params);
|
|
1322
|
+
if (denoWatcher) {
|
|
1323
|
+
return denoWatcher;
|
|
1324
|
+
}
|
|
1325
|
+
const chokidarWatcher = await createChokidarWatcher(params);
|
|
1326
|
+
if (chokidarWatcher) {
|
|
1327
|
+
return chokidarWatcher;
|
|
1328
|
+
}
|
|
1329
|
+
params.logger.warn("Watcher is not available in this runtime; file watching disabled.");
|
|
1330
|
+
return null;
|
|
1331
|
+
}
|
|
1332
|
+
async function createFetchServer(options = {}) {
|
|
1333
|
+
const normalized = normalizeOptions(options);
|
|
1334
|
+
const optionList = normalized.entries;
|
|
1335
|
+
const root = resolveRoot(optionList);
|
|
1336
|
+
const logEnabled = optionList.every((entry) => entry.log !== false);
|
|
1337
|
+
const watchEnabled = optionList.every((entry) => entry.watch !== false);
|
|
1338
|
+
const logger = createLogger(logEnabled);
|
|
1339
|
+
const playgroundConfig = resolvePlaygroundOptions(normalized.playground);
|
|
1340
|
+
const dirs = resolveAllDirs(optionList, root);
|
|
1341
|
+
const routeCounts = {};
|
|
1342
|
+
const wsClients = /* @__PURE__ */ new Set();
|
|
1343
|
+
let totalCount = 0;
|
|
1344
|
+
let wsHandler;
|
|
1345
|
+
let injectWebSocket;
|
|
1346
|
+
function getRouteKey(route) {
|
|
1347
|
+
return `${route.method} ${route.template}`;
|
|
1348
|
+
}
|
|
1349
|
+
function buildSnapshot() {
|
|
1350
|
+
return {
|
|
1351
|
+
type: "snapshot",
|
|
1352
|
+
total: totalCount,
|
|
1353
|
+
perRoute: { ...routeCounts }
|
|
1354
|
+
};
|
|
1355
|
+
}
|
|
1356
|
+
function broadcast(payload) {
|
|
1357
|
+
if (wsClients.size === 0) {
|
|
1358
|
+
return;
|
|
1359
|
+
}
|
|
1360
|
+
const message = JSON.stringify(payload);
|
|
1361
|
+
for (const client of wsClients) {
|
|
1362
|
+
try {
|
|
1363
|
+
client.send(message);
|
|
1364
|
+
} catch {
|
|
1365
|
+
wsClients.delete(client);
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
function registerWsClient(client) {
|
|
1370
|
+
wsClients.add(client);
|
|
1371
|
+
try {
|
|
1372
|
+
client.send(JSON.stringify(buildSnapshot()));
|
|
1373
|
+
} catch {
|
|
1374
|
+
wsClients.delete(client);
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
function handleRouteResponse(route) {
|
|
1378
|
+
const routeKey = getRouteKey(route);
|
|
1379
|
+
routeCounts[routeKey] = (routeCounts[routeKey] ?? 0) + 1;
|
|
1380
|
+
totalCount += 1;
|
|
1381
|
+
broadcast({ type: "increment", routeKey, total: totalCount });
|
|
1382
|
+
}
|
|
1383
|
+
async function setupPlaygroundWebSocket(app2) {
|
|
1384
|
+
if (!playgroundConfig.enabled) {
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1387
|
+
try {
|
|
1388
|
+
const mod = await import('@hono/node-ws');
|
|
1389
|
+
const { createNodeWebSocket } = mod;
|
|
1390
|
+
const { upgradeWebSocket, injectWebSocket: inject } = createNodeWebSocket({ app: app2 });
|
|
1391
|
+
wsHandler = upgradeWebSocket(() => ({
|
|
1392
|
+
onOpen: (_event, ws) => {
|
|
1393
|
+
registerWsClient(ws);
|
|
1394
|
+
},
|
|
1395
|
+
onClose: (_event, ws) => {
|
|
1396
|
+
wsClients.delete(ws);
|
|
1397
|
+
},
|
|
1398
|
+
onMessage: () => {
|
|
1399
|
+
}
|
|
1400
|
+
}));
|
|
1401
|
+
injectWebSocket = (server2) => {
|
|
1402
|
+
inject(server2);
|
|
1403
|
+
};
|
|
1404
|
+
} catch {
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
let routes = [];
|
|
1408
|
+
let disabledRoutes = [];
|
|
1409
|
+
let ignoredRoutes = [];
|
|
1410
|
+
let configFiles = [];
|
|
1411
|
+
let disabledConfigFiles = [];
|
|
1412
|
+
let app = buildApp({
|
|
1413
|
+
routes,
|
|
1414
|
+
disabledRoutes,
|
|
1415
|
+
ignoredRoutes,
|
|
1416
|
+
configFiles,
|
|
1417
|
+
disabledConfigFiles,
|
|
1418
|
+
dirs,
|
|
1419
|
+
playground: playgroundConfig,
|
|
1420
|
+
root,
|
|
1421
|
+
logger,
|
|
1422
|
+
onResponse: handleRouteResponse,
|
|
1423
|
+
...wsHandler ? { wsHandler } : {}
|
|
1424
|
+
});
|
|
1425
|
+
const refreshRoutes = async () => {
|
|
1426
|
+
try {
|
|
1427
|
+
const collected = [];
|
|
1428
|
+
const collectedDisabled = [];
|
|
1429
|
+
const collectedIgnored = [];
|
|
1430
|
+
const collectedConfigs = [];
|
|
1431
|
+
for (const entry of optionList) {
|
|
1432
|
+
const scanParams = {
|
|
1433
|
+
dirs: resolveDirs(entry.dir, root),
|
|
1434
|
+
prefix: entry.prefix ?? "",
|
|
1435
|
+
logger,
|
|
1436
|
+
onSkip: (info) => collectedDisabled.push(info),
|
|
1437
|
+
onIgnore: (info) => collectedIgnored.push(info),
|
|
1438
|
+
onConfig: (info) => collectedConfigs.push(info)
|
|
1439
|
+
};
|
|
1440
|
+
if (entry.include) {
|
|
1441
|
+
scanParams.include = entry.include;
|
|
1442
|
+
}
|
|
1443
|
+
if (entry.exclude) {
|
|
1444
|
+
scanParams.exclude = entry.exclude;
|
|
1445
|
+
}
|
|
1446
|
+
if (typeof entry.ignorePrefix !== "undefined") {
|
|
1447
|
+
scanParams.ignorePrefix = entry.ignorePrefix;
|
|
1448
|
+
}
|
|
1449
|
+
const scanned = await scanRoutes(scanParams);
|
|
1450
|
+
collected.push(...scanned);
|
|
1451
|
+
}
|
|
1452
|
+
const resolvedRoutes = sortRoutes(collected);
|
|
1453
|
+
routes = resolvedRoutes;
|
|
1454
|
+
disabledRoutes = collectedDisabled;
|
|
1455
|
+
ignoredRoutes = collectedIgnored;
|
|
1456
|
+
const configMap = new Map(collectedConfigs.map((entry) => [entry.file, entry]));
|
|
1457
|
+
const resolvedConfigs = Array.from(configMap.values());
|
|
1458
|
+
configFiles = resolvedConfigs.filter((entry) => entry.enabled);
|
|
1459
|
+
disabledConfigFiles = resolvedConfigs.filter((entry) => !entry.enabled);
|
|
1460
|
+
app = buildApp({
|
|
1461
|
+
routes,
|
|
1462
|
+
disabledRoutes,
|
|
1463
|
+
ignoredRoutes,
|
|
1464
|
+
configFiles,
|
|
1465
|
+
disabledConfigFiles,
|
|
1466
|
+
dirs,
|
|
1467
|
+
playground: playgroundConfig,
|
|
1468
|
+
root,
|
|
1469
|
+
logger,
|
|
1470
|
+
onResponse: handleRouteResponse,
|
|
1471
|
+
...wsHandler ? { wsHandler } : {}
|
|
1472
|
+
});
|
|
1473
|
+
logger.info(`Loaded ${routes.length} mock routes.`);
|
|
1474
|
+
} catch (error) {
|
|
1475
|
+
logger.error("Failed to scan mock routes:", error);
|
|
1476
|
+
}
|
|
1477
|
+
};
|
|
1478
|
+
await refreshRoutes();
|
|
1479
|
+
await setupPlaygroundWebSocket(app);
|
|
1480
|
+
if (wsHandler && playgroundConfig.enabled) {
|
|
1481
|
+
app.get(`${playgroundConfig.path}/ws`, wsHandler);
|
|
1482
|
+
}
|
|
1483
|
+
const scheduleRefresh = createDebouncer(80, () => {
|
|
1484
|
+
void refreshRoutes();
|
|
1485
|
+
});
|
|
1486
|
+
const watcher = await createWatcher({
|
|
1487
|
+
enabled: watchEnabled,
|
|
1488
|
+
dirs,
|
|
1489
|
+
onChange: scheduleRefresh,
|
|
1490
|
+
logger
|
|
1491
|
+
});
|
|
1492
|
+
const fetch = async (request) => await app.fetch(request);
|
|
1493
|
+
const server = {
|
|
1494
|
+
fetch,
|
|
1495
|
+
refresh: refreshRoutes,
|
|
1496
|
+
getRoutes: () => routes,
|
|
1497
|
+
...injectWebSocket ? { injectWebSocket } : {}
|
|
1498
|
+
};
|
|
1499
|
+
if (watcher) {
|
|
1500
|
+
server.close = async () => {
|
|
1501
|
+
await watcher.close();
|
|
1502
|
+
};
|
|
1503
|
+
}
|
|
1504
|
+
return server;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
exports.createFetchServer = createFetchServer;
|