UNPKG

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