@vercel/fs-detectors 7.4.0 → 7.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  export { builderToFrameworks, detectBuilders, detectOutputDirectory, detectApiDirectory, detectApiExtensions, getProxyBuilder, validateProxyConfig, validateProxy, type Options as DetectBuildersOptions, type ProxyConfig, } from './detect-builders';
2
2
  export { detectServices, generateServiceRewrites, generateServicesRoutes, } from './services/detect-services';
3
3
  export { resolveAllConfiguredServicesV2, resolveConfiguredServiceV2, validateServiceConfigV2, } from './services/resolve-v2';
4
+ export { resolveAllConfiguredServices } from './services/resolve';
5
+ export { hasLegacyServicesConfig, migrateExperimentalServices, planExperimentalServicesMigration, } from './services/migrate';
6
+ export type { ExperimentalServicesMigrationPlan, LegacyServicesConfig, MigrateExperimentalServicesOptions, ServicesMigrationNote, } from './services/migrate';
4
7
  export { isExperimentalService, isExperimentalServiceV2, } from '@vercel/build-utils';
5
8
  export { autoDetectServices } from './services/auto-detect';
6
9
  export type { AutoDetectOptions, AutoDetectResult, } from './services/auto-detect';
package/dist/index.js CHANGED
@@ -51,14 +51,18 @@ __export(src_exports, {
51
51
  getProxyBuilder: () => import_detect_builders.getProxyBuilder,
52
52
  getWorkspacePackagePaths: () => import_get_workspace_package_paths.getWorkspacePackagePaths,
53
53
  getWorkspaces: () => import_get_workspaces.getWorkspaces,
54
+ hasLegacyServicesConfig: () => import_migrate.hasLegacyServicesConfig,
54
55
  isExperimentalService: () => import_build_utils.isExperimentalService,
55
56
  isExperimentalServiceV2: () => import_build_utils.isExperimentalServiceV2,
56
57
  isOfficialRuntime: () => import_is_official_runtime.isOfficialRuntime,
57
58
  isRouteOwningBuilder: () => import_utils.isRouteOwningBuilder,
58
59
  isStaticBuild: () => import_utils.isStaticBuild,
59
60
  isStaticRuntime: () => import_is_official_runtime.isStaticRuntime,
61
+ migrateExperimentalServices: () => import_migrate.migrateExperimentalServices,
60
62
  monorepoManagers: () => import_monorepo_managers.monorepoManagers,
61
63
  packageManagers: () => import_package_managers.packageManagers,
64
+ planExperimentalServicesMigration: () => import_migrate.planExperimentalServicesMigration,
65
+ resolveAllConfiguredServices: () => import_resolve.resolveAllConfiguredServices,
62
66
  resolveAllConfiguredServicesV2: () => import_resolve_v2.resolveAllConfiguredServicesV2,
63
67
  resolveConfiguredServiceV2: () => import_resolve_v2.resolveConfiguredServiceV2,
64
68
  validateProxy: () => import_detect_builders.validateProxy,
@@ -70,6 +74,8 @@ module.exports = __toCommonJS(src_exports);
70
74
  var import_detect_builders = require("./detect-builders");
71
75
  var import_detect_services = require("./services/detect-services");
72
76
  var import_resolve_v2 = require("./services/resolve-v2");
77
+ var import_resolve = require("./services/resolve");
78
+ var import_migrate = require("./services/migrate");
73
79
  var import_build_utils = require("@vercel/build-utils");
74
80
  var import_auto_detect = require("./services/auto-detect");
75
81
  var import_utils = require("./services/utils");
@@ -121,14 +127,18 @@ var import_detect_instrumentation = require("./detect-instrumentation");
121
127
  getProxyBuilder,
122
128
  getWorkspacePackagePaths,
123
129
  getWorkspaces,
130
+ hasLegacyServicesConfig,
124
131
  isExperimentalService,
125
132
  isExperimentalServiceV2,
126
133
  isOfficialRuntime,
127
134
  isRouteOwningBuilder,
128
135
  isStaticBuild,
129
136
  isStaticRuntime,
137
+ migrateExperimentalServices,
130
138
  monorepoManagers,
131
139
  packageManagers,
140
+ planExperimentalServicesMigration,
141
+ resolveAllConfiguredServices,
132
142
  resolveAllConfiguredServicesV2,
133
143
  resolveConfiguredServiceV2,
134
144
  validateProxy,
@@ -0,0 +1,71 @@
1
+ import type { Rewrite } from '@vercel/routing-utils';
2
+ import type { BuilderFunctions, ExperimentalService, ExperimentalServiceGroups, ExperimentalServices, Services } from '@vercel/build-utils';
3
+ import type { DetectorFilesystem } from '../detectors/filesystem';
4
+ import type { ServiceDetectionError } from './types';
5
+ /** Top-level build settings that are not allowed alongside `services`. */
6
+ declare const TOP_LEVEL_BUILD_KEYS: readonly ["functions", "installCommand", "buildCommand", "devCommand", "ignoreCommand", "outputDirectory", "framework"];
7
+ type TopLevelBuildKey = (typeof TOP_LEVEL_BUILD_KEYS)[number];
8
+ export interface ServicesMigrationNote {
9
+ code: string;
10
+ message: string;
11
+ serviceName?: string;
12
+ link?: string;
13
+ }
14
+ /** Config fields read by the migration; callers retain other fields. */
15
+ export interface LegacyServicesConfig {
16
+ experimentalServices?: ExperimentalServices;
17
+ experimentalServiceGroups?: ExperimentalServiceGroups;
18
+ experimentalServicesV2?: Services;
19
+ services?: Services;
20
+ build?: {
21
+ env?: Record<string, string>;
22
+ };
23
+ rewrites?: Rewrite[];
24
+ functions?: BuilderFunctions;
25
+ installCommand?: string | null;
26
+ buildCommand?: string | null;
27
+ devCommand?: string | null;
28
+ ignoreCommand?: string | null;
29
+ outputDirectory?: string | null;
30
+ framework?: string | null;
31
+ }
32
+ export interface ExperimentalServicesMigrationPlan {
33
+ patch: {
34
+ services: Services;
35
+ /** Append these after existing top-level rewrites. */
36
+ rewrites: Rewrite[];
37
+ /** Public URL defaults to merge into build.env, without replacing existing values. */
38
+ build?: {
39
+ env: Record<string, string>;
40
+ };
41
+ };
42
+ removeKeys: string[];
43
+ movedTopLevelKeys: Array<{
44
+ key: TopLevelBuildKey;
45
+ service: string;
46
+ }>;
47
+ renamedServices: Array<{
48
+ from: string;
49
+ to: string;
50
+ }>;
51
+ warnings: ServicesMigrationNote[];
52
+ unmigrated: ServicesMigrationNote[];
53
+ /** Blocking problems. A plan with errors must not be applied. */
54
+ errors: ServiceDetectionError[];
55
+ }
56
+ export declare function hasLegacyServicesConfig(config: LegacyServicesConfig | null | undefined): boolean;
57
+ /**
58
+ * Pure conversion of already-resolved services. Use `migrateExperimentalServices`
59
+ * for filesystem resolution and validation.
60
+ */
61
+ export declare function planExperimentalServicesMigration(config: LegacyServicesConfig, resolvedServices: ExperimentalService[]): ExperimentalServicesMigrationPlan;
62
+ export interface MigrateExperimentalServicesOptions {
63
+ fs: DetectorFilesystem;
64
+ /** Defaults to vercel.json/vercel.toml read from fs. */
65
+ config?: LegacyServicesConfig;
66
+ /** Defaults to resolving experimentalServices against fs. */
67
+ resolvedServices?: ExperimentalService[];
68
+ }
69
+ /** Resolve legacy services and validate the generated config against the same filesystem. */
70
+ export declare function migrateExperimentalServices(options: MigrateExperimentalServicesOptions): Promise<ExperimentalServicesMigrationPlan>;
71
+ export {};
@@ -0,0 +1,517 @@
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 migrate_exports = {};
20
+ __export(migrate_exports, {
21
+ hasLegacyServicesConfig: () => hasLegacyServicesConfig,
22
+ migrateExperimentalServices: () => migrateExperimentalServices,
23
+ planExperimentalServicesMigration: () => planExperimentalServicesMigration
24
+ });
25
+ module.exports = __toCommonJS(migrate_exports);
26
+ var import_path = require("path");
27
+ var import_frameworks = require("@vercel/frameworks");
28
+ var import_constants = require("./runtimes/constants");
29
+ var import_resolve = require("./resolve");
30
+ var import_resolve_v2 = require("./resolve-v2");
31
+ var import_utils = require("./utils");
32
+ const SERVICES_DOCS_URL = "https://vercel.com/docs/services";
33
+ const SERVICES_ROUTING_DOCS_URL = "https://vercel.com/docs/services/routing";
34
+ const SERVICES_BINDINGS_DOCS_URL = "https://vercel.com/docs/services/bindings";
35
+ const EXPERIMENTAL_SERVICES_DOCS_URL = "https://vercel.com/docs/services/experimental";
36
+ const TOP_LEVEL_BUILD_KEYS = [
37
+ "functions",
38
+ "installCommand",
39
+ "buildCommand",
40
+ "devCommand",
41
+ "ignoreCommand",
42
+ "outputDirectory",
43
+ "framework"
44
+ ];
45
+ const PREVIEW_DOMAIN_MISSING = [
46
+ { type: "host", value: { suf: ".vercel.app" } },
47
+ { type: "host", value: { suf: ".vercel.dev" } }
48
+ ];
49
+ function hasLegacyServicesConfig(config) {
50
+ return isNonEmptyObject(config?.experimentalServices) || isNonEmptyObject(config?.experimentalServicesV2);
51
+ }
52
+ function isNonEmptyObject(value) {
53
+ return value !== null && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length > 0;
54
+ }
55
+ function getServicesConfigConflict(config) {
56
+ const keys = ["services", "experimentalServices", "experimentalServicesV2"].filter((key) => config[key] !== void 0);
57
+ if (keys.length > 1) {
58
+ return {
59
+ code: "CONFLICTING_SERVICES_CONFIG",
60
+ message: `Cannot migrate a configuration containing ${keys.map((key) => `\`${key}\``).join(" and ")}. Keep only the configuration you intend to migrate; existing services will not be overwritten or discarded.`
61
+ };
62
+ }
63
+ return void 0;
64
+ }
65
+ function normalizePrefix(prefix) {
66
+ const withSlash = prefix.startsWith("/") ? prefix : `/${prefix}`;
67
+ return withSlash !== "/" ? withSlash.replace(/\/+$/, "") : withSlash;
68
+ }
69
+ function publicRewrite(prefix, service) {
70
+ return {
71
+ source: prefix === "/" ? "/(.*)" : `${prefix}(/.*)?`,
72
+ destination: { service }
73
+ };
74
+ }
75
+ function planExperimentalServicesMigration(config, resolvedServices) {
76
+ const conflict = getServicesConfigConflict(config);
77
+ if (conflict)
78
+ return emptyPlanWithErrors([conflict]);
79
+ const plan = emptyPlanWithErrors([]);
80
+ const legacy = config.experimentalServices;
81
+ if (isNonEmptyObject(config.experimentalServicesV2)) {
82
+ plan.patch.services = { ...config.experimentalServicesV2 };
83
+ plan.removeKeys.push("experimentalServicesV2");
84
+ return plan;
85
+ }
86
+ if (!isNonEmptyObject(legacy)) {
87
+ plan.errors.push({
88
+ code: "NO_LEGACY_SERVICES",
89
+ message: "No `experimentalServices` configuration found to migrate."
90
+ });
91
+ return plan;
92
+ }
93
+ plan.removeKeys.push("experimentalServices");
94
+ if (config.experimentalServiceGroups !== void 0) {
95
+ plan.removeKeys.push("experimentalServiceGroups");
96
+ plan.warnings.push({
97
+ code: "SERVICE_GROUPS_DROPPED",
98
+ message: "`experimentalServiceGroups` has no equivalent in `services` and was removed.",
99
+ link: SERVICES_DOCS_URL
100
+ });
101
+ }
102
+ const webServices = [];
103
+ const resolvedByName = new Map(resolvedServices.map((s) => [s.name, s]));
104
+ for (const [legacyName, serviceConfig] of Object.entries(legacy)) {
105
+ const resolved = resolvedByName.get(legacyName);
106
+ if (!resolved) {
107
+ plan.errors.push({
108
+ code: "UNRESOLVED_SERVICE",
109
+ serviceName: legacyName,
110
+ message: `Service "${legacyName}" could not be resolved. Fix the errors reported for it before migrating.`
111
+ });
112
+ continue;
113
+ }
114
+ if (resolved.type !== "web") {
115
+ plan.unmigrated.push(getUnmigratedNote(legacyName, resolved));
116
+ continue;
117
+ }
118
+ const name = legacyName.toLowerCase();
119
+ if (name.length > 64 || !/^[a-z]([a-z_-]*[a-z])?$/.test(name)) {
120
+ plan.errors.push({
121
+ code: "INVALID_SERVICE_NAME",
122
+ serviceName: legacyName,
123
+ message: `Service name "${legacyName}" is not valid for \`services\`. Names must be 1-64 characters, start and end with a lowercase letter, and contain only lowercase letters, hyphens, and underscores. Rename it in \`experimentalServices\` first.`
124
+ });
125
+ continue;
126
+ }
127
+ const conflict2 = webServices.find((s) => s.name === name);
128
+ if (conflict2) {
129
+ plan.errors.push({
130
+ code: "DUPLICATE_SERVICE_NAME",
131
+ serviceName: legacyName,
132
+ message: `Services "${conflict2.resolved.name}" and "${legacyName}" both map to "${name}". Rename one of them first.`
133
+ });
134
+ continue;
135
+ }
136
+ if (name !== legacyName) {
137
+ plan.renamedServices.push({ from: legacyName, to: name });
138
+ }
139
+ webServices.push({
140
+ name,
141
+ resolved,
142
+ config: serviceConfig,
143
+ prefix: normalizePrefix(resolved.routePrefix ?? "/")
144
+ });
145
+ }
146
+ if (plan.errors.length > 0)
147
+ return plan;
148
+ if (webServices.length === 0) {
149
+ plan.errors.push({
150
+ code: "NO_WEB_SERVICES",
151
+ message: "None of the configured services are web services. `services` only describes web services; see the manual steps for the others."
152
+ });
153
+ return plan;
154
+ }
155
+ for (const consumer of webServices) {
156
+ plan.patch.services[consumer.name] = convertWebService(
157
+ consumer,
158
+ webServices,
159
+ plan,
160
+ config.build?.env ?? {}
161
+ );
162
+ }
163
+ const subpaths = webServices.filter((svc) => svc.prefix !== "/").sort((a, b) => b.prefix.length - a.prefix.length);
164
+ plan.patch.rewrites = subpaths.map(
165
+ (svc) => publicRewrite(svc.prefix, svc.name)
166
+ );
167
+ for (const { name, prefix, resolved } of subpaths) {
168
+ const { subdomain } = resolved;
169
+ if (!subdomain)
170
+ continue;
171
+ const hostRewrite = {
172
+ source: "/(.*)",
173
+ has: [{ type: "host", value: { pre: `${subdomain}.` } }],
174
+ missing: PREVIEW_DOMAIN_MISSING,
175
+ destination: { service: name }
176
+ };
177
+ if (resolved.builder.use === "@vercel/next") {
178
+ plan.patch.rewrites.push({
179
+ ...hostRewrite,
180
+ source: "/",
181
+ destination: { service: name, path: prefix }
182
+ });
183
+ hostRewrite.source = "/:path(.*)";
184
+ hostRewrite.destination = { service: name, path: `${prefix}/:path` };
185
+ }
186
+ plan.patch.rewrites.push(hostRewrite);
187
+ plan.warnings.push({
188
+ code: "SUBDOMAIN_ROUTING",
189
+ serviceName: resolved.name,
190
+ message: `"${subdomain}.<your-domain>" routes to "${resolved.name}". Explicit service prefixes take precedence. Host routing still applies only to custom domains, not preview URLs.`,
191
+ link: SERVICES_ROUTING_DOCS_URL
192
+ });
193
+ }
194
+ plan.patch.rewrites.push(
195
+ ...webServices.filter((svc) => svc.prefix === "/").map((svc) => publicRewrite("/", svc.name))
196
+ );
197
+ const rootService = webServices.find((s) => s.resolved.workspace === ".");
198
+ for (const key of TOP_LEVEL_BUILD_KEYS) {
199
+ const value = config[key];
200
+ if (value === void 0)
201
+ continue;
202
+ plan.removeKeys.push(key);
203
+ if (key === "framework" && (value === "services" || value === null))
204
+ continue;
205
+ if (!rootService) {
206
+ plan.errors.push({
207
+ code: "TOP_LEVEL_SETTING_WITHOUT_ROOT_SERVICE",
208
+ message: `Top-level \`${key}\` is not allowed with \`services\` and there is no service with root "." to receive it. Move it into the relevant service manually.`
209
+ });
210
+ continue;
211
+ }
212
+ const target = plan.patch.services[rootService.name];
213
+ if (key === "functions") {
214
+ target.functions = {
215
+ ...value,
216
+ ...target.functions
217
+ };
218
+ } else if (value !== null && target[key] !== void 0) {
219
+ plan.warnings.push({
220
+ code: "TOP_LEVEL_SETTING_CONFLICT",
221
+ serviceName: rootService.resolved.name,
222
+ message: `Top-level \`${key}\` was dropped because service "${rootService.resolved.name}" already defines it.`
223
+ });
224
+ continue;
225
+ } else if (value !== null) {
226
+ target[key] = value;
227
+ }
228
+ plan.movedTopLevelKeys.push({ key, service: rootService.name });
229
+ }
230
+ if (Array.isArray(config.rewrites) && config.rewrites.length > 0) {
231
+ plan.warnings.push({
232
+ code: "EXISTING_REWRITES",
233
+ message: "Your existing top-level `rewrites` are kept ahead of the generated service rewrites. They are evaluated before a request is routed into a service, so rewrites that target a path inside a specific service should move into that service's own `rewrites`.",
234
+ link: SERVICES_ROUTING_DOCS_URL
235
+ });
236
+ }
237
+ return plan;
238
+ }
239
+ function convertWebService(consumer, webServices, plan, existingBuildEnv) {
240
+ const { resolved, config: serviceConfig, prefix } = consumer;
241
+ const { name: legacyName, entrypoint } = resolved;
242
+ const service = { root: resolved.workspace };
243
+ const warn = (code, message, link) => {
244
+ plan.warnings.push({
245
+ code,
246
+ serviceName: legacyName,
247
+ message: `Service "${legacyName}": ${message}`,
248
+ ...link ? { link } : {}
249
+ });
250
+ };
251
+ if (serviceConfig.framework)
252
+ service.framework = serviceConfig.framework;
253
+ const runtime = serviceConfig.runtime;
254
+ if (runtime) {
255
+ if (runtime in import_constants.RUNTIME_BUILDERS) {
256
+ service.runtime = runtime;
257
+ } else {
258
+ warn(
259
+ "RUNTIME_DROPPED",
260
+ `Runtime "${runtime}" is not supported (${Object.keys(import_constants.RUNTIME_BUILDERS).join(", ")}); it will be inferred from the entrypoint instead.`
261
+ );
262
+ }
263
+ }
264
+ if (entrypoint) {
265
+ const handler = resolved.builder.config?.handlerFunction;
266
+ service.entrypoint = typeof handler === "string" && entrypoint.endsWith(".py") ? `${entrypoint.slice(0, -3).replace(/\//g, ".")}:${handler}` : entrypoint;
267
+ }
268
+ if (serviceConfig.command !== void 0) {
269
+ service.command = serviceConfig.command;
270
+ }
271
+ if (serviceConfig.installCommand !== void 0) {
272
+ service.installCommand = serviceConfig.installCommand;
273
+ }
274
+ const buildCommand = (0, import_utils.combineBuildCommand)(
275
+ serviceConfig.buildCommand,
276
+ serviceConfig.preDeployCommand
277
+ );
278
+ if (buildCommand !== void 0)
279
+ service.buildCommand = buildCommand;
280
+ if (serviceConfig.preDeployCommand) {
281
+ warn(
282
+ "PRE_DEPLOY_COMMAND_FOLDED",
283
+ "`preDeployCommand` was appended to `buildCommand` and now runs at build time, not right before deployment."
284
+ );
285
+ }
286
+ if (serviceConfig.builder) {
287
+ warn(
288
+ "BUILDER_DROPPED",
289
+ `Builder "${serviceConfig.builder}" was removed; \`services\` selects it from the framework and runtime.`
290
+ );
291
+ }
292
+ if (serviceConfig.workspace) {
293
+ warn("WORKSPACE_REPLACED", "`workspace` is now expressed by `root`.");
294
+ }
295
+ const fnConfig = {};
296
+ for (const key of ["maxDuration", "includeFiles", "excludeFiles"]) {
297
+ if (serviceConfig[key] !== void 0)
298
+ fnConfig[key] = serviceConfig[key];
299
+ }
300
+ if (serviceConfig.memory !== void 0) {
301
+ warn(
302
+ "MEMORY_DROPPED",
303
+ "`memory` cannot be configured in vercel.json with Fluid compute; use the project's Functions settings instead.",
304
+ "https://vercel.com/docs/functions/configuring-functions/memory"
305
+ );
306
+ }
307
+ if (Object.keys(fnConfig).length > 0) {
308
+ if (entrypoint && resolved.runtime !== "container") {
309
+ service.functions = {
310
+ [import_path.posix.normalize(entrypoint)]: fnConfig
311
+ };
312
+ } else {
313
+ warn(
314
+ "FUNCTION_CONFIG_DROPPED",
315
+ `No file entrypoint for ${Object.keys(fnConfig).join(", ")}; configure them under this service's \`functions\` manually.`
316
+ );
317
+ }
318
+ }
319
+ const isStatic = (0, import_utils.isStaticBuild)(resolved);
320
+ const rewrites = [];
321
+ if (prefix !== "/") {
322
+ if (isStatic || resolved.builder.use !== "@vercel/next") {
323
+ rewrites.push({ source: `${prefix}/:path(.*)?`, destination: "/:path" });
324
+ }
325
+ if (isStatic) {
326
+ rewrites.push({ source: "/:path*", destination: "/index.html" });
327
+ warn(
328
+ "STATIC_SUBPATH",
329
+ `Static output is no longer nested under "${prefix}"; a service rewrite maps "${prefix}/*" onto it. Keep the framework base path (e.g. Vite \`base\`) at "${prefix}/".`,
330
+ SERVICES_ROUTING_DOCS_URL
331
+ );
332
+ } else if (resolved.builder.use === "@vercel/next") {
333
+ warn("NEXT_SUBPATH", `Keep \`basePath\` in next.config at "${prefix}".`);
334
+ } else {
335
+ warn(
336
+ "PREFIX_STRIP",
337
+ `A service rewrite keeps stripping "${prefix}", but ASGI \`root_path\` / WSGI \`SCRIPT_NAME\` is no longer set. Check redirects and generated URLs; remove the rewrite if the app should handle the prefix itself.`,
338
+ SERVICES_ROUTING_DOCS_URL
339
+ );
340
+ }
341
+ } else if (isStatic) {
342
+ rewrites.push({ source: "/(.*)", destination: "/index.html" });
343
+ }
344
+ if (rewrites.length > 0)
345
+ service.rewrites = rewrites;
346
+ const bindings = [];
347
+ const seenEnv = /* @__PURE__ */ new Set();
348
+ const consumerPrefix = import_frameworks.frameworkList.find(
349
+ (f) => f.slug === resolved.framework
350
+ )?.envPrefix;
351
+ const bindingsUnsupported = resolved.runtime === "go" || resolved.runtime === "rust";
352
+ const implicit = [];
353
+ const clientSide = [];
354
+ const unsupported = [];
355
+ const add = (target, env, explicit) => {
356
+ if (seenEnv.has(env))
357
+ return;
358
+ seenEnv.add(env);
359
+ if (consumerPrefix && env.startsWith(consumerPrefix)) {
360
+ if (Object.prototype.hasOwnProperty.call(existingBuildEnv, env))
361
+ return;
362
+ const defaults = (plan.patch.build ??= { env: {} }).env;
363
+ if (defaults[env] !== void 0 && defaults[env] !== target.prefix) {
364
+ plan.errors.push({
365
+ code: "CONFLICTING_BUILD_ENV",
366
+ serviceName: legacyName,
367
+ message: `Services use \`${env}\` for different public paths. Set a shared value in \`build.env\` or use distinct environment variable names before migrating.`
368
+ });
369
+ } else {
370
+ defaults[env] = target.prefix;
371
+ }
372
+ return;
373
+ }
374
+ if (isStatic) {
375
+ if (explicit) {
376
+ clientSide.push(`\`${env}="${target.prefix}"\``);
377
+ }
378
+ return;
379
+ }
380
+ if (bindingsUnsupported) {
381
+ unsupported.push(`\`${env}\``);
382
+ return;
383
+ }
384
+ if (!explicit)
385
+ implicit.push(`\`${env}\``);
386
+ bindings.push({
387
+ type: "service",
388
+ service: target.name,
389
+ format: "url",
390
+ env
391
+ });
392
+ };
393
+ for (const [env, ref] of Object.entries(serviceConfig.env ?? {})) {
394
+ if (ref?.type !== "service-ref")
395
+ continue;
396
+ const target = webServices.find((s) => s.resolved.name === ref.service);
397
+ if (!target) {
398
+ warn(
399
+ "SERVICE_REF_DROPPED",
400
+ `\`${env}\` referenced "${ref.service}", which is not a web service and cannot be bound.`
401
+ );
402
+ continue;
403
+ }
404
+ add(target, env, true);
405
+ }
406
+ for (const target of webServices) {
407
+ const env = `${target.resolved.name.replace(/-/g, "_").toUpperCase()}_URL`;
408
+ if (target !== consumer) {
409
+ add(target, env, false);
410
+ } else if (!seenEnv.has(env)) {
411
+ seenEnv.add(env);
412
+ warn(
413
+ "SELF_SERVICE_URL_DROPPED",
414
+ `Implicit self URL \`${env}\` is no longer injected; no self-binding was generated. If used at build time or on the server, define a project environment variable with the absolute public URL for "${prefix}" (including the origin), or derive it from the request.`,
415
+ SERVICES_BINDINGS_DOCS_URL
416
+ );
417
+ }
418
+ if (consumerPrefix)
419
+ add(target, `${consumerPrefix}${env}`, false);
420
+ }
421
+ if (implicit.length > 0) {
422
+ warn(
423
+ "IMPLICIT_URL_TO_BINDING",
424
+ `Previously implicit URLs now use bindings: ${implicit.join(", ")}. These resolve to internal URLs; remove unused bindings.`,
425
+ SERVICES_BINDINGS_DOCS_URL
426
+ );
427
+ }
428
+ if (clientSide.length > 0) {
429
+ warn(
430
+ "CLIENT_SIDE_SERVICE_URL",
431
+ `Bindings only cover server-side calls. Define any client-side URLs still in use as project environment variables: ${clientSide.join(", ")}.`,
432
+ SERVICES_BINDINGS_DOCS_URL
433
+ );
434
+ }
435
+ if (unsupported.length > 0) {
436
+ warn(
437
+ "BINDINGS_UNSUPPORTED_RUNTIME",
438
+ `The ${resolved.runtime} runtime does not support bindings; ${unsupported.join(", ")} will no longer be injected. Build a container image to use bindings, or call other services through public URLs.`,
439
+ SERVICES_BINDINGS_DOCS_URL
440
+ );
441
+ }
442
+ if (bindings.length > 0)
443
+ service.bindings = bindings;
444
+ return service;
445
+ }
446
+ function getUnmigratedNote(legacyName, resolved) {
447
+ const isPython = resolved.runtime === "python";
448
+ let guidance;
449
+ if (resolved.type === "cron" || resolved.trigger === "schedule") {
450
+ guidance = isPython ? "Declare the schedule in your Python app (for example with APScheduler in the web service) or expose the job as a route and add it to top-level `crons`." : "Expose the job as a route in a web service and add it to top-level `crons`.";
451
+ } else if (resolved.trigger === "workflow") {
452
+ guidance = isPython ? "Declare it as a `[[tool.vercel.workflows]]` entry in the owning service's pyproject.toml." : "Move it into a web service that uses the Workflow SDK.";
453
+ } else {
454
+ guidance = isPython ? "Declare it as a `[[tool.vercel.subscribers]]` entry in the owning service's pyproject.toml." : "Move the queue consumer into a web service that subscribes to the topic.";
455
+ }
456
+ return {
457
+ code: "UNSUPPORTED_SERVICE_TYPE",
458
+ serviceName: legacyName,
459
+ message: `"${legacyName}" (type "${resolved.type}"${resolved.trigger ? `, trigger "${resolved.trigger}"` : ""}) has no equivalent in \`services\` and was left out. ${guidance}`,
460
+ link: EXPERIMENTAL_SERVICES_DOCS_URL
461
+ };
462
+ }
463
+ async function migrateExperimentalServices(options) {
464
+ const { fs } = options;
465
+ let { config, resolvedServices } = options;
466
+ if (!config) {
467
+ const read = await (0, import_utils.readVercelConfig)(fs);
468
+ if (read.error)
469
+ return emptyPlanWithErrors([read.error]);
470
+ config = read.config ?? {};
471
+ }
472
+ const conflict = getServicesConfigConflict(config);
473
+ if (conflict)
474
+ return emptyPlanWithErrors([conflict]);
475
+ if (!resolvedServices && isNonEmptyObject(config.experimentalServices)) {
476
+ const result = await (0, import_resolve.resolveAllConfiguredServices)(
477
+ config.experimentalServices,
478
+ fs,
479
+ "configured"
480
+ );
481
+ if (result.errors.length > 0) {
482
+ return emptyPlanWithErrors(result.errors);
483
+ }
484
+ resolvedServices = result.services;
485
+ }
486
+ const plan = planExperimentalServicesMigration(
487
+ config,
488
+ resolvedServices ?? []
489
+ );
490
+ if (plan.errors.length > 0)
491
+ return plan;
492
+ const check = await (0, import_resolve_v2.resolveAllConfiguredServicesV2)(plan.patch.services, fs);
493
+ plan.errors.push(
494
+ ...check.errors.map((error) => ({
495
+ ...error,
496
+ message: `Generated \`services\` config is invalid: ${error.message}`
497
+ }))
498
+ );
499
+ return plan;
500
+ }
501
+ function emptyPlanWithErrors(errors) {
502
+ return {
503
+ patch: { services: {}, rewrites: [] },
504
+ removeKeys: [],
505
+ movedTopLevelKeys: [],
506
+ renamedServices: [],
507
+ warnings: [],
508
+ unmigrated: [],
509
+ errors
510
+ };
511
+ }
512
+ // Annotate the CommonJS export names for ESM import in node:
513
+ 0 && (module.exports = {
514
+ hasLegacyServicesConfig,
515
+ migrateExperimentalServices,
516
+ planExperimentalServicesMigration
517
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vercel/fs-detectors",
3
- "version": "7.4.0",
3
+ "version": "7.5.0",
4
4
  "description": "Vercel filesystem detectors",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -21,10 +21,10 @@
21
21
  "semver": "6.3.1",
22
22
  "smol-toml": "1.5.2",
23
23
  "@vercel/error-utils": "2.2.1",
24
- "@vercel/build-utils": "14.10.1",
24
+ "@vercel/frameworks": "3.34.0",
25
25
  "@vercel/python-analysis": "0.14.0",
26
26
  "@vercel/routing-utils": "6.6.0",
27
- "@vercel/frameworks": "3.34.0"
27
+ "@vercel/build-utils": "14.10.1"
28
28
  },
29
29
  "devDependencies": {
30
30
  "@types/glob": "7.2.0",