@vercel/fs-detectors 5.8.8 → 5.8.9

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.
@@ -117,7 +117,7 @@ async function detectServicesFrontendSubdir(fs, frontendFramework, frontendLocat
117
117
  const serviceName = frontendLocation.split("/").pop() || "frontend";
118
118
  services[serviceName] = {
119
119
  framework: frontendFramework.slug ?? void 0,
120
- workspace: frontendLocation,
120
+ entrypoint: frontendLocation,
121
121
  routePrefix: "/"
122
122
  };
123
123
  const backendResult = await detectBackendServices(fs);
@@ -215,7 +215,7 @@ async function detectServiceInDir(fs, dirPath, serviceName) {
215
215
  return {
216
216
  service: {
217
217
  framework: framework.slug ?? void 0,
218
- workspace: dirPath,
218
+ entrypoint: dirPath,
219
219
  routePrefix: `/_/${serviceName}`
220
220
  }
221
221
  };
@@ -53,7 +53,8 @@ async function detectServices(options) {
53
53
  if (autoResult.services) {
54
54
  const result2 = await (0, import_resolve.resolveAllConfiguredServices)(
55
55
  autoResult.services,
56
- scopedFs
56
+ scopedFs,
57
+ "generated"
57
58
  );
58
59
  const routes2 = generateServicesRoutes(result2.services);
59
60
  return {
@@ -77,7 +78,8 @@ async function detectServices(options) {
77
78
  }
78
79
  const result = await (0, import_resolve.resolveAllConfiguredServices)(
79
80
  configuredServices,
80
- scopedFs
81
+ scopedFs,
82
+ "configured"
81
83
  );
82
84
  const routes = generateServicesRoutes(result.services);
83
85
  return {
@@ -1,18 +1,33 @@
1
1
  import type { Service, ExperimentalServiceConfig, ExperimentalServices, ServiceDetectionError } from './types';
2
2
  import type { DetectorFilesystem } from '../detectors/filesystem';
3
+ interface ResolvedEntrypointPath {
4
+ normalized: string;
5
+ isDirectory: boolean;
6
+ }
7
+ type RoutePrefixSource = 'configured' | 'generated';
8
+ interface ResolveConfiguredServiceOptions {
9
+ name: string;
10
+ config: ExperimentalServiceConfig;
11
+ fs: DetectorFilesystem;
12
+ group?: string;
13
+ resolvedEntrypoint?: ResolvedEntrypointPath;
14
+ routePrefixSource?: RoutePrefixSource;
15
+ }
3
16
  /**
4
17
  * Validate a service configuration from vercel.json experimentalServices.
5
18
  */
6
19
  export declare function validateServiceConfig(name: string, config: ExperimentalServiceConfig): ServiceDetectionError | null;
20
+ export declare function validateServiceEntrypoint(name: string, config: ExperimentalServiceConfig, resolvedEntrypoint: ResolvedEntrypointPath): ServiceDetectionError | null;
7
21
  /**
8
22
  * Resolve a single service from user configuration.
9
23
  */
10
- export declare function resolveConfiguredService(name: string, config: ExperimentalServiceConfig, fs: DetectorFilesystem, group?: string): Promise<Service>;
24
+ export declare function resolveConfiguredService(options: ResolveConfiguredServiceOptions): Promise<Service>;
11
25
  /**
12
26
  * Resolve all services from vercel.json experimentalServices.
13
27
  * Validates each service configuration.
14
28
  */
15
- export declare function resolveAllConfiguredServices(services: ExperimentalServices, fs: DetectorFilesystem): Promise<{
29
+ export declare function resolveAllConfiguredServices(services: ExperimentalServices, fs: DetectorFilesystem, routePrefixSource?: RoutePrefixSource): Promise<{
16
30
  services: Service[];
17
31
  errors: ServiceDetectionError[];
18
32
  }>;
33
+ export {};
@@ -30,16 +30,44 @@ var resolve_exports = {};
30
30
  __export(resolve_exports, {
31
31
  resolveAllConfiguredServices: () => resolveAllConfiguredServices,
32
32
  resolveConfiguredService: () => resolveConfiguredService,
33
- validateServiceConfig: () => validateServiceConfig
33
+ validateServiceConfig: () => validateServiceConfig,
34
+ validateServiceEntrypoint: () => validateServiceEntrypoint
34
35
  });
35
36
  module.exports = __toCommonJS(resolve_exports);
36
37
  var import_path = require("path");
37
38
  var import_types = require("./types");
38
39
  var import_utils = require("./utils");
39
40
  var import_frameworks = __toESM(require("@vercel/frameworks"));
41
+ var import_detect_framework = require("../detect-framework");
40
42
  var import_routing_utils = require("@vercel/routing-utils");
41
43
  const frameworksBySlug = new Map(import_frameworks.default.map((f) => [f.slug, f]));
42
44
  const SERVICE_NAME_REGEX = /^[a-zA-Z]([a-zA-Z0-9_-]*[a-zA-Z0-9])?$/;
45
+ function normalizeServiceEntrypoint(entrypoint) {
46
+ const normalized = import_path.posix.normalize(entrypoint);
47
+ return normalized === "" ? "." : normalized;
48
+ }
49
+ async function resolveEntrypointPath({
50
+ fs,
51
+ serviceName,
52
+ entrypoint
53
+ }) {
54
+ const normalized = normalizeServiceEntrypoint(entrypoint);
55
+ if (!await fs.hasPath(normalized)) {
56
+ return {
57
+ error: {
58
+ code: "ENTRYPOINT_NOT_FOUND",
59
+ message: `Service "${serviceName}" has entrypoint "${entrypoint}" but that path does not exist.`,
60
+ serviceName
61
+ }
62
+ };
63
+ }
64
+ return {
65
+ entrypoint: {
66
+ normalized,
67
+ isDirectory: !await fs.isFile(normalized)
68
+ }
69
+ };
70
+ }
43
71
  function toWorkspaceRelativeEntrypoint(entrypoint, workspace) {
44
72
  const normalizedEntrypoint = import_path.posix.normalize(entrypoint);
45
73
  if (workspace === ".") {
@@ -95,6 +123,33 @@ async function inferWorkspaceFromNearestManifest({
95
123
  }
96
124
  return void 0;
97
125
  }
126
+ async function detectFrameworkFromWorkspace({
127
+ fs,
128
+ workspace,
129
+ serviceName
130
+ }) {
131
+ const serviceFs = workspace === "." ? fs : fs.chdir(workspace);
132
+ const frameworks = await (0, import_detect_framework.detectFrameworks)({
133
+ fs: serviceFs,
134
+ frameworkList: import_frameworks.default
135
+ });
136
+ if (frameworks.length > 1) {
137
+ const frameworkNames = frameworks.map((f) => f.name).join(", ");
138
+ return {
139
+ error: {
140
+ code: "MULTIPLE_FRAMEWORKS_SERVICE",
141
+ message: `Multiple frameworks detected in ${workspace === "." ? "project root" : `${workspace}/`}: ${frameworkNames}. Specify "framework" explicitly in experimentalServices.`,
142
+ serviceName
143
+ }
144
+ };
145
+ }
146
+ if (frameworks.length === 1) {
147
+ return {
148
+ framework: frameworks[0].slug ?? void 0
149
+ };
150
+ }
151
+ return {};
152
+ }
98
153
  function isReservedServiceRoutePrefix(routePrefix) {
99
154
  const normalized = (0, import_routing_utils.normalizeRoutePrefix)(routePrefix);
100
155
  return normalized === import_utils.INTERNAL_SERVICE_PREFIX || normalized.startsWith(`${import_utils.INTERNAL_SERVICE_PREFIX}/`);
@@ -174,7 +229,10 @@ function validateServiceConfig(name, config) {
174
229
  serviceName: name
175
230
  };
176
231
  }
177
- if (hasEntrypoint && !hasBuilderOrRuntime && !hasFramework) {
232
+ return null;
233
+ }
234
+ function validateServiceEntrypoint(name, config, resolvedEntrypoint) {
235
+ if (!resolvedEntrypoint.isDirectory && !config.builder && !config.runtime && !config.framework) {
178
236
  const runtime = (0, import_utils.inferServiceRuntime)({ entrypoint: config.entrypoint });
179
237
  if (!runtime) {
180
238
  const supported = Object.keys(import_types.ENTRYPOINT_EXTENSIONS).join(", ");
@@ -187,22 +245,52 @@ function validateServiceConfig(name, config) {
187
245
  }
188
246
  return null;
189
247
  }
190
- async function resolveConfiguredService(name, config, fs, group) {
248
+ async function resolveConfiguredService(options) {
249
+ const {
250
+ name,
251
+ config,
252
+ fs,
253
+ group,
254
+ resolvedEntrypoint,
255
+ routePrefixSource = "configured"
256
+ } = options;
191
257
  const type = config.type || "web";
192
- const inferredRuntime = (0, import_utils.inferServiceRuntime)(config);
193
- let workspace = config.workspace || ".";
194
- let resolvedEntrypoint = config.entrypoint;
195
- if (!config.workspace) {
258
+ const rawEntrypoint = config.entrypoint;
259
+ let resolvedEntrypointPath = resolvedEntrypoint;
260
+ if (!resolvedEntrypointPath && typeof rawEntrypoint === "string") {
261
+ const resolved = await resolveEntrypointPath({
262
+ fs,
263
+ serviceName: name,
264
+ entrypoint: rawEntrypoint
265
+ });
266
+ resolvedEntrypointPath = resolved.entrypoint;
267
+ }
268
+ if (typeof rawEntrypoint === "string" && !resolvedEntrypointPath) {
269
+ throw new Error(
270
+ `Failed to resolve entrypoint "${rawEntrypoint}" for service "${name}".`
271
+ );
272
+ }
273
+ const normalizedEntrypoint = resolvedEntrypointPath?.normalized;
274
+ const entrypointIsDirectory = Boolean(resolvedEntrypointPath?.isDirectory);
275
+ const inferredRuntime = (0, import_utils.inferServiceRuntime)({
276
+ ...config,
277
+ entrypoint: entrypointIsDirectory ? void 0 : normalizedEntrypoint
278
+ });
279
+ let workspace = ".";
280
+ let resolvedEntrypointFile = entrypointIsDirectory || !normalizedEntrypoint ? void 0 : normalizedEntrypoint;
281
+ if (entrypointIsDirectory && normalizedEntrypoint) {
282
+ workspace = normalizedEntrypoint;
283
+ } else {
196
284
  const inferredWorkspace = await inferWorkspaceFromNearestManifest({
197
285
  fs,
198
- entrypoint: resolvedEntrypoint,
286
+ entrypoint: resolvedEntrypointFile,
199
287
  runtime: inferredRuntime
200
288
  });
201
289
  if (inferredWorkspace) {
202
290
  workspace = inferredWorkspace;
203
- if (resolvedEntrypoint) {
204
- resolvedEntrypoint = toWorkspaceRelativeEntrypoint(
205
- resolvedEntrypoint,
291
+ if (resolvedEntrypointFile) {
292
+ resolvedEntrypointFile = toWorkspaceRelativeEntrypoint(
293
+ resolvedEntrypointFile,
206
294
  inferredWorkspace
207
295
  );
208
296
  }
@@ -215,13 +303,13 @@ async function resolveConfiguredService(name, config, fs, group) {
215
303
  if (config.framework) {
216
304
  const framework = frameworksBySlug.get(config.framework);
217
305
  builderUse = framework?.useRuntime?.use || "@vercel/static-build";
218
- builderSrc = resolvedEntrypoint || framework?.useRuntime?.src || "package.json";
306
+ builderSrc = resolvedEntrypointFile || framework?.useRuntime?.src || "package.json";
219
307
  } else if (config.builder) {
220
308
  builderUse = config.builder;
221
- builderSrc = resolvedEntrypoint;
309
+ builderSrc = resolvedEntrypointFile;
222
310
  } else {
223
311
  builderUse = (0, import_utils.getBuilderForRuntime)(inferredRuntime);
224
- builderSrc = resolvedEntrypoint;
312
+ builderSrc = resolvedEntrypointFile;
225
313
  }
226
314
  const routePrefix = type === "web" && config.routePrefix ? config.routePrefix.startsWith("/") ? config.routePrefix : `/${config.routePrefix}` : void 0;
227
315
  const isRoot = workspace === ".";
@@ -254,8 +342,9 @@ async function resolveConfiguredService(name, config, fs, group) {
254
342
  type,
255
343
  group,
256
344
  workspace,
257
- entrypoint: resolvedEntrypoint,
345
+ entrypoint: resolvedEntrypointFile,
258
346
  routePrefix,
347
+ routePrefixSource: type === "web" && typeof routePrefix === "string" ? routePrefixSource : void 0,
259
348
  framework: config.framework,
260
349
  builder: {
261
350
  src: builderSrc,
@@ -270,7 +359,7 @@ async function resolveConfiguredService(name, config, fs, group) {
270
359
  consumer
271
360
  };
272
361
  }
273
- async function resolveAllConfiguredServices(services, fs) {
362
+ async function resolveAllConfiguredServices(services, fs, routePrefixSource = "configured") {
274
363
  const resolved = [];
275
364
  const errors = [];
276
365
  const webServicesByRoutePrefix = /* @__PURE__ */ new Map();
@@ -281,7 +370,63 @@ async function resolveAllConfiguredServices(services, fs) {
281
370
  errors.push(validationError);
282
371
  continue;
283
372
  }
284
- const service = await resolveConfiguredService(name, serviceConfig, fs);
373
+ let resolvedEntrypoint;
374
+ if (typeof serviceConfig.entrypoint === "string") {
375
+ const resolvedPath = await resolveEntrypointPath({
376
+ fs,
377
+ serviceName: name,
378
+ entrypoint: serviceConfig.entrypoint
379
+ });
380
+ if (resolvedPath.error) {
381
+ errors.push(resolvedPath.error);
382
+ continue;
383
+ }
384
+ resolvedEntrypoint = resolvedPath.entrypoint;
385
+ }
386
+ if (resolvedEntrypoint) {
387
+ const entrypointError = validateServiceEntrypoint(
388
+ name,
389
+ serviceConfig,
390
+ resolvedEntrypoint
391
+ );
392
+ if (entrypointError) {
393
+ errors.push(entrypointError);
394
+ continue;
395
+ }
396
+ }
397
+ let resolvedConfig = serviceConfig;
398
+ const shouldDetectFramework = !serviceConfig.framework && Boolean(resolvedEntrypoint?.isDirectory);
399
+ if (shouldDetectFramework) {
400
+ const workspace = resolvedEntrypoint.normalized;
401
+ const { framework, error } = await detectFrameworkFromWorkspace({
402
+ fs,
403
+ workspace,
404
+ serviceName: name
405
+ });
406
+ if (error) {
407
+ errors.push(error);
408
+ continue;
409
+ }
410
+ if (!framework) {
411
+ errors.push({
412
+ code: "MISSING_SERVICE_FRAMEWORK",
413
+ message: `Service "${name}" uses directory entrypoint "${serviceConfig.entrypoint}" but no framework could be detected in "${workspace}". Specify "framework" explicitly or use a file entrypoint.`,
414
+ serviceName: name
415
+ });
416
+ continue;
417
+ }
418
+ resolvedConfig = {
419
+ ...serviceConfig,
420
+ framework
421
+ };
422
+ }
423
+ const service = await resolveConfiguredService({
424
+ name,
425
+ config: resolvedConfig,
426
+ fs,
427
+ resolvedEntrypoint,
428
+ routePrefixSource
429
+ });
285
430
  if (service.type === "web" && typeof service.routePrefix === "string") {
286
431
  const normalizedRoutePrefix = (0, import_routing_utils.normalizeRoutePrefix)(service.routePrefix);
287
432
  const existingServiceName = webServicesByRoutePrefix.get(
@@ -305,5 +450,6 @@ async function resolveAllConfiguredServices(services, fs) {
305
450
  0 && (module.exports = {
306
451
  resolveAllConfiguredServices,
307
452
  resolveConfiguredService,
308
- validateServiceConfig
453
+ validateServiceConfig,
454
+ validateServiceEntrypoint
309
455
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vercel/fs-detectors",
3
- "version": "5.8.8",
3
+ "version": "5.8.9",
4
4
  "description": "Vercel filesystem detectors",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -19,9 +19,9 @@
19
19
  "json5": "2.2.2",
20
20
  "minimatch": "3.1.2",
21
21
  "semver": "6.3.1",
22
- "@vercel/frameworks": "3.18.0",
23
22
  "@vercel/error-utils": "2.0.3",
24
- "@vercel/routing-utils": "5.3.3"
23
+ "@vercel/routing-utils": "5.3.3",
24
+ "@vercel/frameworks": "3.18.0"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@types/glob": "7.2.0",
@@ -32,7 +32,7 @@
32
32
  "@types/semver": "7.3.10",
33
33
  "jest-junit": "16.0.0",
34
34
  "typescript": "4.9.5",
35
- "@vercel/build-utils": "13.4.1"
35
+ "@vercel/build-utils": "13.4.3"
36
36
  },
37
37
  "scripts": {
38
38
  "build": "node ../../utils/build.mjs",