@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 +3 -0
- package/dist/index.js +10 -0
- package/dist/services/migrate.d.ts +71 -0
- package/dist/services/migrate.js +517 -0
- package/package.json +3 -3
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.
|
|
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/
|
|
24
|
+
"@vercel/frameworks": "3.34.0",
|
|
25
25
|
"@vercel/python-analysis": "0.14.0",
|
|
26
26
|
"@vercel/routing-utils": "6.6.0",
|
|
27
|
-
"@vercel/
|
|
27
|
+
"@vercel/build-utils": "14.10.1"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@types/glob": "7.2.0",
|