2023-12-19 12:21:29 +00:00
import fs , { statSync } from 'node:fs'
import { join , normalize , relative , resolve } from 'pathe'
2024-03-16 22:20:29 +00:00
import { addPluginTemplate , addTemplate , addTypeTemplate , addVitePlugin , addWebpackPlugin , defineNuxtModule , logger , resolveAlias , updateTemplates } from '@nuxt/kit'
2023-03-11 21:16:01 +00:00
import type { Component , ComponentsDir , ComponentsOptions } from 'nuxt/schema'
2022-10-11 15:26:03 +00:00
import { distDir } from '../dirs'
2023-03-08 21:13:06 +00:00
import { clientFallbackAutoIdPlugin } from './client-fallback-auto-id'
2024-03-13 22:57:04 +00:00
import { componentNamesTemplate , componentsIslandsTemplate , componentsMetadataTemplate , componentsPluginTemplate , componentsTypeTemplate } from './templates'
2021-06-18 16:50:03 +00:00
import { scanComponents } from './scan'
2021-07-28 12:11:32 +00:00
import { loaderPlugin } from './loader'
2022-07-14 17:46:12 +00:00
import { TreeShakeTemplatePlugin } from './tree-shake'
2023-12-19 12:21:29 +00:00
import { componentsChunkPlugin , islandsTransform } from './islandsTransform'
2023-04-28 09:14:42 +00:00
import { createTransformPlugin } from './transform'
2021-06-18 16:50:03 +00:00
const isPureObjectOrString = ( val : any ) = > ( ! Array . isArray ( val ) && typeof val === 'object' ) || typeof val === 'string'
2021-08-09 21:54:44 +00:00
const isDirectory = ( p : string ) = > { try { return statSync ( p ) . isDirectory ( ) } catch ( _e ) { return false } }
2022-09-07 11:32:10 +00:00
function compareDirByPathLength ( { path : pathA } : { path : string } , { path : pathB } : { path : string } ) {
2022-02-07 20:48:25 +00:00
return pathB . split ( /[\\/]/ ) . filter ( Boolean ) . length - pathA . split ( /[\\/]/ ) . filter ( Boolean ) . length
}
2021-06-18 16:50:03 +00:00
2022-11-24 12:24:14 +00:00
const DEFAULT_COMPONENTS_DIRS_RE = /\/components(\/global|\/islands)?$/
2022-07-27 13:05:34 +00:00
2023-04-28 09:14:42 +00:00
export type getComponentsT = ( mode ? : 'client' | 'server' | 'all' ) = > Component [ ]
2022-07-27 13:05:34 +00:00
2021-11-19 12:22:27 +00:00
export default defineNuxtModule < ComponentsOptions > ( {
2022-01-05 18:09:53 +00:00
meta : {
name : 'components' ,
configKey : 'components'
} ,
2021-06-18 16:50:03 +00:00
defaults : {
2022-02-24 16:20:49 +00:00
dirs : [ ]
2021-06-18 16:50:03 +00:00
} ,
2022-02-07 20:48:25 +00:00
setup ( componentOptions , nuxt ) {
2022-08-22 10:12:02 +00:00
let componentDirs : ComponentsDir [ ] = [ ]
2022-07-27 13:05:34 +00:00
const context = {
components : [ ] as Component [ ]
}
const getComponents : getComponentsT = ( mode ) = > {
return ( mode && mode !== 'all' )
2023-09-13 22:35:53 +00:00
? context . components . filter ( c = > c . mode === mode || c . mode === 'all' || ( c . mode === 'server' && ! context . components . some ( otherComponent = > otherComponent . mode !== 'server' && otherComponent . pascalName === c . pascalName ) ) )
2022-07-27 13:05:34 +00:00
: context . components
}
2021-06-18 16:50:03 +00:00
2023-09-11 18:17:42 +00:00
const normalizeDirs = ( dir : any , cwd : string , options ? : { priority? : number } ) : ComponentsDir [ ] = > {
2022-02-07 20:48:25 +00:00
if ( Array . isArray ( dir ) ) {
2023-09-11 18:17:42 +00:00
return dir . map ( dir = > normalizeDirs ( dir , cwd , options ) ) . flat ( ) . sort ( compareDirByPathLength )
2022-02-07 20:48:25 +00:00
}
if ( dir === true || dir === undefined ) {
2022-07-27 13:05:34 +00:00
return [
2023-09-11 18:17:42 +00:00
{ priority : options?.priority || 0 , path : resolve ( cwd , 'components/islands' ) , island : true } ,
{ priority : options?.priority || 0 , path : resolve ( cwd , 'components/global' ) , global : true } ,
{ priority : options?.priority || 0 , path : resolve ( cwd , 'components' ) }
2022-07-27 13:05:34 +00:00
]
2022-02-07 20:48:25 +00:00
}
if ( typeof dir === 'string' ) {
2022-08-22 10:12:02 +00:00
return [
2023-09-11 18:17:42 +00:00
{ priority : options?.priority || 0 , path : resolve ( cwd , resolveAlias ( dir ) ) }
2022-08-22 10:12:02 +00:00
]
2022-02-07 20:48:25 +00:00
}
2022-03-16 20:36:30 +00:00
if ( ! dir ) {
return [ ]
2022-02-24 16:20:49 +00:00
}
2022-08-22 10:12:02 +00:00
const dirs : ComponentsDir [ ] = ( dir . dirs || [ dir ] ) . map ( ( dir : any ) : ComponentsDir = > typeof dir === 'string' ? { path : dir } : dir ) . filter ( ( _dir : ComponentsDir ) = > _dir . path )
2022-03-16 20:36:30 +00:00
return dirs . map ( _dir = > ( {
2023-09-11 18:17:42 +00:00
priority : options?.priority || 0 ,
2022-03-16 20:36:30 +00:00
. . . _dir ,
2022-03-22 10:35:16 +00:00
path : resolve ( cwd , resolveAlias ( _dir . path ) )
2022-03-16 20:36:30 +00:00
} ) )
2022-02-07 20:48:25 +00:00
}
2021-06-18 16:50:03 +00:00
// Resolve dirs
nuxt . hook ( 'app:resolve' , async ( ) = > {
2022-03-16 20:36:30 +00:00
// components/ dirs from all layers
const allDirs = nuxt . options . _layers
2023-09-11 18:17:42 +00:00
. map ( layer = > normalizeDirs ( layer . config . components , layer . config . srcDir , { priority : layer.config.srcDir === nuxt . options . srcDir ? 1 : 0 } ) )
2022-03-16 20:36:30 +00:00
. flat ( )
2022-02-07 20:48:25 +00:00
await nuxt . callHook ( 'components:dirs' , allDirs )
2021-06-18 16:50:03 +00:00
2022-02-07 20:48:25 +00:00
componentDirs = allDirs . filter ( isPureObjectOrString ) . map ( ( dir ) = > {
2021-06-18 16:50:03 +00:00
const dirOptions : ComponentsDir = typeof dir === 'object' ? dir : { path : dir }
2022-02-07 21:00:20 +00:00
const dirPath = resolveAlias ( dirOptions . path )
2021-06-18 16:50:03 +00:00
const transpile = typeof dirOptions . transpile === 'boolean' ? dirOptions . transpile : 'auto'
2021-11-02 15:27:42 +00:00
const extensions = ( dirOptions . extensions || nuxt . options . extensions ) . map ( e = > e . replace ( /^\./g , '' ) )
2021-06-18 16:50:03 +00:00
2021-06-21 11:50:28 +00:00
const present = isDirectory ( dirPath )
2022-07-27 13:05:34 +00:00
if ( ! present && ! DEFAULT_COMPONENTS_DIRS_RE . test ( dirOptions . path ) ) {
2023-09-19 21:26:15 +00:00
logger . warn ( 'Components directory not found: `' + dirPath + '`' )
2021-06-18 16:50:03 +00:00
}
return {
2022-02-18 09:37:11 +00:00
global : componentOptions . global ,
2021-06-18 16:50:03 +00:00
. . . dirOptions ,
2021-06-21 11:50:28 +00:00
// TODO: https://github.com/nuxt/framework/pull/251
enabled : true ,
2021-06-18 16:50:03 +00:00
path : dirPath ,
extensions ,
pattern : dirOptions.pattern || ` **/*.{ ${ extensions . join ( ',' ) } ,} ` ,
ignore : [
'**/*{M,.m,-m}ixin.{js,ts,jsx,tsx}' , // ignore mixins
2023-07-19 14:43:28 +00:00
'**/*.d.{cts,mts,ts}' , // .d.ts files
2021-06-18 16:50:03 +00:00
. . . ( dirOptions . ignore || [ ] )
] ,
transpile : ( transpile === 'auto' ? dirPath . includes ( 'node_modules' ) : transpile )
}
} ) . filter ( d = > d . enabled )
2022-08-09 09:13:54 +00:00
componentDirs = [
. . . componentDirs . filter ( dir = > ! dir . path . includes ( 'node_modules' ) ) ,
. . . componentDirs . filter ( dir = > dir . path . includes ( 'node_modules' ) )
]
2021-06-18 16:50:03 +00:00
nuxt . options . build ! . transpile ! . push ( . . . componentDirs . filter ( dir = > dir . transpile ) . map ( dir = > dir . path ) )
} )
2022-07-27 13:05:34 +00:00
// components.d.ts
2024-03-16 22:20:29 +00:00
addTypeTemplate ( componentsTypeTemplate )
2022-07-27 13:05:34 +00:00
// components.plugin.mjs
2023-09-04 15:44:23 +00:00
addPluginTemplate ( componentsPluginTemplate )
2023-04-28 09:14:42 +00:00
// component-names.mjs
2023-09-04 15:44:23 +00:00
addTemplate ( componentNamesTemplate )
2022-11-24 12:24:14 +00:00
// components.islands.mjs
if ( nuxt . options . experimental . componentIslands ) {
2023-05-01 16:35:00 +00:00
addTemplate ( { . . . componentsIslandsTemplate , filename : 'components.islands.mjs' } )
2022-11-24 12:24:14 +00:00
} else {
2023-11-28 22:06:32 +00:00
addTemplate ( { filename : 'components.islands.mjs' , getContents : ( ) = > 'export const islandComponents = {}' } )
2022-11-24 12:24:14 +00:00
}
2022-01-27 12:46:28 +00:00
2024-03-13 22:57:04 +00:00
if ( componentOptions . generateMetadata ) {
addTemplate ( componentsMetadataTemplate )
}
2023-04-28 09:14:42 +00:00
const unpluginServer = createTransformPlugin ( nuxt , getComponents , 'server' )
const unpluginClient = createTransformPlugin ( nuxt , getComponents , 'client' )
2023-07-26 04:30:44 +00:00
addVitePlugin ( ( ) = > unpluginServer . vite ( ) , { server : true , client : false } )
addVitePlugin ( ( ) = > unpluginClient . vite ( ) , { server : false , client : true } )
2023-04-28 09:14:42 +00:00
2023-07-26 04:30:44 +00:00
addWebpackPlugin ( ( ) = > unpluginServer . webpack ( ) , { server : true , client : false } )
addWebpackPlugin ( ( ) = > unpluginClient . webpack ( ) , { server : false , client : true } )
2021-06-18 16:50:03 +00:00
2022-08-30 14:41:11 +00:00
// Do not prefetch global components chunks
nuxt . hook ( 'build:manifest' , ( manifest ) = > {
const sourceFiles = getComponents ( ) . filter ( c = > c . global ) . map ( c = > relative ( nuxt . options . srcDir , c . filePath ) )
for ( const key in manifest ) {
if ( manifest [ key ] . isEntry ) {
manifest [ key ] . dynamicImports =
manifest [ key ] . dynamicImports ? . filter ( i = > ! sourceFiles . includes ( i ) )
}
}
} )
2023-03-09 11:46:08 +00:00
// Restart dev server when component directories are added/removed
2023-07-26 09:16:01 +00:00
nuxt . hook ( 'builder:watch' , ( event , relativePath ) = > {
if ( ! [ 'addDir' , 'unlinkDir' ] . includes ( event ) ) {
return
}
2023-03-09 11:46:08 +00:00
2023-07-26 09:16:01 +00:00
const path = resolve ( nuxt . options . srcDir , relativePath )
if ( componentDirs . some ( dir = > dir . path === path ) ) {
2023-09-19 21:26:15 +00:00
logger . info ( ` Directory \` ${ relativePath } / \` ${ event === 'addDir' ? 'created' : 'removed' } ` )
2023-03-09 11:46:08 +00:00
return nuxt . callHook ( 'restart' )
}
} )
2022-02-25 10:16:24 +00:00
// Scan components and add to plugin
2023-05-01 16:35:00 +00:00
nuxt . hook ( 'app:templates' , async ( app ) = > {
2022-07-27 13:05:34 +00:00
const newComponents = await scanComponents ( componentDirs , nuxt . options . srcDir ! )
await nuxt . callHook ( 'components:extend' , newComponents )
2022-10-11 15:26:03 +00:00
// add server placeholder for .client components server side. issue: #7085
for ( const component of newComponents ) {
if ( component . mode === 'client' && ! newComponents . some ( c = > c . pascalName === component . pascalName && c . mode === 'server' ) ) {
newComponents . push ( {
. . . component ,
2023-09-13 21:56:15 +00:00
_raw : true ,
2022-10-11 15:26:03 +00:00
mode : 'server' ,
filePath : resolve ( distDir , 'app/components/server-placeholder' ) ,
chunkName : 'components/' + component . kebabName
} )
}
2024-03-13 14:39:35 +00:00
if ( component . mode === 'server' && ! nuxt . options . ssr ) {
logger . warn ( ` Using server components with \` ssr: false \` is not supported with auto-detected component islands. If you need to use server component \` ${ component . pascalName } \` , set \` experimental.componentIslands \` to \` true \` . ` )
}
2022-10-11 15:26:03 +00:00
}
2022-07-27 13:05:34 +00:00
context . components = newComponents
2023-05-01 16:35:00 +00:00
app . components = newComponents
2021-06-18 16:50:03 +00:00
} )
2024-03-16 22:20:29 +00:00
nuxt . hook ( 'prepare:types' , ( { tsConfig } ) = > {
2023-06-19 22:29:09 +00:00
tsConfig . compilerOptions ! . paths [ '#components' ] = [ resolve ( nuxt . options . buildDir , 'components' ) ]
2021-08-09 21:54:44 +00:00
} )
2021-06-18 16:50:03 +00:00
// Watch for changes
2023-07-26 09:16:01 +00:00
nuxt . hook ( 'builder:watch' , async ( event , relativePath ) = > {
2021-06-18 16:50:03 +00:00
if ( ! [ 'add' , 'unlink' ] . includes ( event ) ) {
return
}
2023-07-26 09:16:01 +00:00
const path = resolve ( nuxt . options . srcDir , relativePath )
if ( componentDirs . some ( dir = > path . startsWith ( dir . path + '/' ) ) ) {
2022-10-24 08:53:02 +00:00
await updateTemplates ( {
filter : template = > [
'components.plugin.mjs' ,
'components.d.ts' ,
'components.server.mjs' ,
'components.client.mjs'
] . includes ( template . filename )
} )
2021-06-18 16:50:03 +00:00
}
} )
2021-07-28 12:11:32 +00:00
2022-09-07 11:32:10 +00:00
nuxt . hook ( 'vite:extendConfig' , ( config , { isClient , isServer } ) = > {
const mode = isClient ? 'client' : 'server'
2022-04-19 19:13:55 +00:00
config . plugins = config . plugins || [ ]
2022-09-07 11:32:10 +00:00
if ( nuxt . options . experimental . treeshakeClientOnly && isServer ) {
2022-07-27 13:05:34 +00:00
config . plugins . push ( TreeShakeTemplatePlugin . vite ( {
2023-08-24 12:06:44 +00:00
sourcemap : ! ! nuxt . options . sourcemap [ mode ] ,
2022-07-27 13:05:34 +00:00
getComponents
} ) )
2022-07-17 13:13:04 +00:00
}
2023-03-08 21:13:06 +00:00
config . plugins . push ( clientFallbackAutoIdPlugin . vite ( {
2023-08-24 12:06:44 +00:00
sourcemap : ! ! nuxt . options . sourcemap [ mode ] ,
2023-03-08 21:13:06 +00:00
rootDir : nuxt.options.rootDir
} ) )
2023-02-08 08:59:57 +00:00
config . plugins . push ( loaderPlugin . vite ( {
2023-08-24 12:06:44 +00:00
sourcemap : ! ! nuxt . options . sourcemap [ mode ] ,
2023-02-08 08:59:57 +00:00
getComponents ,
mode ,
2023-03-03 10:40:24 +00:00
transform : typeof nuxt . options . components === 'object' && ! Array . isArray ( nuxt . options . components ) ? nuxt.options.components.transform : undefined ,
2023-07-31 12:01:50 +00:00
experimentalComponentIslands : ! ! nuxt . options . experimental . componentIslands
2023-02-08 08:59:57 +00:00
} ) )
2023-05-15 22:43:53 +00:00
2023-12-19 12:21:29 +00:00
if ( nuxt . options . experimental . componentIslands ) {
const selectiveClient = typeof nuxt . options . experimental . componentIslands === 'object' && nuxt . options . experimental . componentIslands . selectiveClient
if ( isClient && selectiveClient ) {
fs . writeFileSync ( join ( nuxt . options . buildDir , 'components-chunk.mjs' ) , 'export const paths = {}' )
2024-03-09 06:48:15 +00:00
if ( ! nuxt . options . dev ) {
2023-12-19 12:21:29 +00:00
config . plugins . push ( componentsChunkPlugin . vite ( {
getComponents ,
buildDir : nuxt.options.buildDir
} ) )
} else {
2024-03-09 06:48:15 +00:00
fs . writeFileSync ( join ( nuxt . options . buildDir , 'components-chunk.mjs' ) , ` export const paths = ${ JSON . stringify (
2023-12-19 12:21:29 +00:00
getComponents ( ) . filter ( c = > c . mode === 'client' || c . mode === 'all' ) . reduce ( ( acc , c ) = > {
2024-03-09 06:48:15 +00:00
if ( c . filePath . endsWith ( '.vue' ) || c . filePath . endsWith ( '.js' ) || c . filePath . endsWith ( '.ts' ) ) { return Object . assign ( acc , { [ c . pascalName ] : ` /@fs/ ${ c . filePath } ` } ) }
const filePath = fs . existsSync ( ` ${ c . filePath } .vue ` ) ? ` ${ c . filePath } .vue ` : fs . existsSync ( ` ${ c . filePath } .js ` ) ? ` ${ c . filePath } .js ` : ` ${ c . filePath } .ts `
return Object . assign ( acc , { [ c . pascalName ] : ` /@fs/ ${ filePath } ` } )
2023-12-19 12:21:29 +00:00
} , { } as Record < string , string > )
) } ` )
2024-03-09 06:48:15 +00:00
}
2023-12-19 12:21:29 +00:00
}
if ( isServer ) {
config . plugins . push ( islandsTransform . vite ( {
getComponents ,
rootDir : nuxt.options.rootDir ,
isDev : nuxt.options.dev ,
selectiveClient
} ) )
2024-03-09 06:48:15 +00:00
}
2023-06-28 16:44:43 +00:00
}
2023-07-04 04:21:27 +00:00
if ( ! isServer && nuxt . options . experimental . componentIslands ) {
config . plugins . push ( {
name : 'nuxt-server-component-hmr' ,
handleHotUpdate ( ctx ) {
const components = getComponents ( )
const filePath = normalize ( ctx . file )
const comp = components . find ( c = > c . filePath === filePath )
if ( comp ? . mode === 'server' ) {
ctx . server . ws . send ( {
event : ` nuxt-server-component: ${ comp . pascalName } ` ,
type : 'custom'
} )
}
}
} )
}
2022-04-19 19:13:55 +00:00
} )
nuxt . hook ( 'webpack:config' , ( configs ) = > {
configs . forEach ( ( config ) = > {
2022-09-07 11:32:10 +00:00
const mode = config . name === 'client' ? 'client' : 'server'
2022-04-19 19:13:55 +00:00
config . plugins = config . plugins || [ ]
2022-09-07 11:32:10 +00:00
if ( nuxt . options . experimental . treeshakeClientOnly && mode === 'server' ) {
2022-07-27 13:05:34 +00:00
config . plugins . push ( TreeShakeTemplatePlugin . webpack ( {
2023-08-24 12:06:44 +00:00
sourcemap : ! ! nuxt . options . sourcemap [ mode ] ,
2022-07-27 13:05:34 +00:00
getComponents
} ) )
2022-07-17 13:13:04 +00:00
}
2023-03-08 21:13:06 +00:00
config . plugins . push ( clientFallbackAutoIdPlugin . webpack ( {
2023-08-24 12:06:44 +00:00
sourcemap : ! ! nuxt . options . sourcemap [ mode ] ,
2023-03-08 21:13:06 +00:00
rootDir : nuxt.options.rootDir
} ) )
2023-02-08 08:59:57 +00:00
config . plugins . push ( loaderPlugin . webpack ( {
2023-08-24 12:06:44 +00:00
sourcemap : ! ! nuxt . options . sourcemap [ mode ] ,
2023-02-08 08:59:57 +00:00
getComponents ,
mode ,
2023-03-03 10:40:24 +00:00
transform : typeof nuxt . options . components === 'object' && ! Array . isArray ( nuxt . options . components ) ? nuxt.options.components.transform : undefined ,
2023-07-31 12:01:50 +00:00
experimentalComponentIslands : ! ! nuxt . options . experimental . componentIslands
2023-02-08 08:59:57 +00:00
} ) )
2023-05-15 22:43:53 +00:00
2023-12-19 12:21:29 +00:00
if ( nuxt . options . experimental . componentIslands ) {
if ( mode === 'server' ) {
config . plugins . push ( islandsTransform . webpack ( {
getComponents
} ) )
} else {
fs . writeFileSync ( join ( nuxt . options . buildDir , 'components-chunk.mjs' ) , 'export const paths = {}' )
}
2023-06-28 16:44:43 +00:00
}
2022-04-19 19:13:55 +00:00
} )
} )
2021-06-18 16:50:03 +00:00
}
} )