@mokup/server 1.0.2 → 1.0.4

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.cjs CHANGED
@@ -141,6 +141,20 @@ function matchesFilter(file, include, exclude) {
141
141
  }
142
142
  return true;
143
143
  }
144
+ function normalizeIgnorePrefix(value, fallback = ["."]) {
145
+ const list = typeof value === "undefined" ? fallback : Array.isArray(value) ? value : [value];
146
+ return list.filter((entry) => typeof entry === "string" && entry.length > 0);
147
+ }
148
+ function hasIgnoredPrefix(file, rootDir, prefixes) {
149
+ if (prefixes.length === 0) {
150
+ return false;
151
+ }
152
+ const relativePath = toPosix(pathe.relative(rootDir, file));
153
+ const segments = relativePath.split("/");
154
+ return segments.some(
155
+ (segment) => prefixes.some((prefix) => segment.startsWith(prefix))
156
+ );
157
+ }
144
158
  function delay(ms) {
145
159
  return new Promise((resolve2) => setTimeout(resolve2, ms));
146
160
  }
@@ -233,7 +247,7 @@ function createRouteHandler(route) {
233
247
  return normalizeHandlerValue(c, value);
234
248
  };
235
249
  }
236
- function createFinalizeMiddleware(route) {
250
+ function createFinalizeMiddleware(route, onResponse) {
237
251
  return async (c, next) => {
238
252
  const response = await next();
239
253
  const resolved = resolveResponse(response, c.res);
@@ -242,6 +256,15 @@ function createFinalizeMiddleware(route) {
242
256
  }
243
257
  const overridden = applyRouteOverrides(resolved, route);
244
258
  c.res = overridden;
259
+ if (onResponse) {
260
+ try {
261
+ const result = onResponse(route, overridden);
262
+ if (result instanceof Promise) {
263
+ result.catch(() => void 0);
264
+ }
265
+ } catch {
266
+ }
267
+ }
245
268
  return overridden;
246
269
  };
247
270
  }
@@ -251,14 +274,14 @@ function wrapMiddleware(handler) {
251
274
  return resolveResponse(response, c.res);
252
275
  };
253
276
  }
