@vercel/fs-detectors 5.8.3 → 5.8.5

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.
@@ -527,7 +527,7 @@ function checkUnusedFunctions(frontendBuilder, usedFunctions, options) {
527
527
  }
528
528
  }
529
529
  }
530
- if (frontendBuilder && ((0, import_is_official_runtime.isOfficialRuntime)("express", frontendBuilder.use) || (0, import_is_official_runtime.isOfficialRuntime)("hono", frontendBuilder.use))) {
530
+ if (frontendBuilder && ((0, import_is_official_runtime.isOfficialRuntime)("express", frontendBuilder.use) || (0, import_is_official_runtime.isOfficialRuntime)("hono", frontendBuilder.use) || (0, import_is_official_runtime.isOfficialRuntime)("backends", frontendBuilder.use))) {
531
531
  const validFilenames = [
532
532
  "app",
533
533
  "index",
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ export { detectBuilders, detectOutputDirectory, detectApiDirectory, detectApiExt
2
2
  export { detectServices, generateServicesRoutes, } from './services/detect-services';
3
3
  export { autoDetectServices } from './services/auto-detect';
4
4
  export type { AutoDetectOptions, AutoDetectResult, } from './services/auto-detect';
5
- export { isStaticBuild } from './services/utils';
5
+ export { isStaticBuild, isRouteOwningBuilder } from './services/utils';
6
6
  export { getServicesBuilders } from './services/get-services-builders';
7
7
  export type { DetectServicesOptions, DetectServicesResult, ResolvedService, Service, ServicesRoutes, ServiceDetectionError, } from './services/types';
8
8
  export { detectFileSystemAPI } from './detect-file-system-api';
package/dist/index.js CHANGED
@@ -44,6 +44,7 @@ __export(src_exports, {
44
44
  getWorkspacePackagePaths: () => import_get_workspace_package_paths.getWorkspacePackagePaths,
45
45
  getWorkspaces: () => import_get_workspaces.getWorkspaces,
46
46
  isOfficialRuntime: () => import_is_official_runtime.isOfficialRuntime,
47
+ isRouteOwningBuilder: () => import_utils.isRouteOwningBuilder,
47
48
  isStaticBuild: () => import_utils.isStaticBuild,
48
49
  isStaticRuntime: () => import_is_official_runtime.isStaticRuntime,
49
50
  monorepoManagers: () => import_monorepo_managers.monorepoManagers,
@@ -97,6 +98,7 @@ var import_detect_instrumentation = require("./detect-instrumentation");
97
98
  getWorkspacePackagePaths,
98
99
  getWorkspaces,
99
100
  isOfficialRuntime,
101
+ isRouteOwningBuilder,
100
102
  isStaticBuild,
101
103
  isStaticRuntime,
102
104
  monorepoManagers,
@@ -13,8 +13,18 @@ export declare function detectServices(options: DetectServicesOptions): Promise<
13
13
  * routes match before broader ones. For example, `/api/users` must be checked
14
14
  * before `/api`, which must be checked before the catch-all `/`.
15
15
  *
16
- * - Static/SPA services: SPA fallback routes to index.html
17
- * - Serverless services: Rewrite to the function entrypoint
16
+ * Services routing only generates *synthetic* routes for builders that do not
17
+ * provide their own route tables:
18
+ *
19
+ * - **Static/SPA services** (`@vercel/static-build`, `@vercel/static`):
20
+ * SPA fallback routes to index.html under the service prefix.
21
+ *
22
+ * - **Runtime services** (`@vercel/python`, `@vercel/go`, `@vercel/ruby`, etc.):
23
+ * Prefix rewrites to the function entrypoint with `check: true`.
24
+ *
25
+ * Builders that provide their own routing (`@vercel/next`, `@vercel/backends`,
26
+ * Build Output API builders, etc.) are not given synthetic routes here.
27
+ *
18
28
  * - Cron/Worker services: TODO - internal routes under `/_svc/`
19
29
  */
20
30
  export declare function generateServicesRoutes(services: ResolvedService[]): ServicesRoutes;
@@ -22,6 +22,7 @@ __export(detect_services_exports, {
22
22
  generateServicesRoutes: () => generateServicesRoutes
23
23
  });
24
24
  module.exports = __toCommonJS(detect_services_exports);
25
+ var import_types = require("./types");
25
26
  var import_utils = require("./utils");
26
27
  var import_resolve = require("./resolve");
27
28
  var import_auto_detect = require("./auto-detect");
@@ -85,12 +86,26 @@ function generateServicesRoutes(services) {
85
86
  const defaults = [];
86
87
  const crons = [];
87
88
  const workers = [];
89
+ const entrypointExtensions = Object.keys(import_types.ENTRYPOINT_EXTENSIONS).sort(
90
+ (a, b) => b.length - a.length
91
+ );
92
+ const stripEntrypointExtension = (entrypoint) => {
93
+ for (const ext of entrypointExtensions) {
94
+ if (entrypoint.endsWith(ext)) {
95
+ return entrypoint.slice(0, -ext.length);
96
+ }
97
+ }
98
+ return entrypoint;
99
+ };
88
100
  const sortedWebServices = services.filter(
89
101
  (s) => s.type === "web" && typeof s.routePrefix === "string"
90
102
  ).sort((a, b) => b.routePrefix.length - a.routePrefix.length);
91
103
  for (const service of sortedWebServices) {
92
104
  const { routePrefix } = service;
93
105
  const normalizedPrefix = routePrefix.slice(1);
106
+ if ((0, import_utils.isRouteOwningBuilder)(service)) {
107
+ continue;
108
+ }
94
109
  if ((0, import_utils.isStaticBuild)(service)) {
95
110
  if (routePrefix === "/") {
96
111
  defaults.push({ handle: "filesystem" });
@@ -101,9 +116,10 @@ function generateServicesRoutes(services) {
101
116
  dest: `/${normalizedPrefix}/index.html`
102
117
  });
103
118
  }
104
- } else {
119
+ } else if (service.runtime) {
105
120
  const builderSrc = service.builder.src || routePrefix;
106
- const functionPath = builderSrc.startsWith("/") ? builderSrc : `/${builderSrc}`;
121
+ const extensionless = stripEntrypointExtension(builderSrc);
122
+ const functionPath = extensionless.startsWith("/") ? extensionless : `/${extensionless}`;
107
123
  if (routePrefix === "/") {
108
124
  defaults.push({ src: "^/(.*)$", dest: functionPath, check: true });
109
125
  } else {
@@ -113,6 +129,8 @@ function generateServicesRoutes(services) {
113
129
  check: true
114
130
  });
115
131
  }
132
+ } else {
133
+ continue;
116
134
  }
117
135
  }
118
136
  return { rewrites, defaults, crons, workers };
@@ -39,6 +39,13 @@ var import_utils = require("./utils");
39
39
  var import_frameworks = __toESM(require("@vercel/frameworks"));
40
40
  const frameworksBySlug = new Map(import_frameworks.default.map((f) => [f.slug, f]));
41
41
  const SERVICE_NAME_REGEX = /^[a-zA-Z]([a-zA-Z0-9_-]*[a-zA-Z0-9])?$/;
42
+ function normalizeRoutePrefix(routePrefix) {
43
+ let normalized = routePrefix.startsWith("/") ? routePrefix : `/${routePrefix}`;
44
+ if (normalized !== "/" && normalized.endsWith("/")) {
45
+ normalized = normalized.slice(0, -1);
46
+ }
47
+ return normalized || "/";
48
+ }
42
49
  function validateServiceConfig(name, config) {
43
50
  if (!SERVICE_NAME_REGEX.test(name)) {
44
51
  return {
@@ -141,10 +148,10 @@ function resolveConfiguredService(name, config, group) {
141
148
  }
142
149
  const routePrefix = type === "web" && config.routePrefix ? config.routePrefix.startsWith("/") ? config.routePrefix : `/${config.routePrefix}` : void 0;
143
150
  const isRoot = workspace === ".";
144
- if (!isRoot && !builderSrc.startsWith(workspace + "/")) {
151
+ if (!isRoot) {
145
152
  builderSrc = import_path.posix.join(workspace, builderSrc);
146
153
  }
147
- const builderConfig = {};
154
+ const builderConfig = { zeroConfig: true };
148
155
  if (config.memory)
149
156
  builderConfig.memory = config.memory;
150
157
  if (config.maxDuration)
@@ -159,6 +166,9 @@ function resolveConfiguredService(name, config, group) {
159
166
  const stripped = routePrefix.startsWith("/") ? routePrefix.slice(1) : routePrefix;
160
167
  builderConfig.routePrefix = stripped || ".";
161
168
  }
169
+ if (workspace && workspace !== ".") {
170
+ builderConfig.workspace = workspace;
171
+ }
162
172
  if (config.framework) {
163
173
  builderConfig.framework = config.framework;
164
174
  }
@@ -186,6 +196,7 @@ function resolveConfiguredService(name, config, group) {
186
196
  function resolveAllConfiguredServices(services) {
187
197
  const resolved = [];
188
198
  const errors = [];
199
+ const webServicesByRoutePrefix = /* @__PURE__ */ new Map();
189
200
  for (const name of Object.keys(services)) {
190
201
  const serviceConfig = services[name];
191
202
  const validationError = validateServiceConfig(name, serviceConfig);
@@ -194,6 +205,21 @@ function resolveAllConfiguredServices(services) {
194
205
  continue;
195
206
  }
196
207
  const service = resolveConfiguredService(name, serviceConfig);
208
+ if (service.type === "web" && typeof service.routePrefix === "string") {
209
+ const normalizedRoutePrefix = normalizeRoutePrefix(service.routePrefix);
210
+ const existingServiceName = webServicesByRoutePrefix.get(
211
+ normalizedRoutePrefix
212
+ );
213
+ if (existingServiceName) {
214
+ errors.push({
215
+ code: "DUPLICATE_ROUTE_PREFIX",
216
+ message: `Web services "${existingServiceName}" and "${name}" cannot share routePrefix "${normalizedRoutePrefix}".`,
217
+ serviceName: name
218
+ });
219
+ continue;
220
+ }
221
+ webServicesByRoutePrefix.set(normalizedRoutePrefix, name);
222
+ }
197
223
  resolved.push(service);
198
224
  }
199
225
  return { services: resolved, errors };
@@ -56,3 +56,16 @@ export declare const ENTRYPOINT_EXTENSIONS: Record<string, ServiceRuntime>;
56
56
  * These don't have a "runtime" - they just build to static files.
57
57
  */
58
58
  export declare const STATIC_BUILDERS: Set<string>;
59
+ /**
60
+ * Builders that produce their own full route table with handle phases
61
+ * (filesystem, miss, rewrite, hit, error).
62
+ *
63
+ * In services mode we generally avoid generating synthetic catch-all routes
64
+ * for builders that provide their own routing. At service-detection time we
65
+ * only have the builder "use" string (not the loaded module), so this is an
66
+ * explicit allow-list for known route-table builders.
67
+ *
68
+ * NOTE: This is an explicit positive set because we can't check
69
+ * `builder.version` at service detection time.
70
+ */
71
+ export declare const ROUTE_OWNING_BUILDERS: Set<string>;
@@ -19,6 +19,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
19
19
  var types_exports = {};
20
20
  __export(types_exports, {
21
21
  ENTRYPOINT_EXTENSIONS: () => ENTRYPOINT_EXTENSIONS,
22
+ ROUTE_OWNING_BUILDERS: () => ROUTE_OWNING_BUILDERS,
22
23
  RUNTIME_BUILDERS: () => RUNTIME_BUILDERS,
23
24
  STATIC_BUILDERS: () => STATIC_BUILDERS
24
25
  });
@@ -46,9 +47,14 @@ const STATIC_BUILDERS = /* @__PURE__ */ new Set([
46
47
  "@vercel/static-build",
47
48
  "@vercel/static"
48
49
  ]);
50
+ const ROUTE_OWNING_BUILDERS = /* @__PURE__ */ new Set([
51
+ "@vercel/next",
52
+ "@vercel/backends"
53
+ ]);
49
54
  // Annotate the CommonJS export names for ESM import in node:
50
55
  0 && (module.exports = {
51
56
  ENTRYPOINT_EXTENSIONS,
57
+ ROUTE_OWNING_BUILDERS,
52
58
  RUNTIME_BUILDERS,
53
59
  STATIC_BUILDERS
54
60
  });
@@ -2,6 +2,15 @@ import type { DetectorFilesystem } from '../detectors/filesystem';
2
2
  import type { ServiceRuntime, ExperimentalServices, ServiceDetectionError, ResolvedService } from './types';
3
3
  export declare function getBuilderForRuntime(runtime: ServiceRuntime): string;
4
4
  export declare function isStaticBuild(service: ResolvedService): boolean;
5
+ /**
6
+ * Determines if a service uses a "route-owning" builder.
7
+ *
8
+ * Route-owning builders (e.g., `@vercel/next`, `@vercel/backends`) produce
9
+ * their own full route table with handle phases (filesystem, miss, rewrite,
10
+ * hit, error). The services system should NOT generate synthetic catch-all
11
+ * rewrites for them — instead, we rely on the builder's own `routes[]`.
12
+ */
13
+ export declare function isRouteOwningBuilder(service: ResolvedService): boolean;
5
14
  /**
6
15
  * Infer runtime from available service configuration.
7
16
  *
@@ -20,6 +20,7 @@ var utils_exports = {};
20
20
  __export(utils_exports, {
21
21
  getBuilderForRuntime: () => getBuilderForRuntime,
22
22
  inferServiceRuntime: () => inferServiceRuntime,
23
+ isRouteOwningBuilder: () => isRouteOwningBuilder,
23
24
  isStaticBuild: () => isStaticBuild,
24
25
  readVercelConfig: () => readVercelConfig
25
26
  });
@@ -36,6 +37,9 @@ function getBuilderForRuntime(runtime) {
36
37
  function isStaticBuild(service) {
37
38
  return import_types.STATIC_BUILDERS.has(service.builder.use);
38
39
  }
40
+ function isRouteOwningBuilder(service) {
41
+ return import_types.ROUTE_OWNING_BUILDERS.has(service.builder.use);
42
+ }
39
43
  function inferServiceRuntime(config) {
40
44
  if (config.runtime && config.runtime in import_types.RUNTIME_BUILDERS) {
41
45
  return config.runtime;
@@ -85,6 +89,7 @@ async function readVercelConfig(fs) {
85
89
  0 && (module.exports = {
86
90
  getBuilderForRuntime,
87
91
  inferServiceRuntime,
92
+ isRouteOwningBuilder,
88
93
  isStaticBuild,
89
94
  readVercelConfig
90
95
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vercel/fs-detectors",
3
- "version": "5.8.3",
3
+ "version": "5.8.5",
4
4
  "description": "Vercel filesystem detectors",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -19,9 +19,9 @@
19
19
  "json5": "2.2.2",
20
20
  "minimatch": "3.1.2",
21
21
  "semver": "6.3.1",
22
- "@vercel/error-utils": "2.0.3",
22
+ "@vercel/routing-utils": "5.3.2",
23
23
  "@vercel/frameworks": "3.17.1",
24
- "@vercel/routing-utils": "5.3.2"
24
+ "@vercel/error-utils": "2.0.3"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@types/glob": "7.2.0",
@@ -32,7 +32,7 @@
32
32
  "@types/semver": "7.3.10",
33
33
  "jest-junit": "16.0.0",
34
34
  "typescript": "4.9.5",
35
- "@vercel/build-utils": "13.3.3"
35
+ "@vercel/build-utils": "13.3.5"
36
36
  },
37
37
  "scripts": {
38
38
  "build": "node ../../utils/build.mjs",