mirror of
https://github.com/nuxt/nuxt.git
synced 2025-02-16 21:58:19 +00:00
fix(nuxt): transform #components
imports into direct component imports (#20547)
Co-authored-by: Daniel Roe <daniel@roe.dev>
This commit is contained in:
parent
ecf41537ca
commit
98b20c45c8
@ -1,14 +1,15 @@
|
|||||||
import { statSync } from 'node:fs'
|
import { statSync } from 'node:fs'
|
||||||
import { relative, resolve } from 'pathe'
|
import { relative, resolve } from 'pathe'
|
||||||
import { addPluginTemplate, addTemplate, defineNuxtModule, resolveAlias, updateTemplates } from '@nuxt/kit'
|
import { addPluginTemplate, addTemplate, addVitePlugin, addWebpackPlugin, defineNuxtModule, resolveAlias, updateTemplates } from '@nuxt/kit'
|
||||||
import type { Component, ComponentsDir, ComponentsOptions } from 'nuxt/schema'
|
import type { Component, ComponentsDir, ComponentsOptions } from 'nuxt/schema'
|
||||||
|
|
||||||
import { distDir } from '../dirs'
|
import { distDir } from '../dirs'
|
||||||
import { clientFallbackAutoIdPlugin } from './client-fallback-auto-id'
|
import { clientFallbackAutoIdPlugin } from './client-fallback-auto-id'
|
||||||
import { componentsIslandsTemplate, componentsPluginTemplate, componentsTemplate, componentsTypeTemplate } from './templates'
|
import { componentNamesTemplate, componentsIslandsTemplate, componentsPluginTemplate, componentsTypeTemplate } from './templates'
|
||||||
import { scanComponents } from './scan'
|
import { scanComponents } from './scan'
|
||||||
import { loaderPlugin } from './loader'
|
import { loaderPlugin } from './loader'
|
||||||
import { TreeShakeTemplatePlugin } from './tree-shake'
|
import { TreeShakeTemplatePlugin } from './tree-shake'
|
||||||
|
import { createTransformPlugin } from './transform'
|
||||||
|
|
||||||
const isPureObjectOrString = (val: any) => (!Array.isArray(val) && typeof val === 'object') || typeof val === 'string'
|
const isPureObjectOrString = (val: any) => (!Array.isArray(val) && typeof val === 'object') || typeof val === 'string'
|
||||||
const isDirectory = (p: string) => { try { return statSync(p).isDirectory() } catch (_e) { return false } }
|
const isDirectory = (p: string) => { try { return statSync(p).isDirectory() } catch (_e) { return false } }
|
||||||
@ -18,7 +19,7 @@ function compareDirByPathLength ({ path: pathA }: { path: string }, { path: path
|
|||||||
|
|
||||||
const DEFAULT_COMPONENTS_DIRS_RE = /\/components(\/global|\/islands)?$/
|
const DEFAULT_COMPONENTS_DIRS_RE = /\/components(\/global|\/islands)?$/
|
||||||
|
|
||||||
type getComponentsT = (mode?: 'client' | 'server' | 'all') => Component[]
|
export type getComponentsT = (mode?: 'client' | 'server' | 'all') => Component[]
|
||||||
|
|
||||||
export default defineNuxtModule<ComponentsOptions>({
|
export default defineNuxtModule<ComponentsOptions>({
|
||||||
meta: {
|
meta: {
|
||||||
@ -115,10 +116,8 @@ export default defineNuxtModule<ComponentsOptions>({
|
|||||||
addTemplate({ ...componentsTypeTemplate, options: { getComponents } })
|
addTemplate({ ...componentsTypeTemplate, options: { getComponents } })
|
||||||
// components.plugin.mjs
|
// components.plugin.mjs
|
||||||
addPluginTemplate({ ...componentsPluginTemplate, options: { getComponents } } as any)
|
addPluginTemplate({ ...componentsPluginTemplate, options: { getComponents } } as any)
|
||||||
// components.server.mjs
|
// component-names.mjs
|
||||||
addTemplate({ ...componentsTemplate, filename: 'components.server.mjs', options: { getComponents, mode: 'server' } })
|
addTemplate({ ...componentNamesTemplate, options: { getComponents, mode: 'all' } })
|
||||||
// components.client.mjs
|
|
||||||
addTemplate({ ...componentsTemplate, filename: 'components.client.mjs', options: { getComponents, mode: 'client' } })
|
|
||||||
// components.islands.mjs
|
// components.islands.mjs
|
||||||
if (nuxt.options.experimental.componentIslands) {
|
if (nuxt.options.experimental.componentIslands) {
|
||||||
addTemplate({ ...componentsIslandsTemplate, filename: 'components.islands.mjs', options: { getComponents } })
|
addTemplate({ ...componentsIslandsTemplate, filename: 'components.islands.mjs', options: { getComponents } })
|
||||||
@ -126,16 +125,14 @@ export default defineNuxtModule<ComponentsOptions>({
|
|||||||
addTemplate({ filename: 'components.islands.mjs', getContents: () => 'export default {}' })
|
addTemplate({ filename: 'components.islands.mjs', getContents: () => 'export default {}' })
|
||||||
}
|
}
|
||||||
|
|
||||||
nuxt.hook('vite:extendConfig', (config, { isClient }) => {
|
const unpluginServer = createTransformPlugin(nuxt, getComponents, 'server')
|
||||||
const mode = isClient ? 'client' : 'server'
|
const unpluginClient = createTransformPlugin(nuxt, getComponents, 'client')
|
||||||
; (config.resolve!.alias as any)['#components'] = resolve(nuxt.options.buildDir, `components.${mode}.mjs`)
|
|
||||||
})
|
addVitePlugin(unpluginServer.vite(), { server: true, client: false })
|
||||||
nuxt.hook('webpack:config', (configs) => {
|
addVitePlugin(unpluginClient.vite(), { server: false, client: true })
|
||||||
for (const config of configs) {
|
|
||||||
const mode = config.name === 'server' ? 'server' : 'client'
|
addWebpackPlugin(unpluginServer.webpack(), { server: true, client: false })
|
||||||
; (config.resolve!.alias as any)['#components'] = resolve(nuxt.options.buildDir, `components.${mode}.mjs`)
|
addWebpackPlugin(unpluginClient.webpack(), { server: false, client: true })
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Do not prefetch global components chunks
|
// Do not prefetch global components chunks
|
||||||
nuxt.hook('build:manifest', (manifest) => {
|
nuxt.hook('build:manifest', (manifest) => {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { isAbsolute, relative } from 'pathe'
|
import { isAbsolute, relative } from 'pathe'
|
||||||
import { genDynamicImport, genExport, genImport, genObjectFromRawEntries } from 'knitwork'
|
import { genDynamicImport } from 'knitwork'
|
||||||
import type { Component, Nuxt, NuxtPluginTemplate, NuxtTemplate } from 'nuxt/schema'
|
import type { Component, Nuxt, NuxtPluginTemplate, NuxtTemplate } from 'nuxt/schema'
|
||||||
|
|
||||||
export interface ComponentsTemplateContext {
|
export interface ComponentsTemplateContext {
|
||||||
@ -25,59 +25,42 @@ const createImportMagicComments = (options: ImportMagicCommentsOptions) => {
|
|||||||
].filter(Boolean).join(', ')
|
].filter(Boolean).join(', ')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const emptyComponentsPlugin = `
|
||||||
|
import { defineNuxtPlugin } from '#app/nuxt'
|
||||||
|
export default defineNuxtPlugin({
|
||||||
|
name: 'nuxt:global-components',
|
||||||
|
})
|
||||||
|
`
|
||||||
|
|
||||||
export const componentsPluginTemplate: NuxtPluginTemplate<ComponentsTemplateContext> = {
|
export const componentsPluginTemplate: NuxtPluginTemplate<ComponentsTemplateContext> = {
|
||||||
filename: 'components.plugin.mjs',
|
filename: 'components.plugin.mjs',
|
||||||
getContents ({ options }) {
|
getContents ({ options }) {
|
||||||
|
const globalComponents = options.getComponents().filter(c => c.global)
|
||||||
|
if (!globalComponents.length) { return emptyComponentsPlugin }
|
||||||
|
|
||||||
return `import { defineNuxtPlugin } from '#app/nuxt'
|
return `import { defineNuxtPlugin } from '#app/nuxt'
|
||||||
import { lazyGlobalComponents } from '#components'
|
import { ${globalComponents.map(c => 'Lazy' + c.pascalName).join(', ')} } from '#components'
|
||||||
|
const lazyGlobalComponents = [
|
||||||
|
${globalComponents.map(c => `["${c.pascalName}", Lazy${c.pascalName}]`).join(',\n')}
|
||||||
|
]
|
||||||
|
|
||||||
export default defineNuxtPlugin({
|
export default defineNuxtPlugin({
|
||||||
name: 'nuxt:global-components',` +
|
name: 'nuxt:global-components',
|
||||||
(options.getComponents().filter(c => c.global).length
|
|
||||||
? `
|
|
||||||
setup (nuxtApp) {
|
setup (nuxtApp) {
|
||||||
for (const name in lazyGlobalComponents) {
|
for (const [name, component] of lazyGlobalComponents) {
|
||||||
nuxtApp.vueApp.component(name, lazyGlobalComponents[name])
|
nuxtApp.vueApp.component(name, component)
|
||||||
nuxtApp.vueApp.component('Lazy' + name, lazyGlobalComponents[name])
|
nuxtApp.vueApp.component('Lazy' + name, component)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}`
|
|
||||||
: '') + `
|
|
||||||
})
|
})
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const componentsTemplate: NuxtTemplate<ComponentsTemplateContext> = {
|
export const componentNamesTemplate: NuxtPluginTemplate<ComponentsTemplateContext> = {
|
||||||
// components.[server|client].mjs'
|
filename: 'component-names.mjs',
|
||||||
getContents ({ options }) {
|
getContents ({ options }) {
|
||||||
const imports = new Set<string>()
|
return `export const componentNames = ${JSON.stringify(options.getComponents().filter(c => !c.island).map(c => c.pascalName))}`
|
||||||
imports.add('import { defineAsyncComponent } from \'vue\'')
|
|
||||||
|
|
||||||
let num = 0
|
|
||||||
const components = options.getComponents(options.mode).filter(c => !c.island).flatMap((c) => {
|
|
||||||
const exp = c.export === 'default' ? 'c.default || c' : `c['${c.export}']`
|
|
||||||
const comment = createImportMagicComments(c)
|
|
||||||
|
|
||||||
const isClient = c.mode === 'client'
|
|
||||||
const definitions = []
|
|
||||||
if (isClient) {
|
|
||||||
num++
|
|
||||||
const identifier = `__nuxt_component_${num}`
|
|
||||||
imports.add(genImport('#app/components/client-only', [{ name: 'createClientOnly' }]))
|
|
||||||
imports.add(genImport(c.filePath, [{ name: c.export, as: identifier }]))
|
|
||||||
definitions.push(`export const ${c.pascalName} = /* #__PURE__ */ createClientOnly(${identifier})`)
|
|
||||||
} else {
|
|
||||||
definitions.push(genExport(c.filePath, [{ name: c.export, as: c.pascalName }]))
|
|
||||||
}
|
|
||||||
definitions.push(`export const Lazy${c.pascalName} = /* #__PURE__ */ defineAsyncComponent(${genDynamicImport(c.filePath, { comment })}.then(c => ${isClient ? `createClientOnly(${exp})` : exp}))`)
|
|
||||||
return definitions
|
|
||||||
})
|
|
||||||
return [
|
|
||||||
...imports,
|
|
||||||
...components,
|
|
||||||
`export const lazyGlobalComponents = ${genObjectFromRawEntries(options.getComponents().filter(c => c.global).map(c => [c.pascalName, `Lazy${c.pascalName}`]))}`,
|
|
||||||
`export const componentNames = ${JSON.stringify(options.getComponents().filter(c => !c.island).map(c => c.pascalName))}`
|
|
||||||
].join('\n')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
95
packages/nuxt/src/components/transform.ts
Normal file
95
packages/nuxt/src/components/transform.ts
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import { isIgnored } from '@nuxt/kit'
|
||||||
|
import type { Nuxt } from 'nuxt/schema'
|
||||||
|
import type { Import } from 'unimport'
|
||||||
|
import { createUnimport } from 'unimport'
|
||||||
|
import { createUnplugin } from 'unplugin'
|
||||||
|
import { parseURL } from 'ufo'
|
||||||
|
import { parseQuery } from 'vue-router'
|
||||||
|
import type { getComponentsT } from './module'
|
||||||
|
|
||||||
|
export function createTransformPlugin (nuxt: Nuxt, getComponents: getComponentsT, mode: 'client' | 'server' | 'all') {
|
||||||
|
const componentUnimport = createUnimport({
|
||||||
|
imports: [
|
||||||
|
{
|
||||||
|
name: 'componentNames',
|
||||||
|
from: '#build/component-names'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
virtualImports: ['#components']
|
||||||
|
})
|
||||||
|
|
||||||
|
function getComponentsImports (): Import[] {
|
||||||
|
const components = getComponents(mode)
|
||||||
|
return components.flatMap((c): Import[] => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
as: c.pascalName,
|
||||||
|
from: c.filePath + (c.mode === 'client' ? '?component=client' : ''),
|
||||||
|
name: 'default'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
as: 'Lazy' + c.pascalName,
|
||||||
|
from: c.filePath + '?component=' + [c.mode === 'client' ? 'client' : '', 'async'].filter(Boolean).join(','),
|
||||||
|
name: 'default'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return createUnplugin(() => ({
|
||||||
|
name: 'nuxt:components:imports',
|
||||||
|
transformInclude (id) {
|
||||||
|
return !isIgnored(id)
|
||||||
|
},
|
||||||
|
async transform (code, id) {
|
||||||
|
// Virtual component wrapper
|
||||||
|
if (id.includes('?component')) {
|
||||||
|
const { search } = parseURL(id)
|
||||||
|
const query = parseQuery(search)
|
||||||
|
const mode = query.component
|
||||||
|
const bare = id.replace(/\?.*/, '')
|
||||||
|
if (mode === 'async') {
|
||||||
|
return [
|
||||||
|
'import { defineAsyncComponent } from "vue"',
|
||||||
|
`export default defineAsyncComponent(() => import(${JSON.stringify(bare)}).then(r => r.default))`
|
||||||
|
].join('\n')
|
||||||
|
} else if (mode === 'client') {
|
||||||
|
return [
|
||||||
|
`import __component from ${JSON.stringify(bare)}`,
|
||||||
|
'import { createClientOnly } from "#app/components/client-only"',
|
||||||
|
'export default createClientOnly(__component)'
|
||||||
|
].join('\n')
|
||||||
|
} else if (mode === 'client,async') {
|
||||||
|
return [
|
||||||
|
'import { defineAsyncComponent } from "vue"',
|
||||||
|
'import { createClientOnly } from "#app/components/client-only"',
|
||||||
|
`export default defineAsyncComponent(() => import(${JSON.stringify(bare)}).then(r => createClientOnly(r.default)))`
|
||||||
|
].join('\n')
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unknown component mode: ${mode}, this might be an internal bug of Nuxt.`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!code.includes('#components')) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
componentUnimport.modifyDynamicImports((imports) => {
|
||||||
|
imports.length = 0
|
||||||
|
imports.push(...getComponentsImports())
|
||||||
|
return imports
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await componentUnimport.injectImports(code, id, { autoImport: false, transformVirtualImports: true })
|
||||||
|
if (!result) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
code: result.code,
|
||||||
|
map: nuxt.options.sourcemap
|
||||||
|
? result.s.generateMap({ hires: true })
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
@ -34,7 +34,7 @@ describe.skipIf(isWindows || process.env.TEST_BUILDER === 'webpack' || process.e
|
|||||||
|
|
||||||
it('default client bundle size', async () => {
|
it('default client bundle size', async () => {
|
||||||
stats.client = await analyzeSizes('**/*.js', publicDir)
|
stats.client = await analyzeSizes('**/*.js', publicDir)
|
||||||
expect(roundToKilobytes(stats.client.totalBytes)).toMatchInlineSnapshot('"94.4k"')
|
expect(roundToKilobytes(stats.client.totalBytes)).toMatchInlineSnapshot('"94.3k"')
|
||||||
expect(stats.client.files.map(f => f.replace(/\..*\.js/, '.js'))).toMatchInlineSnapshot(`
|
expect(stats.client.files.map(f => f.replace(/\..*\.js/, '.js'))).toMatchInlineSnapshot(`
|
||||||
[
|
[
|
||||||
"_nuxt/entry.js",
|
"_nuxt/entry.js",
|
||||||
@ -45,10 +45,10 @@ describe.skipIf(isWindows || process.env.TEST_BUILDER === 'webpack' || process.e
|
|||||||
|
|
||||||
it('default server bundle size', async () => {
|
it('default server bundle size', async () => {
|
||||||
stats.server = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir)
|
stats.server = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir)
|
||||||
expect(roundToKilobytes(stats.server.totalBytes)).toMatchInlineSnapshot('"67.3k"')
|
expect(roundToKilobytes(stats.server.totalBytes)).toMatchInlineSnapshot('"66.8k"')
|
||||||
|
|
||||||
const modules = await analyzeSizes('node_modules/**/*', serverDir)
|
const modules = await analyzeSizes('node_modules/**/*', serverDir)
|
||||||
expect(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot('"2657k"')
|
expect(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot('"2654k"')
|
||||||
|
|
||||||
const packages = modules.files
|
const packages = modules.files
|
||||||
.filter(m => m.endsWith('package.json'))
|
.filter(m => m.endsWith('package.json'))
|
||||||
@ -76,7 +76,6 @@ describe.skipIf(isWindows || process.env.TEST_BUILDER === 'webpack' || process.e
|
|||||||
"h3",
|
"h3",
|
||||||
"hookable",
|
"hookable",
|
||||||
"iron-webcrypto",
|
"iron-webcrypto",
|
||||||
"klona",
|
|
||||||
"node-fetch-native",
|
"node-fetch-native",
|
||||||
"ofetch",
|
"ofetch",
|
||||||
"ohash",
|
"ohash",
|
||||||
|
Loading…
Reference in New Issue
Block a user