2023-07-30 16:14:42 +00:00
import { promises as fsp , mkdirSync , writeFileSync } from 'node:fs'
2024-01-02 22:04:26 +00:00
import { dirname , join , relative , resolve } from 'pathe'
2023-01-30 12:09:48 +00:00
import { defu } from 'defu'
2024-05-01 13:10:33 +00:00
import { compileTemplate as _compileTemplate , findPath , logger , normalizePlugin , normalizeTemplate , resolveAlias , resolveFiles , resolvePath , templateUtils } from '@nuxt/kit'
2023-03-11 21:16:01 +00:00
import type { Nuxt , NuxtApp , NuxtPlugin , NuxtTemplate , ResolvedNuxtTemplate } from 'nuxt/schema'
2021-09-21 14:55:07 +00:00
2021-10-13 20:41:21 +00:00
import * as defaultTemplates from './templates'
2022-06-27 12:10:29 +00:00
import { getNameFromPath , hasSuffix , uniqueBy } from './utils'
2023-06-19 23:00:03 +00:00
import { extractMetadata , orderMap } from './plugins/plugin-metadata'
2021-04-02 11:47:01 +00:00
2023-12-14 17:11:08 +00:00
import type { PluginMeta } from '#app'
2021-05-20 11:42:41 +00:00
export function createApp ( nuxt : Nuxt , options : Partial < NuxtApp > = { } ) : NuxtApp {
return defu ( options , {
2020-08-19 13:06:27 +00:00
dir : nuxt.options.srcDir ,
extensions : nuxt.options.extensions ,
2021-02-19 01:08:45 +00:00
plugins : [ ] ,
2023-05-01 16:35:00 +00:00
components : [ ] ,
2024-04-05 18:08:32 +00:00
templates : [ ] ,
2022-08-12 17:47:58 +00:00
} as unknown as NuxtApp ) as NuxtApp
2021-05-20 11:42:41 +00:00
}
2024-05-01 05:43:35 +00:00
const postTemplates = [
defaultTemplates . clientPluginTemplate . filename ,
defaultTemplates . serverPluginTemplate . filename ,
defaultTemplates . pluginsDeclaration . filename ,
]
2022-10-24 08:53:02 +00:00
export async function generateApp ( nuxt : Nuxt , app : NuxtApp , options : { filter ? : ( template : ResolvedNuxtTemplate < any > ) = > boolean } = { } ) {
2021-05-20 11:42:41 +00:00
// Resolve app
await resolveApp ( nuxt , app )
2021-07-28 11:35:24 +00:00
// User templates from options.build.templates
2022-08-12 17:47:58 +00:00
app . templates = Object . values ( defaultTemplates ) . concat ( nuxt . options . build . templates ) as NuxtTemplate [ ]
2021-06-16 11:22:01 +00:00
2021-07-28 11:35:24 +00:00
// Extend templates with hook
2021-05-20 11:42:41 +00:00
await nuxt . callHook ( 'app:templates' , app )
2021-07-28 11:35:24 +00:00
// Normalize templates
app . templates = app . templates . map ( tmpl = > normalizeTemplate ( tmpl ) )
2024-05-01 05:43:35 +00:00
// compile plugins first as they are needed within the nuxt.vfs
// in order to annotate templated plugins
const filteredTemplates : Record < 'pre' | 'post' , Array < ResolvedNuxtTemplate < any > >> = {
pre : [ ] ,
post : [ ] ,
}
for ( const template of app . templates as Array < ResolvedNuxtTemplate < any > > ) {
if ( options . filter && ! options . filter ( template ) ) { continue }
const key = template . filename && postTemplates . includes ( template . filename ) ? 'post' : 'pre'
filteredTemplates [ key ] . push ( template )
}
2021-07-15 10:18:34 +00:00
// Compile templates into vfs
2023-07-30 16:14:42 +00:00
// TODO: remove utils in v4
2021-07-15 10:18:34 +00:00
const templateContext = { utils : templateUtils , nuxt , app }
2024-04-30 19:34:32 +00:00
const compileTemplate = nuxt . options . experimental . compileTemplate ? _compileTemplate : futureCompileTemplate
2023-07-30 16:14:42 +00:00
const writes : Array < ( ) = > void > = [ ]
2024-05-01 05:43:35 +00:00
const changedTemplates : Array < ResolvedNuxtTemplate < any > > = [ ]
async function processTemplate ( template : ResolvedNuxtTemplate ) {
const fullPath = template . dst || resolve ( nuxt . options . buildDir , template . filename ! )
const mark = performance . mark ( fullPath )
const oldContents = nuxt . vfs [ fullPath ]
const contents = await compileTemplate ( template , templateContext ) . catch ( ( e ) = > {
logger . error ( ` Could not compile template \` ${ template . filename } \` . ` )
logger . error ( e )
throw e
} )
template . modified = oldContents !== contents
if ( template . modified ) {
nuxt . vfs [ fullPath ] = contents
const aliasPath = '#build/' + template . filename ! . replace ( /\.\w+$/ , '' )
nuxt . vfs [ aliasPath ] = contents
// In case a non-normalized absolute path is called for on Windows
if ( process . platform === 'win32' ) {
nuxt . vfs [ fullPath . replace ( /\//g , '\\' ) ] = contents
}
2022-10-24 08:53:02 +00:00
2024-05-01 05:43:35 +00:00
changedTemplates . push ( template )
}
2022-10-24 08:53:02 +00:00
2024-05-29 16:05:21 +00:00
const perf = performance . measure ( fullPath , mark . name )
const setupTime = Math . round ( ( perf . duration * 100 ) ) / 100
2022-10-24 08:53:02 +00:00
2024-05-01 05:43:35 +00:00
if ( nuxt . options . debug || setupTime > 500 ) {
logger . info ( ` Compiled \` ${ template . filename } \` in ${ setupTime } ms ` )
}
2023-07-30 16:14:42 +00:00
2024-05-01 05:43:35 +00:00
if ( template . modified && template . write ) {
writes . push ( ( ) = > {
mkdirSync ( dirname ( fullPath ) , { recursive : true } )
writeFileSync ( fullPath , contents , 'utf8' )
} )
}
}
2023-07-30 16:14:42 +00:00
2024-05-01 05:43:35 +00:00
await Promise . allSettled ( filteredTemplates . pre . map ( processTemplate ) )
await Promise . allSettled ( filteredTemplates . post . map ( processTemplate ) )
2021-05-20 11:42:41 +00:00
2023-07-30 16:14:42 +00:00
// Write template files in single synchronous step to avoid (possible) additional
// runtime overhead of cascading HMRs from vite/webpack
for ( const write of writes ) { write ( ) }
2024-03-14 18:56:17 +00:00
if ( changedTemplates . length ) {
await nuxt . callHook ( 'app:templatesGenerated' , app , changedTemplates , options )
}
2021-05-20 11:42:41 +00:00
}
2020-08-19 12:38:18 +00:00
2023-10-06 10:30:53 +00:00
/** @internal */
2024-04-30 19:34:32 +00:00
async function futureCompileTemplate < T > ( template : NuxtTemplate < T > , ctx : { nuxt : Nuxt , app : NuxtApp , utils? : unknown } ) {
delete ctx . utils
if ( template . src ) {
try {
return await fsp . readFile ( template . src , 'utf-8' )
} catch ( err ) {
logger . error ( ` [nuxt] Error reading template from \` ${ template . src } \` ` )
throw err
}
}
if ( template . getContents ) {
return template . getContents ( { . . . ctx , options : template.options ! } )
}
throw new Error ( '[nuxt] Invalid template. Templates must have either `src` or `getContents`: ' + JSON . stringify ( template ) )
}
2023-10-06 10:30:53 +00:00
export async function resolveApp ( nuxt : Nuxt , app : NuxtApp ) {
2021-05-20 11:42:41 +00:00
// Resolve main (app.vue)
2021-10-12 12:51:41 +00:00
if ( ! app . mainComponent ) {
2022-07-29 11:12:50 +00:00
app . mainComponent = await findPath (
2022-12-06 11:30:14 +00:00
nuxt . options . _layers . flatMap ( layer = > [
join ( layer . config . srcDir , 'App' ) ,
2024-04-05 18:08:32 +00:00
join ( layer . config . srcDir , 'app' ) ,
] ) ,
2022-07-29 11:12:50 +00:00
)
2020-08-17 15:25:06 +00:00
}
2021-10-12 12:51:41 +00:00
if ( ! app . mainComponent ) {
2024-05-01 13:10:33 +00:00
app . mainComponent = resolve ( nuxt . options . appDir , 'components/welcome.vue' )
2021-01-18 12:22:38 +00:00
}
2021-05-20 11:42:41 +00:00
2022-03-17 10:57:02 +00:00
// Resolve root component
if ( ! app . rootComponent ) {
app . rootComponent = await findPath ( [ '~/app.root' , resolve ( nuxt . options . appDir , 'components/nuxt-root.vue' ) ] )
}
2021-10-12 12:51:41 +00:00
2022-03-11 08:22:16 +00:00
// Resolve error component
if ( ! app . errorComponent ) {
2022-12-06 09:44:03 +00:00
app . errorComponent = ( await findPath (
2024-04-05 18:08:32 +00:00
nuxt . options . _layers . map ( layer = > join ( layer . config . srcDir , 'error' ) ) ,
2022-12-06 09:44:03 +00:00
) ) ? ? resolve ( nuxt . options . appDir , 'components/nuxt-error-page.vue' )
2022-03-11 08:22:16 +00:00
}
2022-03-16 20:36:30 +00:00
// Resolve layouts/ from all config layers
2023-12-14 10:33:51 +00:00
const layerConfigs = nuxt . options . _layers . map ( layer = > layer . config )
const reversedConfigs = layerConfigs . slice ( ) . reverse ( )
2022-03-14 10:47:24 +00:00
app . layouts = { }
2023-12-14 10:33:51 +00:00
for ( const config of layerConfigs ) {
2023-09-12 14:27:28 +00:00
const layoutDir = ( config . rootDir === nuxt . options . rootDir ? nuxt.options : config ) . dir ? . layouts || 'layouts'
2023-10-16 21:58:40 +00:00
const layoutFiles = await resolveFiles ( config . srcDir , ` ${ layoutDir } /**/*{ ${ nuxt . options . extensions . join ( ',' ) } } ` )
2022-03-14 10:47:24 +00:00
for ( const file of layoutFiles ) {
2023-10-16 21:58:40 +00:00
const name = getNameFromPath ( file , resolve ( config . srcDir , layoutDir ) )
2024-01-02 22:04:26 +00:00
if ( ! name ) {
// Ignore files like `~/layouts/index.vue` which end up not having a name at all
2024-01-30 13:16:05 +00:00
logger . warn ( ` No layout name could be resolved for \` ~/ ${ relative ( nuxt . options . srcDir , file ) } \` . Bear in mind that \` index \` is ignored for the purpose of creating a layout name. ` )
2024-01-02 22:04:26 +00:00
continue
}
2022-03-14 10:47:24 +00:00
app . layouts [ name ] = app . layouts [ name ] || { name , file }
}
}
2023-09-16 08:39:51 +00:00
// Resolve middleware/ from all config layers, layers first
2022-06-27 12:10:29 +00:00
app . middleware = [ ]
2023-12-14 10:33:51 +00:00
for ( const config of reversedConfigs ) {
2023-09-12 14:27:28 +00:00
const middlewareDir = ( config . rootDir === nuxt . options . rootDir ? nuxt.options : config ) . dir ? . middleware || 'middleware'
2024-05-13 20:23:15 +00:00
const middlewareFiles = await resolveFiles ( config . srcDir , [
` ${ middlewareDir } /*{ ${ nuxt . options . extensions . join ( ',' ) } } ` ,
. . . nuxt . options . future . compatibilityVersion === 4
? [ ` ${ middlewareDir } /*/index{ ${ nuxt . options . extensions . join ( ',' ) } } ` ]
: [ ] ,
] )
2024-01-02 22:04:26 +00:00
for ( const file of middlewareFiles ) {
2022-06-27 12:10:29 +00:00
const name = getNameFromPath ( file )
2024-01-02 22:04:26 +00:00
if ( ! name ) {
// Ignore files like `~/middleware/index.vue` which end up not having a name at all
2024-01-30 13:16:05 +00:00
logger . warn ( ` No middleware name could be resolved for \` ~/ ${ relative ( nuxt . options . srcDir , file ) } \` . Bear in mind that \` index \` is ignored for the purpose of creating a middleware name. ` )
2024-01-02 22:04:26 +00:00
continue
}
app . middleware . push ( { name , path : file , global : hasSuffix ( file , '.global' ) } )
}
2022-06-27 12:10:29 +00:00
}
2023-09-04 22:41:51 +00:00
// Resolve plugins, first extended layers and then base
2023-09-12 14:24:35 +00:00
app . plugins = [ ]
2023-12-14 10:33:51 +00:00
for ( const config of reversedConfigs ) {
2023-09-12 14:27:28 +00:00
const pluginDir = ( config . rootDir === nuxt . options . rootDir ? nuxt.options : config ) . dir ? . plugins || 'plugins'
2022-03-09 10:51:32 +00:00
app . plugins . push ( . . . [
2022-03-16 20:36:30 +00:00
. . . ( config . plugins || [ ] ) ,
2022-08-12 17:47:58 +00:00
. . . config . srcDir
? await resolveFiles ( config . srcDir , [
2024-03-13 22:50:15 +00:00
` ${ pluginDir } /*{ ${ nuxt . options . extensions . join ( ',' ) } } ` ,
2024-05-13 20:23:15 +00:00
` ${ pluginDir } /*/index{ ${ nuxt . options . extensions . join ( ',' ) } } ` ,
2022-08-12 17:47:58 +00:00
] )
2024-04-05 18:08:32 +00:00
: [ ] ,
2022-03-09 10:51:32 +00:00
] . map ( plugin = > normalizePlugin ( plugin as NuxtPlugin ) ) )
}
2022-08-04 15:15:42 +00:00
2023-09-12 14:24:35 +00:00
// Add back plugins not specified in layers or user config
2023-09-13 08:35:11 +00:00
for ( const p of [ . . . nuxt . options . plugins ] . reverse ( ) ) {
2023-09-12 14:24:35 +00:00
const plugin = normalizePlugin ( p )
if ( ! app . plugins . some ( p = > p . src === plugin . src ) ) {
app . plugins . unshift ( plugin )
}
}
2022-08-04 15:15:42 +00:00
// Normalize and de-duplicate plugins and middleware
2023-10-12 22:21:02 +00:00
app . middleware = uniqueBy ( await resolvePaths ( [ . . . app . middleware ] . reverse ( ) , 'path' ) , 'name' ) . reverse ( )
2022-08-04 15:15:42 +00:00
app . plugins = uniqueBy ( await resolvePaths ( app . plugins , 'src' ) , 'src' )
2021-05-20 11:42:41 +00:00
2022-08-17 15:23:13 +00:00
// Resolve app.config
app . configs = [ ]
2023-12-14 10:33:51 +00:00
for ( const config of layerConfigs ) {
2022-08-17 15:23:13 +00:00
const appConfigPath = await findPath ( resolve ( config . srcDir , 'app.config' ) )
if ( appConfigPath ) {
app . configs . push ( appConfigPath )
}
}
2021-05-20 11:42:41 +00:00
// Extend app
await nuxt . callHook ( 'app:resolve' , app )
2022-08-04 15:15:42 +00:00
// Normalize and de-duplicate plugins and middleware
app . middleware = uniqueBy ( await resolvePaths ( app . middleware , 'path' ) , 'name' )
app . plugins = uniqueBy ( await resolvePaths ( app . plugins , 'src' ) , 'src' )
}
2022-12-06 11:30:14 +00:00
function resolvePaths < Item extends Record < string , any > > ( items : Item [ ] , key : { [ K in keyof Item ] : Item [ K ] extends string ? K : never } [ keyof Item ] ) {
2022-08-04 15:15:42 +00:00
return Promise . all ( items . map ( async ( item ) = > {
if ( ! item [ key ] ) { return item }
return {
. . . item ,
2024-04-05 18:08:32 +00:00
[ key ] : await resolvePath ( resolveAlias ( item [ key ] ) ) ,
2022-08-04 15:15:42 +00:00
}
} ) )
2021-05-20 11:42:41 +00:00
}
2023-06-19 23:00:03 +00:00
2024-03-17 16:57:11 +00:00
const IS_TSX = /\.[jt]sx$/
2023-06-19 23:00:03 +00:00
export async function annotatePlugins ( nuxt : Nuxt , plugins : NuxtPlugin [ ] ) {
2023-12-14 17:11:08 +00:00
const _plugins : Array < NuxtPlugin & Omit < PluginMeta , ' enforce ' > > = [ ]
2023-06-19 23:00:03 +00:00
for ( const plugin of plugins ) {
try {
const code = plugin . src in nuxt . vfs ? nuxt . vfs [ plugin . src ] : await fsp . readFile ( plugin . src ! , 'utf-8' )
_plugins . push ( {
2024-03-17 16:57:11 +00:00
. . . await extractMetadata ( code , IS_TSX . test ( plugin . src ) ? 'tsx' : 'ts' ) ,
2024-04-05 18:08:32 +00:00
. . . plugin ,
2023-06-19 23:00:03 +00:00
} )
} catch ( e ) {
2024-03-11 14:34:10 +00:00
const relativePluginSrc = relative ( nuxt . options . rootDir , plugin . src )
if ( ( e as Error ) . message === 'Invalid plugin metadata' ) {
logger . warn ( ` Failed to parse static properties from plugin \` ${ relativePluginSrc } \` , falling back to non-optimized runtime meta. Learn more: https://nuxt.com/docs/guide/directory-structure/plugins#object-syntax-plugins ` )
} else {
logger . warn ( ` Failed to parse static properties from plugin \` ${ relativePluginSrc } \` . ` , e )
}
2023-06-19 23:00:03 +00:00
_plugins . push ( plugin )
}
}
return _plugins . sort ( ( a , b ) = > ( a . order ? ? orderMap . default ) - ( b . order ? ? orderMap . default ) )
}
2023-12-14 17:11:08 +00:00
export function checkForCircularDependencies ( _plugins : Array < NuxtPlugin & Omit < PluginMeta , ' enforce ' > > ) {
const deps : Record < string , string [ ] > = Object . create ( null )
const pluginNames = _plugins . map ( plugin = > plugin . name )
for ( const plugin of _plugins ) {
// Make sure dependency plugins are registered
if ( plugin . dependsOn && plugin . dependsOn . some ( name = > ! pluginNames . includes ( name ) ) ) {
console . error ( ` Plugin \` ${ plugin . name } \` depends on \` ${ plugin . dependsOn . filter ( name = > ! pluginNames . includes ( name ) ) . join ( ', ' ) } \` but they are not registered. ` )
}
// Make graph to detect circular dependencies
if ( plugin . name ) {
deps [ plugin . name ] = plugin . dependsOn || [ ]
}
}
const checkDeps = ( name : string , visited : string [ ] = [ ] ) : string [ ] = > {
if ( visited . includes ( name ) ) {
console . error ( ` Circular dependency detected in plugins: ${ visited . join ( ' -> ' ) } -> ${ name } ` )
return [ ]
}
visited . push ( name )
2024-02-26 16:08:45 +00:00
return deps [ name ] ? . length ? deps [ name ] . flatMap ( dep = > checkDeps ( dep , [ . . . visited ] ) ) : [ ]
2023-12-14 17:11:08 +00:00
}
for ( const name in deps ) {
checkDeps ( name )
}
}