@mokup/server 1.0.3 → 1.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.cjs CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  const runtime = require('@mokup/runtime');
4
4
  const fetch = require('./shared/server.BdTl0qJd.cjs');
5
- const node_process = require('node:process');
5
+ const process = require('node:process');
6
6
  const hono = require('@mokup/shared/hono');
7
7
  const pathe = require('@mokup/shared/pathe');
8
8
  const node_fs = require('node:fs');
@@ -13,6 +13,10 @@ const esbuild = require('@mokup/shared/esbuild');
13
13
  const jsoncParser = require('@mokup/shared/jsonc-parser');
14
14
 
15
15
  var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
16
+ function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; }
17
+
18
+ const process__default = /*#__PURE__*/_interopDefaultCompat(process);
19
+
16
20
  function createConnectMiddleware(options) {
17
21
  const runtime$1 = runtime.createRuntime(fetch.toRuntimeOptions(options));
18
22
  const onNotFound = options.onNotFound ?? "next";
@@ -141,6 +145,20 @@ function matchesFilter(file, include, exclude) {
141
145
  }
142
146
  return true;
143
147
  }
148
+ function normalizeIgnorePrefix(value, fallback = ["."]) {
149
+ const list = typeof value === "undefined" ? fallback : Array.isArray(value) ? value : [value];
150
+ return list.filter((entry) => typeof entry === "string" && entry.length > 0);
151
+ }
152
+ function hasIgnoredPrefix(file, rootDir, prefixes) {
153
+ if (prefixes.length === 0) {
154
+ return false;
155
+ }
156
+ const relativePath = toPosix(pathe.relative(rootDir, file));
157
+ const segments = relativePath.split("/");
158
+ return segments.some(
159
+ (segment) => prefixes.some((prefix) => segment.startsWith(prefix))
160
+ );
161
+ }
144
162
  function delay(ms) {
145
163
  return new Promise((resolve2) => setTimeout(resolve2, ms));
146
164
  }
@@ -344,7 +362,7 @@ function isAncestor(parent, child) {
344
362
  }
345
363
  function resolveGroupRoot(dirs, serverRoot) {
346
364
  if (!dirs || dirs.length === 0) {
347
- return serverRoot ?? node_process.cwd();
365
+ return serverRoot ?? process.cwd();
348
366
  }
349
367
  if (serverRoot) {
350
368
  const normalizedRoot = normalizePath(serverRoot);
@@ -368,10 +386,24 @@ function resolveGroupRoot(dirs, serverRoot) {
368
386
  }
369
387
  }
370
388
  if (!common || common === "/") {
371
- return serverRoot ?? node_process.cwd();
389
+ return serverRoot ?? process.cwd();
372
390
  }
373
391
  return common;
374
392
  }
393
+ const disabledReasonSet = /* @__PURE__ */ new Set([
394
+ "disabled",
395
+ "disabled-dir",
396
+ "exclude",
397
+ "ignore-prefix",
398
+ "include",
399
+ "unknown"
400
+ ]);
401
+ function normalizeDisabledReason(reason) {
402
+ if (reason && disabledReasonSet.has(reason)) {
403
+ return reason;
404
+ }
405
+ return "unknown";
406
+ }
375
407
  function formatRouteFile(file, root) {
376
408
  if (!root) {
377
409
  return toPosixPath(file);
@@ -432,6 +464,24 @@ function toPlaygroundRoute(route, root, groups) {
432
464
  group: matchedGroup?.label
433
465
  };
434
466
  }
467
+ function toPlaygroundDisabledRoute(route, root, groups) {
468
+ const matchedGroup = resolveRouteGroup(route.file, groups);
469
+ const disabled = {
470
+ file: formatRouteFile(route.file, root),
471
+ reason: normalizeDisabledReason(route.reason)
472
+ };
473
+ if (typeof route.method !== "undefined") {
474
+ disabled.method = route.method;
475
+ }
476
+ if (typeof route.url !== "undefined") {
477
+ disabled.url = route.url;
478
+ }
479
+ if (matchedGroup) {
480
+ disabled.groupKey = matchedGroup.key;
481
+ disabled.group = matchedGroup.label;
482
+ }
483
+ return disabled;
484
+ }
435
485
  function registerPlaygroundRoutes(params) {
436
486
  if (!params.config.enabled) {
437
487
  return;
@@ -469,7 +519,8 @@ function registerPlaygroundRoutes(params) {
469
519
  root: baseRoot,
470
520
  count: params.routes.length,
471
521
  groups: groups.map((group) => ({ key: group.key, label: group.label })),
472
- routes: params.routes.map((route) => toPlaygroundRoute(route, baseRoot, groups))
522
+ routes: params.routes.map((route) => toPlaygroundRoute(route, baseRoot, groups)),
523
+ disabled: (params.disabledRoutes ?? []).map((route) => toPlaygroundDisabledRoute(route, baseRoot, groups))
473
524
  });
474
525
  });
475
526
  params.app.get(`${playgroundPath}/*`, async (c) => {
@@ -615,8 +666,79 @@ function sortRoutes(routes) {
615
666
  });
616
667
  }
617
668
 
