@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.
- package/dist/detect-builders.d.ts +3 -1
- package/dist/detect-builders.js +4 -3
- package/dist/index.d.ts +1 -1
- package/dist/services/auto-detect.d.ts +1 -1
- package/dist/services/auto-detect.js +5 -5
- package/dist/services/detect-railway.js +2 -2
- package/dist/services/detect-services.d.ts +1 -1
- package/dist/services/detect-services.js +26 -3
- package/dist/services/get-services-builders.d.ts +1 -0
- package/dist/services/get-services-builders.js +3 -2
- package/dist/services/resolve.d.ts +17 -7
- package/dist/services/resolve.js +160 -20
- package/dist/services/types.d.ts +7 -5
- package/dist/services/utils.d.ts +7 -1
- package/dist/services/utils.js +26 -2
- package/package.json +4 -4
|
@@ -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
|
}>;
|
package/dist/detect-builders.js
CHANGED
|
@@ -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 {
|
|
95
|
+
const { services, experimentalServices, projectSettings = {} } = options;
|
|
96
96
|
const { framework } = projectSettings;
|
|
97
|
-
const
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 `
|
|
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: []
|
|
@@ -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 `
|
|
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,
|
|
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:
|
|
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
|
|
29
|
+
* Validate a service configuration from vercel.json services.
|
|
20
30
|
*/
|
|
21
|
-
export declare function validateServiceConfig(name: string, config:
|
|
22
|
-
export declare function validateServiceEntrypoint(name: string, config:
|
|
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
|
|
38
|
+
* Resolve all services from vercel.json services.
|
|
29
39
|
* Validates each service configuration.
|
|
30
40
|
*/
|
|
31
|
-
export declare function resolveAllConfiguredServices(services:
|
|
41
|
+
export declare function resolveAllConfiguredServices(services: ConfiguredServices, fs: DetectorFilesystem, routePrefixSource?: RoutePrefixSource, options?: ResolveAllConfiguredServicesOptions): Promise<{
|
|
32
42
|
services: Service[];
|
|
33
43
|
errors: ServiceDetectionError[];
|
|
34
44
|
}>;
|
package/dist/services/resolve.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
285
|
-
const
|
|
321
|
+
const effectiveTrigger = getEffectiveServiceTrigger(config);
|
|
322
|
+
const effectiveService = {
|
|
286
323
|
type: serviceType,
|
|
287
|
-
trigger:
|
|
288
|
-
}
|
|
289
|
-
const
|
|
290
|
-
const
|
|
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" &&
|
|
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" &&
|
|
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 "${
|
|
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.
|
|
389
|
-
if (
|
|
427
|
+
if (config.env !== void 0) {
|
|
428
|
+
if (typeof config.env !== "object" || Array.isArray(config.env)) {
|
|
390
429
|
return {
|
|
391
|
-
code: "
|
|
392
|
-
message: `Service "${name}" has invalid
|
|
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 =
|
|
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
|
-
|
|
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,
|
package/dist/services/types.d.ts
CHANGED
|
@@ -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
|
|
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:
|
|
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 (
|
|
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;
|
package/dist/services/utils.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/services/utils.js
CHANGED
|
@@ -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.
|
|
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/
|
|
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",
|