@vercel/fs-detectors 6.10.2 → 6.11.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.
@@ -48,5 +48,6 @@ export declare function detectBuilders(files: string[], pkg?: PackageJson | unde
48
48
  rewriteRoutes: Route[] | null;
49
49
  errorRoutes: Route[] | null;
50
50
  services?: Service[];
51
+ experimentalServicesV2?: Services;
51
52
  useImplicitEnvInjection?: boolean;
52
53
  }>;
@@ -582,7 +582,7 @@ function checkUnusedFunctions(frontendBuilder, usedFunctions, options) {
582
582
  }
583
583
  }
584
584
  }
585
- 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))) {
585
+ 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)("python", frontendBuilder.use) || (0, import_is_official_runtime.isOfficialRuntime)("backends", frontendBuilder.use))) {
586
586
  return null;
587
587
  }
588
588
  if (unusedFunctions.size) {
package/dist/index.d.ts CHANGED
@@ -6,7 +6,7 @@ export { autoDetectServices } from './services/auto-detect';
6
6
  export type { AutoDetectOptions, AutoDetectResult, } from './services/auto-detect';
7
7
  export { isStaticBuild, isRouteOwningBuilder, INTERNAL_SERVICE_PREFIX, getInternalServiceFunctionPath, getInternalServiceCronPath, getInternalServiceCronPathPrefix, getInternalServiceWorkerPath, getInternalServiceWorkerPathPrefix, } from './services/utils';
8
8
  export { getServicesBuilders } from './services/get-services-builders';
9
- export type { DetectServicesOptions, DetectServicesResult, DetectServicesSource, InferredServicesConfig, ResolvedServicesResult, InferredServicesResult, ResolvedService, Service, ExperimentalService, ExperimentalServiceV2, ExperimentalServiceV2Config, ExperimentalServicesV2, ExperimentalServiceV2Binding, ServiceBinding, ServiceConfig, Services, ServicesRoutes, ServiceDetectionError, } from './services/types';
9
+ export type { DetectServicesOptions, DetectServicesResult, DetectServicesSource, InferredServiceConfig, InferredServicesConfig, ResolvedServicesResult, InferredServicesResult, ResolvedService, Service, ExperimentalService, ExperimentalServiceV2, ExperimentalServiceV2Config, ExperimentalServicesV2, ExperimentalServiceV2Binding, ServiceBinding, ServiceConfig, Services, ServicesRoutes, ServiceDetectionError, } from './services/types';
10
10
  export { detectFileSystemAPI } from './detect-file-system-api';
11
11
  export { detectFramework, detectFrameworks, detectFrameworkRecord, detectFrameworkVersion, } from './detect-framework';
12
12
  export { getProjectPaths } from './get-project-paths';
@@ -1,6 +1,6 @@
1
1
  import type { DetectEntrypointFn } from '@vercel/build-utils';
2
2
  import type { DetectorFilesystem } from '../detectors/filesystem';
3
- import type { ExperimentalServices, ServiceDetectionError, ServiceDetectionWarning } from './types';
3
+ import type { InferredServicesConfig, ServiceDetectionError, ServiceDetectionWarning } from './types';
4
4
  export interface AutoDetectOptions {
5
5
  fs: DetectorFilesystem;
6
6
  /**
@@ -10,7 +10,7 @@ export interface AutoDetectOptions {
10
10
  detectEntrypoint?: DetectEntrypointFn;
11
11
  }
12
12
  export interface AutoDetectResult {
13
- services: ExperimentalServices | null;
13
+ services: InferredServicesConfig | null;
14
14
  errors: ServiceDetectionError[];
15
15
  warnings: ServiceDetectionWarning[];
16
16
  }
@@ -97,8 +97,9 @@ async function autoDetectServices(options) {
97
97
  async function detectServicesAtRoot(fs, rootFramework, detectEntrypoint) {
98
98
  const services = {};
99
99
  services.frontend = {
100
+ root: ".",
100
101
  framework: rootFramework.slug ?? void 0,
101
- routePrefix: "/"
102
+ mountPath: "/"
102
103
  };
103
104
  const backendResult = await detectBackendServices(fs, detectEntrypoint);
104
105
  if (backendResult.error) {
@@ -116,9 +117,10 @@ async function detectServicesAtRoot(fs, rootFramework, detectEntrypoint) {
116
117
  };
117
118
  }
118
119
  Object.assign(services, backendResult.services);
120
+ const mountWarnings = (0, import_utils.assignMountPaths)(services);
119
121
  return {
120
122
  services,
121
- warnings: [],
123
+ warnings: mountWarnings,
122
124
  errors: []
123
125
  };
124
126
  }
@@ -128,7 +130,7 @@ async function detectServicesFrontendSubdir(fs, frontendFramework, frontendLocat
128
130
  services[serviceName] = {
129
131
  framework: frontendFramework.slug ?? void 0,
130
132
  root: frontendLocation,
131
- routePrefix: "/"
133
+ mountPath: "/"
132
134
  };
133
135
  const backendResult = await detectBackendServices(fs, detectEntrypoint);
134
136
  if (backendResult.error) {
@@ -151,9 +153,10 @@ async function detectServicesFrontendSubdir(fs, frontendFramework, frontendLocat
151
153
  };
152
154
  }
153
155
  Object.assign(services, backendResult.services);
156
+ const mountWarnings = (0, import_utils.assignMountPaths)(services);
154
157
  return {
155
158
  services,
156
- warnings: [],
159
+ warnings: mountWarnings,
157
160
  errors: []
158
161
  };
159
162
  }
@@ -248,14 +251,14 @@ async function detectServiceInDir(fs, dirPath, serviceName, detectEntrypoint) {
248
251
  }
249
252
  const framework = frameworks[0];
250
253
  const slug = framework.slug ?? void 0;
251
- const routePrefix = `/_/${serviceName}`;
254
+ const mountPath = `/${serviceName}`;
252
255
  const detected = detectEntrypoint && !(0, import_utils.isFrontendFramework)(slug) ? await detectEntrypoint({ workPath: dirPath, framework: slug }) : null;
253
256
  return {
254
257
  service: {
255
258
  framework: slug,
256
259
  root: dirPath,
257
260
  ...detected ? { entrypoint: detected.entrypoint } : {},
258
- routePrefix
261
+ mountPath
259
262
  }
260
263
  };
261
264
  }
@@ -1,7 +1,7 @@
1
1
  import type { DetectorFilesystem } from '../detectors/filesystem';
2
- import type { ExperimentalServices, ServiceDetectionError, ServiceDetectionWarning } from './types';
2
+ import type { InferredServicesConfig, ServiceDetectionError, ServiceDetectionWarning } from './types';
3
3
  export interface ProcfileDetectResult {
4
- services: ExperimentalServices | null;
4
+ services: InferredServicesConfig | null;
5
5
  errors: ServiceDetectionError[];
6
6
  warnings: ServiceDetectionWarning[];
7
7
  }
@@ -115,6 +115,7 @@ async function detectProcfileServices(options) {
115
115
  if (isWorkerLikeProcess) {
116
116
  if (hasSupportedWorkerCommand(tokens) && entrypoint?.endsWith(".py")) {
117
117
  services[processType] = {
118
+ root: ".",
118
119
  type: "worker",
119
120
  entrypoint,
120
121
  runtime: "python"
@@ -131,12 +132,12 @@ async function detectProcfileServices(options) {
131
132
  });
132
133
  continue;
133
134
  }
134
- const serviceConfig = { type: "web" };
135
- if (detectedFramework) {
136
- serviceConfig.framework = detectedFramework.slug ?? void 0;
137
- }
138
- serviceConfig.entrypoint = entrypoint ?? ".";
139
- services[processType] = serviceConfig;
135
+ services[processType] = {
136
+ root: ".",
137
+ type: "web",
138
+ ...detectedFramework?.slug ? { framework: detectedFramework.slug } : {},
139
+ entrypoint: entrypoint ?? "."
140
+ };
140
141
  }
141
142
  if (errors.length > 0) {
142
143
  return { services: null, errors, warnings };
@@ -152,7 +153,7 @@ async function detectProcfileServices(options) {
152
153
  firstService.buildCommand = releaseCommand;
153
154
  }
154
155
  }
155
- warnings.push(...(0, import_utils.assignRoutePrefixes)(services));
156
+ warnings.push(...(0, import_utils.assignMountPaths)(services));
156
157
  return { services, errors: [], warnings };
157
158
  }
158
159
  function parseProcfile(content) {
@@ -1,8 +1,8 @@
1
1
  import type { DetectEntrypointFn } from '@vercel/build-utils';
2
2
  import type { DetectorFilesystem } from '../detectors/filesystem';
3
- import type { ExperimentalServices, ServiceDetectionError, ServiceDetectionWarning } from './types';
3
+ import type { InferredServicesConfig, ServiceDetectionError, ServiceDetectionWarning } from './types';
4
4
  export interface RailwayDetectResult {
5
- services: ExperimentalServices | null;
5
+ services: InferredServicesConfig | null;
6
6
  errors: ServiceDetectionError[];
7
7
  warnings: ServiceDetectionWarning[];
8
8
  }
@@ -120,18 +120,17 @@ async function detectRailwayServices(options) {
120
120
  }
121
121
  const framework = frameworks[0];
122
122
  const slug = framework.slug ?? void 0;
123
- let serviceConfig = {};
124
- serviceConfig.framework = slug;
125
- if (cf.dirPath !== ".") {
126
- serviceConfig.root = cf.dirPath;
127
- if (detectEntrypoint && !(0, import_utils.isFrontendFramework)(slug)) {
128
- const detected = await detectEntrypoint({
129
- workPath: cf.dirPath,
130
- framework: slug
131
- });
132
- if (detected) {
133
- serviceConfig.entrypoint = detected.entrypoint;
134
- }
123
+ const serviceConfig = {
124
+ root: cf.dirPath,
125
+ framework: slug
126
+ };
127
+ if (cf.dirPath !== "." && detectEntrypoint && !(0, import_utils.isFrontendFramework)(slug)) {
128
+ const detected = await detectEntrypoint({
129
+ workPath: cf.dirPath,
130
+ framework: slug
131
+ });
132
+ if (detected) {
133
+ serviceConfig.entrypoint = detected.entrypoint;
135
134
  }
136
135
  }
137
136
  if (cf.config.build?.buildCommand) {
@@ -150,7 +149,7 @@ async function detectRailwayServices(options) {
150
149
  if (serviceNames.length === 0) {
151
150
  return { services: null, errors: [], warnings };
152
151
  }
153
- warnings.push(...(0, import_utils.assignRoutePrefixes)(services));
152
+ warnings.push(...(0, import_utils.assignMountPaths)(services));
154
153
  return { services, errors: [], warnings };
155
154
  }
156
155
  async function findRailwayConfigs(fs, dirPath = ".", depth = 0) {
@@ -1,7 +1,7 @@
1
1
  import type { DetectorFilesystem } from '../detectors/filesystem';
2
- import type { DetectEntrypointFn, ExperimentalServices, ServiceDetectionError, ServiceDetectionWarning } from './types';
2
+ import type { DetectEntrypointFn, InferredServicesConfig, ServiceDetectionError, ServiceDetectionWarning } from './types';
3
3
  export interface RenderDetectResult {
4
- services: ExperimentalServices | null;
4
+ services: InferredServicesConfig | null;
5
5
  errors: ServiceDetectionError[];
6
6
  warnings: ServiceDetectionWarning[];
7
7
  }
@@ -107,7 +107,7 @@ async function detectRenderServices(options) {
107
107
  const name = rs.name ?? "unnamed";
108
108
  const hint = {
109
109
  entrypoint: rs.rootDir ?? "<path-to-entrypoint>",
110
- routePrefix: `/_/${name}`
110
+ mountPath: `/api/${name}`
111
111
  };
112
112
  warnings.push({
113
113
  code: "RENDER_PSERV_HINT",
@@ -161,19 +161,18 @@ async function detectRenderServices(options) {
161
161
  }
162
162
  const framework = frameworks[0];
163
163
  const vercelType = SERVICE_TYPE_MAP[serviceType];
164
- const serviceConfig = {};
165
- serviceConfig.type = vercelType;
166
- serviceConfig.framework = framework.slug ?? void 0;
167
- if (rootDir !== ".") {
168
- serviceConfig.root = rootDir;
169
- if (detectEntrypoint && !(0, import_utils.isFrontendFramework)(serviceConfig.framework)) {
170
- const detected = await detectEntrypoint({
171
- workPath: rootDir,
172
- framework: serviceConfig.framework
173
- });
174
- if (detected) {
175
- serviceConfig.entrypoint = detected.entrypoint;
176
- }
164
+ const serviceConfig = {
165
+ root: rootDir,
166
+ type: vercelType,
167
+ framework: framework.slug ?? void 0
168
+ };
169
+ if (rootDir !== "." && detectEntrypoint && !(0, import_utils.isFrontendFramework)(serviceConfig.framework)) {
170
+ const detected = await detectEntrypoint({
171
+ workPath: rootDir,
172
+ framework: serviceConfig.framework
173
+ });
174
+ if (detected) {
175
+ serviceConfig.entrypoint = detected.entrypoint;
177
176
  }
178
177
  }
179
178
  const buildCommand = (0, import_utils.combineBuildCommand)(
@@ -191,7 +190,7 @@ async function detectRenderServices(options) {
191
190
  if (Object.keys(services).length === 0) {
192
191
  return { services: null, errors: [], warnings };
193
192
  }
194
- warnings.push(...(0, import_utils.assignRoutePrefixes)(services));
193
+ warnings.push(...(0, import_utils.assignMountPaths)(services));
195
194
  return { services, errors: [], warnings };
196
195
  }
197
196
  async function readRenderYaml(fs) {
@@ -1,4 +1,5 @@
1
- import type { DetectServicesOptions, DetectServicesResult, Service, ServicesRoutes } from './types';
1
+ import type { Rewrite } from '@vercel/routing-utils';
2
+ import type { DetectServicesOptions, DetectServicesResult, InferredServicesConfig, Service, ServicesRoutes } from './types';
2
3
  /**
3
4
  * Detect and resolve services within a project.
4
5
  *
@@ -6,6 +7,17 @@ import type { DetectServicesOptions, DetectServicesResult, Service, ServicesRout
6
7
  * Returns an error if no services are configured.
7
8
  */
8
9
  export declare function detectServices(options: DetectServicesOptions): Promise<DetectServicesResult>;
10
+ /**
11
+ * Generate top-level service-targeted rewrites from inferred mount paths.
12
+ *
13
+ * Produces `Rewrite` objects (same format as vercel.json `rewrites`) that
14
+ * delegate public traffic into services based on their `mountPath`.
15
+ *
16
+ * Rewrites are ordered by mount path length (longest first) so more
17
+ * specific paths match before broader ones. The root service (`/`) is
18
+ * always last as a catch-all.
19
+ */
20
+ export declare function generateServiceRewrites(services: InferredServicesConfig): Rewrite[];
9
21
  /**
10
22
  * Generate routing rules for services.
11
23
  *
@@ -19,6 +19,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
19
19
  var detect_services_exports = {};
20
20
  __export(detect_services_exports, {
21
21
  detectServices: () => detectServices,
22
+ generateServiceRewrites: () => generateServiceRewrites,
22
23
  generateServicesRoutes: () => generateServicesRoutes
23
24
  });
24
25
  module.exports = __toCommonJS(detect_services_exports);
@@ -51,6 +52,7 @@ function withResolvedResult(resolved, inferred = null) {
51
52
  source: resolved.source,
52
53
  useImplicitEnvInjection: resolved.useImplicitEnvInjection,
53
54
  routes: resolved.routes,
55
+ rewrites: resolved.rewrites,
54
56
  errors: resolved.errors,
55
57
  warnings: resolved.warnings,
56
58
  resolved,
@@ -60,18 +62,17 @@ function withResolvedResult(resolved, inferred = null) {
60
62
  function toInferredLayoutConfig(services) {
61
63
  const inferredConfig = {};
62
64
  for (const [name, service] of Object.entries(services)) {
63
- const serviceConfig = {};
65
+ const serviceConfig = {
66
+ root: service.root
67
+ };
64
68
  if (service.type) {
65
69
  serviceConfig.type = service.type;
66
70
  }
67
- if (typeof service.root === "string") {
68
- serviceConfig.root = service.root;
69
- }
70
71
  if (typeof service.entrypoint === "string") {
71
72
  serviceConfig.entrypoint = service.entrypoint;
72
73
  }
73
- if (typeof service.routePrefix === "string") {
74
- serviceConfig.routePrefix = service.routePrefix;
74
+ if (typeof service.mountPath === "string") {
75
+ serviceConfig.mountPath = service.mountPath;
75
76
  }
76
77
  if ((0, import_utils.isFrontendFramework)(service.framework)) {
77
78
  serviceConfig.framework = service.framework;
@@ -102,6 +103,7 @@ async function detectServices(options) {
102
103
  source: "configured",
103
104
  useImplicitEnvInjection: true,
104
105
  routes: emptyRoutes(),
106
+ rewrites: [],
105
107
  errors: [configError],
106
108
  warnings: []
107
109
  });
@@ -112,6 +114,7 @@ async function detectServices(options) {
112
114
  source: "configured",
113
115
  useImplicitEnvInjection: false,
114
116
  routes: emptyRoutes(),
117
+ rewrites: [],
115
118
  errors: [
116
119
  {
117
120
  code: "SERVICES_AND_EXPERIMENTAL_SERVICES_V2",
@@ -124,63 +127,66 @@ async function detectServices(options) {
124
127
  const hasProvidedConfiguredServices = providedConfiguredServices && Object.keys(providedConfiguredServices).length > 0;
125
128
  const experimentalServicesV2 = hasProvidedConfiguredServices && (providedConfiguredServicesType === "services" || providedConfiguredServicesType === "experimentalServicesV2") ? providedConfiguredServices : hasProvidedConfiguredServices ? void 0 : vercelConfig?.services ?? vercelConfig?.experimentalServicesV2;
126
129
  if (experimentalServicesV2 && Object.keys(experimentalServicesV2).length > 0) {
127
- const result2 = await (0, import_resolve_v2.resolveAllConfiguredServicesV2)(
130
+ const result = await (0, import_resolve_v2.resolveAllConfiguredServicesV2)(
128
131
  experimentalServicesV2,
129
132
  scopedFs
130
133
  );
131
134
  return withResolvedResult({
132
- services: result2.services,
135
+ services: result.services,
133
136
  source: "configured",
134
137
  // V2 uses explicit `bindings`, so no implicit `{NAME}_URL` injection.
135
138
  useImplicitEnvInjection: false,
136
139
  // V2 routes are explicitly carried per-service to output them separately.
137
140
  routes: emptyRoutes(),
138
- errors: result2.errors,
141
+ rewrites: [],
142
+ errors: result.errors,
139
143
  warnings: []
140
144
  });
141
145
  }
142
146
  const experimentalServicesV1 = hasProvidedConfiguredServices ? providedConfiguredServices : vercelConfig?.experimentalServices;
143
147
  const hasExperimentalServicesV1 = experimentalServicesV1 && Object.keys(experimentalServicesV1).length > 0;
144
- if (!hasExperimentalServicesV1) {
145
- const detectors = [
146
- { detect: import_detect_railway.detectRailwayServices, source: "railway" },
147
- { detect: import_detect_render.detectRenderServices, source: "render" },
148
- { detect: import_detect_procfile.detectProcfileServices, source: "procfile" },
149
- { detect: import_auto_detect.autoDetectServices, source: "layout" }
150
- ];
151
- for (const { detect, source } of detectors) {
152
- const detectResult = await detect({ fs: scopedFs, detectEntrypoint });
153
- const match = await tryResolveInferred(detectResult, source, scopedFs);
154
- if (match)
155
- return match;
156
- }
148
+ if (hasExperimentalServicesV1) {
149
+ const result = await (0, import_resolve.resolveAllConfiguredServices)(
150
+ experimentalServicesV1,
151
+ scopedFs,
152
+ "configured"
153
+ );
154
+ const routes = generateServicesRoutes(result.services);
157
155
  return withResolvedResult({
158
- services: [],
159
- source: "auto-detected",
156
+ services: result.services,
157
+ source: "configured",
158
+ // experimentalServices uses the legacy `{NAME}_URL` injection.
160
159
  useImplicitEnvInjection: true,
161
- routes: emptyRoutes(),
162
- errors: [
163
- {
164
- code: "NO_EXPERIMENTAL_SERVICES_CONFIGURED",
165
- message: "No services configured. Add `experimentalServices` to vercel.json."
166
- }
167
- ],
160
+ routes,
161
+ rewrites: [],
162
+ errors: result.errors,
168
163
  warnings: []
169
164
  });
170
165
  }
171
- const result = await (0, import_resolve.resolveAllConfiguredServices)(
172
- experimentalServicesV1,
173
- scopedFs,
174
- "configured"
175
- );
176
- const routes = generateServicesRoutes(result.services);
166
+ const detectors = [
167
+ { detect: import_detect_railway.detectRailwayServices, source: "railway" },
168
+ { detect: import_detect_render.detectRenderServices, source: "render" },
169
+ { detect: import_detect_procfile.detectProcfileServices, source: "procfile" },
170
+ { detect: import_auto_detect.autoDetectServices, source: "layout" }
171
+ ];
172
+ for (const { detect, source } of detectors) {
173
+ const detectResult = await detect({ fs: scopedFs, detectEntrypoint });
174
+ const match = await tryResolveInferred(detectResult, source, scopedFs);
175
+ if (match)
176
+ return match;
177
+ }
177
178
  return withResolvedResult({
178
- services: result.services,
179
- source: "configured",
180
- // experimentalServices uses the legacy `{NAME}_URL` injection.
179
+ services: [],
180
+ source: "auto-detected",
181
181
  useImplicitEnvInjection: true,
182
- routes,
183
- errors: result.errors,
182
+ routes: emptyRoutes(),
183
+ rewrites: [],
184
+ errors: [
185
+ {
186
+ code: "NO_EXPERIMENTAL_SERVICES_CONFIGURED",
187
+ message: "No services configured. Add `experimentalServices` to vercel.json."
188
+ }
189
+ ],
184
190
  warnings: []
185
191
  });
186
192
  }
@@ -189,8 +195,9 @@ async function tryResolveInferred(detectResult, source, scopedFs) {
189
195
  return withResolvedResult({
190
196
  services: [],
191
197
  source: "auto-detected",
192
- useImplicitEnvInjection: true,
198
+ useImplicitEnvInjection: source !== "layout",
193
199
  routes: emptyRoutes(),
200
+ rewrites: [],
194
201
  errors: detectResult.errors,
195
202
  warnings: detectResult.warnings
196
203
  });
@@ -198,52 +205,96 @@ async function tryResolveInferred(detectResult, source, scopedFs) {
198
205
  if (!detectResult.services) {
199
206
  return null;
200
207
  }
208
+ if (source === "layout") {
209
+ const v2Services = {};
210
+ for (const [name, svc] of Object.entries(detectResult.services)) {
211
+ v2Services[name] = {
212
+ root: svc.root,
213
+ ...svc.framework ? { framework: svc.framework } : {},
214
+ ...svc.entrypoint ? { entrypoint: svc.entrypoint } : {}
215
+ };
216
+ }
217
+ const result2 = await (0, import_resolve_v2.resolveAllConfiguredServicesV2)(v2Services, scopedFs);
218
+ const rootServices = Object.values(detectResult.services).filter(
219
+ (svc) => svc.mountPath === "/" && typeof svc.framework === "string"
220
+ );
221
+ const shouldInfer2 = result2.errors.length === 0 && rootServices.length === 1 && result2.services.length > 1;
222
+ const inferred2 = shouldInfer2 ? {
223
+ source,
224
+ config: toInferredLayoutConfig(detectResult.services),
225
+ services: result2.services,
226
+ warnings: detectResult.warnings
227
+ } : null;
228
+ return withResolvedResult(
229
+ {
230
+ services: shouldInfer2 ? result2.services : [],
231
+ source: "auto-detected",
232
+ useImplicitEnvInjection: false,
233
+ routes: emptyRoutes(),
234
+ rewrites: shouldInfer2 ? generateServiceRewrites(detectResult.services) : [],
235
+ experimentalServicesV2: shouldInfer2 ? v2Services : void 0,
236
+ errors: result2.errors,
237
+ warnings: detectResult.warnings
238
+ },
239
+ inferred2
240
+ );
241
+ }
242
+ const v1Services = {};
243
+ for (const [name, svc] of Object.entries(detectResult.services)) {
244
+ v1Services[name] = {
245
+ root: svc.root === "." ? void 0 : svc.root,
246
+ ...svc.framework ? { framework: svc.framework } : {},
247
+ ...svc.entrypoint ? { entrypoint: svc.entrypoint } : {},
248
+ ...svc.type ? { type: svc.type } : {},
249
+ ...svc.buildCommand ? { buildCommand: svc.buildCommand } : {},
250
+ ...svc.preDeployCommand ? { preDeployCommand: svc.preDeployCommand } : {},
251
+ ...svc.mountPath ? { routePrefix: svc.mountPath } : {}
252
+ };
253
+ }
201
254
  const result = await (0, import_resolve.resolveAllConfiguredServices)(
202
- detectResult.services,
255
+ v1Services,
203
256
  scopedFs,
204
257
  "generated"
205
258
  );
206
- let shouldInfer;
207
- if (source === "layout") {
208
- const rootWebFrameworkServices = result.services.filter(
209
- (service) => service.type === "web" && service.routePrefix === "/" && typeof service.framework === "string"
210
- );
211
- shouldInfer = result.errors.length === 0 && rootWebFrameworkServices.length === 1 && result.services.length > 1;
212
- } else {
213
- shouldInfer = result.errors.length === 0 && result.services.length > 0;
214
- }
259
+ const shouldInfer = result.errors.length === 0 && result.services.length > 0;
215
260
  const inferred = shouldInfer ? {
216
261
  source,
217
262
  config: toInferredLayoutConfig(detectResult.services),
218
263
  services: result.services,
219
264
  warnings: detectResult.warnings
220
265
  } : null;
221
- if (source === "layout" && shouldInfer) {
222
- const routes = generateServicesRoutes(result.services);
223
- return withResolvedResult(
224
- {
225
- services: result.services,
226
- source: "auto-detected",
227
- useImplicitEnvInjection: true,
228
- routes,
229
- errors: result.errors,
230
- warnings: detectResult.warnings
231
- },
232
- inferred
233
- );
234
- }
235
266
  return withResolvedResult(
236
267
  {
237
268
  services: [],
238
269
  source: "auto-detected",
239
270
  useImplicitEnvInjection: true,
240
271
  routes: emptyRoutes(),
272
+ rewrites: [],
241
273
  errors: result.errors,
242
274
  warnings: detectResult.warnings
243
275
  },
244
276
  inferred
245
277
  );
246
278
  }
279
+ function generateServiceRewrites(services) {
280
+ const entries = Object.entries(services).filter(
281
+ ([, svc]) => typeof svc.mountPath === "string" && (!svc.type || svc.type === "web")
282
+ ).sort(([, a], [, b]) => b.mountPath.length - a.mountPath.length);
283
+ return entries.map(([name, svc]) => {
284
+ const mountPath = svc.mountPath;
285
+ if (mountPath === "/") {
286
+ return {
287
+ source: "/(.*)",
288
+ destination: { type: "service", service: name }
289
+ };
290
+ }
291
+ const prefix = mountPath.startsWith("/") ? mountPath.slice(1) : mountPath;
292
+ return {
293
+ source: `/${prefix}(/.*)?`,
294
+ destination: { type: "service", service: name }
295
+ };
296
+ });
297
+ }
247
298
  function generateServicesRoutes(allServices) {
248
299
  const services = allServices.filter(import_build_utils.isExperimentalService);
249
300
  const hostRewrites = [];
@@ -367,5 +418,6 @@ function getHostCondition(service) {
367
418
  // Annotate the CommonJS export names for ESM import in node:
368
419
  0 && (module.exports = {
369
420
  detectServices,
421
+ generateServiceRewrites,
370
422
  generateServicesRoutes
371
423
  });
@@ -1,5 +1,5 @@
1
- import type { Route } from '@vercel/routing-utils';
2
- import type { Builder } from '@vercel/build-utils';
1
+ import type { Rewrite, Route } from '@vercel/routing-utils';
2
+ import type { Builder, Services } from '@vercel/build-utils';
3
3
  import type { ConfiguredServices, ConfiguredServicesType, Service } from './types';
4
4
  export interface ErrorResponse {
5
5
  code: string;
@@ -23,6 +23,10 @@ export interface ServicesBuildersResult {
23
23
  redirectRoutes: Route[] | null;
24
24
  rewriteRoutes: Route[] | null;
25
25
  errorRoutes: Route[] | null;
26
+ /** Top-level service-targeted rewrites generated by auto-detection. */
27
+ serviceRewrites?: Rewrite[];
28
+ /** V2 services config so the platform activates V2 routing. */
29
+ experimentalServicesV2?: Services;
26
30
  services?: Service[];
27
31
  useImplicitEnvInjection?: boolean;
28
32
  }
@@ -131,6 +131,8 @@ async function getServicesBuilders(options) {
131
131
  ...result.routes.crons
132
132
  ] : null,
133
133
  errorRoutes: [],
134
+ serviceRewrites: result.rewrites.length > 0 ? result.rewrites : void 0,
135
+ experimentalServicesV2: result.experimentalServicesV2,
134
136
  services: result.services,
135
137
  useImplicitEnvInjection: result.useImplicitEnvInjection
136
138
  };
