@vercel/fs-detectors 5.13.1 → 5.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { DetectorFilesystem } from '../detectors/filesystem';
|
|
2
|
+
import type { ExperimentalServices, ServiceDetectionError, ServiceDetectionWarning } from './types';
|
|
3
|
+
export interface RailwayDetectResult {
|
|
4
|
+
services: ExperimentalServices | null;
|
|
5
|
+
errors: ServiceDetectionError[];
|
|
6
|
+
warnings: ServiceDetectionWarning[];
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Detect Railway service configurations in the project.
|
|
10
|
+
*
|
|
11
|
+
* Scans for railway.{json,toml} files, parses them,
|
|
12
|
+
* tries to detect frameworks in each service directory, and maps to
|
|
13
|
+
* services format.
|
|
14
|
+
*
|
|
15
|
+
* When a Railway config has `deploy.preDeployCommand`, it's added to
|
|
16
|
+
* `buildCommand` since we don't have a separate pre-deploy yet.
|
|
17
|
+
*/
|
|
18
|
+
export declare function detectRailwayServices(options: {
|
|
19
|
+
fs: DetectorFilesystem;
|
|
20
|
+
}): Promise<RailwayDetectResult>;
|
|
@@ -0,0 +1,270 @@
|
|
|
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_railway_exports = {};
|
|
30
|
+
__export(detect_railway_exports, {
|
|
31
|
+
detectRailwayServices: () => detectRailwayServices
|
|
32
|
+
});
|
|
33
|
+
module.exports = __toCommonJS(detect_railway_exports);
|
|
34
|
+
var import_path = require("path");
|
|
35
|
+
var import_smol_toml = __toESM(require("smol-toml"));
|
|
36
|
+
var import_frameworks = require("@vercel/frameworks");
|
|
37
|
+
var import_detect_framework = require("../detect-framework");
|
|
38
|
+
var import_utils = require("./utils");
|
|
39
|
+
const RAILWAY_JSON = "railway.json";
|
|
40
|
+
const RAILWAY_TOML = "railway.toml";
|
|
41
|
+
const MAX_SCAN_DEPTH = 5;
|
|
42
|
+
const SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
43
|
+
".hg",
|
|
44
|
+
".git",
|
|
45
|
+
".svn",
|
|
46
|
+
".cache",
|
|
47
|
+
".next",
|
|
48
|
+
".now",
|
|
49
|
+
".vercel",
|
|
50
|
+
".venv",
|
|
51
|
+
".yarn",
|
|
52
|
+
".turbo",
|
|
53
|
+
".output",
|
|
54
|
+
"node_modules",
|
|
55
|
+
"__pycache__",
|
|
56
|
+
"venv",
|
|
57
|
+
"CVS"
|
|
58
|
+
]);
|
|
59
|
+
const DETECTION_FRAMEWORKS = import_frameworks.frameworkList.filter(
|
|
60
|
+
(framework) => !framework.experimental || framework.runtimeFramework
|
|
61
|
+
);
|
|
62
|
+
async function detectRailwayServices(options) {
|
|
63
|
+
const { fs } = options;
|
|
64
|
+
const { configs, warnings } = await findRailwayConfigs(fs);
|
|
65
|
+
if (configs.length === 0) {
|
|
66
|
+
return { services: null, errors: [], warnings };
|
|
67
|
+
}
|
|
68
|
+
const services = {};
|
|
69
|
+
const serviceDirs = /* @__PURE__ */ new Map();
|
|
70
|
+
const errors = [];
|
|
71
|
+
for (const cf of configs) {
|
|
72
|
+
const serviceFs = cf.dirPath === "." ? fs : fs.chdir(cf.dirPath);
|
|
73
|
+
const dirLabel = cf.dirPath === "." ? "root" : cf.dirPath;
|
|
74
|
+
const frameworks = await (0, import_detect_framework.detectFrameworks)({
|
|
75
|
+
fs: serviceFs,
|
|
76
|
+
frameworkList: DETECTION_FRAMEWORKS,
|
|
77
|
+
useExperimentalFrameworks: true
|
|
78
|
+
});
|
|
79
|
+
if (cf.config.deploy?.cronSchedule) {
|
|
80
|
+
const schedule = cf.config.deploy.cronSchedule;
|
|
81
|
+
const runtime = frameworks.length === 1 ? (0, import_utils.inferRuntimeFromFramework)(frameworks[0].slug) : void 0;
|
|
82
|
+
const hint = {
|
|
83
|
+
type: "cron",
|
|
84
|
+
schedule,
|
|
85
|
+
entrypoint: "<path-to-handler>"
|
|
86
|
+
};
|
|
87
|
+
if (runtime) {
|
|
88
|
+
hint.runtime = runtime;
|
|
89
|
+
}
|
|
90
|
+
warnings.push({
|
|
91
|
+
code: "RAILWAY_CRON_HINT",
|
|
92
|
+
message: `Found Railway cron in ${dirLabel}/ (schedule: "${schedule}"). Vercel crons work with a file entrypoint. You can add the following to define this cron service:
|
|
93
|
+
"${deriveServiceName(cf.dirPath)}": ${JSON.stringify(hint, null, 2)}`
|
|
94
|
+
});
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const serviceName = deriveServiceName(cf.dirPath);
|
|
98
|
+
const existingDir = serviceDirs.get(serviceName);
|
|
99
|
+
if (existingDir) {
|
|
100
|
+
errors.push({
|
|
101
|
+
code: "DUPLICATE_SERVICE",
|
|
102
|
+
message: `Duplicate service name "${serviceName}" derived from ${existingDir}/ and ${dirLabel}/. Rename one of the directories to avoid conflicts.`,
|
|
103
|
+
serviceName
|
|
104
|
+
});
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
serviceDirs.set(serviceName, dirLabel);
|
|
108
|
+
if (frameworks.length === 0) {
|
|
109
|
+
warnings.push({
|
|
110
|
+
code: "SERVICE_SKIPPED",
|
|
111
|
+
message: `Skipped service in ${dirLabel}/: no framework detected. Configure it manually in experimentalServices.`
|
|
112
|
+
});
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (frameworks.length > 1) {
|
|
116
|
+
const names = frameworks.map((f) => f.name).join(", ");
|
|
117
|
+
errors.push({
|
|
118
|
+
code: "MULTIPLE_FRAMEWORKS_SERVICE",
|
|
119
|
+
message: `Multiple frameworks detected in ${dirLabel}/: ${names}. Use explicit experimentalServices config.`,
|
|
120
|
+
serviceName
|
|
121
|
+
});
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const framework = frameworks[0];
|
|
125
|
+
let serviceConfig = {};
|
|
126
|
+
serviceConfig.framework = framework.slug ?? void 0;
|
|
127
|
+
if (cf.dirPath !== ".") {
|
|
128
|
+
serviceConfig.entrypoint = cf.dirPath;
|
|
129
|
+
}
|
|
130
|
+
const buildCommand = combineBuildCommand(
|
|
131
|
+
cf.config.build?.buildCommand,
|
|
132
|
+
cf.config.deploy?.preDeployCommand
|
|
133
|
+
);
|
|
134
|
+
if (buildCommand) {
|
|
135
|
+
serviceConfig.buildCommand = buildCommand;
|
|
136
|
+
}
|
|
137
|
+
services[serviceName] = serviceConfig;
|
|
138
|
+
}
|
|
139
|
+
if (errors.length > 0) {
|
|
140
|
+
return { services: null, errors, warnings };
|
|
141
|
+
}
|
|
142
|
+
const serviceNames = Object.keys(services);
|
|
143
|
+
if (serviceNames.length === 0) {
|
|
144
|
+
return { services: null, errors: [], warnings };
|
|
145
|
+
}
|
|
146
|
+
warnings.push(...assignRoutePrefixes(services));
|
|
147
|
+
return { services, errors: [], warnings };
|
|
148
|
+
}
|
|
149
|
+
async function findRailwayConfigs(fs, dirPath = ".", depth = 0) {
|
|
150
|
+
const configs = [];
|
|
151
|
+
const warnings = [];
|
|
152
|
+
const readResult = await readRailwayConfigRaw(fs, dirPath);
|
|
153
|
+
warnings.push(...readResult.warnings);
|
|
154
|
+
const { config, warning } = tryParseRailwayConfig(readResult.raw);
|
|
155
|
+
if (warning) {
|
|
156
|
+
warnings.push(warning);
|
|
157
|
+
}
|
|
158
|
+
if (config) {
|
|
159
|
+
configs.push({ dirPath, config });
|
|
160
|
+
}
|
|
161
|
+
if (depth >= MAX_SCAN_DEPTH) {
|
|
162
|
+
return { configs, warnings };
|
|
163
|
+
}
|
|
164
|
+
const readPath = dirPath === "." ? "/" : dirPath;
|
|
165
|
+
let entries;
|
|
166
|
+
try {
|
|
167
|
+
entries = await fs.readdir(readPath);
|
|
168
|
+
} catch {
|
|
169
|
+
return { configs, warnings };
|
|
170
|
+
}
|
|
171
|
+
for (const entry of entries) {
|
|
172
|
+
if (entry.type !== "dir" || SKIP_DIRS.has(entry.name)) {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
const childPath = dirPath === "." ? entry.name : import_path.posix.join(dirPath, entry.name);
|
|
176
|
+
const child = await findRailwayConfigs(fs, childPath, depth + 1);
|
|
177
|
+
configs.push(...child.configs);
|
|
178
|
+
warnings.push(...child.warnings);
|
|
179
|
+
}
|
|
180
|
+
return { configs, warnings };
|
|
181
|
+
}
|
|
182
|
+
async function readRailwayConfigRaw(fs, dirPath) {
|
|
183
|
+
const warnings = [];
|
|
184
|
+
for (const filename of [RAILWAY_JSON, RAILWAY_TOML]) {
|
|
185
|
+
const filePath = dirPath === "." ? filename : import_path.posix.join(dirPath, filename);
|
|
186
|
+
try {
|
|
187
|
+
const exists = await fs.isFile(filePath);
|
|
188
|
+
if (!exists)
|
|
189
|
+
continue;
|
|
190
|
+
} catch {
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
const buf = await fs.readFile(filePath);
|
|
195
|
+
return {
|
|
196
|
+
raw: { path: filePath, content: buf.toString("utf-8") },
|
|
197
|
+
warnings
|
|
198
|
+
};
|
|
199
|
+
} catch (err) {
|
|
200
|
+
warnings.push({
|
|
201
|
+
code: "RAILWAY_CONFIG_ERROR",
|
|
202
|
+
message: `Failed to read ${filePath}: ${err instanceof Error ? err.message : String(err)}`
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return { raw: null, warnings };
|
|
207
|
+
}
|
|
208
|
+
function tryParseRailwayConfig(raw) {
|
|
209
|
+
if (!raw) {
|
|
210
|
+
return { config: null };
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
const config = raw.path.endsWith(".toml") ? import_smol_toml.default.parse(raw.content) : JSON.parse(raw.content);
|
|
214
|
+
return { config };
|
|
215
|
+
} catch (err) {
|
|
216
|
+
return {
|
|
217
|
+
config: null,
|
|
218
|
+
warning: {
|
|
219
|
+
code: "RAILWAY_PARSE_ERROR",
|
|
220
|
+
message: `Failed to parse ${raw.path}: ${err instanceof Error ? err.message : String(err)}`
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function deriveServiceName(dirPath) {
|
|
226
|
+
if (dirPath === ".") {
|
|
227
|
+
return "web";
|
|
228
|
+
}
|
|
229
|
+
const segments = dirPath.split("/");
|
|
230
|
+
return segments[segments.length - 1];
|
|
231
|
+
}
|
|
232
|
+
function combineBuildCommand(buildCommand, preDeployCommand) {
|
|
233
|
+
const preDeploy = Array.isArray(preDeployCommand) ? preDeployCommand.join(" && ") : preDeployCommand;
|
|
234
|
+
if (preDeploy && buildCommand) {
|
|
235
|
+
return `${buildCommand} && ${preDeploy}`;
|
|
236
|
+
} else if (preDeploy) {
|
|
237
|
+
return preDeploy;
|
|
238
|
+
} else {
|
|
239
|
+
return buildCommand;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function assignRoutePrefixes(services) {
|
|
243
|
+
const warnings = [];
|
|
244
|
+
const names = Object.keys(services);
|
|
245
|
+
if (names.length === 1) {
|
|
246
|
+
services[names[0]].routePrefix = "/";
|
|
247
|
+
return warnings;
|
|
248
|
+
}
|
|
249
|
+
const frontendNames = names.filter(
|
|
250
|
+
(name) => (0, import_utils.isFrontendFramework)(services[name].framework)
|
|
251
|
+
);
|
|
252
|
+
let rootName = null;
|
|
253
|
+
if (frontendNames.length === 1) {
|
|
254
|
+
rootName = frontendNames[0];
|
|
255
|
+
} else if (frontendNames.length > 1) {
|
|
256
|
+
rootName = frontendNames.find((n) => n === "frontend" || n === "web") ?? frontendNames.sort()[0];
|
|
257
|
+
warnings.push({
|
|
258
|
+
code: "MULTIPLE_FRONTENDS",
|
|
259
|
+
message: `Multiple frontend services detected (${frontendNames.join(", ")}). "${rootName}" was assigned routePrefix "/". Adjust manually if a different service should be the root.`
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
for (const name of names) {
|
|
263
|
+
services[name].routePrefix = name === rootName ? "/" : `/_/${name}`;
|
|
264
|
+
}
|
|
265
|
+
return warnings;
|
|
266
|
+
}
|
|
267
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
268
|
+
0 && (module.exports = {
|
|
269
|
+
detectRailwayServices
|
|
270
|
+
});
|
|
@@ -26,6 +26,7 @@ var import_routing_utils = require("@vercel/routing-utils");
|
|
|
26
26
|
var import_utils = require("./utils");
|
|
27
27
|
var import_resolve = require("./resolve");
|
|
28
28
|
var import_auto_detect = require("./auto-detect");
|
|
29
|
+
var import_detect_railway = require("./detect-railway");
|
|
29
30
|
const PREVIEW_DOMAIN_MISSING = [
|
|
30
31
|
{ type: "host", value: { suf: ".vercel.app" } },
|
|
31
32
|
{ type: "host", value: { suf: ".vercel.dev" } }
|
|
@@ -63,6 +64,9 @@ function toInferredLayoutConfig(services) {
|
|
|
63
64
|
if ((0, import_utils.isFrontendFramework)(service.framework)) {
|
|
64
65
|
serviceConfig.framework = service.framework;
|
|
65
66
|
}
|
|
67
|
+
if (typeof service.buildCommand === "string") {
|
|
68
|
+
serviceConfig.buildCommand = service.buildCommand;
|
|
69
|
+
}
|
|
66
70
|
inferredConfig[name] = serviceConfig;
|
|
67
71
|
}
|
|
68
72
|
return inferredConfig;
|
|
@@ -83,17 +87,41 @@ async function detectServices(options) {
|
|
|
83
87
|
const configuredServices = vercelConfig?.experimentalServices;
|
|
84
88
|
const hasConfiguredServices = configuredServices && Object.keys(configuredServices).length > 0;
|
|
85
89
|
if (!hasConfiguredServices) {
|
|
86
|
-
const
|
|
87
|
-
if (
|
|
90
|
+
const railwayResult = await (0, import_detect_railway.detectRailwayServices)({ fs: scopedFs });
|
|
91
|
+
if (railwayResult.errors.length > 0) {
|
|
88
92
|
return withResolvedResult({
|
|
89
93
|
services: [],
|
|
90
94
|
source: "auto-detected",
|
|
91
95
|
routes: emptyRoutes(),
|
|
92
|
-
errors:
|
|
93
|
-
warnings:
|
|
96
|
+
errors: railwayResult.errors,
|
|
97
|
+
warnings: railwayResult.warnings
|
|
94
98
|
});
|
|
95
99
|
}
|
|
96
|
-
if (
|
|
100
|
+
if (railwayResult.services) {
|
|
101
|
+
const result2 = await (0, import_resolve.resolveAllConfiguredServices)(
|
|
102
|
+
railwayResult.services,
|
|
103
|
+
scopedFs,
|
|
104
|
+
"generated"
|
|
105
|
+
);
|
|
106
|
+
const inferred = result2.errors.length === 0 && result2.services.length > 0 ? {
|
|
107
|
+
source: "railway",
|
|
108
|
+
config: toInferredLayoutConfig(railwayResult.services),
|
|
109
|
+
services: result2.services,
|
|
110
|
+
warnings: railwayResult.warnings
|
|
111
|
+
} : null;
|
|
112
|
+
return withResolvedResult(
|
|
113
|
+
{
|
|
114
|
+
services: [],
|
|
115
|
+
source: "auto-detected",
|
|
116
|
+
routes: emptyRoutes(),
|
|
117
|
+
errors: result2.errors,
|
|
118
|
+
warnings: railwayResult.warnings
|
|
119
|
+
},
|
|
120
|
+
inferred
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
const autoResult = await (0, import_auto_detect.autoDetectServices)({ fs: scopedFs });
|
|
124
|
+
if (autoResult.services && autoResult.errors.length === 0) {
|
|
97
125
|
const result2 = await (0, import_resolve.resolveAllConfiguredServices)(
|
|
98
126
|
autoResult.services,
|
|
99
127
|
scopedFs,
|
|
@@ -117,6 +145,14 @@ async function detectServices(options) {
|
|
|
117
145
|
warnings: []
|
|
118
146
|
} : null;
|
|
119
147
|
return withResolvedResult(resolved, inferred);
|
|
148
|
+
} else if (autoResult.errors.length > 0) {
|
|
149
|
+
return withResolvedResult({
|
|
150
|
+
services: [],
|
|
151
|
+
source: "auto-detected",
|
|
152
|
+
routes: emptyRoutes(),
|
|
153
|
+
errors: autoResult.errors,
|
|
154
|
+
warnings: []
|
|
155
|
+
});
|
|
120
156
|
}
|
|
121
157
|
return withResolvedResult({
|
|
122
158
|
services: [],
|
package/dist/services/types.d.ts
CHANGED
|
@@ -41,7 +41,7 @@ export interface ResolvedServicesResult {
|
|
|
41
41
|
warnings: ServiceDetectionWarning[];
|
|
42
42
|
}
|
|
43
43
|
export interface InferredServicesResult {
|
|
44
|
-
source: 'layout' | 'procfile';
|
|
44
|
+
source: 'layout' | 'procfile' | 'railway';
|
|
45
45
|
config: ServicesConfig;
|
|
46
46
|
services: Service[];
|
|
47
47
|
warnings: ServiceDetectionWarning[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vercel/fs-detectors",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.14.0",
|
|
4
4
|
"description": "Vercel filesystem detectors",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -19,10 +19,11 @@
|
|
|
19
19
|
"json5": "2.2.2",
|
|
20
20
|
"minimatch": "3.1.2",
|
|
21
21
|
"semver": "6.3.1",
|
|
22
|
+
"smol-toml": "1.5.2",
|
|
22
23
|
"@vercel/build-utils": "13.12.2",
|
|
23
|
-
"@vercel/routing-utils": "6.1.1",
|
|
24
24
|
"@vercel/error-utils": "2.0.3",
|
|
25
|
-
"@vercel/frameworks": "3.22.0"
|
|
25
|
+
"@vercel/frameworks": "3.22.0",
|
|
26
|
+
"@vercel/routing-utils": "6.1.1"
|
|
26
27
|
},
|
|
27
28
|
"devDependencies": {
|
|
28
29
|
"@types/glob": "7.2.0",
|