@vercel/fs-detectors 6.4.0 → 6.6.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/services/auto-detect.d.ts +8 -1
- package/dist/services/auto-detect.js +53 -28
- package/dist/services/detect-procfile.d.ts +22 -0
- package/dist/services/detect-procfile.js +248 -0
- package/dist/services/detect-railway.d.ts +2 -0
- package/dist/services/detect-railway.js +15 -34
- package/dist/services/detect-render.d.ts +13 -0
- package/dist/services/detect-render.js +222 -0
- package/dist/services/detect-services.d.ts +1 -1
- package/dist/services/detect-services.js +83 -71
- package/dist/services/types.d.ts +9 -3
- package/dist/services/utils.d.ts +14 -1
- package/dist/services/utils.js +45 -0
- package/package.json +3 -3
|
@@ -1,11 +1,18 @@
|
|
|
1
|
+
import type { DetectEntrypointFn } from '@vercel/build-utils';
|
|
1
2
|
import type { DetectorFilesystem } from '../detectors/filesystem';
|
|
2
|
-
import type { ExperimentalServices, ServiceDetectionError } from './types';
|
|
3
|
+
import type { ExperimentalServices, ServiceDetectionError, ServiceDetectionWarning } from './types';
|
|
3
4
|
export interface AutoDetectOptions {
|
|
4
5
|
fs: DetectorFilesystem;
|
|
6
|
+
/**
|
|
7
|
+
* Optional callback used to enrich runtime services with a normalized
|
|
8
|
+
* entrypoint (file path or `module:attr` reference).
|
|
9
|
+
*/
|
|
10
|
+
detectEntrypoint?: DetectEntrypointFn;
|
|
5
11
|
}
|
|
6
12
|
export interface AutoDetectResult {
|
|
7
13
|
services: ExperimentalServices | null;
|
|
8
14
|
errors: ServiceDetectionError[];
|
|
15
|
+
warnings: ServiceDetectionWarning[];
|
|
9
16
|
}
|
|
10
17
|
/**
|
|
11
18
|
* Auto-detect services when services are not configured.
|
|
@@ -23,16 +23,14 @@ __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
|
-
const { fs } = options;
|
|
33
|
+
const { fs, detectEntrypoint } = options;
|
|
36
34
|
const rootFrameworks = await (0, import_detect_framework.detectFrameworks)({
|
|
37
35
|
fs,
|
|
38
36
|
frameworkList: import_frameworks.frameworkList
|
|
@@ -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",
|
|
@@ -50,7 +49,7 @@ async function autoDetectServices(options) {
|
|
|
50
49
|
};
|
|
51
50
|
}
|
|
52
51
|
if (rootFrameworks.length === 1) {
|
|
53
|
-
return detectServicesAtRoot(fs, rootFrameworks[0]);
|
|
52
|
+
return detectServicesAtRoot(fs, rootFrameworks[0], detectEntrypoint);
|
|
54
53
|
}
|
|
55
54
|
for (const frontendLocation of FRONTEND_LOCATIONS) {
|
|
56
55
|
const hasFrontendDir = await fs.hasPath(frontendLocation);
|
|
@@ -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",
|
|
@@ -78,12 +78,14 @@ async function autoDetectServices(options) {
|
|
|
78
78
|
return detectServicesFrontendSubdir(
|
|
79
79
|
fs,
|
|
80
80
|
frontendFrameworks[0],
|
|
81
|
-
frontendLocation
|
|
81
|
+
frontendLocation,
|
|
82
|
+
detectEntrypoint
|
|
82
83
|
);
|
|
83
84
|
}
|
|
84
85
|
}
|
|
85
86
|
return {
|
|
86
87
|
services: null,
|
|
88
|
+
warnings: [],
|
|
87
89
|
errors: [
|
|
88
90
|
{
|
|
89
91
|
code: "NO_SERVICES_CONFIGURED",
|
|
@@ -92,49 +94,54 @@ async function autoDetectServices(options) {
|
|
|
92
94
|
]
|
|
93
95
|
};
|
|
94
96
|
}
|
|
95
|
-
async function detectServicesAtRoot(fs, rootFramework) {
|
|
97
|
+
async function detectServicesAtRoot(fs, rootFramework, detectEntrypoint) {
|
|
96
98
|
const services = {};
|
|
97
99
|
services.frontend = {
|
|
98
100
|
framework: rootFramework.slug ?? void 0,
|
|
99
101
|
routePrefix: "/"
|
|
100
102
|
};
|
|
101
|
-
const backendResult = await detectBackendServices(fs);
|
|
103
|
+
const backendResult = await detectBackendServices(fs, detectEntrypoint);
|
|
102
104
|
if (backendResult.error) {
|
|
103
105
|
return {
|
|
104
106
|
services: null,
|
|
107
|
+
warnings: [],
|
|
105
108
|
errors: [backendResult.error]
|
|
106
109
|
};
|
|
107
110
|
}
|
|
108
111
|
if (Object.keys(backendResult.services).length === 0) {
|
|
109
112
|
return {
|
|
110
113
|
services: null,
|
|
114
|
+
warnings: [],
|
|
111
115
|
errors: []
|
|
112
116
|
};
|
|
113
117
|
}
|
|
114
118
|
Object.assign(services, backendResult.services);
|
|
115
119
|
return {
|
|
116
120
|
services,
|
|
121
|
+
warnings: [],
|
|
117
122
|
errors: []
|
|
118
123
|
};
|
|
119
124
|
}
|
|
120
|
-
async function detectServicesFrontendSubdir(fs, frontendFramework, frontendLocation) {
|
|
125
|
+
async function detectServicesFrontendSubdir(fs, frontendFramework, frontendLocation, detectEntrypoint) {
|
|
121
126
|
const services = {};
|
|
122
127
|
const serviceName = frontendLocation.split("/").pop() || "frontend";
|
|
123
128
|
services[serviceName] = {
|
|
124
129
|
framework: frontendFramework.slug ?? void 0,
|
|
125
|
-
|
|
130
|
+
root: frontendLocation,
|
|
126
131
|
routePrefix: "/"
|
|
127
132
|
};
|
|
128
|
-
const backendResult = await detectBackendServices(fs);
|
|
133
|
+
const backendResult = await detectBackendServices(fs, detectEntrypoint);
|
|
129
134
|
if (backendResult.error) {
|
|
130
135
|
return {
|
|
131
136
|
services: null,
|
|
137
|
+
warnings: [],
|
|
132
138
|
errors: [backendResult.error]
|
|
133
139
|
};
|
|
134
140
|
}
|
|
135
141
|
if (Object.keys(backendResult.services).length === 0) {
|
|
136
142
|
return {
|
|
137
143
|
services: null,
|
|
144
|
+
warnings: [],
|
|
138
145
|
errors: [
|
|
139
146
|
{
|
|
140
147
|
code: "NO_BACKEND_SERVICES",
|
|
@@ -146,19 +153,28 @@ async function detectServicesFrontendSubdir(fs, frontendFramework, frontendLocat
|
|
|
146
153
|
Object.assign(services, backendResult.services);
|
|
147
154
|
return {
|
|
148
155
|
services,
|
|
156
|
+
warnings: [],
|
|
149
157
|
errors: []
|
|
150
158
|
};
|
|
151
159
|
}
|
|
152
|
-
async function detectBackendServices(fs) {
|
|
160
|
+
async function detectBackendServices(fs, detectEntrypoint) {
|
|
153
161
|
const services = {};
|
|
154
|
-
const backendResult = await detectServiceInDir(
|
|
162
|
+
const backendResult = await detectServiceInDir(
|
|
163
|
+
fs,
|
|
164
|
+
BACKEND_DIR,
|
|
165
|
+
"backend",
|
|
166
|
+
detectEntrypoint
|
|
167
|
+
);
|
|
155
168
|
if (backendResult.error) {
|
|
156
169
|
return { services: {}, error: backendResult.error };
|
|
157
170
|
}
|
|
158
171
|
if (backendResult.service) {
|
|
159
172
|
services.backend = backendResult.service;
|
|
160
173
|
}
|
|
161
|
-
const multiServicesResult = await detectServicesDirectory(
|
|
174
|
+
const multiServicesResult = await detectServicesDirectory(
|
|
175
|
+
fs,
|
|
176
|
+
detectEntrypoint
|
|
177
|
+
);
|
|
162
178
|
if (multiServicesResult.error) {
|
|
163
179
|
return { services: {}, error: multiServicesResult.error };
|
|
164
180
|
}
|
|
@@ -177,7 +193,7 @@ async function detectBackendServices(fs) {
|
|
|
177
193
|
Object.assign(services, multiServicesResult.services);
|
|
178
194
|
return { services };
|
|
179
195
|
}
|
|
180
|
-
async function detectServicesDirectory(fs) {
|
|
196
|
+
async function detectServicesDirectory(fs, detectEntrypoint) {
|
|
181
197
|
const services = {};
|
|
182
198
|
const hasServicesDir = await fs.hasPath(SERVICES_DIR);
|
|
183
199
|
if (!hasServicesDir) {
|
|
@@ -191,7 +207,12 @@ async function detectServicesDirectory(fs) {
|
|
|
191
207
|
}
|
|
192
208
|
const serviceName = entry.name;
|
|
193
209
|
const serviceDir = `${SERVICES_DIR}/${serviceName}`;
|
|
194
|
-
const result = await detectServiceInDir(
|
|
210
|
+
const result = await detectServiceInDir(
|
|
211
|
+
fs,
|
|
212
|
+
serviceDir,
|
|
213
|
+
serviceName,
|
|
214
|
+
detectEntrypoint
|
|
215
|
+
);
|
|
195
216
|
if (result.error) {
|
|
196
217
|
return { services: {}, error: result.error };
|
|
197
218
|
}
|
|
@@ -201,7 +222,7 @@ async function detectServicesDirectory(fs) {
|
|
|
201
222
|
}
|
|
202
223
|
return { services };
|
|
203
224
|
}
|
|
204
|
-
async function detectServiceInDir(fs, dirPath, serviceName) {
|
|
225
|
+
async function detectServiceInDir(fs, dirPath, serviceName, detectEntrypoint) {
|
|
205
226
|
const hasDirPath = await fs.hasPath(dirPath);
|
|
206
227
|
if (!hasDirPath) {
|
|
207
228
|
return {};
|
|
@@ -209,7 +230,7 @@ async function detectServiceInDir(fs, dirPath, serviceName) {
|
|
|
209
230
|
const serviceFs = fs.chdir(dirPath);
|
|
210
231
|
const frameworks = await (0, import_detect_framework.detectFrameworks)({
|
|
211
232
|
fs: serviceFs,
|
|
212
|
-
frameworkList: DETECTION_FRAMEWORKS,
|
|
233
|
+
frameworkList: import_utils.DETECTION_FRAMEWORKS,
|
|
213
234
|
useExperimentalFrameworks: true
|
|
214
235
|
});
|
|
215
236
|
if (frameworks.length > 1) {
|
|
@@ -222,17 +243,21 @@ async function detectServiceInDir(fs, dirPath, serviceName) {
|
|
|
222
243
|
}
|
|
223
244
|
};
|
|
224
245
|
}
|
|
225
|
-
if (frameworks.length
|
|
226
|
-
|
|
227
|
-
return {
|
|
228
|
-
service: {
|
|
229
|
-
framework: framework.slug ?? void 0,
|
|
230
|
-
entrypoint: dirPath,
|
|
231
|
-
routePrefix: `/_/${serviceName}`
|
|
232
|
-
}
|
|
233
|
-
};
|
|
246
|
+
if (frameworks.length !== 1) {
|
|
247
|
+
return {};
|
|
234
248
|
}
|
|
235
|
-
|
|
249
|
+
const framework = frameworks[0];
|
|
250
|
+
const slug = framework.slug ?? void 0;
|
|
251
|
+
const routePrefix = `/_/${serviceName}`;
|
|
252
|
+
const detected = detectEntrypoint && !(0, import_utils.isFrontendFramework)(slug) ? await detectEntrypoint({ workPath: dirPath, framework: slug }) : null;
|
|
253
|
+
return {
|
|
254
|
+
service: {
|
|
255
|
+
framework: slug,
|
|
256
|
+
root: dirPath,
|
|
257
|
+
...detected ? { entrypoint: detected.entrypoint } : {},
|
|
258
|
+
routePrefix
|
|
259
|
+
}
|
|
260
|
+
};
|
|
236
261
|
}
|
|
237
262
|
// Annotate the CommonJS export names for ESM import in node:
|
|
238
263
|
0 && (module.exports = {
|
|
@@ -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
|
+
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { DetectEntrypointFn } from '@vercel/build-utils';
|
|
1
2
|
import type { DetectorFilesystem } from '../detectors/filesystem';
|
|
2
3
|
import type { ExperimentalServices, ServiceDetectionError, ServiceDetectionWarning } from './types';
|
|
3
4
|
export interface RailwayDetectResult {
|
|
@@ -17,4 +18,5 @@ export interface RailwayDetectResult {
|
|
|
17
18
|
*/
|
|
18
19
|
export declare function detectRailwayServices(options: {
|
|
19
20
|
fs: DetectorFilesystem;
|
|
21
|
+
detectEntrypoint?: DetectEntrypointFn;
|
|
20
22
|
}): Promise<RailwayDetectResult>;
|
|
@@ -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,11 +55,8 @@ 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
|
-
const { fs } = options;
|
|
59
|
+
const { fs, detectEntrypoint } = options;
|
|
64
60
|
const { configs, warnings } = await findRailwayConfigs(fs);
|
|
65
61
|
if (configs.length === 0) {
|
|
66
62
|
return { services: null, errors: [], warnings };
|
|
@@ -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) {
|
|
@@ -123,10 +119,20 @@ async function detectRailwayServices(options) {
|
|
|
123
119
|
continue;
|
|
124
120
|
}
|
|
125
121
|
const framework = frameworks[0];
|
|
122
|
+
const slug = framework.slug ?? void 0;
|
|
126
123
|
let serviceConfig = {};
|
|
127
|
-
serviceConfig.framework =
|
|
124
|
+
serviceConfig.framework = slug;
|
|
128
125
|
if (cf.dirPath !== ".") {
|
|
129
|
-
serviceConfig.
|
|
126
|
+
serviceConfig.root = cf.dirPath;
|
|
127
|
+
if (detectEntrypoint && !(0, import_utils.isFrontendFramework)(slug)) {
|
|
128
|
+
const detected = await detectEntrypoint({
|
|
129
|
+
workPath: cf.dirPath,
|
|
130
|
+
framework: slug
|
|
131
|
+
});
|
|
132
|
+
if (detected) {
|
|
133
|
+
serviceConfig.entrypoint = detected.entrypoint;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
130
136
|
}
|
|
131
137
|
if (cf.config.build?.buildCommand) {
|
|
132
138
|
serviceConfig.buildCommand = cf.config.build.buildCommand;
|
|
@@ -144,7 +150,7 @@ async function detectRailwayServices(options) {
|
|
|
144
150
|
if (serviceNames.length === 0) {
|
|
145
151
|
return { services: null, errors: [], warnings };
|
|
146
152
|
}
|
|
147
|
-
warnings.push(...assignRoutePrefixes(services));
|
|
153
|
+
warnings.push(...(0, import_utils.assignRoutePrefixes)(services));
|
|
148
154
|
return { services, errors: [], warnings };
|
|
149
155
|
}
|
|
150
156
|
async function findRailwayConfigs(fs, dirPath = ".", depth = 0) {
|
|
@@ -230,31 +236,6 @@ function deriveServiceName(dirPath) {
|
|
|
230
236
|
const segments = dirPath.split("/");
|
|
231
237
|
return segments[segments.length - 1];
|
|
232
238
|
}
|
|
233
|
-
function assignRoutePrefixes(services) {
|
|
234
|
-
const warnings = [];
|
|
235
|
-
const names = Object.keys(services);
|
|
236
|
-
if (names.length === 1) {
|
|
237
|
-
services[names[0]].routePrefix = "/";
|
|
238
|
-
return warnings;
|
|
239
|
-
}
|
|
240
|
-
const frontendNames = names.filter(
|
|
241
|
-
(name) => (0, import_utils.isFrontendFramework)(services[name].framework)
|
|
242
|
-
);
|
|
243
|
-
let rootName = null;
|
|
244
|
-
if (frontendNames.length === 1) {
|
|
245
|
-
rootName = frontendNames[0];
|
|
246
|
-
} else if (frontendNames.length > 1) {
|
|
247
|
-
rootName = frontendNames.find((n) => n === "frontend" || n === "web") ?? frontendNames.sort()[0];
|
|
248
|
-
warnings.push({
|
|
249
|
-
code: "MULTIPLE_FRONTENDS",
|
|
250
|
-
message: `Multiple frontend services detected (${frontendNames.join(", ")}). "${rootName}" was assigned routePrefix "/". Adjust manually if a different service should be the root.`
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
for (const name of names) {
|
|
254
|
-
services[name].routePrefix = name === rootName ? "/" : `/_/${name}`;
|
|
255
|
-
}
|
|
256
|
-
return warnings;
|
|
257
|
-
}
|
|
258
239
|
// Annotate the CommonJS export names for ESM import in node:
|
|
259
240
|
0 && (module.exports = {
|
|
260
241
|
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 {
|
|
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,12 @@ 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
|
+
}
|
|
72
|
+
if (typeof service.root === "string") {
|
|
73
|
+
serviceConfig.root = service.root;
|
|
74
|
+
}
|
|
67
75
|
if (typeof service.entrypoint === "string") {
|
|
68
76
|
serviceConfig.entrypoint = service.entrypoint;
|
|
69
77
|
}
|
|
@@ -76,12 +84,15 @@ function toInferredLayoutConfig(services) {
|
|
|
76
84
|
if (typeof service.buildCommand === "string") {
|
|
77
85
|
serviceConfig.buildCommand = service.buildCommand;
|
|
78
86
|
}
|
|
87
|
+
if (typeof service.runtime === "string") {
|
|
88
|
+
serviceConfig.runtime = service.runtime;
|
|
89
|
+
}
|
|
79
90
|
inferredConfig[name] = serviceConfig;
|
|
80
91
|
}
|
|
81
92
|
return inferredConfig;
|
|
82
93
|
}
|
|
83
94
|
async function detectServices(options) {
|
|
84
|
-
const { fs, workPath } = options;
|
|
95
|
+
const { fs, workPath, detectEntrypoint } = options;
|
|
85
96
|
const scopedFs = workPath ? fs.chdir(workPath) : fs;
|
|
86
97
|
const { config: vercelConfig, error: configError } = await (0, import_utils.readVercelConfig)(scopedFs);
|
|
87
98
|
if (configError) {
|
|
@@ -98,76 +109,17 @@ async function detectServices(options) {
|
|
|
98
109
|
const configuredServices = hasNonEmptyPublicServicesConfig ? vercelConfig.services : vercelConfig?.experimentalServices;
|
|
99
110
|
const hasConfiguredServices = configuredServices && Object.keys(configuredServices).length > 0;
|
|
100
111
|
if (!hasConfiguredServices) {
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
});
|
|
112
|
+
const detectors = [
|
|
113
|
+
{ detect: import_detect_railway.detectRailwayServices, source: "railway" },
|
|
114
|
+
{ detect: import_detect_render.detectRenderServices, source: "render" },
|
|
115
|
+
{ detect: import_detect_procfile.detectProcfileServices, source: "procfile" },
|
|
116
|
+
{ detect: import_auto_detect.autoDetectServices, source: "layout" }
|
|
117
|
+
];
|
|
118
|
+
for (const { detect, source } of detectors) {
|
|
119
|
+
const detectResult = await detect({ fs: scopedFs, detectEntrypoint });
|
|
120
|
+
const match = await tryResolveInferred(detectResult, source, scopedFs);
|
|
121
|
+
if (match)
|
|
122
|
+
return match;
|
|
171
123
|
}
|
|
172
124
|
return withResolvedResult({
|
|
173
125
|
services: [],
|
|
@@ -206,6 +158,66 @@ async function detectServices(options) {
|
|
|
206
158
|
warnings: []
|
|
207
159
|
});
|
|
208
160
|
}
|
|
161
|
+
async function tryResolveInferred(detectResult, source, scopedFs) {
|
|
162
|
+
if (detectResult.errors.length > 0) {
|
|
163
|
+
return withResolvedResult({
|
|
164
|
+
services: [],
|
|
165
|
+
source: "auto-detected",
|
|
166
|
+
useImplicitEnvInjection: true,
|
|
167
|
+
routes: emptyRoutes(),
|
|
168
|
+
errors: detectResult.errors,
|
|
169
|
+
warnings: detectResult.warnings
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
if (!detectResult.services) {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
const result = await (0, import_resolve.resolveAllConfiguredServices)(
|
|
176
|
+
detectResult.services,
|
|
177
|
+
scopedFs,
|
|
178
|
+
"generated"
|
|
179
|
+
);
|
|
180
|
+
let shouldInfer;
|
|
181
|
+
if (source === "layout") {
|
|
182
|
+
const rootWebFrameworkServices = result.services.filter(
|
|
183
|
+
(service) => service.type === "web" && service.routePrefix === "/" && typeof service.framework === "string"
|
|
184
|
+
);
|
|
185
|
+
shouldInfer = result.errors.length === 0 && rootWebFrameworkServices.length === 1 && result.services.length > 1;
|
|
186
|
+
} else {
|
|
187
|
+
shouldInfer = result.errors.length === 0 && result.services.length > 0;
|
|
188
|
+
}
|
|
189
|
+
const inferred = shouldInfer ? {
|
|
190
|
+
source,
|
|
191
|
+
config: toInferredLayoutConfig(detectResult.services),
|
|
192
|
+
services: result.services,
|
|
193
|
+
warnings: detectResult.warnings
|
|
194
|
+
} : null;
|
|
195
|
+
if (source === "layout" && shouldInfer) {
|
|
196
|
+
const routes = generateServicesRoutes(result.services);
|
|
197
|
+
return withResolvedResult(
|
|
198
|
+
{
|
|
199
|
+
services: result.services,
|
|
200
|
+
source: "auto-detected",
|
|
201
|
+
useImplicitEnvInjection: true,
|
|
202
|
+
routes,
|
|
203
|
+
errors: result.errors,
|
|
204
|
+
warnings: detectResult.warnings
|
|
205
|
+
},
|
|
206
|
+
inferred
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
return withResolvedResult(
|
|
210
|
+
{
|
|
211
|
+
services: [],
|
|
212
|
+
source: "auto-detected",
|
|
213
|
+
useImplicitEnvInjection: true,
|
|
214
|
+
routes: emptyRoutes(),
|
|
215
|
+
errors: result.errors,
|
|
216
|
+
warnings: detectResult.warnings
|
|
217
|
+
},
|
|
218
|
+
inferred
|
|
219
|
+
);
|
|
220
|
+
}
|
|
209
221
|
function generateServicesRoutes(services) {
|
|
210
222
|
const hostRewrites = [];
|
|
211
223
|
const rewrites = [];
|
package/dist/services/types.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Route } from '@vercel/routing-utils';
|
|
2
|
-
import type { EnvVar, EnvVars, ExperimentalServiceConfig, ExperimentalServiceGroups, ExperimentalServices, ServiceConfig, Services, ServiceRuntime, ServiceType, ServiceRefEnvVar, Service, Builder } from '@vercel/build-utils';
|
|
2
|
+
import type { DetectEntrypointFn, EnvVar, EnvVars, ExperimentalServiceConfig, ExperimentalServiceGroups, ExperimentalServices, ServiceConfig, Services, ServiceRuntime, ServiceType, ServiceRefEnvVar, Service, Builder } from '@vercel/build-utils';
|
|
3
3
|
import type { DetectorFilesystem } from '../detectors/filesystem';
|
|
4
|
-
export type { EnvVar, EnvVars, ExperimentalServiceConfig, ExperimentalServiceGroups, ExperimentalServices, ServiceConfig, Services, ServiceRuntime, ServiceType, ServiceRefEnvVar, Service, Builder, };
|
|
4
|
+
export type { DetectEntrypointFn, EnvVar, EnvVars, ExperimentalServiceConfig, ExperimentalServiceGroups, ExperimentalServices, ServiceConfig, Services, ServiceRuntime, ServiceType, ServiceRefEnvVar, Service, Builder, };
|
|
5
5
|
/**
|
|
6
6
|
* @deprecated Use `Service` instead
|
|
7
7
|
*/
|
|
@@ -13,6 +13,12 @@ export interface DetectServicesOptions {
|
|
|
13
13
|
* If provided, vercel.json is read from this path.
|
|
14
14
|
*/
|
|
15
15
|
workPath?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Optional callback that, given a candidate service directory and its
|
|
18
|
+
* detected framework, returns a normalized entrypoint (file path or
|
|
19
|
+
* `module:attr` reference). Used to suggested service configs.
|
|
20
|
+
*/
|
|
21
|
+
detectEntrypoint?: DetectEntrypointFn;
|
|
16
22
|
}
|
|
17
23
|
export interface ServicesRoutes {
|
|
18
24
|
/** Host-based rewrite routes for subdomain-mounted web services */
|
|
@@ -45,7 +51,7 @@ export interface ResolvedServicesResult {
|
|
|
45
51
|
warnings: ServiceDetectionWarning[];
|
|
46
52
|
}
|
|
47
53
|
export interface InferredServicesResult {
|
|
48
|
-
source: 'layout' | 'procfile' | 'railway';
|
|
54
|
+
source: 'layout' | 'procfile' | 'railway' | 'render';
|
|
49
55
|
config: InferredServicesConfig;
|
|
50
56
|
services: Service[];
|
|
51
57
|
warnings: ServiceDetectionWarning[];
|
package/dist/services/utils.d.ts
CHANGED
|
@@ -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;
|
package/dist/services/utils.js
CHANGED
|
@@ -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
|
+
"version": "6.6.0",
|
|
4
4
|
"description": "Vercel filesystem detectors",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
"minimatch": "3.1.2",
|
|
21
21
|
"semver": "6.3.1",
|
|
22
22
|
"smol-toml": "1.5.2",
|
|
23
|
-
"@vercel/build-utils": "13.
|
|
24
|
-
"@vercel/frameworks": "3.26.1",
|
|
23
|
+
"@vercel/build-utils": "13.26.0",
|
|
25
24
|
"@vercel/error-utils": "2.1.0",
|
|
25
|
+
"@vercel/frameworks": "3.26.1",
|
|
26
26
|
"@vercel/routing-utils": "6.2.0"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|