@vercel/fs-detectors 6.2.1 → 6.3.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.
@@ -1,5 +1,5 @@
1
1
  import type { Route } from '@vercel/routing-utils';
2
- import type { PackageJson, Builder, BuilderFunctions, ExperimentalServices, ProjectSettings, Service } from '@vercel/build-utils';
2
+ import type { PackageJson, Builder, BuilderFunctions, Services, ExperimentalServices, ProjectSettings, Service } from '@vercel/build-utils';
3
3
  /**
4
4
  * Pattern for finding all supported middleware files.
5
5
  */
@@ -22,6 +22,7 @@ export interface ErrorResponse {
22
22
  export interface Options {
23
23
  tag?: string;
24
24
  functions?: BuilderFunctions;
25
+ services?: Services;
25
26
  experimentalServices?: ExperimentalServices;
26
27
  ignoreBuildScript?: boolean;
27
28
  projectSettings?: ProjectSettings;
@@ -46,4 +47,5 @@ export declare function detectBuilders(files: string[], pkg?: PackageJson | unde
46
47
  rewriteRoutes: Route[] | null;
47
48
  errorRoutes: Route[] | null;
48
49
  services?: Service[];
50
+ useImplicitEnvInjection?: boolean;
49
51
  }>;
@@ -92,13 +92,14 @@ function detectOutputDirectory(builders) {
92
92
  return publicBuilder ? publicBuilder.src.replace("/**/*", "") : null;
93
93
  }
94
94
  async function detectBuilders(files, pkg, options = {}) {
95
- const { experimentalServices: services, projectSettings = {} } = options;
95
+ const { services, experimentalServices, projectSettings = {} } = options;
96
96
  const { framework } = projectSettings;
97
- const hasServicesConfig = services != null && typeof services === "object";
97
+ const configuredServices = services ?? experimentalServices;
98
+ const hasServicesConfig = configuredServices != null && typeof configuredServices === "object";
98
99
  if (hasServicesConfig || framework === "services") {
99
100
  return (0, import_get_services_builders.getServicesBuilders)({
100
101
  workPath: options.workPath,
101
- configuredServices: services,
102
+ configuredServices,
102
103
  projectFramework: framework
103
104
  });
104
105
  }
package/dist/index.d.ts CHANGED
@@ -4,7 +4,7 @@ export { autoDetectServices } from './services/auto-detect';
4
4
  export type { AutoDetectOptions, AutoDetectResult, } from './services/auto-detect';
5
5
  export { isStaticBuild, isRouteOwningBuilder, INTERNAL_SERVICE_PREFIX, getInternalServiceFunctionPath, getInternalServiceCronPath, getInternalServiceCronPathPrefix, getInternalServiceWorkerPath, getInternalServiceWorkerPathPrefix, } from './services/utils';
6
6
  export { getServicesBuilders } from './services/get-services-builders';
7
- export type { DetectServicesOptions, DetectServicesResult, DetectServicesSource, ServicesConfig, ResolvedServicesResult, InferredServicesResult, ResolvedService, Service, ServicesRoutes, ServiceDetectionError, } from './services/types';
7
+ export type { DetectServicesOptions, DetectServicesResult, DetectServicesSource, InferredServicesConfig, ResolvedServicesResult, InferredServicesResult, ResolvedService, Service, ServicesRoutes, ServiceDetectionError, } from './services/types';
8
8
  export { detectFileSystemAPI } from './detect-file-system-api';
9
9
  export { detectFramework, detectFrameworks, detectFrameworkRecord, detectFrameworkVersion, } from './detect-framework';
10
10
  export { getProjectPaths } from './get-project-paths';
@@ -8,7 +8,7 @@ export interface AutoDetectResult {
8
8
  errors: ServiceDetectionError[];
9
9
  }
10
10
  /**
11
- * Auto-detect services when experimentalServices is not configured.
11
+ * Auto-detect services when services are not configured.
12
12
  *
13
13
  * Scans the project for frameworks, supporting multiple layouts:
14
14
  *
@@ -44,7 +44,7 @@ async function autoDetectServices(options) {
44
44
  errors: [
45
45
  {
46
46
  code: "MULTIPLE_FRAMEWORKS_ROOT",
47
- message: `Multiple frameworks detected at root: ${frameworkNames}. Use explicit experimentalServices config.`
47
+ message: `Multiple frameworks detected at root: ${frameworkNames}. Use explicit services config.`
48
48
  }
49
49
  ]
50
50
  };
@@ -69,7 +69,7 @@ async function autoDetectServices(options) {
69
69
  errors: [
70
70
  {
71
71
  code: "MULTIPLE_FRAMEWORKS_SERVICE",
72
- message: `Multiple frameworks detected in ${frontendLocation}/: ${frameworkNames}. Use explicit experimentalServices config.`
72
+ message: `Multiple frameworks detected in ${frontendLocation}/: ${frameworkNames}. Use explicit services config.`
73
73
  }
74
74
  ]
75
75
  };
@@ -87,7 +87,7 @@ async function autoDetectServices(options) {
87
87
  errors: [
88
88
  {
89
89
  code: "NO_SERVICES_CONFIGURED",
90
- message: "No services detected. Configure experimentalServices in vercel.json or ensure a framework exists at project root, frontend/, or apps/web/."
90
+ message: "No services detected. Configure services in vercel.json or ensure a framework exists at project root, frontend/, or apps/web/."
91
91
  }
92
92
  ]
93
93
  };
@@ -168,7 +168,7 @@ async function detectBackendServices(fs) {
168
168
  services: {},
169
169
  error: {
170
170
  code: "SERVICE_NAME_CONFLICT",
171
- message: `Service name conflict: "${serviceName}" exists in both ${BACKEND_DIR}/ and ${SERVICES_DIR}/${serviceName}/. Rename one of the directories or use explicit experimentalServices config.`,
171
+ message: `Service name conflict: "${serviceName}" exists in both ${BACKEND_DIR}/ and ${SERVICES_DIR}/${serviceName}/. Rename one of the directories or use explicit services config.`,
172
172
  serviceName
173
173
  }
174
174
  };
@@ -217,7 +217,7 @@ async function detectServiceInDir(fs, dirPath, serviceName) {
217
217
  return {
218
218
  error: {
219
219
  code: "MULTIPLE_FRAMEWORKS_SERVICE",
220
- message: `Multiple frameworks detected in ${dirPath}/: ${frameworkNames}. Use explicit experimentalServices config.`,
220
+ message: `Multiple frameworks detected in ${dirPath}/: ${frameworkNames}. Use explicit services config.`,
221
221
  serviceName
222
222
  }
223
223
  };
@@ -109,7 +109,7 @@ async function detectRailwayServices(options) {
109
109
  if (frameworks.length === 0) {
110
110
  warnings.push({
111
111
  code: "SERVICE_SKIPPED",
112
- message: `Skipped service in ${dirLabel}/: no framework detected. Configure it manually in experimentalServices.`
112
+ message: `Skipped service in ${dirLabel}/: no framework detected. Configure it manually in services.`
113
113
  });
114
114
  continue;
115
115
  }
@@ -117,7 +117,7 @@ async function detectRailwayServices(options) {
117
117
  const names = frameworks.map((f) => f.name).join(", ");
118
118
  errors.push({
119
119
  code: "MULTIPLE_FRAMEWORKS_SERVICE",
120
- message: `Multiple frameworks detected in ${dirLabel}/: ${names}. Use explicit experimentalServices config.`,
120
+ message: `Multiple frameworks detected in ${dirLabel}/: ${names}. Use explicit services config.`,
121
121
  serviceName
122
122
  });
123
123
  continue;
@@ -2,7 +2,7 @@ import { type DetectServicesOptions, type DetectServicesResult, type Service, ty
2
2
  /**
3
3
  * Detect and resolve services within a project.
4
4
  *
5
- * Reads vercel.json and resolves `experimentalServices` into Service objects.
5
+ * Reads vercel.json and resolves configured services into Service objects.
6
6
  * Returns an error if no services are configured.
7
7
  */
8
8
  export declare function detectServices(options: DetectServicesOptions): Promise<DetectServicesResult>;
@@ -42,10 +42,17 @@ function emptyRoutes() {
42
42
  workers: []
43
43
  };
44
44
  }
45
+ function isEnvVars(env) {
46
+ if (!env)
47
+ return false;
48
+ const first = Object.values(env)[0];
49
+ return typeof first === "object" && first !== null;
50
+ }
45
51
  function withResolvedResult(resolved, inferred = null) {
46
52
  return {
47
53
  services: resolved.services,
48
54
  source: resolved.source,
55
+ useImplicitEnvInjection: resolved.useImplicitEnvInjection,
49
56
  routes: resolved.routes,
50
57
  errors: resolved.errors,
51
58
  warnings: resolved.warnings,
@@ -81,12 +88,14 @@ async function detectServices(options) {
81
88
  return withResolvedResult({
82
89
  services: [],
83
90
  source: "configured",
91
+ useImplicitEnvInjection: true,
84
92
  routes: emptyRoutes(),
85
93
  errors: [configError],
86
94
  warnings: []
87
95
  });
88
96
  }
89
- const configuredServices = vercelConfig?.experimentalServices;
97
+ const hasNonEmptyPublicServicesConfig = vercelConfig?.services && Object.keys(vercelConfig.services).length > 0;
98
+ const configuredServices = hasNonEmptyPublicServicesConfig ? vercelConfig.services : vercelConfig?.experimentalServices;
90
99
  const hasConfiguredServices = configuredServices && Object.keys(configuredServices).length > 0;
91
100
  if (!hasConfiguredServices) {
92
101
  const railwayResult = await (0, import_detect_railway.detectRailwayServices)({ fs: scopedFs });
@@ -94,6 +103,7 @@ async function detectServices(options) {
94
103
  return withResolvedResult({
95
104
  services: [],
96
105
  source: "auto-detected",
106
+ useImplicitEnvInjection: true,
97
107
  routes: emptyRoutes(),
98
108
  errors: railwayResult.errors,
99
109
  warnings: railwayResult.warnings
@@ -115,6 +125,7 @@ async function detectServices(options) {
115
125
  {
116
126
  services: [],
117
127
  source: "auto-detected",
128
+ useImplicitEnvInjection: true,
118
129
  routes: emptyRoutes(),
119
130
  errors: result2.errors,
120
131
  warnings: railwayResult.warnings
@@ -133,6 +144,7 @@ async function detectServices(options) {
133
144
  const resolved = {
134
145
  services: result2.services,
135
146
  source: "auto-detected",
147
+ useImplicitEnvInjection: true,
136
148
  routes: routes2,
137
149
  errors: result2.errors,
138
150
  warnings: []
@@ -151,6 +163,7 @@ async function detectServices(options) {
151
163
  return withResolvedResult({
152
164
  services: [],
153
165
  source: "auto-detected",
166
+ useImplicitEnvInjection: true,
154
167
  routes: emptyRoutes(),
155
168
  errors: autoResult.errors,
156
169
  warnings: []
@@ -159,11 +172,12 @@ async function detectServices(options) {
159
172
  return withResolvedResult({
160
173
  services: [],
161
174
  source: "auto-detected",
175
+ useImplicitEnvInjection: true,
162
176
  routes: emptyRoutes(),
163
177
  errors: [
164
178
  {
165
179
  code: "NO_SERVICES_CONFIGURED",
166
- message: "No services configured. Add `experimentalServices` to vercel.json."
180
+ message: "No services configured. Add `services` to vercel.json."
167
181
  }
168
182
  ],
169
183
  warnings: []
@@ -172,12 +186,21 @@ async function detectServices(options) {
172
186
  const result = await (0, import_resolve.resolveAllConfiguredServices)(
173
187
  configuredServices,
174
188
  scopedFs,
175
- "configured"
189
+ "configured",
190
+ {
191
+ requireFileEntrypointForBackendRuntimes: Boolean(
192
+ hasNonEmptyPublicServicesConfig
193
+ ),
194
+ rootEnv: isEnvVars(vercelConfig?.env) ? vercelConfig?.env : void 0
195
+ }
176
196
  );
177
197
  const routes = generateServicesRoutes(result.services);
178
198
  return withResolvedResult({
179
199
  services: result.services,
180
200
  source: "configured",
201
+ // GA `services` opts into explicit `env`; experimentalServices keeps
202
+ // the legacy `{NAME}_URL` injection.
203
+ useImplicitEnvInjection: !hasNonEmptyPublicServicesConfig,
181
204
  routes,
182
205
  errors: result.errors,
183
206
  warnings: []
@@ -23,6 +23,7 @@ export interface ServicesBuildersResult {
23
23
  rewriteRoutes: Route[] | null;
24
24
  errorRoutes: Route[] | null;
25
25
  services?: ResolvedService[];
26
+ useImplicitEnvInjection?: boolean;
26
27
  }
27
28
  /**
28
29
  * Get builders for services - adapter for detectBuilders.
@@ -94,7 +94,7 @@ async function getServicesBuilders(options) {
94
94
  errors: [
95
95
  {
96
96
  code: "NO_SERVICES_CONFIGURED",
97
- message: "No services configured. Add `experimentalServices` to vercel.json."
97
+ message: "No services configured. Add `services` to vercel.json."
98
98
  }
99
99
  ],
100
100
  warnings: warningResponses,
@@ -121,7 +121,8 @@ async function getServicesBuilders(options) {
121
121
  ...result.routes.crons
122
122
  ] : null,
123
123
  errorRoutes: [],
124
- services: result.services
124
+ services: result.services,
125
+ useImplicitEnvInjection: result.useImplicitEnvInjection
125
126
  };
126
127
  }
127
128
  // Annotate the CommonJS export names for ESM import in node:
@@ -1,5 +1,6 @@
1
- import type { Service, ExperimentalServiceConfig, ExperimentalServices, ServiceDetectionError } from './types';
1
+ import type { EnvVars, Service, ConfiguredServices, ExperimentalServiceConfig, ServiceConfig, ServiceDetectionError } from './types';
2
2
  import type { DetectorFilesystem } from '../detectors/filesystem';
3
+ type ConfiguredServiceConfig = (ServiceConfig | ExperimentalServiceConfig) & Partial<ExperimentalServiceConfig>;
3
4
  interface ResolvedEntrypointPath {
4
5
  normalized: string;
5
6
  isDirectory: boolean;
@@ -7,7 +8,7 @@ interface ResolvedEntrypointPath {
7
8
  type RoutePrefixSource = 'configured' | 'generated';
8
9
  interface ResolveConfiguredServiceOptions {
9
10
  name: string;
10
- config: ExperimentalServiceConfig;
11
+ config: ConfiguredServiceConfig;
11
12
  /** Filesystem scoped to the service root (via chdir) when root is set, otherwise the project-level fs. */
12
13
  serviceFs: DetectorFilesystem;
13
14
  root?: string;
@@ -15,20 +16,29 @@ interface ResolveConfiguredServiceOptions {
15
16
  resolvedEntrypoint?: ResolvedEntrypointPath;
16
17
  routePrefixSource?: RoutePrefixSource;
17
18
  }
19
+ interface ResolveAllConfiguredServicesOptions {
20
+ requireFileEntrypointForBackendRuntimes?: boolean;
21
+ /**
22
+ * Optional top-level `env` (from vercel.json `env`). Per-service `env`
23
+ * values take precedence; entries here are folded into every service that
24
+ * doesn't already define the same name.
25
+ */
26
+ rootEnv?: EnvVars;
27
+ }
18
28
  /**
19
- * Validate a service configuration from vercel.json experimentalServices.
29
+ * Validate a service configuration from vercel.json services.
20
30
  */
21
- export declare function validateServiceConfig(name: string, config: ExperimentalServiceConfig): ServiceDetectionError | null;
22
- export declare function validateServiceEntrypoint(name: string, config: ExperimentalServiceConfig, resolvedEntrypoint: ResolvedEntrypointPath): ServiceDetectionError | null;
31
+ export declare function validateServiceConfig(name: string, config: ConfiguredServiceConfig, options?: ResolveAllConfiguredServicesOptions): ServiceDetectionError | null;
32
+ export declare function validateServiceEntrypoint(name: string, config: ConfiguredServiceConfig, resolvedEntrypoint: ResolvedEntrypointPath): ServiceDetectionError | null;
23
33
  /**
24
34
  * Resolve a single service from user configuration.
25
35
  */
26
36
  export declare function resolveConfiguredService(options: ResolveConfiguredServiceOptions): Promise<Service>;
27
37
  /**
28
- * Resolve all services from vercel.json experimentalServices.
38
+ * Resolve all services from vercel.json services.
29
39
  * Validates each service configuration.
30
40
  */
31
- export declare function resolveAllConfiguredServices(services: ExperimentalServices, fs: DetectorFilesystem, routePrefixSource?: RoutePrefixSource): Promise<{
41
+ export declare function resolveAllConfiguredServices(services: ConfiguredServices, fs: DetectorFilesystem, routePrefixSource?: RoutePrefixSource, options?: ResolveAllConfiguredServicesOptions): Promise<{
32
42
  services: Service[];
33
43
  errors: ServiceDetectionError[];
34
44
  }>;
@@ -45,7 +45,12 @@ function parsePyModuleAttrEntrypoint(entrypoint) {
45
45
  }
46
46
  const SERVICE_NAME_REGEX = /^[a-zA-Z]([a-zA-Z0-9_-]*[a-zA-Z0-9])?$/;
47
47
  const DNS_LABEL_RE = /^(?!-)[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i;
48
- const ENV_PREFIX_RE = /^[A-Z][A-Z0-9_]*_$/;
48
+ const ENV_VAR_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
49
+ const ENTRYPOINT_REQUIRED_RUNTIMES = /* @__PURE__ */ new Set([
50
+ "node",
51
+ "python",
52
+ "go"
53
+ ]);
49
54
  async function getServiceFs(fs, serviceName, root) {
50
55
  if (!root) {
51
56
  return { fs };
@@ -77,6 +82,38 @@ function normalizeServiceEntrypoint(entrypoint) {
77
82
  const normalized = import_path.posix.normalize(entrypoint);
78
83
  return normalized === "" ? "." : normalized;
79
84
  }
85
+ function getEffectiveServiceTrigger(config) {
86
+ if (config.type === "cron") {
87
+ return "schedule";
88
+ }
89
+ if (config.type === "worker") {
90
+ return "queue";
91
+ }
92
+ if (config.type !== "job") {
93
+ return void 0;
94
+ }
95
+ return config.trigger;
96
+ }
97
+ function getEntrypointRequiredRuntime(config) {
98
+ if (config.runtime && config.runtime in import_types.RUNTIME_BUILDERS) {
99
+ return config.runtime;
100
+ }
101
+ return (0, import_utils.inferRuntimeFromFramework)(config.framework);
102
+ }
103
+ function validateBackendFileEntrypoint(name, config, resolvedEntrypoint, options) {
104
+ if (!options.requireFileEntrypointForBackendRuntimes || !resolvedEntrypoint?.isDirectory) {
105
+ return null;
106
+ }
107
+ const runtime = getEntrypointRequiredRuntime(config);
108
+ if (!runtime || !ENTRYPOINT_REQUIRED_RUNTIMES.has(runtime)) {
109
+ return null;
110
+ }
111
+ return {
112
+ code: "INVALID_ENTRYPOINT",
113
+ message: `Service "${name}" must specify a file "entrypoint" when using "${config.runtime ? "runtime" : "framework"}" "${config.runtime || config.framework}".`,
114
+ serviceName: name
115
+ };
116
+ }
80
117
  async function resolveEntrypointPath({
81
118
  fs,
82
119
  serviceName,
@@ -171,7 +208,7 @@ async function detectFrameworkFromWorkspace({
171
208
  return {
172
209
  error: {
173
210
  code: "MULTIPLE_FRAMEWORKS_SERVICE",
174
- message: `Multiple frameworks detected in ${workspace === "." ? "project root" : `${workspace}/`}: ${frameworkNames}. Specify "framework" explicitly in experimentalServices.`,
211
+ message: `Multiple frameworks detected in ${workspace === "." ? "project root" : `${workspace}/`}: ${frameworkNames}. Specify "framework" explicitly in services.`,
175
212
  serviceName
176
213
  }
177
214
  };
@@ -265,7 +302,7 @@ function resolveServiceRoutingConfig(name, config) {
265
302
  }
266
303
  };
267
304
  }
268
- function validateServiceConfig(name, config) {
305
+ function validateServiceConfig(name, config, options = {}) {
269
306
  if (!SERVICE_NAME_REGEX.test(name)) {
270
307
  return {
271
308
  code: "INVALID_SERVICE_NAME",
@@ -281,13 +318,15 @@ function validateServiceConfig(name, config) {
281
318
  };
282
319
  }
283
320
  const serviceType = config.type || "web";
284
- const isJobService = serviceType === "job" || serviceType === "cron";
285
- const isScheduleJobService = (0, import_build_utils.isScheduleTriggeredService)({
321
+ const effectiveTrigger = getEffectiveServiceTrigger(config);
322
+ const effectiveService = {
286
323
  type: serviceType,
287
- trigger: config.trigger
288
- });
289
- const isQueueJobService = serviceType === "job" && config.trigger === "queue";
290
- const isWorkflowService = serviceType === "job" && config.trigger === "workflow";
324
+ trigger: effectiveTrigger
325
+ };
326
+ const isJobService = serviceType === "job" || serviceType === "cron";
327
+ const isScheduleJobService = (0, import_build_utils.isScheduleTriggeredService)(effectiveService);
328
+ const isQueueJobService = serviceType === "job" && (0, import_build_utils.isQueueTriggeredService)(effectiveService);
329
+ const isWorkflowService = serviceType === "job" && effectiveTrigger === "workflow";
291
330
  const isNonWebService = serviceType === "worker" || isJobService;
292
331
  const serviceTypeLabel = isJobService ? "Job" : serviceType === "worker" ? "Worker" : "Web";
293
332
  const routingResult = resolveServiceRoutingConfig(name, config);
@@ -333,17 +372,17 @@ function validateServiceConfig(name, config) {
333
372
  serviceName: name
334
373
  };
335
374
  }
336
- if (serviceType === "job" && config.trigger === void 0) {
375
+ if (serviceType === "job" && effectiveTrigger === void 0) {
337
376
  return {
338
377
  code: "MISSING_JOB_TRIGGER",
339
378
  message: `Job service "${name}" is missing required "trigger" field.`,
340
379
  serviceName: name
341
380
  };
342
381
  }
343
- if (serviceType === "job" && config.trigger && !import_build_utils.JOB_TRIGGERS.includes(config.trigger)) {
382
+ if (serviceType === "job" && effectiveTrigger && !import_build_utils.JOB_TRIGGERS.includes(effectiveTrigger)) {
344
383
  return {
345
384
  code: "INVALID_JOB_TRIGGER",
346
- message: `Job service "${name}" has invalid trigger "${config.trigger}". Expected ${import_build_utils.JOB_TRIGGERS.map((t) => `"${t}"`).join(", ")}.`,
385
+ message: `Job service "${name}" has invalid trigger "${effectiveTrigger}". Expected ${import_build_utils.JOB_TRIGGERS.map((t) => `"${t}"`).join(", ")}.`,
347
386
  serviceName: name
348
387
  };
349
388
  }
@@ -385,14 +424,46 @@ function validateServiceConfig(name, config) {
385
424
  };
386
425
  }
387
426
  }
388
- if (config.envPrefix !== void 0) {
389
- if (!ENV_PREFIX_RE.test(config.envPrefix)) {
427
+ if (config.env !== void 0) {
428
+ if (typeof config.env !== "object" || Array.isArray(config.env)) {
390
429
  return {
391
- code: "INVALID_ENV_PREFIX",
392
- message: `Service "${name}" has invalid envPrefix "${config.envPrefix}". Must start with an uppercase letter, contain only uppercase letters, digits, and underscores, and end with "_" (e.g., "MY_SERVICE_").`,
430
+ code: "INVALID_ENV_VARS",
431
+ message: `Service "${name}" has invalid "env". Must be an object keyed by environment variable name.`,
393
432
  serviceName: name
394
433
  };
395
434
  }
435
+ for (const [envVarName, envVar] of Object.entries(config.env)) {
436
+ if (!ENV_VAR_NAME_RE.test(envVarName)) {
437
+ return {
438
+ code: "INVALID_ENV_VAR_NAME",
439
+ message: `Service "${name}" has invalid env key "${envVarName}". Must match /^[A-Za-z_][A-Za-z0-9_]*$/.`,
440
+ serviceName: name
441
+ };
442
+ }
443
+ if (!envVar || typeof envVar !== "object" || Array.isArray(envVar)) {
444
+ return {
445
+ code: "INVALID_ENV_VAR",
446
+ message: `Service "${name}" has invalid env["${envVarName}"]. Must be an object with a "type" discriminator.`,
447
+ serviceName: name
448
+ };
449
+ }
450
+ const envVarType = envVar.type;
451
+ if (envVarType !== "service-ref") {
452
+ return {
453
+ code: "INVALID_ENV_VAR_TYPE",
454
+ message: `Service "${name}" env["${envVarName}"] has unknown type "${envVarType}".`,
455
+ serviceName: name
456
+ };
457
+ }
458
+ const refService = envVar.service;
459
+ if (typeof refService !== "string" || refService.length === 0) {
460
+ return {
461
+ code: "INVALID_ENV_VAR_REF",
462
+ message: `Service "${name}" env["${envVarName}"] must specify "service" as a non-empty string.`,
463
+ serviceName: name
464
+ };
465
+ }
466
+ }
396
467
  }
397
468
  if (config.runtime && !(config.runtime in import_types.RUNTIME_BUILDERS)) {
398
469
  return {
@@ -421,6 +492,7 @@ function validateServiceConfig(name, config) {
421
492
  const hasFramework = Boolean(config.framework);
422
493
  const hasBuilderOrRuntime = Boolean(config.builder || config.runtime);
423
494
  const hasEntrypoint = Boolean(config.entrypoint);
495
+ const entrypointRequiredRuntime = getEntrypointRequiredRuntime(config);
424
496
  if (!hasFramework && !hasBuilderOrRuntime && !hasEntrypoint) {
425
497
  return {
426
498
  code: "MISSING_SERVICE_CONFIG",
@@ -428,6 +500,13 @@ function validateServiceConfig(name, config) {
428
500
  serviceName: name
429
501
  };
430
502
  }
503
+ if (options.requireFileEntrypointForBackendRuntimes && !hasEntrypoint && entrypointRequiredRuntime && ENTRYPOINT_REQUIRED_RUNTIMES.has(entrypointRequiredRuntime)) {
504
+ return {
505
+ code: "MISSING_ENTRYPOINT",
506
+ message: `Service "${name}" must specify "entrypoint" when using "${config.runtime ? "runtime" : "framework"}" "${config.runtime || config.framework}".`,
507
+ serviceName: name
508
+ };
509
+ }
431
510
  if (hasBuilderOrRuntime && !hasFramework && !hasEntrypoint) {
432
511
  return {
433
512
  code: "MISSING_ENTRYPOINT",
@@ -465,7 +544,7 @@ async function resolveConfiguredService(options) {
465
544
  routePrefixSource = "configured"
466
545
  } = options;
467
546
  const type = config.type || "web";
468
- const trigger = type === "cron" ? "schedule" : type === "job" ? config.trigger : void 0;
547
+ const trigger = getEffectiveServiceTrigger(config);
469
548
  const rawEntrypoint = config.entrypoint;
470
549
  const moduleAttrParsed = typeof rawEntrypoint === "string" ? parsePyModuleAttrEntrypoint(rawEntrypoint) : null;
471
550
  const routingResult = resolveServiceRoutingConfig(name, config);
@@ -608,16 +687,16 @@ async function resolveConfiguredService(options) {
608
687
  schedule: config.schedule,
609
688
  handlerFunction: moduleAttrParsed?.attrName,
610
689
  topics,
611
- envPrefix: config.envPrefix
690
+ env: config.env
612
691
  };
613
692
  }
614
- async function resolveAllConfiguredServices(services, fs, routePrefixSource = "configured") {
693
+ async function resolveAllConfiguredServices(services, fs, routePrefixSource = "configured", options = {}) {
615
694
  const resolved = [];
616
695
  const errors = [];
617
696
  const webServicesByRoutePrefix = /* @__PURE__ */ new Map();
618
697
  for (const name of Object.keys(services)) {
619
698
  const serviceConfig = services[name];
620
- const validationError = validateServiceConfig(name, serviceConfig);
699
+ const validationError = validateServiceConfig(name, serviceConfig, options);
621
700
  if (validationError) {
622
701
  errors.push(validationError);
623
702
  continue;
@@ -655,6 +734,16 @@ async function resolveAllConfiguredServices(services, fs, routePrefixSource = "c
655
734
  continue;
656
735
  }
657
736
  }
737
+ const explicitBackendEntrypointError = validateBackendFileEntrypoint(
738
+ name,
739
+ serviceConfig,
740
+ resolvedEntrypoint,
741
+ options
742
+ );
743
+ if (explicitBackendEntrypointError) {
744
+ errors.push(explicitBackendEntrypointError);
745
+ continue;
746
+ }
658
747
  let resolvedConfig = serviceConfig;
659
748
  if (!serviceConfig.framework && resolvedEntrypoint) {
660
749
  if (resolvedEntrypoint.isDirectory) {
@@ -711,6 +800,16 @@ async function resolveAllConfiguredServices(services, fs, routePrefixSource = "c
711
800
  }
712
801
  }
713
802
  }
803
+ const backendEntrypointError = validateBackendFileEntrypoint(
804
+ name,
805
+ resolvedConfig,
806
+ resolvedEntrypoint,
807
+ options
808
+ );
809
+ if (backendEntrypointError) {
810
+ errors.push(backendEntrypointError);
811
+ continue;
812
+ }
714
813
  const service = await resolveConfiguredService({
715
814
  name,
716
815
  config: resolvedConfig,
@@ -736,8 +835,49 @@ async function resolveAllConfiguredServices(services, fs, routePrefixSource = "c
736
835
  }
737
836
  resolved.push(service);
738
837
  }
838
+ const servicesByName = new Map(resolved.map((s) => [s.name, s]));
839
+ for (const service of resolved) {
840
+ if (!service.env)
841
+ continue;
842
+ validateEnvRefs(
843
+ service.env,
844
+ `Service "${service.name}" env`,
845
+ servicesByName,
846
+ errors,
847
+ service.name
848
+ );
849
+ }
850
+ if (options.rootEnv) {
851
+ validateEnvRefs(options.rootEnv, "env", servicesByName, errors);
852
+ for (const service of resolved) {
853
+ service.env = { ...options.rootEnv, ...service.env ?? {} };
854
+ }
855
+ }
739
856
  return { services: resolved, errors };
740
857
  }
858
+ function validateEnvRefs(env, pathPrefix, servicesByName, errors, serviceName) {
859
+ for (const [envVarName, envVar] of Object.entries(env)) {
860
+ if (envVar.type !== "service-ref")
861
+ continue;
862
+ const refName = envVar.service;
863
+ const target = servicesByName.get(refName);
864
+ if (!target) {
865
+ errors.push({
866
+ code: "UNKNOWN_SERVICE_REF",
867
+ message: `${pathPrefix}["${envVarName}"] references unknown service "${refName}".`,
868
+ ...serviceName ? { serviceName } : {}
869
+ });
870
+ continue;
871
+ }
872
+ if (target.type !== "web") {
873
+ errors.push({
874
+ code: "INVALID_SERVICE_REF_TYPE",
875
+ message: `${pathPrefix}["${envVarName}"] references service "${refName}" which is a ${target.type} service and has no URL. Only web services can be referenced.`,
876
+ ...serviceName ? { serviceName } : {}
877
+ });
878
+ }
879
+ }
880
+ }
741
881
  // Annotate the CommonJS export names for ESM import in node:
742
882
  0 && (module.exports = {
743
883
  resolveAllConfiguredServices,
@@ -1,7 +1,7 @@
1
1
  import type { Route } from '@vercel/routing-utils';
2
- import type { ExperimentalServiceConfig, ExperimentalServiceGroups, ExperimentalServices, ServiceRuntime, ServiceType, Service, Builder } from '@vercel/build-utils';
2
+ import type { EnvVar, EnvVars, ExperimentalServiceConfig, ExperimentalServiceGroups, ExperimentalServices, ServiceConfig, Services, ServiceRuntime, ServiceType, ServiceRefEnvVar, Service, Builder } from '@vercel/build-utils';
3
3
  import type { DetectorFilesystem } from '../detectors/filesystem';
4
- export type { ExperimentalServiceConfig, ExperimentalServiceGroups, ExperimentalServices, ServiceRuntime, ServiceType, Service, Builder, };
4
+ export type { EnvVar, EnvVars, ExperimentalServiceConfig, ExperimentalServiceGroups, ExperimentalServices, ServiceConfig, Services, ServiceRuntime, ServiceType, ServiceRefEnvVar, Service, Builder, };
5
5
  /**
6
6
  * @deprecated Use `Service` instead
7
7
  */
@@ -34,24 +34,26 @@ export interface ServicesRoutes {
34
34
  */
35
35
  workers: Route[];
36
36
  }
37
- export type ServicesConfig = ExperimentalServices;
37
+ export type ConfiguredServices = Services | ExperimentalServices;
38
+ export type InferredServicesConfig = ExperimentalServices;
38
39
  export interface ResolvedServicesResult {
39
40
  services: Service[];
40
41
  source: DetectServicesSource;
42
+ useImplicitEnvInjection: boolean;
41
43
  routes: ServicesRoutes;
42
44
  errors: ServiceDetectionError[];
43
45
  warnings: ServiceDetectionWarning[];
44
46
  }
45
47
  export interface InferredServicesResult {
46
48
  source: 'layout' | 'procfile' | 'railway';
47
- config: ServicesConfig;
49
+ config: InferredServicesConfig;
48
50
  services: Service[];
49
51
  warnings: ServiceDetectionWarning[];
50
52
  }
51
53
  export interface DetectServicesResult extends ResolvedServicesResult {
52
54
  /**
53
55
  * Source of service definitions:
54
- * - `configured`: loaded from explicit project configuration (currently `vercel.json#experimentalServices`)
56
+ * - `configured`: loaded from explicit project configuration (`vercel.json#services` or legacy `experimentalServices`)
55
57
  * - `auto-detected`: inferred from project structure
56
58
  */
57
59
  resolved: ResolvedServicesResult;
@@ -1,8 +1,12 @@
1
1
  import { INTERNAL_SERVICE_PREFIX, getInternalServiceFunctionPath, getInternalServiceCronPathPrefix, getInternalServiceCronPath } from '@vercel/build-utils';
2
2
  import type { DetectorFilesystem } from '../detectors/filesystem';
3
- import type { ServiceRuntime, ExperimentalServices, ServiceDetectionError, ResolvedService } from './types';
3
+ import type { EnvVars, ServiceRuntime, ExperimentalServices, Services, ServiceDetectionError, ResolvedService } from './types';
4
4
  export { INTERNAL_SERVICE_PREFIX, getInternalServiceFunctionPath, getInternalServiceCronPathPrefix, getInternalServiceCronPath, };
5
5
  export declare function hasFile(fs: DetectorFilesystem, filePath: string): Promise<boolean>;
6
+ export declare function isPublicServicesEnabled(): boolean;
7
+ export declare function validateServicesConfigGate(config: {
8
+ services?: Services;
9
+ } | null | undefined): ServiceDetectionError | null;
6
10
  /**
7
11
  * Reserved internal namespace used by the dev queue proxy.
8
12
  */
@@ -53,7 +57,9 @@ export declare function inferServiceRuntime(config: {
53
57
  }): ServiceRuntime | undefined;
54
58
  export interface ReadVercelConfigResult {
55
59
  config: {
60
+ services?: Services;
56
61
  experimentalServices?: ExperimentalServices;
62
+ env?: Record<string, string> | EnvVars;
57
63
  } | null;
58
64
  error: ServiceDetectionError | null;
59
65
  }
@@ -41,9 +41,11 @@ __export(utils_exports, {
41
41
  inferRuntimeFromFramework: () => inferRuntimeFromFramework,
42
42
  inferServiceRuntime: () => inferServiceRuntime,
43
43
  isFrontendFramework: () => isFrontendFramework,
44
+ isPublicServicesEnabled: () => isPublicServicesEnabled,
44
45
  isRouteOwningBuilder: () => isRouteOwningBuilder,
45
46
  isStaticBuild: () => isStaticBuild,
46
- readVercelConfig: () => readVercelConfig
47
+ readVercelConfig: () => readVercelConfig,
48
+ validateServicesConfigGate: () => validateServicesConfigGate
47
49
  });
48
50
  module.exports = __toCommonJS(utils_exports);
49
51
  var import_framework_helpers = require("@vercel/build-utils/dist/framework-helpers");
@@ -56,6 +58,18 @@ async function hasFile(fs, filePath) {
56
58
  return false;
57
59
  }
58
60
  }
61
+ function isPublicServicesEnabled() {
62
+ return process.env.VERCEL_USE_SERVICES === "1" || process.env.VERCEL_USE_SERVICES?.toLowerCase() === "true";
63
+ }
64
+ function validateServicesConfigGate(config) {
65
+ if (config?.services !== void 0 && !isPublicServicesEnabled()) {
66
+ return {
67
+ code: "INVALID_VERCEL_CONFIG",
68
+ message: "Invalid vercel.json - should NOT have additional property `services`. Please remove it."
69
+ };
70
+ }
71
+ return null;
72
+ }
59
73
  const INTERNAL_QUEUES_PREFIX = "/_svc/_queues";
60
74
  function normalizeInternalServiceEntrypoint(entrypoint) {
61
75
  const normalized = entrypoint.replace(/\\/g, "/").replace(/^\/+/, "").replace(/\.[^/.]+$/, "");
@@ -140,6 +154,10 @@ async function readVercelConfig(fs) {
140
154
  try {
141
155
  const content = await fs.readFile("vercel.json");
142
156
  const config = JSON.parse(content.toString());
157
+ const gateError = validateServicesConfigGate(config);
158
+ if (gateError) {
159
+ return { config: null, error: gateError };
160
+ }
143
161
  return { config, error: null };
144
162
  } catch {
145
163
  return {
@@ -157,6 +175,10 @@ async function readVercelConfig(fs) {
157
175
  const { parse: tomlParse } = await import("smol-toml");
158
176
  const content = await fs.readFile("vercel.toml");
159
177
  const config = tomlParse(content.toString());
178
+ const gateError = validateServicesConfigGate(config);
179
+ if (gateError) {
180
+ return { config: null, error: gateError };
181
+ }
160
182
  return { config, error: null };
161
183
  } catch {
162
184
  return {
@@ -185,7 +207,9 @@ async function readVercelConfig(fs) {
185
207
  inferRuntimeFromFramework,
186
208
  inferServiceRuntime,
187
209
  isFrontendFramework,
210
+ isPublicServicesEnabled,
188
211
  isRouteOwningBuilder,
189
212
  isStaticBuild,
190
- readVercelConfig
213
+ readVercelConfig,
214
+ validateServicesConfigGate
191
215
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vercel/fs-detectors",
3
- "version": "6.2.1",
3
+ "version": "6.3.0",
4
4
  "description": "Vercel filesystem detectors",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -20,10 +20,10 @@
20
20
  "minimatch": "3.1.2",
21
21
  "semver": "6.3.1",
22
22
  "smol-toml": "1.5.2",
23
- "@vercel/error-utils": "2.1.0",
24
- "@vercel/frameworks": "3.25.1",
25
23
  "@vercel/routing-utils": "6.2.0",
26
- "@vercel/build-utils": "13.22.1"
24
+ "@vercel/frameworks": "3.26.0",
25
+ "@vercel/error-utils": "2.1.0",
26
+ "@vercel/build-utils": "13.24.0"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/glob": "7.2.0",