@@ -31,9 +31,27 @@ var import_resolve = require("./resolve");
31
31
  var import_utils = require("./utils");
32
32
  const frameworksBySlug = new Map(import_frameworks.frameworkList.map((f) => [f.slug, f]));
33
33
  const SERVICE_NAME_REGEX = /^[a-zA-Z]([a-zA-Z0-9_-]*[a-zA-Z0-9])?$/;
34
+ const CONTAINER_ENTRYPOINT_CANDIDATES = [
35
+ "Dockerfile.vercel",
36
+ "Containerfile.vercel",
37
+ "Dockerfile",
38
+ "Containerfile"
39
+ ];
40
+ const CONTAINER_ENTRYPOINT_BASENAMES = new Set(
41
+ CONTAINER_ENTRYPOINT_CANDIDATES.map((name) => name.toLowerCase())
42
+ );
34
43
  function isDockerfileEntrypoint(entrypoint) {
35
- const base = import_path.posix.basename(entrypoint).toLowerCase();
36
- return base === "dockerfile" || base === "containerfile" || base.endsWith(".dockerfile");
44
+ return CONTAINER_ENTRYPOINT_BASENAMES.has(
45
+ import_path.posix.basename(entrypoint).toLowerCase()
46
+ );
47
+ }
48
+ async function detectContainerEntrypoint(serviceFs) {
49
+ for (const candidate of CONTAINER_ENTRYPOINT_CANDIDATES) {
50
+ if (await serviceFs.hasPath(candidate)) {
51
+ return candidate;
52
+ }
53
+ }
54
+ return void 0;
37
55
  }
