@vercel/fs-detectors 6.2.1 → 6.2.2

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;
@@ -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>;
@@ -86,7 +86,8 @@ async function detectServices(options) {
86
86
  warnings: []
87
87
  });
88
88
  }
89
- const configuredServices = vercelConfig?.experimentalServices;
89
+ const hasNonEmptyPublicServicesConfig = vercelConfig?.services && Object.keys(vercelConfig.services).length > 0;
90
+ const configuredServices = hasNonEmptyPublicServicesConfig ? vercelConfig.services : vercelConfig?.experimentalServices;
90
91
  const hasConfiguredServices = configuredServices && Object.keys(configuredServices).length > 0;
91
92
  if (!hasConfiguredServices) {
92
93
  const railwayResult = await (0, import_detect_railway.detectRailwayServices)({ fs: scopedFs });
@@ -163,7 +164,7 @@ async function detectServices(options) {
163
164
  errors: [
164
165
  {
165
166
  code: "NO_SERVICES_CONFIGURED",
166
- message: "No services configured. Add `experimentalServices` to vercel.json."
167
+ message: "No services configured. Add `services` to vercel.json."
167
168
  }
168
169
  ],
169
170
  warnings: []
@@ -172,7 +173,12 @@ async function detectServices(options) {
172
173
  const result = await (0, import_resolve.resolveAllConfiguredServices)(
173
174
  configuredServices,
174
175
  scopedFs,
175
- "configured"
176
+ "configured",
177
+ {
178
+ requireFileEntrypointForBackendRuntimes: Boolean(
179
+ hasNonEmptyPublicServicesConfig
180
+ )
181
+ }
176
182
  );
177
183
  const routes = generateServicesRoutes(result.services);
178
184
  return withResolvedResult({
@@ -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,
@@ -1,5 +1,6 @@
1
- import type { Service, ExperimentalServiceConfig, ExperimentalServices, ServiceDetectionError } from './types';
1
+ import type { 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,23 @@ interface ResolveConfiguredServiceOptions {
15
16
  resolvedEntrypoint?: ResolvedEntrypointPath;
16
17
  routePrefixSource?: RoutePrefixSource;
17
18
  }
19
+ interface ResolveAllConfiguredServicesOptions {
20
+ requireFileEntrypointForBackendRuntimes?: boolean;
21
+ }
18
22
  /**
19
- * Validate a service configuration from vercel.json experimentalServices.
23
+ * Validate a service configuration from vercel.json services.
20
24
  */
21
- export declare function validateServiceConfig(name: string, config: ExperimentalServiceConfig): ServiceDetectionError | null;
22
- export declare function validateServiceEntrypoint(name: string, config: ExperimentalServiceConfig, resolvedEntrypoint: ResolvedEntrypointPath): ServiceDetectionError | null;
25
+ export declare function validateServiceConfig(name: string, config: ConfiguredServiceConfig, options?: ResolveAllConfiguredServicesOptions): ServiceDetectionError | null;
26
+ export declare function validateServiceEntrypoint(name: string, config: ConfiguredServiceConfig, resolvedEntrypoint: ResolvedEntrypointPath): ServiceDetectionError | null;
23
27
  /**
24
28
  * Resolve a single service from user configuration.
25
29
  */
26
30
  export declare function resolveConfiguredService(options: ResolveConfiguredServiceOptions): Promise<Service>;
27
31
  /**
28
- * Resolve all services from vercel.json experimentalServices.
32
+ * Resolve all services from vercel.json services.
29
33
  * Validates each service configuration.
30
34
  */
31
- export declare function resolveAllConfiguredServices(services: ExperimentalServices, fs: DetectorFilesystem, routePrefixSource?: RoutePrefixSource): Promise<{
35
+ export declare function resolveAllConfiguredServices(services: ConfiguredServices, fs: DetectorFilesystem, routePrefixSource?: RoutePrefixSource, options?: ResolveAllConfiguredServicesOptions): Promise<{
32
36
  services: Service[];
33
37
  errors: ServiceDetectionError[];
34
38
  }>;
@@ -46,6 +46,11 @@ function parsePyModuleAttrEntrypoint(entrypoint) {
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
48
  const ENV_PREFIX_RE = /^[A-Z][A-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
  }
@@ -421,6 +460,7 @@ function validateServiceConfig(name, config) {
421
460
  const hasFramework = Boolean(config.framework);
422
461
  const hasBuilderOrRuntime = Boolean(config.builder || config.runtime);
423
462
  const hasEntrypoint = Boolean(config.entrypoint);
463
+ const entrypointRequiredRuntime = getEntrypointRequiredRuntime(config);
424
464
  if (!hasFramework && !hasBuilderOrRuntime && !hasEntrypoint) {
425
465
  return {
426
466
  code: "MISSING_SERVICE_CONFIG",
@@ -428,6 +468,13 @@ function validateServiceConfig(name, config) {
428
468
  serviceName: name
429
469
  };
430
470
  }
471
+ if (options.requireFileEntrypointForBackendRuntimes && !hasEntrypoint && entrypointRequiredRuntime && ENTRYPOINT_REQUIRED_RUNTIMES.has(entrypointRequiredRuntime)) {
472
+ return {
473
+ code: "MISSING_ENTRYPOINT",
474
+ message: `Service "${name}" must specify "entrypoint" when using "${config.runtime ? "runtime" : "framework"}" "${config.runtime || config.framework}".`,
475
+ serviceName: name
476
+ };
477
+ }
431
478
  if (hasBuilderOrRuntime && !hasFramework && !hasEntrypoint) {
432
479
  return {
433
480
  code: "MISSING_ENTRYPOINT",
@@ -465,7 +512,7 @@ async function resolveConfiguredService(options) {
465
512
  routePrefixSource = "configured"
466
513
  } = options;
467
514
  const type = config.type || "web";
468
- const trigger = type === "cron" ? "schedule" : type === "job" ? config.trigger : void 0;
515
+ const trigger = getEffectiveServiceTrigger(config);
469
516
  const rawEntrypoint = config.entrypoint;
470
517
  const moduleAttrParsed = typeof rawEntrypoint === "string" ? parsePyModuleAttrEntrypoint(rawEntrypoint) : null;
471
518
  const routingResult = resolveServiceRoutingConfig(name, config);
@@ -611,13 +658,13 @@ async function resolveConfiguredService(options) {
611
658
  envPrefix: config.envPrefix
612
659
  };
613
660
  }
614
- async function resolveAllConfiguredServices(services, fs, routePrefixSource = "configured") {
661
+ async function resolveAllConfiguredServices(services, fs, routePrefixSource = "configured", options = {}) {
615
662
  const resolved = [];
616
663
  const errors = [];
617
664
  const webServicesByRoutePrefix = /* @__PURE__ */ new Map();
618
665
  for (const name of Object.keys(services)) {
619
666
  const serviceConfig = services[name];
620
- const validationError = validateServiceConfig(name, serviceConfig);
667
+ const validationError = validateServiceConfig(name, serviceConfig, options);
621
668
  if (validationError) {
622
669
  errors.push(validationError);
623
670
  continue;
@@ -655,6 +702,16 @@ async function resolveAllConfiguredServices(services, fs, routePrefixSource = "c
655
702
  continue;
656
703
  }
657
704
  }
705
+ const explicitBackendEntrypointError = validateBackendFileEntrypoint(
706
+ name,
707
+ serviceConfig,
708
+ resolvedEntrypoint,
709
+ options
710
+ );
711
+ if (explicitBackendEntrypointError) {
712
+ errors.push(explicitBackendEntrypointError);
713
+ continue;
714
+ }
658
715
  let resolvedConfig = serviceConfig;
659
716
  if (!serviceConfig.framework && resolvedEntrypoint) {
660
717
  if (resolvedEntrypoint.isDirectory) {
@@ -711,6 +768,16 @@ async function resolveAllConfiguredServices(services, fs, routePrefixSource = "c
711
768
  }
712
769
  }
713
770
  }
771
+ const backendEntrypointError = validateBackendFileEntrypoint(
772
+ name,
773
+ resolvedConfig,
774
+ resolvedEntrypoint,
775
+ options
776
+ );
777
+ if (backendEntrypointError) {
778
+ errors.push(backendEntrypointError);
779
+ continue;
780
+ }
714
781
  const service = await resolveConfiguredService({
715
782
  name,
716
783
  config: resolvedConfig,
@@ -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 { ExperimentalServiceConfig, ExperimentalServiceGroups, ExperimentalServices, ServiceConfig, Services, ServiceRuntime, ServiceType, 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 { ExperimentalServiceConfig, ExperimentalServiceGroups, ExperimentalServices, ServiceConfig, Services, ServiceRuntime, ServiceType, Service, Builder, };
5
5
  /**
6
6
  * @deprecated Use `Service` instead
7
7
  */
@@ -34,7 +34,8 @@ 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;
@@ -44,14 +45,14 @@ export interface ResolvedServicesResult {
44
45
  }
45
46
  export interface InferredServicesResult {
46
47
  source: 'layout' | 'procfile' | 'railway';
47
- config: ServicesConfig;
48
+ config: InferredServicesConfig;
48
49
  services: Service[];
49
50
  warnings: ServiceDetectionWarning[];
50
51
  }
51
52
  export interface DetectServicesResult extends ResolvedServicesResult {
52
53
  /**
53
54
  * Source of service definitions:
54
- * - `configured`: loaded from explicit project configuration (currently `vercel.json#experimentalServices`)
55
+ * - `configured`: loaded from explicit project configuration (`vercel.json#services` or legacy `experimentalServices`)
55
56
  * - `auto-detected`: inferred from project structure
56
57
  */
57
58
  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 { 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,6 +57,7 @@ export declare function inferServiceRuntime(config: {
53
57
  }): ServiceRuntime | undefined;
54
58
  export interface ReadVercelConfigResult {
55
59
  config: {
60
+ services?: Services;
56
61
  experimentalServices?: ExperimentalServices;
57
62
  } | null;
58
63
  error: ServiceDetectionError | null;
@@ -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.2.2",
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/build-utils": "13.23.0",
25
+ "@vercel/error-utils": "2.1.0",
26
+ "@vercel/frameworks": "3.26.0"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/glob": "7.2.0",