669
+ let registerPromise = null;
670
+ let hasLoggedFailure = false;
671
+ async function ensureTsxRegister(logger) {
672
+ if (registerPromise) {
673
+ return registerPromise;
674
+ }
675
+ registerPromise = (async () => {
676
+ try {
677
+ const mod = await import('tsx/esm/api');
678
+ const setSourceMapsEnabled = process__default.setSourceMapsEnabled;
679
+ if (typeof setSourceMapsEnabled === "function") {
680
+ setSourceMapsEnabled(true);
681
+ }
682
+ if (typeof mod.register === "function") {
683
+ mod.register();
684
+ }
685
+ return true;
686
+ } catch (error) {
687
+ if (!hasLoggedFailure && logger) {
688
+ logger.warn(
689
+ "Failed to register tsx loader; falling back to bundled TS loader.",
690
+ error
691
+ );
692
+ hasLoggedFailure = true;
693
+ }
694
+ return false;
695
+ }
696
+ })();
697
+ return registerPromise;
698
+ }
699
+
618
700
  const configExtensions = [".ts", ".js", ".mjs", ".cjs"];
619
- async function loadModule$1(file) {
701
+ function isUnknownFileExtensionError$1(error) {
702
+ if (!error || typeof error !== "object") {
703
+ return false;
704
+ }
705
+ const code = error.code;
706
+ if (code === "ERR_UNKNOWN_FILE_EXTENSION") {
707
+ return true;
708
+ }
709
+ const message = error.message;
710
+ return typeof message === "string" && message.includes("Unknown file extension");
711
+ }
712
+ async function loadTsModule$1(file, logger) {
713
+ const cacheBust = Date.now();
714
+ const fileUrl = `${node_url.pathToFileURL(file).href}?t=${cacheBust}`;
715
+ const registered = await ensureTsxRegister(logger);
716
+ if (registered) {
717
+ try {
718
+ return await import(fileUrl);
719
+ } catch (error) {
720
+ if (!isUnknownFileExtensionError$1(error)) {
721
+ throw error;
722
+ }
723
+ }
724
+ }
725
+ const result = await esbuild.build({
726
+ entryPoints: [file],
727
+ bundle: true,
728
+ format: "esm",
729
+ platform: "node",
730
+ sourcemap: "inline",
731
+ target: "es2020",
732
+ write: false
733
+ });
734
+ const output = result.outputFiles[0];
735
+ const code = output?.text ?? "";
736
+ const dataUrl = `data:text/javascript;base64,${node_buffer.Buffer.from(code).toString(
737
+ "base64"
738
+ )}`;
739
+ return import(`${dataUrl}#${cacheBust}`);
740
+ }
741
+ async function loadModule$1(file, logger) {
620
742
  const ext = configExtensions.find((extension) => file.endsWith(extension));
621
743
  if (ext === ".cjs") {
622
744
  const require$1 = node_module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
@@ -627,21 +749,7 @@ async function loadModule$1(file) {
627
749
  return import(`${node_url.pathToFileURL(file).href}?t=${Date.now()}`);
628
750
  }
629
751
  if (ext === ".ts") {
630
- const result = await esbuild.build({
631
- entryPoints: [file],
632
- bundle: true,
633
- format: "esm",
634
- platform: "node",
635
- sourcemap: "inline",
636
- target: "es2020",
637
- write: false
638
- });
639
- const output = result.outputFiles[0];
640
- const code = output?.text ?? "";
641
- const dataUrl = `data:text/javascript;base64,${node_buffer.Buffer.from(code).toString(
642
- "base64"
643
- )}`;
644
- return import(`${dataUrl}#${Date.now()}`);
752
+ return loadTsModule$1(file, logger);
645
753
  }
646
754
  return null;
647
755
  }
@@ -665,8 +773,8 @@ async function findConfigFile(dir, cache) {
665
773
  cache.set(dir, null);
666
774
  return null;
667
775
  }
668
- async function loadConfig(file) {
669
- const mod = await loadModule$1(file);
776
+ async function loadConfig(file, logger) {
777
+ const mod = await loadModule$1(file, logger);
670
778
  if (!mod) {
671
779
  return null;
672
780
  }
@@ -717,7 +825,7 @@ async function resolveDirectoryConfig(params) {
717
825
  }
718
826
  let config = configCache.get(configPath);
719
827
  if (config === void 0) {
720
- config = await loadConfig(configPath);
828
+ config = await loadConfig(configPath, logger);
721
829
  configCache.set(configPath, config);
722
830
  }
723
831
  if (!config) {
@@ -736,6 +844,15 @@ async function resolveDirectoryConfig(params) {
736
844
  if (typeof config.enabled === "boolean") {
737
845
  merged.enabled = config.enabled;
738
846
  }
847
+ if (typeof config.ignorePrefix !== "undefined") {
848
+ merged.ignorePrefix = config.ignorePrefix;
849
+ }
850
+ if (typeof config.include !== "undefined") {
851
+ merged.include = config.include;
852
+ }
853
+ if (typeof config.exclude !== "undefined") {
854
+ merged.exclude = config.exclude;
855
+ }
739
856
  const normalized = normalizeMiddlewares(config.middleware, configPath, logger);
740
857
  if (normalized.length > 0) {
741
858
  merged.middlewares.push(...normalized);
@@ -789,7 +906,47 @@ function isSupportedFile(file) {
789
906
  return supportedExtensions.has(ext);
790
907
  }
791
908
 
792
- async function loadModule(file) {
909
+ function isUnknownFileExtensionError(error) {
910
+ if (!error || typeof error !== "object") {
911
+ return false;
912
+ }
913
+ const code = error.code;
914
+ if (code === "ERR_UNKNOWN_FILE_EXTENSION") {
915
+ return true;
916
+ }
917
+ const message = error.message;
918
+ return typeof message === "string" && message.includes("Unknown file extension");
919
+ }
920
+ async function loadTsModule(file, logger) {
921
+ const cacheBust = Date.now();
922
+ const fileUrl = `${node_url.pathToFileURL(file).href}?t=${cacheBust}`;
923
+ const registered = await ensureTsxRegister(logger);
924
+ if (registered) {
925
+ try {
926
+ return await import(fileUrl);
927
+ } catch (error) {
928
+ if (!isUnknownFileExtensionError(error)) {
929
+ throw error;
930
+ }
931
+ }
932
+ }
933
+ const result = await esbuild.build({
934
+ entryPoints: [file],
935
+ bundle: true,
936
+ format: "esm",
937
+ platform: "node",
938
+ sourcemap: "inline",
939
+ target: "es2020",
940
+ write: false
941
+ });
942
+ const output = result.outputFiles[0];
943
+ const code = output?.text ?? "";
944
+ const dataUrl = `data:text/javascript;base64,${node_buffer.Buffer.from(code).toString(
945
+ "base64"
946
+ )}`;
947
+ return import(`${dataUrl}#${cacheBust}`);
948
+ }
949
+ async function loadModule(file, logger) {
793
950
  const ext = pathe.extname(file).toLowerCase();
794
951
  if (ext === ".cjs") {
795
952
  const require$1 = node_module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
@@ -800,21 +957,7 @@ async function loadModule(file) {
800
957
  return import(`${node_url.pathToFileURL(file).href}?t=${Date.now()}`);
801
958
  }
802
959
  if (ext === ".ts") {
803
- const result = await esbuild.build({
804
- entryPoints: [file],
805
- bundle: true,
806
- format: "esm",
807
- platform: "node",
808
- sourcemap: "inline",
809
- target: "es2020",
810
- write: false
811
- });
812
- const output = result.outputFiles[0];
813
- const code = output?.text ?? "";
814
- const dataUrl = `data:text/javascript;base64,${node_buffer.Buffer.from(code).toString(
815
- "base64"
816
- )}`;
817
- return import(`${dataUrl}#${Date.now()}`);
960
+ return loadTsModule(file, logger);
818
961
  }
819
962
  return null;
820
963
  }
@@ -849,7 +992,7 @@ async function loadRules(file, logger) {
849
992
  }
850
993
  ];
851
994
  }
852
- const mod = await loadModule(file);
995
+ const mod = await loadModule(file, logger);
853
996
  const value = mod?.default ?? mod;
854
997
  if (!value) {
855
998
  return [];
@@ -867,19 +1010,52 @@ async function loadRules(file, logger) {
867
1010
  return [value];
868
1011
  }
869
1012
 
1013
+ const silentLogger = {
1014
+ info: () => {
1015
+ },
1016
+ warn: () => {
1017
+ },
1018
+ error: () => {
1019
+ }
1020
+ };
1021
+ function resolveSkipRoute(params) {
1022
+ const derived = params.derived ?? deriveRouteFromFile(params.file, params.rootDir, silentLogger);
1023
+ if (!derived?.method) {
1024
+ return null;
1025
+ }
1026
+ const resolved = resolveRule({
1027
+ rule: { handler: null },
1028
+ derivedTemplate: derived.template,
1029
+ derivedMethod: derived.method,
1030
+ prefix: params.prefix,
1031
+ file: params.file,
1032
+ logger: silentLogger
1033
+ });
1034
+ if (!resolved) {
1035
+ return null;
1036
+ }
1037
+ return {
1038
+ method: resolved.method,
1039
+ url: resolved.template
1040
+ };
1041
+ }
1042
+ function buildSkipInfo(file, reason, resolved) {
1043
+ const info = { file, reason };
1044
+ if (resolved) {
1045
+ info.method = resolved.method;
1046
+ info.url = resolved.url;
1047
+ }
1048
+ return info;
1049
+ }
870
1050
  async function scanRoutes(params) {
871
1051
  const routes = [];
872
1052
  const seen = /* @__PURE__ */ new Set();
873
1053
  const files = await collectFiles(params.dirs);
1054
+ const globalIgnorePrefix = normalizeIgnorePrefix(params.ignorePrefix);
874
1055
  const configCache = /* @__PURE__ */ new Map();
875
1056
  const fileCache = /* @__PURE__ */ new Map();
1057
+ const shouldCollectSkip = typeof params.onSkip === "function";
876
1058
  for (const fileInfo of files) {
877
- if (!isSupportedFile(fileInfo.file)) {
878
- continue;
879
- }
880
- if (!matchesFilter(fileInfo.file, params.include, params.exclude)) {
881
- continue;
882
- }
883
1059
  const config = await resolveDirectoryConfig({
884
1060
  file: fileInfo.file,
885
1061
  rootDir: fileInfo.rootDir,
@@ -888,6 +1064,43 @@ async function scanRoutes(params) {
888
1064
  fileCache
889
1065
  });
890
1066
  if (config.enabled === false) {
1067
+ if (shouldCollectSkip && isSupportedFile(fileInfo.file)) {
1068
+ const resolved = resolveSkipRoute({
1069
+ file: fileInfo.file,
1070
+ rootDir: fileInfo.rootDir,
1071
+ prefix: params.prefix
1072
+ });
1073
+ params.onSkip?.(buildSkipInfo(fileInfo.file, "disabled-dir", resolved));
1074
+ }
1075
+ continue;
1076
+ }
1077
+ const effectiveIgnorePrefix = typeof config.ignorePrefix !== "undefined" ? normalizeIgnorePrefix(config.ignorePrefix, []) : globalIgnorePrefix;
1078
+ if (hasIgnoredPrefix(fileInfo.file, fileInfo.rootDir, effectiveIgnorePrefix)) {
1079
+ if (shouldCollectSkip && isSupportedFile(fileInfo.file)) {
1080
+ const resolved = resolveSkipRoute({
1081
+ file: fileInfo.file,
1082
+ rootDir: fileInfo.rootDir,
1083
+ prefix: params.prefix
1084
+ });
1085
+ params.onSkip?.(buildSkipInfo(fileInfo.file, "ignore-prefix", resolved));
1086
+ }
1087
+ continue;
1088
+ }
1089
+ if (!isSupportedFile(fileInfo.file)) {
1090
+ continue;
1091
+ }
1092
+ const effectiveInclude = typeof config.include !== "undefined" ? config.include : params.include;
1093
+ const effectiveExclude = typeof config.exclude !== "undefined" ? config.exclude : params.exclude;
1094
+ if (!matchesFilter(fileInfo.file, effectiveInclude, effectiveExclude)) {
1095
+ if (shouldCollectSkip) {
1096
+ const resolved = resolveSkipRoute({
1097
+ file: fileInfo.file,
1098
+ rootDir: fileInfo.rootDir,
1099
+ prefix: params.prefix
1100
+ });
1101
+ const reason = effectiveExclude && matchesFilter(fileInfo.file, void 0, effectiveExclude) ? "exclude" : "include";
1102
+ params.onSkip?.(buildSkipInfo(fileInfo.file, reason, resolved));
1103
+ }
891
1104
  continue;
892
1105
  }
893
1106
  const derived = deriveRouteFromFile(fileInfo.file, fileInfo.rootDir, params.logger);
@@ -899,6 +1112,18 @@ async function scanRoutes(params) {
899
1112
  if (!rule || typeof rule !== "object") {
900
1113
  continue;
901
1114
  }
1115
+ if (rule.enabled === false) {
1116
+ if (shouldCollectSkip) {
1117
+ const resolved2 = resolveSkipRoute({
1118
+ file: fileInfo.file,
1119
+ rootDir: fileInfo.rootDir,
1120
+ prefix: params.prefix,
1121
+ derived
1122
+ });
1123
+ params.onSkip?.(buildSkipInfo(fileInfo.file, "disabled", resolved2));
1124
+ }
1125
+ continue;
1126
+ }
902
1127
  const ruleValue = rule;
903
1128
  const unsupportedKeys = ["response", "url", "method"].filter(
904
1129
  (key2) => key2 in ruleValue
@@ -978,7 +1203,7 @@ function resolveRoot(list) {
978
1203
  if (deno?.cwd) {
979
1204
  return deno.cwd();
980
1205
  }
981
- return node_process.cwd();
1206
+ return process.cwd();
982
1207
  }
983
1208
  function resolveAllDirs(list, root) {
984
1209
  const dirs = [];
@@ -999,6 +1224,7 @@ function buildApp(params) {
999
1224
  registerPlaygroundRoutes({
1000
1225
  app,
1001
1226
  routes: params.routes,
1227
+ disabledRoutes: params.disabledRoutes,
1002
1228
  dirs: params.dirs,
1003
1229
  logger: params.logger,
1004
1230
  config: params.playground,
@@ -1008,7 +1234,8 @@ function buildApp(params) {
1008
1234
  app.get(`${params.playground.path}/ws`, params.wsHandler);
1009
1235
  }
1010
1236
  if (params.routes.length > 0) {
1011
- const mockApp = createHonoApp(params.routes, { onResponse: params.onResponse });
1237
+ const mockAppOptions = params.onResponse ? { onResponse: params.onResponse } : {};
1238
+ const mockApp = createHonoApp(params.routes, mockAppOptions);
1012
1239
  app.route("/", mockApp);
1013
1240
  }
1014
1241
  return app;
@@ -1162,23 +1389,27 @@ async function createFetchServer(options = {}) {
1162
1389
  }
1163
1390
  }
1164
1391
  let routes = [];
1392
+ let disabledRoutes = [];
1165
1393
  let app = buildApp({
1166
1394
  routes,
1395
+ disabledRoutes,
1167
1396
  dirs,
1168
1397
  playground: playgroundConfig,
1169
1398
  root,
1170
1399
  logger,
1171
1400
  onResponse: handleRouteResponse,
1172
- wsHandler
1401
+ ...wsHandler ? { wsHandler } : {}
1173
1402
  });
1174
1403
  const refreshRoutes = async () => {
1175
1404
  try {
1176
1405
  const collected = [];
1406
+ const collectedDisabled = [];
1177
1407
  for (const entry of optionList) {
1178
1408
  const scanParams = {
1179
1409
  dirs: resolveDirs(entry.dir, root),
1180
1410
  prefix: entry.prefix ?? "",
1181
- logger
1411
+ logger,
1412
+ onSkip: (info) => collectedDisabled.push(info)
1182
1413
  };
1183
1414
  if (entry.include) {
1184
1415
  scanParams.include = entry.include;
@@ -1186,19 +1417,24 @@ async function createFetchServer(options = {}) {
1186
1417
  if (entry.exclude) {
1187
1418
  scanParams.exclude = entry.exclude;
1188
1419
  }
1420
+ if (typeof entry.ignorePrefix !== "undefined") {
1421
+ scanParams.ignorePrefix = entry.ignorePrefix;
1422
+ }
1189
1423
  const scanned = await scanRoutes(scanParams);
1190
1424
  collected.push(...scanned);
1191
1425
  }
1192
1426
  const resolvedRoutes = sortRoutes(collected);
1193
1427
  routes = resolvedRoutes;
1428
+ disabledRoutes = collectedDisabled;
1194
1429
  app = buildApp({
1195
1430
  routes,
1431
+ disabledRoutes,
1196
1432
  dirs,
1197
1433
  playground: playgroundConfig,
1198
1434
  root,
1199
1435
  logger,
1200
1436
  onResponse: handleRouteResponse,
1201
- wsHandler
1437
+ ...wsHandler ? { wsHandler } : {}
1202
1438
  });
1203
1439
  logger.info(`Loaded ${routes.length} mock routes.`);
1204
1440
  } catch (error) {
@@ -1224,7 +1460,7 @@ async function createFetchServer(options = {}) {
1224
1460
  fetch,
1225
1461
  refresh: refreshRoutes,
1226
1462
  getRoutes: () => routes,
1227
- injectWebSocket
1463
+ ...injectWebSocket ? { injectWebSocket } : {}
1228
1464
  };
1229
1465
  if (watcher) {
1230
1466
  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 | {
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 | {
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 | {
package/dist/index.mjs CHANGED
@@ -1,8 +1,8 @@
1
1
  import { createRuntime, compareRouteScore, parseRouteTemplate } from '@mokup/runtime';
2
2
  import { t as toRuntimeOptions, a as toRuntimeRequestFromNode, b as applyRuntimeResultToNode, d as toBinaryBody, c as createFetchHandler } from './shared/server.Dje1y79O.mjs';
3
- import { cwd } from 'node:process';
3
+ import process, { 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
  }
@@ -369,6 +383,20 @@ function resolveGroupRoot(dirs, serverRoot) {
369
383
  }
370
384
  return common;
371
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
+ }
372
400
  function formatRouteFile(file, root) {
373
401
  if (!root) {
374
402
  return toPosixPath(file);
@@ -429,6 +457,24 @@ function toPlaygroundRoute(route, root, groups) {
429
457
  group: matchedGroup?.label
430
458
  };
431
459
  }
460
+ function toPlaygroundDisabledRoute(route, root, groups) {
461
+ const matchedGroup = resolveRouteGroup(route.file, groups);
462
+ const disabled = {
463
+ file: formatRouteFile(route.file, root),
464
+ reason: normalizeDisabledReason(route.reason)
465
+ };
466
+ if (typeof route.method !== "undefined") {
467
+ disabled.method = route.method;
468
+ }
469
+ if (typeof route.url !== "undefined") {
470
+ disabled.url = route.url;
471
+ }
472
+ if (matchedGroup) {
473
+ disabled.groupKey = matchedGroup.key;
474
+ disabled.group = matchedGroup.label;
475
+ }
476
+ return disabled;
477
+ }
432
478
  function registerPlaygroundRoutes(params) {
433
479
  if (!params.config.enabled) {
434
480
  return;
@@ -466,7 +512,8 @@ function registerPlaygroundRoutes(params) {
466
512
  root: baseRoot,
467
513
  count: params.routes.length,
468
514
  groups: groups.map((group) => ({ key: group.key, label: group.label })),
469
- routes: params.routes.map((route) => toPlaygroundRoute(route, baseRoot, groups))
515
+ routes: params.routes.map((route) => toPlaygroundRoute(route, baseRoot, groups)),
516
+ disabled: (params.disabledRoutes ?? []).map((route) => toPlaygroundDisabledRoute(route, baseRoot, groups))
470
517
  });
471
518
  });
472
519
  params.app.get(`${playgroundPath}/*`, async (c) => {
@@ -612,8 +659,79 @@ function sortRoutes(routes) {
612
659
  });
613
660
  }
614
661
 
662
+ let registerPromise = null;
663
+ let hasLoggedFailure = false;
664
+ async function ensureTsxRegister(logger) {
665
+ if (registerPromise) {
666
+ return registerPromise;
667
+ }
668
+ registerPromise = (async () => {
669
+ try {
670
+ const mod = await import('tsx/esm/api');
671
+ const setSourceMapsEnabled = process.setSourceMapsEnabled;
672
+ if (typeof setSourceMapsEnabled === "function") {
673
+ setSourceMapsEnabled(true);
674
+ }
675
+ if (typeof mod.register === "function") {
676
+ mod.register();
677
+ }
678
+ return true;
679
+ } catch (error) {
680
+ if (!hasLoggedFailure && logger) {
681
+ logger.warn(
682
+ "Failed to register tsx loader; falling back to bundled TS loader.",
683
+ error
684
+ );
685
+ hasLoggedFailure = true;
686
+ }
687
+ return false;
688
+ }
689
+ })();
690
+ return registerPromise;
691
+ }
692
+
615
693
  const configExtensions = [".ts", ".js", ".mjs", ".cjs"];
616
- async function loadModule$1(file) {
694
+ function isUnknownFileExtensionError$1(error) {
695
+ if (!error || typeof error !== "object") {
696
+ return false;
697
+ }
698
+ const code = error.code;
699
+ if (code === "ERR_UNKNOWN_FILE_EXTENSION") {
700
+ return true;
701
+ }
702
+ const message = error.message;
703
+ return typeof message === "string" && message.includes("Unknown file extension");
704
+ }
705
+ async function loadTsModule$1(file, logger) {
706
+ const cacheBust = Date.now();
707
+ const fileUrl = `${pathToFileURL(file).href}?t=${cacheBust}`;
708
+ const registered = await ensureTsxRegister(logger);
709
+ if (registered) {
710
+ try {
711
+ return await import(fileUrl);
712
+ } catch (error) {
713
+ if (!isUnknownFileExtensionError$1(error)) {
714
+ throw error;
715
+ }
716
+ }
717
+ }
718
+ const result = await build({
719
+ entryPoints: [file],
720
+ bundle: true,
721
+ format: "esm",
722
+ platform: "node",
723
+ sourcemap: "inline",
724
+ target: "es2020",
725
+ write: false
726
+ });
727
+ const output = result.outputFiles[0];
728
+ const code = output?.text ?? "";
729
+ const dataUrl = `data:text/javascript;base64,${Buffer.from(code).toString(
730
+ "base64"
731
+ )}`;
732
+ return import(`${dataUrl}#${cacheBust}`);
733
+ }
734
+ async function loadModule$1(file, logger) {
617
735
  const ext = configExtensions.find((extension) => file.endsWith(extension));
618
736
  if (ext === ".cjs") {
619
737
  const require = createRequire(import.meta.url);
@@ -624,21 +742,7 @@ async function loadModule$1(file) {
624
742
  return import(`${pathToFileURL(file).href}?t=${Date.now()}`);
625
743
  }
626
744
  if (ext === ".ts") {
627
- const result = await build({
628
- entryPoints: [file],
629
- bundle: true,
630
- format: "esm",
631
- platform: "node",
632
- sourcemap: "inline",
633
- target: "es2020",
634
- write: false
635
- });
636
- const output = result.outputFiles[0];
637
- const code = output?.text ?? "";
638
- const dataUrl = `data:text/javascript;base64,${Buffer.from(code).toString(
639
- "base64"
640
- )}`;
641
- return import(`${dataUrl}#${Date.now()}`);
745
+ return loadTsModule$1(file, logger);
642
746
  }
643
747
  return null;
644
748
  }
@@ -662,8 +766,8 @@ async function findConfigFile(dir, cache) {
662
766
  cache.set(dir, null);
663
767
  return null;
664
768
  }
665
- async function loadConfig(file) {
666
- const mod = await loadModule$1(file);
769
+ async function loadConfig(file, logger) {
770
+ const mod = await loadModule$1(file, logger);
667
771
  if (!mod) {
668
772
  return null;
669
773
  }
@@ -714,7 +818,7 @@ async function resolveDirectoryConfig(params) {
714
818
  }
715
819
  let config = configCache.get(configPath);
716
820
  if (config === void 0) {
717
- config = await loadConfig(configPath);
821
+ config = await loadConfig(configPath, logger);
718
822
  configCache.set(configPath, config);
719
823
  }
720
824
  if (!config) {
@@ -733,6 +837,15 @@ async function resolveDirectoryConfig(params) {
733
837
  if (typeof config.enabled === "boolean") {
734
838
  merged.enabled = config.enabled;
735
839
  }
840
+ if (typeof config.ignorePrefix !== "undefined") {
841
+ merged.ignorePrefix = config.ignorePrefix;
842
+ }
843
+ if (typeof config.include !== "undefined") {
844
+ merged.include = config.include;
845
+ }
846
+ if (typeof config.exclude !== "undefined") {
847
+ merged.exclude = config.exclude;
848
+ }
736
849
  const normalized = normalizeMiddlewares(config.middleware, configPath, logger);
737
850
  if (normalized.length > 0) {
738
851
  merged.middlewares.push(...normalized);
@@ -786,7 +899,47 @@ function isSupportedFile(file) {
786
899
  return supportedExtensions.has(ext);
787
900
  }
788
901
 
789
- async function loadModule(file) {
902
+ function isUnknownFileExtensionError(error) {
903
+ if (!error || typeof error !== "object") {
904
+ return false;
905
+ }
906
+ const code = error.code;
907
+ if (code === "ERR_UNKNOWN_FILE_EXTENSION") {
908
+ return true;
909
+ }
910
+ const message = error.message;
911
+ return typeof message === "string" && message.includes("Unknown file extension");
912
+ }
913
+ async function loadTsModule(file, logger) {
914
+ const cacheBust = Date.now();
915
+ const fileUrl = `${pathToFileURL(file).href}?t=${cacheBust}`;
916
+ const registered = await ensureTsxRegister(logger);
917
+ if (registered) {
918
+ try {
919
+ return await import(fileUrl);
920
+ } catch (error) {
921
+ if (!isUnknownFileExtensionError(error)) {
922
+ throw error;
923
+ }
924
+ }
925
+ }
926
+ const result = await build({
927
+ entryPoints: [file],
928
+ bundle: true,
929
+ format: "esm",
930
+ platform: "node",
931
+ sourcemap: "inline",
932
+ target: "es2020",
933
+ write: false
934
+ });
935
+ const output = result.outputFiles[0];
936
+ const code = output?.text ?? "";
937
+ const dataUrl = `data:text/javascript;base64,${Buffer.from(code).toString(
938
+ "base64"
939
+ )}`;
940
+ return import(`${dataUrl}#${cacheBust}`);
941
+ }
942
+ async function loadModule(file, logger) {
790
943
  const ext = extname(file).toLowerCase();
791
944
  if (ext === ".cjs") {
792
945
  const require = createRequire(import.meta.url);
@@ -797,21 +950,7 @@ async function loadModule(file) {
797
950
  return import(`${pathToFileURL(file).href}?t=${Date.now()}`);
798
951
  }
799
952
  if (ext === ".ts") {
800
- const result = await build({
801
- entryPoints: [file],
802
- bundle: true,
803
- format: "esm",
804
- platform: "node",
805
- sourcemap: "inline",
806
- target: "es2020",
807
- write: false
808
- });
809
- const output = result.outputFiles[0];
810
- const code = output?.text ?? "";
811
- const dataUrl = `data:text/javascript;base64,${Buffer.from(code).toString(
812
- "base64"
813
- )}`;
814
- return import(`${dataUrl}#${Date.now()}`);
953
+ return loadTsModule(file, logger);
815
954
  }
816
955
  return null;
817
956
  }
@@ -846,7 +985,7 @@ async function loadRules(file, logger) {
846
985
  }
847
986
  ];
848
987
  }
849
- const mod = await loadModule(file);
988
+ const mod = await loadModule(file, logger);
850
989
  const value = mod?.default ?? mod;
851
990
  if (!value) {
852
991
  return [];
@@ -864,19 +1003,52 @@ async function loadRules(file, logger) {
864
1003
  return [value];
865
1004
  }
866
1005
 
1006
+ const silentLogger = {
1007
+ info: () => {
1008
+ },
1009
+ warn: () => {
1010
+ },
1011
+ error: () => {
1012
+ }
1013
+ };
1014
+ function resolveSkipRoute(params) {
1015
+ const derived = params.derived ?? deriveRouteFromFile(params.file, params.rootDir, silentLogger);
1016
+ if (!derived?.method) {
1017
+ return null;
1018
+ }
1019
+ const resolved = resolveRule({
1020
+ rule: { handler: null },
1021
+ derivedTemplate: derived.template,
1022
+ derivedMethod: derived.method,
1023
+ prefix: params.prefix,
1024
+ file: params.file,
1025
+ logger: silentLogger
1026
+ });
1027
+ if (!resolved) {
1028
+ return null;
1029
+ }
1030
+ return {
1031
+ method: resolved.method,
1032
+ url: resolved.template
1033
+ };
1034
+ }
1035
+ function buildSkipInfo(file, reason, resolved) {
1036
+ const info = { file, reason };
1037
+ if (resolved) {
1038
+ info.method = resolved.method;
1039
+ info.url = resolved.url;
1040
+ }
1041
+ return info;
1042
+ }
867
1043
  async function scanRoutes(params) {
868
1044
  const routes = [];
869
1045
  const seen = /* @__PURE__ */ new Set();
870
1046
  const files = await collectFiles(params.dirs);
1047
+ const globalIgnorePrefix = normalizeIgnorePrefix(params.ignorePrefix);
871
1048
  const configCache = /* @__PURE__ */ new Map();
872
1049
  const fileCache = /* @__PURE__ */ new Map();
1050
+ const shouldCollectSkip = typeof params.onSkip === "function";
873
1051
  for (const fileInfo of files) {
874
- if (!isSupportedFile(fileInfo.file)) {
875
- continue;
876
- }
877
- if (!matchesFilter(fileInfo.file, params.include, params.exclude)) {
878
- continue;
879
- }
880
1052
  const config = await resolveDirectoryConfig({
881
1053
  file: fileInfo.file,
882
1054
  rootDir: fileInfo.rootDir,
@@ -885,6 +1057,43 @@ async function scanRoutes(params) {
885
1057
  fileCache
886
1058
  });
887
1059
  if (config.enabled === false) {
1060
+ if (shouldCollectSkip && isSupportedFile(fileInfo.file)) {
1061
+ const resolved = resolveSkipRoute({
1062
+ file: fileInfo.file,
1063
+ rootDir: fileInfo.rootDir,
1064
+ prefix: params.prefix
1065
+ });
1066
+ params.onSkip?.(buildSkipInfo(fileInfo.file, "disabled-dir", resolved));
1067
+ }
1068
+ continue;
1069
+ }
1070
+ const effectiveIgnorePrefix = typeof config.ignorePrefix !== "undefined" ? normalizeIgnorePrefix(config.ignorePrefix, []) : globalIgnorePrefix;
1071
+ if (hasIgnoredPrefix(fileInfo.file, fileInfo.rootDir, effectiveIgnorePrefix)) {
1072
+ if (shouldCollectSkip && isSupportedFile(fileInfo.file)) {
1073
+ const resolved = resolveSkipRoute({
1074
+ file: fileInfo.file,
1075
+ rootDir: fileInfo.rootDir,
1076
+ prefix: params.prefix
1077
+ });
1078
+ params.onSkip?.(buildSkipInfo(fileInfo.file, "ignore-prefix", resolved));
1079
+ }
1080
+ continue;
1081
+ }
1082
+ if (!isSupportedFile(fileInfo.file)) {
1083
+ continue;
1084
+ }
1085
+ const effectiveInclude = typeof config.include !== "undefined" ? config.include : params.include;
1086
+ const effectiveExclude = typeof config.exclude !== "undefined" ? config.exclude : params.exclude;
1087
+ if (!matchesFilter(fileInfo.file, effectiveInclude, effectiveExclude)) {
1088
+ if (shouldCollectSkip) {
1089
+ const resolved = resolveSkipRoute({
1090
+ file: fileInfo.file,
1091
+ rootDir: fileInfo.rootDir,
1092
+ prefix: params.prefix
1093
+ });
1094
+ const reason = effectiveExclude && matchesFilter(fileInfo.file, void 0, effectiveExclude) ? "exclude" : "include";
1095
+ params.onSkip?.(buildSkipInfo(fileInfo.file, reason, resolved));
1096
+ }
888
1097
  continue;
889
1098
  }
890
1099
  const derived = deriveRouteFromFile(fileInfo.file, fileInfo.rootDir, params.logger);
@@ -896,6 +1105,18 @@ async function scanRoutes(params) {
896
1105
  if (!rule || typeof rule !== "object") {
897
1106
  continue;
898
1107
  }
1108
+ if (rule.enabled === false) {
1109
+ if (shouldCollectSkip) {
1110
+ const resolved2 = resolveSkipRoute({
1111
+ file: fileInfo.file,
1112
+ rootDir: fileInfo.rootDir,
1113
+ prefix: params.prefix,
1114
+ derived
1115
+ });
1116
+ params.onSkip?.(buildSkipInfo(fileInfo.file, "disabled", resolved2));
1117
+ }
1118
+ continue;
1119
+ }
899
1120
  const ruleValue = rule;
900
1121
  const unsupportedKeys = ["response", "url", "method"].filter(
901
1122
  (key2) => key2 in ruleValue
@@ -996,6 +1217,7 @@ function buildApp(params) {
996
1217
  registerPlaygroundRoutes({
997
1218
  app,
998
1219
  routes: params.routes,
1220
+ disabledRoutes: params.disabledRoutes,
999
1221
  dirs: params.dirs,
1000
1222
  logger: params.logger,
1001
1223
  config: params.playground,
@@ -1005,7 +1227,8 @@ function buildApp(params) {
1005
1227
  app.get(`${params.playground.path}/ws`, params.wsHandler);
1006
1228
  }
1007
1229
  if (params.routes.length > 0) {
1008
- const mockApp = createHonoApp(params.routes, { onResponse: params.onResponse });
1230
+ const mockAppOptions = params.onResponse ? { onResponse: params.onResponse } : {};
1231
+ const mockApp = createHonoApp(params.routes, mockAppOptions);
1009
1232
  app.route("/", mockApp);
1010
1233
  }
1011
1234
  return app;
@@ -1159,23 +1382,27 @@ async function createFetchServer(options = {}) {
1159
1382
  }
1160
1383
  }
1161
1384
  let routes = [];
1385
+ let disabledRoutes = [];
1162
1386
  let app = buildApp({
1163
1387
  routes,
1388
+ disabledRoutes,
1164
1389
  dirs,
1165
1390
  playground: playgroundConfig,
1166
1391
  root,
1167
1392
  logger,
1168
1393
  onResponse: handleRouteResponse,
1169
- wsHandler
1394
+ ...wsHandler ? { wsHandler } : {}
1170
1395
  });
1171
1396
  const refreshRoutes = async () => {
1172
1397
  try {
1173
1398
  const collected = [];
1399
+ const collectedDisabled = [];
1174
1400
  for (const entry of optionList) {
1175
1401
  const scanParams = {
1176
1402
  dirs: resolveDirs(entry.dir, root),
1177
1403
  prefix: entry.prefix ?? "",
1178
- logger
1404
+ logger,
1405
+ onSkip: (info) => collectedDisabled.push(info)
1179
1406
  };
1180
1407
  if (entry.include) {
1181
1408
  scanParams.include = entry.include;
@@ -1183,19 +1410,24 @@ async function createFetchServer(options = {}) {
1183
1410
  if (entry.exclude) {
1184
1411
  scanParams.exclude = entry.exclude;
1185
1412
  }
1413
+ if (typeof entry.ignorePrefix !== "undefined") {
1414
+ scanParams.ignorePrefix = entry.ignorePrefix;
1415
+ }
1186
1416
  const scanned = await scanRoutes(scanParams);
1187
1417
  collected.push(...scanned);
1188
1418
  }
1189
1419
  const resolvedRoutes = sortRoutes(collected);
1190
1420
  routes = resolvedRoutes;
1421
+ disabledRoutes = collectedDisabled;
1191
1422
  app = buildApp({
1192
1423
  routes,
1424
+ disabledRoutes,
1193
1425
  dirs,
1194
1426
  playground: playgroundConfig,
1195
1427
  root,
1196
1428
  logger,
1197
1429
  onResponse: handleRouteResponse,
1198
- wsHandler
1430
+ ...wsHandler ? { wsHandler } : {}
1199
1431
  });
1200
1432
  logger.info(`Loaded ${routes.length} mock routes.`);
1201
1433
  } catch (error) {
@@ -1221,7 +1453,7 @@ async function createFetchServer(options = {}) {
1221
1453
  fetch,
1222
1454
  refresh: refreshRoutes,
1223
1455
  getRoutes: () => routes,
1224
- injectWebSocket
1456
+ ...injectWebSocket ? { injectWebSocket } : {}
1225
1457
  };
1226
1458
  if (watcher) {
1227
1459
  server.close = async () => {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mokup/server",
3
3
  "type": "module",
4
- "version": "1.0.3",
4
+ "version": "1.1.0",
5
5
  "description": "Server adapters for @mokup/runtime.",
6
6
  "license": "MIT",
7
7
  "homepage": "https://mokup.icebreaker.top",
@@ -39,12 +39,13 @@
39
39
  "dependencies": {
40
40
  "@hono/node-server": "^1.19.9",
41
41
  "@hono/node-ws": "^1.1.1",
42
+ "tsx": "^4.21.0",
43
+ "@mokup/playground": "0.0.9",
42
44
  "@mokup/runtime": "1.0.0",
43
- "@mokup/playground": "0.0.7",
44
45
  "@mokup/shared": "1.0.0"
45
46
  },
46
47
  "devDependencies": {
47
- "@types/node": "^25.0.9",
48
+ "@types/node": "^25.0.10",
48
49
  "typescript": "^5.9.3",
49
50
  "unbuild": "^3.6.1"
50
51
  },