@vercel/build-utils 4.2.1 → 5.0.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.
- package/dist/fs/run-user-scripts.d.ts +1 -1
- package/dist/fs/run-user-scripts.js +28 -7
- package/dist/index.d.ts +0 -14
- package/dist/index.js +36 -7552
- package/package.json +2 -3
- package/dist/detect-builders.d.ts +0 -37
- package/dist/detect-builders.js +0 -841
- package/dist/detect-file-system-api.d.ts +0 -34
- package/dist/detect-file-system-api.js +0 -173
- package/dist/detect-framework.d.ts +0 -12
- package/dist/detect-framework.js +0 -60
- package/dist/detectors/filesystem.d.ts +0 -54
- package/dist/detectors/filesystem.js +0 -78
- package/dist/fs/get-glob-fs.d.ts +0 -6
- package/dist/fs/get-glob-fs.js +0 -71
- package/dist/get-project-paths.d.ts +0 -9
- package/dist/get-project-paths.js +0 -39
- package/dist/monorepos/monorepo-managers.d.ts +0 -16
- package/dist/monorepos/monorepo-managers.js +0 -34
- package/dist/workspaces/get-workspace-package-paths.d.ts +0 -11
- package/dist/workspaces/get-workspace-package-paths.js +0 -62
- package/dist/workspaces/get-workspaces.d.ts +0 -12
- package/dist/workspaces/get-workspaces.js +0 -35
- package/dist/workspaces/workspace-managers.d.ts +0 -16
- package/dist/workspaces/workspace-managers.js +0 -60
package/dist/detect-builders.js
DELETED
@@ -1,841 +0,0 @@
|
|
1
|
-
"use strict";
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
4
|
-
};
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
6
|
-
exports.detectBuilders = exports.detectOutputDirectory = exports.detectApiDirectory = exports.detectApiExtensions = exports.sortFiles = void 0;
|
7
|
-
const minimatch_1 = __importDefault(require("minimatch"));
|
8
|
-
const semver_1 = require("semver");
|
9
|
-
const path_1 = require("path");
|
10
|
-
const frameworks_1 = __importDefault(require("@vercel/frameworks"));
|
11
|
-
const _1 = require("./");
|
12
|
-
const slugToFramework = new Map(frameworks_1.default.map(f => [f.slug, f]));
|
13
|
-
// We need to sort the file paths by alphabet to make
|
14
|
-
// sure the routes stay in the same order e.g. for deduping
|
15
|
-
function sortFiles(fileA, fileB) {
|
16
|
-
return fileA.localeCompare(fileB);
|
17
|
-
}
|
18
|
-
exports.sortFiles = sortFiles;
|
19
|
-
function detectApiExtensions(builders) {
|
20
|
-
return new Set(builders
|
21
|
-
.filter((b) => Boolean(b.config && b.config.zeroConfig && b.src?.startsWith('api/')))
|
22
|
-
.map(b => path_1.extname(b.src))
|
23
|
-
.filter(Boolean));
|
24
|
-
}
|
25
|
-
exports.detectApiExtensions = detectApiExtensions;
|
26
|
-
function detectApiDirectory(builders) {
|
27
|
-
// TODO: We eventually want to save the api directory to
|
28
|
-
// builder.config.apiDirectory so it is only detected once
|
29
|
-
const found = builders.some(b => b.config && b.config.zeroConfig && b.src?.startsWith('api/'));
|
30
|
-
return found ? 'api' : null;
|
31
|
-
}
|
32
|
-
exports.detectApiDirectory = detectApiDirectory;
|
33
|
-
// TODO: Replace this function with `config.outputDirectory`
|
34
|
-
function getPublicBuilder(builders) {
|
35
|
-
for (const builder of builders) {
|
36
|
-
if (typeof builder.src === 'string' &&
|
37
|
-
_1.isOfficialRuntime('static', builder.use) &&
|
38
|
-
/^.*\/\*\*\/\*$/.test(builder.src) &&
|
39
|
-
builder.config?.zeroConfig === true) {
|
40
|
-
return builder;
|
41
|
-
}
|
42
|
-
}
|
43
|
-
return null;
|
44
|
-
}
|
45
|
-
function detectOutputDirectory(builders) {
|
46
|
-
// TODO: We eventually want to save the output directory to
|
47
|
-
// builder.config.outputDirectory so it is only detected once
|
48
|
-
const publicBuilder = getPublicBuilder(builders);
|
49
|
-
return publicBuilder ? publicBuilder.src.replace('/**/*', '') : null;
|
50
|
-
}
|
51
|
-
exports.detectOutputDirectory = detectOutputDirectory;
|
52
|
-
async function detectBuilders(files, pkg, options = {}) {
|
53
|
-
const errors = [];
|
54
|
-
const warnings = [];
|
55
|
-
const apiBuilders = [];
|
56
|
-
let frontendBuilder = null;
|
57
|
-
const functionError = validateFunctions(options);
|
58
|
-
if (functionError) {
|
59
|
-
return {
|
60
|
-
builders: null,
|
61
|
-
errors: [functionError],
|
62
|
-
warnings,
|
63
|
-
defaultRoutes: null,
|
64
|
-
redirectRoutes: null,
|
65
|
-
rewriteRoutes: null,
|
66
|
-
errorRoutes: null,
|
67
|
-
limitedRoutes: null,
|
68
|
-
};
|
69
|
-
}
|
70
|
-
const sortedFiles = files.sort(sortFiles);
|
71
|
-
const apiSortedFiles = files.sort(sortFilesBySegmentCount);
|
72
|
-
// Keep track of functions that are used
|
73
|
-
const usedFunctions = new Set();
|
74
|
-
const addToUsedFunctions = (builder) => {
|
75
|
-
const key = Object.keys(builder.config.functions || {})[0];
|
76
|
-
if (key)
|
77
|
-
usedFunctions.add(key);
|
78
|
-
};
|
79
|
-
const absolutePathCache = new Map();
|
80
|
-
const { projectSettings = {} } = options;
|
81
|
-
const { buildCommand, outputDirectory, framework } = projectSettings;
|
82
|
-
const ignoreRuntimes = new Set(slugToFramework.get(framework || '')?.ignoreRuntimes);
|
83
|
-
const withTag = options.tag ? `@${options.tag}` : '';
|
84
|
-
const apiMatches = getApiMatches()
|
85
|
-
.filter(b => !ignoreRuntimes.has(b.use))
|
86
|
-
.map(b => {
|
87
|
-
b.use = `${b.use}${withTag}`;
|
88
|
-
return b;
|
89
|
-
});
|
90
|
-
// If either is missing we'll make the frontend static
|
91
|
-
const makeFrontendStatic = buildCommand === '' || outputDirectory === '';
|
92
|
-
// Only used when there is no frontend builder,
|
93
|
-
// but prevents looping over the files again.
|
94
|
-
const usedOutputDirectory = outputDirectory || 'public';
|
95
|
-
let hasUsedOutputDirectory = false;
|
96
|
-
let hasNoneApiFiles = false;
|
97
|
-
let hasNextApiFiles = false;
|
98
|
-
let fallbackEntrypoint = null;
|
99
|
-
const apiRoutes = [];
|
100
|
-
const dynamicRoutes = [];
|
101
|
-
// API
|
102
|
-
for (const fileName of sortedFiles) {
|
103
|
-
const apiBuilder = maybeGetApiBuilder(fileName, apiMatches, options);
|
104
|
-
if (apiBuilder) {
|
105
|
-
const { routeError, apiRoute, isDynamic } = getApiRoute(fileName, apiSortedFiles, options, absolutePathCache);
|
106
|
-
if (routeError) {
|
107
|
-
return {
|
108
|
-
builders: null,
|
109
|
-
errors: [routeError],
|
110
|
-
warnings,
|
111
|
-
defaultRoutes: null,
|
112
|
-
redirectRoutes: null,
|
113
|
-
rewriteRoutes: null,
|
114
|
-
errorRoutes: null,
|
115
|
-
limitedRoutes: null,
|
116
|
-
};
|
117
|
-
}
|
118
|
-
if (apiRoute) {
|
119
|
-
apiRoutes.push(apiRoute);
|
120
|
-
if (isDynamic) {
|
121
|
-
dynamicRoutes.push(apiRoute);
|
122
|
-
}
|
123
|
-
}
|
124
|
-
addToUsedFunctions(apiBuilder);
|
125
|
-
apiBuilders.push(apiBuilder);
|
126
|
-
continue;
|
127
|
-
}
|
128
|
-
if (!hasUsedOutputDirectory &&
|
129
|
-
fileName.startsWith(`${usedOutputDirectory}/`)) {
|
130
|
-
hasUsedOutputDirectory = true;
|
131
|
-
}
|
132
|
-
if (!hasNoneApiFiles &&
|
133
|
-
!fileName.startsWith('api/') &&
|
134
|
-
fileName !== 'package.json') {
|
135
|
-
hasNoneApiFiles = true;
|
136
|
-
}
|
137
|
-
if (!hasNextApiFiles &&
|
138
|
-
(fileName.startsWith('pages/api') || fileName.startsWith('src/pages/api'))) {
|
139
|
-
hasNextApiFiles = true;
|
140
|
-
}
|
141
|
-
if (!fallbackEntrypoint &&
|
142
|
-
buildCommand &&
|
143
|
-
!fileName.includes('/') &&
|
144
|
-
fileName !== 'now.json' &&
|
145
|
-
fileName !== 'vercel.json') {
|
146
|
-
fallbackEntrypoint = fileName;
|
147
|
-
}
|
148
|
-
}
|
149
|
-
if (!makeFrontendStatic &&
|
150
|
-
(hasBuildScript(pkg) || buildCommand || framework)) {
|
151
|
-
// Framework or Build
|
152
|
-
frontendBuilder = detectFrontBuilder(pkg, files, usedFunctions, fallbackEntrypoint, options);
|
153
|
-
}
|
154
|
-
else {
|
155
|
-
if (pkg &&
|
156
|
-
!makeFrontendStatic &&
|
157
|
-
!apiBuilders.length &&
|
158
|
-
!options.ignoreBuildScript) {
|
159
|
-
// We only show this error when there are no api builders
|
160
|
-
// since the dependencies of the pkg could be used for those
|
161
|
-
errors.push(getMissingBuildScriptError());
|
162
|
-
return {
|
163
|
-
errors,
|
164
|
-
warnings,
|
165
|
-
builders: null,
|
166
|
-
redirectRoutes: null,
|
167
|
-
defaultRoutes: null,
|
168
|
-
rewriteRoutes: null,
|
169
|
-
errorRoutes: null,
|
170
|
-
limitedRoutes: null,
|
171
|
-
};
|
172
|
-
}
|
173
|
-
// If `outputDirectory` is an empty string,
|
174
|
-
// we'll default to the root directory.
|
175
|
-
if (hasUsedOutputDirectory && outputDirectory !== '') {
|
176
|
-
frontendBuilder = {
|
177
|
-
use: '@vercel/static',
|
178
|
-
src: `${usedOutputDirectory}/**/*`,
|
179
|
-
config: {
|
180
|
-
zeroConfig: true,
|
181
|
-
outputDirectory: usedOutputDirectory,
|
182
|
-
},
|
183
|
-
};
|
184
|
-
}
|
185
|
-
else if (apiBuilders.length && hasNoneApiFiles) {
|
186
|
-
// Everything besides the api directory
|
187
|
-
// and package.json can be served as static files
|
188
|
-
frontendBuilder = {
|
189
|
-
use: '@vercel/static',
|
190
|
-
src: '!{api/**,package.json,middleware.[jt]s}',
|
191
|
-
config: {
|
192
|
-
zeroConfig: true,
|
193
|
-
},
|
194
|
-
};
|
195
|
-
}
|
196
|
-
}
|
197
|
-
const unusedFunctionError = checkUnusedFunctions(frontendBuilder, usedFunctions, options);
|
198
|
-
if (unusedFunctionError) {
|
199
|
-
return {
|
200
|
-
builders: null,
|
201
|
-
errors: [unusedFunctionError],
|
202
|
-
warnings,
|
203
|
-
redirectRoutes: null,
|
204
|
-
defaultRoutes: null,
|
205
|
-
rewriteRoutes: null,
|
206
|
-
errorRoutes: null,
|
207
|
-
limitedRoutes: null,
|
208
|
-
};
|
209
|
-
}
|
210
|
-
const builders = [];
|
211
|
-
if (apiBuilders.length) {
|
212
|
-
builders.push(...apiBuilders);
|
213
|
-
}
|
214
|
-
if (frontendBuilder) {
|
215
|
-
builders.push(frontendBuilder);
|
216
|
-
if (hasNextApiFiles &&
|
217
|
-
apiBuilders.some(b => _1.isOfficialRuntime('node', b.use))) {
|
218
|
-
warnings.push({
|
219
|
-
code: 'conflicting_files',
|
220
|
-
message: 'When using Next.js, it is recommended to place Node.js Serverless Functions inside of the `pages/api` (provided by Next.js) directory instead of `api` (provided by Vercel).',
|
221
|
-
link: 'https://nextjs.org/docs/api-routes/introduction',
|
222
|
-
action: 'Learn More',
|
223
|
-
});
|
224
|
-
}
|
225
|
-
}
|
226
|
-
const routesResult = getRouteResult(pkg, apiRoutes, dynamicRoutes, usedOutputDirectory, apiBuilders, frontendBuilder, options);
|
227
|
-
return {
|
228
|
-
warnings,
|
229
|
-
builders: builders.length ? builders : null,
|
230
|
-
errors: errors.length ? errors : null,
|
231
|
-
redirectRoutes: routesResult.redirectRoutes,
|
232
|
-
defaultRoutes: routesResult.defaultRoutes,
|
233
|
-
rewriteRoutes: routesResult.rewriteRoutes,
|
234
|
-
errorRoutes: routesResult.errorRoutes,
|
235
|
-
limitedRoutes: routesResult.limitedRoutes,
|
236
|
-
};
|
237
|
-
}
|
238
|
-
exports.detectBuilders = detectBuilders;
|
239
|
-
function maybeGetApiBuilder(fileName, apiMatches, options) {
|
240
|
-
const middleware = fileName === 'middleware.js' || fileName === 'middleware.ts';
|
241
|
-
// Root-level Middleware file is handled by `@vercel/next`, so don't
|
242
|
-
// schedule a separate Builder when "nextjs" framework is selected
|
243
|
-
if (middleware && options.projectSettings?.framework === 'nextjs') {
|
244
|
-
return null;
|
245
|
-
}
|
246
|
-
if (!(fileName.startsWith('api/') || middleware)) {
|
247
|
-
return null;
|
248
|
-
}
|
249
|
-
if (fileName.includes('/.')) {
|
250
|
-
return null;
|
251
|
-
}
|
252
|
-
if (fileName.includes('/_')) {
|
253
|
-
return null;
|
254
|
-
}
|
255
|
-
if (fileName.includes('/node_modules/')) {
|
256
|
-
return null;
|
257
|
-
}
|
258
|
-
if (fileName.endsWith('.d.ts')) {
|
259
|
-
return null;
|
260
|
-
}
|
261
|
-
const match = apiMatches.find(({ src = '**' }) => {
|
262
|
-
return src === fileName || minimatch_1.default(fileName, src);
|
263
|
-
});
|
264
|
-
const { fnPattern, func } = getFunction(fileName, options);
|
265
|
-
const use = func?.runtime || match?.use;
|
266
|
-
if (!use) {
|
267
|
-
return null;
|
268
|
-
}
|
269
|
-
const config = { zeroConfig: true };
|
270
|
-
if (middleware) {
|
271
|
-
config.middleware = true;
|
272
|
-
}
|
273
|
-
if (fnPattern && func) {
|
274
|
-
config.functions = { [fnPattern]: func };
|
275
|
-
if (func.includeFiles) {
|
276
|
-
config.includeFiles = func.includeFiles;
|
277
|
-
}
|
278
|
-
if (func.excludeFiles) {
|
279
|
-
config.excludeFiles = func.excludeFiles;
|
280
|
-
}
|
281
|
-
}
|
282
|
-
const builder = {
|
283
|
-
use,
|
284
|
-
src: fileName,
|
285
|
-
config,
|
286
|
-
};
|
287
|
-
return builder;
|
288
|
-
}
|
289
|
-
function getFunction(fileName, { functions = {} }) {
|
290
|
-
const keys = Object.keys(functions);
|
291
|
-
if (!keys.length) {
|
292
|
-
return { fnPattern: null, func: null };
|
293
|
-
}
|
294
|
-
const func = keys.find(key => key === fileName || minimatch_1.default(fileName, key));
|
295
|
-
return func
|
296
|
-
? { fnPattern: func, func: functions[func] }
|
297
|
-
: { fnPattern: null, func: null };
|
298
|
-
}
|
299
|
-
function getApiMatches() {
|
300
|
-
const config = { zeroConfig: true };
|
301
|
-
return [
|
302
|
-
{ src: 'middleware.[jt]s', use: `@vercel/node`, config },
|
303
|
-
{ src: 'api/**/*.js', use: `@vercel/node`, config },
|
304
|
-
{ src: 'api/**/*.mjs', use: `@vercel/node`, config },
|
305
|
-
{ src: 'api/**/*.ts', use: `@vercel/node`, config },
|
306
|
-
{ src: 'api/**/!(*_test).go', use: `@vercel/go`, config },
|
307
|
-
{ src: 'api/**/*.py', use: `@vercel/python`, config },
|
308
|
-
{ src: 'api/**/*.rb', use: `@vercel/ruby`, config },
|
309
|
-
];
|
310
|
-
}
|
311
|
-
function hasBuildScript(pkg) {
|
312
|
-
const { scripts = {} } = pkg || {};
|
313
|
-
return Boolean(scripts && scripts['build']);
|
314
|
-
}
|
315
|
-
function detectFrontBuilder(pkg, files, usedFunctions, fallbackEntrypoint, options) {
|
316
|
-
const { tag, projectSettings = {} } = options;
|
317
|
-
const withTag = tag ? `@${tag}` : '';
|
318
|
-
const { createdAt = 0 } = projectSettings;
|
319
|
-
let { framework } = projectSettings;
|
320
|
-
const config = {
|
321
|
-
zeroConfig: true,
|
322
|
-
};
|
323
|
-
if (framework) {
|
324
|
-
config.framework = framework;
|
325
|
-
}
|
326
|
-
if (projectSettings.devCommand) {
|
327
|
-
config.devCommand = projectSettings.devCommand;
|
328
|
-
}
|
329
|
-
if (typeof projectSettings.installCommand === 'string') {
|
330
|
-
config.installCommand = projectSettings.installCommand;
|
331
|
-
}
|
332
|
-
if (projectSettings.buildCommand) {
|
333
|
-
config.buildCommand = projectSettings.buildCommand;
|
334
|
-
}
|
335
|
-
if (projectSettings.outputDirectory) {
|
336
|
-
config.outputDirectory = projectSettings.outputDirectory;
|
337
|
-
}
|
338
|
-
if (pkg &&
|
339
|
-
(framework === undefined || createdAt < Date.parse('2020-03-01'))) {
|
340
|
-
const deps = {
|
341
|
-
...pkg.dependencies,
|
342
|
-
...pkg.devDependencies,
|
343
|
-
};
|
344
|
-
if (deps['next']) {
|
345
|
-
framework = 'nextjs';
|
346
|
-
}
|
347
|
-
}
|
348
|
-
if (options.functions) {
|
349
|
-
// When the builder is not used yet we'll use it for the frontend
|
350
|
-
Object.entries(options.functions).forEach(([key, func]) => {
|
351
|
-
if (!usedFunctions.has(key)) {
|
352
|
-
if (!config.functions)
|
353
|
-
config.functions = {};
|
354
|
-
config.functions[key] = { ...func };
|
355
|
-
}
|
356
|
-
});
|
357
|
-
}
|
358
|
-
const f = slugToFramework.get(framework || '');
|
359
|
-
if (f && f.useRuntime) {
|
360
|
-
const { src, use } = f.useRuntime;
|
361
|
-
return { src, use: `${use}${withTag}`, config };
|
362
|
-
}
|
363
|
-
// Entrypoints for other frameworks
|
364
|
-
// TODO - What if just a build script is provided, but no entrypoint.
|
365
|
-
const entrypoints = new Set([
|
366
|
-
'package.json',
|
367
|
-
'config.yaml',
|
368
|
-
'config.toml',
|
369
|
-
'config.json',
|
370
|
-
'_config.yml',
|
371
|
-
'config.yml',
|
372
|
-
'config.rb',
|
373
|
-
]);
|
374
|
-
const source = pkg
|
375
|
-
? 'package.json'
|
376
|
-
: files.find(file => entrypoints.has(file)) ||
|
377
|
-
fallbackEntrypoint ||
|
378
|
-
'package.json';
|
379
|
-
return {
|
380
|
-
src: source || 'package.json',
|
381
|
-
use: `@vercel/static-build${withTag}`,
|
382
|
-
config,
|
383
|
-
};
|
384
|
-
}
|
385
|
-
function getMissingBuildScriptError() {
|
386
|
-
return {
|
387
|
-
code: 'missing_build_script',
|
388
|
-
message: 'Your `package.json` file is missing a `build` property inside the `scripts` property.' +
|
389
|
-
'\nLearn More: https://vercel.link/missing-build-script',
|
390
|
-
};
|
391
|
-
}
|
392
|
-
function validateFunctions({ functions = {} }) {
|
393
|
-
for (const [path, func] of Object.entries(functions)) {
|
394
|
-
if (path.length > 256) {
|
395
|
-
return {
|
396
|
-
code: 'invalid_function_glob',
|
397
|
-
message: 'Function globs must be less than 256 characters long.',
|
398
|
-
};
|
399
|
-
}
|
400
|
-
if (!func || typeof func !== 'object') {
|
401
|
-
return {
|
402
|
-
code: 'invalid_function',
|
403
|
-
message: 'Function must be an object.',
|
404
|
-
};
|
405
|
-
}
|
406
|
-
if (Object.keys(func).length === 0) {
|
407
|
-
return {
|
408
|
-
code: 'invalid_function',
|
409
|
-
message: 'Function must contain at least one property.',
|
410
|
-
};
|
411
|
-
}
|
412
|
-
if (func.maxDuration !== undefined &&
|
413
|
-
(func.maxDuration < 1 ||
|
414
|
-
func.maxDuration > 900 ||
|
415
|
-
!Number.isInteger(func.maxDuration))) {
|
416
|
-
return {
|
417
|
-
code: 'invalid_function_duration',
|
418
|
-
message: 'Functions must have a duration between 1 and 900.',
|
419
|
-
};
|
420
|
-
}
|
421
|
-
if (func.memory !== undefined &&
|
422
|
-
(func.memory < 128 || func.memory > 3008 || func.memory % 64 !== 0)) {
|
423
|
-
return {
|
424
|
-
code: 'invalid_function_memory',
|
425
|
-
message: 'Functions must have a memory value between 128 and 3008 in steps of 64.',
|
426
|
-
};
|
427
|
-
}
|
428
|
-
if (path.startsWith('/')) {
|
429
|
-
return {
|
430
|
-
code: 'invalid_function_source',
|
431
|
-
message: `The function path "${path}" is invalid. The path must be relative to your project root and therefore cannot start with a slash.`,
|
432
|
-
};
|
433
|
-
}
|
434
|
-
if (func.runtime !== undefined) {
|
435
|
-
const tag = `${func.runtime}`.split('@').pop();
|
436
|
-
if (!tag || !semver_1.valid(tag)) {
|
437
|
-
return {
|
438
|
-
code: 'invalid_function_runtime',
|
439
|
-
message: 'Function Runtimes must have a valid version, for example `now-php@1.0.0`.',
|
440
|
-
};
|
441
|
-
}
|
442
|
-
}
|
443
|
-
if (func.includeFiles !== undefined) {
|
444
|
-
if (typeof func.includeFiles !== 'string') {
|
445
|
-
return {
|
446
|
-
code: 'invalid_function_property',
|
447
|
-
message: `The property \`includeFiles\` must be a string.`,
|
448
|
-
};
|
449
|
-
}
|
450
|
-
}
|
451
|
-
if (func.excludeFiles !== undefined) {
|
452
|
-
if (typeof func.excludeFiles !== 'string') {
|
453
|
-
return {
|
454
|
-
code: 'invalid_function_property',
|
455
|
-
message: `The property \`excludeFiles\` must be a string.`,
|
456
|
-
};
|
457
|
-
}
|
458
|
-
}
|
459
|
-
}
|
460
|
-
return null;
|
461
|
-
}
|
462
|
-
function checkUnusedFunctions(frontendBuilder, usedFunctions, options) {
|
463
|
-
const unusedFunctions = new Set(Object.keys(options.functions || {}).filter(key => !usedFunctions.has(key)));
|
464
|
-
if (!unusedFunctions.size) {
|
465
|
-
return null;
|
466
|
-
}
|
467
|
-
// Next.js can use functions only for `src/pages` or `pages`
|
468
|
-
if (frontendBuilder && _1.isOfficialRuntime('next', frontendBuilder.use)) {
|
469
|
-
for (const fnKey of unusedFunctions.values()) {
|
470
|
-
if (fnKey.startsWith('pages/') || fnKey.startsWith('src/pages')) {
|
471
|
-
unusedFunctions.delete(fnKey);
|
472
|
-
}
|
473
|
-
else {
|
474
|
-
return {
|
475
|
-
code: 'unused_function',
|
476
|
-
message: `The pattern "${fnKey}" defined in \`functions\` doesn't match any Serverless Functions.`,
|
477
|
-
action: 'Learn More',
|
478
|
-
link: 'https://vercel.link/unmatched-function-pattern',
|
479
|
-
};
|
480
|
-
}
|
481
|
-
}
|
482
|
-
}
|
483
|
-
if (unusedFunctions.size) {
|
484
|
-
const [fnKey] = Array.from(unusedFunctions);
|
485
|
-
return {
|
486
|
-
code: 'unused_function',
|
487
|
-
message: `The pattern "${fnKey}" defined in \`functions\` doesn't match any Serverless Functions inside the \`api\` directory.`,
|
488
|
-
action: 'Learn More',
|
489
|
-
link: 'https://vercel.link/unmatched-function-pattern',
|
490
|
-
};
|
491
|
-
}
|
492
|
-
return null;
|
493
|
-
}
|
494
|
-
function getApiRoute(fileName, sortedFiles, options, absolutePathCache) {
|
495
|
-
const conflictingSegment = getConflictingSegment(fileName);
|
496
|
-
if (conflictingSegment) {
|
497
|
-
return {
|
498
|
-
apiRoute: null,
|
499
|
-
isDynamic: false,
|
500
|
-
routeError: {
|
501
|
-
code: 'conflicting_path_segment',
|
502
|
-
message: `The segment "${conflictingSegment}" occurs more than ` +
|
503
|
-
`one time in your path "${fileName}". Please make sure that ` +
|
504
|
-
`every segment in a path is unique.`,
|
505
|
-
},
|
506
|
-
};
|
507
|
-
}
|
508
|
-
const occurrences = pathOccurrences(fileName, sortedFiles, absolutePathCache);
|
509
|
-
if (occurrences.length > 0) {
|
510
|
-
const messagePaths = concatArrayOfText(occurrences.map(name => `"${name}"`));
|
511
|
-
return {
|
512
|
-
apiRoute: null,
|
513
|
-
isDynamic: false,
|
514
|
-
routeError: {
|
515
|
-
code: 'conflicting_file_path',
|
516
|
-
message: `Two or more files have conflicting paths or names. ` +
|
517
|
-
`Please make sure path segments and filenames, without their extension, are unique. ` +
|
518
|
-
`The path "${fileName}" has conflicts with ${messagePaths}.`,
|
519
|
-
},
|
520
|
-
};
|
521
|
-
}
|
522
|
-
const out = createRouteFromPath(fileName, Boolean(options.featHandleMiss), Boolean(options.cleanUrls));
|
523
|
-
return {
|
524
|
-
apiRoute: out.route,
|
525
|
-
isDynamic: out.isDynamic,
|
526
|
-
routeError: null,
|
527
|
-
};
|
528
|
-
}
|
529
|
-
// Checks if a placeholder with the same name is used
|
530
|
-
// multiple times inside the same path
|
531
|
-
function getConflictingSegment(filePath) {
|
532
|
-
const segments = new Set();
|
533
|
-
for (const segment of filePath.split('/')) {
|
534
|
-
const name = getSegmentName(segment);
|
535
|
-
if (name !== null && segments.has(name)) {
|
536
|
-
return name;
|
537
|
-
}
|
538
|
-
if (name) {
|
539
|
-
segments.add(name);
|
540
|
-
}
|
541
|
-
}
|
542
|
-
return null;
|
543
|
-
}
|
544
|
-
// Takes a filename or foldername, strips the extension
|
545
|
-
// gets the part between the "[]" brackets.
|
546
|
-
// It will return `null` if there are no brackets
|
547
|
-
// and therefore no segment.
|
548
|
-
function getSegmentName(segment) {
|
549
|
-
const { name } = path_1.parse(segment);
|
550
|
-
if (name.startsWith('[') && name.endsWith(']')) {
|
551
|
-
return name.slice(1, -1);
|
552
|
-
}
|
553
|
-
return null;
|
554
|
-
}
|
555
|
-
function getAbsolutePath(unresolvedPath) {
|
556
|
-
const { dir, name } = path_1.parse(unresolvedPath);
|
557
|
-
const parts = joinPath(dir, name).split('/');
|
558
|
-
return parts.map(part => part.replace(/\[.*\]/, '1')).join('/');
|
559
|
-
}
|
560
|
-
// Counts how often a path occurs when all placeholders
|
561
|
-
// got resolved, so we can check if they have conflicts
|
562
|
-
function pathOccurrences(fileName, files, absolutePathCache) {
|
563
|
-
let currentAbsolutePath = absolutePathCache.get(fileName);
|
564
|
-
if (!currentAbsolutePath) {
|
565
|
-
currentAbsolutePath = getAbsolutePath(fileName);
|
566
|
-
absolutePathCache.set(fileName, currentAbsolutePath);
|
567
|
-
}
|
568
|
-
const prev = [];
|
569
|
-
// Do not call expensive functions like `minimatch` in here
|
570
|
-
// because we iterate over every file.
|
571
|
-
for (const file of files) {
|
572
|
-
if (file === fileName) {
|
573
|
-
continue;
|
574
|
-
}
|
575
|
-
let absolutePath = absolutePathCache.get(file);
|
576
|
-
if (!absolutePath) {
|
577
|
-
absolutePath = getAbsolutePath(file);
|
578
|
-
absolutePathCache.set(file, absolutePath);
|
579
|
-
}
|
580
|
-
if (absolutePath === currentAbsolutePath) {
|
581
|
-
prev.push(file);
|
582
|
-
}
|
583
|
-
else if (partiallyMatches(fileName, file)) {
|
584
|
-
prev.push(file);
|
585
|
-
}
|
586
|
-
}
|
587
|
-
return prev;
|
588
|
-
}
|
589
|
-
function joinPath(...segments) {
|
590
|
-
const joinedPath = segments.join('/');
|
591
|
-
return joinedPath.replace(/\/{2,}/g, '/');
|
592
|
-
}
|
593
|
-
function escapeName(name) {
|
594
|
-
const special = '[]^$.|?*+()'.split('');
|
595
|
-
for (const char of special) {
|
596
|
-
name = name.replace(new RegExp(`\\${char}`, 'g'), `\\${char}`);
|
597
|
-
}
|
598
|
-
return name;
|
599
|
-
}
|
600
|
-
function concatArrayOfText(texts) {
|
601
|
-
if (texts.length <= 2) {
|
602
|
-
return texts.join(' and ');
|
603
|
-
}
|
604
|
-
const last = texts.pop();
|
605
|
-
return `${texts.join(', ')}, and ${last}`;
|
606
|
-
}
|
607
|
-
// Check if the path partially matches and has the same
|
608
|
-
// name for the path segment at the same position
|
609
|
-
function partiallyMatches(pathA, pathB) {
|
610
|
-
const partsA = pathA.split('/');
|
611
|
-
const partsB = pathB.split('/');
|
612
|
-
const long = partsA.length > partsB.length ? partsA : partsB;
|
613
|
-
const short = long === partsA ? partsB : partsA;
|
614
|
-
let index = 0;
|
615
|
-
for (const segmentShort of short) {
|
616
|
-
const segmentLong = long[index];
|
617
|
-
const nameLong = getSegmentName(segmentLong);
|
618
|
-
const nameShort = getSegmentName(segmentShort);
|
619
|
-
// If there are no segments or the paths differ we
|
620
|
-
// return as they are not matching
|
621
|
-
if (segmentShort !== segmentLong && (!nameLong || !nameShort)) {
|
622
|
-
return false;
|
623
|
-
}
|
624
|
-
if (nameLong !== nameShort) {
|
625
|
-
return true;
|
626
|
-
}
|
627
|
-
index += 1;
|
628
|
-
}
|
629
|
-
return false;
|
630
|
-
}
|
631
|
-
function createRouteFromPath(filePath, featHandleMiss, cleanUrls) {
|
632
|
-
const parts = filePath.split('/');
|
633
|
-
let counter = 1;
|
634
|
-
const query = [];
|
635
|
-
let isDynamic = false;
|
636
|
-
const srcParts = parts.map((segment, i) => {
|
637
|
-
const name = getSegmentName(segment);
|
638
|
-
const isLast = i === parts.length - 1;
|
639
|
-
if (name !== null) {
|
640
|
-
// We can't use `URLSearchParams` because `$` would get escaped
|
641
|
-
query.push(`${name}=$${counter++}`);
|
642
|
-
isDynamic = true;
|
643
|
-
return `([^/]+)`;
|
644
|
-
}
|
645
|
-
else if (isLast) {
|
646
|
-
const { name: fileName, ext } = path_1.parse(segment);
|
647
|
-
const isIndex = fileName === 'index';
|
648
|
-
const prefix = isIndex ? '/' : '';
|
649
|
-
const names = [
|
650
|
-
isIndex ? prefix : `${fileName}/`,
|
651
|
-
prefix + escapeName(fileName),
|
652
|
-
featHandleMiss && cleanUrls
|
653
|
-
? ''
|
654
|
-
: prefix + escapeName(fileName) + escapeName(ext),
|
655
|
-
].filter(Boolean);
|
656
|
-
// Either filename with extension, filename without extension
|
657
|
-
// or nothing when the filename is `index`.
|
658
|
-
// When `cleanUrls: true` then do *not* add the filename with extension.
|
659
|
-
return `(${names.join('|')})${isIndex ? '?' : ''}`;
|
660
|
-
}
|
661
|
-
return segment;
|
662
|
-
});
|
663
|
-
const { name: fileName, ext } = path_1.parse(filePath);
|
664
|
-
const isIndex = fileName === 'index';
|
665
|
-
const queryString = `${query.length ? '?' : ''}${query.join('&')}`;
|
666
|
-
const src = isIndex
|
667
|
-
? `^/${srcParts.slice(0, -1).join('/')}${srcParts.slice(-1)[0]}$`
|
668
|
-
: `^/${srcParts.join('/')}$`;
|
669
|
-
let route;
|
670
|
-
if (featHandleMiss) {
|
671
|
-
const extensionless = ext ? filePath.slice(0, -ext.length) : filePath;
|
672
|
-
route = {
|
673
|
-
src,
|
674
|
-
dest: `/${extensionless}${queryString}`,
|
675
|
-
check: true,
|
676
|
-
};
|
677
|
-
}
|
678
|
-
else {
|
679
|
-
route = {
|
680
|
-
src,
|
681
|
-
dest: `/${filePath}${queryString}`,
|
682
|
-
};
|
683
|
-
}
|
684
|
-
return { route, isDynamic };
|
685
|
-
}
|
686
|
-
function getRouteResult(pkg, apiRoutes, dynamicRoutes, outputDirectory, apiBuilders, frontendBuilder, options) {
|
687
|
-
const deps = Object.assign({}, pkg?.dependencies, pkg?.devDependencies);
|
688
|
-
const defaultRoutes = [];
|
689
|
-
const redirectRoutes = [];
|
690
|
-
const rewriteRoutes = [];
|
691
|
-
const errorRoutes = [];
|
692
|
-
const limitedRoutes = {
|
693
|
-
defaultRoutes: [],
|
694
|
-
redirectRoutes: [],
|
695
|
-
rewriteRoutes: [],
|
696
|
-
};
|
697
|
-
const framework = frontendBuilder?.config?.framework || '';
|
698
|
-
const isNextjs = framework === 'nextjs' || _1.isOfficialRuntime('next', frontendBuilder?.use);
|
699
|
-
const ignoreRuntimes = slugToFramework.get(framework)?.ignoreRuntimes;
|
700
|
-
if (apiRoutes && apiRoutes.length > 0) {
|
701
|
-
if (options.featHandleMiss) {
|
702
|
-
// Exclude extension names if the corresponding plugin is not found in package.json
|
703
|
-
// detectBuilders({ignoreRoutesForBuilders: ['@vercel/python']})
|
704
|
-
// return a copy of routes.
|
705
|
-
// We should exclud errorRoutes and
|
706
|
-
const extSet = detectApiExtensions(apiBuilders);
|
707
|
-
const withTag = options.tag ? `@${options.tag}` : '';
|
708
|
-
const extSetLimited = detectApiExtensions(apiBuilders.filter(b => {
|
709
|
-
if (b.use === `@vercel/python${withTag}` &&
|
710
|
-
!('vercel-plugin-python' in deps)) {
|
711
|
-
return false;
|
712
|
-
}
|
713
|
-
if (b.use === `@vercel/go${withTag}` &&
|
714
|
-
!('vercel-plugin-go' in deps)) {
|
715
|
-
return false;
|
716
|
-
}
|
717
|
-
if (b.use === `@vercel/ruby${withTag}` &&
|
718
|
-
!('vercel-plugin-ruby' in deps)) {
|
719
|
-
return false;
|
720
|
-
}
|
721
|
-
return true;
|
722
|
-
}));
|
723
|
-
if (extSet.size > 0) {
|
724
|
-
const extGroup = `(?:\\.(?:${Array.from(extSet)
|
725
|
-
.map(ext => ext.slice(1))
|
726
|
-
.join('|')}))`;
|
727
|
-
const extGroupLimited = `(?:\\.(?:${Array.from(extSetLimited)
|
728
|
-
.map(ext => ext.slice(1))
|
729
|
-
.join('|')}))`;
|
730
|
-
if (options.cleanUrls) {
|
731
|
-
redirectRoutes.push({
|
732
|
-
src: `^/(api(?:.+)?)/index${extGroup}?/?$`,
|
733
|
-
headers: { Location: options.trailingSlash ? '/$1/' : '/$1' },
|
734
|
-
status: 308,
|
735
|
-
});
|
736
|
-
redirectRoutes.push({
|
737
|
-
src: `^/api/(.+)${extGroup}/?$`,
|
738
|
-
headers: {
|
739
|
-
Location: options.trailingSlash ? '/api/$1/' : '/api/$1',
|
740
|
-
},
|
741
|
-
status: 308,
|
742
|
-
});
|
743
|
-
limitedRoutes.redirectRoutes.push({
|
744
|
-
src: `^/(api(?:.+)?)/index${extGroupLimited}?/?$`,
|
745
|
-
headers: { Location: options.trailingSlash ? '/$1/' : '/$1' },
|
746
|
-
status: 308,
|
747
|
-
});
|
748
|
-
limitedRoutes.redirectRoutes.push({
|
749
|
-
src: `^/api/(.+)${extGroupLimited}/?$`,
|
750
|
-
headers: {
|
751
|
-
Location: options.trailingSlash ? '/api/$1/' : '/api/$1',
|
752
|
-
},
|
753
|
-
status: 308,
|
754
|
-
});
|
755
|
-
}
|
756
|
-
else {
|
757
|
-
defaultRoutes.push({ handle: 'miss' });
|
758
|
-
defaultRoutes.push({
|
759
|
-
src: `^/api/(.+)${extGroup}$`,
|
760
|
-
dest: '/api/$1',
|
761
|
-
check: true,
|
762
|
-
});
|
763
|
-
limitedRoutes.defaultRoutes.push({ handle: 'miss' });
|
764
|
-
limitedRoutes.defaultRoutes.push({
|
765
|
-
src: `^/api/(.+)${extGroupLimited}$`,
|
766
|
-
dest: '/api/$1',
|
767
|
-
check: true,
|
768
|
-
});
|
769
|
-
}
|
770
|
-
}
|
771
|
-
rewriteRoutes.push(...dynamicRoutes);
|
772
|
-
limitedRoutes.rewriteRoutes.push(...dynamicRoutes);
|
773
|
-
if (typeof ignoreRuntimes === 'undefined') {
|
774
|
-
// This route is only necessary to hide the directory listing
|
775
|
-
// to avoid enumerating serverless function names.
|
776
|
-
// But it causes issues in `vc dev` for frameworks that handle
|
777
|
-
// their own functions such as redwood, so we ignore.
|
778
|
-
rewriteRoutes.push({
|
779
|
-
src: '^/api(/.*)?$',
|
780
|
-
status: 404,
|
781
|
-
});
|
782
|
-
}
|
783
|
-
}
|
784
|
-
else {
|
785
|
-
defaultRoutes.push(...apiRoutes);
|
786
|
-
if (apiRoutes.length) {
|
787
|
-
defaultRoutes.push({
|
788
|
-
status: 404,
|
789
|
-
src: '^/api(/.*)?$',
|
790
|
-
});
|
791
|
-
}
|
792
|
-
}
|
793
|
-
}
|
794
|
-
if (outputDirectory &&
|
795
|
-
frontendBuilder &&
|
796
|
-
!options.featHandleMiss &&
|
797
|
-
_1.isOfficialRuntime('static', frontendBuilder.use)) {
|
798
|
-
defaultRoutes.push({
|
799
|
-
src: '/(.*)',
|
800
|
-
dest: `/${outputDirectory}/$1`,
|
801
|
-
});
|
802
|
-
}
|
803
|
-
if (options.featHandleMiss && !isNextjs) {
|
804
|
-
// Exclude Next.js to avoid overriding custom error page
|
805
|
-
// https://nextjs.org/docs/advanced-features/custom-error-page
|
806
|
-
errorRoutes.push({
|
807
|
-
status: 404,
|
808
|
-
src: '^(?!/api).*$',
|
809
|
-
dest: options.cleanUrls ? '/404' : '/404.html',
|
810
|
-
});
|
811
|
-
}
|
812
|
-
return {
|
813
|
-
defaultRoutes,
|
814
|
-
redirectRoutes,
|
815
|
-
rewriteRoutes,
|
816
|
-
errorRoutes,
|
817
|
-
limitedRoutes,
|
818
|
-
};
|
819
|
-
}
|
820
|
-
function sortFilesBySegmentCount(fileA, fileB) {
|
821
|
-
const lengthA = fileA.split('/').length;
|
822
|
-
const lengthB = fileB.split('/').length;
|
823
|
-
if (lengthA > lengthB) {
|
824
|
-
return -1;
|
825
|
-
}
|
826
|
-
if (lengthA < lengthB) {
|
827
|
-
return 1;
|
828
|
-
}
|
829
|
-
// Paths that have the same segment length but
|
830
|
-
// less placeholders are preferred
|
831
|
-
const countSegments = (prev, segment) => getSegmentName(segment) ? prev + 1 : 0;
|
832
|
-
const segmentLengthA = fileA.split('/').reduce(countSegments, 0);
|
833
|
-
const segmentLengthB = fileB.split('/').reduce(countSegments, 0);
|
834
|
-
if (segmentLengthA > segmentLengthB) {
|
835
|
-
return 1;
|
836
|
-
}
|
837
|
-
if (segmentLengthA < segmentLengthB) {
|
838
|
-
return -1;
|
839
|
-
}
|
840
|
-
return fileA.localeCompare(fileB);
|
841
|
-
}
|