38
56
  function normalizeContainerCommand(command) {
39
57
  if (command === void 0) {
@@ -41,29 +59,40 @@ function normalizeContainerCommand(command) {
41
59
  }
42
60
  return Array.isArray(command) ? command : [command];
43
61
  }
44
- function resolveContainerServiceV2(name, config, normalizedRoot) {
62
+ async function resolveContainerServiceV2(name, config, normalizedRoot, serviceFs) {
45
63
  const isRoot = normalizedRoot === ".";
46
64
  const entrypoint = config.entrypoint;
47
- const dockerfile = typeof entrypoint === "string" && isDockerfileEntrypoint(entrypoint) ? import_path.posix.normalize(entrypoint) : void 0;
48
- const image = typeof entrypoint === "string" && !dockerfile ? entrypoint : void 0;
49
- if (!dockerfile && !image) {
50
- return {
51
- error: {
52
- code: "MISSING_SERVICE_CONFIG",
53
- message: `Container service "${name}" must specify an "entrypoint": a Dockerfile path to build, or a prebuilt OCI image reference.`,
54
- serviceName: name
55
- }
56
- };
65
+ let dockerfile;
66
+ if (typeof entrypoint === "string") {
67
+ if (!isDockerfileEntrypoint(entrypoint)) {
68
+ return {
69
+ error: {
70
+ code: "INVALID_SERVICE_CONFIG",
71
+ message: `Container service "${name}" has invalid "entrypoint" "${entrypoint}". It must name a Dockerfile or Containerfile.`,
72
+ serviceName: name
73
+ }
74
+ };
75
+ }
76
+ dockerfile = import_path.posix.normalize(entrypoint);
77
+ } else {
78
+ dockerfile = await detectContainerEntrypoint(serviceFs);
79
+ if (!dockerfile) {
80
+ return {
81
+ error: {
82
+ code: "MISSING_SERVICE_CONFIG",
83
+ message: `Container service "${name}" has no "entrypoint" and no ${CONTAINER_ENTRYPOINT_CANDIDATES.join(
84
+ ", "
85
+ )} was found in "${normalizedRoot}".`,
86
+ serviceName: name
87
+ }
88
+ };
89
+ }
57
90
  }
58
- const localSrc = dockerfile ?? image ?? "Dockerfile";
59
- const builderSrc = isRoot ? localSrc : import_path.posix.join(normalizedRoot, localSrc);
91
+ const builderSrc = isRoot ? dockerfile : import_path.posix.join(normalizedRoot, dockerfile);
60
92
  const builderConfig = { zeroConfig: true };
61
93
  if (!isRoot) {
62
94
  builderConfig.workspace = normalizedRoot;
63
95
  }
64
- if (image) {
65
- builderConfig.handler = image;
66
- }
67
96
  const command = normalizeContainerCommand(config.command);
68
97
  if (command) {
69
98
  builderConfig.command = command;
@@ -74,7 +103,7 @@ function resolveContainerServiceV2(name, config, normalizedRoot) {
74
103
  name,
75
104
  root: normalizedRoot,
76
105
  runtime: "container",
77
- entrypoint: image ?? dockerfile,
106
+ entrypoint: dockerfile,
78
107
  command,
79
108
  builder: {
80
109
  src: builderSrc,
@@ -165,15 +194,15 @@ function validateServiceConfigV2(name, config) {
165
194
  }
166
195
  async function resolveConfiguredServiceV2(name, config, fs) {
167
196
  const normalizedRoot = (0, import_utils.stripTrailingSlash)(import_path.posix.normalize(config.root));
168
- const isContainer = config.runtime === "container" || typeof config.entrypoint === "string" && isDockerfileEntrypoint(config.entrypoint);
169
- if (isContainer) {
170
- return resolveContainerServiceV2(name, config, normalizedRoot);
171
- }
172
197
  const serviceFsResult = normalizedRoot === "." ? { fs } : await (0, import_resolve.getServiceFs)(fs, name, normalizedRoot);
173
198
  if (serviceFsResult.error) {
174
199
  return { error: serviceFsResult.error };
175
200
  }
176
201
  const serviceFs = serviceFsResult.fs;
202
+ const isContainer = config.runtime === "container" || typeof config.entrypoint === "string" && isDockerfileEntrypoint(config.entrypoint);
203
+ if (isContainer) {
204
+ return resolveContainerServiceV2(name, config, normalizedRoot, serviceFs);
205
+ }
177
206
  const rawEntrypoint = config.entrypoint;
178
207
  const moduleAttr = typeof rawEntrypoint === "string" ? (0, import_resolve.parsePyModuleAttrEntrypoint)(rawEntrypoint) : null;
179
208
  let normalizedEntrypoint;
@@ -1,4 +1,4 @@
1
- import type { Route } from '@vercel/routing-utils';
1
+ import type { Rewrite, Route } from '@vercel/routing-utils';
2
2
  import type { DetectEntrypointFn, EnvVar, EnvVars, ExperimentalServiceConfig, ExperimentalServiceV2Config, ExperimentalServiceGroups, ExperimentalServices, ExperimentalServicesV2, ExperimentalServiceV2Binding, ServiceBinding, ServiceConfig, Services, ExperimentalService, ExperimentalServiceV2, ServiceRuntime, ServiceType, ServiceRefEnvVar, Service, Builder } from '@vercel/build-utils';
3
3
  import type { DetectorFilesystem } from '../detectors/filesystem';
4
4
  export type { DetectEntrypointFn, EnvVar, EnvVars, ExperimentalServiceConfig, ExperimentalServiceGroups, ExperimentalServices, ExperimentalServiceV2Config, ExperimentalServicesV2, ExperimentalServiceV2Binding, ServiceBinding, ServiceConfig, Services, ExperimentalService, ExperimentalServiceV2, ServiceRuntime, ServiceType, ServiceRefEnvVar, Service, Builder, };
@@ -44,19 +44,51 @@ export interface ServicesRoutes {
44
44
  }
45
45
  export type ConfiguredServicesType = 'experimentalServices' | 'services' | 'experimentalServicesV2';
46
46
  export type ConfiguredServices = ExperimentalServices | Services;
47
- export type InferredServicesConfig = ExperimentalServices;
47
+ /**
48
+ * A single service entry inferred from project structure.
49
+ *
50
+ * This is an intermediate format produced by auto-detection — it carries
51
+ * the detection results (including `mountPath`, the inferred route mount
52
+ * point) before they are converted into a concrete config format (V1 or V2).
53
+ */
54
+ export interface InferredServiceConfig {
55
+ /** Service root directory relative to the project root. */
56
+ root: string;
57
+ /** Framework slug, if detected. */
58
+ framework?: string;
59
+ /** Service entrypoint (file path or `module:attr` reference). */
60
+ entrypoint?: string;
61
+ /** Runtime identifier (e.g. "python", "node"). */
62
+ runtime?: string;
63
+ /** Service type (e.g. "web", "cron", "worker"). */
64
+ type?: ServiceType;
65
+ /** Build command override. */
66
+ buildCommand?: string;
67
+ /** Pre-deploy command override. */
68
+ preDeployCommand?: string;
69
+ /**
70
+ * Inferred route mount path for this service.
71
+ * For example, `"/"` for the root frontend, `"/_/backend"` for a backend.
72
+ */
73
+ mountPath?: string;
74
+ }
75
+ export type InferredServicesConfig = Record<string, InferredServiceConfig>;
48
76
  export interface ResolvedServicesResult {
49
77
  services: Service[];
50
78
  source: DetectServicesSource;
51
79
  useImplicitEnvInjection: boolean;
52
80
  routes: ServicesRoutes;
81
+ /** Top-level service-targeted rewrites (V2). */
82
+ rewrites: Rewrite[];
83
+ /** V2 services config for the build output, so the platform activates V2 routing. */
84
+ experimentalServicesV2?: Services;
53
85
  errors: ServiceDetectionError[];
54
86
  warnings: ServiceDetectionWarning[];
55
87
  }
56
88
  export interface InferredServicesResult {
57
89
  source: 'layout' | 'procfile' | 'railway' | 'render';
58
90
  config: InferredServicesConfig;
59
- services: ExperimentalService[];
91
+ services: Service[];
60
92
  warnings: ServiceDetectionWarning[];
61
93
  }
62
94
  export interface DetectServicesResult extends ResolvedServicesResult {
@@ -1,7 +1,7 @@
1
1
  import { INTERNAL_SERVICE_PREFIX, getInternalServiceFunctionPath, getInternalServiceCronPathPrefix, getInternalServiceCronPath } from '@vercel/build-utils';
2
2
  import type { Framework } from '@vercel/frameworks';
3
3
  import type { DetectorFilesystem } from '../detectors/filesystem';
4
- import type { ServiceRuntime, ExperimentalServices, ExperimentalServicesV2, Services, ServiceDetectionError, ServiceDetectionWarning, ResolvedService } from './types';
4
+ import type { ServiceRuntime, ExperimentalServices, ExperimentalServicesV2, InferredServicesConfig, Services, ServiceDetectionError, ServiceDetectionWarning, ResolvedService } from './types';
5
5
  export declare const DETECTION_FRAMEWORKS: Framework[];
6
6
  export { INTERNAL_SERVICE_PREFIX, getInternalServiceFunctionPath, getInternalServiceCronPathPrefix, getInternalServiceCronPath, };
7
7
  /**
@@ -42,6 +42,7 @@ export declare function isRouteOwningBuilder(service: ResolvedService): boolean;
42
42
  */
43
43
  export declare function inferRuntimeFromFramework(framework: string | null | undefined): ServiceRuntime | undefined;
44
44
  export declare function isFrontendFramework(framework: string | null | undefined): boolean;
45
+ export declare function isBFFFramework(framework: string | null | undefined): boolean;
45
46
  export declare function filterFrameworksByRuntime<T extends {
46
47
  slug?: string | null;
47
48
  }>(frameworks: readonly T[], runtime?: ServiceRuntime): T[];
@@ -77,13 +78,18 @@ export interface ReadVercelConfigResult {
77
78
  */
78
79
  export declare function readVercelConfig(fs: DetectorFilesystem): Promise<ReadVercelConfigResult>;
79
80
  /**
80
- * Assign route prefixes to inferred services.
81
+ * Assign mount paths to inferred services.
81
82
  *
82
- * A frontend service gets `/`, the rest get `/_/{name}`.
83
- * A single non-frontend service would also get `/`.
84
- * If no frontend service found, then multiple services get `/_/{name}`.
83
+ * A frontend service gets `/`, backend services get `/api/...`:
84
+ * - If the frontend is a BFF (e.g. Next.js, has its own API routes):
85
+ * backends get `/api/{name}/(.*)` to avoid shadowing the frontend's API routes.
86
+ * - If the frontend is client-only (e.g. Vite):
87
+ * backends get `/api/(.*)`.
88
+ *
89
+ * A single non-frontend service gets `/`.
90
+ * If no frontend service found, multiple services get `/api/{name}`.
85
91
  *
86
92
  * Priority for `/`: single service or frontend > name "frontend" or "web" > alphabetical.
87
93
  */
88
- export declare function assignRoutePrefixes(services: ExperimentalServices): ServiceDetectionWarning[];
94
+ export declare function assignMountPaths(services: InferredServicesConfig): ServiceDetectionWarning[];
89
95
  export declare function combineBuildCommand(buildCommand: string | undefined, preDeployCommand: string | string[] | undefined): string | undefined;
@@ -31,7 +31,7 @@ __export(utils_exports, {
31
31
  DETECTION_FRAMEWORKS: () => DETECTION_FRAMEWORKS,
32
32
  INTERNAL_QUEUES_PREFIX: () => INTERNAL_QUEUES_PREFIX,
33
33
  INTERNAL_SERVICE_PREFIX: () => import_build_utils.INTERNAL_SERVICE_PREFIX,
34
- assignRoutePrefixes: () => assignRoutePrefixes,
34
+ assignMountPaths: () => assignMountPaths,
35
35
  combineBuildCommand: () => combineBuildCommand,
36
36
  filterFrameworksByRuntime: () => filterFrameworksByRuntime,
37
37
  getBuilderForRuntime: () => getBuilderForRuntime,
@@ -43,6 +43,7 @@ __export(utils_exports, {
43
43
  hasFile: () => hasFile,
44
44
  inferRuntimeFromFramework: () => inferRuntimeFromFramework,
45
45
  inferServiceRuntime: () => inferServiceRuntime,
46
+ isBFFFramework: () => isBFFFramework,
46
47
  isFrontendFramework: () => isFrontendFramework,
47
48
  isRouteOwningBuilder: () => isRouteOwningBuilder,
48
49
  isStaticBuild: () => isStaticBuild,
@@ -114,6 +115,16 @@ function isFrontendFramework(framework) {
114
115
  }
115
116
  return !inferRuntimeFromFramework(framework);
116
117
  }
118
+ const BFF_FRAMEWORKS = /* @__PURE__ */ new Set([
119
+ "nextjs",
120
+ "nuxtjs",
121
+ "sveltekit",
122
+ "remix",
123
+ "solidstart"
124
+ ]);
125
+ function isBFFFramework(framework) {
126
+ return !!framework && BFF_FRAMEWORKS.has(framework);
127
+ }
117
128
  function filterFrameworksByRuntime(frameworks, runtime) {
118
129
  if (!runtime) {
119
130
  return [...frameworks];
@@ -182,11 +193,11 @@ async function readVercelConfig(fs) {
182
193
  }
183
194
  return { config: null, error: null };
184
195
  }
185
- function assignRoutePrefixes(services) {
196
+ function assignMountPaths(services) {
186
197
  const warnings = [];
187
198
  const names = Object.keys(services);
188
199
  if (names.length === 1) {
189
- services[names[0]].routePrefix = "/";
200
+ services[names[0]].mountPath = "/";
190
201
  return warnings;
191
202
  }
192
203
  const frontendNames = names.filter(
@@ -199,11 +210,19 @@ function assignRoutePrefixes(services) {
199
210
  rootName = frontendNames.find((n) => n === "frontend" || n === "web") ?? frontendNames.sort()[0];
200
211
  warnings.push({
201
212
  code: "MULTIPLE_FRONTENDS",
202
- message: `Multiple frontend services detected (${frontendNames.join(", ")}). "${rootName}" was assigned routePrefix "/". Adjust manually if a different service should be the root.`
213
+ message: `Multiple frontend services detected (${frontendNames.join(", ")}). "${rootName}" was assigned mount path "/". Adjust manually if a different service should be the root.`
203
214
  });
204
215
  }
216
+ const rootFramework = rootName ? services[rootName].framework : void 0;
217
+ const isBFF = rootFramework ? isBFFFramework(rootFramework) : false;
218
+ const nonRootNames = names.filter((n) => n !== rootName);
219
+ const needsNamespace = isBFF || nonRootNames.length > 1;
205
220
  for (const name of names) {
206
- services[name].routePrefix = name === rootName ? "/" : `/_/${name}`;
221
+ if (name === rootName) {
222
+ services[name].mountPath = "/";
223
+ } else {
224
+ services[name].mountPath = needsNamespace ? `/api/${name}` : "/api";
225
+ }
207
226
  }
208
227
  return warnings;
209
228
  }
@@ -222,7 +241,7 @@ function combineBuildCommand(buildCommand, preDeployCommand) {
222
241
  DETECTION_FRAMEWORKS,
223
242
  INTERNAL_QUEUES_PREFIX,
224
243
  INTERNAL_SERVICE_PREFIX,
225
- assignRoutePrefixes,
244
+ assignMountPaths,
226
245
  combineBuildCommand,
227
246
  filterFrameworksByRuntime,
228
247
  getBuilderForRuntime,
@@ -234,6 +253,7 @@ function combineBuildCommand(buildCommand, preDeployCommand) {
234
253
  hasFile,
235
254
  inferRuntimeFromFramework,
236
255
  inferServiceRuntime,
256
+ isBFFFramework,
237
257
  isFrontendFramework,
238
258
  isRouteOwningBuilder,
239
259
  isStaticBuild,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vercel/fs-detectors",
3
- "version": "6.10.2",
3
+ "version": "6.11.0",
4
4
  "description": "Vercel filesystem detectors",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -21,9 +21,9 @@
21
21
  "semver": "6.3.1",
22
22
  "smol-toml": "1.5.2",
23
23
  "@vercel/build-utils": "13.32.1",
24
- "@vercel/frameworks": "3.30.0",
24
+ "@vercel/error-utils": "2.2.0",
25
25
  "@vercel/routing-utils": "6.4.0",
26
- "@vercel/error-utils": "2.2.0"
26
+ "@vercel/frameworks": "3.30.1"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/glob": "7.2.0",