@vercel/fs-detectors 6.2.2 → 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.
@@ -47,4 +47,5 @@ export declare function detectBuilders(files: string[], pkg?: PackageJson | unde
47
47
  rewriteRoutes: Route[] | null;
48
48
  errorRoutes: Route[] | null;
49
49
  services?: Service[];
50
+ useImplicitEnvInjection?: boolean;
50
51
  }>;
@@ -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,6 +88,7 @@ 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: []
@@ -95,6 +103,7 @@ async function detectServices(options) {
95
103
  return withResolvedResult({
96
104
  services: [],
97
105
  source: "auto-detected",
106
+ useImplicitEnvInjection: true,
98
107
  routes: emptyRoutes(),
99
108
  errors: railwayResult.errors,
100
109
  warnings: railwayResult.warnings
@@ -116,6 +125,7 @@ async function detectServices(options) {
116
125
  {
117
126
  services: [],
118
127
  source: "auto-detected",
128
+ useImplicitEnvInjection: true,
119
129
  routes: emptyRoutes(),
120
130
  errors: result2.errors,
121
131
  warnings: railwayResult.warnings
@@ -134,6 +144,7 @@ async function detectServices(options) {
134
144
  const resolved = {
135
145
  services: result2.services,
136
146
  source: "auto-detected",
147
+ useImplicitEnvInjection: true,
137
148
  routes: routes2,
138
149
  errors: result2.errors,
139
150
  warnings: []
@@ -152,6 +163,7 @@ async function detectServices(options) {
152
163
  return withResolvedResult({
153
164
  services: [],
154
165
  source: "auto-detected",
166
+ useImplicitEnvInjection: true,
155
167
  routes: emptyRoutes(),
156
168
  errors: autoResult.errors,
157
169
  warnings: []
@@ -160,6 +172,7 @@ async function detectServices(options) {
160
172
  return withResolvedResult({
161
173
  services: [],
162
174
  source: "auto-detected",
175
+ useImplicitEnvInjection: true,
163
176
  routes: emptyRoutes(),
164
177
  errors: [
165
178
  {
@@ -177,13 +190,17 @@ async function detectServices(options) {
177
190
  {
178
191
  requireFileEntrypointForBackendRuntimes: Boolean(
179
192
  hasNonEmptyPublicServicesConfig
180
- )
193
+ ),
194
+ rootEnv: isEnvVars(vercelConfig?.env) ? vercelConfig?.env : void 0
181
195
  }
182
196
  );
183
197
  const routes = generateServicesRoutes(result.services);
184
198
  return withResolvedResult({
185
199
  services: result.services,
186
200
  source: "configured",
201
+ // GA `services` opts into explicit `env`; experimentalServices keeps
202
+ // the legacy `{NAME}_URL` injection.
203
+ useImplicitEnvInjection: !hasNonEmptyPublicServicesConfig,
187
204
  routes,
188
205
  errors: result.errors,
189
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.
@@ -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,4 +1,4 @@
1
- import type { Service, ConfiguredServices, ExperimentalServiceConfig, ServiceConfig, ServiceDetectionError } from './types';
1
+ import type { EnvVars, Service, ConfiguredServices, ExperimentalServiceConfig, ServiceConfig, ServiceDetectionError } from './types';
2
2
  import type { DetectorFilesystem } from '../detectors/filesystem';
3
3
  type ConfiguredServiceConfig = (ServiceConfig | ExperimentalServiceConfig) & Partial<ExperimentalServiceConfig>;
4
4
  interface ResolvedEntrypointPath {
@@ -18,6 +18,12 @@ interface ResolveConfiguredServiceOptions {
18
18
  }
19
19
  interface ResolveAllConfiguredServicesOptions {
20
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;
21
27
  }
22
28
  /**
23
29
  * Validate a service configuration from vercel.json services.
@@ -45,7 +45,7 @@ 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
49
  const ENTRYPOINT_REQUIRED_RUNTIMES = /* @__PURE__ */ new Set([
50
50
  "node",
51
51
  "python",
@@ -424,14 +424,46 @@ function validateServiceConfig(name, config, options = {}) {
424
424
  };
425
425
  }
426
426
  }
427
- if (config.envPrefix !== void 0) {
428
- if (!ENV_PREFIX_RE.test(config.envPrefix)) {
427
+ if (config.env !== void 0) {
428
+ if (typeof config.env !== "object" || Array.isArray(config.env)) {
429
429
  return {
430
- code: "INVALID_ENV_PREFIX",
431
- 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.`,
432
432
  serviceName: name
433
433
  };
434
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
+ }
435
467
  }
436
468
  if (config.runtime && !(config.runtime in import_types.RUNTIME_BUILDERS)) {
437
469
  return {
@@ -655,7 +687,7 @@ async function resolveConfiguredService(options) {
655
687
  schedule: config.schedule,
656
688
  handlerFunction: moduleAttrParsed?.attrName,
657
689
  topics,
658
- envPrefix: config.envPrefix
690
+ env: config.env
659
691
  };
660
692
  }
661
693
  async function resolveAllConfiguredServices(services, fs, routePrefixSource = "configured", options = {}) {
@@ -803,8 +835,49 @@ async function resolveAllConfiguredServices(services, fs, routePrefixSource = "c
803
835
  }
804
836
  resolved.push(service);
805
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
+ }
806
856
  return { services: resolved, errors };
807
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
+ }
808
881
  // Annotate the CommonJS export names for ESM import in node:
809
882
  0 && (module.exports = {
810
883
  resolveAllConfiguredServices,
@@ -1,7 +1,7 @@
1
1
  import type { Route } from '@vercel/routing-utils';
2
- import type { ExperimentalServiceConfig, ExperimentalServiceGroups, ExperimentalServices, ServiceConfig, Services, 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, ServiceConfig, Services, 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
  */
@@ -39,6 +39,7 @@ export type InferredServicesConfig = ExperimentalServices;
39
39
  export interface ResolvedServicesResult {
40
40
  services: Service[];
41
41
  source: DetectServicesSource;
42
+ useImplicitEnvInjection: boolean;
42
43
  routes: ServicesRoutes;
43
44
  errors: ServiceDetectionError[];
44
45
  warnings: ServiceDetectionWarning[];
@@ -1,6 +1,6 @@
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, Services, 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
6
  export declare function isPublicServicesEnabled(): boolean;
@@ -59,6 +59,7 @@ export interface ReadVercelConfigResult {
59
59
  config: {
60
60
  services?: Services;
61
61
  experimentalServices?: ExperimentalServices;
62
+ env?: Record<string, string> | EnvVars;
62
63
  } | null;
63
64
  error: ServiceDetectionError | null;
64
65
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vercel/fs-detectors",
3
- "version": "6.2.2",
3
+ "version": "6.3.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/routing-utils": "6.2.0",
24
- "@vercel/build-utils": "13.23.0",
24
+ "@vercel/frameworks": "3.26.0",
25
25
  "@vercel/error-utils": "2.1.0",
26
- "@vercel/frameworks": "3.26.0"
26
+ "@vercel/build-utils": "13.24.0"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/glob": "7.2.0",