mokup 0.0.1 → 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/dist/index.d.cts CHANGED
@@ -1,27 +1,23 @@
1
- import { IncomingMessage, ServerResponse } from 'node:http';
1
+ import { MiddlewareHandler, Context } from 'hono';
2
2
 
3
3
  type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD';
4
- interface MockRequest {
5
- url: string;
6
- method: HttpMethod;
7
- headers: IncomingMessage['headers'];
8
- query: Record<string, string | string[]>;
9
- body: unknown;
10
- rawBody?: string;
11
- params?: Record<string, string | string[]>;
12
- }
13
- interface MockContext {
14
- delay: (ms: number) => Promise<void>;
15
- json: (data: unknown) => unknown;
16
- }
17
- type MockResponseHandler = (req: MockRequest, res: ServerResponse, ctx: MockContext) => unknown | Promise<unknown>;
4
+ type MockContext = Context;
5
+ type MockMiddleware = MiddlewareHandler;
6
+ type MockResponseHandler = (context: Context) => Response | Promise<Response> | unknown;
18
7
  type MockResponse = unknown | MockResponseHandler;
19
8
  interface MockRule {
20
- response: MockResponse;
9
+ handler: MockResponse;
21
10
  status?: number;
22
11
  headers?: Record<string, string>;
23
12
  delay?: number;
24
13
  }