254
- function createHonoApp(routes) {
277
+ function createHonoApp(routes, options = {}) {
255
278
  const app = new hono.Hono({ router: new hono.PatternRouter(), strict: false });
256
279
  for (const route of routes) {
257
280
  const middlewares = route.middlewares?.map((entry) => wrapMiddleware(entry.handle)) ?? [];
258
281
  app.on(
259
282
  route.method,
260
283
  toHonoPath(route),
261
- createFinalizeMiddleware(route),
284
+ createFinalizeMiddleware(route, options.onResponse),
262
285
  ...middlewares,
263
286
  createRouteHandler(route)
264
287
  );
@@ -363,6 +386,20 @@ function resolveGroupRoot(dirs, serverRoot) {
363
386
  }
364
387
  return common;
365
388
  }
389
+ const disabledReasonSet = /* @__PURE__ */ new Set([
390
+ "disabled",
391
+ "disabled-dir",
392
+ "exclude",
393
+ "ignore-prefix",
394
+ "include",
395
+ "unknown"
396
+ ]);
397
+ function normalizeDisabledReason(reason) {
398
+ if (reason && disabledReasonSet.has(reason)) {
399
+ return reason;
400
+ }
401
+ return "unknown";
402
+ }
366
403
  function formatRouteFile(file, root) {
367
404
  if (!root) {
368
405
  return toPosixPath(file);
@@ -423,6 +460,17 @@ function toPlaygroundRoute(route, root, groups) {
423
460
  group: matchedGroup?.label
424
461
  };
425
462
  }
463
+ function toPlaygroundDisabledRoute(route, root, groups) {
464
+ const matchedGroup = resolveRouteGroup(route.file, groups);
465
+ return {
466
+ file: formatRouteFile(route.file, root),
467
+ reason: normalizeDisabledReason(route.reason),
468
+ method: route.method,
469
+ url: route.url,
470
+ groupKey: matchedGroup?.key,
471
+ group: matchedGroup?.label
472
+ };
473
+ }
426
474
  function registerPlaygroundRoutes(params) {
427
475
  if (!params.config.enabled) {
428
476
  return;
@@ -440,7 +488,16 @@ function registerPlaygroundRoutes(params) {
440
488
  return new Response("Playground is not available.", { status: 500 });
441
489
  }
442
490
  };
443
- params.app.get(playgroundPath, (c) => c.redirect(`${playgroundPath}/`));
491
+ params.app.get(playgroundPath, (c) => {
492
+ try {
493
+ const pathname = new URL(c.req.raw.url, "http://localhost").pathname;
494
+ if (pathname.endsWith("/")) {
495
+ return serveIndex();
496
+ }
497
+ } catch {
498
+ }
499
+ return c.redirect(`${playgroundPath}/`);
500
+ });
444
501
  params.app.get(`${playgroundPath}/`, () => serveIndex());
445
502
  params.app.get(`${playgroundPath}/index.html`, () => serveIndex());
446
503
  params.app.get(`${playgroundPath}/routes`, (c) => {
@@ -451,7 +508,8 @@ function registerPlaygroundRoutes(params) {
451
508
  root: baseRoot,
452
509
  count: params.routes.length,
453
510
  groups: groups.map((group) => ({ key: group.key, label: group.label })),
454
- routes: params.routes.map((route) => toPlaygroundRoute(route, baseRoot, groups))
511
+ routes: params.routes.map((route) => toPlaygroundRoute(route, baseRoot, groups)),
512
+ disabled: (params.disabledRoutes ?? []).map((route) => toPlaygroundDisabledRoute(route, baseRoot, groups))
455
513
  });
456
514
  });
457
515
  params.app.get(`${playgroundPath}/*`, async (c) => {
@@ -718,6 +776,15 @@ async function resolveDirectoryConfig(params) {
718
776
  if (typeof config.enabled === "boolean") {
719
777
  merged.enabled = config.enabled;
720
778
  }
779
+ if (typeof config.ignorePrefix !== "undefined") {
780
+ merged.ignorePrefix = config.ignorePrefix;
781
+ }
782
+ if (typeof config.include !== "undefined") {
783
+ merged.include = config.include;
784
+ }
785
+ if (typeof config.exclude !== "undefined") {
786
+ merged.exclude = config.exclude;
787
+ }
721
788
  const normalized = normalizeMiddlewares(config.middleware, configPath, logger);
722
789
  if (normalized.length > 0) {
723
790
  merged.middlewares.push(...normalized);
@@ -849,19 +916,44 @@ async function loadRules(file, logger) {
849
916
  return [value];
850
917
  }
851
918
 
919
+ const silentLogger = {
920
+ info: () => {
921
+ },
922
+ warn: () => {
923
+ },
924
+ error: () => {
925
+ }
926
+ };
927
+ function resolveSkipRoute(params) {
928
+ const derived = params.derived ?? deriveRouteFromFile(params.file, params.rootDir, silentLogger);
929
+ if (!derived?.method) {
930
+ return null;
931
+ }
932
+ const resolved = resolveRule({
933
+ rule: { handler: null },
934
+ derivedTemplate: derived.template,
935
+ derivedMethod: derived.method,
936
+ prefix: params.prefix,
937
+ file: params.file,
938
+ logger: silentLogger
939
+ });
940
+ if (!resolved) {
941
+ return null;
942
+ }
943
+ return {
944
+ method: resolved.method,
945
+ url: resolved.template
946
+ };
947
+ }
852
948
  async function scanRoutes(params) {
853
949
  const routes = [];
854
950
  const seen = /* @__PURE__ */ new Set();
855
951
  const files = await collectFiles(params.dirs);
952
+ const globalIgnorePrefix = normalizeIgnorePrefix(params.ignorePrefix);
856
953
  const configCache = /* @__PURE__ */ new Map();
857
954
  const fileCache = /* @__PURE__ */ new Map();
955
+ const shouldCollectSkip = typeof params.onSkip === "function";
858
956
  for (const fileInfo of files) {
859
- if (!isSupportedFile(fileInfo.file)) {
860
- continue;
861
- }
862
- if (!matchesFilter(fileInfo.file, params.include, params.exclude)) {
863
- continue;
864
- }
865
957
  const config = await resolveDirectoryConfig({
866
958
  file: fileInfo.file,
867
959
  rootDir: fileInfo.rootDir,
@@ -870,6 +962,58 @@ async function scanRoutes(params) {
870
962
  fileCache
871
963
  });
872
964
  if (config.enabled === false) {
965
+ if (shouldCollectSkip && isSupportedFile(fileInfo.file)) {
966
+ const resolved = resolveSkipRoute({
967
+ file: fileInfo.file,
968
+ rootDir: fileInfo.rootDir,
969
+ prefix: params.prefix
970
+ });
971
+ params.onSkip?.({
972
+ file: fileInfo.file,
973
+ reason: "disabled-dir",
974
+ method: resolved?.method,
975
+ url: resolved?.url
976
+ });
977
+ }
978
+ continue;
979
+ }
980
+ const effectiveIgnorePrefix = typeof config.ignorePrefix !== "undefined" ? normalizeIgnorePrefix(config.ignorePrefix, []) : globalIgnorePrefix;
981
+ if (hasIgnoredPrefix(fileInfo.file, fileInfo.rootDir, effectiveIgnorePrefix)) {
982
+ if (shouldCollectSkip && isSupportedFile(fileInfo.file)) {
983
+ const resolved = resolveSkipRoute({
984
+ file: fileInfo.file,
985
+ rootDir: fileInfo.rootDir,
986
+ prefix: params.prefix
987
+ });
988
+ params.onSkip?.({
989
+ file: fileInfo.file,
990
+ reason: "ignore-prefix",
991
+ method: resolved?.method,
992
+ url: resolved?.url
993
+ });
994
+ }
995
+ continue;
996
+ }
997
+ if (!isSupportedFile(fileInfo.file)) {
998
+ continue;
999
+ }
1000
+ const effectiveInclude = typeof config.include !== "undefined" ? config.include : params.include;
1001
+ const effectiveExclude = typeof config.exclude !== "undefined" ? config.exclude : params.exclude;
1002
+ if (!matchesFilter(fileInfo.file, effectiveInclude, effectiveExclude)) {
1003
+ if (shouldCollectSkip) {
1004
+ const resolved = resolveSkipRoute({
1005
+ file: fileInfo.file,
1006
+ rootDir: fileInfo.rootDir,
1007
+ prefix: params.prefix
1008
+ });
1009
+ const reason = effectiveExclude && matchesFilter(fileInfo.file, void 0, effectiveExclude) ? "exclude" : "include";
1010
+ params.onSkip?.({
1011
+ file: fileInfo.file,
1012
+ reason,
1013
+ method: resolved?.method,
1014
+ url: resolved?.url
1015
+ });
1016
+ }
873
1017
  continue;
874
1018
  }
875
1019
  const derived = deriveRouteFromFile(fileInfo.file, fileInfo.rootDir, params.logger);
@@ -881,6 +1025,23 @@ async function scanRoutes(params) {
881
1025
  if (!rule || typeof rule !== "object") {
882
1026
  continue;
883
1027
  }
1028
+ if (rule.enabled === false) {
1029
+ if (shouldCollectSkip) {
1030
+ const resolved2 = resolveSkipRoute({
1031
+ file: fileInfo.file,
1032
+ rootDir: fileInfo.rootDir,
1033
+ prefix: params.prefix,
1034
+ derived
1035
+ });
1036
+ params.onSkip?.({
1037
+ file: fileInfo.file,
1038
+ reason: "disabled",
1039
+ method: resolved2?.method,
1040
+ url: resolved2?.url
1041
+ });
1042
+ }
1043
+ continue;
1044
+ }
884
1045
  const ruleValue = rule;
885
1046
  const unsupportedKeys = ["response", "url", "method"].filter(
886
1047
  (key2) => key2 in ruleValue
@@ -981,13 +1142,17 @@ function buildApp(params) {
981
1142
  registerPlaygroundRoutes({
982
1143
  app,
983
1144
  routes: params.routes,
1145
+ disabledRoutes: params.disabledRoutes,
984
1146
  dirs: params.dirs,
985
1147
  logger: params.logger,
986
1148
  config: params.playground,
987
1149
  root: params.root
988
1150
  });
1151
+ if (params.wsHandler && params.playground.enabled) {
1152
+ app.get(`${params.playground.path}/ws`, params.wsHandler);
1153
+ }
989
1154
  if (params.routes.length > 0) {
990
- const mockApp = createHonoApp(params.routes);
1155
+ const mockApp = createHonoApp(params.routes, { onResponse: params.onResponse });
991
1156
  app.route("/", mockApp);
992
1157
  }
993
1158
  return app;
@@ -1074,22 +1239,94 @@ async function createFetchServer(options = {}) {
1074
1239
  const logger = createLogger(logEnabled);
1075
1240
  const playgroundConfig = resolvePlaygroundOptions(resolvePlaygroundInput(optionList));
1076
1241
  const dirs = resolveAllDirs(optionList, root);
1242
+ const routeCounts = {};
1243
+ const wsClients = /* @__PURE__ */ new Set();
1244
+ let totalCount = 0;
1245
+ let wsHandler;
1246
+ let injectWebSocket;
1247
+ function getRouteKey(route) {
1248
+ return `${route.method} ${route.template}`;
1249
+ }
1250
+ function buildSnapshot() {
1251
+ return {
1252
+ type: "snapshot",
1253
+ total: totalCount,
1254
+ perRoute: { ...routeCounts }
1255
+ };
1256
+ }
1257
+ function broadcast(payload) {
1258
+ if (wsClients.size === 0) {
1259
+ return;
1260
+ }
1261
+ const message = JSON.stringify(payload);
1262
+ for (const client of wsClients) {
1263
+ try {
1264
+ client.send(message);
1265
+ } catch {
1266
+ wsClients.delete(client);
1267
+ }
1268
+ }
1269
+ }
1270
+ function registerWsClient(client) {
1271
+ wsClients.add(client);
1272
+ try {
1273
+ client.send(JSON.stringify(buildSnapshot()));
1274
+ } catch {
1275
+ wsClients.delete(client);
1276
+ }
1277
+ }
1278
+ function handleRouteResponse(route) {
1279
+ const routeKey = getRouteKey(route);
1280
+ routeCounts[routeKey] = (routeCounts[routeKey] ?? 0) + 1;
1281
+ totalCount += 1;
1282
+ broadcast({ type: "increment", routeKey, total: totalCount });
1283
+ }
1284
+ async function setupPlaygroundWebSocket(app2) {
1285
+ if (!playgroundConfig.enabled) {
1286
+ return;
1287
+ }
1288
+ try {
1289
+ const mod = await import('@hono/node-ws');
1290
+ const { createNodeWebSocket } = mod;
1291
+ const { upgradeWebSocket, injectWebSocket: inject } = createNodeWebSocket({ app: app2 });
1292
+ wsHandler = upgradeWebSocket(() => ({
1293
+ onOpen: (_event, ws) => {
1294
+ registerWsClient(ws);
1295
+ },
1296
+ onClose: (_event, ws) => {
1297
+ wsClients.delete(ws);
1298
+ },
1299
+ onMessage: () => {
1300
+ }
1301
+ }));
1302
+ injectWebSocket = (server2) => {
1303
+ inject(server2);
1304
+ };
1305
+ } catch {
1306
+ }
1307
+ }
1077
1308
  let routes = [];
1309
+ let disabledRoutes = [];
1078
1310
  let app = buildApp({
1079
1311
  routes,
1312
+ disabledRoutes,
1080
1313
  dirs,
1081
1314
  playground: playgroundConfig,
1082
1315
  root,
1083
- logger
1316
+ logger,
1317
+ onResponse: handleRouteResponse,
1318
+ wsHandler
1084
1319
  });
1085
1320
  const refreshRoutes = async () => {
1086
1321
  try {
1087
1322
  const collected = [];
1323
+ const collectedDisabled = [];
1088
1324
  for (const entry of optionList) {
1089
1325
  const scanParams = {
1090
1326
  dirs: resolveDirs(entry.dir, root),
1091
1327
  prefix: entry.prefix ?? "",
1092
- logger
1328
+ logger,
1329
+ onSkip: (info) => collectedDisabled.push(info)
1093
1330
  };
1094
1331
  if (entry.include) {
1095
1332
  scanParams.include = entry.include;
@@ -1097,17 +1334,24 @@ async function createFetchServer(options = {}) {
1097
1334
  if (entry.exclude) {
1098
1335
  scanParams.exclude = entry.exclude;
1099
1336
  }
1337
+ if (typeof entry.ignorePrefix !== "undefined") {
1338
+ scanParams.ignorePrefix = entry.ignorePrefix;
1339
+ }
1100
1340
  const scanned = await scanRoutes(scanParams);
1101
1341
  collected.push(...scanned);
1102
1342
  }
1103
1343
  const resolvedRoutes = sortRoutes(collected);
1104
1344
  routes = resolvedRoutes;
1345
+ disabledRoutes = collectedDisabled;
1105
1346
  app = buildApp({
1106
1347
  routes,
1348
+ disabledRoutes,
1107
1349
  dirs,
1108
1350
  playground: playgroundConfig,
1109
1351
  root,
1110
- logger
1352
+ logger,
1353
+ onResponse: handleRouteResponse,
1354
+ wsHandler
1111
1355
  });
1112
1356
  logger.info(`Loaded ${routes.length} mock routes.`);
1113
1357
  } catch (error) {
@@ -1115,6 +1359,10 @@ async function createFetchServer(options = {}) {
1115
1359
  }
1116
1360
  };
1117
1361
  await refreshRoutes();
1362
+ await setupPlaygroundWebSocket(app);
1363
+ if (wsHandler && playgroundConfig.enabled) {
1364
+ app.get(`${playgroundConfig.path}/ws`, wsHandler);
1365
+ }
1118
1366
  const scheduleRefresh = createDebouncer(80, () => {
1119
1367
  void refreshRoutes();
1120
1368
  });
@@ -1128,7 +1376,8 @@ async function createFetchServer(options = {}) {
1128
1376
  const server = {
1129
1377
  fetch,
1130
1378
  refresh: refreshRoutes,
1131
- getRoutes: () => routes
1379
+ getRoutes: () => routes,
1380
+ injectWebSocket
1132
1381
  };
1133
1382
  if (watcher) {
1134
1383
  server.close = async () => {
package/dist/index.d.cts CHANGED
@@ -70,6 +70,7 @@ interface FetchServerOptions {
70
70
  prefix?: string;
71
71
  include?: RegExp | RegExp[];
72
72
  exclude?: RegExp | RegExp[];
73
+ ignorePrefix?: string | string[];
73
74
  watch?: boolean;
74
75
  log?: boolean;
75
76
  playground?: boolean | {
@@ -86,8 +87,12 @@ interface FetchServer {
86
87
  fetch: (request: Request) => Promise<Response>;
87
88
  refresh: () => Promise<void>;
88
89
  getRoutes: () => RouteTable;
90
+ injectWebSocket?: (server: NodeWebSocketServer) => void;
89
91
  close?: () => Promise<void>;
90
92
  }
93
+ interface NodeWebSocketServer {
94
+ on: (event: string, listener: (...args: unknown[]) => void) => void;
95
+ }
91
96
  declare function createFetchServer(options?: FetchServerOptionsInput): Promise<FetchServer>;
92
97
 
93
98
  interface HonoContextLike {
package/dist/index.d.mts CHANGED
@@ -70,6 +70,7 @@ interface FetchServerOptions {
70
70
  prefix?: string;
71
71
  include?: RegExp | RegExp[];
72
72
  exclude?: RegExp | RegExp[];
73
+ ignorePrefix?: string | string[];
73
74
  watch?: boolean;
74
75
  log?: boolean;
75
76
  playground?: boolean | {
@@ -86,8 +87,12 @@ interface FetchServer {
86
87
  fetch: (request: Request) => Promise<Response>;
87
88
  refresh: () => Promise<void>;
88
89
  getRoutes: () => RouteTable;
90
+ injectWebSocket?: (server: NodeWebSocketServer) => void;
89
91
  close?: () => Promise<void>;
90
92
  }
93
+ interface NodeWebSocketServer {
94
+ on: (event: string, listener: (...args: unknown[]) => void) => void;
95
+ }
91
96
  declare function createFetchServer(options?: FetchServerOptionsInput): Promise<FetchServer>;
92
97
 
93
98
  interface HonoContextLike {
package/dist/index.d.ts CHANGED
@@ -70,6 +70,7 @@ interface FetchServerOptions {
70
70
  prefix?: string;
71
71
  include?: RegExp | RegExp[];
72
72
  exclude?: RegExp | RegExp[];
73
+ ignorePrefix?: string | string[];
73
74
  watch?: boolean;
74
75
  log?: boolean;
75
76
  playground?: boolean | {
@@ -86,8 +87,12 @@ interface FetchServer {
86
87
  fetch: (request: Request) => Promise<Response>;
87
88
  refresh: () => Promise<void>;
88
89
  getRoutes: () => RouteTable;
90
+ injectWebSocket?: (server: NodeWebSocketServer) => void;
89
91
  close?: () => Promise<void>;
90
92
  }
93
+ interface NodeWebSocketServer {
94
+ on: (event: string, listener: (...args: unknown[]) => void) => void;
95
+ }
91
96
  declare function createFetchServer(options?: FetchServerOptionsInput): Promise<FetchServer>;
92
97
 
93
98
  interface HonoContextLike {
package/dist/index.mjs CHANGED
@@ -1,8 +1,8 @@
1
1
  import { createRuntime, compareRouteScore, parseRouteTemplate } from '@mokup/runtime';
2
- import { t as toRuntimeOptions, a as toRuntimeRequestFromNode, b as applyRuntimeResultToNode, c as toBinaryBody, d as createFetchHandler } from './shared/server.HVB7OYyI.mjs';
2
+ import { t as toRuntimeOptions, a as toRuntimeRequestFromNode, b as applyRuntimeResultToNode, d as toBinaryBody, c as createFetchHandler } from './shared/server.Dje1y79O.mjs';
3
3
  import { cwd } from 'node:process';
4
4
  import { Hono, PatternRouter } from '@mokup/shared/hono';
5
- import { resolve, isAbsolute, join, normalize, extname, dirname, relative, basename } from '@mokup/shared/pathe';
5
+ import { resolve, isAbsolute, relative, join, normalize, extname, dirname, basename } from '@mokup/shared/pathe';
6
6
  import { promises } from 'node:fs';
7
7
  import { createRequire } from 'node:module';
8
8
  import { Buffer } from 'node:buffer';
@@ -138,6 +138,20 @@ function matchesFilter(file, include, exclude) {
138
138
  }
139
139
  return true;
140
140
  }
141
+ function normalizeIgnorePrefix(value, fallback = ["."]) {
142
+ const list = typeof value === "undefined" ? fallback : Array.isArray(value) ? value : [value];
143
+ return list.filter((entry) => typeof entry === "string" && entry.length > 0);
144
+ }
145
+ function hasIgnoredPrefix(file, rootDir, prefixes) {
146
+ if (prefixes.length === 0) {
147
+ return false;
148
+ }
149
+ const relativePath = toPosix(relative(rootDir, file));
150
+ const segments = relativePath.split("/");
151
+ return segments.some(
152
+ (segment) => prefixes.some((prefix) => segment.startsWith(prefix))
153
+ );
154
+ }
141
155
  function delay(ms) {
142
156
  return new Promise((resolve2) => setTimeout(resolve2, ms));
143
157
  }
@@ -230,7 +244,7 @@ function createRouteHandler(route) {
230
244
  return normalizeHandlerValue(c, value);
231
245
  };
232
246
  }
233
- function createFinalizeMiddleware(route) {
247
+ function createFinalizeMiddleware(route, onResponse) {
234
248
  return async (c, next) => {
235
249
  const response = await next();
236
250
  const resolved = resolveResponse(response, c.res);
@@ -239,6 +253,15 @@ function createFinalizeMiddleware(route) {
239
253
  }
240
254
  const overridden = applyRouteOverrides(resolved, route);
241
255
  c.res = overridden;
256
+ if (onResponse) {
257
+ try {
258
+ const result = onResponse(route, overridden);
259
+ if (result instanceof Promise) {
260
+ result.catch(() => void 0);
261
+ }
262
+ } catch {
263
+ }
264
+ }
242
265
  return overridden;
243
266
  };
244
267
  }
@@ -248,14 +271,14 @@ function wrapMiddleware(handler) {
248
271
  return resolveResponse(response, c.res);
249
272
  };
250
273
  }
251
- function createHonoApp(routes) {
274
+ function createHonoApp(routes, options = {}) {
252
275
  const app = new Hono({ router: new PatternRouter(), strict: false });
253
276
  for (const route of routes) {
254
277
  const middlewares = route.middlewares?.map((entry) => wrapMiddleware(entry.handle)) ?? [];
255
278
  app.on(
256
279
  route.method,
257
280
  toHonoPath(route),
258
- createFinalizeMiddleware(route),
281
+ createFinalizeMiddleware(route, options.onResponse),
259
282
  ...middlewares,
260
283
  createRouteHandler(route)
261
284
  );
@@ -360,6 +383,20 @@ function resolveGroupRoot(dirs, serverRoot) {
360
383
  }
361
384
  return common;
362
385
  }
386
+ const disabledReasonSet = /* @__PURE__ */ new Set([
387
+ "disabled",
388
+ "disabled-dir",
389
+ "exclude",
390
+ "ignore-prefix",
391
+ "include",
392
+ "unknown"
393
+ ]);
394
+ function normalizeDisabledReason(reason) {
395
+ if (reason && disabledReasonSet.has(reason)) {
396
+ return reason;
397
+ }
398
+ return "unknown";
399
+ }
363
400
  function formatRouteFile(file, root) {
364
401
  if (!root) {
365
402
  return toPosixPath(file);
@@ -420,6 +457,17 @@ function toPlaygroundRoute(route, root, groups) {
420
457
  group: matchedGroup?.label
421
458
  };
422
459
  }
460
+ function toPlaygroundDisabledRoute(route, root, groups) {
461
+ const matchedGroup = resolveRouteGroup(route.file, groups);
462
+ return {
463
+ file: formatRouteFile(route.file, root),
464
+ reason: normalizeDisabledReason(route.reason),
465
+ method: route.method,
466
+ url: route.url,
467
+ groupKey: matchedGroup?.key,
468
+ group: matchedGroup?.label
469
+ };
470
+ }
423
471
  function registerPlaygroundRoutes(params) {
424
472
  if (!params.config.enabled) {
425
473
  return;
@@ -437,7 +485,16 @@ function registerPlaygroundRoutes(params) {
437
485
  return new Response("Playground is not available.", { status: 500 });
438
486
  }
439
487
  };
440
- params.app.get(playgroundPath, (c) => c.redirect(`${playgroundPath}/`));
488
+ params.app.get(playgroundPath, (c) => {
489
+ try {
490
+ const pathname = new URL(c.req.raw.url, "http://localhost").pathname;
491
+ if (pathname.endsWith("/")) {
492
+ return serveIndex();
493
+ }
494
+ } catch {
495
+ }
496
+ return c.redirect(`${playgroundPath}/`);
497
+ });
441
498
  params.app.get(`${playgroundPath}/`, () => serveIndex());
442
499
  params.app.get(`${playgroundPath}/index.html`, () => serveIndex());
443
500
  params.app.get(`${playgroundPath}/routes`, (c) => {
@@ -448,7 +505,8 @@ function registerPlaygroundRoutes(params) {
448
505
  root: baseRoot,
449
506
  count: params.routes.length,
450
507
  groups: groups.map((group) => ({ key: group.key, label: group.label })),
451
- routes: params.routes.map((route) => toPlaygroundRoute(route, baseRoot, groups))
508
+ routes: params.routes.map((route) => toPlaygroundRoute(route, baseRoot, groups)),
509
+ disabled: (params.disabledRoutes ?? []).map((route) => toPlaygroundDisabledRoute(route, baseRoot, groups))
452
510
  });
453
511
  });
454
512
  params.app.get(`${playgroundPath}/*`, async (c) => {
@@ -715,6 +773,15 @@ async function resolveDirectoryConfig(params) {
715
773
  if (typeof config.enabled === "boolean") {
716
774
  merged.enabled = config.enabled;
717
775
  }
776
+ if (typeof config.ignorePrefix !== "undefined") {
777
+ merged.ignorePrefix = config.ignorePrefix;
778
+ }
779
+ if (typeof config.include !== "undefined") {
780
+ merged.include = config.include;
781
+ }
782
+ if (typeof config.exclude !== "undefined") {
783
+ merged.exclude = config.exclude;
784
+ }
718
785
  const normalized = normalizeMiddlewares(config.middleware, configPath, logger);
719
786
  if (normalized.length > 0) {
720
787
  merged.middlewares.push(...normalized);
@@ -846,19 +913,44 @@ async function loadRules(file, logger) {
846
913
  return [value];
847
914
  }
848
915
 
916
+ const silentLogger = {
917
+ info: () => {
918
+ },
919
+ warn: () => {
920
+ },
921
+ error: () => {
922
+ }
923
+ };
924
+ function resolveSkipRoute(params) {
925
+ const derived = params.derived ?? deriveRouteFromFile(params.file, params.rootDir, silentLogger);
926
+ if (!derived?.method) {
927
+ return null;
928
+ }
929
+ const resolved = resolveRule({
930
+ rule: { handler: null },
931
+ derivedTemplate: derived.template,
932
+ derivedMethod: derived.method,
933
+ prefix: params.prefix,
934
+ file: params.file,
935
+ logger: silentLogger
936
+ });
937
+ if (!resolved) {
938
+ return null;
939
+ }
940
+ return {
941
+ method: resolved.method,
942
+ url: resolved.template
943
+ };
944
+ }
849
945
  async function scanRoutes(params) {
850
946
  const routes = [];
851
947
  const seen = /* @__PURE__ */ new Set();
852
948
  const files = await collectFiles(params.dirs);
949
+ const globalIgnorePrefix = normalizeIgnorePrefix(params.ignorePrefix);
853
950
  const configCache = /* @__PURE__ */ new Map();
854
951
  const fileCache = /* @__PURE__ */ new Map();
952
+ const shouldCollectSkip = typeof params.onSkip === "function";
855
953
  for (const fileInfo of files) {
856
- if (!isSupportedFile(fileInfo.file)) {
857
- continue;
858
- }
859
- if (!matchesFilter(fileInfo.file, params.include, params.exclude)) {
860
- continue;
861
- }
862
954
  const config = await resolveDirectoryConfig({
863
955
  file: fileInfo.file,
864
956
  rootDir: fileInfo.rootDir,
@@ -867,6 +959,58 @@ async function scanRoutes(params) {
867
959
  fileCache
868
960
  });
869
961
  if (config.enabled === false) {
962
+ if (shouldCollectSkip && isSupportedFile(fileInfo.file)) {
963
+ const resolved = resolveSkipRoute({
964
+ file: fileInfo.file,
965
+ rootDir: fileInfo.rootDir,
966
+ prefix: params.prefix
967
+ });
968
+ params.onSkip?.({
969
+ file: fileInfo.file,
970
+ reason: "disabled-dir",
971
+ method: resolved?.method,
972
+ url: resolved?.url
973
+ });
974
+ }
975
+ continue;
976
+ }
977
+ const effectiveIgnorePrefix = typeof config.ignorePrefix !== "undefined" ? normalizeIgnorePrefix(config.ignorePrefix, []) : globalIgnorePrefix;
978
+ if (hasIgnoredPrefix(fileInfo.file, fileInfo.rootDir, effectiveIgnorePrefix)) {
979
+ if (shouldCollectSkip && isSupportedFile(fileInfo.file)) {
980
+ const resolved = resolveSkipRoute({
981
+ file: fileInfo.file,
982
+ rootDir: fileInfo.rootDir,
983
+ prefix: params.prefix
984
+ });
985
+ params.onSkip?.({
986
+ file: fileInfo.file,
987
+ reason: "ignore-prefix",
988
+ method: resolved?.method,
989
+ url: resolved?.url
990
+ });
991
+ }
992
+ continue;
993
+ }
994
+ if (!isSupportedFile(fileInfo.file)) {
995
+ continue;
996
+ }
997
+ const effectiveInclude = typeof config.include !== "undefined" ? config.include : params.include;
998
+ const effectiveExclude = typeof config.exclude !== "undefined" ? config.exclude : params.exclude;
999
+ if (!matchesFilter(fileInfo.file, effectiveInclude, effectiveExclude)) {
1000
+ if (shouldCollectSkip) {
1001
+ const resolved = resolveSkipRoute({
1002
+ file: fileInfo.file,
1003
+ rootDir: fileInfo.rootDir,
1004
+ prefix: params.prefix
1005
+ });
1006
+ const reason = effectiveExclude && matchesFilter(fileInfo.file, void 0, effectiveExclude) ? "exclude" : "include";
1007
+ params.onSkip?.({
1008
+ file: fileInfo.file,
1009
+ reason,
1010
+ method: resolved?.method,
1011
+ url: resolved?.url
1012
+ });
1013
+ }
870
1014
  continue;
871
1015
  }
872
1016
  const derived = deriveRouteFromFile(fileInfo.file, fileInfo.rootDir, params.logger);
@@ -878,6 +1022,23 @@ async function scanRoutes(params) {
878
1022
  if (!rule || typeof rule !== "object") {
879
1023
  continue;
880
1024
  }
1025
+ if (rule.enabled === false) {
1026
+ if (shouldCollectSkip) {
1027
+ const resolved2 = resolveSkipRoute({
1028
+ file: fileInfo.file,
1029
+ rootDir: fileInfo.rootDir,
1030
+ prefix: params.prefix,
1031
+ derived
1032
+ });
1033
+ params.onSkip?.({
1034
+ file: fileInfo.file,
1035
+ reason: "disabled",
1036
+ method: resolved2?.method,
1037
+ url: resolved2?.url
1038
+ });
1039
+ }
1040
+ continue;
1041
+ }
881
1042
  const ruleValue = rule;
882
1043
  const unsupportedKeys = ["response", "url", "method"].filter(
883
1044
  (key2) => key2 in ruleValue
@@ -978,13 +1139,17 @@ function buildApp(params) {
978
1139
  registerPlaygroundRoutes({
979
1140
  app,
980
1141
  routes: params.routes,
1142
+ disabledRoutes: params.disabledRoutes,
981
1143
  dirs: params.dirs,
982
1144
  logger: params.logger,
983
1145
  config: params.playground,
984
1146
  root: params.root
985
1147
  });
1148
+ if (params.wsHandler && params.playground.enabled) {
1149
+ app.get(`${params.playground.path}/ws`, params.wsHandler);
1150
+ }
986
1151
  if (params.routes.length > 0) {
987
- const mockApp = createHonoApp(params.routes);
1152
+ const mockApp = createHonoApp(params.routes, { onResponse: params.onResponse });
988
1153
  app.route("/", mockApp);
989
1154
  }
990
1155
  return app;
@@ -1071,22 +1236,94 @@ async function createFetchServer(options = {}) {
1071
1236
  const logger = createLogger(logEnabled);
1072
1237
  const playgroundConfig = resolvePlaygroundOptions(resolvePlaygroundInput(optionList));
1073
1238
  const dirs = resolveAllDirs(optionList, root);
1239
+ const routeCounts = {};
1240
+ const wsClients = /* @__PURE__ */ new Set();
1241
+ let totalCount = 0;
1242
+ let wsHandler;
1243
+ let injectWebSocket;
1244
+ function getRouteKey(route) {
1245
+ return `${route.method} ${route.template}`;
1246
+ }
1247
+ function buildSnapshot() {
1248
+ return {
1249
+ type: "snapshot",
1250
+ total: totalCount,
1251
+ perRoute: { ...routeCounts }
1252
+ };
1253
+ }
1254
+ function broadcast(payload) {
1255
+ if (wsClients.size === 0) {
1256
+ return;
1257
+ }
1258
+ const message = JSON.stringify(payload);
1259
+ for (const client of wsClients) {
1260
+ try {
1261
+ client.send(message);
1262
+ } catch {
1263
+ wsClients.delete(client);
1264
+ }
1265
+ }
1266
+ }
1267
+ function registerWsClient(client) {
1268
+ wsClients.add(client);
1269
+ try {
1270
+ client.send(JSON.stringify(buildSnapshot()));
1271
+ } catch {
1272
+ wsClients.delete(client);
1273
+ }
1274
+ }
1275
+ function handleRouteResponse(route) {
1276
+ const routeKey = getRouteKey(route);
1277
+ routeCounts[routeKey] = (routeCounts[routeKey] ?? 0) + 1;
1278
+ totalCount += 1;
1279
+ broadcast({ type: "increment", routeKey, total: totalCount });
1280
+ }
1281
+ async function setupPlaygroundWebSocket(app2) {
1282
+ if (!playgroundConfig.enabled) {
1283
+ return;
1284
+ }
1285
+ try {
1286
+ const mod = await import('@hono/node-ws');
1287
+ const { createNodeWebSocket } = mod;
1288
+ const { upgradeWebSocket, injectWebSocket: inject } = createNodeWebSocket({ app: app2 });
1289
+ wsHandler = upgradeWebSocket(() => ({
1290
+ onOpen: (_event, ws) => {
1291
+ registerWsClient(ws);
1292
+ },
1293
+ onClose: (_event, ws) => {
1294
+ wsClients.delete(ws);
1295
+ },
1296
+ onMessage: () => {
1297
+ }
1298
+ }));
1299
+ injectWebSocket = (server2) => {
1300
+ inject(server2);
1301
+ };
1302
+ } catch {
1303
+ }
1304
+ }
1074
1305
  let routes = [];
1306
+ let disabledRoutes = [];
1075
1307
  let app = buildApp({
1076
1308
  routes,
1309
+ disabledRoutes,
1077
1310
  dirs,
1078
1311
  playground: playgroundConfig,
1079
1312
  root,
1080
- logger
1313
+ logger,
1314
+ onResponse: handleRouteResponse,
1315
+ wsHandler
1081
1316
  });
1082
1317
  const refreshRoutes = async () => {
1083
1318
  try {
1084
1319
  const collected = [];
1320
+ const collectedDisabled = [];
1085
1321
  for (const entry of optionList) {
1086
1322
  const scanParams = {
1087
1323
  dirs: resolveDirs(entry.dir, root),
1088
1324
  prefix: entry.prefix ?? "",
1089
- logger
1325
+ logger,
1326
+ onSkip: (info) => collectedDisabled.push(info)
1090
1327
  };
1091
1328
  if (entry.include) {
1092
1329
  scanParams.include = entry.include;
@@ -1094,17 +1331,24 @@ async function createFetchServer(options = {}) {
1094
1331
  if (entry.exclude) {
1095
1332
  scanParams.exclude = entry.exclude;
1096
1333
  }
1334
+ if (typeof entry.ignorePrefix !== "undefined") {
1335
+ scanParams.ignorePrefix = entry.ignorePrefix;
1336
+ }
1097
1337
  const scanned = await scanRoutes(scanParams);
1098
1338
  collected.push(...scanned);
1099
1339
  }
1100
1340
  const resolvedRoutes = sortRoutes(collected);
1101
1341
  routes = resolvedRoutes;
1342
+ disabledRoutes = collectedDisabled;
1102
1343
  app = buildApp({
1103
1344
  routes,
1345
+ disabledRoutes,
1104
1346
  dirs,
1105
1347
  playground: playgroundConfig,
1106
1348
  root,
1107
- logger
1349
+ logger,
1350
+ onResponse: handleRouteResponse,
1351
+ wsHandler
1108
1352
  });
1109
1353
  logger.info(`Loaded ${routes.length} mock routes.`);
1110
1354
  } catch (error) {
@@ -1112,6 +1356,10 @@ async function createFetchServer(options = {}) {
1112
1356
  }
1113
1357
  };
1114
1358
  await refreshRoutes();
1359
+ await setupPlaygroundWebSocket(app);
1360
+ if (wsHandler && playgroundConfig.enabled) {
1361
+ app.get(`${playgroundConfig.path}/ws`, wsHandler);
1362
+ }
1115
1363
  const scheduleRefresh = createDebouncer(80, () => {
1116
1364
  void refreshRoutes();
1117
1365
  });
@@ -1125,7 +1373,8 @@ async function createFetchServer(options = {}) {
1125
1373
  const server = {
1126
1374
  fetch,
1127
1375
  refresh: refreshRoutes,
1128
- getRoutes: () => routes
1376
+ getRoutes: () => routes,
1377
+ injectWebSocket
1129
1378
  };
1130
1379
  if (watcher) {
1131
1380
  server.close = async () => {
package/dist/node.cjs ADDED
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+
3
+ const nodeServer = require('@hono/node-server');
4
+
5
+
6
+
7
+ exports.serve = nodeServer.serve;
@@ -0,0 +1 @@
1
+ export { serve } from '@hono/node-server';
@@ -0,0 +1 @@
1
+ export { serve } from '@hono/node-server';
package/dist/node.d.ts ADDED
@@ -0,0 +1 @@
1
+ export { serve } from '@hono/node-server';
package/dist/node.mjs ADDED
@@ -0,0 +1 @@
1
+ export { serve } from '@hono/node-server';
@@ -255,4 +255,4 @@ function createFetchHandler(options) {
255
255
  };
256
256
  }
257
257
 
258
- export { toRuntimeRequestFromNode as a, applyRuntimeResultToNode as b, toBinaryBody as c, createFetchHandler as d, toRuntimeOptions as t };
258
+ export { toRuntimeRequestFromNode as a, applyRuntimeResultToNode as b, createFetchHandler as c, toBinaryBody as d, toRuntimeOptions as t };
package/dist/worker.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { d as createFetchHandler } from './shared/server.HVB7OYyI.mjs';
1
+ import { c as createFetchHandler } from './shared/server.Dje1y79O.mjs';
2
2
  import '@mokup/runtime';
3
3
 
4
4
  function isManifest(value) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mokup/server",
3
3
  "type": "module",
4
- "version": "1.0.2",
4
+ "version": "1.0.4",
5
5
  "description": "Server adapters for @mokup/runtime.",
6
6
  "license": "MIT",
7
7
  "homepage": "https://mokup.icebreaker.top",
@@ -16,6 +16,11 @@
16
16
  "import": "./dist/index.mjs",
17
17
  "require": "./dist/index.cjs"
18
18
  },
19
+ "./node": {
20
+ "types": "./dist/node.d.ts",
21
+ "import": "./dist/node.mjs",
22
+ "require": "./dist/node.cjs"
23
+ },
19
24
  "./worker": {
20
25
  "types": "./dist/worker.d.ts",
21
26
  "import": "./dist/worker.mjs",
@@ -32,13 +37,14 @@
32
37
  "dist"
33
38
  ],
34
39
  "dependencies": {
35
- "@mokup/playground": "0.0.6",
40
+ "@hono/node-server": "^1.19.9",
41
+ "@hono/node-ws": "^1.1.1",
42
+ "@mokup/playground": "0.0.8",
36
43
  "@mokup/runtime": "1.0.0",
37
44
  "@mokup/shared": "1.0.0"
38
45
  },
39
46
  "devDependencies": {
40
- "@hono/node-server": "^1.19.9",
41
- "@types/node": "^25.0.9",
47
+ "@types/node": "^25.0.10",
42
48
  "typescript": "^5.9.3",
43
49
  "unbuild": "^3.6.1"
44
50
  },