@vercel/fs-detectors 6.3.0 → 6.5.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.
@@ -1,11 +1,12 @@
1
1
  import type { DetectorFilesystem } from '../detectors/filesystem';
2
- import type { ExperimentalServices, ServiceDetectionError } from './types';
2
+ import type { ExperimentalServices, ServiceDetectionError, ServiceDetectionWarning } from './types';
3
3
  export interface AutoDetectOptions {
4
4
  fs: DetectorFilesystem;
5
5
  }
6
6
  export interface AutoDetectResult {
7
7
  services: ExperimentalServices | null;
8
8
  errors: ServiceDetectionError[];
9
+ warnings: ServiceDetectionWarning[];
9
10
  }
10
11
  /**
11
12
  * Auto-detect services when services are not configured.
@@ -23,14 +23,12 @@ __export(auto_detect_exports, {
23
23
  module.exports = __toCommonJS(auto_detect_exports);
24
24
  var import_detect_framework = require("../detect-framework");
25
25
  var import_frameworks = require("@vercel/frameworks");
26
+ var import_utils = require("./utils");
26
27
  const FRONTEND_DIR = "frontend";
27
28
  const APPS_WEB_DIR = "apps/web";
28
29
  const BACKEND_DIR = "backend";
29
30
  const SERVICES_DIR = "services";
30
31
  const FRONTEND_LOCATIONS = [FRONTEND_DIR, APPS_WEB_DIR];
31
- const DETECTION_FRAMEWORKS = import_frameworks.frameworkList.filter(
32
- (framework) => !framework.experimental || framework.runtimeFramework
33
- );
34
32
  async function autoDetectServices(options) {
35
33
  const { fs } = options;
36
34
  const rootFrameworks = await (0, import_detect_framework.detectFrameworks)({
@@ -41,6 +39,7 @@ async function autoDetectServices(options) {
41
39
  const frameworkNames = rootFrameworks.map((f) => f.name).join(", ");
42
40
  return {
43
41
  services: null,
42
+ warnings: [],
44
43
  errors: [
45
44
  {
46
45
  code: "MULTIPLE_FRAMEWORKS_ROOT",
@@ -66,6 +65,7 @@ async function autoDetectServices(options) {
66
65
  const frameworkNames = frontendFrameworks.map((f) => f.name).join(", ");
67
66
  return {
68
67
  services: null,
68
+ warnings: [],
69
69
  errors: [
70
70
  {
71
71
  code: "MULTIPLE_FRAMEWORKS_SERVICE",
@@ -84,6 +84,7 @@ async function autoDetectServices(options) {
84
84
  }
85
85
  return {
86
86
  services: null,
87
+ warnings: [],
87
88
  errors: [
88
89
  {
89
90
  code: "NO_SERVICES_CONFIGURED",
@@ -102,18 +103,21 @@ async function detectServicesAtRoot(fs, rootFramework) {
102
103
  if (backendResult.error) {
103
104
  return {
104
105
  services: null,
106
+ warnings: [],
105
107
  errors: [backendResult.error]
106
108
  };
107
109
  }
108
110
  if (Object.keys(backendResult.services).length === 0) {
109
111
  return {
110
112
  services: null,
113
+ warnings: [],
111
114
  errors: []
112
115
  };
113
116
  }
114
117
  Object.assign(services, backendResult.services);
115
118
  return {
116
119
  services,
120
+ warnings: [],
117
121
  errors: []
118
122
  };
119
123
  }
@@ -129,12 +133,14 @@ async function detectServicesFrontendSubdir(fs, frontendFramework, frontendLocat
129
133
  if (backendResult.error) {
130
134
  return {
131
135
  services: null,
136
+ warnings: [],
132
137
  errors: [backendResult.error]
133
138
  };
134
139
  }
135
140
  if (Object.keys(backendResult.services).length === 0) {
136
141
  return {
137
142
  services: null,
143
+ warnings: [],
138
144
  errors: [
139
145
  {
140
146
  code: "NO_BACKEND_SERVICES",
@@ -146,6 +152,7 @@ async function detectServicesFrontendSubdir(fs, frontendFramework, frontendLocat
146
152
  Object.assign(services, backendResult.services);
147
153
  return {
148
154
  services,
155
+ warnings: [],
149
156
  errors: []
150
157
  };
151
158
  }
@@ -209,7 +216,7 @@ async function detectServiceInDir(fs, dirPath, serviceName) {
209
216
  const serviceFs = fs.chdir(dirPath);
210
217
  const frameworks = await (0, import_detect_framework.detectFrameworks)({
211
218
  fs: serviceFs,
212
- frameworkList: DETECTION_FRAMEWORKS,
219
+ frameworkList: import_utils.DETECTION_FRAMEWORKS,
213
220
  useExperimentalFrameworks: true
214
221
  });
215
222
  if (frameworks.length > 1) {
@@ -0,0 +1,22 @@
1
+ import type { DetectorFilesystem } from '../detectors/filesystem';
2
+ import type { ExperimentalServices, ServiceDetectionError, ServiceDetectionWarning } from './types';
3
+ export interface ProcfileDetectResult {
4
+ services: ExperimentalServices | null;
5
+ errors: ServiceDetectionError[];
6
+ warnings: ServiceDetectionWarning[];
7
+ }
8
+ /**
9
+ * Detect service configurations from a Procfile.
10
+ *
11
+ * We infer runtime and entrypoint from the start command where possible,
12
+ * then run framework detection from the project root to fill in the framework.
13
+ *
14
+ * Process type mapping:
15
+ * - `web`: web service
16
+ * - `release`: embeded into one of web service's buildCommand
17
+ * - `worker`-like process name: try to infer entrypoint for support Python worker or produce a hint
18
+ * - everything else is tried to be inferred as `web` or produces a hint
19
+ */
20
+ export declare function detectProcfileServices(options: {
21
+ fs: DetectorFilesystem;
22
+ }): Promise<ProcfileDetectResult>;
@@ -0,0 +1,248 @@
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 detect_procfile_exports = {};
20
+ __export(detect_procfile_exports, {
21
+ detectProcfileServices: () => detectProcfileServices
22
+ });
23
+ module.exports = __toCommonJS(detect_procfile_exports);
24
+ var import_detect_framework = require("../detect-framework");
25
+ var import_utils = require("./utils");
26
+ const PROCFILE = "Procfile";
27
+ const PROCFILE_LINE_RE = /^\s*([A-Za-z_][A-Za-z0-9_-]*):\s*(.+)/;
28
+ const PY_IDENT = "[A-Za-z_][A-Za-z0-9_]*";
29
+ const PY_MODULE_RE = new RegExp(
30
+ `^${PY_IDENT}(?:\\.${PY_IDENT})*(?::${PY_IDENT})?$`
31
+ );
32
+ const SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([
33
+ ".js",
34
+ ".cjs",
35
+ ".mjs",
36
+ ".ts",
37
+ ".cts",
38
+ ".mts",
39
+ ".py",
40
+ ".go",
41
+ ".rs",
42
+ ".rb",
43
+ ".ru"
44
+ ]);
45
+ const SUPPORTED_WORKER_COMMANDS = /* @__PURE__ */ new Set(["celery", "dramatiq"]);
46
+ async function detectProcfileServices(options) {
47
+ const { fs } = options;
48
+ const raw = await readProcfile(fs);
49
+ if (raw.warning) {
50
+ return { services: null, errors: [], warnings: [raw.warning] };
51
+ }
52
+ if (!raw.content) {
53
+ return { services: null, errors: [], warnings: [] };
54
+ }
55
+ const entries = parseProcfile(raw.content);
56
+ if (entries.length === 0) {
57
+ return { services: null, errors: [], warnings: [] };
58
+ }
59
+ const services = {};
60
+ const errors = [];
61
+ const warnings = [];
62
+ let releaseCommand;
63
+ const serviceEntries = [];
64
+ for (const entry of entries) {
65
+ if (entry.processType === "release") {
66
+ releaseCommand = entry.command;
67
+ } else {
68
+ serviceEntries.push(entry);
69
+ }
70
+ }
71
+ if (serviceEntries.length === 0 && releaseCommand) {
72
+ warnings.push({
73
+ code: "PROCFILE_RELEASE_ONLY",
74
+ message: `Found only a release process in Procfile (command: "${releaseCommand}"). The release command can be used as a build step. You can add it as part of a web service if you add it to "buildCommand".`
75
+ });
76
+ return { services: null, errors: [], warnings };
77
+ }
78
+ if (serviceEntries.length === 0) {
79
+ return { services: null, errors: [], warnings: [] };
80
+ }
81
+ const frameworks = await (0, import_detect_framework.detectFrameworks)({
82
+ fs,
83
+ frameworkList: import_utils.DETECTION_FRAMEWORKS,
84
+ useExperimentalFrameworks: true
85
+ });
86
+ if (frameworks.length > 1) {
87
+ const names = frameworks.map((f) => f.name).join(", ");
88
+ return {
89
+ services: null,
90
+ errors: [
91
+ {
92
+ code: "MULTIPLE_FRAMEWORKS_SERVICE",
93
+ message: `Multiple frameworks detected: ${names}. Use explicit experimentalServices config.`
94
+ }
95
+ ],
96
+ warnings
97
+ };
98
+ }
99
+ const detectedFramework = frameworks.length === 1 ? frameworks[0] : null;
100
+ const serviceNames = /* @__PURE__ */ new Set();
101
+ for (const entry of serviceEntries) {
102
+ const { processType, command } = entry;
103
+ const tokens = command.split(/\s+/).filter(Boolean);
104
+ const entrypoint = await extractEntrypoint(tokens, fs);
105
+ const isWorkerLikeProcess = processType.includes("worker") || hasSupportedWorkerCommand(tokens);
106
+ if (serviceNames.has(processType)) {
107
+ errors.push({
108
+ code: "DUPLICATE_SERVICE",
109
+ message: `Duplicate process type "${processType}" in Procfile.`,
110
+ serviceName: processType
111
+ });
112
+ continue;
113
+ }
114
+ serviceNames.add(processType);
115
+ if (isWorkerLikeProcess) {
116
+ if (hasSupportedWorkerCommand(tokens) && entrypoint?.endsWith(".py")) {
117
+ services[processType] = {
118
+ type: "worker",
119
+ entrypoint,
120
+ runtime: "python"
121
+ };
122
+ } else {
123
+ emitWorkerHint(processType, command, entrypoint, warnings);
124
+ }
125
+ continue;
126
+ }
127
+ if (!detectedFramework && !entrypoint) {
128
+ warnings.push({
129
+ code: "SERVICE_SKIPPED",
130
+ message: `Skipped Procfile process "${processType}": no framework detected and could not infer entrypoint from command "${command}". Configure it manually in experimentalServices.`
131
+ });
132
+ continue;
133
+ }
134
+ const serviceConfig = { type: "web" };
135
+ if (detectedFramework) {
136
+ serviceConfig.framework = detectedFramework.slug ?? void 0;
137
+ }
138
+ serviceConfig.entrypoint = entrypoint ?? ".";
139
+ services[processType] = serviceConfig;
140
+ }
141
+ if (errors.length > 0) {
142
+ return { services: null, errors, warnings };
143
+ }
144
+ if (Object.keys(services).length === 0) {
145
+ return { services: null, errors: [], warnings };
146
+ }
147
+ if (releaseCommand && services.web) {
148
+ services.web.buildCommand = releaseCommand;
149
+ } else if (releaseCommand) {
150
+ const firstService = Object.values(services)[0];
151
+ if (firstService) {
152
+ firstService.buildCommand = releaseCommand;
153
+ }
154
+ }
155
+ warnings.push(...(0, import_utils.assignRoutePrefixes)(services));
156
+ return { services, errors: [], warnings };
157
+ }
158
+ function parseProcfile(content) {
159
+ const entries = [];
160
+ for (const rawLine of content.split("\n")) {
161
+ const match = rawLine.match(PROCFILE_LINE_RE);
162
+ if (match) {
163
+ entries.push({ processType: match[1], command: match[2].trim() });
164
+ }
165
+ }
166
+ return entries;
167
+ }
168
+ async function extractEntrypoint(tokens, fs) {
169
+ let firstModulePath;
170
+ let lastFilePath;
171
+ for (const token of tokens) {
172
+ if (!firstModulePath && PY_MODULE_RE.test(token)) {
173
+ const resolved = await resolvePythonModule(token, fs);
174
+ if (resolved)
175
+ firstModulePath = resolved;
176
+ }
177
+ if (hasSupportedExtension(token)) {
178
+ lastFilePath = token;
179
+ }
180
+ }
181
+ return firstModulePath ?? lastFilePath;
182
+ }
183
+ async function resolvePythonModule(spec, fs) {
184
+ const [modulePart] = spec.split(":");
185
+ const filePath = `${modulePart.replace(/\./g, "/")}.py`;
186
+ const initPath = `${modulePart.replace(/\./g, "/")}/__init__.py`;
187
+ try {
188
+ if (await fs.isFile(filePath))
189
+ return filePath;
190
+ if (await fs.isFile(initPath))
191
+ return initPath;
192
+ } catch {
193
+ }
194
+ return void 0;
195
+ }
196
+ function hasSupportedExtension(token) {
197
+ const dot = token.lastIndexOf(".");
198
+ return dot > 0 && SUPPORTED_EXTENSIONS.has(token.slice(dot));
199
+ }
200
+ function hasSupportedWorkerCommand(tokens) {
201
+ return tokens.some((t) => SUPPORTED_WORKER_COMMANDS.has(baseCommand(t)));
202
+ }
203
+ function emitWorkerHint(processType, command, entrypoint, warnings) {
204
+ const hint = {
205
+ type: "worker",
206
+ entrypoint: entrypoint ?? "<path-to-handler>",
207
+ runtime: "python"
208
+ };
209
+ if (entrypoint?.endsWith(".py")) {
210
+ warnings.push({
211
+ code: "PROCFILE_WORKER_HINT",
212
+ message: `Found Procfile worker process "${processType}". Python workers that use Celery, Dramatiq and Django tasks are supported. You can add the following to define this worker:
213
+ "${processType}": ${JSON.stringify(hint, null, 2)}`
214
+ });
215
+ return;
216
+ }
217
+ warnings.push({
218
+ code: "PROCFILE_WORKER_HINT",
219
+ message: `Found Procfile worker process "${processType}" (command: "${command}"). Could not determine runtime. Only Python workers are currently supported.`
220
+ });
221
+ }
222
+ function baseCommand(token) {
223
+ if (!token)
224
+ return "";
225
+ const parts = token.split("/");
226
+ return parts[parts.length - 1];
227
+ }
228
+ async function readProcfile(fs) {
229
+ try {
230
+ const exists = await fs.isFile(PROCFILE);
231
+ if (!exists)
232
+ return { content: null };
233
+ const buf = await fs.readFile(PROCFILE);
234
+ return { content: buf.toString("utf-8") };
235
+ } catch (err) {
236
+ return {
237
+ content: null,
238
+ warning: {
239
+ code: "PROCFILE_READ_ERROR",
240
+ message: `Failed to read ${PROCFILE}: ${err instanceof Error ? err.message : String(err)}`
241
+ }
242
+ };
243
+ }
244
+ }
245
+ // Annotate the CommonJS export names for ESM import in node:
246
+ 0 && (module.exports = {
247
+ detectProcfileServices
248
+ });
@@ -33,7 +33,6 @@ __export(detect_railway_exports, {
33
33
  module.exports = __toCommonJS(detect_railway_exports);
34
34
  var import_path = require("path");
35
35
  var import_smol_toml = __toESM(require("smol-toml"));
36
- var import_frameworks = require("@vercel/frameworks");
37
36
  var import_detect_framework = require("../detect-framework");
38
37
  var import_utils = require("./utils");
39
38
  const RAILWAY_JSON = "railway.json";
@@ -56,9 +55,6 @@ const SKIP_DIRS = /* @__PURE__ */ new Set([
56
55
  "venv",
57
56
  "CVS"
58
57
  ]);
59
- const DETECTION_FRAMEWORKS = import_frameworks.frameworkList.filter(
60
- (framework) => !framework.experimental || framework.runtimeFramework
61
- );
62
58
  async function detectRailwayServices(options) {
63
59
  const { fs } = options;
64
60
  const { configs, warnings } = await findRailwayConfigs(fs);
@@ -73,7 +69,7 @@ async function detectRailwayServices(options) {
73
69
  const dirLabel = cf.dirPath === "." ? "root" : cf.dirPath;
74
70
  const frameworks = await (0, import_detect_framework.detectFrameworks)({
75
71
  fs: serviceFs,
76
- frameworkList: DETECTION_FRAMEWORKS,
72
+ frameworkList: import_utils.DETECTION_FRAMEWORKS,
77
73
  useExperimentalFrameworks: true
78
74
  });
79
75
  if (cf.config.deploy?.cronSchedule) {
@@ -128,12 +124,12 @@ async function detectRailwayServices(options) {
128
124
  if (cf.dirPath !== ".") {
129
125
  serviceConfig.entrypoint = cf.dirPath;
130
126
  }
131
- const buildCommand = combineBuildCommand(
132
- cf.config.build?.buildCommand,
133
- cf.config.deploy?.preDeployCommand
134
- );
135
- if (buildCommand) {
136
- serviceConfig.buildCommand = buildCommand;
127
+ if (cf.config.build?.buildCommand) {
128
+ serviceConfig.buildCommand = cf.config.build.buildCommand;
129
+ }
130
+ const railwayPreDeploy = cf.config.deploy?.preDeployCommand;
131
+ if (railwayPreDeploy) {
132
+ serviceConfig.preDeployCommand = Array.isArray(railwayPreDeploy) ? railwayPreDeploy.join(" && ") : railwayPreDeploy;
137
133
  }
138
134
  services[serviceName] = serviceConfig;
139
135
  }
@@ -144,7 +140,7 @@ async function detectRailwayServices(options) {
144
140
  if (serviceNames.length === 0) {
145
141
  return { services: null, errors: [], warnings };
146
142
  }
147
- warnings.push(...assignRoutePrefixes(services));
143
+ warnings.push(...(0, import_utils.assignRoutePrefixes)(services));
148
144
  return { services, errors: [], warnings };
149
145
  }
150
146
  async function findRailwayConfigs(fs, dirPath = ".", depth = 0) {
@@ -230,41 +226,6 @@ function deriveServiceName(dirPath) {
230
226
  const segments = dirPath.split("/");
231
227
  return segments[segments.length - 1];
232
228
  }
233
- function combineBuildCommand(buildCommand, preDeployCommand) {
234
- const preDeploy = Array.isArray(preDeployCommand) ? preDeployCommand.join(" && ") : preDeployCommand;
235
- if (preDeploy && buildCommand) {
236
- return `${buildCommand} && ${preDeploy}`;
237
- } else if (preDeploy) {
238
- return preDeploy;
239
- } else {
240
- return buildCommand;
241
- }
242
- }
243
- function assignRoutePrefixes(services) {
244
- const warnings = [];
245
- const names = Object.keys(services);
246
- if (names.length === 1) {
247
- services[names[0]].routePrefix = "/";
248
- return warnings;
249
- }
250
- const frontendNames = names.filter(
251
- (name) => (0, import_utils.isFrontendFramework)(services[name].framework)
252
- );
253
- let rootName = null;
254
- if (frontendNames.length === 1) {
255
- rootName = frontendNames[0];
256
- } else if (frontendNames.length > 1) {
257
- rootName = frontendNames.find((n) => n === "frontend" || n === "web") ?? frontendNames.sort()[0];
258
- warnings.push({
259
- code: "MULTIPLE_FRONTENDS",
260
- message: `Multiple frontend services detected (${frontendNames.join(", ")}). "${rootName}" was assigned routePrefix "/". Adjust manually if a different service should be the root.`
261
- });
262
- }
263
- for (const name of names) {
264
- services[name].routePrefix = name === rootName ? "/" : `/_/${name}`;
265
- }
266
- return warnings;
267
- }
268
229
  // Annotate the CommonJS export names for ESM import in node:
269
230
  0 && (module.exports = {
270
231
  detectRailwayServices
@@ -0,0 +1,13 @@
1
+ import type { DetectorFilesystem } from '../detectors/filesystem';
2
+ import type { ExperimentalServices, ServiceDetectionError, ServiceDetectionWarning } from './types';
3
+ export interface RenderDetectResult {
4
+ services: ExperimentalServices | null;
5
+ errors: ServiceDetectionError[];
6
+ warnings: ServiceDetectionWarning[];
7
+ }
8
+ /**
9
+ * Detect Render service configurations from render.yaml.
10
+ */
11
+ export declare function detectRenderServices(options: {
12
+ fs: DetectorFilesystem;
13
+ }): Promise<RenderDetectResult>;
@@ -0,0 +1,222 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+ var detect_render_exports = {};
30
+ __export(detect_render_exports, {
31
+ detectRenderServices: () => detectRenderServices
32
+ });
33
+ module.exports = __toCommonJS(detect_render_exports);
34
+ var import_js_yaml = __toESM(require("js-yaml"));
35
+ var import_detect_framework = require("../detect-framework");
36
+ var import_types = require("./types");
37
+ var import_utils = require("./utils");
38
+ const RENDER_YAML = "render.yaml";
39
+ const SERVICE_TYPE_MAP = {
40
+ web: "web",
41
+ static: "web"
42
+ };
43
+ async function detectRenderServices(options) {
44
+ const { fs } = options;
45
+ const raw = await readRenderYaml(fs);
46
+ if (raw.warning) {
47
+ return { services: null, errors: [], warnings: [raw.warning] };
48
+ } else if (!raw.content) {
49
+ return { services: null, errors: [], warnings: [] };
50
+ }
51
+ const parsed = tryParseRenderConfig(raw.content);
52
+ if (parsed.warning) {
53
+ return { services: null, errors: [], warnings: [parsed.warning] };
54
+ } else if (!parsed.config) {
55
+ return { services: null, errors: [], warnings: [] };
56
+ }
57
+ const renderServices = parsed.config.services;
58
+ if (!Array.isArray(renderServices) || renderServices.length === 0) {
59
+ return { services: null, errors: [], warnings: [] };
60
+ }
61
+ const services = {};
62
+ const serviceNames = /* @__PURE__ */ new Set();
63
+ const errors = [];
64
+ const warnings = [];
65
+ for (const rs of renderServices) {
66
+ const serviceType = rs.type;
67
+ if (serviceType === "cron") {
68
+ const name = rs.name ?? "unnamed";
69
+ const schedule = rs.schedule;
70
+ const runtime = rs.runtime && rs.runtime in import_types.RUNTIME_BUILDERS ? rs.runtime : void 0;
71
+ const hint = {
72
+ type: "cron",
73
+ ...schedule ? { schedule } : {},
74
+ entrypoint: "<path-to-handler>",
75
+ ...runtime ? { runtime } : {}
76
+ };
77
+ warnings.push({
78
+ code: "RENDER_CRON_HINT",
79
+ message: `Found Render cron service "${name}"` + (schedule ? ` (schedule: "${schedule}")` : "") + `. Vercel crons work with a file entrypoint. You can add the following to define this cron service:
80
+ "${name}": ${JSON.stringify(hint, null, 2)}`
81
+ });
82
+ continue;
83
+ }
84
+ if (serviceType === "worker") {
85
+ const name = rs.name ?? "unnamed";
86
+ const runtime = rs.runtime ?? "unknown";
87
+ if (runtime === "python") {
88
+ const hint = {
89
+ type: "worker",
90
+ entrypoint: "<path-to-celery-app>",
91
+ runtime: "python"
92
+ };
93
+ warnings.push({
94
+ code: "RENDER_WORKER_HINT",
95
+ message: `Found Render worker service "${name}". Python workers using Celery are supported. You can add the following to define this worker:
96
+ "${name}": ${JSON.stringify(hint, null, 2)}`
97
+ });
98
+ } else {
99
+ warnings.push({
100
+ code: "RENDER_WORKER_HINT",
101
+ message: `Found Render worker service "${name}" with runtime "${runtime}". Only Python workers are currently supported.`
102
+ });
103
+ }
104
+ continue;
105
+ }
106
+ if (serviceType === "pserv") {
107
+ const name = rs.name ?? "unnamed";
108
+ const hint = {
109
+ entrypoint: rs.rootDir ?? "<path-to-entrypoint>",
110
+ routePrefix: `/_/${name}`
111
+ };
112
+ warnings.push({
113
+ code: "RENDER_PSERV_HINT",
114
+ message: `Found Render private service "${name}". Private services are not yet supported. If you'd like to deploy it as a regular web service, you can add the following:
115
+ "${name}": ${JSON.stringify(hint, null, 2)}`
116
+ });
117
+ continue;
118
+ }
119
+ if (!serviceType || !(serviceType in SERVICE_TYPE_MAP)) {
120
+ continue;
121
+ }
122
+ const serviceName = rs.name;
123
+ if (!serviceName) {
124
+ warnings.push({
125
+ code: "RENDER_CONFIG_ERROR",
126
+ message: "Skipped a Render service with no name. Each service in render.yaml must have a name."
127
+ });
128
+ continue;
129
+ }
130
+ if (serviceNames.has(serviceName)) {
131
+ errors.push({
132
+ code: "DUPLICATE_SERVICE",
133
+ message: `Duplicate service name "${serviceName}" in render.yaml.`,
134
+ serviceName
135
+ });
136
+ continue;
137
+ }
138
+ serviceNames.add(serviceName);
139
+ const rootDir = rs.rootDir || ".";
140
+ const serviceFs = rootDir === "." ? fs : fs.chdir(rootDir);
141
+ const frameworks = await (0, import_detect_framework.detectFrameworks)({
142
+ fs: serviceFs,
143
+ frameworkList: import_utils.DETECTION_FRAMEWORKS,
144
+ useExperimentalFrameworks: true
145
+ });
146
+ if (frameworks.length === 0) {
147
+ warnings.push({
148
+ code: "SERVICE_SKIPPED",
149
+ message: `Skipped Render service "${serviceName}": no framework detected. Configure it manually in experimentalServices.`
150
+ });
151
+ continue;
152
+ }
153
+ if (frameworks.length > 1) {
154
+ const names = frameworks.map((f) => f.name).join(", ");
155
+ errors.push({
156
+ code: "MULTIPLE_FRAMEWORKS_SERVICE",
157
+ message: `Multiple frameworks detected for Render service "${serviceName}": ${names}. Use explicit experimentalServices config.`,
158
+ serviceName
159
+ });
160
+ continue;
161
+ }
162
+ const framework = frameworks[0];
163
+ const vercelType = SERVICE_TYPE_MAP[serviceType];
164
+ const serviceConfig = {};
165
+ serviceConfig.type = vercelType;
166
+ serviceConfig.framework = framework.slug ?? void 0;
167
+ if (rootDir !== ".") {
168
+ serviceConfig.entrypoint = rootDir;
169
+ }
170
+ const buildCommand = (0, import_utils.combineBuildCommand)(
171
+ rs.buildCommand,
172
+ rs.preDeployCommand
173
+ );
174
+ if (buildCommand) {
175
+ serviceConfig.buildCommand = buildCommand;
176
+ }
177
+ services[serviceName] = serviceConfig;
178
+ }
179
+ if (errors.length > 0) {
180
+ return { services: null, errors, warnings };
181
+ }
182
+ if (Object.keys(services).length === 0) {
183
+ return { services: null, errors: [], warnings };
184
+ }
185
+ warnings.push(...(0, import_utils.assignRoutePrefixes)(services));
186
+ return { services, errors: [], warnings };
187
+ }
188
+ async function readRenderYaml(fs) {
189
+ try {
190
+ const exists = await fs.isFile(RENDER_YAML);
191
+ if (!exists)
192
+ return { content: null };
193
+ const buf = await fs.readFile(RENDER_YAML);
194
+ return { content: buf.toString("utf-8") };
195
+ } catch (err) {
196
+ return {
197
+ content: null,
198
+ warning: {
199
+ code: "RENDER_CONFIG_ERROR",
200
+ message: `Failed to read ${RENDER_YAML}: ${err instanceof Error ? err.message : String(err)}`
201
+ }
202
+ };
203
+ }
204
+ }
205
+ function tryParseRenderConfig(content) {
206
+ try {
207
+ const config = import_js_yaml.default.load(content);
208
+ return { config };
209
+ } catch (err) {
210
+ return {
211
+ config: null,
212
+ warning: {
213
+ code: "RENDER_PARSE_ERROR",
214
+ message: `Failed to parse ${RENDER_YAML}: ${err instanceof Error ? err.message : String(err)}`
215
+ }
216
+ };
217
+ }
218
+ }
219
+ // Annotate the CommonJS export names for ESM import in node:
220
+ 0 && (module.exports = {
221
+ detectRenderServices
222
+ });
@@ -1,4 +1,4 @@
1
- import { type DetectServicesOptions, type DetectServicesResult, type Service, type ServicesRoutes } from './types';
1
+ import type { DetectServicesOptions, DetectServicesResult, Service, ServicesRoutes } from './types';
2
2
  /**
3
3
  * Detect and resolve services within a project.
4
4
  *
@@ -28,6 +28,8 @@ var import_utils = require("./utils");
28
28
  var import_resolve = require("./resolve");
29
29
  var import_auto_detect = require("./auto-detect");
30
30
  var import_detect_railway = require("./detect-railway");
31
+ var import_detect_render = require("./detect-render");
32
+ var import_detect_procfile = require("./detect-procfile");
31
33
  const PREVIEW_DOMAIN_MISSING = [
32
34
  { type: "host", value: { suf: ".vercel.app" } },
33
35
  { type: "host", value: { suf: ".vercel.dev" } }
@@ -64,6 +66,9 @@ function toInferredLayoutConfig(services) {
64
66
  const inferredConfig = {};
65
67
  for (const [name, service] of Object.entries(services)) {
66
68
  const serviceConfig = {};
69
+ if (service.type) {
70
+ serviceConfig.type = service.type;
71
+ }
67
72
  if (typeof service.entrypoint === "string") {
68
73
  serviceConfig.entrypoint = service.entrypoint;
69
74
  }
@@ -76,6 +81,9 @@ function toInferredLayoutConfig(services) {
76
81
  if (typeof service.buildCommand === "string") {
77
82
  serviceConfig.buildCommand = service.buildCommand;
78
83
  }
84
+ if (typeof service.runtime === "string") {
85
+ serviceConfig.runtime = service.runtime;
86
+ }
79
87
  inferredConfig[name] = serviceConfig;
80
88
  }
81
89
  return inferredConfig;
@@ -98,76 +106,17 @@ async function detectServices(options) {
98
106
  const configuredServices = hasNonEmptyPublicServicesConfig ? vercelConfig.services : vercelConfig?.experimentalServices;
99
107
  const hasConfiguredServices = configuredServices && Object.keys(configuredServices).length > 0;
100
108
  if (!hasConfiguredServices) {
101
- const railwayResult = await (0, import_detect_railway.detectRailwayServices)({ fs: scopedFs });
102
- if (railwayResult.errors.length > 0) {
103
- return withResolvedResult({
104
- services: [],
105
- source: "auto-detected",
106
- useImplicitEnvInjection: true,
107
- routes: emptyRoutes(),
108
- errors: railwayResult.errors,
109
- warnings: railwayResult.warnings
110
- });
111
- }
112
- if (railwayResult.services) {
113
- const result2 = await (0, import_resolve.resolveAllConfiguredServices)(
114
- railwayResult.services,
115
- scopedFs,
116
- "generated"
117
- );
118
- const inferred = result2.errors.length === 0 && result2.services.length > 0 ? {
119
- source: "railway",
120
- config: toInferredLayoutConfig(railwayResult.services),
121
- services: result2.services,
122
- warnings: railwayResult.warnings
123
- } : null;
124
- return withResolvedResult(
125
- {
126
- services: [],
127
- source: "auto-detected",
128
- useImplicitEnvInjection: true,
129
- routes: emptyRoutes(),
130
- errors: result2.errors,
131
- warnings: railwayResult.warnings
132
- },
133
- inferred
134
- );
135
- }
136
- const autoResult = await (0, import_auto_detect.autoDetectServices)({ fs: scopedFs });
137
- if (autoResult.services && autoResult.errors.length === 0) {
138
- const result2 = await (0, import_resolve.resolveAllConfiguredServices)(
139
- autoResult.services,
140
- scopedFs,
141
- "generated"
142
- );
143
- const routes2 = generateServicesRoutes(result2.services);
144
- const resolved = {
145
- services: result2.services,
146
- source: "auto-detected",
147
- useImplicitEnvInjection: true,
148
- routes: routes2,
149
- errors: result2.errors,
150
- warnings: []
151
- };
152
- const rootWebFrameworkServices = result2.services.filter(
153
- (service) => service.type === "web" && service.routePrefix === "/" && typeof service.framework === "string"
154
- );
155
- const inferred = result2.errors.length === 0 && rootWebFrameworkServices.length === 1 && result2.services.length > 1 ? {
156
- source: "layout",
157
- config: toInferredLayoutConfig(autoResult.services),
158
- services: result2.services,
159
- warnings: []
160
- } : null;
161
- return withResolvedResult(resolved, inferred);
162
- } else if (autoResult.errors.length > 0) {
163
- return withResolvedResult({
164
- services: [],
165
- source: "auto-detected",
166
- useImplicitEnvInjection: true,
167
- routes: emptyRoutes(),
168
- errors: autoResult.errors,
169
- warnings: []
170
- });
109
+ const detectors = [
110
+ { detect: import_detect_railway.detectRailwayServices, source: "railway" },
111
+ { detect: import_detect_render.detectRenderServices, source: "render" },
112
+ { detect: import_detect_procfile.detectProcfileServices, source: "procfile" },
113
+ { detect: import_auto_detect.autoDetectServices, source: "layout" }
114
+ ];
115
+ for (const { detect, source } of detectors) {
116
+ const detectResult = await detect({ fs: scopedFs });
117
+ const match = await tryResolveInferred(detectResult, source, scopedFs);
118
+ if (match)
119
+ return match;
171
120
  }
172
121
  return withResolvedResult({
173
122
  services: [],
@@ -206,6 +155,66 @@ async function detectServices(options) {
206
155
  warnings: []
207
156
  });
208
157
  }
158
+ async function tryResolveInferred(detectResult, source, scopedFs) {
159
+ if (detectResult.errors.length > 0) {
160
+ return withResolvedResult({
161
+ services: [],
162
+ source: "auto-detected",
163
+ useImplicitEnvInjection: true,
164
+ routes: emptyRoutes(),
165
+ errors: detectResult.errors,
166
+ warnings: detectResult.warnings
167
+ });
168
+ }
169
+ if (!detectResult.services) {
170
+ return null;
171
+ }
172
+ const result = await (0, import_resolve.resolveAllConfiguredServices)(
173
+ detectResult.services,
174
+ scopedFs,
175
+ "generated"
176
+ );
177
+ let shouldInfer;
178
+ if (source === "layout") {
179
+ const rootWebFrameworkServices = result.services.filter(
180
+ (service) => service.type === "web" && service.routePrefix === "/" && typeof service.framework === "string"
181
+ );
182
+ shouldInfer = result.errors.length === 0 && rootWebFrameworkServices.length === 1 && result.services.length > 1;
183
+ } else {
184
+ shouldInfer = result.errors.length === 0 && result.services.length > 0;
185
+ }
186
+ const inferred = shouldInfer ? {
187
+ source,
188
+ config: toInferredLayoutConfig(detectResult.services),
189
+ services: result.services,
190
+ warnings: detectResult.warnings
191
+ } : null;
192
+ if (source === "layout" && shouldInfer) {
193
+ const routes = generateServicesRoutes(result.services);
194
+ return withResolvedResult(
195
+ {
196
+ services: result.services,
197
+ source: "auto-detected",
198
+ useImplicitEnvInjection: true,
199
+ routes,
200
+ errors: result.errors,
201
+ warnings: detectResult.warnings
202
+ },
203
+ inferred
204
+ );
205
+ }
206
+ return withResolvedResult(
207
+ {
208
+ services: [],
209
+ source: "auto-detected",
210
+ useImplicitEnvInjection: true,
211
+ routes: emptyRoutes(),
212
+ errors: result.errors,
213
+ warnings: detectResult.warnings
214
+ },
215
+ inferred
216
+ );
217
+ }
209
218
  function generateServicesRoutes(services) {
210
219
  const hostRewrites = [];
211
220
  const rewrites = [];
@@ -684,6 +684,7 @@ async function resolveConfiguredService(options) {
684
684
  runtime,
685
685
  buildCommand: config.buildCommand,
686
686
  installCommand: config.installCommand,
687
+ preDeployCommand: config.preDeployCommand,
687
688
  schedule: config.schedule,
688
689
  handlerFunction: moduleAttrParsed?.attrName,
689
690
  topics,
@@ -45,7 +45,7 @@ export interface ResolvedServicesResult {
45
45
  warnings: ServiceDetectionWarning[];
46
46
  }
47
47
  export interface InferredServicesResult {
48
- source: 'layout' | 'procfile' | 'railway';
48
+ source: 'layout' | 'procfile' | 'railway' | 'render';
49
49
  config: InferredServicesConfig;
50
50
  services: Service[];
51
51
  warnings: ServiceDetectionWarning[];
@@ -1,6 +1,8 @@
1
1
  import { INTERNAL_SERVICE_PREFIX, getInternalServiceFunctionPath, getInternalServiceCronPathPrefix, getInternalServiceCronPath } from '@vercel/build-utils';
2
+ import type { Framework } from '@vercel/frameworks';
2
3
  import type { DetectorFilesystem } from '../detectors/filesystem';
3
- import type { EnvVars, ServiceRuntime, ExperimentalServices, Services, ServiceDetectionError, ResolvedService } from './types';
4
+ import type { EnvVars, ServiceRuntime, ExperimentalServices, Services, ServiceDetectionError, ServiceDetectionWarning, ResolvedService } from './types';
5
+ export declare const DETECTION_FRAMEWORKS: Framework[];
4
6
  export { INTERNAL_SERVICE_PREFIX, getInternalServiceFunctionPath, getInternalServiceCronPathPrefix, getInternalServiceCronPath, };
5
7
  export declare function hasFile(fs: DetectorFilesystem, filePath: string): Promise<boolean>;
6
8
  export declare function isPublicServicesEnabled(): boolean;
@@ -68,3 +70,14 @@ export interface ReadVercelConfigResult {
68
70
  * Returns the parsed config or an error if the file exists but is invalid.
69
71
  */
70
72
  export declare function readVercelConfig(fs: DetectorFilesystem): Promise<ReadVercelConfigResult>;
73
+ /**
74
+ * Assign route prefixes to inferred services.
75
+ *
76
+ * A frontend service gets `/`, the rest get `/_/{name}`.
77
+ * A single non-frontend service would also get `/`.
78
+ * If no frontend service found, then multiple services get `/_/{name}`.
79
+ *
80
+ * Priority for `/`: single service or frontend > name "frontend" or "web" > alphabetical.
81
+ */
82
+ export declare function assignRoutePrefixes(services: ExperimentalServices): ServiceDetectionWarning[];
83
+ export declare function combineBuildCommand(buildCommand: string | undefined, preDeployCommand: string | string[] | undefined): string | undefined;
@@ -28,8 +28,11 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
28
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
29
  var utils_exports = {};
30
30
  __export(utils_exports, {
31
+ DETECTION_FRAMEWORKS: () => DETECTION_FRAMEWORKS,
31
32
  INTERNAL_QUEUES_PREFIX: () => INTERNAL_QUEUES_PREFIX,
32
33
  INTERNAL_SERVICE_PREFIX: () => import_build_utils.INTERNAL_SERVICE_PREFIX,
34
+ assignRoutePrefixes: () => assignRoutePrefixes,
35
+ combineBuildCommand: () => combineBuildCommand,
33
36
  filterFrameworksByRuntime: () => filterFrameworksByRuntime,
34
37
  getBuilderForRuntime: () => getBuilderForRuntime,
35
38
  getInternalServiceCronPath: () => import_build_utils.getInternalServiceCronPath,
@@ -50,7 +53,11 @@ __export(utils_exports, {
50
53
  module.exports = __toCommonJS(utils_exports);
51
54
  var import_framework_helpers = require("@vercel/build-utils/dist/framework-helpers");
52
55
  var import_build_utils = require("@vercel/build-utils");
56
+ var import_frameworks = require("@vercel/frameworks");
53
57
  var import_types = require("./types");
58
+ const DETECTION_FRAMEWORKS = import_frameworks.frameworkList.filter(
59
+ (framework) => !framework.experimental || framework.runtimeFramework
60
+ );
54
61
  async function hasFile(fs, filePath) {
55
62
  try {
56
63
  return await fs.isFile(filePath);
@@ -192,10 +199,48 @@ async function readVercelConfig(fs) {
192
199
  }
193
200
  return { config: null, error: null };
194
201
  }
202
+ function assignRoutePrefixes(services) {
203
+ const warnings = [];
204
+ const names = Object.keys(services);
205
+ if (names.length === 1) {
206
+ services[names[0]].routePrefix = "/";
207
+ return warnings;
208
+ }
209
+ const frontendNames = names.filter(
210
+ (name) => isFrontendFramework(services[name].framework)
211
+ );
212
+ let rootName = null;
213
+ if (frontendNames.length === 1) {
214
+ rootName = frontendNames[0];
215
+ } else if (frontendNames.length > 1) {
216
+ rootName = frontendNames.find((n) => n === "frontend" || n === "web") ?? frontendNames.sort()[0];
217
+ warnings.push({
218
+ code: "MULTIPLE_FRONTENDS",
219
+ message: `Multiple frontend services detected (${frontendNames.join(", ")}). "${rootName}" was assigned routePrefix "/". Adjust manually if a different service should be the root.`
220
+ });
221
+ }
222
+ for (const name of names) {
223
+ services[name].routePrefix = name === rootName ? "/" : `/_/${name}`;
224
+ }
225
+ return warnings;
226
+ }
227
+ function combineBuildCommand(buildCommand, preDeployCommand) {
228
+ const preDeploy = Array.isArray(preDeployCommand) ? preDeployCommand.join(" && ") : preDeployCommand;
229
+ if (preDeploy && buildCommand) {
230
+ return `${buildCommand} && ${preDeploy}`;
231
+ } else if (preDeploy) {
232
+ return preDeploy;
233
+ } else {
234
+ return buildCommand;
235
+ }
236
+ }
195
237
  // Annotate the CommonJS export names for ESM import in node:
196
238
  0 && (module.exports = {
239
+ DETECTION_FRAMEWORKS,
197
240
  INTERNAL_QUEUES_PREFIX,
198
241
  INTERNAL_SERVICE_PREFIX,
242
+ assignRoutePrefixes,
243
+ combineBuildCommand,
199
244
  filterFrameworksByRuntime,
200
245
  getBuilderForRuntime,
201
246
  getInternalServiceCronPath,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vercel/fs-detectors",
3
- "version": "6.3.0",
3
+ "version": "6.5.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/routing-utils": "6.2.0",
24
- "@vercel/frameworks": "3.26.0",
25
23
  "@vercel/error-utils": "2.1.0",
26
- "@vercel/build-utils": "13.24.0"
24
+ "@vercel/routing-utils": "6.2.0",
25
+ "@vercel/build-utils": "13.26.0",
26
+ "@vercel/frameworks": "3.26.1"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/glob": "7.2.0",