diff --git a/docs/1.getting-started/12.upgrade.md b/docs/1.getting-started/12.upgrade.md index 842f76c0ed..ac86d49f25 100644 --- a/docs/1.getting-started/12.upgrade.md +++ b/docs/1.getting-started/12.upgrade.md @@ -73,6 +73,7 @@ export default defineNuxtConfig({ // resetAsyncDataToUndefined: true, // templateUtils: true, // relativeWatchPaths: true, + // normalizeComponentNames: false // defaults: { // useAsyncData: { // 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 `` 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 `` 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 🚦 **Impact Level**: Medium diff --git a/docs/2.guide/3.going-further/1.experimental-features.md b/docs/2.guide/3.going-further/1.experimental-features.md index fcdf8cc00e..31bed1a8ae 100644 --- a/docs/2.guide/3.going-further/1.experimental-features.md +++ b/docs/2.guide/3.going-further/1.experimental-features.md @@ -390,3 +390,31 @@ In addition, any changes to files within `srcDir` will trigger a rebuild of the ::note 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 `` 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. diff --git a/docs/2.guide/3.going-further/1.features.md b/docs/2.guide/3.going-further/1.features.md index 02056f1e12..247df516e2 100644 --- a/docs/2.guide/3.going-further/1.features.md +++ b/docs/2.guide/3.going-further/1.features.md @@ -61,9 +61,12 @@ export default defineNuxtConfig({ app: 'app' }, experimental: { + sharedPrerenderData: false, compileTemplate: true, + resetAsyncDataToUndefined: true, templateUtils: true, relativeWatchPaths: true, + normalizeComponentNames: false defaults: { useAsyncData: { deep: true diff --git a/packages/nuxt/src/components/module.ts b/packages/nuxt/src/components/module.ts index dbfd5b9653..4a5540c91d 100644 --- a/packages/nuxt/src/components/module.ts +++ b/packages/nuxt/src/components/module.ts @@ -1,16 +1,18 @@ import { existsSync, statSync, writeFileSync } from 'node:fs' 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 { distDir } from '../dirs' -import { clientFallbackAutoIdPlugin } from './client-fallback-auto-id' import { componentNamesTemplate, componentsIslandsTemplate, componentsMetadataTemplate, componentsPluginTemplate, componentsTypeTemplate } from './templates' import { scanComponents } from './scan' -import { loaderPlugin } from './loader' -import { TreeShakeTemplatePlugin } from './tree-shake' -import { componentsChunkPlugin, islandsTransform } from './islandsTransform' -import { createTransformPlugin } from './transform' + +import { ClientFallbackAutoIdPlugin } from './plugins/client-fallback-auto-id' +import { LoaderPlugin } from './plugins/loader' +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 isDirectory = (p: string) => { try { return statSync(p).isDirectory() } catch { return false } } @@ -42,6 +44,11 @@ export default defineNuxtModule({ : 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[] => { if (Array.isArray(dir)) { return dir.map(dir => normalizeDirs(dir, cwd, options)).flat().sort(compareDirByPathLength) @@ -127,14 +134,14 @@ export default defineNuxtModule({ addTemplate(componentsMetadataTemplate) } - const unpluginServer = createTransformPlugin(nuxt, getComponents, 'server') - const unpluginClient = createTransformPlugin(nuxt, getComponents, 'client') + const TransformPluginServer = createTransformPlugin(nuxt, getComponents, 'server') + const TransformPluginClient = createTransformPlugin(nuxt, getComponents, 'client') - addVitePlugin(() => unpluginServer.vite(), { server: true, client: false }) - addVitePlugin(() => unpluginClient.vite(), { server: false, client: true }) + addVitePlugin(() => TransformPluginServer.vite(), { server: true, client: false }) + addVitePlugin(() => TransformPluginClient.vite(), { server: false, client: true }) - addWebpackPlugin(() => unpluginServer.webpack(), { server: true, client: false }) - addWebpackPlugin(() => unpluginClient.webpack(), { server: false, client: true }) + addWebpackPlugin(() => TransformPluginServer.webpack(), { server: true, client: false }) + addWebpackPlugin(() => TransformPluginClient.webpack(), { server: false, client: true }) // Do not prefetch global components chunks nuxt.hook('build:manifest', (manifest) => { @@ -223,12 +230,12 @@ export default defineNuxtModule({ })) } if (nuxt.options.experimental.clientFallback) { - config.plugins.push(clientFallbackAutoIdPlugin.vite({ + config.plugins.push(ClientFallbackAutoIdPlugin.vite({ sourcemap: !!nuxt.options.sourcemap[mode], rootDir: nuxt.options.rootDir, })) } - config.plugins.push(loaderPlugin.vite({ + config.plugins.push(LoaderPlugin.vite({ sourcemap: !!nuxt.options.sourcemap[mode], getComponents, mode, @@ -292,12 +299,12 @@ export default defineNuxtModule({ })) } if (nuxt.options.experimental.clientFallback) { - config.plugins.push(clientFallbackAutoIdPlugin.webpack({ + config.plugins.push(ClientFallbackAutoIdPlugin.webpack({ sourcemap: !!nuxt.options.sourcemap[mode], rootDir: nuxt.options.rootDir, })) } - config.plugins.push(loaderPlugin.webpack({ + config.plugins.push(LoaderPlugin.webpack({ sourcemap: !!nuxt.options.sourcemap[mode], getComponents, mode, diff --git a/packages/nuxt/src/components/client-fallback-auto-id.ts b/packages/nuxt/src/components/plugins/client-fallback-auto-id.ts similarity index 93% rename from packages/nuxt/src/components/client-fallback-auto-id.ts rename to packages/nuxt/src/components/plugins/client-fallback-auto-id.ts index 7aa42f1d0a..b12753e4e6 100644 --- a/packages/nuxt/src/components/client-fallback-auto-id.ts +++ b/packages/nuxt/src/components/plugins/client-fallback-auto-id.ts @@ -3,7 +3,7 @@ import type { ComponentsOptions } from '@nuxt/schema' import MagicString from 'magic-string' import { isAbsolute, relative } from 'pathe' import { hash } from 'ohash' -import { isVue } from '../core/utils' +import { isVue } from '../../core/utils' interface LoaderOptions { sourcemap?: boolean @@ -12,7 +12,7 @@ interface LoaderOptions { } const CLIENT_FALLBACK_RE = /<(?:NuxtClientFallback|nuxt-client-fallback)(?: [^>]*)?>/ 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 include = options.transform?.include || [] diff --git a/packages/nuxt/src/components/plugins/component-names.ts b/packages/nuxt/src/components/plugins/component-names.ts new file mode 100644 index 0000000000..4a24ebe96b --- /dev/null +++ b/packages/nuxt/src/components/plugins/component-names.ts @@ -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, + } + } + }, + } +}) diff --git a/packages/nuxt/src/components/islandsTransform.ts b/packages/nuxt/src/components/plugins/islands-transform.ts similarity index 99% rename from packages/nuxt/src/components/islandsTransform.ts rename to packages/nuxt/src/components/plugins/islands-transform.ts index e01b2ea3cc..ba5aa02d0b 100644 --- a/packages/nuxt/src/components/islandsTransform.ts +++ b/packages/nuxt/src/components/plugins/islands-transform.ts @@ -9,7 +9,7 @@ import { ELEMENT_NODE, parse, walk } from 'ultrahtml' import { hash } from 'ohash' import { resolvePath } from '@nuxt/kit' import defu from 'defu' -import { isVue } from '../core/utils' +import { isVue } from '../../core/utils' interface ServerOnlyComponentTransformPluginOptions { getComponents: () => Component[] diff --git a/packages/nuxt/src/components/loader.ts b/packages/nuxt/src/components/plugins/loader.ts similarity index 94% rename from packages/nuxt/src/components/loader.ts rename to packages/nuxt/src/components/plugins/loader.ts index b8e5d38b62..d2d4813767 100644 --- a/packages/nuxt/src/components/loader.ts +++ b/packages/nuxt/src/components/plugins/loader.ts @@ -6,8 +6,8 @@ import { resolve } from 'pathe' import type { Component, ComponentsOptions } from 'nuxt/schema' import { logger, tryUseNuxt } from '@nuxt/kit' -import { distDir } from '../dirs' -import { isVue } from '../core/utils' +import { distDir } from '../../dirs' +import { isVue } from '../../core/utils' interface LoaderOptions { getComponents (): Component[] @@ -17,7 +17,7 @@ interface LoaderOptions { experimentalComponentIslands?: boolean } -export const loaderPlugin = createUnplugin((options: LoaderOptions) => { +export const LoaderPlugin = createUnplugin((options: LoaderOptions) => { const exclude = options.transform?.exclude || [] const include = options.transform?.include || [] 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 if (component._internal_install && tryUseNuxt()?.options.test === false) { // @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++}` map.set(component, identifier) diff --git a/packages/nuxt/src/components/transform.ts b/packages/nuxt/src/components/plugins/transform.ts similarity index 98% rename from packages/nuxt/src/components/transform.ts rename to packages/nuxt/src/components/plugins/transform.ts index 16f1f38052..064fe9a56f 100644 --- a/packages/nuxt/src/components/transform.ts +++ b/packages/nuxt/src/components/plugins/transform.ts @@ -7,8 +7,8 @@ import { parseURL } from 'ufo' import { parseQuery } from 'vue-router' import { normalize, resolve } from 'pathe' import { genImport } from 'knitwork' -import { distDir } from '../dirs' -import type { getComponentsT } from './module' +import { distDir } from '../../dirs' +import type { getComponentsT } from '../module' const COMPONENT_QUERY_RE = /[?&]nuxt_component=/ diff --git a/packages/nuxt/src/components/tree-shake.ts b/packages/nuxt/src/components/plugins/tree-shake.ts similarity index 99% rename from packages/nuxt/src/components/tree-shake.ts rename to packages/nuxt/src/components/plugins/tree-shake.ts index 8108a9434d..2eaf341d87 100644 --- a/packages/nuxt/src/components/tree-shake.ts +++ b/packages/nuxt/src/components/plugins/tree-shake.ts @@ -6,7 +6,7 @@ import type { AssignmentProperty, CallExpression, Identifier, Literal, MemberExp import { createUnplugin } from 'unplugin' import type { Component } from '@nuxt/schema' import { resolve } from 'pathe' -import { distDir } from '../dirs' +import { distDir } from '../../dirs' interface TreeShakeTemplatePluginOptions { sourcemap?: boolean diff --git a/packages/nuxt/test/components-transform.test.ts b/packages/nuxt/test/components-transform.test.ts index d61f0bb55c..338d470003 100644 --- a/packages/nuxt/test/components-transform.test.ts +++ b/packages/nuxt/test/components-transform.test.ts @@ -4,7 +4,7 @@ import type { Component, Nuxt } from '@nuxt/schema' import { kebabCase } from 'scule' import { normalize } from 'pathe' -import { createTransformPlugin } from '../src/components/transform' +import { createTransformPlugin } from '../src/components/plugins/transform' describe('components:transform', () => { it('should transform #components imports', async () => { diff --git a/packages/nuxt/test/islandTransform.test.ts b/packages/nuxt/test/islandTransform.test.ts index dcf99e663c..5634dee92e 100644 --- a/packages/nuxt/test/islandTransform.test.ts +++ b/packages/nuxt/test/islandTransform.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it, vi } from 'vitest' import type { Plugin } from 'vite' import type { Component } from '@nuxt/schema' import type { UnpluginOptions } from 'unplugin' -import { islandsTransform } from '../src/components/islandsTransform' +import { islandsTransform } from '../src/components/plugins/islands-transform' import { normalizeLineEndings } from './utils' const getComponents = () => [{ diff --git a/packages/nuxt/test/treeshake-client.test.ts b/packages/nuxt/test/treeshake-client.test.ts index 375ceff182..2cc0ac27c9 100644 --- a/packages/nuxt/test/treeshake-client.test.ts +++ b/packages/nuxt/test/treeshake-client.test.ts @@ -6,7 +6,7 @@ import type { Plugin } from 'vite' import { Parser } from 'acorn' import type { Options } 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' // mock due to differences of results between windows and linux diff --git a/packages/schema/src/config/experimental.ts b/packages/schema/src/config/experimental.ts index 8f0249cc3d..711a20b776 100644 --- a/packages/schema/src/config/experimental.ts +++ b/packages/schema/src/config/experimental.ts @@ -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. */ 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).compatibilityVersion === 4) + }, + }, }, })