14
+ interface DirectoryConfig {
15
+ headers?: Record<string, string>;
16
+ status?: number;
17
+ delay?: number;
18
+ enabled?: boolean;
19
+ middleware?: MockMiddleware | MockMiddleware[];
20
+ }
25
21
  interface MokupViteOptions {
26
22
  dir?: string | string[] | ((root: string) => string | string[]);
27
23
  prefix?: string;
@@ -36,4 +32,4 @@ interface MokupViteOptions {
36
32
  }
37
33
  type MokupViteOptionsInput = MokupViteOptions | MokupViteOptions[];
38
34
 
39
- export type { HttpMethod, MockContext, MockRequest, MockResponse, MockResponseHandler, MockRule, MokupViteOptions, MokupViteOptionsInput };
35
+ export type { DirectoryConfig, HttpMethod, MockContext, MockMiddleware, MockResponse, MockResponseHandler, MockRule, MokupViteOptions, MokupViteOptionsInput };
package/dist/index.d.mts CHANGED
@@ -1,27 +1,23 @@
1
- import { IncomingMessage, ServerResponse } from 'node:http';
1
+ import { MiddlewareHandler, Context } from 'hono';
2
2
 
3
3
  type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD';
4
- interface MockRequest {
5
- url: string;
6
- method: HttpMethod;
7
- headers: IncomingMessage['headers'];
8
- query: Record<string, string | string[]>;
9
- body: unknown;
10
- rawBody?: string;
11
- params?: Record<string, string | string[]>;
12
- }
13
- interface MockContext {
14
- delay: (ms: number) => Promise<void>;
15
- json: (data: unknown) => unknown;
16
- }
17
- type MockResponseHandler = (req: MockRequest, res: ServerResponse, ctx: MockContext) => unknown | Promise<unknown>;
4
+ type MockContext = Context;
5
+ type MockMiddleware = MiddlewareHandler;
6
+ type MockResponseHandler = (context: Context) => Response | Promise<Response> | unknown;
18
7
  type MockResponse = unknown | MockResponseHandler;
19
8
  interface MockRule {
20
- response: MockResponse;
9
+ handler: MockResponse;
21
10
  status?: number;
22
11
  headers?: Record<string, string>;
23
12
  delay?: number;
24
13
  }
14
+ interface DirectoryConfig {
15
+ headers?: Record<string, string>;
16
+ status?: number;
17
+ delay?: number;
18
+ enabled?: boolean;
19
+ middleware?: MockMiddleware | MockMiddleware[];
20
+ }
25
21
  interface MokupViteOptions {
26
22
  dir?: string | string[] | ((root: string) => string | string[]);
27
23
  prefix?: string;
@@ -36,4 +32,4 @@ interface MokupViteOptions {
36
32
  }
37
33
  type MokupViteOptionsInput = MokupViteOptions | MokupViteOptions[];
38
34
 
39
- export type { HttpMethod, MockContext, MockRequest, MockResponse, MockResponseHandler, MockRule, MokupViteOptions, MokupViteOptionsInput };
35
+ export type { DirectoryConfig, HttpMethod, MockContext, MockMiddleware, MockResponse, MockResponseHandler, MockRule, MokupViteOptions, MokupViteOptionsInput };
package/dist/index.d.ts CHANGED
@@ -1,27 +1,23 @@
1
- import { IncomingMessage, ServerResponse } from 'node:http';
1
+ import { MiddlewareHandler, Context } from 'hono';
2
2
 
3
3
  type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD';
4
- interface MockRequest {
5
- url: string;
6
- method: HttpMethod;
7
- headers: IncomingMessage['headers'];
8
- query: Record<string, string | string[]>;
9
- body: unknown;
10
- rawBody?: string;
11
- params?: Record<string, string | string[]>;
12
- }
13
- interface MockContext {
14
- delay: (ms: number) => Promise<void>;
15
- json: (data: unknown) => unknown;
16
- }
17
- type MockResponseHandler = (req: MockRequest, res: ServerResponse, ctx: MockContext) => unknown | Promise<unknown>;
4
+ type MockContext = Context;
5
+ type MockMiddleware = MiddlewareHandler;
6
+ type MockResponseHandler = (context: Context) => Response | Promise<Response> | unknown;
18
7
  type MockResponse = unknown | MockResponseHandler;
19
8
  interface MockRule {
20
- response: MockResponse;
9
+ handler: MockResponse;
21
10
  status?: number;
22
11
  headers?: Record<string, string>;
23
12
  delay?: number;
24
13
  }
14
+ interface DirectoryConfig {
15
+ headers?: Record<string, string>;
16
+ status?: number;
17
+ delay?: number;
18
+ enabled?: boolean;
19
+ middleware?: MockMiddleware | MockMiddleware[];
20
+ }
25
21
  interface MokupViteOptions {
26
22
  dir?: string | string[] | ((root: string) => string | string[]);
27
23
  prefix?: string;
@@ -36,4 +32,4 @@ interface MokupViteOptions {
36
32
  }
37
33
  type MokupViteOptionsInput = MokupViteOptions | MokupViteOptions[];
38
34
 
39
- export type { HttpMethod, MockContext, MockRequest, MockResponse, MockResponseHandler, MockRule, MokupViteOptions, MokupViteOptionsInput };
35
+ export type { DirectoryConfig, HttpMethod, MockContext, MockMiddleware, MockResponse, MockResponseHandler, MockRule, MokupViteOptions, MokupViteOptionsInput };
package/dist/vite.cjs CHANGED
@@ -3,10 +3,12 @@
3
3
  const node_process = require('node:process');
4
4
  const chokidar = require('chokidar');
5
5
  const node_buffer = require('node:buffer');
6
- const runtime = require('@mokup/runtime');
6
+ const hono = require('hono');
7
+ const patternRouter = require('hono/router/pattern-router');
7
8
  const pathe = require('pathe');
8
9
  const node_fs = require('node:fs');
9
10
  const node_module = require('node:module');
11
+ const runtime = require('@mokup/runtime');
10
12
  const node_url = require('node:url');
11
13
  const esbuild = require('esbuild');
12
14
  const jsoncParser = require('jsonc-parser');
@@ -122,25 +124,113 @@ function delay(ms) {
122
124
  return new Promise((resolve2) => setTimeout(resolve2, ms));
123
125
  }
124
126
 
125
- function extractQuery(parsedUrl) {
126
- const query = {};
127
- for (const [key, value] of parsedUrl.searchParams.entries()) {
128
- const current = query[key];
129
- if (typeof current === "undefined") {
130
- query[key] = value;
131
- } else if (Array.isArray(current)) {
132
- current.push(value);
133
- } else {
134
- query[key] = [current, value];
127
+ function toHonoPath(route) {
128
+ if (!route.tokens || route.tokens.length === 0) {
129
+ return "/";
130
+ }
131
+ const segments = route.tokens.map((token) => {
132
+ if (token.type === "static") {
133
+ return token.value;
134
+ }
135
+ if (token.type === "param") {
136
+ return `:${token.name}`;
137
+ }
138
+ if (token.type === "catchall") {
139
+ return `:${token.name}{.+}`;
140
+ }
141
+ return `:${token.name}{.+}?`;
142
+ });
143
+ return `/${segments.join("/")}`;
144
+ }
145
+ function applyRouteOverrides(response, route) {
146
+ const headers = new Headers(response.headers);
147
+ const hasHeaders = !!route.headers && Object.keys(route.headers).length > 0;
148
+ if (route.headers) {
149
+ for (const [key, value] of Object.entries(route.headers)) {
150
+ headers.set(key, value);
151
+ }
152
+ }
153
+ const status = route.status ?? response.status;
154
+ if (status === response.status && !hasHeaders) {
155
+ return response;
156
+ }
157
+ return new Response(response.body, { status, headers });
158
+ }
159
+ function normalizeHandlerValue(c, value) {
160
+ if (value instanceof Response) {
161
+ return value;
162
+ }
163
+ if (typeof value === "undefined") {
164
+ const response = c.body(null);
165
+ if (response.status === 200) {
166
+ return new Response(response.body, {
167
+ status: 204,
168
+ headers: response.headers
169
+ });
170
+ }
171
+ return response;
172
+ }
173
+ if (typeof value === "string") {
174
+ return c.text(value);
175
+ }
176
+ if (value instanceof Uint8Array || value instanceof ArrayBuffer) {
177
+ if (!c.res.headers.get("content-type")) {
178
+ c.header("content-type", "application/octet-stream");
135
179
  }
180
+ const data = value instanceof ArrayBuffer ? new Uint8Array(value) : new Uint8Array(value);
181
+ return c.body(data);
136
182
  }
137
- return query;
183
+ return c.json(value);
184
+ }
185
+ function createRouteHandler(route) {
186
+ return async (c) => {
187
+ const value = typeof route.handler === "function" ? await route.handler(c) : route.handler;
188
+ return normalizeHandlerValue(c, value);
189
+ };
190
+ }
191
+ function createFinalizeMiddleware(route) {
192
+ return async (c, next) => {
193
+ const response = await next();
194
+ const resolved = response ?? c.res;
195
+ if (route.delay && route.delay > 0) {
196
+ await delay(route.delay);
197
+ }
198
+ return applyRouteOverrides(resolved, route);
199
+ };
200
+ }
201
+ function wrapMiddleware(handler) {
202
+ return async (c, next) => {
203
+ const response = await handler(c, next);
204
+ return response ?? c.res;
205
+ };
206
+ }
207
+ function createHonoApp(routes) {
208
+ const app = new hono.Hono({ router: new patternRouter.PatternRouter(), strict: false });
209
+ for (const route of routes) {
210
+ const middlewares = route.middlewares?.map((entry) => wrapMiddleware(entry.handle)) ?? [];
211
+ app.on(
212
+ route.method,
213
+ toHonoPath(route),
214
+ createFinalizeMiddleware(route),
215
+ ...middlewares,
216
+ createRouteHandler(route)
217
+ );
218
+ }
219
+ return app;
138
220
  }
139
221
  async function readRawBody(req) {
140
- return new Promise((resolve, reject) => {
222
+ return await new Promise((resolve, reject) => {
141
223
  const chunks = [];
142
224
  req.on("data", (chunk) => {
143
- chunks.push(node_buffer.Buffer.isBuffer(chunk) ? chunk : node_buffer.Buffer.from(chunk));
225
+ if (typeof chunk === "string") {
226
+ chunks.push(node_buffer.Buffer.from(chunk));
227
+ return;
228
+ }
229
+ if (chunk instanceof Uint8Array) {
230
+ chunks.push(chunk);
231
+ return;
232
+ }
233
+ chunks.push(node_buffer.Buffer.from(String(chunk)));
144
234
  });
145
235
  req.on("end", () => {
146
236
  if (chunks.length === 0) {
@@ -152,148 +242,74 @@ async function readRawBody(req) {
152
242
  req.on("error", reject);
153
243
  });
154
244
  }
155
- async function readRequestBody(req, parsedUrl) {
156
- const query = extractQuery(parsedUrl);
157
- const rawBody = await readRawBody(req);
158
- if (!rawBody) {
159
- return { query, body: void 0, rawBody: void 0 };
160
- }
161
- const rawText = rawBody.toString("utf8");
162
- const contentTypeHeader = req.headers["content-type"];
163
- const contentTypeValue = Array.isArray(contentTypeHeader) ? contentTypeHeader[0] ?? "" : contentTypeHeader ?? "";
164
- const contentType = contentTypeValue.split(";")[0]?.trim() ?? "";
165
- if (contentType === "application/json" || contentType.endsWith("+json")) {
166
- try {
167
- return { query, body: JSON.parse(rawText), rawBody: rawText };
168
- } catch {
169
- return { query, body: rawText, rawBody: rawText };
245
+ function buildHeaders(headers) {
246
+ const result = new Headers();
247
+ for (const [key, value] of Object.entries(headers)) {
248
+ if (typeof value === "undefined") {
249
+ continue;
250
+ }
251
+ if (Array.isArray(value)) {
252
+ result.set(key, value.join(","));
253
+ } else {
254
+ result.set(key, value);
170
255
  }
171
256
  }
172
- if (contentType === "application/x-www-form-urlencoded") {
173
- const params = new URLSearchParams(rawText);
174
- const body = Object.fromEntries(params.entries());
175
- return { query, body, rawBody: rawText };
176
- }
177
- return { query, body: rawText, rawBody: rawText };
257
+ return result;
178
258
  }
179
- function applyHeaders(res, headers) {
180
- if (!headers) {
181
- return;
182
- }
183
- for (const [key, value] of Object.entries(headers)) {
184
- res.setHeader(key, value);
259
+ async function toRequest(req) {
260
+ const url = new URL(req.url ?? "/", "http://mokup.local");
261
+ const method = req.method ?? "GET";
262
+ const headers = buildHeaders(req.headers);
263
+ const init = { method, headers };
264
+ const rawBody = await readRawBody(req);
265
+ if (rawBody && method !== "GET" && method !== "HEAD") {
266
+ init.body = rawBody;
185
267
  }
268
+ return new Request(url.toString(), init);
186
269
  }
187
- function writeResponse(res, body) {
188
- if (typeof body === "undefined") {
189
- if (!res.headersSent && res.statusCode === 200) {
190
- res.statusCode = 204;
191
- }
270
+ async function sendResponse(res, response) {
271
+ res.statusCode = response.status;
272
+ response.headers.forEach((value, key) => {
273
+ res.setHeader(key, value);
274
+ });
275
+ if (!response.body) {
192
276
  res.end();
193
277
  return;
194
278
  }
195
- if (node_buffer.Buffer.isBuffer(body)) {
196
- if (!res.getHeader("Content-Type")) {
197
- res.setHeader("Content-Type", "application/octet-stream");
198
- }
199
- res.end(body);
200
- return;
201
- }
202
- if (typeof body === "string") {
203
- if (!res.getHeader("Content-Type")) {
204
- res.setHeader("Content-Type", "text/plain; charset=utf-8");
205
- }
206
- res.end(body);
207
- return;
208
- }
209
- if (!res.getHeader("Content-Type")) {
210
- res.setHeader("Content-Type", "application/json; charset=utf-8");
211
- }
212
- res.end(JSON.stringify(body));
279
+ const buffer = new Uint8Array(await response.arrayBuffer());
280
+ res.end(buffer);
213
281
  }
214
- function findMatchingRoute(routes, method, pathname) {
215
- for (const route of routes) {
216
- if (route.method !== method) {
217
- continue;
218
- }
219
- const matched = runtime.matchRouteTokens(route.tokens, pathname);
220
- if (matched) {
221
- return { route, params: matched.params };
222
- }
223
- }
224
- return null;
282
+ function hasMatch(app, method, pathname) {
283
+ const matchMethod = method === "HEAD" ? "GET" : method;
284
+ const match = app.router.match(matchMethod, pathname);
285
+ return !!match && match[0].length > 0;
225
286
  }
226
- function createMiddleware(getRoutes, logger) {
287
+ function createMiddleware(getApp, logger) {
227
288
  return async (req, res, next) => {
289
+ const app = getApp();
290
+ if (!app) {
291
+ return next();
292
+ }
228
293
  const url = req.url ?? "/";
229
294
  const parsedUrl = new URL(url, "http://mokup.local");
230
295
  const pathname = parsedUrl.pathname;
231
296
  const method = normalizeMethod(req.method) ?? "GET";
232
- const matched = findMatchingRoute(getRoutes(), method, pathname);
233
- if (!matched) {
297
+ if (!hasMatch(app, method, pathname)) {
234
298
  return next();
235
299
  }
300
+ const startedAt = Date.now();
236
301
  try {
237
- const { query, body, rawBody } = await readRequestBody(req, parsedUrl);
238
- const mockReq = {
239
- url: pathname,
240
- method,
241
- headers: req.headers,
242
- query,
243
- body,
244
- params: matched.params
245
- };
246
- if (rawBody) {
247
- mockReq.rawBody = rawBody;
248
- }
249
- const ctx = {
250
- delay: (ms) => delay(ms),
251
- json: (data) => data
252
- };
253
- const startedAt = Date.now();
254
- const executeHandler = async () => {
255
- return typeof matched.route.response === "function" ? await matched.route.response(mockReq, res, ctx) : matched.route.response;
256
- };
257
- const runMiddlewares = async (middlewares) => {
258
- let lastIndex = -1;
259
- const dispatch = async (index) => {
260
- if (index <= lastIndex) {
261
- throw new Error("Middleware next() called multiple times.");
262
- }
263
- lastIndex = index;
264
- const entry = middlewares[index];
265
- if (!entry) {
266
- return executeHandler();
267
- }
268
- let nextResult;
269
- const next2 = async () => {
270
- nextResult = await dispatch(index + 1);
271
- return nextResult;
272
- };
273
- const value = await entry.handle(mockReq, res, ctx, next2);
274
- if (typeof value !== "undefined") {
275
- return value;
276
- }
277
- return nextResult;
278
- };
279
- return dispatch(0);
280
- };
281
- const responseValue = matched.route.middlewares && matched.route.middlewares.length > 0 ? await runMiddlewares(matched.route.middlewares) : await executeHandler();
302
+ const response = await app.fetch(await toRequest(req));
282
303
  if (res.writableEnded) {
283
304
  return;
284
305
  }
285
- if (matched.route.delay && matched.route.delay > 0) {
286
- await delay(matched.route.delay);
287
- }
288
- applyHeaders(res, matched.route.headers);
289
- if (matched.route.status) {
290
- res.statusCode = matched.route.status;
291
- }
292
- writeResponse(res, responseValue);
306
+ await sendResponse(res, response);
293
307
  logger.info(`${method} ${pathname} ${Date.now() - startedAt}ms`);
294
308
  } catch (error) {
295
- res.statusCode = 500;
296
- res.setHeader("Content-Type", "text/plain; charset=utf-8");
309
+ if (!res.headersSent) {
310
+ res.statusCode = 500;
311
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
312
+ }
297
313
  res.end("Mock handler error");
298
314
  logger.error("Mock handler failed:", error);
299
315
  }
@@ -475,7 +491,7 @@ function toPlaygroundRoute(route, root, groups) {
475
491
  method: route.method,
476
492
  url: route.template,
477
493
  file: formatRouteFile(route.file, root),
478
- type: typeof route.response === "function" ? "handler" : "static",
494
+ type: typeof route.handler === "function" ? "handler" : "static",
479
495
  status: route.status,
480
496
  delay: route.delay,
481
497
  middlewareCount: middlewareSources?.length ?? 0,
@@ -648,7 +664,7 @@ function resolveRule(params) {
648
664
  method,
649
665
  tokens: parsed.tokens,
650
666
  score: parsed.score,
651
- response: params.rule.response
667
+ handler: params.rule.handler
652
668
  };
653
669
  if (typeof params.rule.status === "number") {
654
670
  route.status = params.rule.status;
@@ -926,7 +942,7 @@ async function loadRules(file, server, logger) {
926
942
  }
927
943
  return [
928
944
  {
929
- response: json
945
+ handler: json
930
946
  }
931
947
  ];
932
948
  }
@@ -941,7 +957,7 @@ async function loadRules(file, server, logger) {
941
957
  if (typeof value === "function") {
942
958
  return [
943
959
  {
944
- response: value
960
+ handler: value
945
961
  }
946
962
  ];
947
963
  }
@@ -984,8 +1000,18 @@ async function scanRoutes(params) {
984
1000
  if (!rule || typeof rule !== "object") {
985
1001
  continue;
986
1002
  }
987
- if (typeof rule.response === "undefined") {
988
- params.logger.warn(`Skip mock without response: ${fileInfo.file}`);
1003
+ const ruleValue = rule;
1004
+ const unsupportedKeys = ["response", "url", "method"].filter(
1005
+ (key2) => key2 in ruleValue
1006
+ );
1007
+ if (unsupportedKeys.length > 0) {
1008
+ params.logger.warn(
1009
+ `Skip mock with unsupported fields (${unsupportedKeys.join(", ")}): ${fileInfo.file}`
1010
+ );
1011
+ continue;
1012
+ }
1013
+ if (typeof rule.handler === "undefined") {
1014
+ params.logger.warn(`Skip mock without handler: ${fileInfo.file}`);
989
1015
  continue;
990
1016
  }
991
1017
  const resolved = resolveRule({
@@ -1028,7 +1054,7 @@ function buildRouteSignature(routes) {
1028
1054
  route.method,
1029
1055
  route.template,
1030
1056
  route.file,
1031
- typeof route.response === "function" ? "handler" : "static",
1057
+ typeof route.handler === "function" ? "handler" : "static",
1032
1058
  route.status ?? "",
1033
1059
  route.delay ?? ""
1034
1060
  ].join("|")
@@ -1052,6 +1078,7 @@ function resolvePlaygroundInput(list) {
1052
1078
  function createMokupPlugin(options = {}) {
1053
1079
  let root = node_process.cwd();
1054
1080
  let routes = [];
1081
+ let app = null;
1055
1082
  let previewWatcher = null;
1056
1083
  let currentServer = null;
1057
1084
  let lastSignature = null;
@@ -1103,6 +1130,7 @@ function createMokupPlugin(options = {}) {
1103
1130
  collected.push(...scanned);
1104
1131
  }
1105
1132
  routes = sortRoutes(collected);
1133
+ app = createHonoApp(routes);
1106
1134
  const signature = buildRouteSignature(routes);
1107
1135
  if (isViteDevServer(server) && server.ws) {
1108
1136
  if (lastSignature && signature !== lastSignature) {
@@ -1125,7 +1153,7 @@ function createMokupPlugin(options = {}) {
1125
1153
  currentServer = server;
1126
1154
  await refreshRoutes(server);
1127
1155
  server.middlewares.use(playgroundMiddleware);
1128
- server.middlewares.use(createMiddleware(() => routes, logger));
1156
+ server.middlewares.use(createMiddleware(() => app, logger));
1129
1157
  if (!watchEnabled) {
1130
1158
  return;
1131
1159
  }
@@ -1152,7 +1180,7 @@ function createMokupPlugin(options = {}) {
1152
1180
  currentServer = server;
1153
1181
  await refreshRoutes(server);
1154
1182
  server.middlewares.use(playgroundMiddleware);
1155
- server.middlewares.use(createMiddleware(() => routes, logger));
1183
+ server.middlewares.use(createMiddleware(() => app, logger));
1156
1184
  if (!watchEnabled) {
1157
1185
  return;
1158
1186
  }
package/dist/vite.d.cts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Plugin } from 'vite';
2
2
  import { MokupViteOptionsInput } from './index.cjs';
3
- export { HttpMethod, MockContext, MockRequest, MockResponse, MockResponseHandler, MockRule, MokupViteOptions } from './index.cjs';
4
- import 'node:http';
3
+ export { DirectoryConfig, HttpMethod, MockContext, MockMiddleware, MockResponse, MockResponseHandler, MockRule, MokupViteOptions } from './index.cjs';
4
+ import 'hono';
5
5
 
6
6
  declare function createMokupPlugin(options?: MokupViteOptionsInput): Plugin;
7
7
 
package/dist/vite.d.mts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Plugin } from 'vite';
2
2
  import { MokupViteOptionsInput } from './index.mjs';
3
- export { HttpMethod, MockContext, MockRequest, MockResponse, MockResponseHandler, MockRule, MokupViteOptions } from './index.mjs';
4
- import 'node:http';
3
+ export { DirectoryConfig, HttpMethod, MockContext, MockMiddleware, MockResponse, MockResponseHandler, MockRule, MokupViteOptions } from './index.mjs';
4
+ import 'hono';
5
5
 
6
6
  declare function createMokupPlugin(options?: MokupViteOptionsInput): Plugin;
7
7
 
package/dist/vite.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Plugin } from 'vite';
2
2
  import { MokupViteOptionsInput } from './index.js';
3
- export { HttpMethod, MockContext, MockRequest, MockResponse, MockResponseHandler, MockRule, MokupViteOptions } from './index.js';
4
- import 'node:http';
3
+ export { DirectoryConfig, HttpMethod, MockContext, MockMiddleware, MockResponse, MockResponseHandler, MockRule, MokupViteOptions } from './index.js';
4
+ import 'hono';
5
5
 
6
6
  declare function createMokupPlugin(options?: MokupViteOptionsInput): Plugin;
7
7
 
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 { matchRouteTokens, compareRouteScore, parseRouteTemplate } from '@mokup/runtime';
4
+ import { Hono } from 'hono';
5
+ import { PatternRouter } from 'hono/router/pattern-router';
5
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 extractQuery(parsedUrl) {
119
- const query = {};
120
- for (const [key, value] of parsedUrl.searchParams.entries()) {
121
- const current = query[key];
122
- if (typeof current === "undefined") {
123
- query[key] = value;
124
- } else if (Array.isArray(current)) {
125
- current.push(value);
126
- } else {
127
- query[key] = [current, value];
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);
128
144
  }
129
145
  }
130
- return query;
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
+ });
163
+ }
164
+ return response;
165
+ }
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
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
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,148 +235,74 @@ async function readRawBody(req) {
145
235
  req.on("error", reject);
146
236
  });
147
237
  }
148
- async function readRequestBody(req, parsedUrl) {
149
- const query = extractQuery(parsedUrl);
150
- const rawBody = await readRawBody(req);
151
- if (!rawBody) {
152
- return { query, body: void 0, rawBody: void 0 };
153
- }
154
- const rawText = rawBody.toString("utf8");
155
- const contentTypeHeader = req.headers["content-type"];
156
- const contentTypeValue = Array.isArray(contentTypeHeader) ? contentTypeHeader[0] ?? "" : contentTypeHeader ?? "";
157
- const contentType = contentTypeValue.split(";")[0]?.trim() ?? "";
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
- if (contentType === "application/x-www-form-urlencoded") {
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 applyHeaders(res, headers) {
173
- if (!headers) {
174
- return;
175
- }
176
- for (const [key, value] of Object.entries(headers)) {
177
- res.setHeader(key, value);
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 writeResponse(res, body) {
181
- if (typeof body === "undefined") {
182
- if (!res.headersSent && res.statusCode === 200) {
183
- res.statusCode = 204;
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
- if (Buffer.isBuffer(body)) {
189
- if (!res.getHeader("Content-Type")) {
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 findMatchingRoute(routes, method, pathname) {
208
- for (const route of routes) {
209
- if (route.method !== method) {
210
- continue;
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(getRoutes, logger) {
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
- const matched = findMatchingRoute(getRoutes(), method, pathname);
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 { query, body, rawBody } = await readRequestBody(req, parsedUrl);
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 executeHandler = async () => {
248
- return typeof matched.route.response === "function" ? await matched.route.response(mockReq, res, ctx) : matched.route.response;
249
- };
250
- const runMiddlewares = async (middlewares) => {
251
- let lastIndex = -1;
252
- const dispatch = async (index) => {
253
- if (index <= lastIndex) {
254
- throw new Error("Middleware next() called multiple times.");
255
- }
256
- lastIndex = index;
257
- const entry = middlewares[index];
258
- if (!entry) {
259
- return executeHandler();
260
- }
261
- let nextResult;
262
- const next2 = async () => {
263
- nextResult = await dispatch(index + 1);
264
- return nextResult;
265
- };
266
- const value = await entry.handle(mockReq, res, ctx, next2);
267
- if (typeof value !== "undefined") {
268
- return value;
269
- }
270
- return nextResult;
271
- };
272
- return dispatch(0);
273
- };
274
- const responseValue = matched.route.middlewares && matched.route.middlewares.length > 0 ? await runMiddlewares(matched.route.middlewares) : await executeHandler();
295
+ const response = await app.fetch(await toRequest(req));
275
296
  if (res.writableEnded) {
276
297
  return;
277
298
  }
278
- if (matched.route.delay && matched.route.delay > 0) {
279
- await delay(matched.route.delay);
280
- }
281
- applyHeaders(res, matched.route.headers);
282
- if (matched.route.status) {
283
- res.statusCode = matched.route.status;
284
- }
285
- writeResponse(res, responseValue);
299
+ await sendResponse(res, response);
286
300
  logger.info(`${method} ${pathname} ${Date.now() - startedAt}ms`);
287
301
  } catch (error) {
288
- res.statusCode = 500;
289
- res.setHeader("Content-Type", "text/plain; charset=utf-8");
302
+ if (!res.headersSent) {
303
+ res.statusCode = 500;
304
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
305
+ }
290
306
  res.end("Mock handler error");
291
307
  logger.error("Mock handler failed:", error);
292
308
  }
@@ -468,7 +484,7 @@ function toPlaygroundRoute(route, root, groups) {
468
484
  method: route.method,
469
485
  url: route.template,
470
486
  file: formatRouteFile(route.file, root),
471
- type: typeof route.response === "function" ? "handler" : "static",
487
+ type: typeof route.handler === "function" ? "handler" : "static",
472
488
  status: route.status,
473
489
  delay: route.delay,
474
490
  middlewareCount: middlewareSources?.length ?? 0,
@@ -641,7 +657,7 @@ function resolveRule(params) {
641
657
  method,
642
658
  tokens: parsed.tokens,
643
659
  score: parsed.score,
644
- response: params.rule.response
660
+ handler: params.rule.handler
645
661
  };
646
662
  if (typeof params.rule.status === "number") {
647
663
  route.status = params.rule.status;
@@ -919,7 +935,7 @@ async function loadRules(file, server, logger) {
919
935
  }
920
936
  return [
921
937
  {
922
- response: json
938
+ handler: json
923
939
  }
924
940
  ];
925
941
  }
@@ -934,7 +950,7 @@ async function loadRules(file, server, logger) {
934
950
  if (typeof value === "function") {
935
951
  return [
936
952
  {
937
- response: value
953
+ handler: value
938
954
  }
939
955
  ];
940
956
  }
@@ -977,8 +993,18 @@ async function scanRoutes(params) {
977
993
  if (!rule || typeof rule !== "object") {
978
994
  continue;
979
995
  }
980
- if (typeof rule.response === "undefined") {
981
- params.logger.warn(`Skip mock without response: ${fileInfo.file}`);
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}`);
982
1008
  continue;
983
1009
  }
984
1010
  const resolved = resolveRule({
@@ -1021,7 +1047,7 @@ function buildRouteSignature(routes) {
1021
1047
  route.method,
1022
1048
  route.template,
1023
1049
  route.file,
1024
- typeof route.response === "function" ? "handler" : "static",
1050
+ typeof route.handler === "function" ? "handler" : "static",
1025
1051
  route.status ?? "",
1026
1052
  route.delay ?? ""
1027
1053
  ].join("|")
@@ -1045,6 +1071,7 @@ function resolvePlaygroundInput(list) {
1045
1071
  function createMokupPlugin(options = {}) {
1046
1072
  let root = cwd();
1047
1073
  let routes = [];
1074
+ let app = null;
1048
1075
  let previewWatcher = null;
1049
1076
  let currentServer = null;
1050
1077
  let lastSignature = null;
@@ -1096,6 +1123,7 @@ function createMokupPlugin(options = {}) {
1096
1123
  collected.push(...scanned);
1097
1124
  }
1098
1125
  routes = sortRoutes(collected);
1126
+ app = createHonoApp(routes);
1099
1127
  const signature = buildRouteSignature(routes);
1100
1128
  if (isViteDevServer(server) && server.ws) {
1101
1129
  if (lastSignature && signature !== lastSignature) {
@@ -1118,7 +1146,7 @@ function createMokupPlugin(options = {}) {
1118
1146
  currentServer = server;
1119
1147
  await refreshRoutes(server);
1120
1148
  server.middlewares.use(playgroundMiddleware);
1121
- server.middlewares.use(createMiddleware(() => routes, logger));
1149
+ server.middlewares.use(createMiddleware(() => app, logger));
1122
1150
  if (!watchEnabled) {
1123
1151
  return;
1124
1152
  }
@@ -1145,7 +1173,7 @@ function createMokupPlugin(options = {}) {
1145
1173
  currentServer = server;
1146
1174
  await refreshRoutes(server);
1147
1175
  server.middlewares.use(playgroundMiddleware);
1148
- server.middlewares.use(createMiddleware(() => routes, logger));
1176
+ server.middlewares.use(createMiddleware(() => app, logger));
1149
1177
  if (!watchEnabled) {
1150
1178
  return;
1151
1179
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mokup",
3
3
  "type": "module",
4
- "version": "0.0.1",
4
+ "version": "0.1.0",
5
5
  "description": "Mock utilities and Vite plugin for mokup.",
6
6
  "license": "MIT",
7
7
  "homepage": "https://mokup.icebreaker.top",
@@ -37,10 +37,11 @@
37
37
  "dependencies": {
38
38
  "chokidar": "^5.0.0",
39
39
  "esbuild": "^0.27.2",
40
+ "hono": "^4.11.4",
40
41
  "jsonc-parser": "^3.3.1",
41
42
  "pathe": "^2.0.3",
42
43
  "@mokup/playground": "0.0.1",
43
- "@mokup/runtime": "0.0.1"
44
+ "@mokup/runtime": "0.1.0"
44
45
  },
45
46
  "devDependencies": {
46
47
  "@types/node": "^25.0.9",