mokup 1.0.3 → 2.0.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
@@ -6,6 +6,7 @@ type RequestHandler = (context: Context) => Response | Promise<Response> | unkno
6
6
  type RouteResponse = unknown | RequestHandler;
7
7
  interface RouteRule {
8
8
  handler: RouteResponse;
9
+ enabled?: boolean;
9
10
  status?: number;
10
11
  headers?: Record<string, string>;
11
12
  delay?: number;
@@ -19,11 +20,18 @@ interface ServiceWorkerOptions {
19
20
  fallback?: boolean;
20
21
  basePath?: string | string[];
21
22
  }
23
+ type PlaygroundOptionsInput = boolean | {
24
+ path?: string;
25
+ enabled?: boolean;
26
+ } | undefined;
22
27
  interface RouteDirectoryConfig {
23
28
  headers?: Record<string, string>;
24
29
  status?: number;
25
30
  delay?: number;
26
31
  enabled?: boolean;
32
+ ignorePrefix?: string | string[];
33
+ include?: RegExp | RegExp[];
34
+ exclude?: RegExp | RegExp[];
27
35
  middleware?: MiddlewareHandler | MiddlewareHandler[];
28
36
  }
29
37
  interface VitePluginOptions {
@@ -31,15 +39,16 @@ interface VitePluginOptions {
31
39
  prefix?: string;
32
40
  include?: RegExp | RegExp[];
33
41
  exclude?: RegExp | RegExp[];
42
+ ignorePrefix?: string | string[];
34
43
  watch?: boolean;
35
44
  log?: boolean;
36
45
  mode?: RuntimeMode;
37
46
  sw?: ServiceWorkerOptions;
38
- playground?: boolean | {
39
- path?: string;
40
- enabled?: boolean;
41
- };
42
47
  }
43
- type VitePluginOptionsInput = VitePluginOptions | VitePluginOptions[];
48
+ interface MokupPluginOptions {
49
+ entries?: VitePluginOptions | VitePluginOptions[];
50
+ playground?: PlaygroundOptionsInput;
51
+ }
52
+ type VitePluginOptionsInput = MokupPluginOptions;
44
53
 
45
- export type { HttpMethod, RequestHandler, RouteDirectoryConfig, RouteResponse, RouteRule, RuntimeMode, ServiceWorkerOptions, VitePluginOptions, VitePluginOptionsInput };
54
+ export type { HttpMethod, MokupPluginOptions, PlaygroundOptionsInput, RequestHandler, RouteDirectoryConfig, RouteResponse, RouteRule, RuntimeMode, ServiceWorkerOptions, VitePluginOptions, VitePluginOptionsInput };
package/dist/index.d.mts CHANGED
@@ -6,6 +6,7 @@ type RequestHandler = (context: Context) => Response | Promise<Response> | unkno
6
6
  type RouteResponse = unknown | RequestHandler;
7
7
  interface RouteRule {
8
8
  handler: RouteResponse;
9
+ enabled?: boolean;
9
10
  status?: number;
10
11
  headers?: Record<string, string>;
11
12
  delay?: number;
@@ -19,11 +20,18 @@ interface ServiceWorkerOptions {
19
20
  fallback?: boolean;
20
21
  basePath?: string | string[];
21
22
  }
23
+ type PlaygroundOptionsInput = boolean | {
24
+ path?: string;
25
+ enabled?: boolean;
26
+ } | undefined;
22
27
  interface RouteDirectoryConfig {
23
28
  headers?: Record<string, string>;
24
29
  status?: number;
25
30
  delay?: number;
26
31
  enabled?: boolean;
32
+ ignorePrefix?: string | string[];
33
+ include?: RegExp | RegExp[];
34
+ exclude?: RegExp | RegExp[];
27
35
  middleware?: MiddlewareHandler | MiddlewareHandler[];
28
36
  }
29
37
  interface VitePluginOptions {
@@ -31,15 +39,16 @@ interface VitePluginOptions {
31
39
  prefix?: string;
32
40
  include?: RegExp | RegExp[];
33
41
  exclude?: RegExp | RegExp[];
42
+ ignorePrefix?: string | string[];
34
43
  watch?: boolean;
35
44
  log?: boolean;
36
45
  mode?: RuntimeMode;
37
46
  sw?: ServiceWorkerOptions;
38
- playground?: boolean | {
39
- path?: string;
40
- enabled?: boolean;
41
- };
42
47
  }
43
- type VitePluginOptionsInput = VitePluginOptions | VitePluginOptions[];
48
+ interface MokupPluginOptions {
49
+ entries?: VitePluginOptions | VitePluginOptions[];
50
+ playground?: PlaygroundOptionsInput;
51
+ }
52
+ type VitePluginOptionsInput = MokupPluginOptions;
44
53
 
45
- export type { HttpMethod, RequestHandler, RouteDirectoryConfig, RouteResponse, RouteRule, RuntimeMode, ServiceWorkerOptions, VitePluginOptions, VitePluginOptionsInput };
54
+ export type { HttpMethod, MokupPluginOptions, PlaygroundOptionsInput, RequestHandler, RouteDirectoryConfig, RouteResponse, RouteRule, RuntimeMode, ServiceWorkerOptions, VitePluginOptions, VitePluginOptionsInput };
package/dist/index.d.ts CHANGED
@@ -6,6 +6,7 @@ type RequestHandler = (context: Context) => Response | Promise<Response> | unkno
6
6
  type RouteResponse = unknown | RequestHandler;
7
7
  interface RouteRule {
8
8
  handler: RouteResponse;
9
+ enabled?: boolean;
9
10
  status?: number;
10
11
  headers?: Record<string, string>;
11
12
  delay?: number;
@@ -19,11 +20,18 @@ interface ServiceWorkerOptions {
19
20
  fallback?: boolean;
20
21
  basePath?: string | string[];
21
22
  }
23
+ type PlaygroundOptionsInput = boolean | {
24
+ path?: string;
25
+ enabled?: boolean;
26
+ } | undefined;
22
27
  interface RouteDirectoryConfig {
23
28
  headers?: Record<string, string>;
24
29
  status?: number;
25
30
  delay?: number;
26
31
  enabled?: boolean;
32
+ ignorePrefix?: string | string[];
33
+ include?: RegExp | RegExp[];
34
+ exclude?: RegExp | RegExp[];
27
35
  middleware?: MiddlewareHandler | MiddlewareHandler[];
28
36
  }
29
37
  interface VitePluginOptions {
@@ -31,15 +39,16 @@ interface VitePluginOptions {
31
39
  prefix?: string;
32
40
  include?: RegExp | RegExp[];
33
41
  exclude?: RegExp | RegExp[];
42
+ ignorePrefix?: string | string[];
34
43
  watch?: boolean;
35
44
  log?: boolean;
36
45
  mode?: RuntimeMode;
37
46
  sw?: ServiceWorkerOptions;
38
- playground?: boolean | {
39
- path?: string;
40
- enabled?: boolean;
41
- };
42
47
  }
43
- type VitePluginOptionsInput = VitePluginOptions | VitePluginOptions[];
48
+ interface MokupPluginOptions {
49
+ entries?: VitePluginOptions | VitePluginOptions[];
50
+ playground?: PlaygroundOptionsInput;
51
+ }
52
+ type VitePluginOptionsInput = MokupPluginOptions;
44
53
 
45
- export type { HttpMethod, RequestHandler, RouteDirectoryConfig, RouteResponse, RouteRule, RuntimeMode, ServiceWorkerOptions, VitePluginOptions, VitePluginOptionsInput };
54
+ export type { HttpMethod, MokupPluginOptions, PlaygroundOptionsInput, RequestHandler, RouteDirectoryConfig, RouteResponse, RouteRule, RuntimeMode, ServiceWorkerOptions, VitePluginOptions, VitePluginOptionsInput };
@@ -114,6 +114,20 @@ function matchesFilter(file, include, exclude) {
114
114
  }
115
115
  return true;
116
116
  }
117
+ function normalizeIgnorePrefix(value, fallback = ["."]) {
118
+ const list = typeof value === "undefined" ? fallback : Array.isArray(value) ? value : [value];
119
+ return list.filter((entry) => typeof entry === "string" && entry.length > 0);
120
+ }
121
+ function hasIgnoredPrefix(file, rootDir, prefixes) {
122
+ if (prefixes.length === 0) {
123
+ return false;
124
+ }
125
+ const relativePath = toPosix(pathe.relative(rootDir, file));
126
+ const segments = relativePath.split("/");
127
+ return segments.some(
128
+ (segment) => prefixes.some((prefix) => segment.startsWith(prefix))
129
+ );
130
+ }
117
131
  function delay(ms) {
118
132
  return new Promise((resolve2) => setTimeout(resolve2, ms));
119
133
  }
@@ -336,7 +350,7 @@ function createMiddleware(getApp, logger) {
336
350
  };
337
351
  }
338
352
 
339
- const require$1 = node_module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('shared/mokup.Cvbs0IQE.cjs', document.baseURI).href)));
353
+ const require$1 = node_module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('shared/mokup.CvPjMpMJ.cjs', document.baseURI).href)));
340
354
  const mimeTypes = {
341
355
  ".html": "text/html; charset=utf-8",
342
356
  ".css": "text/css; charset=utf-8",
@@ -494,6 +508,20 @@ function resolveGroupRoot(dirs, serverRoot) {
494
508
  }
495
509
  return common;
496
510
  }
