import minimatch from 'minimatch' ; import { valid as validSemver } from 'semver' ; import { parse as parsePath , extname } from 'path' ; import { Route , Source } from '@vercel/routing-utils' ; import frameworkList , { Framework } from '@vercel/frameworks' ; import { PackageJson , Builder , Config , BuilderFunctions , ProjectSettings , } from './types' ; import { isOfficialRuntime } from './' ; const slugToFramework = new Map < string | null , Framework > ( frameworkList . map ( f => [ f . slug , f ] ) ) ; interface ErrorResponse { code : string ; message : string ; action ? : string ; link ? : string ; } interface Options { tag ? : 'canary' | 'latest' | string ; functions ? : BuilderFunctions ; ignoreBuildScript ? : boolean ; projectSettings ? : ProjectSettings ; cleanUrls ? : boolean ; trailingSlash ? : boolean ; featHandleMiss ? : boolean ; } export function sortFiles ( fileA , fileB ) { return fileA . localeCompare ( fileB ) ; } export function detectApiExtensions ( builders ) { return new Set < string > ( builders . filter ( ( b ) : b is Builder & { src : string ; } => Boolean ( b . config && b . config . zeroConfig && b . src ?. startsWith ( 'api/' ) ) ) . map ( b => extname ( b . src ) ) . filter ( Boolean ) ) ; } export function detectApiDirectory ( builders ) { const found = builders . some ( b => b . config && b . config . zeroConfig && b . src ?. startsWith ( 'api/' ) ) ; return found ? 'api' : null ; } function getPublicBuilder ( builders ) { for ( const builder of builders ) { if ( typeof builder . src === 'string' && isOfficialRuntime ( 'static' , builder . use ) && /^.*\/\*\*\/\*$/ . test ( builder . src ) && builder . config && builder . config . zeroConfig === true ) { return builder as Builder & { src : string ; } ; } } return null ; } export function detectOutputDirectory ( builders ) { const publicBuilder = getPublicBuilder ( builders ) ; return publicBuilder ? publicBuilder . src . replace ( '/**/*' , '' ) : null ; } export async function detectBuilders ( files , pkg ? , options = { } ) { const errors = [ ] ; const warnings = [ ] ; const apiBuilders = [ ] ; let frontendBuilder = null ; const functionError = validateFunctions ( options ) ; if ( functionError ) { return { builders : null , errors : [ functionError ] , warnings , defaultRoutes : null , redirectRoutes : null , rewriteRoutes : null , errorRoutes : null , limitedRoutes : null , } ; } const sortedFiles = files . sort ( sortFiles ) ; const apiSortedFiles = files . sort ( sortFilesBySegmentCount ) ; const usedFunctions = new Set < string > ( ) ; const addToUsedFunctions = ( builder ) => { const key = Object . keys ( builder . config ! . functions || { } ) [ 0 ] ; if ( key ) usedFunctions . add ( key ) ; } ; const absolutePathCache = new Map < string , string > ( ) ; const { projectSettings = { } } = options ; const { buildCommand , outputDirectory , framework } = projectSettings ; const ignoreRuntimes = new Set ( slugToFramework . get ( framework || '' ) ?. ignoreRuntimes ) ; const withTag = options . tag ? `@${ options . tag }` : '' ; const apiMatches = getApiMatches ( ) . filter ( b => ! ignoreRuntimes . has ( b . use ) ) . map ( b => { b . use = `${ b . use }${ withTag }` ; return b ; } ) ; const makeFrontendStatic = buildCommand === '' || outputDirectory === '' ; const usedOutputDirectory = outputDirectory || 'public' ; let hasUsedOutputDirectory = false ; let hasNoneApiFiles = false ; let hasNextApiFiles = false ; let fallbackEntrypoint = null ; const apiRoutes = [ ] ; const dynamicRoutes = [ ] ; for ( const fileName of sortedFiles ) { const apiBuilder = maybeGetApiBuilder ( fileName , apiMatches , options ) ; if ( apiBuilder ) { const { routeError , apiRoute , isDynamic } = getApiRoute ( fileName , apiSortedFiles , options , absolutePathCache ) ; if ( routeError ) { return { builders : null , errors : [ routeError ] , warnings , defaultRoutes : null , redirectRoutes : null , rewriteRoutes : null , errorRoutes : null , limitedRoutes : null , } ; } if ( apiRoute ) { apiRoutes . push ( apiRoute ) ; if ( isDynamic ) { dynamicRoutes . push ( apiRoute ) ; } } addToUsedFunctions ( apiBuilder ) ; apiBuilders . push ( apiBuilder ) ; continue ; } if ( ! hasUsedOutputDirectory && fileName . startsWith ( `${ usedOutputDirectory }/` ) ) { hasUsedOutputDirectory = true ; } if ( ! hasNoneApiFiles && ! fileName . startsWith ( 'api/' ) && fileName !== 'package.json' ) { hasNoneApiFiles = true ; } if ( ! hasNextApiFiles && ( fileName . startsWith ( 'pages/api' ) || fileName . startsWith ( 'src/pages/api' ) ) ) { hasNextApiFiles = true ; } if ( ! fallbackEntrypoint && buildCommand && ! fileName . includes ( '/' ) && fileName !== 'now.json' && fileName !== 'vercel.json' ) { fallbackEntrypoint = fileName ; } } if ( ! makeFrontendStatic && ( hasBuildScript ( pkg ) || buildCommand || framework ) ) { frontendBuilder = detectFrontBuilder ( pkg , files , usedFunctions , fallbackEntrypoint , options ) ; } else { if ( pkg && ! makeFrontendStatic && ! apiBuilders . length && ! options . ignoreBuildScript ) { errors . push ( getMissingBuildScriptError ( ) ) ; return { errors , warnings , builders : null , redirectRoutes : null , defaultRoutes : null , rewriteRoutes : null , errorRoutes : null , limitedRoutes : null , } ; } if ( hasUsedOutputDirectory && outputDirectory !== '' ) { frontendBuilder = { use : '@vercel/static' , src : `${ usedOutputDirectory }/**/*` , config : { zeroConfig : true , outputDirectory : usedOutputDirectory , } , } ; } else if ( apiBuilders . length && hasNoneApiFiles ) { frontendBuilder = { use : '@vercel/static' , src : '!{api/**,package.json}' , config : { zeroConfig : true , } , } ; } } const unusedFunctionError = checkUnusedFunctions ( frontendBuilder , usedFunctions , options ) ; if ( unusedFunctionError ) { return { builders : null , errors : [ unusedFunctionError ] , warnings , redirectRoutes : null , defaultRoutes : null , rewriteRoutes : null , errorRoutes : null , limitedRoutes : null , } ; } const builders = [ ] ; if ( apiBuilders . length ) { builders . push ( ... apiBuilders ) ; } if ( frontendBuilder ) { builders . push ( frontendBuilder ) ; if ( hasNextApiFiles && apiBuilders . some ( b => isOfficialRuntime ( 'node' , b . use ) ) ) { warnings . push ( { code : 'conflicting_files' , 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).' , link : 'https://nextjs.org/docs/api-routes/introduction' , action : 'Learn More' , } ) ; } } const routesResult = getRouteResult ( pkg , apiRoutes , dynamicRoutes , usedOutputDirectory , apiBuilders , frontendBuilder , options ) ; return { warnings , builders : builders . length ? builders : null , errors : errors . length ? errors : null , redirectRoutes : routesResult . redirectRoutes , defaultRoutes : routesResult . defaultRoutes , rewriteRoutes : routesResult . rewriteRoutes , errorRoutes : routesResult . errorRoutes , limitedRoutes : routesResult . limitedRoutes , } ; } function maybeGetApiBuilder ( fileName , apiMatches , options ) { if ( ! fileName . startsWith ( 'api/' ) ) { return null ; } if ( fileName . includes ( '/.' ) ) { return null ; } if ( fileName . includes ( '/_' ) ) { return null ; } if ( fileName . includes ( '/node_modules/' ) ) { return null ; } if ( fileName . endsWith ( '.d.ts' ) ) { return null ; } const match = apiMatches . find ( ( { src = '**' } ) => { return src === fileName || minimatch ( fileName , src ) ; } ) ; const { fnPattern , func } = getFunction ( fileName , options ) ; const use = ( func && func . runtime ) || ( match && match . use ) ; if ( ! use ) { return null ; } const config = { zeroConfig : true } ; if ( fnPattern && func ) { config . functions = { [ fnPattern ] : func } ; if ( func . includeFiles ) { config . includeFiles = func . includeFiles ; } if ( func . excludeFiles ) { config . excludeFiles = func . excludeFiles ; } } const builder = { use , src : fileName , config , } ; return builder ; } function getFunction ( fileName , { functions = { } } ) { const keys = Object . keys ( functions ) ; if ( ! keys . length ) { return { fnPattern : null , func : null } ; } const func = keys . find ( key => key === fileName || minimatch ( fileName , key ) ) ; return func ? { fnPattern : func , func : functions [ func ] } : { fnPattern : null , func : null } ; } function getApiMatches ( ) { const config = { zeroConfig : true } ; return [ { src : 'api/**/*.js' , use : `@vercel/node` , config } , { src : 'api/**/*.mjs' , use : `@vercel/node` , config } , { src : 'api/**/*.ts' , use : `@vercel/node` , config } , { src : 'api/**/!(*_test).go' , use : `@vercel/go` , config } , { src : 'api/**/*.py' , use : `@vercel/python` , config } , { src : 'api/**/*.rb' , use : `@vercel/ruby` , config } , ] ; } function hasBuildScript ( pkg ) { const { scripts = { } } = pkg || { } ; return Boolean ( scripts && scripts [ 'build' ] ) ; } function detectFrontBuilder ( pkg , files , usedFunctions , fallbackEntrypoint , options ) { const { tag , projectSettings = { } } = options ; const withTag = tag ? `@${ tag }` : '' ; const { createdAt = 0 } = projectSettings ; let { framework } = projectSettings ; const config = { zeroConfig : true , } ; if ( framework ) { config . framework = framework ; } if ( projectSettings . devCommand ) { config . devCommand = projectSettings . devCommand ; } if ( typeof projectSettings . installCommand === 'string' ) { config . installCommand = projectSettings . installCommand ; } if ( projectSettings . buildCommand ) { config . buildCommand = projectSettings . buildCommand ; } if ( projectSettings . outputDirectory ) { config . outputDirectory = projectSettings . outputDirectory ; } if ( pkg && ( framework === undefined || createdAt < Date . parse ( '2020-03-01' ) ) ) { const deps = { ... pkg . dependencies , ... pkg . devDependencies , } ; if ( deps [ 'next' ] ) { framework = 'nextjs' ; } } if ( options . functions ) { Object . entries ( options . functions ) . forEach ( ( [ key , func ] ) => { if ( ! usedFunctions . has ( key ) ) { if ( ! config . functions ) config . functions = { } ; config . functions [ key ] = { ... func } ; } } ) ; } const f = slugToFramework . get ( framework || '' ) ; if ( f && f . useRuntime ) { const { src , use } = f . useRuntime ; return { src , use : `${ use }${ withTag }` , config } ; } const entrypoints = new Set ( [ 'package.json' , 'config.yaml' , 'config.toml' , 'config.json' , '_config.yml' , 'config.yml' , 'config.rb' , ] ) ; const source = pkg ? 'package.json' : files . find ( file => entrypoints . has ( file ) ) || fallbackEntrypoint || 'package.json' ; return { src : source || 'package.json' , use : `@vercel/static-build${ withTag }` , config , } ; } function getMissingBuildScriptError ( ) { return { code : 'missing_build_script' , message : 'Your `package.json` file is missing a `build` property inside the `scripts` property.' + '\nLearn More: https://vercel.com/docs/v2/platform/frequently-asked-questions#missing-build-script' , } ; } function validateFunctions ( { functions = { } } ) { for ( const [ path , func ] of Object . entries ( functions ) ) { if ( path . length > 256 ) { return { code : 'invalid_function_glob' , message : 'Function globs must be less than 256 characters long.' , } ; } if ( ! func || typeof func !== 'object' ) { return { code : 'invalid_function' , message : 'Function must be an object.' , } ; } if ( Object . keys ( func ) . length === 0 ) { return { code : 'invalid_function' , message : 'Function must contain at least one property.' , } ; } if ( func . maxDuration !== undefined && ( func . maxDuration < 1 || func . maxDuration > 900 || ! Number . isInteger ( func . maxDuration ) ) ) { return { code : 'invalid_function_duration' , message : 'Functions must have a duration between 1 and 900.' , } ; } if ( func . memory !== undefined && ( func . memory < 128 || func . memory > 3008 || func . memory % 64 !== 0 ) ) { return { code : 'invalid_function_memory' , message : 'Functions must have a memory value between 128 and 3008 in steps of 64.' , } ; } if ( path . startsWith ( '/' ) ) { return { code : 'invalid_function_source' , message : `The function path "${ path }" is invalid. The path must be relative to your project root and therefore cannot start with a slash.` , } ; } if ( func . runtime !== undefined ) { const tag = `${ func . runtime }` . split ( '@' ) . pop ( ) ; if ( ! tag || ! validSemver ( tag ) ) { return { code : 'invalid_function_runtime' , message : 'Function Runtimes must have a valid version, for example `now-php@1.0.0`.' , } ; } } if ( func . includeFiles !== undefined ) { if ( typeof func . includeFiles !== 'string' ) { return { code : 'invalid_function_property' , message : `The property \`includeFiles\` must be a string.` , } ; } } if ( func . excludeFiles !== undefined ) { if ( typeof func . excludeFiles !== 'string' ) { return { code : 'invalid_function_property' , message : `The property \`excludeFiles\` must be a string.` , } ; } } } return null ; } function checkUnusedFunctions ( frontendBuilder , usedFunctions , options ) { const unusedFunctions = new Set ( Object . keys ( options . functions || { } ) . filter ( key => ! usedFunctions . has ( key ) ) ) ; if ( ! unusedFunctions . size ) { return null ; } if ( frontendBuilder && isOfficialRuntime ( 'next' , frontendBuilder . use ) ) { for ( const fnKey of unusedFunctions . values ( ) ) { if ( fnKey . startsWith ( 'pages/' ) || fnKey . startsWith ( 'src/pages' ) ) { unusedFunctions . delete ( fnKey ) ; } else { return { code : 'unused_function' , message : `The pattern "${ fnKey }" defined in \`functions\` doesn't match any Serverless Functions.` , action : 'Learn More' , link : 'https://vercel.link/unmatched-function-pattern' , } ; } } } if ( unusedFunctions . size ) { const [ fnKey ] = Array . from ( unusedFunctions ) ; return { code : 'unused_function' , message : `The pattern "${ fnKey }" defined in \`functions\` doesn't match any Serverless Functions inside the \`api\` directory.` , action : 'Learn More' , link : 'https://vercel.link/unmatched-function-pattern' , } ; } return null ; } function getApiRoute ( fileName , sortedFiles , options , absolutePathCache ) { const conflictingSegment = getConflictingSegment ( fileName ) ; if ( conflictingSegment ) { return { apiRoute : null , isDynamic : false , routeError : { code : 'conflicting_path_segment' , message : `The segment "${ conflictingSegment }" occurs more than ` + `one time in your path "${ fileName }". Please make sure that ` + `every segment in a path is unique.` , } , } ; } const occurrences = pathOccurrences ( fileName , sortedFiles , absolutePathCache ) ; if ( occurrences . length > 0 ) { const messagePaths = concatArrayOfText ( occurrences . map ( name => `"${ name }"` ) ) ; return { apiRoute : null , isDynamic : false , routeError : { code : 'conflicting_file_path' , message : `Two or more files have conflicting paths or names. ` + `Please make sure path segments and filenames, without their extension, are unique. ` + `The path "${ fileName }" has conflicts with ${ messagePaths }.` , } , } ; } const out = createRouteFromPath ( fileName , Boolean ( options . featHandleMiss ) , Boolean ( options . cleanUrls ) ) ; return { apiRoute : out . route , isDynamic : out . isDynamic , routeError : null , } ; } function getConflictingSegment ( filePath ) { const segments = new Set < string > ( ) ; for ( const segment of filePath . split ( '/' ) ) { const name = getSegmentName ( segment ) ; if ( name !== null && segments . has ( name ) ) { return name ; } if ( name ) { segments . add ( name ) ; } } return null ; } function getSegmentName ( segment ) { const { name } = parsePath ( segment ) ; if ( name . startsWith ( '[' ) && name . endsWith ( ']' ) ) { return name . slice ( 1 , - 1 ) ; } return null ; } function getAbsolutePath ( unresolvedPath ) { const { dir , name } = parsePath ( unresolvedPath ) ; const parts = joinPath ( dir , name ) . split ( '/' ) ; return parts . map ( part => part . replace ( /\[.*\]/ , '1' ) ) . join ( '/' ) ; } function pathOccurrences ( fileName , files , absolutePathCache ) { let currentAbsolutePath = absolutePathCache . get ( fileName ) ; if ( ! currentAbsolutePath ) { currentAbsolutePath = getAbsolutePath ( fileName ) ; absolutePathCache . set ( fileName , currentAbsolutePath ) ; } const prev = [ ] ; for ( const file of files ) { if ( file === fileName ) { continue ; } let absolutePath = absolutePathCache . get ( file ) ; if ( ! absolutePath ) { absolutePath = getAbsolutePath ( file ) ; absolutePathCache . set ( file , absolutePath ) ; } if ( absolutePath === currentAbsolutePath ) { prev . push ( file ) ; } else if ( partiallyMatches ( fileName , file ) ) { prev . push ( file ) ; } } return prev ; } function joinPath ( ... segments ) { const joinedPath = segments . join ( '/' ) ; return joinedPath . replace ( /\/{2,}/g , '/' ) ; } function escapeName ( name ) { const special = '[]^$.|?*+()' . split ( '' ) ; for ( const char of special ) { name = name . replace ( new RegExp ( `\\${ char }` , 'g' ) , `\\${ char }` ) ; } return name ; } function concatArrayOfText ( texts ) { if ( texts . length <= 2 ) { return texts . join ( ' and ' ) ; } const last = texts . pop ( ) ; return `${ texts . join ( ', ' ) }, and ${ last }` ; } function partiallyMatches ( pathA , pathB ) { const partsA = pathA . split ( '/' ) ; const partsB = pathB . split ( '/' ) ; const long = partsA . length > partsB . length ? partsA : partsB ; const short = long === partsA ? partsB : partsA ; let index = 0 ; for ( const segmentShort of short ) { const segmentLong = long [ index ] ; const nameLong = getSegmentName ( segmentLong ) ; const nameShort = getSegmentName ( segmentShort ) ; if ( segmentShort !== segmentLong && ( ! nameLong || ! nameShort ) ) { return false ; } if ( nameLong !== nameShort ) { return true ; } index += 1 ; } return false ; } function createRouteFromPath ( filePath , featHandleMiss , cleanUrls ) { const parts = filePath . split ( '/' ) ; let counter = 1 ; const query = [ ] ; let isDynamic = false ; const srcParts = parts . map ( ( segment , i ) : string => { const name = getSegmentName ( segment ) ; const isLast = i === parts . length - 1 ; if ( name !== null ) { query . push ( `${ name }=$${ counter ++ }` ) ; isDynamic = true ; return `([^/]+)` ; } else if ( isLast ) { const { name : fileName , ext } = parsePath ( segment ) ; const isIndex = fileName === 'index' ; const prefix = isIndex ? '/' : '' ; const names = [ isIndex ? prefix : `${ fileName }/` , prefix + escapeName ( fileName ) , featHandleMiss && cleanUrls ? '' : prefix + escapeName ( fileName ) + escapeName ( ext ) , ] . filter ( Boolean ) ; return `(${ names . join ( '|' ) })${ isIndex ? '?' : '' }` ; } return segment ; } ) ; const { name : fileName , ext } = parsePath ( filePath ) ; const isIndex = fileName === 'index' ; const queryString = `${ query . length ? '?' : '' }${ query . join ( '&' ) }` ; const src = isIndex ? `^/${ srcParts . slice ( 0 , - 1 ) . join ( '/' ) }${ srcParts . slice ( - 1 ) [ 0 ] }$` : `^/${ srcParts . join ( '/' ) }$` ; let route ; if ( featHandleMiss ) { const extensionless = ext ? filePath . slice ( 0 , - ext . length ) : filePath ; route = { src , dest : `/${ extensionless }${ queryString }` , check : true , } ; } else { route = { src , dest : `/${ filePath }${ queryString }` , } ; } return { route , isDynamic } ; } interface LimitedRoutes { defaultRoutes : Route [ ] ; redirectRoutes : Route [ ] ; rewriteRoutes : Route [ ] ; } function getRouteResult ( pkg , apiRoutes , dynamicRoutes , outputDirectory , apiBuilders , frontendBuilder , options ) { const deps = Object . assign ( { } , pkg ?. dependencies , pkg ?. devDependencies ) ; const defaultRoutes = [ ] ; const redirectRoutes = [ ] ; const rewriteRoutes = [ ] ; const errorRoutes = [ ] ; const limitedRoutes = { defaultRoutes : [ ] , redirectRoutes : [ ] , rewriteRoutes : [ ] , } ; const framework = frontendBuilder ?. config ?. framework || '' ; const isNextjs = framework === 'nextjs' || isOfficialRuntime ( 'next' , frontendBuilder ?. use ) ; const ignoreRuntimes = slugToFramework . get ( framework ) ?. ignoreRuntimes ; if ( apiRoutes && apiRoutes . length > 0 ) { if ( options . featHandleMiss ) { const extSet = detectApiExtensions ( apiBuilders ) ; const withTag = options . tag ? `@${ options . tag }` : '' ; const extSetLimited = detectApiExtensions ( apiBuilders . filter ( b => { if ( b . use === `@vercel/python${ withTag }` && ! ( 'vercel-plugin-python' in deps ) ) { return false ; } if ( b . use === `@vercel/go${ withTag }` && ! ( 'vercel-plugin-go' in deps ) ) { return false ; } if ( b . use === `@vercel/ruby${ withTag }` && ! ( 'vercel-plugin-ruby' in deps ) ) { return false ; } return true ; } ) ) ; if ( extSet . size > 0 ) { const extGroup = `(?:\\.(?:${ Array . from ( extSet ) . map ( ext => ext . slice ( 1 ) ) . join ( '|' ) }))` ; const extGroupLimited = `(?:\\.(?:${ Array . from ( extSetLimited ) . map ( ext => ext . slice ( 1 ) ) . join ( '|' ) }))` ; if ( options . cleanUrls ) { redirectRoutes . push ( { src : `^/(api(?:.+)?)/index${ extGroup }?/?$` , headers : { Location : options . trailingSlash ? '/$1/' : '/$1' } , status : 308 , } ) ; redirectRoutes . push ( { src : `^/api/(.+)${ extGroup }/?$` , headers : { Location : options . trailingSlash ? '/api/$1/' : '/api/$1' , } , status : 308 , } ) ; limitedRoutes . redirectRoutes . push ( { src : `^/(api(?:.+)?)/index${ extGroupLimited }?/?$` , headers : { Location : options . trailingSlash ? '/$1/' : '/$1' } , status : 308 , } ) ; limitedRoutes . redirectRoutes . push ( { src : `^/api/(.+)${ extGroupLimited }/?$` , headers : { Location : options . trailingSlash ? '/api/$1/' : '/api/$1' , } , status : 308 , } ) ; } else { defaultRoutes . push ( { handle : 'miss' } ) ; defaultRoutes . push ( { src : `^/api/(.+)${ extGroup }$` , dest : '/api/$1' , check : true , } ) ; limitedRoutes . defaultRoutes . push ( { handle : 'miss' } ) ; limitedRoutes . defaultRoutes . push ( { src : `^/api/(.+)${ extGroupLimited }$` , dest : '/api/$1' , check : true , } ) ; } } rewriteRoutes . push ( ... dynamicRoutes ) ; limitedRoutes . rewriteRoutes . push ( ... dynamicRoutes ) ; if ( typeof ignoreRuntimes === 'undefined' ) { rewriteRoutes . push ( { src : '^/api(/.*)?$' , status : 404 , } ) ; } } else { defaultRoutes . push ( ... apiRoutes ) ; if ( apiRoutes . length ) { defaultRoutes . push ( { status : 404 , src : '^/api(/.*)?$' , } ) ; } } } if ( outputDirectory && frontendBuilder && ! options . featHandleMiss && isOfficialRuntime ( 'static' , frontendBuilder . use ) ) { defaultRoutes . push ( { src : '/(.*)' , dest : `/${ outputDirectory }/$1` , } ) ; } if ( options . featHandleMiss && ! isNextjs ) { errorRoutes . push ( { status : 404 , src : '^(?!/api).*$' , dest : options . cleanUrls ? '/404' : '/404.html' , } ) ; } return { defaultRoutes , redirectRoutes , rewriteRoutes , errorRoutes , limitedRoutes , } ; } function sortFilesBySegmentCount ( fileA , fileB ) { const lengthA = fileA . split ( '/' ) . length ; const lengthB = fileB . split ( '/' ) . length ; if ( lengthA > lengthB ) { return - 1 ; } if ( lengthA < lengthB ) { return 1 ; } const countSegments = ( prev , segment ) => getSegmentName ( segment ) ? prev + 1 : 0 ; const segmentLengthA = fileA . split ( '/' ) . reduce ( countSegments , 0 ) ; const segmentLengthB = fileB . split ( '/' ) . reduce ( countSegments , 0 ) ; if ( segmentLengthA > segmentLengthB ) { return 1 ; } if ( segmentLengthA < segmentLengthB ) { return - 1 ; } return fileA . localeCompare ( fileB ) ; }