@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.
@@ -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
+ });