511
+ const disabledReasonSet = /* @__PURE__ */ new Set([
512
+ "disabled",
513
+ "disabled-dir",
514
+ "exclude",
515
+ "ignore-prefix",
516
+ "include",
517
+ "unknown"
518
+ ]);
519
+ function normalizeDisabledReason(reason) {
520
+ if (reason && disabledReasonSet.has(reason)) {
521
+ return reason;
522
+ }
523
+ return "unknown";
524
+ }
497
525
  function formatRouteFile(file, root) {
498
526
  if (!root) {
499
527
  return toPosixPath(file);
@@ -554,6 +582,24 @@ function toPlaygroundRoute(route, root, groups) {
554
582
  group: matchedGroup?.label
555
583
  };
556
584
  }
585
+ function toPlaygroundDisabledRoute(route, root, groups) {
586
+ const matchedGroup = resolveRouteGroup(route.file, groups);
587
+ const disabled = {
588
+ file: formatRouteFile(route.file, root),
589
+ reason: normalizeDisabledReason(route.reason)
590
+ };
591
+ if (typeof route.method !== "undefined") {
592
+ disabled.method = route.method;
593
+ }
594
+ if (typeof route.url !== "undefined") {
595
+ disabled.url = route.url;
596
+ }
597
+ if (matchedGroup) {
598
+ disabled.groupKey = matchedGroup.key;
599
+ disabled.group = matchedGroup.label;
600
+ }
601
+ return disabled;
602
+ }
557
603
  function createPlaygroundMiddleware(params) {
558
604
  const distDir = resolvePlaygroundDist();
559
605
  const playgroundPath = params.config.path;
@@ -601,12 +647,14 @@ function createPlaygroundMiddleware(params) {
601
647
  const baseRoot = resolveGroupRoot(dirs, server?.config?.root);
602
648
  const groups = resolveGroups(dirs, baseRoot);
603
649
  const routes = params.getRoutes();
650
+ const disabledRoutes = params.getDisabledRoutes?.() ?? [];
604
651
  sendJson(res, {
605
652
  basePath: matchedPath,
606
653
  root: baseRoot,
607
654
  count: routes.length,
608
655
  groups: groups.map((group) => ({ key: group.key, label: group.label })),
609
- routes: routes.map((route) => toPlaygroundRoute(route, baseRoot, groups))
656
+ routes: routes.map((route) => toPlaygroundRoute(route, baseRoot, groups)),
657
+ disabled: disabledRoutes.map((route) => toPlaygroundDisabledRoute(route, baseRoot, groups))
610
658
  });
611
659
  return;
612
660
  }
@@ -754,7 +802,7 @@ const configExtensions = [".ts", ".js", ".mjs", ".cjs"];
754
802
  async function loadModule$1(file) {
755
803
  const ext = configExtensions.find((extension) => file.endsWith(extension));
756
804
  if (ext === ".cjs") {
757
- const require$1 = node_module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('shared/mokup.Cvbs0IQE.cjs', document.baseURI).href)));
805
+ const require$1 = node_module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('shared/mokup.CvPjMpMJ.cjs', document.baseURI).href)));
758
806
  delete require$1.cache[file];
