@vercel/build-utils 4.2.0 → 5.0.1-canary.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.
@@ -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
- }