@vercel/fs-detectors 5.13.1 → 5.14.1

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 autoResult = await (0, import_auto_detect.autoDetectServices)({ fs: scopedFs });
87
- if (autoResult.errors.length > 0) {
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: autoResult.errors,
93
- warnings: []
96
+ errors: railwayResult.errors,
97
+ warnings: railwayResult.warnings
94
98
  });
95
99
  }
96
- if (autoResult.services) {
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: [],
@@ -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.13.1",
3
+ "version": "5.14.1",
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
- "@vercel/build-utils": "13.12.2",
23
- "@vercel/routing-utils": "6.1.1",
22
+ "smol-toml": "1.5.2",
23
+ "@vercel/frameworks": "3.23.0",
24
24
  "@vercel/error-utils": "2.0.3",
25
- "@vercel/frameworks": "3.22.0"
25
+ "@vercel/build-utils": "13.12.2",
26
+ "@vercel/routing-utils": "6.1.1"
26
27
  },
27
28
  "devDependencies": {
28
29
  "@types/glob": "7.2.0",