feat(nuxt): normalise component names to match nuxt pattern (#28745)

This commit is contained in:
Julien Huang 2024-09-25 14:32:00 +02:00 committed by GitHub
parent ed7f946ab8
commit 9cb34dd49b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 161 additions and 29 deletions

View File

@ -73,6 +73,7 @@ export default defineNuxtConfig({
// resetAsyncDataToUndefined: true, // resetAsyncDataToUndefined: true,
// templateUtils: true, // templateUtils: true,
// relativeWatchPaths: true, // relativeWatchPaths: true,
// normalizeComponentNames: false
// defaults: { // defaults: {
// useAsyncData: { // useAsyncData: {
// deep: true // deep: true
@ -198,6 +199,43 @@ export default defineNuxtConfig({
}) })
``` ```
#### Normalized Component Names
🚦 **Impact Level**: Moderate
Vue will now generate component names that match the Nuxt pattern for component naming.
##### What Changed
By default, if you haven't set it manually, Vue will assign a component name that matches
the filename of the component.
```bash [Directory structure]
├─ components/
├─── SomeFolder/
├───── MyComponent.vue
```
In this case, the component name would be `MyComponent`, as far as Vue is concerned. If you wanted to use `<KeepAlive>` with it, or identify it in the Vue DevTools, you would need to use this name.
But in order to auto-import it, you would need to use `SomeFolderMyComponent`.
With this change, these two values will match, and Vue will generate a component name that matches the Nuxt pattern for component naming.
##### Migration Steps
Ensure that you use the updated name in any tests which use `findComponent` from `@vue/test-utils` and in any `<KeepAlive>` which depends on the name of your component.
Alternatively, for now, you can disable this behaviour with:
```ts twoslash [nuxt.config.ts]
export default defineNuxtConfig({
experimental: {
normalizeComponentNames: false
}
})
```
#### Shared Prerender Data #### Shared Prerender Data
🚦 **Impact Level**: Medium 🚦 **Impact Level**: Medium

View File

@ -390,3 +390,31 @@ In addition, any changes to files within `srcDir` will trigger a rebuild of the
::note ::note
A maximum of 10 cache tarballs are kept. A maximum of 10 cache tarballs are kept.
:: ::
## normalizeComponentNames
Ensure that auto-generated Vue component names match the full component name
you would use to auto-import the component.
```ts twoslash [nuxt.config.ts]
export default defineNuxtConfig({
experimental: {
normalizeComponentNames: true
}
})
```
By default, if you haven't set it manually, Vue will assign a component name that matches
the filename of the component.
```bash [Directory structure]
├─ components/
├─── SomeFolder/
├───── MyComponent.vue
```
In this case, the component name would be `MyComponent`, as far as Vue is concerned. If you wanted to use `<KeepAlive>` with it, or identify it in the Vue DevTools, you would need to use this component.
But in order to auto-import it, you would need to use `SomeFolderMyComponent`.
By setting `experimental.normalizeComponentNames`, these two values match, and Vue will generate a component name that matches the Nuxt pattern for component naming.

View File

@ -61,9 +61,12 @@ export default defineNuxtConfig({
app: 'app' app: 'app'
}, },
experimental: { experimental: {
sharedPrerenderData: false,
compileTemplate: true, compileTemplate: true,
resetAsyncDataToUndefined: true,
templateUtils: true, templateUtils: true,
relativeWatchPaths: true, relativeWatchPaths: true,
normalizeComponentNames: false
defaults: { defaults: {
useAsyncData: { useAsyncData: {
deep: true deep: true

View File

@ -1,16 +1,18 @@
import { existsSync, statSync, writeFileSync } from 'node:fs' import { existsSync, statSync, writeFileSync } from 'node:fs'
import { isAbsolute, join, normalize, relative, resolve } from 'pathe' import { isAbsolute, join, normalize, relative, resolve } from 'pathe'
import { addPluginTemplate, addTemplate, addTypeTemplate, addVitePlugin, addWebpackPlugin, defineNuxtModule, logger, resolveAlias, resolvePath, updateTemplates } from '@nuxt/kit' import { addBuildPlugin, addPluginTemplate, addTemplate, addTypeTemplate, addVitePlugin, addWebpackPlugin, defineNuxtModule, logger, resolveAlias, resolvePath, 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 { componentNamesTemplate, componentsIslandsTemplate, componentsMetadataTemplate, componentsPluginTemplate, componentsTypeTemplate } from './templates' import { componentNamesTemplate, componentsIslandsTemplate, componentsMetadataTemplate, componentsPluginTemplate, componentsTypeTemplate } from './templates'
import { scanComponents } from './scan' import { scanComponents } from './scan'
import { loaderPlugin } from './loader'
import { TreeShakeTemplatePlugin } from './tree-shake' import { ClientFallbackAutoIdPlugin } from './plugins/client-fallback-auto-id'
import { componentsChunkPlugin, islandsTransform } from './islandsTransform' import { LoaderPlugin } from './plugins/loader'
import { createTransformPlugin } from './transform' import { componentsChunkPlugin, islandsTransform } from './plugins/islands-transform'
import { createTransformPlugin } from './plugins/transform'
import { TreeShakeTemplatePlugin } from './plugins/tree-shake'
import { ComponentNamePlugin } from './plugins/component-names'
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 { return false } } const isDirectory = (p: string) => { try { return statSync(p).isDirectory() } catch { return false } }
@ -42,6 +44,11 @@ export default defineNuxtModule<ComponentsOptions>({
: context.components : context.components
} }
if (nuxt.options.experimental.normalizeComponentNames) {
addBuildPlugin(ComponentNamePlugin({ sourcemap: !!nuxt.options.sourcemap.client, getComponents }), { server: false })
addBuildPlugin(ComponentNamePlugin({ sourcemap: !!nuxt.options.sourcemap.server, getComponents }), { client: false })
}
const normalizeDirs = (dir: any, cwd: string, options?: { priority?: number }): ComponentsDir[] => { const normalizeDirs = (dir: any, cwd: string, options?: { priority?: number }): ComponentsDir[] => {
if (Array.isArray(dir)) { if (Array.isArray(dir)) {
return dir.map(dir => normalizeDirs(dir, cwd, options)).flat().sort(compareDirByPathLength) return dir.map(dir => normalizeDirs(dir, cwd, options)).flat().sort(compareDirByPathLength)
@ -127,14 +134,14 @@ export default defineNuxtModule<ComponentsOptions>({
addTemplate(componentsMetadataTemplate) addTemplate(componentsMetadataTemplate)
} }
const unpluginServer = createTransformPlugin(nuxt, getComponents, 'server') const TransformPluginServer = createTransformPlugin(nuxt, getComponents, 'server')
const unpluginClient = createTransformPlugin(nuxt, getComponents, 'client') const TransformPluginClient = createTransformPlugin(nuxt, getComponents, 'client')
addVitePlugin(() => unpluginServer.vite(), { server: true, client: false }) addVitePlugin(() => TransformPluginServer.vite(), { server: true, client: false })
addVitePlugin(() => unpluginClient.vite(), { server: false, client: true }) addVitePlugin(() => TransformPluginClient.vite(), { server: false, client: true })
addWebpackPlugin(() => unpluginServer.webpack(), { server: true, client: false }) addWebpackPlugin(() => TransformPluginServer.webpack(), { server: true, client: false })
addWebpackPlugin(() => unpluginClient.webpack(), { server: false, client: true }) addWebpackPlugin(() => TransformPluginClient.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) => {
@ -223,12 +230,12 @@ export default defineNuxtModule<ComponentsOptions>({
})) }))
} }
if (nuxt.options.experimental.clientFallback) { if (nuxt.options.experimental.clientFallback) {
config.plugins.push(clientFallbackAutoIdPlugin.vite({ config.plugins.push(ClientFallbackAutoIdPlugin.vite({
sourcemap: !!nuxt.options.sourcemap[mode], sourcemap: !!nuxt.options.sourcemap[mode],
rootDir: nuxt.options.rootDir, rootDir: nuxt.options.rootDir,
})) }))
} }
config.plugins.push(loaderPlugin.vite({ config.plugins.push(LoaderPlugin.vite({
sourcemap: !!nuxt.options.sourcemap[mode], sourcemap: !!nuxt.options.sourcemap[mode],
getComponents, getComponents,
mode, mode,
@ -292,12 +299,12 @@ export default defineNuxtModule<ComponentsOptions>({
})) }))
} }
if (nuxt.options.experimental.clientFallback) { if (nuxt.options.experimental.clientFallback) {
config.plugins.push(clientFallbackAutoIdPlugin.webpack({ config.plugins.push(ClientFallbackAutoIdPlugin.webpack({
sourcemap: !!nuxt.options.sourcemap[mode], sourcemap: !!nuxt.options.sourcemap[mode],
rootDir: nuxt.options.rootDir, rootDir: nuxt.options.rootDir,
})) }))
} }
config.plugins.push(loaderPlugin.webpack({ config.plugins.push(LoaderPlugin.webpack({
sourcemap: !!nuxt.options.sourcemap[mode], sourcemap: !!nuxt.options.sourcemap[mode],
getComponents, getComponents,
mode, mode,

View File

@ -3,7 +3,7 @@ import type { ComponentsOptions } from '@nuxt/schema'
import MagicString from 'magic-string' import MagicString from 'magic-string'
import { isAbsolute, relative } from 'pathe' import { isAbsolute, relative } from 'pathe'
import { hash } from 'ohash' import { hash } from 'ohash'
import { isVue } from '../core/utils' import { isVue } from '../../core/utils'
interface LoaderOptions { interface LoaderOptions {
sourcemap?: boolean sourcemap?: boolean
@ -12,7 +12,7 @@ interface LoaderOptions {
} }
const CLIENT_FALLBACK_RE = /<(?:NuxtClientFallback|nuxt-client-fallback)(?: [^>]*)?>/ const CLIENT_FALLBACK_RE = /<(?:NuxtClientFallback|nuxt-client-fallback)(?: [^>]*)?>/
const CLIENT_FALLBACK_GLOBAL_RE = /<(NuxtClientFallback|nuxt-client-fallback)( [^>]*)?>/g const CLIENT_FALLBACK_GLOBAL_RE = /<(NuxtClientFallback|nuxt-client-fallback)( [^>]*)?>/g
export const clientFallbackAutoIdPlugin = createUnplugin((options: LoaderOptions) => { export const ClientFallbackAutoIdPlugin = createUnplugin((options: LoaderOptions) => {
const exclude = options.transform?.exclude || [] const exclude = options.transform?.exclude || []
const include = options.transform?.include || [] const include = options.transform?.include || []

View File

@ -0,0 +1,46 @@
import { createUnplugin } from 'unplugin'
import MagicString from 'magic-string'
import type { Component } from 'nuxt/schema'
import { isVue } from '../../core/utils'
interface NameDevPluginOptions {
sourcemap: boolean
getComponents: () => Component[]
}
/**
* Set the default name of components to their PascalCase name
*/
export const ComponentNamePlugin = (options: NameDevPluginOptions) => createUnplugin(() => {
return {
name: 'nuxt:component-name-plugin',
enforce: 'post',
transformInclude (id) {
return isVue(id) || !!id.match(/\.[tj]sx$/)
},
transform (code, id) {
const filename = id.match(/([^/\\]+)\.\w+$/)?.[1]
if (!filename) {
return
}
const component = options.getComponents().find(c => c.filePath === id)
if (!component) {
return
}
const NAME_RE = new RegExp(`__name:\\s*['"]${filename}['"]`)
const s = new MagicString(code)
s.replace(NAME_RE, `__name: ${JSON.stringify(component.pascalName)}`)
if (s.hasChanged()) {
return {
code: s.toString(),
map: options.sourcemap
? s.generateMap({ hires: true })
: undefined,
}
}
},
}
})

View File

@ -9,7 +9,7 @@ import { ELEMENT_NODE, parse, walk } from 'ultrahtml'
import { hash } from 'ohash' import { hash } from 'ohash'
import { resolvePath } from '@nuxt/kit' import { resolvePath } from '@nuxt/kit'
import defu from 'defu' import defu from 'defu'
import { isVue } from '../core/utils' import { isVue } from '../../core/utils'
interface ServerOnlyComponentTransformPluginOptions { interface ServerOnlyComponentTransformPluginOptions {
getComponents: () => Component[] getComponents: () => Component[]

View File

@ -6,8 +6,8 @@ import { resolve } from 'pathe'
import type { Component, ComponentsOptions } from 'nuxt/schema' import type { Component, ComponentsOptions } from 'nuxt/schema'
import { logger, tryUseNuxt } from '@nuxt/kit' import { logger, tryUseNuxt } from '@nuxt/kit'
import { distDir } from '../dirs' import { distDir } from '../../dirs'
import { isVue } from '../core/utils' import { isVue } from '../../core/utils'
interface LoaderOptions { interface LoaderOptions {
getComponents (): Component[] getComponents (): Component[]
@ -17,7 +17,7 @@ interface LoaderOptions {
experimentalComponentIslands?: boolean experimentalComponentIslands?: boolean
} }
export const loaderPlugin = createUnplugin((options: LoaderOptions) => { export const LoaderPlugin = createUnplugin((options: LoaderOptions) => {
const exclude = options.transform?.exclude || [] const exclude = options.transform?.exclude || []
const include = options.transform?.include || [] const include = options.transform?.include || []
const serverComponentRuntime = resolve(distDir, 'components/runtime/server-component') const serverComponentRuntime = resolve(distDir, 'components/runtime/server-component')
@ -49,7 +49,7 @@ export const loaderPlugin = createUnplugin((options: LoaderOptions) => {
// @ts-expect-error TODO: refactor to nuxi // @ts-expect-error TODO: refactor to nuxi
if (component._internal_install && tryUseNuxt()?.options.test === false) { if (component._internal_install && tryUseNuxt()?.options.test === false) {
// @ts-expect-error TODO: refactor to nuxi // @ts-expect-error TODO: refactor to nuxi
import('../core/features').then(({ installNuxtModule }) => installNuxtModule(component._internal_install)) import('../../core/features').then(({ installNuxtModule }) => installNuxtModule(component._internal_install))
} }
let identifier = map.get(component) || `__nuxt_component_${num++}` let identifier = map.get(component) || `__nuxt_component_${num++}`
map.set(component, identifier) map.set(component, identifier)

View File

@ -7,8 +7,8 @@ import { parseURL } from 'ufo'
import { parseQuery } from 'vue-router' import { parseQuery } from 'vue-router'
import { normalize, resolve } from 'pathe' import { normalize, resolve } from 'pathe'
import { genImport } from 'knitwork' import { genImport } from 'knitwork'
import { distDir } from '../dirs' import { distDir } from '../../dirs'
import type { getComponentsT } from './module' import type { getComponentsT } from '../module'
const COMPONENT_QUERY_RE = /[?&]nuxt_component=/ const COMPONENT_QUERY_RE = /[?&]nuxt_component=/

View File

@ -6,7 +6,7 @@ import type { AssignmentProperty, CallExpression, Identifier, Literal, MemberExp
import { createUnplugin } from 'unplugin' import { createUnplugin } from 'unplugin'
import type { Component } from '@nuxt/schema' import type { Component } from '@nuxt/schema'
import { resolve } from 'pathe' import { resolve } from 'pathe'
import { distDir } from '../dirs' import { distDir } from '../../dirs'
interface TreeShakeTemplatePluginOptions { interface TreeShakeTemplatePluginOptions {
sourcemap?: boolean sourcemap?: boolean

View File

@ -4,7 +4,7 @@ import type { Component, Nuxt } from '@nuxt/schema'
import { kebabCase } from 'scule' import { kebabCase } from 'scule'
import { normalize } from 'pathe' import { normalize } from 'pathe'
import { createTransformPlugin } from '../src/components/transform' import { createTransformPlugin } from '../src/components/plugins/transform'
describe('components:transform', () => { describe('components:transform', () => {
it('should transform #components imports', async () => { it('should transform #components imports', async () => {

View File

@ -2,7 +2,7 @@ import { describe, expect, it, vi } from 'vitest'
import type { Plugin } from 'vite' import type { Plugin } from 'vite'
import type { Component } from '@nuxt/schema' import type { Component } from '@nuxt/schema'
import type { UnpluginOptions } from 'unplugin' import type { UnpluginOptions } from 'unplugin'
import { islandsTransform } from '../src/components/islandsTransform' import { islandsTransform } from '../src/components/plugins/islands-transform'
import { normalizeLineEndings } from './utils' import { normalizeLineEndings } from './utils'
const getComponents = () => [{ const getComponents = () => [{

View File

@ -6,7 +6,7 @@ import type { Plugin } from 'vite'
import { Parser } from 'acorn' import { Parser } from 'acorn'
import type { Options } from '@vitejs/plugin-vue' import type { Options } from '@vitejs/plugin-vue'
import _vuePlugin from '@vitejs/plugin-vue' import _vuePlugin from '@vitejs/plugin-vue'
import { TreeShakeTemplatePlugin } from '../src/components/tree-shake' import { TreeShakeTemplatePlugin } from '../src/components/plugins/tree-shake'
import { fixtureDir, normalizeLineEndings } from './utils' import { fixtureDir, normalizeLineEndings } from './utils'
// mock due to differences of results between windows and linux // mock due to differences of results between windows and linux

View File

@ -389,5 +389,15 @@ export default defineUntypedSchema({
* This only works for source files within `srcDir` and `serverDir` for the Vue/Nitro parts of your app. * This only works for source files within `srcDir` and `serverDir` for the Vue/Nitro parts of your app.
*/ */
buildCache: false, buildCache: false,
/**
* Ensure that auto-generated Vue component names match the full component name
* you would use to auto-import the component.
*/
normalizeComponentNames: {
$resolve: async (val, get) => {
return val ?? ((await get('future') as Record<string, unknown>).compatibilityVersion === 4)
},
},
}, },
}) })