mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-22 13:45:18 +00:00
feat(nuxt): add support for components/global
(#6070)
Co-authored-by: Pooya Parsa <pooya@pi0.io>
This commit is contained in:
parent
12ebe3aeb6
commit
4e2667fcb7
@ -80,6 +80,12 @@ Alternatively, though not recommended, you can register all your components glob
|
|||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
::StabilityEdge{title="Automatic global components"}
|
||||||
|
In the current version, components in `~/components/global` are not yet auto-registered.
|
||||||
|
::
|
||||||
|
|
||||||
|
You can also selectively register some components globally by placing them in a `~/components/global` directory.
|
||||||
|
|
||||||
::alert{type=info}
|
::alert{type=info}
|
||||||
The `global` option can also be set per component directory.
|
The `global` option can also be set per component directory.
|
||||||
::
|
::
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { statSync } from 'node:fs'
|
import { statSync } from 'node:fs'
|
||||||
import { resolve, basename } from 'pathe'
|
import { resolve } from 'pathe'
|
||||||
import { defineNuxtModule, resolveAlias, addTemplate, addPluginTemplate } from '@nuxt/kit'
|
import { defineNuxtModule, resolveAlias, addTemplate, addPluginTemplate } from '@nuxt/kit'
|
||||||
import type { Component, ComponentsDir, ComponentsOptions } from '@nuxt/schema'
|
import type { Component, ComponentsDir, ComponentsOptions } from '@nuxt/schema'
|
||||||
import { componentsPluginTemplate, componentsTemplate, componentsTypeTemplate } from './templates'
|
import { componentsPluginTemplate, componentsTemplate, componentsTypeTemplate } from './templates'
|
||||||
@ -13,6 +13,10 @@ function compareDirByPathLength ({ path: pathA }, { path: pathB }) {
|
|||||||
return pathB.split(/[\\/]/).filter(Boolean).length - pathA.split(/[\\/]/).filter(Boolean).length
|
return pathB.split(/[\\/]/).filter(Boolean).length - pathA.split(/[\\/]/).filter(Boolean).length
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DEFAULT_COMPONENTS_DIRS_RE = /\/components$|\/components\/global$/
|
||||||
|
|
||||||
|
type getComponentsT = (mode?: 'client' | 'server' | 'all') => Component[]
|
||||||
|
|
||||||
export default defineNuxtModule<ComponentsOptions>({
|
export default defineNuxtModule<ComponentsOptions>({
|
||||||
meta: {
|
meta: {
|
||||||
name: 'components',
|
name: 'components',
|
||||||
@ -23,14 +27,25 @@ export default defineNuxtModule<ComponentsOptions>({
|
|||||||
},
|
},
|
||||||
setup (componentOptions, nuxt) {
|
setup (componentOptions, nuxt) {
|
||||||
let componentDirs = []
|
let componentDirs = []
|
||||||
const components: Component[] = []
|
const context = {
|
||||||
|
components: [] as Component[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const getComponents: getComponentsT = (mode) => {
|
||||||
|
return (mode && mode !== 'all')
|
||||||
|
? context.components.filter(c => c.mode === mode || c.mode === 'all')
|
||||||
|
: context.components
|
||||||
|
}
|
||||||
|
|
||||||
const normalizeDirs = (dir: any, cwd: string) => {
|
const normalizeDirs = (dir: any, cwd: string) => {
|
||||||
if (Array.isArray(dir)) {
|
if (Array.isArray(dir)) {
|
||||||
return dir.map(dir => normalizeDirs(dir, cwd)).flat().sort(compareDirByPathLength)
|
return dir.map(dir => normalizeDirs(dir, cwd)).flat().sort(compareDirByPathLength)
|
||||||
}
|
}
|
||||||
if (dir === true || dir === undefined) {
|
if (dir === true || dir === undefined) {
|
||||||
return [{ path: resolve(cwd, 'components') }]
|
return [
|
||||||
|
{ path: resolve(cwd, 'components/global'), global: true },
|
||||||
|
{ path: resolve(cwd, 'components') }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
if (typeof dir === 'string') {
|
if (typeof dir === 'string') {
|
||||||
return {
|
return {
|
||||||
@ -65,7 +80,7 @@ export default defineNuxtModule<ComponentsOptions>({
|
|||||||
dirOptions.level = Number(dirOptions.level || 0)
|
dirOptions.level = Number(dirOptions.level || 0)
|
||||||
|
|
||||||
const present = isDirectory(dirPath)
|
const present = isDirectory(dirPath)
|
||||||
if (!present && basename(dirOptions.path) !== 'components') {
|
if (!present && !DEFAULT_COMPONENTS_DIRS_RE.test(dirOptions.path)) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.warn('Components directory not found: `' + dirPath + '`')
|
console.warn('Components directory not found: `' + dirPath + '`')
|
||||||
}
|
}
|
||||||
@ -90,28 +105,31 @@ export default defineNuxtModule<ComponentsOptions>({
|
|||||||
nuxt.options.build!.transpile!.push(...componentDirs.filter(dir => dir.transpile).map(dir => dir.path))
|
nuxt.options.build!.transpile!.push(...componentDirs.filter(dir => dir.transpile).map(dir => dir.path))
|
||||||
})
|
})
|
||||||
|
|
||||||
const options = { components, buildDir: nuxt.options.buildDir }
|
// components.d.ts
|
||||||
|
addTemplate({ ...componentsTypeTemplate, options: { getComponents } })
|
||||||
|
// components.plugin.mjs
|
||||||
|
addPluginTemplate({ ...componentsPluginTemplate, options: { getComponents } })
|
||||||
|
// components.server.mjs
|
||||||
|
addTemplate({ ...componentsTemplate, filename: 'components.server.mjs', options: { getComponents, mode: 'server' } })
|
||||||
|
// components.client.mjs
|
||||||
|
addTemplate({ ...componentsTemplate, filename: 'components.client.mjs', options: { getComponents, mode: 'client' } })
|
||||||
|
|
||||||
addTemplate({
|
nuxt.hook('vite:extendConfig', (config, { isClient }) => {
|
||||||
...componentsTypeTemplate,
|
const mode = isClient ? 'client' : 'server'
|
||||||
options
|
config.resolve.alias['#components'] = resolve(nuxt.options.buildDir, `components.${mode}.mjs`)
|
||||||
})
|
})
|
||||||
|
nuxt.hook('webpack:config', (configs) => {
|
||||||
addPluginTemplate({
|
for (const config of configs) {
|
||||||
...componentsPluginTemplate,
|
const mode = config.name === 'server' ? 'server' : 'client'
|
||||||
options
|
config.resolve.alias['#components'] = resolve(nuxt.options.buildDir, `components.${mode}.mjs`)
|
||||||
})
|
}
|
||||||
|
|
||||||
nuxt.options.alias['#components'] = resolve(nuxt.options.buildDir, componentsTemplate.filename)
|
|
||||||
addTemplate({
|
|
||||||
...componentsTemplate,
|
|
||||||
options
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Scan components and add to plugin
|
// Scan components and add to plugin
|
||||||
nuxt.hook('app:templates', async () => {
|
nuxt.hook('app:templates', async () => {
|
||||||
options.components = await scanComponents(componentDirs, nuxt.options.srcDir!)
|
const newComponents = await scanComponents(componentDirs, nuxt.options.srcDir!)
|
||||||
await nuxt.callHook('components:extend', options.components)
|
await nuxt.callHook('components:extend', newComponents)
|
||||||
|
context.components = newComponents
|
||||||
})
|
})
|
||||||
|
|
||||||
nuxt.hook('prepare:types', ({ references }) => {
|
nuxt.hook('prepare:types', ({ references }) => {
|
||||||
@ -129,7 +147,6 @@ export default defineNuxtModule<ComponentsOptions>({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const getComponents = () => options.components
|
|
||||||
nuxt.hook('vite:extendConfig', (config, { isClient }) => {
|
nuxt.hook('vite:extendConfig', (config, { isClient }) => {
|
||||||
config.plugins = config.plugins || []
|
config.plugins = config.plugins || []
|
||||||
config.plugins.push(loaderPlugin.vite({
|
config.plugins.push(loaderPlugin.vite({
|
||||||
@ -138,7 +155,10 @@ export default defineNuxtModule<ComponentsOptions>({
|
|||||||
mode: isClient ? 'client' : 'server'
|
mode: isClient ? 'client' : 'server'
|
||||||
}))
|
}))
|
||||||
if (nuxt.options.experimental.treeshakeClientOnly) {
|
if (nuxt.options.experimental.treeshakeClientOnly) {
|
||||||
config.plugins.push(TreeShakeTemplatePlugin.vite({ sourcemap: nuxt.options.sourcemap, getComponents }))
|
config.plugins.push(TreeShakeTemplatePlugin.vite({
|
||||||
|
sourcemap: nuxt.options.sourcemap,
|
||||||
|
getComponents
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
nuxt.hook('webpack:config', (configs) => {
|
nuxt.hook('webpack:config', (configs) => {
|
||||||
@ -150,7 +170,10 @@ export default defineNuxtModule<ComponentsOptions>({
|
|||||||
mode: config.name === 'client' ? 'client' : 'server'
|
mode: config.name === 'client' ? 'client' : 'server'
|
||||||
}))
|
}))
|
||||||
if (nuxt.options.experimental.treeshakeClientOnly) {
|
if (nuxt.options.experimental.treeshakeClientOnly) {
|
||||||
config.plugins.push(TreeShakeTemplatePlugin.webpack({ sourcemap: nuxt.options.sourcemap, getComponents }))
|
config.plugins.push(TreeShakeTemplatePlugin.webpack({
|
||||||
|
sourcemap: nuxt.options.sourcemap,
|
||||||
|
getComponents
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -62,8 +62,9 @@ export async function scanComponents (dirs: ComponentsDir[], srcDir: string): Pr
|
|||||||
*/
|
*/
|
||||||
let fileName = basename(filePath, extname(filePath))
|
let fileName = basename(filePath, extname(filePath))
|
||||||
|
|
||||||
const mode = fileName.match(/(?<=\.)(client|server)$/)?.[0] as 'client' | 'server' || 'all'
|
const global = /\.(global)$/.test(fileName) || dir.global
|
||||||
fileName = fileName.replace(/\.(client|server)$/, '')
|
const mode = fileName.match(/(?<=\.)(client|server)(\.global)?$/)?.[1] as 'client' | 'server' || 'all'
|
||||||
|
fileName = fileName.replace(/(\.(client|server))?(\.global)?$/, '')
|
||||||
|
|
||||||
if (fileName.toLowerCase() === 'index') {
|
if (fileName.toLowerCase() === 'index') {
|
||||||
fileName = dir.pathPrefix === false ? basename(dirname(filePath)) : '' /* inherits from path */
|
fileName = dir.pathPrefix === false ? basename(dirname(filePath)) : '' /* inherits from path */
|
||||||
@ -103,16 +104,18 @@ export async function scanComponents (dirs: ComponentsDir[], srcDir: string): Pr
|
|||||||
const chunkName = 'components/' + kebabName + suffix
|
const chunkName = 'components/' + kebabName + suffix
|
||||||
|
|
||||||
let component: Component = {
|
let component: Component = {
|
||||||
|
// inheritable from directory configuration
|
||||||
|
mode,
|
||||||
|
global,
|
||||||
|
prefetch: Boolean(dir.prefetch),
|
||||||
|
preload: Boolean(dir.preload),
|
||||||
|
// specific to the file
|
||||||
filePath,
|
filePath,
|
||||||
pascalName,
|
pascalName,
|
||||||
kebabName,
|
kebabName,
|
||||||
chunkName,
|
chunkName,
|
||||||
shortPath,
|
shortPath,
|
||||||
export: 'default',
|
export: 'default'
|
||||||
global: dir.global,
|
|
||||||
prefetch: Boolean(dir.prefetch),
|
|
||||||
preload: Boolean(dir.preload),
|
|
||||||
mode
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof dir.extendComponent === 'function') {
|
if (typeof dir.extendComponent === 'function') {
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
import { isAbsolute, relative } from 'pathe'
|
import { isAbsolute, relative } from 'pathe'
|
||||||
import type { Component } from '@nuxt/schema'
|
import type { Component, Nuxt } from '@nuxt/schema'
|
||||||
import { genDynamicImport, genExport, genObjectFromRawEntries } from 'knitwork'
|
import { genDynamicImport, genExport, genObjectFromRawEntries } from 'knitwork'
|
||||||
import { upperFirst } from 'scule'
|
|
||||||
|
|
||||||
export type ComponentsTemplateOptions = {
|
export interface ComponentsTemplateContext {
|
||||||
buildDir?: string
|
nuxt: Nuxt
|
||||||
components: Component[]
|
options: {
|
||||||
|
getComponents: (mode?: 'client' | 'server' | 'all') => Component[]
|
||||||
|
mode?: 'client' | 'server'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ImportMagicCommentsOptions = {
|
export type ImportMagicCommentsOptions = {
|
||||||
@ -25,11 +27,13 @@ const createImportMagicComments = (options: ImportMagicCommentsOptions) => {
|
|||||||
|
|
||||||
export const componentsPluginTemplate = {
|
export const componentsPluginTemplate = {
|
||||||
filename: 'components.plugin.mjs',
|
filename: 'components.plugin.mjs',
|
||||||
getContents ({ options }: { options: ComponentsTemplateOptions }) {
|
getContents ({ options }: ComponentsTemplateContext) {
|
||||||
|
const globalComponents = options.getComponents().filter(c => c.global === true)
|
||||||
|
|
||||||
return `import { defineAsyncComponent } from 'vue'
|
return `import { defineAsyncComponent } from 'vue'
|
||||||
import { defineNuxtPlugin } from '#app'
|
import { defineNuxtPlugin } from '#app'
|
||||||
|
|
||||||
const components = ${genObjectFromRawEntries(options.components.filter(c => c.global === true).map((c) => {
|
const components = ${genObjectFromRawEntries(globalComponents.map((c) => {
|
||||||
const exp = c.export === 'default' ? 'c.default || c' : `c['${c.export}']`
|
const exp = c.export === 'default' ? 'c.default || c' : `c['${c.export}']`
|
||||||
const comment = createImportMagicComments(c)
|
const comment = createImportMagicComments(c)
|
||||||
|
|
||||||
@ -47,36 +51,45 @@ export default defineNuxtPlugin(nuxtApp => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const componentsTemplate = {
|
export const componentsTemplate = {
|
||||||
filename: 'components.mjs',
|
// components.[server|client].mjs'
|
||||||
getContents ({ options }: { options: ComponentsTemplateOptions }) {
|
getContents ({ options }: ComponentsTemplateContext) {
|
||||||
return [
|
return [
|
||||||
'import { defineAsyncComponent } from \'vue\'',
|
'import { defineAsyncComponent } from \'vue\'',
|
||||||
...options.components.flatMap((c) => {
|
...options.getComponents(options.mode).flatMap((c) => {
|
||||||
const exp = c.export === 'default' ? 'c.default || c' : `c['${c.export}']`
|
const exp = c.export === 'default' ? 'c.default || c' : `c['${c.export}']`
|
||||||
const comment = createImportMagicComments(c)
|
const comment = createImportMagicComments(c)
|
||||||
const nameWithSuffix = `${c.pascalName}${c.mode !== 'all' ? upperFirst(c.mode) : ''}`
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
genExport(c.filePath, [{ name: c.export, as: nameWithSuffix }]),
|
genExport(c.filePath, [{ name: c.export, as: c.pascalName }]),
|
||||||
`export const Lazy${nameWithSuffix} = defineAsyncComponent(${genDynamicImport(c.filePath, { comment })}.then(c => ${exp}))`
|
`export const Lazy${c.pascalName} = defineAsyncComponent(${genDynamicImport(c.filePath, { comment })}.then(c => ${exp}))`
|
||||||
]
|
]
|
||||||
}),
|
}),
|
||||||
`export const componentNames = ${JSON.stringify(options.components.map(c => c.pascalName))}`
|
`export const componentNames = ${JSON.stringify(options.getComponents().map(c => c.pascalName))}`
|
||||||
].join('\n')
|
].join('\n')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const componentsTypeTemplate = {
|
export const componentsTypeTemplate = {
|
||||||
filename: 'components.d.ts',
|
filename: 'components.d.ts',
|
||||||
getContents: ({ options }: { options: ComponentsTemplateOptions }) => `// Generated by components discovery
|
getContents: ({ options, nuxt }: ComponentsTemplateContext) => {
|
||||||
|
const buildDir = nuxt.options.buildDir
|
||||||
|
const componentTypes = options.getComponents().map(c => [
|
||||||
|
c.pascalName,
|
||||||
|
`typeof ${genDynamicImport(isAbsolute(c.filePath) ? relative(buildDir, c.filePath) : c.filePath, { wrapper: false })}['${c.export}']`
|
||||||
|
])
|
||||||
|
|
||||||
|
return `// Generated by components discovery
|
||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
${options.components.map(c => ` '${c.pascalName}': typeof ${genDynamicImport(isAbsolute(c.filePath) ? relative(options.buildDir, c.filePath) : c.filePath, { wrapper: false })}['${c.export}']`).join(',\n')}
|
${componentTypes.map(([pascalName, type]) => ` '${pascalName}': ${type}`).join(',\n')}
|
||||||
${options.components.map(c => ` 'Lazy${c.pascalName}': typeof ${genDynamicImport(isAbsolute(c.filePath) ? relative(options.buildDir, c.filePath) : c.filePath, { wrapper: false })}['${c.export}']`).join(',\n')}
|
${componentTypes.map(([pascalName, type]) => ` 'Lazy${pascalName}': ${type}`).join(',\n')}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
${options.components.map(c => `export const ${c.pascalName}${c.mode !== 'all' ? upperFirst(c.mode) : ''}: typeof ${genDynamicImport(isAbsolute(c.filePath) ? relative(options.buildDir, c.filePath) : c.filePath, { wrapper: false })}['${c.export}']`).join('\n')}
|
|
||||||
${options.components.map(c => `export const Lazy${c.pascalName}${c.mode !== 'all' ? upperFirst(c.mode) : ''}: typeof ${genDynamicImport(isAbsolute(c.filePath) ? relative(options.buildDir, c.filePath) : c.filePath, { wrapper: false })}['${c.export}']`).join('\n')}
|
${componentTypes.map(([pascalName, type]) => `export const ${pascalName}: ${type}`).join(',\n')}
|
||||||
|
${componentTypes.map(([pascalName, type]) => `export const Lazy${pascalName}: ${type}`).join(',\n')}
|
||||||
|
|
||||||
export const componentNames: string[]
|
export const componentNames: string[]
|
||||||
`
|
`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ export default {
|
|||||||
return { dirs: val }
|
return { dirs: val }
|
||||||
}
|
}
|
||||||
if (val === undefined || val === true) {
|
if (val === undefined || val === true) {
|
||||||
return { dirs: ['~/components'] }
|
return { dirs: [{ path: '~/components/global', global: true }, '~/components'] }
|
||||||
}
|
}
|
||||||
return val
|
return val
|
||||||
}
|
}
|
||||||
|
@ -44,6 +44,8 @@ describe('pages', () => {
|
|||||||
expect(html).toContain('Composable | template: auto imported from ~/components/template.ts')
|
expect(html).toContain('Composable | template: auto imported from ~/components/template.ts')
|
||||||
// should import components
|
// should import components
|
||||||
expect(html).toContain('This is a custom component with a named export.')
|
expect(html).toContain('This is a custom component with a named export.')
|
||||||
|
expect(html).toContain('global component registered automatically')
|
||||||
|
expect(html).toContain('global component via suffix')
|
||||||
|
|
||||||
await expectNoClientErrors('/')
|
await expectNoClientErrors('/')
|
||||||
})
|
})
|
||||||
|
3
test/fixtures/basic/components/WithSuffix.global.vue
vendored
Normal file
3
test/fixtures/basic/components/WithSuffix.global.vue
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<template>
|
||||||
|
global component via suffix
|
||||||
|
</template>
|
3
test/fixtures/basic/components/global/TestGlobal.vue
vendored
Normal file
3
test/fixtures/basic/components/global/TestGlobal.vue
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<template>
|
||||||
|
global component registered automatically
|
||||||
|
</template>
|
2
test/fixtures/basic/pages/index.vue
vendored
2
test/fixtures/basic/pages/index.vue
vendored
@ -14,6 +14,8 @@
|
|||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<SugarCounter :count="12" />
|
<SugarCounter :count="12" />
|
||||||
<CustomComponent />
|
<CustomComponent />
|
||||||
|
<component :is="`test${'-'.toString()}global`" />
|
||||||
|
<component :is="`with${'-'.toString()}suffix`" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user