@vercel/fs-detectors 7.1.7 → 7.2.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/index.d.ts +7 -0
- package/dist/index.js +39 -0
- package/dist/services/auto-detect.d.ts +46 -0
- package/dist/services/auto-detect.js +269 -0
- package/dist/services/detect-procfile.d.ts +22 -0
- package/dist/services/detect-procfile.js +250 -0
- package/dist/services/detect-railway.d.ts +22 -0
- package/dist/services/detect-railway.js +242 -0
- package/dist/services/detect-render.d.ts +14 -0
- package/dist/services/detect-render.js +231 -0
- package/dist/services/detect-services.d.ts +48 -0
- package/dist/services/detect-services.js +424 -0
- package/dist/services/resolve-v2.d.ts +11 -0
- package/dist/services/resolve-v2.js +383 -0
- package/dist/services/resolve.d.ts +65 -0
- package/dist/services/resolve.js +901 -0
- package/dist/services/runtimes/constants.d.ts +19 -0
- package/dist/services/runtimes/constants.js +85 -0
- package/dist/services/runtimes/entrypoint.d.ts +20 -0
- package/dist/services/runtimes/entrypoint.js +58 -0
- package/dist/services/runtimes/framework.d.ts +25 -0
- package/dist/services/runtimes/framework.js +70 -0
- package/dist/services/runtimes/runtime.d.ts +18 -0
- package/dist/services/runtimes/runtime.js +61 -0
- package/dist/services/types.d.ts +113 -0
- package/dist/services/types.js +16 -0
- package/dist/services/utils.d.ts +48 -0
- package/dist/services/utils.js +157 -0
- package/package.json +4 -3
|
@@ -0,0 +1,901 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var resolve_exports = {};
|
|
20
|
+
__export(resolve_exports, {
|
|
21
|
+
detectFrameworkFromWorkspace: () => detectFrameworkFromWorkspace,
|
|
22
|
+
getServiceFs: () => getServiceFs,
|
|
23
|
+
inferWorkspaceFromNearestManifest: () => inferWorkspaceFromNearestManifest,
|
|
24
|
+
resolveAllConfiguredServices: () => resolveAllConfiguredServices,
|
|
25
|
+
resolveConfiguredService: () => resolveConfiguredService,
|
|
26
|
+
resolveEntrypointPath: () => resolveEntrypointPath,
|
|
27
|
+
validateServiceConfig: () => validateServiceConfig,
|
|
28
|
+
validateServiceEntrypoint: () => validateServiceEntrypoint
|
|
29
|
+
});
|
|
30
|
+
module.exports = __toCommonJS(resolve_exports);
|
|
31
|
+
var import_path = require("path");
|
|
32
|
+
var import_build_utils = require("@vercel/build-utils");
|
|
33
|
+
var import_constants = require("./runtimes/constants");
|
|
34
|
+
var import_framework = require("./runtimes/framework");
|
|
35
|
+
var import_runtime = require("./runtimes/runtime");
|
|
36
|
+
var import_entrypoint = require("./runtimes/entrypoint");
|
|
37
|
+
var import_utils = require("./utils");
|
|
38
|
+
var import_frameworks = require("@vercel/frameworks");
|
|
39
|
+
var import_detect_framework = require("../detect-framework");
|
|
40
|
+
var import_routing_utils = require("@vercel/routing-utils");
|
|
41
|
+
var import_build_utils2 = require("@vercel/build-utils");
|
|
42
|
+
const frameworksBySlug = new Map(import_frameworks.frameworkList.map((f) => [f.slug, f]));
|
|
43
|
+
const SERVICE_NAME_REGEX = /^[a-zA-Z]([a-zA-Z0-9_-]*[a-zA-Z0-9])?$/;
|
|
44
|
+
const DNS_LABEL_RE = /^(?!-)[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i;
|
|
45
|
+
const ENV_VAR_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
46
|
+
const ENTRYPOINT_REQUIRED_RUNTIMES = /* @__PURE__ */ new Set([
|
|
47
|
+
"node",
|
|
48
|
+
"python",
|
|
49
|
+
"go"
|
|
50
|
+
]);
|
|
51
|
+
function isContainerRuntime(config) {
|
|
52
|
+
return config.runtime === "container";
|
|
53
|
+
}
|
|
54
|
+
function normalizeContainerCommand(command) {
|
|
55
|
+
return Array.isArray(command) ? command : [command];
|
|
56
|
+
}
|
|
57
|
+
async function getServiceFs(fs, serviceName, root) {
|
|
58
|
+
if (!root) {
|
|
59
|
+
return { fs };
|
|
60
|
+
}
|
|
61
|
+
const normalizedRoot = (0, import_entrypoint.stripTrailingSlash)(import_path.posix.normalize(root));
|
|
62
|
+
if (!await fs.hasPath(normalizedRoot)) {
|
|
63
|
+
return {
|
|
64
|
+
fs,
|
|
65
|
+
error: {
|
|
66
|
+
code: "ROOT_NOT_FOUND",
|
|
67
|
+
message: `Service "${serviceName}" has root "${root}" but that directory does not exist.`,
|
|
68
|
+
serviceName
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (await fs.isFile(normalizedRoot)) {
|
|
73
|
+
return {
|
|
74
|
+
fs,
|
|
75
|
+
error: {
|
|
76
|
+
code: "ROOT_NOT_DIRECTORY",
|
|
77
|
+
message: `Service "${serviceName}" has root "${root}" but that path is a file, not a directory.`,
|
|
78
|
+
serviceName
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return { fs: fs.chdir(normalizedRoot) };
|
|
83
|
+
}
|
|
84
|
+
function normalizeServiceEntrypoint(entrypoint) {
|
|
85
|
+
const normalized = import_path.posix.normalize(entrypoint);
|
|
86
|
+
return normalized === "" ? "." : normalized;
|
|
87
|
+
}
|
|
88
|
+
function getEffectiveServiceTrigger(config) {
|
|
89
|
+
if (config.type === "cron") {
|
|
90
|
+
return "schedule";
|
|
91
|
+
}
|
|
92
|
+
if (config.type === "worker") {
|
|
93
|
+
return "queue";
|
|
94
|
+
}
|
|
95
|
+
if (config.type !== "job") {
|
|
96
|
+
return void 0;
|
|
97
|
+
}
|
|
98
|
+
return config.trigger;
|
|
99
|
+
}
|
|
100
|
+
function getEntrypointRequiredRuntime(config) {
|
|
101
|
+
if (config.runtime && config.runtime in import_constants.RUNTIME_BUILDERS) {
|
|
102
|
+
return config.runtime;
|
|
103
|
+
}
|
|
104
|
+
return (0, import_framework.inferRuntimeFromFramework)(config.framework);
|
|
105
|
+
}
|
|
106
|
+
function validateBackendFileEntrypoint(name, config, resolvedEntrypoint, options) {
|
|
107
|
+
if (!options.requireFileEntrypointForBackendRuntimes || !resolvedEntrypoint?.isDirectory) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
const runtime = getEntrypointRequiredRuntime(config);
|
|
111
|
+
if (!runtime || !ENTRYPOINT_REQUIRED_RUNTIMES.has(runtime)) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
code: "INVALID_ENTRYPOINT",
|
|
116
|
+
message: `Service "${name}" must specify a file "entrypoint" when using "${config.runtime ? "runtime" : "framework"}" "${config.runtime || config.framework}".`,
|
|
117
|
+
serviceName: name
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
async function resolveEntrypointPath({
|
|
121
|
+
fs,
|
|
122
|
+
serviceName,
|
|
123
|
+
entrypoint
|
|
124
|
+
}) {
|
|
125
|
+
const normalized = normalizeServiceEntrypoint(entrypoint);
|
|
126
|
+
if (!await fs.hasPath(normalized)) {
|
|
127
|
+
return {
|
|
128
|
+
error: {
|
|
129
|
+
code: "ENTRYPOINT_NOT_FOUND",
|
|
130
|
+
message: `Service "${serviceName}" has entrypoint "${entrypoint}" but that path does not exist.`,
|
|
131
|
+
serviceName
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
entrypoint: {
|
|
137
|
+
normalized,
|
|
138
|
+
isDirectory: !await fs.isFile(normalized)
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function toWorkspaceRelativeEntrypoint(entrypoint, workspace) {
|
|
143
|
+
const normalizedEntrypoint = import_path.posix.normalize(entrypoint);
|
|
144
|
+
if (workspace === ".") {
|
|
145
|
+
return normalizedEntrypoint;
|
|
146
|
+
}
|
|
147
|
+
const workspacePrefix = `${workspace}/`;
|
|
148
|
+
if (normalizedEntrypoint.startsWith(workspacePrefix)) {
|
|
149
|
+
return normalizedEntrypoint.slice(workspacePrefix.length);
|
|
150
|
+
}
|
|
151
|
+
const relativeEntrypoint = import_path.posix.relative(
|
|
152
|
+
workspace,
|
|
153
|
+
normalizedEntrypoint
|
|
154
|
+
);
|
|
155
|
+
if (relativeEntrypoint === "" || relativeEntrypoint.startsWith("..")) {
|
|
156
|
+
return normalizedEntrypoint;
|
|
157
|
+
}
|
|
158
|
+
return relativeEntrypoint;
|
|
159
|
+
}
|
|
160
|
+
async function inferWorkspaceFromNearestManifest({
|
|
161
|
+
fs,
|
|
162
|
+
entrypoint,
|
|
163
|
+
runtime
|
|
164
|
+
}) {
|
|
165
|
+
if (!entrypoint || !runtime) {
|
|
166
|
+
return void 0;
|
|
167
|
+
}
|
|
168
|
+
const manifests = import_constants.RUNTIME_MANIFESTS[runtime];
|
|
169
|
+
if (!manifests || manifests.length === 0) {
|
|
170
|
+
return void 0;
|
|
171
|
+
}
|
|
172
|
+
let dir = import_path.posix.dirname(import_path.posix.normalize(entrypoint)) || ".";
|
|
173
|
+
if (dir === "") {
|
|
174
|
+
dir = ".";
|
|
175
|
+
}
|
|
176
|
+
let reachedRoot = false;
|
|
177
|
+
while (!reachedRoot) {
|
|
178
|
+
for (const manifest of manifests) {
|
|
179
|
+
const manifestPath = dir === "." ? manifest : import_path.posix.join(dir, manifest);
|
|
180
|
+
if (await (0, import_utils.hasFile)(fs, manifestPath)) {
|
|
181
|
+
return dir;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (dir === "." || dir === "/") {
|
|
185
|
+
reachedRoot = true;
|
|
186
|
+
} else {
|
|
187
|
+
const parent = import_path.posix.dirname(dir);
|
|
188
|
+
if (!parent || parent === dir) {
|
|
189
|
+
reachedRoot = true;
|
|
190
|
+
} else {
|
|
191
|
+
dir = parent;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return void 0;
|
|
196
|
+
}
|
|
197
|
+
async function detectFrameworkFromWorkspace({
|
|
198
|
+
fs,
|
|
199
|
+
workspace,
|
|
200
|
+
serviceName,
|
|
201
|
+
runtime
|
|
202
|
+
}) {
|
|
203
|
+
const serviceFs = workspace === "." ? fs : fs.chdir(workspace);
|
|
204
|
+
const frameworkCandidates = (0, import_framework.filterFrameworksByRuntime)(import_frameworks.frameworkList, runtime);
|
|
205
|
+
const frameworks = await (0, import_detect_framework.detectFrameworks)({
|
|
206
|
+
fs: serviceFs,
|
|
207
|
+
frameworkList: frameworkCandidates
|
|
208
|
+
});
|
|
209
|
+
if (frameworks.length > 1) {
|
|
210
|
+
const frameworkNames = frameworks.map((f) => f.name).join(", ");
|
|
211
|
+
return {
|
|
212
|
+
error: {
|
|
213
|
+
code: "MULTIPLE_FRAMEWORKS_SERVICE",
|
|
214
|
+
message: `Multiple frameworks detected in ${workspace === "." ? "project root" : `${workspace}/`}: ${frameworkNames}. Specify "framework" explicitly in services.`,
|
|
215
|
+
serviceName
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
if (frameworks.length === 1) {
|
|
220
|
+
return {
|
|
221
|
+
framework: frameworks[0].slug ?? void 0
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
return {};
|
|
225
|
+
}
|
|
226
|
+
function isReservedServiceRoutePrefix(routePrefix) {
|
|
227
|
+
const normalized = (0, import_routing_utils.normalizeRoutePrefix)(routePrefix);
|
|
228
|
+
return normalized === import_utils.INTERNAL_SERVICE_PREFIX || normalized.startsWith(`${import_utils.INTERNAL_SERVICE_PREFIX}/`);
|
|
229
|
+
}
|
|
230
|
+
function resolveServiceRoutingConfig(name, config) {
|
|
231
|
+
const hasLegacyRoutePrefix = typeof config.routePrefix === "string";
|
|
232
|
+
const hasLegacySubdomain = typeof config.subdomain === "string";
|
|
233
|
+
if (config.mount === void 0) {
|
|
234
|
+
return {
|
|
235
|
+
routing: {
|
|
236
|
+
routePrefix: config.routePrefix,
|
|
237
|
+
subdomain: config.subdomain,
|
|
238
|
+
routePrefixConfigured: hasLegacyRoutePrefix
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
if (hasLegacyRoutePrefix || hasLegacySubdomain) {
|
|
243
|
+
return {
|
|
244
|
+
error: {
|
|
245
|
+
code: "CONFLICTING_MOUNT_CONFIG",
|
|
246
|
+
message: `Service "${name}" cannot mix "mount" with "routePrefix" or "subdomain". Use only one routing configuration style.`,
|
|
247
|
+
serviceName: name
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
if (typeof config.mount === "string") {
|
|
252
|
+
return {
|
|
253
|
+
routing: {
|
|
254
|
+
routePrefix: config.mount,
|
|
255
|
+
routePrefixConfigured: true
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
if (!config.mount || typeof config.mount !== "object" || Array.isArray(config.mount)) {
|
|
260
|
+
return {
|
|
261
|
+
error: {
|
|
262
|
+
code: "INVALID_MOUNT",
|
|
263
|
+
message: `Service "${name}" has invalid "mount" config. Use a string path such as "/api" or an object like { path: "/api", subdomain: "api" }.`,
|
|
264
|
+
serviceName: name
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
const hasInvalidMountKeys = Object.keys(config.mount).some(
|
|
269
|
+
(key) => key !== "path" && key !== "subdomain"
|
|
270
|
+
);
|
|
271
|
+
if (hasInvalidMountKeys) {
|
|
272
|
+
return {
|
|
273
|
+
error: {
|
|
274
|
+
code: "INVALID_MOUNT",
|
|
275
|
+
message: `Service "${name}" has invalid "mount" config. Only "path" and "subdomain" are supported.`,
|
|
276
|
+
serviceName: name
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
const mountPath = config.mount.path;
|
|
281
|
+
const mountSubdomain = config.mount.subdomain;
|
|
282
|
+
if (mountPath !== void 0 && typeof mountPath !== "string" || mountSubdomain !== void 0 && typeof mountSubdomain !== "string") {
|
|
283
|
+
return {
|
|
284
|
+
error: {
|
|
285
|
+
code: "INVALID_MOUNT",
|
|
286
|
+
message: `Service "${name}" has invalid "mount" config. "path" and "subdomain" must be strings when provided.`,
|
|
287
|
+
serviceName: name
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
if (typeof mountPath !== "string" && typeof mountSubdomain !== "string") {
|
|
292
|
+
return {
|
|
293
|
+
error: {
|
|
294
|
+
code: "INVALID_MOUNT",
|
|
295
|
+
message: `Service "${name}" has invalid "mount" config. Specify at least one of "mount.path" or "mount.subdomain".`,
|
|
296
|
+
serviceName: name
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
return {
|
|
301
|
+
routing: {
|
|
302
|
+
routePrefix: mountPath,
|
|
303
|
+
subdomain: mountSubdomain,
|
|
304
|
+
routePrefixConfigured: typeof mountPath === "string"
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
function validateServiceConfig(name, config, options = {}) {
|
|
309
|
+
if (!SERVICE_NAME_REGEX.test(name)) {
|
|
310
|
+
return {
|
|
311
|
+
code: "INVALID_SERVICE_NAME",
|
|
312
|
+
message: `Service name "${name}" is invalid. Names must start with a letter, end with an alphanumeric character, and contain only alphanumeric characters, hyphens, and underscores.`,
|
|
313
|
+
serviceName: name
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
if (!config || typeof config !== "object") {
|
|
317
|
+
return {
|
|
318
|
+
code: "INVALID_SERVICE_CONFIG",
|
|
319
|
+
message: `Service "${name}" has an invalid configuration. Expected an object.`,
|
|
320
|
+
serviceName: name
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
const serviceType = config.type || "web";
|
|
324
|
+
const effectiveTrigger = getEffectiveServiceTrigger(config);
|
|
325
|
+
const effectiveService = {
|
|
326
|
+
type: serviceType,
|
|
327
|
+
trigger: effectiveTrigger
|
|
328
|
+
};
|
|
329
|
+
const isJobService = serviceType === "job" || serviceType === "cron";
|
|
330
|
+
const isScheduleJobService = (0, import_build_utils.isScheduleTriggeredService)(effectiveService);
|
|
331
|
+
const isQueueJobService = serviceType === "job" && (0, import_build_utils.isQueueTriggeredService)(effectiveService);
|
|
332
|
+
const isWorkflowService = serviceType === "job" && effectiveTrigger === "workflow";
|
|
333
|
+
const isNonWebService = serviceType === "worker" || isJobService;
|
|
334
|
+
const serviceTypeLabel = isJobService ? "Job" : serviceType === "worker" ? "Worker" : "Web";
|
|
335
|
+
const routingResult = resolveServiceRoutingConfig(name, config);
|
|
336
|
+
if (routingResult.error) {
|
|
337
|
+
return routingResult.error;
|
|
338
|
+
}
|
|
339
|
+
const configuredRoutePrefix = routingResult.routing?.routePrefix;
|
|
340
|
+
const configuredSubdomain = routingResult.routing?.subdomain;
|
|
341
|
+
const hasRoutePrefix = typeof configuredRoutePrefix === "string";
|
|
342
|
+
const hasSubdomain = typeof configuredSubdomain === "string";
|
|
343
|
+
if (hasSubdomain && !DNS_LABEL_RE.test(configuredSubdomain)) {
|
|
344
|
+
return {
|
|
345
|
+
code: "INVALID_SUBDOMAIN",
|
|
346
|
+
message: `Web service "${name}" has invalid subdomain "${configuredSubdomain}". Use a single DNS label such as "api".`,
|
|
347
|
+
serviceName: name
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
if (serviceType === "web" && !hasRoutePrefix && !hasSubdomain) {
|
|
351
|
+
return {
|
|
352
|
+
code: "MISSING_ROUTE_PREFIX",
|
|
353
|
+
message: `Web service "${name}" must specify at least one of "mount", "routePrefix", or "subdomain".`,
|
|
354
|
+
serviceName: name
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
if (serviceType === "web" && configuredRoutePrefix && isReservedServiceRoutePrefix(configuredRoutePrefix)) {
|
|
358
|
+
return {
|
|
359
|
+
code: "RESERVED_ROUTE_PREFIX",
|
|
360
|
+
message: `Web service "${name}" cannot use routePrefix "${configuredRoutePrefix}". The "${import_utils.INTERNAL_SERVICE_PREFIX}" prefix is reserved for internal services routing.`,
|
|
361
|
+
serviceName: name
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
if (isNonWebService && configuredRoutePrefix) {
|
|
365
|
+
return {
|
|
366
|
+
code: "INVALID_ROUTE_PREFIX",
|
|
367
|
+
message: `${serviceTypeLabel} service "${name}" cannot have "routePrefix" or "mount". Only web services should specify path-based routing.`,
|
|
368
|
+
serviceName: name
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
if (isNonWebService && hasSubdomain) {
|
|
372
|
+
return {
|
|
373
|
+
code: "INVALID_HOST_ROUTING_CONFIG",
|
|
374
|
+
message: `${serviceTypeLabel} service "${name}" cannot have "subdomain" or "mount.subdomain". Only web services should specify subdomain routing.`,
|
|
375
|
+
serviceName: name
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
if (serviceType === "job" && effectiveTrigger === void 0) {
|
|
379
|
+
return {
|
|
380
|
+
code: "MISSING_JOB_TRIGGER",
|
|
381
|
+
message: `Job service "${name}" is missing required "trigger" field.`,
|
|
382
|
+
serviceName: name
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
if (serviceType === "job" && effectiveTrigger && !import_build_utils.JOB_TRIGGERS.includes(effectiveTrigger)) {
|
|
386
|
+
return {
|
|
387
|
+
code: "INVALID_JOB_TRIGGER",
|
|
388
|
+
message: `Job service "${name}" has invalid trigger "${effectiveTrigger}". Expected ${import_build_utils.JOB_TRIGGERS.map((t) => `"${t}"`).join(", ")}.`,
|
|
389
|
+
serviceName: name
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
if (isScheduleJobService && !config.schedule) {
|
|
393
|
+
return {
|
|
394
|
+
code: serviceType === "cron" ? "MISSING_CRON_SCHEDULE" : "MISSING_JOB_SCHEDULE",
|
|
395
|
+
message: `${serviceTypeLabel} service "${name}" is missing required "schedule" field.`,
|
|
396
|
+
serviceName: name
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
if (isQueueJobService && (!Array.isArray(config.topics) || config.topics.length === 0)) {
|
|
400
|
+
return {
|
|
401
|
+
code: "MISSING_QUEUE_TOPICS",
|
|
402
|
+
message: `${serviceTypeLabel} service "${name}" is missing required "topics" field.`,
|
|
403
|
+
serviceName: name
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
if (isWorkflowService && typeof config.entrypoint !== "string") {
|
|
407
|
+
return {
|
|
408
|
+
code: "MISSING_ENTRYPOINT",
|
|
409
|
+
message: `Job service "${name}" with "workflow" trigger must specify "entrypoint".`,
|
|
410
|
+
serviceName: name
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
if (config.root !== void 0) {
|
|
414
|
+
const normalizedRoot = import_path.posix.normalize(config.root);
|
|
415
|
+
if (normalizedRoot.startsWith("/")) {
|
|
416
|
+
return {
|
|
417
|
+
code: "INVALID_ROOT",
|
|
418
|
+
message: `Service "${name}" has invalid "root" "${config.root}". Must be a relative path.`,
|
|
419
|
+
serviceName: name
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
if (normalizedRoot === ".." || normalizedRoot.startsWith("../")) {
|
|
423
|
+
return {
|
|
424
|
+
code: "INVALID_ROOT",
|
|
425
|
+
message: `Service "${name}" has invalid "root" "${config.root}". Must not escape the project root.`,
|
|
426
|
+
serviceName: name
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
if (config.env !== void 0) {
|
|
431
|
+
if (typeof config.env !== "object" || Array.isArray(config.env)) {
|
|
432
|
+
return {
|
|
433
|
+
code: "INVALID_ENV_VARS",
|
|
434
|
+
message: `Service "${name}" has invalid "env". Must be an object keyed by environment variable name.`,
|
|
435
|
+
serviceName: name
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
for (const [envVarName, envVar] of Object.entries(config.env)) {
|
|
439
|
+
if (!ENV_VAR_NAME_RE.test(envVarName)) {
|
|
440
|
+
return {
|
|
441
|
+
code: "INVALID_ENV_VAR_NAME",
|
|
442
|
+
message: `Service "${name}" has invalid env key "${envVarName}". Must match /^[A-Za-z_][A-Za-z0-9_]*$/.`,
|
|
443
|
+
serviceName: name
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
if (!envVar || typeof envVar !== "object" || Array.isArray(envVar)) {
|
|
447
|
+
return {
|
|
448
|
+
code: "INVALID_ENV_VAR",
|
|
449
|
+
message: `Service "${name}" has invalid env["${envVarName}"]. Must be an object with a "type" discriminator.`,
|
|
450
|
+
serviceName: name
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
const envVarType = envVar.type;
|
|
454
|
+
if (envVarType !== "service-ref") {
|
|
455
|
+
return {
|
|
456
|
+
code: "INVALID_ENV_VAR_TYPE",
|
|
457
|
+
message: `Service "${name}" env["${envVarName}"] has unknown type "${envVarType}".`,
|
|
458
|
+
serviceName: name
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
const refService = envVar.service;
|
|
462
|
+
if (typeof refService !== "string" || refService.length === 0) {
|
|
463
|
+
return {
|
|
464
|
+
code: "INVALID_ENV_VAR_REF",
|
|
465
|
+
message: `Service "${name}" env["${envVarName}"] must specify "service" as a non-empty string.`,
|
|
466
|
+
serviceName: name
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
if (config.runtime && !(config.runtime in import_constants.RUNTIME_BUILDERS)) {
|
|
472
|
+
return {
|
|
473
|
+
code: "INVALID_RUNTIME",
|
|
474
|
+
message: `Service "${name}" has invalid runtime "${config.runtime}".`,
|
|
475
|
+
serviceName: name
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
if (config.framework && !frameworksBySlug.has(config.framework)) {
|
|
479
|
+
return {
|
|
480
|
+
code: "INVALID_FRAMEWORK",
|
|
481
|
+
message: `Service "${name}" has invalid framework "${config.framework}".`,
|
|
482
|
+
serviceName: name
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
if (config.runtime && config.framework) {
|
|
486
|
+
const frameworkRuntime = (0, import_framework.inferRuntimeFromFramework)(config.framework);
|
|
487
|
+
if (frameworkRuntime && frameworkRuntime !== config.runtime) {
|
|
488
|
+
return {
|
|
489
|
+
code: "RUNTIME_FRAMEWORK_MISMATCH",
|
|
490
|
+
message: `Service "${name}" has conflicting runtime/framework: runtime "${config.runtime}" is incompatible with framework "${config.framework}" (runtime "${frameworkRuntime}").`,
|
|
491
|
+
serviceName: name
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
const hasFramework = Boolean(config.framework);
|
|
496
|
+
const hasBuilderOrRuntime = Boolean(config.builder || config.runtime);
|
|
497
|
+
const hasEntrypoint = Boolean(config.entrypoint);
|
|
498
|
+
const entrypointRequiredRuntime = getEntrypointRequiredRuntime(config);
|
|
499
|
+
if (!hasFramework && !hasBuilderOrRuntime && !hasEntrypoint) {
|
|
500
|
+
return {
|
|
501
|
+
code: "MISSING_SERVICE_CONFIG",
|
|
502
|
+
message: `Service "${name}" must specify "framework", "entrypoint", or both "builder"/"runtime" with "entrypoint".`,
|
|
503
|
+
serviceName: name
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
if (options.requireFileEntrypointForBackendRuntimes && !hasEntrypoint && entrypointRequiredRuntime && ENTRYPOINT_REQUIRED_RUNTIMES.has(entrypointRequiredRuntime)) {
|
|
507
|
+
return {
|
|
508
|
+
code: "MISSING_ENTRYPOINT",
|
|
509
|
+
message: `Service "${name}" must specify "entrypoint" when using "${config.runtime ? "runtime" : "framework"}" "${config.runtime || config.framework}".`,
|
|
510
|
+
serviceName: name
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
if (hasBuilderOrRuntime && !hasFramework && !hasEntrypoint) {
|
|
514
|
+
return {
|
|
515
|
+
code: "MISSING_ENTRYPOINT",
|
|
516
|
+
message: `Service "${name}" must specify "entrypoint" when using "${config.builder ? "builder" : "runtime"}".`,
|
|
517
|
+
serviceName: name
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
if (config.command !== void 0 && !isContainerRuntime(config)) {
|
|
521
|
+
return {
|
|
522
|
+
code: "INVALID_COMMAND",
|
|
523
|
+
message: `Service "${name}" can only specify "command" when using runtime "container".`,
|
|
524
|
+
serviceName: name
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
function validateServiceEntrypoint(name, config, resolvedEntrypoint) {
|
|
530
|
+
if (!resolvedEntrypoint.isDirectory && !config.builder && !config.runtime && !config.framework) {
|
|
531
|
+
const runtime = (0, import_runtime.inferRuntime)({
|
|
532
|
+
...config,
|
|
533
|
+
entrypoint: resolvedEntrypoint.normalized
|
|
534
|
+
});
|
|
535
|
+
if (!runtime) {
|
|
536
|
+
const supported = Object.keys(import_constants.ENTRYPOINT_EXTENSIONS).join(", ");
|
|
537
|
+
return {
|
|
538
|
+
code: "UNSUPPORTED_ENTRYPOINT",
|
|
539
|
+
message: `Service "${name}" has unsupported entrypoint "${config.entrypoint}". Use a supported extension (${supported}) or specify "builder", "framework", or "runtime".`,
|
|
540
|
+
serviceName: name
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
async function resolveConfiguredService(options) {
|
|
547
|
+
const {
|
|
548
|
+
name,
|
|
549
|
+
config,
|
|
550
|
+
serviceFs,
|
|
551
|
+
root,
|
|
552
|
+
group,
|
|
553
|
+
resolvedEntrypoint,
|
|
554
|
+
routePrefixSource = "configured"
|
|
555
|
+
} = options;
|
|
556
|
+
const type = config.type || "web";
|
|
557
|
+
const trigger = getEffectiveServiceTrigger(config);
|
|
558
|
+
const rawEntrypoint = config.entrypoint;
|
|
559
|
+
const moduleAttrParsed = typeof rawEntrypoint === "string" ? (0, import_entrypoint.parsePyModuleAttrEntrypoint)(rawEntrypoint) : null;
|
|
560
|
+
const routingResult = resolveServiceRoutingConfig(name, config);
|
|
561
|
+
if (routingResult.error) {
|
|
562
|
+
throw new Error(routingResult.error.message);
|
|
563
|
+
}
|
|
564
|
+
const configuredRoutePrefix = routingResult.routing?.routePrefix;
|
|
565
|
+
const configuredSubdomain = routingResult.routing?.subdomain;
|
|
566
|
+
const routePrefixWasConfigured = routingResult.routing?.routePrefixConfigured ?? false;
|
|
567
|
+
const containerEntrypoint = isContainerRuntime(config) && typeof rawEntrypoint === "string" ? rawEntrypoint : void 0;
|
|
568
|
+
const containerDockerfile = containerEntrypoint && (0, import_entrypoint.isDockerfileEntrypoint)(containerEntrypoint) ? import_path.posix.normalize(containerEntrypoint) : void 0;
|
|
569
|
+
const containerImage = containerEntrypoint && !containerDockerfile ? containerEntrypoint : void 0;
|
|
570
|
+
let resolvedEntrypointPath = resolvedEntrypoint;
|
|
571
|
+
if (!containerEntrypoint && !resolvedEntrypointPath && typeof rawEntrypoint === "string") {
|
|
572
|
+
const entrypointToResolve = moduleAttrParsed ? moduleAttrParsed.filePath : rawEntrypoint;
|
|
573
|
+
const resolved = await resolveEntrypointPath({
|
|
574
|
+
fs: serviceFs,
|
|
575
|
+
serviceName: name,
|
|
576
|
+
entrypoint: entrypointToResolve
|
|
577
|
+
});
|
|
578
|
+
resolvedEntrypointPath = resolved.entrypoint;
|
|
579
|
+
}
|
|
580
|
+
if (!containerEntrypoint && typeof rawEntrypoint === "string" && !resolvedEntrypointPath) {
|
|
581
|
+
throw new Error(
|
|
582
|
+
`Failed to resolve entrypoint "${rawEntrypoint}" for service "${name}".`
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
const normalizedEntrypoint = resolvedEntrypointPath?.normalized;
|
|
586
|
+
const entrypointIsDirectory = Boolean(resolvedEntrypointPath?.isDirectory);
|
|
587
|
+
const inferredRuntime = (0, import_runtime.inferRuntime)({
|
|
588
|
+
...config,
|
|
589
|
+
entrypoint: entrypointIsDirectory ? void 0 : normalizedEntrypoint
|
|
590
|
+
});
|
|
591
|
+
let workspace = ".";
|
|
592
|
+
let resolvedEntrypointFile = entrypointIsDirectory || !normalizedEntrypoint ? void 0 : normalizedEntrypoint;
|
|
593
|
+
if (entrypointIsDirectory && normalizedEntrypoint) {
|
|
594
|
+
workspace = normalizedEntrypoint;
|
|
595
|
+
} else {
|
|
596
|
+
const inferredWorkspace = await inferWorkspaceFromNearestManifest({
|
|
597
|
+
fs: serviceFs,
|
|
598
|
+
entrypoint: resolvedEntrypointFile,
|
|
599
|
+
runtime: inferredRuntime
|
|
600
|
+
});
|
|
601
|
+
if (inferredWorkspace) {
|
|
602
|
+
workspace = inferredWorkspace;
|
|
603
|
+
if (resolvedEntrypointFile) {
|
|
604
|
+
resolvedEntrypointFile = toWorkspaceRelativeEntrypoint(
|
|
605
|
+
resolvedEntrypointFile,
|
|
606
|
+
inferredWorkspace
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
if (root) {
|
|
612
|
+
const normalizedRoot = import_path.posix.normalize(root);
|
|
613
|
+
if (normalizedRoot !== ".") {
|
|
614
|
+
workspace = workspace === "." ? normalizedRoot : import_path.posix.join(normalizedRoot, workspace);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
const topics = type === "worker" ? (0, import_build_utils.getServiceQueueTopics)({ type, topics: config.topics }) : trigger === "queue" ? config.topics : trigger === "workflow" ? ["__wkf_*"] : void 0;
|
|
618
|
+
let builderUse;
|
|
619
|
+
let builderSrc;
|
|
620
|
+
const frameworkDefinition = config.framework ? frameworksBySlug.get(config.framework) : void 0;
|
|
621
|
+
if (config.builder) {
|
|
622
|
+
builderUse = config.builder;
|
|
623
|
+
builderSrc = resolvedEntrypointFile || frameworkDefinition?.useRuntime?.src || "package.json";
|
|
624
|
+
} else if (config.framework) {
|
|
625
|
+
const isCronService = (0, import_build_utils.isScheduleTriggeredService)({ type, trigger });
|
|
626
|
+
if ((0, import_build_utils2.isNodeBackendFramework)(config.framework) && (type === "web" || isCronService)) {
|
|
627
|
+
builderUse = "@vercel/backends";
|
|
628
|
+
} else {
|
|
629
|
+
builderUse = frameworkDefinition?.useRuntime?.use || "@vercel/static-build";
|
|
630
|
+
}
|
|
631
|
+
builderSrc = resolvedEntrypointFile || frameworkDefinition?.useRuntime?.src || "package.json";
|
|
632
|
+
} else {
|
|
633
|
+
if (!inferredRuntime) {
|
|
634
|
+
throw new Error(
|
|
635
|
+
`Could not infer runtime for service "${name}" and no builder or framework were provided.`
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
if (inferredRuntime === "node") {
|
|
639
|
+
const isCronService = (0, import_build_utils.isScheduleTriggeredService)({ type, trigger });
|
|
640
|
+
builderUse = type === "web" || isCronService ? "@vercel/backends" : "@vercel/node";
|
|
641
|
+
} else {
|
|
642
|
+
builderUse = (0, import_runtime.getBuilderForRuntime)(inferredRuntime);
|
|
643
|
+
}
|
|
644
|
+
builderSrc = inferredRuntime === "container" && typeof containerEntrypoint === "string" ? containerEntrypoint : resolvedEntrypointFile;
|
|
645
|
+
}
|
|
646
|
+
const normalizedSubdomain = type === "web" && typeof configuredSubdomain === "string" ? configuredSubdomain.toLowerCase() : void 0;
|
|
647
|
+
const defaultRoutePrefix = type === "web" && normalizedSubdomain ? `/_/${name}` : void 0;
|
|
648
|
+
const routePrefix = type === "web" && (configuredRoutePrefix || defaultRoutePrefix) ? (configuredRoutePrefix || defaultRoutePrefix).startsWith("/") ? configuredRoutePrefix || defaultRoutePrefix : `/${configuredRoutePrefix || defaultRoutePrefix}` : void 0;
|
|
649
|
+
const resolvedRoutePrefixSource = type === "web" && typeof routePrefix === "string" ? routePrefixWasConfigured ? routePrefixSource : "generated" : void 0;
|
|
650
|
+
const isRoot = workspace === ".";
|
|
651
|
+
if (!isRoot) {
|
|
652
|
+
builderSrc = import_path.posix.join(workspace, builderSrc);
|
|
653
|
+
}
|
|
654
|
+
const builderConfig = { zeroConfig: true };
|
|
655
|
+
if (builderUse === "@vercel/backends") {
|
|
656
|
+
builderConfig.serviceName = name;
|
|
657
|
+
}
|
|
658
|
+
if (config.memory)
|
|
659
|
+
builderConfig.memory = config.memory;
|
|
660
|
+
if (config.maxDuration)
|
|
661
|
+
builderConfig.maxDuration = config.maxDuration;
|
|
662
|
+
if (config.includeFiles)
|
|
663
|
+
builderConfig.includeFiles = config.includeFiles;
|
|
664
|
+
if (config.excludeFiles)
|
|
665
|
+
builderConfig.excludeFiles = config.excludeFiles;
|
|
666
|
+
const isStaticBuild = import_constants.STATIC_BUILDERS.has(builderUse);
|
|
667
|
+
const runtime = isStaticBuild ? void 0 : inferredRuntime;
|
|
668
|
+
if (routePrefix) {
|
|
669
|
+
const stripped = routePrefix.startsWith("/") ? routePrefix.slice(1) : routePrefix;
|
|
670
|
+
builderConfig.routePrefix = stripped || ".";
|
|
671
|
+
}
|
|
672
|
+
if (workspace && workspace !== ".") {
|
|
673
|
+
builderConfig.workspace = workspace;
|
|
674
|
+
}
|
|
675
|
+
if (config.framework) {
|
|
676
|
+
builderConfig.framework = config.framework;
|
|
677
|
+
}
|
|
678
|
+
if (containerImage) {
|
|
679
|
+
builderConfig.handler = containerImage;
|
|
680
|
+
}
|
|
681
|
+
if (config.command !== void 0) {
|
|
682
|
+
builderConfig.command = normalizeContainerCommand(config.command);
|
|
683
|
+
}
|
|
684
|
+
if (moduleAttrParsed) {
|
|
685
|
+
builderConfig.handlerFunction = moduleAttrParsed.attrName;
|
|
686
|
+
}
|
|
687
|
+
return {
|
|
688
|
+
schema: "experimentalServices",
|
|
689
|
+
name,
|
|
690
|
+
type,
|
|
691
|
+
trigger,
|
|
692
|
+
group,
|
|
693
|
+
workspace,
|
|
694
|
+
entrypoint: containerImage ?? containerDockerfile ?? resolvedEntrypointFile,
|
|
695
|
+
routePrefix,
|
|
696
|
+
routePrefixSource: resolvedRoutePrefixSource,
|
|
697
|
+
subdomain: normalizedSubdomain,
|
|
698
|
+
framework: config.framework,
|
|
699
|
+
builder: {
|
|
700
|
+
src: builderSrc,
|
|
701
|
+
use: builderUse,
|
|
702
|
+
config: Object.keys(builderConfig).length > 0 ? builderConfig : void 0
|
|
703
|
+
},
|
|
704
|
+
runtime,
|
|
705
|
+
buildCommand: config.buildCommand,
|
|
706
|
+
installCommand: config.installCommand,
|
|
707
|
+
preDeployCommand: config.preDeployCommand,
|
|
708
|
+
schedule: config.schedule,
|
|
709
|
+
handlerFunction: moduleAttrParsed?.attrName,
|
|
710
|
+
topics,
|
|
711
|
+
env: config.env
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
async function resolveAllConfiguredServices(services, fs, routePrefixSource = "configured", options = {}) {
|
|
715
|
+
const resolved = [];
|
|
716
|
+
const errors = [];
|
|
717
|
+
const webServicesByRoutePrefix = /* @__PURE__ */ new Map();
|
|
718
|
+
for (const name of Object.keys(services)) {
|
|
719
|
+
const serviceConfig = services[name];
|
|
720
|
+
const validationError = validateServiceConfig(name, serviceConfig, options);
|
|
721
|
+
if (validationError) {
|
|
722
|
+
errors.push(validationError);
|
|
723
|
+
continue;
|
|
724
|
+
}
|
|
725
|
+
const root = serviceConfig.root;
|
|
726
|
+
const serviceFsResult = await getServiceFs(fs, name, root);
|
|
727
|
+
if (serviceFsResult.error) {
|
|
728
|
+
errors.push(serviceFsResult.error);
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
731
|
+
const serviceFs = serviceFsResult.fs;
|
|
732
|
+
let resolvedEntrypoint;
|
|
733
|
+
if (typeof serviceConfig.entrypoint === "string" && !isContainerRuntime(serviceConfig)) {
|
|
734
|
+
const moduleAttr = (0, import_entrypoint.parsePyModuleAttrEntrypoint)(serviceConfig.entrypoint);
|
|
735
|
+
const entrypointToResolve = moduleAttr?.filePath ?? serviceConfig.entrypoint;
|
|
736
|
+
const resolvedPath = await resolveEntrypointPath({
|
|
737
|
+
fs: serviceFs,
|
|
738
|
+
serviceName: name,
|
|
739
|
+
entrypoint: entrypointToResolve
|
|
740
|
+
});
|
|
741
|
+
if (resolvedPath.error) {
|
|
742
|
+
errors.push(resolvedPath.error);
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
resolvedEntrypoint = resolvedPath.entrypoint;
|
|
746
|
+
}
|
|
747
|
+
if (resolvedEntrypoint) {
|
|
748
|
+
const entrypointError = validateServiceEntrypoint(
|
|
749
|
+
name,
|
|
750
|
+
serviceConfig,
|
|
751
|
+
resolvedEntrypoint
|
|
752
|
+
);
|
|
753
|
+
if (entrypointError) {
|
|
754
|
+
errors.push(entrypointError);
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
const explicitBackendEntrypointError = validateBackendFileEntrypoint(
|
|
759
|
+
name,
|
|
760
|
+
serviceConfig,
|
|
761
|
+
resolvedEntrypoint,
|
|
762
|
+
options
|
|
763
|
+
);
|
|
764
|
+
if (explicitBackendEntrypointError) {
|
|
765
|
+
errors.push(explicitBackendEntrypointError);
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
768
|
+
let resolvedConfig = serviceConfig;
|
|
769
|
+
if (!serviceConfig.framework && resolvedEntrypoint) {
|
|
770
|
+
if (resolvedEntrypoint.isDirectory) {
|
|
771
|
+
const inferredRuntime = (0, import_runtime.inferRuntime)({
|
|
772
|
+
...serviceConfig
|
|
773
|
+
});
|
|
774
|
+
const workspace = resolvedEntrypoint.normalized;
|
|
775
|
+
const { framework, error } = await detectFrameworkFromWorkspace({
|
|
776
|
+
fs: serviceFs,
|
|
777
|
+
workspace,
|
|
778
|
+
runtime: inferredRuntime,
|
|
779
|
+
serviceName: name
|
|
780
|
+
});
|
|
781
|
+
if (error) {
|
|
782
|
+
errors.push(error);
|
|
783
|
+
continue;
|
|
784
|
+
}
|
|
785
|
+
if (!framework) {
|
|
786
|
+
errors.push({
|
|
787
|
+
code: "MISSING_SERVICE_FRAMEWORK",
|
|
788
|
+
message: `Service "${name}" uses directory entrypoint "${serviceConfig.entrypoint}" but no framework could be detected in "${workspace}". Specify "framework" explicitly or use a file entrypoint.`,
|
|
789
|
+
serviceName: name
|
|
790
|
+
});
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
793
|
+
resolvedConfig = {
|
|
794
|
+
...resolvedConfig,
|
|
795
|
+
framework
|
|
796
|
+
};
|
|
797
|
+
} else {
|
|
798
|
+
const inferredRuntime = (0, import_runtime.inferRuntime)({
|
|
799
|
+
...serviceConfig,
|
|
800
|
+
entrypoint: resolvedEntrypoint.normalized
|
|
801
|
+
});
|
|
802
|
+
if (inferredRuntime) {
|
|
803
|
+
const inferredWorkspace = await inferWorkspaceFromNearestManifest({
|
|
804
|
+
fs: serviceFs,
|
|
805
|
+
entrypoint: resolvedEntrypoint.normalized,
|
|
806
|
+
runtime: inferredRuntime
|
|
807
|
+
});
|
|
808
|
+
const workspace = inferredWorkspace ?? import_path.posix.dirname(resolvedEntrypoint.normalized);
|
|
809
|
+
const detection = await detectFrameworkFromWorkspace({
|
|
810
|
+
fs: serviceFs,
|
|
811
|
+
workspace,
|
|
812
|
+
serviceName: name,
|
|
813
|
+
runtime: inferredRuntime
|
|
814
|
+
});
|
|
815
|
+
if (!detection.error && detection.framework) {
|
|
816
|
+
resolvedConfig = {
|
|
817
|
+
...resolvedConfig,
|
|
818
|
+
framework: detection.framework
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
const backendEntrypointError = validateBackendFileEntrypoint(
|
|
825
|
+
name,
|
|
826
|
+
resolvedConfig,
|
|
827
|
+
resolvedEntrypoint,
|
|
828
|
+
options
|
|
829
|
+
);
|
|
830
|
+
if (backendEntrypointError) {
|
|
831
|
+
errors.push(backendEntrypointError);
|
|
832
|
+
continue;
|
|
833
|
+
}
|
|
834
|
+
const service = await resolveConfiguredService({
|
|
835
|
+
name,
|
|
836
|
+
config: resolvedConfig,
|
|
837
|
+
serviceFs,
|
|
838
|
+
root,
|
|
839
|
+
resolvedEntrypoint,
|
|
840
|
+
routePrefixSource
|
|
841
|
+
});
|
|
842
|
+
if (service.type === "web" && typeof service.routePrefix === "string") {
|
|
843
|
+
const normalizedRoutePrefix = (0, import_routing_utils.normalizeRoutePrefix)(service.routePrefix);
|
|
844
|
+
const existingServiceName = webServicesByRoutePrefix.get(
|
|
845
|
+
normalizedRoutePrefix
|
|
846
|
+
);
|
|
847
|
+
if (existingServiceName) {
|
|
848
|
+
errors.push({
|
|
849
|
+
code: "DUPLICATE_ROUTE_PREFIX",
|
|
850
|
+
message: `Web services "${existingServiceName}" and "${name}" cannot share routePrefix "${normalizedRoutePrefix}".`,
|
|
851
|
+
serviceName: name
|
|
852
|
+
});
|
|
853
|
+
continue;
|
|
854
|
+
}
|
|
855
|
+
webServicesByRoutePrefix.set(normalizedRoutePrefix, name);
|
|
856
|
+
}
|
|
857
|
+
resolved.push(service);
|
|
858
|
+
}
|
|
859
|
+
const servicesByName = new Map(resolved.map((s) => [s.name, s]));
|
|
860
|
+
for (const service of resolved) {
|
|
861
|
+
if (!service.env)
|
|
862
|
+
continue;
|
|
863
|
+
validateEnvRefs(service.env, service.name, servicesByName, errors);
|
|
864
|
+
}
|
|
865
|
+
return { services: resolved, errors };
|
|
866
|
+
}
|
|
867
|
+
function validateEnvRefs(env, serviceName, servicesByName, errors) {
|
|
868
|
+
const pathPrefix = `Service "${serviceName}" env`;
|
|
869
|
+
for (const [envVarName, envVar] of Object.entries(env)) {
|
|
870
|
+
if (envVar.type !== "service-ref")
|
|
871
|
+
continue;
|
|
872
|
+
const refName = envVar.service;
|
|
873
|
+
const target = servicesByName.get(refName);
|
|
874
|
+
if (!target) {
|
|
875
|
+
errors.push({
|
|
876
|
+
code: "UNKNOWN_SERVICE_REF",
|
|
877
|
+
message: `${pathPrefix}["${envVarName}"] references unknown service "${refName}".`,
|
|
878
|
+
serviceName
|
|
879
|
+
});
|
|
880
|
+
continue;
|
|
881
|
+
}
|
|
882
|
+
if (target.type !== "web") {
|
|
883
|
+
errors.push({
|
|
884
|
+
code: "INVALID_SERVICE_REF_TYPE",
|
|
885
|
+
message: `${pathPrefix}["${envVarName}"] references service "${refName}" which is a ${target.type} service and has no URL. Only web services can be referenced.`,
|
|
886
|
+
serviceName
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
892
|
+
0 && (module.exports = {
|
|
893
|
+
detectFrameworkFromWorkspace,
|
|
894
|
+
getServiceFs,
|
|
895
|
+
inferWorkspaceFromNearestManifest,
|
|
896
|
+
resolveAllConfiguredServices,
|
|
897
|
+
resolveConfiguredService,
|
|
898
|
+
resolveEntrypointPath,
|
|
899
|
+
validateServiceConfig,
|
|
900
|
+
validateServiceEntrypoint
|
|
901
|
+
});
|