UNPKG

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