759
807
  return require$1(file);
760
808
  }
@@ -882,6 +930,15 @@ async function resolveDirectoryConfig(params) {
882
930
  if (typeof config.enabled === "boolean") {
883
931
  merged.enabled = config.enabled;
884
932
  }
933
+ if (typeof config.ignorePrefix !== "undefined") {
934
+ merged.ignorePrefix = config.ignorePrefix;
935
+ }
936
+ if (typeof config.include !== "undefined") {
937
+ merged.include = config.include;
938
+ }
939
+ if (typeof config.exclude !== "undefined") {
940
+ merged.exclude = config.exclude;
941
+ }
885
942
  const normalized = normalizeMiddlewares(config.middleware, configPath, logger);
886
943
  if (normalized.length > 0) {
887
944
  merged.middlewares.push(...normalized);
@@ -938,7 +995,7 @@ function isSupportedFile(file) {
938
995
  async function loadModule(file) {
939
996
  const ext = pathe.extname(file).toLowerCase();
940
997
  if (ext === ".cjs") {
941
- const require$1 = node_module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('shared/mokup.Cvbs0IQE.cjs', document.baseURI).href)));
998
+ const require$1 = node_module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('shared/mokup.CvPjMpMJ.cjs', document.baseURI).href)));
942
999
  delete require$1.cache[file];
