mokup 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -49
- package/README.zh-CN.md +7 -0
- package/dist/index.d.cts +14 -19
- package/dist/index.d.mts +14 -19
- package/dist/index.d.ts +14 -19
- package/dist/vite.cjs +692 -270
- package/dist/vite.d.cts +5 -5
- package/dist/vite.d.mts +5 -5
- package/dist/vite.d.ts +5 -5
- package/dist/vite.mjs +693 -271
- package/package.json +8 -3
package/dist/vite.mjs
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { cwd } from 'node:process';
|
|
2
2
|
import chokidar from 'chokidar';
|
|
3
3
|
import { Buffer } from 'node:buffer';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { Hono } from 'hono';
|
|
5
|
+
import { PatternRouter } from 'hono/router/pattern-router';
|
|
6
|
+
import { resolve, isAbsolute, join, normalize, extname, dirname, relative, basename } from 'pathe';
|
|
6
7
|
import { promises } from 'node:fs';
|
|
7
8
|
import { createRequire } from 'node:module';
|
|
9
|
+
import { compareRouteScore, parseRouteTemplate } from '@mokup/runtime';
|
|
8
10
|
import { pathToFileURL } from 'node:url';
|
|
9
11
|
import { build } from 'esbuild';
|
|
10
12
|
import { parse } from 'jsonc-parser';
|
|
@@ -115,25 +117,113 @@ function delay(ms) {
|
|
|
115
117
|
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
116
118
|
}
|
|
117
119
|
|
|
118
|
-
function
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
120
|
+
function toHonoPath(route) {
|
|
121
|
+
if (!route.tokens || route.tokens.length === 0) {
|
|
122
|
+
return "/";
|
|
123
|
+
}
|
|
124
|
+
const segments = route.tokens.map((token) => {
|
|
125
|
+
if (token.type === "static") {
|
|
126
|
+
return token.value;
|
|
127
|
+
}
|
|
128
|
+
if (token.type === "param") {
|
|
129
|
+
return `:${token.name}`;
|
|
130
|
+
}
|
|
131
|
+
if (token.type === "catchall") {
|
|
132
|
+
return `:${token.name}{.+}`;
|
|
133
|
+
}
|
|
134
|
+
return `:${token.name}{.+}?`;
|
|
135
|
+
});
|
|
136
|
+
return `/${segments.join("/")}`;
|
|
137
|
+
}
|
|
138
|
+
function applyRouteOverrides(response, route) {
|
|
139
|
+
const headers = new Headers(response.headers);
|
|
140
|
+
const hasHeaders = !!route.headers && Object.keys(route.headers).length > 0;
|
|
141
|
+
if (route.headers) {
|
|
142
|
+
for (const [key, value] of Object.entries(route.headers)) {
|
|
143
|
+
headers.set(key, value);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const status = route.status ?? response.status;
|
|
147
|
+
if (status === response.status && !hasHeaders) {
|
|
148
|
+
return response;
|
|
149
|
+
}
|
|
150
|
+
return new Response(response.body, { status, headers });
|
|
151
|
+
}
|
|
152
|
+
function normalizeHandlerValue(c, value) {
|
|
153
|
+
if (value instanceof Response) {
|
|
154
|
+
return value;
|
|
155
|
+
}
|
|
156
|
+
if (typeof value === "undefined") {
|
|
157
|
+
const response = c.body(null);
|
|
158
|
+
if (response.status === 200) {
|
|
159
|
+
return new Response(response.body, {
|
|
160
|
+
status: 204,
|
|
161
|
+
headers: response.headers
|
|
162
|
+
});
|
|
128
163
|
}
|
|
164
|
+
return response;
|
|
129
165
|
}
|
|
130
|
-
|
|
166
|
+
if (typeof value === "string") {
|
|
167
|
+
return c.text(value);
|
|
168
|
+
}
|
|
169
|
+
if (value instanceof Uint8Array || value instanceof ArrayBuffer) {
|
|
170
|
+
if (!c.res.headers.get("content-type")) {
|
|
171
|
+
c.header("content-type", "application/octet-stream");
|
|
172
|
+
}
|
|
173
|
+
const data = value instanceof ArrayBuffer ? new Uint8Array(value) : new Uint8Array(value);
|
|
174
|
+
return c.body(data);
|
|
175
|
+
}
|
|
176
|
+
return c.json(value);
|
|
177
|
+
}
|
|
178
|
+
function createRouteHandler(route) {
|
|
179
|
+
return async (c) => {
|
|
180
|
+
const value = typeof route.handler === "function" ? await route.handler(c) : route.handler;
|
|
181
|
+
return normalizeHandlerValue(c, value);
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function createFinalizeMiddleware(route) {
|
|
185
|
+
return async (c, next) => {
|
|
186
|
+
const response = await next();
|
|
187
|
+
const resolved = response ?? c.res;
|
|
188
|
+
if (route.delay && route.delay > 0) {
|
|
189
|
+
await delay(route.delay);
|
|
190
|
+
}
|
|
191
|
+
return applyRouteOverrides(resolved, route);
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function wrapMiddleware(handler) {
|
|
195
|
+
return async (c, next) => {
|
|
196
|
+
const response = await handler(c, next);
|
|
197
|
+
return response ?? c.res;
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function createHonoApp(routes) {
|
|
201
|
+
const app = new Hono({ router: new PatternRouter(), strict: false });
|
|
202
|
+
for (const route of routes) {
|
|
203
|
+
const middlewares = route.middlewares?.map((entry) => wrapMiddleware(entry.handle)) ?? [];
|
|
204
|
+
app.on(
|
|
205
|
+
route.method,
|
|
206
|
+
toHonoPath(route),
|
|
207
|
+
createFinalizeMiddleware(route),
|
|
208
|
+
...middlewares,
|
|
209
|
+
createRouteHandler(route)
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
return app;
|
|
131
213
|
}
|
|
132
214
|
async function readRawBody(req) {
|
|
133
|
-
return new Promise((resolve, reject) => {
|
|
215
|
+
return await new Promise((resolve, reject) => {
|
|
134
216
|
const chunks = [];
|
|
135
217
|
req.on("data", (chunk) => {
|
|
136
|
-
|
|
218
|
+
if (typeof chunk === "string") {
|
|
219
|
+
chunks.push(Buffer.from(chunk));
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (chunk instanceof Uint8Array) {
|
|
223
|
+
chunks.push(chunk);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
chunks.push(Buffer.from(String(chunk)));
|
|
137
227
|
});
|
|
138
228
|
req.on("end", () => {
|
|
139
229
|
if (chunks.length === 0) {
|
|
@@ -145,121 +235,74 @@ async function readRawBody(req) {
|
|
|
145
235
|
req.on("error", reject);
|
|
146
236
|
});
|
|
147
237
|
}
|
|
148
|
-
|
|
149
|
-
const
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if (contentType === "application/json" || contentType.endsWith("+json")) {
|
|
159
|
-
try {
|
|
160
|
-
return { query, body: JSON.parse(rawText), rawBody: rawText };
|
|
161
|
-
} catch {
|
|
162
|
-
return { query, body: rawText, rawBody: rawText };
|
|
238
|
+
function buildHeaders(headers) {
|
|
239
|
+
const result = new Headers();
|
|
240
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
241
|
+
if (typeof value === "undefined") {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (Array.isArray(value)) {
|
|
245
|
+
result.set(key, value.join(","));
|
|
246
|
+
} else {
|
|
247
|
+
result.set(key, value);
|
|
163
248
|
}
|
|
164
249
|
}
|
|
165
|
-
|
|
166
|
-
const params = new URLSearchParams(rawText);
|
|
167
|
-
const body = Object.fromEntries(params.entries());
|
|
168
|
-
return { query, body, rawBody: rawText };
|
|
169
|
-
}
|
|
170
|
-
return { query, body: rawText, rawBody: rawText };
|
|
250
|
+
return result;
|
|
171
251
|
}
|
|
172
|
-
function
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
252
|
+
async function toRequest(req) {
|
|
253
|
+
const url = new URL(req.url ?? "/", "http://mokup.local");
|
|
254
|
+
const method = req.method ?? "GET";
|
|
255
|
+
const headers = buildHeaders(req.headers);
|
|
256
|
+
const init = { method, headers };
|
|
257
|
+
const rawBody = await readRawBody(req);
|
|
258
|
+
if (rawBody && method !== "GET" && method !== "HEAD") {
|
|
259
|
+
init.body = rawBody;
|
|
178
260
|
}
|
|
261
|
+
return new Request(url.toString(), init);
|
|
179
262
|
}
|
|
180
|
-
function
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
263
|
+
async function sendResponse(res, response) {
|
|
264
|
+
res.statusCode = response.status;
|
|
265
|
+
response.headers.forEach((value, key) => {
|
|
266
|
+
res.setHeader(key, value);
|
|
267
|
+
});
|
|
268
|
+
if (!response.body) {
|
|
185
269
|
res.end();
|
|
186
270
|
return;
|
|
187
271
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
res.setHeader("Content-Type", "application/octet-stream");
|
|
191
|
-
}
|
|
192
|
-
res.end(body);
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
if (typeof body === "string") {
|
|
196
|
-
if (!res.getHeader("Content-Type")) {
|
|
197
|
-
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
198
|
-
}
|
|
199
|
-
res.end(body);
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
if (!res.getHeader("Content-Type")) {
|
|
203
|
-
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
204
|
-
}
|
|
205
|
-
res.end(JSON.stringify(body));
|
|
272
|
+
const buffer = new Uint8Array(await response.arrayBuffer());
|
|
273
|
+
res.end(buffer);
|
|
206
274
|
}
|
|
207
|
-
function
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
212
|
-
const matched = matchRouteTokens(route.tokens, pathname);
|
|
213
|
-
if (matched) {
|
|
214
|
-
return { route, params: matched.params };
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
return null;
|
|
275
|
+
function hasMatch(app, method, pathname) {
|
|
276
|
+
const matchMethod = method === "HEAD" ? "GET" : method;
|
|
277
|
+
const match = app.router.match(matchMethod, pathname);
|
|
278
|
+
return !!match && match[0].length > 0;
|
|
218
279
|
}
|
|
219
|
-
function createMiddleware(
|
|
280
|
+
function createMiddleware(getApp, logger) {
|
|
220
281
|
return async (req, res, next) => {
|
|
282
|
+
const app = getApp();
|
|
283
|
+
if (!app) {
|
|
284
|
+
return next();
|
|
285
|
+
}
|
|
221
286
|
const url = req.url ?? "/";
|
|
222
287
|
const parsedUrl = new URL(url, "http://mokup.local");
|
|
223
288
|
const pathname = parsedUrl.pathname;
|
|
224
289
|
const method = normalizeMethod(req.method) ?? "GET";
|
|
225
|
-
|
|
226
|
-
if (!matched) {
|
|
290
|
+
if (!hasMatch(app, method, pathname)) {
|
|
227
291
|
return next();
|
|
228
292
|
}
|
|
293
|
+
const startedAt = Date.now();
|
|
229
294
|
try {
|
|
230
|
-
const
|
|
231
|
-
const mockReq = {
|
|
232
|
-
url: pathname,
|
|
233
|
-
method,
|
|
234
|
-
headers: req.headers,
|
|
235
|
-
query,
|
|
236
|
-
body,
|
|
237
|
-
params: matched.params
|
|
238
|
-
};
|
|
239
|
-
if (rawBody) {
|
|
240
|
-
mockReq.rawBody = rawBody;
|
|
241
|
-
}
|
|
242
|
-
const ctx = {
|
|
243
|
-
delay: (ms) => delay(ms),
|
|
244
|
-
json: (data) => data
|
|
245
|
-
};
|
|
246
|
-
const startedAt = Date.now();
|
|
247
|
-
const responseValue = typeof matched.route.response === "function" ? await matched.route.response(mockReq, res, ctx) : matched.route.response;
|
|
295
|
+
const response = await app.fetch(await toRequest(req));
|
|
248
296
|
if (res.writableEnded) {
|
|
249
297
|
return;
|
|
250
298
|
}
|
|
251
|
-
|
|
252
|
-
await delay(matched.route.delay);
|
|
253
|
-
}
|
|
254
|
-
applyHeaders(res, matched.route.headers);
|
|
255
|
-
if (matched.route.status) {
|
|
256
|
-
res.statusCode = matched.route.status;
|
|
257
|
-
}
|
|
258
|
-
writeResponse(res, responseValue);
|
|
299
|
+
await sendResponse(res, response);
|
|
259
300
|
logger.info(`${method} ${pathname} ${Date.now() - startedAt}ms`);
|
|
260
301
|
} catch (error) {
|
|
261
|
-
res.
|
|
262
|
-
|
|
302
|
+
if (!res.headersSent) {
|
|
303
|
+
res.statusCode = 500;
|
|
304
|
+
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
305
|
+
}
|
|
263
306
|
res.end("Mock handler error");
|
|
264
307
|
logger.error("Mock handler failed:", error);
|
|
265
308
|
}
|
|
@@ -286,6 +329,43 @@ function normalizePlaygroundPath(value) {
|
|
|
286
329
|
const normalized = value.startsWith("/") ? value : `/${value}`;
|
|
287
330
|
return normalized.length > 1 && normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
|
|
288
331
|
}
|
|
332
|
+
function normalizeBase(base) {
|
|
333
|
+
if (!base || base === "/") {
|
|
334
|
+
return "";
|
|
335
|
+
}
|
|
336
|
+
return base.endsWith("/") ? base.slice(0, -1) : base;
|
|
337
|
+
}
|
|
338
|
+
function injectPlaygroundHmr(html, base) {
|
|
339
|
+
if (html.includes("mokup-playground-hmr")) {
|
|
340
|
+
return html;
|
|
341
|
+
}
|
|
342
|
+
const normalizedBase = normalizeBase(base);
|
|
343
|
+
const clientPath = `${normalizedBase}/@vite/client`;
|
|
344
|
+
const snippet = [
|
|
345
|
+
'<script type="module" id="mokup-playground-hmr">',
|
|
346
|
+
`import('${clientPath}').then(({ createHotContext }) => {`,
|
|
347
|
+
" const hot = createHotContext('/@mokup/playground')",
|
|
348
|
+
" hot.on('mokup:routes-changed', () => {",
|
|
349
|
+
" const api = window.__MOKUP_PLAYGROUND__",
|
|
350
|
+
" if (api && typeof api.reloadRoutes === 'function') {",
|
|
351
|
+
" api.reloadRoutes()",
|
|
352
|
+
" return",
|
|
353
|
+
" }",
|
|
354
|
+
" window.location.reload()",
|
|
355
|
+
" })",
|
|
356
|
+
"}).catch(() => {})",
|
|
357
|
+
"<\/script>"
|
|
358
|
+
].join("\n");
|
|
359
|
+
if (html.includes("</body>")) {
|
|
360
|
+
return html.replace("</body>", `${snippet}
|
|
361
|
+
</body>`);
|
|
362
|
+
}
|
|
363
|
+
return `${html}
|
|
364
|
+
${snippet}`;
|
|
365
|
+
}
|
|
366
|
+
function isViteDevServer$1(server) {
|
|
367
|
+
return !!server && "ws" in server;
|
|
368
|
+
}
|
|
289
369
|
function resolvePlaygroundOptions(playground) {
|
|
290
370
|
if (playground === false) {
|
|
291
371
|
return { enabled: false, path: "/_mokup" };
|
|
@@ -312,14 +392,105 @@ function sendFile(res, content, contentType) {
|
|
|
312
392
|
res.setHeader("Content-Type", contentType);
|
|
313
393
|
res.end(content);
|
|
314
394
|
}
|
|
315
|
-
function
|
|
395
|
+
function toPosixPath(value) {
|
|
396
|
+
return value.replace(/\\/g, "/");
|
|
397
|
+
}
|
|
398
|
+
function normalizePath(value) {
|
|
399
|
+
return toPosixPath(normalize(value));
|
|
400
|
+
}
|
|
401
|
+
function isAncestor(parent, child) {
|
|
402
|
+
const normalizedParent = normalizePath(parent).replace(/\/$/, "");
|
|
403
|
+
const normalizedChild = normalizePath(child);
|
|
404
|
+
return normalizedChild === normalizedParent || normalizedChild.startsWith(`${normalizedParent}/`);
|
|
405
|
+
}
|
|
406
|
+
function resolveGroupRoot(dirs, serverRoot) {
|
|
407
|
+
if (!dirs || dirs.length === 0) {
|
|
408
|
+
return serverRoot ?? cwd();
|
|
409
|
+
}
|
|
410
|
+
if (serverRoot) {
|
|
411
|
+
const normalizedRoot = normalizePath(serverRoot);
|
|
412
|
+
const canUseRoot = dirs.every((dir) => isAncestor(normalizedRoot, dir));
|
|
413
|
+
if (canUseRoot) {
|
|
414
|
+
return normalizedRoot;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
if (dirs.length === 1) {
|
|
418
|
+
return normalizePath(dirname(dirs[0]));
|
|
419
|
+
}
|
|
420
|
+
let common = normalizePath(dirs[0]);
|
|
421
|
+
for (const dir of dirs.slice(1)) {
|
|
422
|
+
const normalizedDir = normalizePath(dir);
|
|
423
|
+
while (common && !isAncestor(common, normalizedDir)) {
|
|
424
|
+
const parent = normalizePath(dirname(common));
|
|
425
|
+
if (parent === common) {
|
|
426
|
+
break;
|
|
427
|
+
}
|
|
428
|
+
common = parent;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
if (!common || common === "/") {
|
|
432
|
+
return serverRoot ?? cwd();
|
|
433
|
+
}
|
|
434
|
+
return common;
|
|
435
|
+
}
|
|
436
|
+
function formatRouteFile(file, root) {
|
|
437
|
+
if (!root) {
|
|
438
|
+
return toPosixPath(file);
|
|
439
|
+
}
|
|
440
|
+
const rel = toPosixPath(relative(root, file));
|
|
441
|
+
if (!rel || rel.startsWith("..")) {
|
|
442
|
+
return toPosixPath(file);
|
|
443
|
+
}
|
|
444
|
+
return rel;
|
|
445
|
+
}
|
|
446
|
+
function resolveGroups(dirs, root) {
|
|
447
|
+
const groups = [];
|
|
448
|
+
const seen = /* @__PURE__ */ new Set();
|
|
449
|
+
for (const dir of dirs) {
|
|
450
|
+
const normalized = normalizePath(dir);
|
|
451
|
+
if (seen.has(normalized)) {
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
seen.add(normalized);
|
|
455
|
+
const rel = toPosixPath(relative(root, normalized));
|
|
456
|
+
const label = rel && !rel.startsWith("..") ? rel : normalized;
|
|
457
|
+
groups.push({
|
|
458
|
+
key: normalized,
|
|
459
|
+
label,
|
|
460
|
+
path: normalized
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
return groups;
|
|
464
|
+
}
|
|
465
|
+
function resolveRouteGroup(routeFile, groups) {
|
|
466
|
+
if (groups.length === 0) {
|
|
467
|
+
return void 0;
|
|
468
|
+
}
|
|
469
|
+
const normalizedFile = toPosixPath(normalize(routeFile));
|
|
470
|
+
let matched;
|
|
471
|
+
for (const group of groups) {
|
|
472
|
+
if (normalizedFile === group.path || normalizedFile.startsWith(`${group.path}/`)) {
|
|
473
|
+
if (!matched || group.path.length > matched.path.length) {
|
|
474
|
+
matched = group;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return matched;
|
|
479
|
+
}
|
|
480
|
+
function toPlaygroundRoute(route, root, groups) {
|
|
481
|
+
const matchedGroup = resolveRouteGroup(route.file, groups);
|
|
482
|
+
const middlewareSources = route.middlewares?.map((entry) => formatRouteFile(entry.source, root));
|
|
316
483
|
return {
|
|
317
484
|
method: route.method,
|
|
318
485
|
url: route.template,
|
|
319
|
-
file: route.file,
|
|
320
|
-
type: typeof route.
|
|
486
|
+
file: formatRouteFile(route.file, root),
|
|
487
|
+
type: typeof route.handler === "function" ? "handler" : "static",
|
|
321
488
|
status: route.status,
|
|
322
|
-
delay: route.delay
|
|
489
|
+
delay: route.delay,
|
|
490
|
+
middlewareCount: middlewareSources?.length ?? 0,
|
|
491
|
+
middlewares: middlewareSources,
|
|
492
|
+
groupKey: matchedGroup?.key,
|
|
493
|
+
group: matchedGroup?.label
|
|
323
494
|
};
|
|
324
495
|
}
|
|
325
496
|
function createPlaygroundMiddleware(params) {
|
|
@@ -337,11 +508,20 @@ function createPlaygroundMiddleware(params) {
|
|
|
337
508
|
return next();
|
|
338
509
|
}
|
|
339
510
|
const subPath = pathname.slice(playgroundPath.length);
|
|
511
|
+
if (subPath === "") {
|
|
512
|
+
const suffix = url.search ?? "";
|
|
513
|
+
res.statusCode = 302;
|
|
514
|
+
res.setHeader("Location", `${playgroundPath}/${suffix}`);
|
|
515
|
+
res.end();
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
340
518
|
if (subPath === "" || subPath === "/" || subPath === "/index.html") {
|
|
341
519
|
try {
|
|
342
520
|
const html = await promises.readFile(indexPath, "utf8");
|
|
521
|
+
const server = params.getServer?.();
|
|
522
|
+
const output = isViteDevServer$1(server) ? injectPlaygroundHmr(html, server.config.base ?? "/") : html;
|
|
343
523
|
const contentType = mimeTypes[".html"] ?? "text/html; charset=utf-8";
|
|
344
|
-
sendFile(res,
|
|
524
|
+
sendFile(res, output, contentType);
|
|
345
525
|
} catch (error) {
|
|
346
526
|
params.logger.error("Failed to load playground index:", error);
|
|
347
527
|
res.statusCode = 500;
|
|
@@ -350,11 +530,16 @@ function createPlaygroundMiddleware(params) {
|
|
|
350
530
|
return;
|
|
351
531
|
}
|
|
352
532
|
if (subPath === "/routes") {
|
|
533
|
+
const server = params.getServer?.();
|
|
534
|
+
const dirs = params.getDirs?.() ?? [];
|
|
535
|
+
const baseRoot = resolveGroupRoot(dirs, server?.config?.root);
|
|
536
|
+
const groups = resolveGroups(dirs, baseRoot);
|
|
353
537
|
const routes = params.getRoutes();
|
|
354
538
|
sendJson(res, {
|
|
355
539
|
basePath: playgroundPath,
|
|
356
540
|
count: routes.length,
|
|
357
|
-
|
|
541
|
+
groups: groups.map((group) => ({ key: group.key, label: group.label })),
|
|
542
|
+
routes: routes.map((route) => toPlaygroundRoute(route, baseRoot, groups))
|
|
358
543
|
});
|
|
359
544
|
return;
|
|
360
545
|
}
|
|
@@ -377,6 +562,267 @@ function createPlaygroundMiddleware(params) {
|
|
|
377
562
|
};
|
|
378
563
|
}
|
|
379
564
|
|
|
565
|
+
const jsonExtensions = /* @__PURE__ */ new Set([".json", ".jsonc"]);
|
|
566
|
+
function resolveTemplate(template, prefix) {
|
|
567
|
+
const normalized = template.startsWith("/") ? template : `/${template}`;
|
|
568
|
+
if (!prefix) {
|
|
569
|
+
return normalized;
|
|
570
|
+
}
|
|
571
|
+
const normalizedPrefix = normalizePrefix(prefix);
|
|
572
|
+
if (!normalizedPrefix) {
|
|
573
|
+
return normalized;
|
|
574
|
+
}
|
|
575
|
+
if (normalized === normalizedPrefix || normalized.startsWith(`${normalizedPrefix}/`)) {
|
|
576
|
+
return normalized;
|
|
577
|
+
}
|
|
578
|
+
if (normalized === "/") {
|
|
579
|
+
return `${normalizedPrefix}/`;
|
|
580
|
+
}
|
|
581
|
+
return `${normalizedPrefix}${normalized}`;
|
|
582
|
+
}
|
|
583
|
+
function stripMethodSuffix(base) {
|
|
584
|
+
const segments = base.split(".");
|
|
585
|
+
const last = segments.at(-1);
|
|
586
|
+
if (last && methodSuffixSet.has(last.toLowerCase())) {
|
|
587
|
+
segments.pop();
|
|
588
|
+
return {
|
|
589
|
+
name: segments.join("."),
|
|
590
|
+
method: last.toUpperCase()
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
return {
|
|
594
|
+
name: base,
|
|
595
|
+
method: void 0
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
function deriveRouteFromFile(file, rootDir, logger) {
|
|
599
|
+
const rel = toPosix(relative(rootDir, file));
|
|
600
|
+
const ext = extname(rel);
|
|
601
|
+
const withoutExt = rel.slice(0, rel.length - ext.length);
|
|
602
|
+
const dir = dirname(withoutExt);
|
|
603
|
+
const base = basename(withoutExt);
|
|
604
|
+
const { name, method } = stripMethodSuffix(base);
|
|
605
|
+
const resolvedMethod = method ?? (jsonExtensions.has(ext) ? "GET" : void 0);
|
|
606
|
+
if (!resolvedMethod) {
|
|
607
|
+
logger.warn(`Skip mock without method suffix: ${file}`);
|
|
608
|
+
return null;
|
|
609
|
+
}
|
|
610
|
+
if (!name) {
|
|
611
|
+
logger.warn(`Skip mock with empty route name: ${file}`);
|
|
612
|
+
return null;
|
|
613
|
+
}
|
|
614
|
+
const joined = dir === "." ? name : join(dir, name);
|
|
615
|
+
const segments = toPosix(joined).split("/");
|
|
616
|
+
if (segments.at(-1) === "index") {
|
|
617
|
+
segments.pop();
|
|
618
|
+
}
|
|
619
|
+
const template = segments.length === 0 ? "/" : `/${segments.join("/")}`;
|
|
620
|
+
const parsed = parseRouteTemplate(template);
|
|
621
|
+
if (parsed.errors.length > 0) {
|
|
622
|
+
for (const error of parsed.errors) {
|
|
623
|
+
logger.warn(`${error} in ${file}`);
|
|
624
|
+
}
|
|
625
|
+
return null;
|
|
626
|
+
}
|
|
627
|
+
for (const warning of parsed.warnings) {
|
|
628
|
+
logger.warn(`${warning} in ${file}`);
|
|
629
|
+
}
|
|
630
|
+
return {
|
|
631
|
+
template: parsed.template,
|
|
632
|
+
method: resolvedMethod,
|
|
633
|
+
tokens: parsed.tokens,
|
|
634
|
+
score: parsed.score
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
function resolveRule(params) {
|
|
638
|
+
const method = params.derivedMethod;
|
|
639
|
+
if (!method) {
|
|
640
|
+
params.logger.warn(`Skip mock without method suffix: ${params.file}`);
|
|
641
|
+
return null;
|
|
642
|
+
}
|
|
643
|
+
const template = resolveTemplate(params.derivedTemplate, params.prefix);
|
|
644
|
+
const parsed = parseRouteTemplate(template);
|
|
645
|
+
if (parsed.errors.length > 0) {
|
|
646
|
+
for (const error of parsed.errors) {
|
|
647
|
+
params.logger.warn(`${error} in ${params.file}`);
|
|
648
|
+
}
|
|
649
|
+
return null;
|
|
650
|
+
}
|
|
651
|
+
for (const warning of parsed.warnings) {
|
|
652
|
+
params.logger.warn(`${warning} in ${params.file}`);
|
|
653
|
+
}
|
|
654
|
+
const route = {
|
|
655
|
+
file: params.file,
|
|
656
|
+
template: parsed.template,
|
|
657
|
+
method,
|
|
658
|
+
tokens: parsed.tokens,
|
|
659
|
+
score: parsed.score,
|
|
660
|
+
handler: params.rule.handler
|
|
661
|
+
};
|
|
662
|
+
if (typeof params.rule.status === "number") {
|
|
663
|
+
route.status = params.rule.status;
|
|
664
|
+
}
|
|
665
|
+
if (params.rule.headers) {
|
|
666
|
+
route.headers = params.rule.headers;
|
|
667
|
+
}
|
|
668
|
+
if (typeof params.rule.delay === "number") {
|
|
669
|
+
route.delay = params.rule.delay;
|
|
670
|
+
}
|
|
671
|
+
return route;
|
|
672
|
+
}
|
|
673
|
+
function sortRoutes(routes) {
|
|
674
|
+
return routes.sort((a, b) => {
|
|
675
|
+
if (a.method !== b.method) {
|
|
676
|
+
return a.method.localeCompare(b.method);
|
|
677
|
+
}
|
|
678
|
+
const scoreCompare = compareRouteScore(a.score, b.score);
|
|
679
|
+
if (scoreCompare !== 0) {
|
|
680
|
+
return scoreCompare;
|
|
681
|
+
}
|
|
682
|
+
return a.template.localeCompare(b.template);
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const configExtensions = [".ts", ".js", ".mjs", ".cjs"];
|
|
687
|
+
async function loadModule$1(file) {
|
|
688
|
+
const ext = configExtensions.find((extension) => file.endsWith(extension));
|
|
689
|
+
if (ext === ".cjs") {
|
|
690
|
+
const require = createRequire(import.meta.url);
|
|
691
|
+
delete require.cache[file];
|
|
692
|
+
return require(file);
|
|
693
|
+
}
|
|
694
|
+
if (ext === ".js" || ext === ".mjs") {
|
|
695
|
+
return import(`${pathToFileURL(file).href}?t=${Date.now()}`);
|
|
696
|
+
}
|
|
697
|
+
if (ext === ".ts") {
|
|
698
|
+
const result = await build({
|
|
699
|
+
entryPoints: [file],
|
|
700
|
+
bundle: true,
|
|
701
|
+
format: "esm",
|
|
702
|
+
platform: "node",
|
|
703
|
+
sourcemap: "inline",
|
|
704
|
+
target: "es2020",
|
|
705
|
+
write: false
|
|
706
|
+
});
|
|
707
|
+
const output = result.outputFiles[0];
|
|
708
|
+
const code = output?.text ?? "";
|
|
709
|
+
const dataUrl = `data:text/javascript;base64,${Buffer.from(code).toString(
|
|
710
|
+
"base64"
|
|
711
|
+
)}`;
|
|
712
|
+
return import(`${dataUrl}#${Date.now()}`);
|
|
713
|
+
}
|
|
714
|
+
return null;
|
|
715
|
+
}
|
|
716
|
+
async function loadModuleWithVite$1(server, file) {
|
|
717
|
+
const asDevServer = server;
|
|
718
|
+
if ("ssrLoadModule" in asDevServer) {
|
|
719
|
+
const moduleNode = asDevServer.moduleGraph.getModuleById(file);
|
|
720
|
+
if (moduleNode) {
|
|
721
|
+
asDevServer.moduleGraph.invalidateModule(moduleNode);
|
|
722
|
+
}
|
|
723
|
+
return asDevServer.ssrLoadModule(file);
|
|
724
|
+
}
|
|
725
|
+
return loadModule$1(file);
|
|
726
|
+
}
|
|
727
|
+
function getConfigFileCandidates(dir) {
|
|
728
|
+
return configExtensions.map((extension) => join(dir, `index.config${extension}`));
|
|
729
|
+
}
|
|
730
|
+
async function findConfigFile(dir, cache) {
|
|
731
|
+
const cached = cache.get(dir);
|
|
732
|
+
if (cached !== void 0) {
|
|
733
|
+
return cached;
|
|
734
|
+
}
|
|
735
|
+
for (const candidate of getConfigFileCandidates(dir)) {
|
|
736
|
+
try {
|
|
737
|
+
await promises.stat(candidate);
|
|
738
|
+
cache.set(dir, candidate);
|
|
739
|
+
return candidate;
|
|
740
|
+
} catch {
|
|
741
|
+
continue;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
cache.set(dir, null);
|
|
745
|
+
return null;
|
|
746
|
+
}
|
|
747
|
+
async function loadConfig(file, server) {
|
|
748
|
+
const mod = server ? await loadModuleWithVite$1(server, file) : await loadModule$1(file);
|
|
749
|
+
if (!mod) {
|
|
750
|
+
return null;
|
|
751
|
+
}
|
|
752
|
+
const value = mod?.default ?? mod;
|
|
753
|
+
if (!value || typeof value !== "object") {
|
|
754
|
+
return null;
|
|
755
|
+
}
|
|
756
|
+
return value;
|
|
757
|
+
}
|
|
758
|
+
function normalizeMiddlewares(value, source, logger) {
|
|
759
|
+
if (!value) {
|
|
760
|
+
return [];
|
|
761
|
+
}
|
|
762
|
+
const list = Array.isArray(value) ? value : [value];
|
|
763
|
+
const middlewares = [];
|
|
764
|
+
for (const entry of list) {
|
|
765
|
+
if (typeof entry !== "function") {
|
|
766
|
+
logger.warn(`Invalid middleware in ${source}`);
|
|
767
|
+
continue;
|
|
768
|
+
}
|
|
769
|
+
middlewares.push({ handle: entry, source });
|
|
770
|
+
}
|
|
771
|
+
return middlewares;
|
|
772
|
+
}
|
|
773
|
+
async function resolveDirectoryConfig(params) {
|
|
774
|
+
const { file, rootDir, server, logger, configCache, fileCache } = params;
|
|
775
|
+
const resolvedRoot = normalize(rootDir);
|
|
776
|
+
const resolvedFileDir = normalize(dirname(file));
|
|
777
|
+
const chain = [];
|
|
778
|
+
let current = resolvedFileDir;
|
|
779
|
+
while (true) {
|
|
780
|
+
chain.push(current);
|
|
781
|
+
if (current === resolvedRoot) {
|
|
782
|
+
break;
|
|
783
|
+
}
|
|
784
|
+
const parent = dirname(current);
|
|
785
|
+
if (parent === current) {
|
|
786
|
+
break;
|
|
787
|
+
}
|
|
788
|
+
current = parent;
|
|
789
|
+
}
|
|
790
|
+
chain.reverse();
|
|
791
|
+
const merged = { middlewares: [] };
|
|
792
|
+
for (const dir of chain) {
|
|
793
|
+
const configPath = await findConfigFile(dir, fileCache);
|
|
794
|
+
if (!configPath) {
|
|
795
|
+
continue;
|
|
796
|
+
}
|
|
797
|
+
let config = configCache.get(configPath);
|
|
798
|
+
if (config === void 0) {
|
|
799
|
+
config = await loadConfig(configPath, server);
|
|
800
|
+
configCache.set(configPath, config);
|
|
801
|
+
}
|
|
802
|
+
if (!config) {
|
|
803
|
+
logger.warn(`Invalid config in ${configPath}`);
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
806
|
+
if (config.headers) {
|
|
807
|
+
merged.headers = { ...merged.headers ?? {}, ...config.headers };
|
|
808
|
+
}
|
|
809
|
+
if (typeof config.status === "number") {
|
|
810
|
+
merged.status = config.status;
|
|
811
|
+
}
|
|
812
|
+
if (typeof config.delay === "number") {
|
|
813
|
+
merged.delay = config.delay;
|
|
814
|
+
}
|
|
815
|
+
if (typeof config.enabled === "boolean") {
|
|
816
|
+
merged.enabled = config.enabled;
|
|
817
|
+
}
|
|
818
|
+
const normalized = normalizeMiddlewares(config.middleware, configPath, logger);
|
|
819
|
+
if (normalized.length > 0) {
|
|
820
|
+
merged.middlewares.push(...normalized);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
return merged;
|
|
824
|
+
}
|
|
825
|
+
|
|
380
826
|
async function walkDir(dir, rootDir, files) {
|
|
381
827
|
const entries = await promises.readdir(dir, { withFileTypes: true });
|
|
382
828
|
for (const entry of entries) {
|
|
@@ -415,6 +861,9 @@ function isSupportedFile(file) {
|
|
|
415
861
|
if (file.endsWith(".d.ts")) {
|
|
416
862
|
return false;
|
|
417
863
|
}
|
|
864
|
+
if (basename(file).startsWith("index.config.")) {
|
|
865
|
+
return false;
|
|
866
|
+
}
|
|
418
867
|
const ext = extname(file).toLowerCase();
|
|
419
868
|
return supportedExtensions.has(ext);
|
|
420
869
|
}
|
|
@@ -486,7 +935,7 @@ async function loadRules(file, server, logger) {
|
|
|
486
935
|
}
|
|
487
936
|
return [
|
|
488
937
|
{
|
|
489
|
-
|
|
938
|
+
handler: json
|
|
490
939
|
}
|
|
491
940
|
];
|
|
492
941
|
}
|
|
@@ -501,149 +950,19 @@ async function loadRules(file, server, logger) {
|
|
|
501
950
|
if (typeof value === "function") {
|
|
502
951
|
return [
|
|
503
952
|
{
|
|
504
|
-
|
|
953
|
+
handler: value
|
|
505
954
|
}
|
|
506
955
|
];
|
|
507
956
|
}
|
|
508
957
|
return [value];
|
|
509
958
|
}
|
|
510
959
|
|
|
511
|
-
function resolveMethod(fileMethod, ruleMethod, logger) {
|
|
512
|
-
if (ruleMethod) {
|
|
513
|
-
const normalized = normalizeMethod(ruleMethod);
|
|
514
|
-
if (normalized) {
|
|
515
|
-
return normalized;
|
|
516
|
-
}
|
|
517
|
-
logger.warn(`Unknown method "${ruleMethod}", falling back to file method`);
|
|
518
|
-
}
|
|
519
|
-
if (fileMethod) {
|
|
520
|
-
return fileMethod;
|
|
521
|
-
}
|
|
522
|
-
return "GET";
|
|
523
|
-
}
|
|
524
|
-
function resolveTemplate(template, prefix) {
|
|
525
|
-
const normalized = template.startsWith("/") ? template : `/${template}`;
|
|
526
|
-
if (!prefix) {
|
|
527
|
-
return normalized;
|
|
528
|
-
}
|
|
529
|
-
const normalizedPrefix = normalizePrefix(prefix);
|
|
530
|
-
if (!normalizedPrefix) {
|
|
531
|
-
return normalized;
|
|
532
|
-
}
|
|
533
|
-
if (normalized === normalizedPrefix || normalized.startsWith(`${normalizedPrefix}/`)) {
|
|
534
|
-
return normalized;
|
|
535
|
-
}
|
|
536
|
-
if (normalized === "/") {
|
|
537
|
-
return `${normalizedPrefix}/`;
|
|
538
|
-
}
|
|
539
|
-
return `${normalizedPrefix}${normalized}`;
|
|
540
|
-
}
|
|
541
|
-
function stripMethodSuffix(base) {
|
|
542
|
-
const segments = base.split(".");
|
|
543
|
-
const last = segments.at(-1);
|
|
544
|
-
if (last && methodSuffixSet.has(last.toLowerCase())) {
|
|
545
|
-
segments.pop();
|
|
546
|
-
return {
|
|
547
|
-
name: segments.join("."),
|
|
548
|
-
method: last.toUpperCase()
|
|
549
|
-
};
|
|
550
|
-
}
|
|
551
|
-
return {
|
|
552
|
-
name: base,
|
|
553
|
-
method: void 0
|
|
554
|
-
};
|
|
555
|
-
}
|
|
556
|
-
function deriveRouteFromFile(file, rootDir, logger) {
|
|
557
|
-
const rel = toPosix(relative(rootDir, file));
|
|
558
|
-
const ext = extname(rel);
|
|
559
|
-
const withoutExt = rel.slice(0, rel.length - ext.length);
|
|
560
|
-
const dir = dirname(withoutExt);
|
|
561
|
-
const base = basename(withoutExt);
|
|
562
|
-
const { name, method } = stripMethodSuffix(base);
|
|
563
|
-
if (!method) {
|
|
564
|
-
logger.warn(`Skip mock without method suffix: ${file}`);
|
|
565
|
-
return null;
|
|
566
|
-
}
|
|
567
|
-
if (!name) {
|
|
568
|
-
logger.warn(`Skip mock with empty route name: ${file}`);
|
|
569
|
-
return null;
|
|
570
|
-
}
|
|
571
|
-
const joined = dir === "." ? name : join(dir, name);
|
|
572
|
-
const segments = toPosix(joined).split("/");
|
|
573
|
-
if (segments.at(-1) === "index") {
|
|
574
|
-
segments.pop();
|
|
575
|
-
}
|
|
576
|
-
const template = segments.length === 0 ? "/" : `/${segments.join("/")}`;
|
|
577
|
-
const parsed = parseRouteTemplate(template);
|
|
578
|
-
if (parsed.errors.length > 0) {
|
|
579
|
-
for (const error of parsed.errors) {
|
|
580
|
-
logger.warn(`${error} in ${file}`);
|
|
581
|
-
}
|
|
582
|
-
return null;
|
|
583
|
-
}
|
|
584
|
-
for (const warning of parsed.warnings) {
|
|
585
|
-
logger.warn(`${warning} in ${file}`);
|
|
586
|
-
}
|
|
587
|
-
return {
|
|
588
|
-
template: parsed.template,
|
|
589
|
-
method,
|
|
590
|
-
tokens: parsed.tokens,
|
|
591
|
-
score: parsed.score
|
|
592
|
-
};
|
|
593
|
-
}
|
|
594
|
-
function resolveRule(params) {
|
|
595
|
-
const method = resolveMethod(params.derivedMethod, params.rule.method, params.logger);
|
|
596
|
-
if (!method) {
|
|
597
|
-
params.logger.warn(`Invalid mock method in ${params.file}`);
|
|
598
|
-
return null;
|
|
599
|
-
}
|
|
600
|
-
const template = resolveTemplate(params.rule.url ?? params.derivedTemplate, params.prefix);
|
|
601
|
-
const parsed = parseRouteTemplate(template);
|
|
602
|
-
if (parsed.errors.length > 0) {
|
|
603
|
-
for (const error of parsed.errors) {
|
|
604
|
-
params.logger.warn(`${error} in ${params.file}`);
|
|
605
|
-
}
|
|
606
|
-
return null;
|
|
607
|
-
}
|
|
608
|
-
for (const warning of parsed.warnings) {
|
|
609
|
-
params.logger.warn(`${warning} in ${params.file}`);
|
|
610
|
-
}
|
|
611
|
-
const route = {
|
|
612
|
-
file: params.file,
|
|
613
|
-
template: parsed.template,
|
|
614
|
-
method,
|
|
615
|
-
tokens: parsed.tokens,
|
|
616
|
-
score: parsed.score,
|
|
617
|
-
response: params.rule.response
|
|
618
|
-
};
|
|
619
|
-
if (typeof params.rule.status === "number") {
|
|
620
|
-
route.status = params.rule.status;
|
|
621
|
-
}
|
|
622
|
-
if (params.rule.headers) {
|
|
623
|
-
route.headers = params.rule.headers;
|
|
624
|
-
}
|
|
625
|
-
if (typeof params.rule.delay === "number") {
|
|
626
|
-
route.delay = params.rule.delay;
|
|
627
|
-
}
|
|
628
|
-
return route;
|
|
629
|
-
}
|
|
630
|
-
function sortRoutes(routes) {
|
|
631
|
-
return routes.sort((a, b) => {
|
|
632
|
-
if (a.method !== b.method) {
|
|
633
|
-
return a.method.localeCompare(b.method);
|
|
634
|
-
}
|
|
635
|
-
const scoreCompare = compareRouteScore(a.score, b.score);
|
|
636
|
-
if (scoreCompare !== 0) {
|
|
637
|
-
return scoreCompare;
|
|
638
|
-
}
|
|
639
|
-
return a.template.localeCompare(b.template);
|
|
640
|
-
});
|
|
641
|
-
}
|
|
642
|
-
|
|
643
960
|
async function scanRoutes(params) {
|
|
644
961
|
const routes = [];
|
|
645
962
|
const seen = /* @__PURE__ */ new Set();
|
|
646
963
|
const files = await collectFiles(params.dirs);
|
|
964
|
+
const configCache = /* @__PURE__ */ new Map();
|
|
965
|
+
const fileCache = /* @__PURE__ */ new Map();
|
|
647
966
|
for (const fileInfo of files) {
|
|
648
967
|
if (!isSupportedFile(fileInfo.file)) {
|
|
649
968
|
continue;
|
|
@@ -651,6 +970,20 @@ async function scanRoutes(params) {
|
|
|
651
970
|
if (!matchesFilter(fileInfo.file, params.include, params.exclude)) {
|
|
652
971
|
continue;
|
|
653
972
|
}
|
|
973
|
+
const configParams = {
|
|
974
|
+
file: fileInfo.file,
|
|
975
|
+
rootDir: fileInfo.rootDir,
|
|
976
|
+
logger: params.logger,
|
|
977
|
+
configCache,
|
|
978
|
+
fileCache
|
|
979
|
+
};
|
|
980
|
+
if (params.server) {
|
|
981
|
+
configParams.server = params.server;
|
|
982
|
+
}
|
|
983
|
+
const config = await resolveDirectoryConfig(configParams);
|
|
984
|
+
if (config.enabled === false) {
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
654
987
|
const derived = deriveRouteFromFile(fileInfo.file, fileInfo.rootDir, params.logger);
|
|
655
988
|
if (!derived) {
|
|
656
989
|
continue;
|
|
@@ -660,8 +993,18 @@ async function scanRoutes(params) {
|
|
|
660
993
|
if (!rule || typeof rule !== "object") {
|
|
661
994
|
continue;
|
|
662
995
|
}
|
|
663
|
-
|
|
664
|
-
|
|
996
|
+
const ruleValue = rule;
|
|
997
|
+
const unsupportedKeys = ["response", "url", "method"].filter(
|
|
998
|
+
(key2) => key2 in ruleValue
|
|
999
|
+
);
|
|
1000
|
+
if (unsupportedKeys.length > 0) {
|
|
1001
|
+
params.logger.warn(
|
|
1002
|
+
`Skip mock with unsupported fields (${unsupportedKeys.join(", ")}): ${fileInfo.file}`
|
|
1003
|
+
);
|
|
1004
|
+
continue;
|
|
1005
|
+
}
|
|
1006
|
+
if (typeof rule.handler === "undefined") {
|
|
1007
|
+
params.logger.warn(`Skip mock without handler: ${fileInfo.file}`);
|
|
665
1008
|
continue;
|
|
666
1009
|
}
|
|
667
1010
|
const resolved = resolveRule({
|
|
@@ -675,6 +1018,18 @@ async function scanRoutes(params) {
|
|
|
675
1018
|
if (!resolved) {
|
|
676
1019
|
continue;
|
|
677
1020
|
}
|
|
1021
|
+
if (config.headers) {
|
|
1022
|
+
resolved.headers = { ...config.headers, ...resolved.headers ?? {} };
|
|
1023
|
+
}
|
|
1024
|
+
if (typeof resolved.status === "undefined" && typeof config.status === "number") {
|
|
1025
|
+
resolved.status = config.status;
|
|
1026
|
+
}
|
|
1027
|
+
if (typeof resolved.delay === "undefined" && typeof config.delay === "number") {
|
|
1028
|
+
resolved.delay = config.delay;
|
|
1029
|
+
}
|
|
1030
|
+
if (config.middlewares.length > 0) {
|
|
1031
|
+
resolved.middlewares = config.middlewares;
|
|
1032
|
+
}
|
|
678
1033
|
const key = `${resolved.method} ${resolved.template}`;
|
|
679
1034
|
if (seen.has(key)) {
|
|
680
1035
|
params.logger.warn(`Duplicate mock route ${key} from ${fileInfo.file}`);
|
|
@@ -686,35 +1041,100 @@ async function scanRoutes(params) {
|
|
|
686
1041
|
return sortRoutes(routes);
|
|
687
1042
|
}
|
|
688
1043
|
|
|
1044
|
+
function buildRouteSignature(routes) {
|
|
1045
|
+
return routes.map(
|
|
1046
|
+
(route) => [
|
|
1047
|
+
route.method,
|
|
1048
|
+
route.template,
|
|
1049
|
+
route.file,
|
|
1050
|
+
typeof route.handler === "function" ? "handler" : "static",
|
|
1051
|
+
route.status ?? "",
|
|
1052
|
+
route.delay ?? ""
|
|
1053
|
+
].join("|")
|
|
1054
|
+
).join("\n");
|
|
1055
|
+
}
|
|
1056
|
+
function isViteDevServer(server) {
|
|
1057
|
+
return !!server && "ws" in server;
|
|
1058
|
+
}
|
|
1059
|
+
function normalizeOptions(options) {
|
|
1060
|
+
const list = Array.isArray(options) ? options : [options];
|
|
1061
|
+
return list.length > 0 ? list : [{}];
|
|
1062
|
+
}
|
|
1063
|
+
function resolvePlaygroundInput(list) {
|
|
1064
|
+
for (const entry of list) {
|
|
1065
|
+
if (typeof entry.playground !== "undefined") {
|
|
1066
|
+
return entry.playground;
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
return void 0;
|
|
1070
|
+
}
|
|
689
1071
|
function createMokupPlugin(options = {}) {
|
|
690
1072
|
let root = cwd();
|
|
691
1073
|
let routes = [];
|
|
1074
|
+
let app = null;
|
|
692
1075
|
let previewWatcher = null;
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
const
|
|
1076
|
+
let currentServer = null;
|
|
1077
|
+
let lastSignature = null;
|
|
1078
|
+
const optionList = normalizeOptions(options);
|
|
1079
|
+
const logEnabled = optionList.every((entry) => entry.log !== false);
|
|
1080
|
+
const watchEnabled = optionList.every((entry) => entry.watch !== false);
|
|
1081
|
+
const playgroundConfig = resolvePlaygroundOptions(resolvePlaygroundInput(optionList));
|
|
1082
|
+
const logger = createLogger(logEnabled);
|
|
1083
|
+
const resolveAllDirs = () => {
|
|
1084
|
+
const dirs = [];
|
|
1085
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1086
|
+
for (const entry of optionList) {
|
|
1087
|
+
for (const dir of resolveDirs(entry.dir, root)) {
|
|
1088
|
+
if (seen.has(dir)) {
|
|
1089
|
+
continue;
|
|
1090
|
+
}
|
|
1091
|
+
seen.add(dir);
|
|
1092
|
+
dirs.push(dir);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
return dirs;
|
|
1096
|
+
};
|
|
696
1097
|
const playgroundMiddleware = createPlaygroundMiddleware({
|
|
697
1098
|
getRoutes: () => routes,
|
|
698
1099
|
config: playgroundConfig,
|
|
699
|
-
logger
|
|
1100
|
+
logger,
|
|
1101
|
+
getServer: () => currentServer,
|
|
1102
|
+
getDirs: () => resolveAllDirs()
|
|
700
1103
|
});
|
|
701
1104
|
const refreshRoutes = async (server) => {
|
|
702
|
-
const
|
|
703
|
-
const
|
|
704
|
-
dirs,
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
1105
|
+
const collected = [];
|
|
1106
|
+
for (const entry of optionList) {
|
|
1107
|
+
const dirs = resolveDirs(entry.dir, root);
|
|
1108
|
+
const scanParams = {
|
|
1109
|
+
dirs,
|
|
1110
|
+
prefix: entry.prefix ?? "",
|
|
1111
|
+
logger
|
|
1112
|
+
};
|
|
1113
|
+
if (entry.include) {
|
|
1114
|
+
scanParams.include = entry.include;
|
|
1115
|
+
}
|
|
1116
|
+
if (entry.exclude) {
|
|
1117
|
+
scanParams.exclude = entry.exclude;
|
|
1118
|
+
}
|
|
1119
|
+
if (server) {
|
|
1120
|
+
scanParams.server = server;
|
|
1121
|
+
}
|
|
1122
|
+
const scanned = await scanRoutes(scanParams);
|
|
1123
|
+
collected.push(...scanned);
|
|
713
1124
|
}
|
|
714
|
-
|
|
715
|
-
|
|
1125
|
+
routes = sortRoutes(collected);
|
|
1126
|
+
app = createHonoApp(routes);
|
|
1127
|
+
const signature = buildRouteSignature(routes);
|
|
1128
|
+
if (isViteDevServer(server) && server.ws) {
|
|
1129
|
+
if (lastSignature && signature !== lastSignature) {
|
|
1130
|
+
server.ws.send({
|
|
1131
|
+
type: "custom",
|
|
1132
|
+
event: "mokup:routes-changed",
|
|
1133
|
+
data: { ts: Date.now() }
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
716
1136
|
}
|
|
717
|
-
|
|
1137
|
+
lastSignature = signature;
|
|
718
1138
|
};
|
|
719
1139
|
return {
|
|
720
1140
|
name: "mokup:vite",
|
|
@@ -723,13 +1143,14 @@ function createMokupPlugin(options = {}) {
|
|
|
723
1143
|
root = config.root;
|
|
724
1144
|
},
|
|
725
1145
|
async configureServer(server) {
|
|
1146
|
+
currentServer = server;
|
|
726
1147
|
await refreshRoutes(server);
|
|
727
1148
|
server.middlewares.use(playgroundMiddleware);
|
|
728
|
-
server.middlewares.use(createMiddleware(() =>
|
|
1149
|
+
server.middlewares.use(createMiddleware(() => app, logger));
|
|
729
1150
|
if (!watchEnabled) {
|
|
730
1151
|
return;
|
|
731
1152
|
}
|
|
732
|
-
const dirs =
|
|
1153
|
+
const dirs = resolveAllDirs();
|
|
733
1154
|
server.watcher.add(dirs);
|
|
734
1155
|
const scheduleRefresh = createDebouncer(80, () => refreshRoutes(server));
|
|
735
1156
|
server.watcher.on("add", (file) => {
|
|
@@ -749,13 +1170,14 @@ function createMokupPlugin(options = {}) {
|
|
|
749
1170
|
});
|
|
750
1171
|
},
|
|
751
1172
|
async configurePreviewServer(server) {
|
|
1173
|
+
currentServer = server;
|
|
752
1174
|
await refreshRoutes(server);
|
|
753
1175
|
server.middlewares.use(playgroundMiddleware);
|
|
754
|
-
server.middlewares.use(createMiddleware(() =>
|
|
1176
|
+
server.middlewares.use(createMiddleware(() => app, logger));
|
|
755
1177
|
if (!watchEnabled) {
|
|
756
1178
|
return;
|
|
757
1179
|
}
|
|
758
|
-
const dirs =
|
|
1180
|
+
const dirs = resolveAllDirs();
|
|
759
1181
|
previewWatcher = chokidar.watch(dirs, { ignoreInitial: true });
|
|
760
1182
|
const scheduleRefresh = createDebouncer(80, () => refreshRoutes(server));
|
|
761
1183
|
previewWatcher.on("add", scheduleRefresh);
|