2023-07-04 05:24:50 +00:00
import fs from 'node:fs'
2022-06-27 12:10:29 +00:00
import { extname , normalize , relative , resolve } from 'pathe'
2021-01-18 12:22:38 +00:00
import { encodePath } from 'ufo'
2022-01-25 12:29:11 +00:00
import { resolveFiles , useNuxt } from '@nuxt/kit'
2023-04-07 16:02:47 +00:00
import { genArrayFromRaw , genDynamicImport , genImport , genSafeVariableName } from 'knitwork'
2022-01-27 11:13:32 +00:00
import escapeRE from 'escape-string-regexp'
2023-01-30 20:24:58 +00:00
import { filename } from 'pathe/utils'
import { hash } from 'ohash'
2023-07-04 05:24:50 +00:00
import { transform } from 'esbuild'
import { parse } from 'acorn'
import type { CallExpression , ExpressionStatement , ObjectExpression , Program , Property } from 'estree'
2023-02-13 22:42:04 +00:00
import type { NuxtPage } from 'nuxt/schema'
2020-08-18 18:34:08 +00:00
2023-03-11 21:16:01 +00:00
import { uniqueBy } from '../core/utils'
2021-03-18 14:26:41 +00:00
enum SegmentParserState {
initial ,
static ,
dynamic ,
2022-04-26 16:10:05 +00:00
optional ,
2021-06-21 12:09:08 +00:00
catchall ,
2021-03-18 14:26:41 +00:00
}
enum SegmentTokenType {
static ,
dynamic ,
2022-04-26 16:10:05 +00:00
optional ,
2021-06-21 12:09:08 +00:00
catchall ,
2021-03-18 14:26:41 +00:00
}
interface SegmentToken {
type : SegmentTokenType
value : string
}
2022-03-22 18:12:54 +00:00
export async function resolvePagesRoutes ( ) : Promise < NuxtPage [ ] > {
const nuxt = useNuxt ( )
const pagesDirs = nuxt . options . _layers . map (
layer = > resolve ( layer . config . srcDir , layer . config . dir ? . pages || 'pages' )
)
2020-08-18 18:34:08 +00:00
2022-03-22 18:12:54 +00:00
const allRoutes = ( await Promise . all (
pagesDirs . map ( async ( dir ) = > {
const files = await resolveFiles ( dir , ` **/*{ ${ nuxt . options . extensions . join ( ',' ) } } ` )
// Sort to make sure parent are listed first
files . sort ( )
2023-07-04 05:24:50 +00:00
return generateRoutesFromFiles ( files , dir , nuxt . options . experimental . typedPages , nuxt . vfs )
2022-03-22 18:12:54 +00:00
} )
) ) . flat ( )
2021-05-20 11:42:41 +00:00
2022-03-25 11:55:05 +00:00
return uniqueBy ( allRoutes , 'path' )
2021-01-18 12:22:38 +00:00
}
2023-07-04 05:24:50 +00:00
export async function generateRoutesFromFiles ( files : string [ ] , pagesDir : string , shouldExtractBuildMeta = false , vfs? : Record < string , string > ) : Promise < NuxtPage [ ] > {
2022-01-17 18:27:23 +00:00
const routes : NuxtPage [ ] = [ ]
2020-08-18 18:34:08 +00:00
for ( const file of files ) {
2021-01-18 12:22:38 +00:00
const segments = relative ( pagesDir , file )
2022-01-27 11:13:32 +00:00
. replace ( new RegExp ( ` ${ escapeRE ( extname ( file ) ) } $ ` ) , '' )
2020-08-18 18:34:08 +00:00
. split ( '/' )
2022-01-17 18:27:23 +00:00
const route : NuxtPage = {
2020-08-18 18:34:08 +00:00
name : '' ,
path : '' ,
2021-01-18 12:22:38 +00:00
file ,
children : [ ]
2020-08-18 18:34:08 +00:00
}
2021-05-20 11:42:41 +00:00
// Array where routes should be added, useful when adding child routes
2020-08-18 18:34:08 +00:00
let parent = routes
2021-01-18 12:22:38 +00:00
for ( let i = 0 ; i < segments . length ; i ++ ) {
const segment = segments [ i ]
2020-08-18 18:34:08 +00:00
2021-01-18 12:22:38 +00:00
const tokens = parseSegment ( segment )
const segmentName = tokens . map ( ( { value } ) = > value ) . join ( '' )
2020-08-18 18:34:08 +00:00
2021-01-18 12:22:38 +00:00
// ex: parent/[slug].vue -> parent-slug
2023-02-13 22:56:39 +00:00
route . name += ( route . name && '/' ) + segmentName
2021-01-18 12:22:38 +00:00
// ex: parent.vue + parent/child.vue
2022-08-01 07:51:46 +00:00
const child = parent . find ( parentRoute = > parentRoute . name === route . name && ! parentRoute . path . endsWith ( '(.*)*' ) )
2022-08-12 17:47:58 +00:00
if ( child && child . children ) {
2020-08-18 18:34:08 +00:00
parent = child . children
route . path = ''
2021-01-18 12:22:38 +00:00
} else if ( segmentName === 'index' && ! route . path ) {
2020-08-18 18:34:08 +00:00
route . path += '/'
2021-01-18 12:22:38 +00:00
} else if ( segmentName !== 'index' ) {
route . path += getRoutePath ( tokens )
2020-08-18 18:34:08 +00:00
}
}
2023-07-04 05:24:50 +00:00
if ( shouldExtractBuildMeta && vfs ) {
const fileContent = file in vfs ? vfs [ file ] : fs . readFileSync ( resolve ( pagesDir , file ) , 'utf-8' )
const overrideRouteName = await getRouteName ( fileContent )
if ( overrideRouteName ) {
route . name = overrideRouteName
}
}
2020-08-18 18:34:08 +00:00
parent . push ( route )
}
return prepareRoutes ( routes )
}
2023-07-04 05:24:50 +00:00
const SFC_SCRIPT_RE = /<script\s*[^>]*>([\s\S]*?)<\/script\s*[^>]*>/i
2023-08-23 20:38:17 +00:00
export function extractScriptContent ( html : string ) {
2023-07-04 05:24:50 +00:00
const match = html . match ( SFC_SCRIPT_RE )
if ( match && match [ 1 ] ) {
return match [ 1 ] . trim ( )
}
return null
}
const PAGE_META_RE = /(definePageMeta\([\s\S]*?\))/
async function getRouteName ( file : string ) {
const script = extractScriptContent ( file )
if ( ! script ) { return null }
if ( ! PAGE_META_RE . test ( script ) ) { return null }
const js = await transform ( script , { loader : 'ts' } )
const ast = parse ( js . code , {
sourceType : 'module' ,
ecmaVersion : 'latest'
} ) as unknown as Program
const pageMetaAST = ast . body . find ( node = > node . type === 'ExpressionStatement' && node . expression . type === 'CallExpression' && node . expression . callee . type === 'Identifier' && node . expression . callee . name === 'definePageMeta' )
if ( ! pageMetaAST ) { return null }
const pageMetaArgument = ( ( pageMetaAST as ExpressionStatement ) . expression as CallExpression ) . arguments [ 0 ] as ObjectExpression
const nameProperty = pageMetaArgument . properties . find ( property = > property . type === 'Property' && property . key . type === 'Identifier' && property . key . name === 'name' ) as Property
if ( ! nameProperty || nameProperty . value . type !== 'Literal' || typeof nameProperty . value . value !== 'string' ) { return null }
return nameProperty . value . value
}
2021-01-18 12:22:38 +00:00
function getRoutePath ( tokens : SegmentToken [ ] ) : string {
return tokens . reduce ( ( path , token ) = > {
return (
path +
2022-04-26 16:10:05 +00:00
( token . type === SegmentTokenType . optional
? ` : ${ token . value } ? `
: token . type === SegmentTokenType . dynamic
2023-04-03 09:56:44 +00:00
? ` : ${ token . value } () `
2022-04-26 16:10:05 +00:00
: token . type === SegmentTokenType . catchall
? ` : ${ token . value } (.*)* `
2023-06-25 16:40:30 +00:00
: encodePath ( token . value ) . replace ( /:/g , '\\:' ) )
2021-01-18 12:22:38 +00:00
)
} , '/' )
}
2021-06-21 12:09:08 +00:00
const PARAM_CHAR_RE = /[\w\d_.]/
2021-01-18 12:22:38 +00:00
function parseSegment ( segment : string ) {
2021-06-21 12:09:08 +00:00
let state : SegmentParserState = SegmentParserState . initial
2021-01-18 12:22:38 +00:00
let i = 0
let buffer = ''
const tokens : SegmentToken [ ] = [ ]
function consumeBuffer ( ) {
if ( ! buffer ) {
return
}
if ( state === SegmentParserState . initial ) {
throw new Error ( 'wrong state' )
}
tokens . push ( {
type :
state === SegmentParserState . static
? SegmentTokenType . static
2021-06-21 12:09:08 +00:00
: state === SegmentParserState . dynamic
? SegmentTokenType . dynamic
2022-04-26 16:10:05 +00:00
: state === SegmentParserState . optional
? SegmentTokenType . optional
: SegmentTokenType . catchall ,
2021-01-18 12:22:38 +00:00
value : buffer
} )
buffer = ''
}
while ( i < segment . length ) {
const c = segment [ i ]
switch ( state ) {
case SegmentParserState . initial :
buffer = ''
if ( c === '[' ) {
state = SegmentParserState . dynamic
} else {
i --
state = SegmentParserState . static
}
break
case SegmentParserState . static :
if ( c === '[' ) {
consumeBuffer ( )
state = SegmentParserState . dynamic
} else {
buffer += c
}
break
2021-06-21 12:09:08 +00:00
case SegmentParserState . catchall :
2021-01-18 12:22:38 +00:00
case SegmentParserState . dynamic :
2022-04-26 16:10:05 +00:00
case SegmentParserState . optional :
2021-06-21 12:09:08 +00:00
if ( buffer === '...' ) {
buffer = ''
state = SegmentParserState . catchall
}
2022-04-26 16:10:05 +00:00
if ( c === '[' && state === SegmentParserState . dynamic ) {
state = SegmentParserState . optional
}
2023-09-11 08:13:24 +00:00
if ( c === ']' && ( state !== SegmentParserState . optional || segment [ i - 1 ] === ']' ) ) {
2021-10-20 18:12:55 +00:00
if ( ! buffer ) {
throw new Error ( 'Empty param' )
} else {
consumeBuffer ( )
}
2021-01-18 12:22:38 +00:00
state = SegmentParserState . initial
} else if ( PARAM_CHAR_RE . test ( c ) ) {
buffer += c
} else {
2023-01-14 01:13:48 +00:00
2021-10-20 18:12:55 +00:00
// console.debug(`[pages]Ignored character "${c}" while building param "${buffer}" from "segment"`)
2021-01-18 12:22:38 +00:00
}
break
}
i ++
}
if ( state === SegmentParserState . dynamic ) {
throw new Error ( ` Unfinished param " ${ buffer } " ` )
}
consumeBuffer ( )
return tokens
}
2023-02-13 22:56:39 +00:00
function findRouteByName ( name : string , routes : NuxtPage [ ] ) : NuxtPage | undefined {
for ( const route of routes ) {
if ( route . name === name ) {
return route
}
}
return findRouteByName ( name , routes )
}
function prepareRoutes ( routes : NuxtPage [ ] , parent? : NuxtPage , names = new Set < string > ( ) ) {
2020-08-18 18:34:08 +00:00
for ( const route of routes ) {
2021-01-18 12:22:38 +00:00
// Remove -index
2020-08-18 18:34:08 +00:00
if ( route . name ) {
2023-02-13 22:56:39 +00:00
route . name = route . name
. replace ( /\/index$/ , '' )
. replace ( /\//g , '-' )
if ( names . has ( route . name ) ) {
const existingRoute = findRouteByName ( route . name , routes )
const extra = existingRoute ? . name ? ` is the same as \` ${ existingRoute . file } \` ` : 'is a duplicate'
console . warn ( ` [nuxt] Route name generated for \` ${ route . file } \` ${ extra } . You may wish to set a custom name using \` definePageMeta \` within the page file. ` )
}
2020-08-18 18:34:08 +00:00
}
2021-01-18 12:22:38 +00:00
// Remove leading / if children route
if ( parent && route . path . startsWith ( '/' ) ) {
route . path = route . path . slice ( 1 )
2020-08-18 18:34:08 +00:00
}
2022-08-12 17:47:58 +00:00
if ( route . children ? . length ) {
2023-02-13 22:56:39 +00:00
route . children = prepareRoutes ( route . children , route , names )
2021-01-18 12:22:38 +00:00
}
2022-08-12 17:47:58 +00:00
if ( route . children ? . find ( childRoute = > childRoute . path === '' ) ) {
2020-08-18 18:34:08 +00:00
delete route . name
}
2023-02-13 22:56:39 +00:00
if ( route . name ) {
names . add ( route . name )
}
2020-08-18 18:34:08 +00:00
}
2021-01-18 12:22:38 +00:00
2020-08-18 18:34:08 +00:00
return routes
}
2021-06-30 16:32:22 +00:00
2022-02-07 13:45:47 +00:00
export function normalizeRoutes ( routes : NuxtPage [ ] , metaImports : Set < string > = new Set ( ) ) : { imports : Set < string > , routes : string } {
2022-01-17 18:27:23 +00:00
return {
imports : metaImports ,
2022-12-12 12:25:00 +00:00
routes : genArrayFromRaw ( routes . map ( ( page ) = > {
2023-03-03 14:07:42 +00:00
const route = Object . fromEntries (
Object . entries ( page )
. filter ( ( [ key , value ] ) = > key !== 'file' && ( Array . isArray ( value ) ? value.length : value ) )
. map ( ( [ key , value ] ) = > [ key , JSON . stringify ( value ) ] )
) as Record < Exclude < keyof NuxtPage , ' file ' > , string > & { component? : string }
if ( page . children ? . length ) {
route . children = normalizeRoutes ( page . children , metaImports ) . routes
}
// Without a file, we can't use `definePageMeta` to extract route-level meta from the file
if ( ! page . file ) {
for ( const key of [ 'name' , 'path' , 'meta' , 'alias' , 'redirect' ] as const ) {
if ( page [ key ] ) { route [ key ] = JSON . stringify ( page [ key ] ) }
}
return route
}
2022-12-12 12:25:00 +00:00
const file = normalize ( page . file )
2023-01-30 20:24:58 +00:00
const metaImportName = genSafeVariableName ( filename ( file ) + hash ( file ) ) + 'Meta'
2022-11-02 10:28:41 +00:00
metaImports . add ( genImport ( ` ${ file } ?macro=true ` , [ { name : 'default' , as : metaImportName } ] ) )
2022-09-05 07:53:01 +00:00
let aliasCode = ` ${ metaImportName } ?.alias || [] `
2023-03-03 14:07:42 +00:00
const alias = Array . isArray ( page . alias ) ? page . alias : [ page . alias ] . filter ( Boolean )
if ( alias . length ) {
aliasCode = ` ${ JSON . stringify ( alias ) } .concat( ${ aliasCode } ) `
2022-01-17 18:27:23 +00:00
}
2022-12-12 12:25:00 +00:00
2023-03-03 14:07:42 +00:00
route . name = ` ${ metaImportName } ?.name ?? ${ page . name ? JSON . stringify ( page . name ) : 'undefined' } `
route . path = ` ${ metaImportName } ?.path ?? ${ JSON . stringify ( page . path ) } `
route . meta = page . meta && Object . values ( page . meta ) . filter ( value = > value !== undefined ) . length ? ` {...( ${ metaImportName } || {}), ... ${ JSON . stringify ( page . meta ) } } ` : ` ${ metaImportName } || {} `
route . alias = aliasCode
route . redirect = page . redirect ? JSON . stringify ( page . redirect ) : ` ${ metaImportName } ?.redirect || undefined `
route . component = genDynamicImport ( file , { interopDefault : true } )
2022-12-12 12:25:00 +00:00
return route
2022-02-07 13:45:47 +00:00
} ) )
2022-01-17 18:27:23 +00:00
}
2021-10-20 18:49:15 +00:00
}
2023-08-23 20:38:17 +00:00
export function pathToNitroGlob ( path : string ) {
if ( ! path ) {
return null
}
// Ignore pages with multiple dynamic parameters.
if ( path . indexOf ( ':' ) !== path . lastIndexOf ( ':' ) ) {
return null
}
return path . replace ( /\/(?:[^:/]+)?:\w+.*$/ , '/**' )
}