mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-22 05:35:13 +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 { 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 { distDir } from '../dirs'
|
||||
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 { loaderPlugin } from './loader'
|
||||
import { TreeShakeTemplatePlugin } from './tree-shake'
|
||||
import { createTransformPlugin } from './transform'
|
||||
|
||||
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 } }
|
||||
@ -18,7 +19,7 @@ function compareDirByPathLength ({ path: pathA }: { path: string }, { path: path
|
||||
|
||||
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>({
|
||||
meta: {
|
||||
@ -115,10 +116,8 @@ export default defineNuxtModule<ComponentsOptions>({
|
||||
addTemplate({ ...componentsTypeTemplate, options: { getComponents } })
|
||||
// components.plugin.mjs
|
||||
addPluginTemplate({ ...componentsPluginTemplate, options: { getComponents } } as any)
|
||||
// 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' } })
|
||||
// component-names.mjs
|
||||
addTemplate({ ...componentNamesTemplate, options: { getComponents, mode: 'all' } })
|
||||
// components.islands.mjs
|
||||
if (nuxt.options.experimental.componentIslands) {
|
||||
addTemplate({ ...componentsIslandsTemplate, filename: 'components.islands.mjs', options: { getComponents } })
|
||||
@ -126,16 +125,14 @@ export default defineNuxtModule<ComponentsOptions>({
|
||||
addTemplate({ filename: 'components.islands.mjs', getContents: () => 'export default {}' })
|
||||
}
|
||||
|
||||
nuxt.hook('vite:extendConfig', (config, { isClient }) => {
|
||||
const mode = isClient ? 'client' : 'server'
|
||||
; (config.resolve!.alias as any)['#components'] = resolve(nuxt.options.buildDir, `components.${mode}.mjs`)
|
||||
})
|
||||
nuxt.hook('webpack:config', (configs) => {
|
||||
for (const config of configs) {
|
||||
const mode = config.name === 'server' ? 'server' : 'client'
|
||||
; (config.resolve!.alias as any)['#components'] = resolve(nuxt.options.buildDir, `components.${mode}.mjs`)
|
||||
}
|
||||
})
|
||||
const unpluginServer = createTransformPlugin(nuxt, getComponents, 'server')
|
||||
const unpluginClient = createTransformPlugin(nuxt, getComponents, 'client')
|
||||
|
||||
addVitePlugin(unpluginServer.vite(), { server: true, client: false })
|
||||
addVitePlugin(unpluginClient.vite(), { server: false, client: true })
|
||||
|
||||
addWebpackPlugin(unpluginServer.webpack(), { server: true, client: false })
|
||||
addWebpackPlugin(unpluginClient.webpack(), { server: false, client: true })
|
||||
|
||||
// Do not prefetch global components chunks
|
||||
nuxt.hook('build:manifest', (manifest) => {
|
||||
|
@ -1,5 +1,5 @@
|
||||
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'
|
||||
|
||||
export interface ComponentsTemplateContext {
|
||||
@ -25,59 +25,42 @@ const createImportMagicComments = (options: ImportMagicCommentsOptions) => {
|
||||
].filter(Boolean).join(', ')
|
||||
}
|
||||
|
||||
const emptyComponentsPlugin = `
|
||||
import { defineNuxtPlugin } from '#app/nuxt'
|
||||
export default defineNuxtPlugin({
|
||||
name: 'nuxt:global-components',
|
||||
})
|
||||
`
|
||||
|
||||
export const componentsPluginTemplate: NuxtPluginTemplate<ComponentsTemplateContext> = {
|
||||
filename: 'components.plugin.mjs',
|
||||
getContents ({ options }) {
|
||||
const globalComponents = options.getComponents().filter(c => c.global)
|
||||
if (!globalComponents.length) { return emptyComponentsPlugin }
|
||||
|
||||
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({
|
||||
name: 'nuxt:global-components',` +
|
||||
(options.getComponents().filter(c => c.global).length
|
||||
? `
|
||||
name: 'nuxt:global-components',
|
||||
setup (nuxtApp) {
|
||||
for (const name in lazyGlobalComponents) {
|
||||
nuxtApp.vueApp.component(name, lazyGlobalComponents[name])
|
||||
nuxtApp.vueApp.component('Lazy' + name, lazyGlobalComponents[name])
|
||||
for (const [name, component] of lazyGlobalComponents) {
|
||||
nuxtApp.vueApp.component(name, component)
|
||||
nuxtApp.vueApp.component('Lazy' + name, component)
|
||||
}
|
||||
}`
|
||||
: '') + `
|
||||
}
|
||||
})
|
||||
`
|
||||
}
|
||||
}
|
||||
|
||||
export const componentsTemplate: NuxtTemplate<ComponentsTemplateContext> = {
|
||||
// components.[server|client].mjs'
|
||||
export const componentNamesTemplate: NuxtPluginTemplate<ComponentsTemplateContext> = {
|
||||
filename: 'component-names.mjs',
|
||||
getContents ({ options }) {
|
||||
const imports = new Set<string>()
|
||||
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')
|
||||
return `export const componentNames = ${JSON.stringify(options.getComponents().filter(c => !c.island).map(c => c.pascalName))}`
|
||||
}
|
||||
}
|
||||
|
||||
|
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 () => {
|
||||
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(`
|
||||
[
|
||||
"_nuxt/entry.js",
|
||||
@ -45,10 +45,10 @@ describe.skipIf(isWindows || process.env.TEST_BUILDER === 'webpack' || process.e
|
||||
|
||||
it('default server bundle size', async () => {
|
||||
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)
|
||||
expect(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot('"2657k"')
|
||||
expect(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot('"2654k"')
|
||||
|
||||
const packages = modules.files
|
||||
.filter(m => m.endsWith('package.json'))
|
||||
@ -76,7 +76,6 @@ describe.skipIf(isWindows || process.env.TEST_BUILDER === 'webpack' || process.e
|
||||
"h3",
|
||||
"hookable",
|
||||
"iron-webcrypto",
|
||||
"klona",
|
||||
"node-fetch-native",
|
||||
"ofetch",
|
||||
"ohash",
|
||||
|
Loading…
Reference in New Issue
Block a user