943
1000
  return require$1(file);
944
1001
  }
@@ -1024,19 +1081,52 @@ async function loadRules(file, server, logger) {
1024
1081
  return [value];
1025
1082
  }
1026
1083
 
1084
+ const silentLogger = {
1085
+ info: () => {
1086
+ },
1087
+ warn: () => {
1088
+ },
1089
+ error: () => {
1090
+ }
1091
+ };
1092
+ function resolveSkipRoute(params) {
1093
+ const derived = params.derived ?? deriveRouteFromFile(params.file, params.rootDir, silentLogger);
1094
+ if (!derived?.method) {
1095
+ return null;
1096
+ }
1097
+ const resolved = resolveRule({
1098
+ rule: { handler: null },
1099
+ derivedTemplate: derived.template,
1100
+ derivedMethod: derived.method,
1101
+ prefix: params.prefix,
1102
+ file: params.file,
1103
+ logger: silentLogger
1104
+ });
1105
+ if (!resolved) {
1106
+ return null;
1107
+ }
1108
+ return {
1109
+ method: resolved.method,
1110
+ url: resolved.template
1111
+ };
1112
+ }
1113
+ function buildSkipInfo(file, reason, resolved) {
1114
+ const info = { file, reason };
1115
+ if (resolved) {
1116
+ info.method = resolved.method;
1117
+ info.url = resolved.url;
1118
+ }
1119
+ return info;
1120
+ }
1027
1121
  async function scanRoutes(params) {
1028
1122
  const routes = [];
1029
1123
  const seen = /* @__PURE__ */ new Set();
1030
1124
  const files = await collectFiles(params.dirs);
1125
+ const globalIgnorePrefix = normalizeIgnorePrefix(params.ignorePrefix);
1031
1126
  const configCache = /* @__PURE__ */ new Map();
1032
1127
  const fileCache = /* @__PURE__ */ new Map();
1128
+ const shouldCollectSkip = typeof params.onSkip === "function";
1033
1129
  for (const fileInfo of files) {
1034
- if (!isSupportedFile(fileInfo.file)) {
1035
- continue;
1036
- }
1037
- if (!matchesFilter(fileInfo.file, params.include, params.exclude)) {
1038
- continue;
1039
- }
1040
1130
  const configParams = {
1041
1131
  file: fileInfo.file,
1042
1132
  rootDir: fileInfo.rootDir,
@@ -1049,6 +1139,43 @@ async function scanRoutes(params) {
1049
1139
  }
1050
1140
  const config = await resolveDirectoryConfig(configParams);
1051
1141
  if (config.enabled === false) {
1142
+ if (shouldCollectSkip && isSupportedFile(fileInfo.file)) {
1143
+ const resolved = resolveSkipRoute({
1144
+ file: fileInfo.file,
1145
+ rootDir: fileInfo.rootDir,
1146
+ prefix: params.prefix
1147
+ });
1148
+ params.onSkip?.(buildSkipInfo(fileInfo.file, "disabled-dir", resolved));
1149
+ }
1150
+ continue;
1151
+ }
1152
+ const effectiveIgnorePrefix = typeof config.ignorePrefix !== "undefined" ? normalizeIgnorePrefix(config.ignorePrefix, []) : globalIgnorePrefix;
1153
+ if (hasIgnoredPrefix(fileInfo.file, fileInfo.rootDir, effectiveIgnorePrefix)) {
1154
+ if (shouldCollectSkip && isSupportedFile(fileInfo.file)) {
1155
+ const resolved = resolveSkipRoute({
1156
+ file: fileInfo.file,
1157
+ rootDir: fileInfo.rootDir,
1158
+ prefix: params.prefix
1159
+ });
1160
+ params.onSkip?.(buildSkipInfo(fileInfo.file, "ignore-prefix", resolved));
1161
+ }
1162
+ continue;
1163
+ }
1164
+ if (!isSupportedFile(fileInfo.file)) {
1165
+ continue;
1166
+ }
1167
+ const effectiveInclude = typeof config.include !== "undefined" ? config.include : params.include;
1168
+ const effectiveExclude = typeof config.exclude !== "undefined" ? config.exclude : params.exclude;
1169
+ if (!matchesFilter(fileInfo.file, effectiveInclude, effectiveExclude)) {
1170
+ if (shouldCollectSkip) {
1171
+ const resolved = resolveSkipRoute({
1172
+ file: fileInfo.file,
1173
+ rootDir: fileInfo.rootDir,
1174
+ prefix: params.prefix
1175
+ });
1176
+ const reason = effectiveExclude && matchesFilter(fileInfo.file, void 0, effectiveExclude) ? "exclude" : "include";
1177
+ params.onSkip?.(buildSkipInfo(fileInfo.file, reason, resolved));
1178
+ }
1052
1179
  continue;
1053
1180
  }
1054
1181
  const derived = deriveRouteFromFile(fileInfo.file, fileInfo.rootDir, params.logger);
@@ -1060,6 +1187,18 @@ async function scanRoutes(params) {
1060
1187
  if (!rule || typeof rule !== "object") {
1061
1188
  continue;
1062
1189
  }
1190
+ if (rule.enabled === false) {
1191
+ if (shouldCollectSkip) {
1192
+ const resolved2 = resolveSkipRoute({
1193
+ file: fileInfo.file,
1194
+ rootDir: fileInfo.rootDir,
1195
+ prefix: params.prefix,
1196
+ derived
1197
+ });
1198
+ params.onSkip?.(buildSkipInfo(fileInfo.file, "disabled", resolved2));
1199
+ }
1200
+ continue;
1201
+ }
1063
1202
  const ruleValue = rule;
1064
1203
  const unsupportedKeys = ["response", "url", "method"].filter(
1065
1204
  (key2) => key2 in ruleValue
@@ -3,7 +3,7 @@ import { Hono, PatternRouter } from '@mokup/shared/hono';
3
3
  import { promises } from 'node:fs';
4
4
  import { createRequire } from 'node:module';
5
5
  import { cwd } from 'node:process';
6
- import { resolve, isAbsolute, join, normalize, extname, dirname, relative, basename } from '@mokup/shared/pathe';
6
+ import { resolve, isAbsolute, relative, join, normalize, extname, dirname, basename } from '@mokup/shared/pathe';
7
7
  import { pathToFileURL } from 'node:url';
8
8
  import { build } from '@mokup/shared/esbuild';
9
9
  import { parse } from '@mokup/shared/jsonc-parser';
@@ -111,6 +111,20 @@ function matchesFilter(file, include, exclude) {
111
111
  }
112
112
  return true;
113
113
  }
114
+ function normalizeIgnorePrefix(value, fallback = ["."]) {
115
+ const list = typeof value === "undefined" ? fallback : Array.isArray(value) ? value : [value];
116
+ return list.filter((entry) => typeof entry === "string" && entry.length > 0);
117
+ }
118
+ function hasIgnoredPrefix(file, rootDir, prefixes) {
119
+ if (prefixes.length === 0) {
120
+ return false;
121
+ }
122
+ const relativePath = toPosix(relative(rootDir, file));
123
+ const segments = relativePath.split("/");
124
+ return segments.some(
125
+ (segment) => prefixes.some((prefix) => segment.startsWith(prefix))
126
+ );
127
+ }
114
128
  function delay(ms) {
115
129
  return new Promise((resolve2) => setTimeout(resolve2, ms));
116
130
  }
@@ -491,6 +505,20 @@ function resolveGroupRoot(dirs, serverRoot) {
491
505
  }
492
506
  return common;
493
507
  }
508
+ const disabledReasonSet = /* @__PURE__ */ new Set([
509
+ "disabled",
510
+ "disabled-dir",
511
+ "exclude",
512
+ "ignore-prefix",
513
+ "include",
514
+ "unknown"
515
+ ]);
516
+ function normalizeDisabledReason(reason) {
517
+ if (reason && disabledReasonSet.has(reason)) {
518
+ return reason;
519
+ }
520
+ return "unknown";
521
+ }
494
522
  function formatRouteFile(file, root) {
495
523
  if (!root) {
496
524
  return toPosixPath(file);
@@ -551,6 +579,24 @@ function toPlaygroundRoute(route, root, groups) {
551
579
  group: matchedGroup?.label
552
580
  };
553
581
  }
582
+ function toPlaygroundDisabledRoute(route, root, groups) {
583
+ const matchedGroup = resolveRouteGroup(route.file, groups);
584
+ const disabled = {
585
+ file: formatRouteFile(route.file, root),
586
+ reason: normalizeDisabledReason(route.reason)
587
+ };
588
+ if (typeof route.method !== "undefined") {
589
+ disabled.method = route.method;
590
+ }
591
+ if (typeof route.url !== "undefined") {
592
+ disabled.url = route.url;
593
+ }
594
+ if (matchedGroup) {
595
+ disabled.groupKey = matchedGroup.key;
596
+ disabled.group = matchedGroup.label;
597
+ }
598
+ return disabled;
599
+ }
554
600
  function createPlaygroundMiddleware(params) {
555
601
  const distDir = resolvePlaygroundDist();
556
602
  const playgroundPath = params.config.path;
@@ -598,12 +644,14 @@ function createPlaygroundMiddleware(params) {
598
644
  const baseRoot = resolveGroupRoot(dirs, server?.config?.root);
599
645
  const groups = resolveGroups(dirs, baseRoot);
600
646
  const routes = params.getRoutes();
647
+ const disabledRoutes = params.getDisabledRoutes?.() ?? [];
601
648
  sendJson(res, {
602
649
  basePath: matchedPath,
603
650
  root: baseRoot,
604
651
  count: routes.length,
605
652
  groups: groups.map((group) => ({ key: group.key, label: group.label })),
606
- routes: routes.map((route) => toPlaygroundRoute(route, baseRoot, groups))
653
+ routes: routes.map((route) => toPlaygroundRoute(route, baseRoot, groups)),
654
+ disabled: disabledRoutes.map((route) => toPlaygroundDisabledRoute(route, baseRoot, groups))
607
655
  });
608
656
  return;
609
657
  }
@@ -879,6 +927,15 @@ async function resolveDirectoryConfig(params) {
879
927
  if (typeof config.enabled === "boolean") {
880
928
  merged.enabled = config.enabled;
881
929
  }
930
+ if (typeof config.ignorePrefix !== "undefined") {
931
+ merged.ignorePrefix = config.ignorePrefix;
932
+ }
933
+ if (typeof config.include !== "undefined") {
934
+ merged.include = config.include;
935
+ }
936
+ if (typeof config.exclude !== "undefined") {
937
+ merged.exclude = config.exclude;
938
+ }
882
939
  const normalized = normalizeMiddlewares(config.middleware, configPath, logger);
883
940
  if (normalized.length > 0) {
884
941
  merged.middlewares.push(...normalized);
@@ -1021,19 +1078,52 @@ async function loadRules(file, server, logger) {
1021
1078
  return [value];
1022
1079
  }
1023
1080
 
1081
+ const silentLogger = {
1082
+ info: () => {
1083
+ },
1084
+ warn: () => {
1085
+ },
1086
+ error: () => {
1087
+ }
1088
+ };
1089
+ function resolveSkipRoute(params) {
1090
+ const derived = params.derived ?? deriveRouteFromFile(params.file, params.rootDir, silentLogger);
1091
+ if (!derived?.method) {
1092
+ return null;
1093
+ }
1094
+ const resolved = resolveRule({
1095
+ rule: { handler: null },
1096
+ derivedTemplate: derived.template,
1097
+ derivedMethod: derived.method,
1098
+ prefix: params.prefix,
1099
+ file: params.file,
1100
+ logger: silentLogger
1101
+ });
1102
+ if (!resolved) {
1103
+ return null;
1104
+ }
1105
+ return {
1106
+ method: resolved.method,
1107
+ url: resolved.template
1108
+ };
1109
+ }
1110
+ function buildSkipInfo(file, reason, resolved) {
1111
+ const info = { file, reason };
1112
+ if (resolved) {
1113
+ info.method = resolved.method;
1114
+ info.url = resolved.url;
1115
+ }
1116
+ return info;
1117
+ }
1024
1118
  async function scanRoutes(params) {
1025
1119
  const routes = [];
1026
1120
  const seen = /* @__PURE__ */ new Set();
1027
1121
  const files = await collectFiles(params.dirs);
1122
+ const globalIgnorePrefix = normalizeIgnorePrefix(params.ignorePrefix);
1028
1123
  const configCache = /* @__PURE__ */ new Map();
1029
1124
  const fileCache = /* @__PURE__ */ new Map();
1125
+ const shouldCollectSkip = typeof params.onSkip === "function";
1030
1126
  for (const fileInfo of files) {
1031
- if (!isSupportedFile(fileInfo.file)) {
1032
- continue;
1033
- }
1034
- if (!matchesFilter(fileInfo.file, params.include, params.exclude)) {
1035
- continue;
1036
- }
1037
1127
  const configParams = {
1038
1128
  file: fileInfo.file,
1039
1129
  rootDir: fileInfo.rootDir,
@@ -1046,6 +1136,43 @@ async function scanRoutes(params) {
1046
1136
  }
1047
1137
  const config = await resolveDirectoryConfig(configParams);
1048
1138
  if (config.enabled === false) {
1139
+ if (shouldCollectSkip && isSupportedFile(fileInfo.file)) {
1140
+ const resolved = resolveSkipRoute({
1141
+ file: fileInfo.file,
1142
+ rootDir: fileInfo.rootDir,
1143
+ prefix: params.prefix
1144
+ });
1145
+ params.onSkip?.(buildSkipInfo(fileInfo.file, "disabled-dir", resolved));
1146
+ }
1147
+ continue;
1148
+ }
1149
+ const effectiveIgnorePrefix = typeof config.ignorePrefix !== "undefined" ? normalizeIgnorePrefix(config.ignorePrefix, []) : globalIgnorePrefix;
1150
+ if (hasIgnoredPrefix(fileInfo.file, fileInfo.rootDir, effectiveIgnorePrefix)) {
1151
+ if (shouldCollectSkip && isSupportedFile(fileInfo.file)) {
1152
+ const resolved = resolveSkipRoute({
1153
+ file: fileInfo.file,
1154
+ rootDir: fileInfo.rootDir,
1155
+ prefix: params.prefix
1156
+ });
1157
+ params.onSkip?.(buildSkipInfo(fileInfo.file, "ignore-prefix", resolved));
1158
+ }
1159
+ continue;
1160
+ }
1161
+ if (!isSupportedFile(fileInfo.file)) {
1162
+ continue;
1163
+ }
1164
+ const effectiveInclude = typeof config.include !== "undefined" ? config.include : params.include;
1165
+ const effectiveExclude = typeof config.exclude !== "undefined" ? config.exclude : params.exclude;
1166
+ if (!matchesFilter(fileInfo.file, effectiveInclude, effectiveExclude)) {
1167
+ if (shouldCollectSkip) {
1168
+ const resolved = resolveSkipRoute({
1169
+ file: fileInfo.file,
1170
+ rootDir: fileInfo.rootDir,
1171
+ prefix: params.prefix
1172
+ });
1173
+ const reason = effectiveExclude && matchesFilter(fileInfo.file, void 0, effectiveExclude) ? "exclude" : "include";
1174
+ params.onSkip?.(buildSkipInfo(fileInfo.file, reason, resolved));
1175
+ }
1049
1176
  continue;
1050
1177
  }
1051
1178
  const derived = deriveRouteFromFile(fileInfo.file, fileInfo.rootDir, params.logger);
@@ -1057,6 +1184,18 @@ async function scanRoutes(params) {
1057
1184
  if (!rule || typeof rule !== "object") {
1058
1185
  continue;
1059
1186
  }
1187
+ if (rule.enabled === false) {
1188
+ if (shouldCollectSkip) {
1189
+ const resolved2 = resolveSkipRoute({
1190
+ file: fileInfo.file,
1191
+ rootDir: fileInfo.rootDir,
1192
+ prefix: params.prefix,
1193
+ derived
1194
+ });
1195
+ params.onSkip?.(buildSkipInfo(fileInfo.file, "disabled", resolved2));
1196
+ }
1197
+ continue;
1198
+ }
1060
1199
  const ruleValue = rule;
1061
1200
  const unsupportedKeys = ["response", "url", "method"].filter(
1062
1201
  (key2) => key2 in ruleValue