diff --git a/docs/content/2.guide/2.directory-structure/1.components.md b/docs/content/2.guide/2.directory-structure/1.components.md index 000aafd66f9..9e959c11ce7 100644 --- a/docs/content/2.guide/2.directory-structure/1.components.md +++ b/docs/content/2.guide/2.directory-structure/1.components.md @@ -223,6 +223,24 @@ This feature only works with Nuxt auto-imports and `#components` imports. Explic ``` +## `` Component + +Nuxt provides the `` component to render a component only during development. + +The content will not be included in production builds and tree-shaken. + +```html{}[pages/example.vue] + +``` + ## Library Authors Making Vue component libraries with automatic tree-shaking and component registration is super easy ✨ diff --git a/packages/nuxt/src/app/components/dev-only.mjs b/packages/nuxt/src/app/components/dev-only.mjs new file mode 100644 index 00000000000..ba7cd22deaf --- /dev/null +++ b/packages/nuxt/src/app/components/dev-only.mjs @@ -0,0 +1,11 @@ +import { defineComponent } from 'vue' + +export default defineComponent({ + name: 'DevOnly', + setup (_, props) { + if (process.dev) { + return () => props.slots.default?.() + } + return () => null + } +}) diff --git a/packages/nuxt/src/core/nuxt.ts b/packages/nuxt/src/core/nuxt.ts index a3caf948798..2f1d34c1a3d 100644 --- a/packages/nuxt/src/core/nuxt.ts +++ b/packages/nuxt/src/core/nuxt.ts @@ -17,6 +17,7 @@ import { version } from '../../package.json' import { ImportProtectionPlugin, vueAppPatterns } from './plugins/import-protection' import { UnctxTransformPlugin } from './plugins/unctx' import { TreeShakePlugin } from './plugins/tree-shake' +import { DevOnlyPlugin } from './plugins/dev-only' import { addModuleTranspiles } from './modules' import { initNitro } from './nitro' @@ -89,6 +90,10 @@ async function initNuxt (nuxt: Nuxt) { addVitePlugin(TreeShakePlugin.vite({ sourcemap: nuxt.options.sourcemap.client, treeShake: removeFromClient }), { server: false }) addWebpackPlugin(TreeShakePlugin.webpack({ sourcemap: nuxt.options.sourcemap.server, treeShake: removeFromServer }), { client: false }) addWebpackPlugin(TreeShakePlugin.webpack({ sourcemap: nuxt.options.sourcemap.client, treeShake: removeFromClient }), { server: false }) + + // DevOnly component tree-shaking - build time only + addVitePlugin(DevOnlyPlugin.vite({ sourcemap: nuxt.options.sourcemap.server || nuxt.options.sourcemap.client })) + addWebpackPlugin(DevOnlyPlugin.webpack({ sourcemap: nuxt.options.sourcemap.server || nuxt.options.sourcemap.client })) } // TODO: [Experimental] Avoid emitting assets when flag is enabled @@ -139,6 +144,12 @@ async function initNuxt (nuxt: Nuxt) { filePath: resolve(nuxt.options.appDir, 'components/client-only') }) + // Add + addComponent({ + name: 'DevOnly', + filePath: resolve(nuxt.options.appDir, 'components/dev-only') + }) + // Add addComponent({ name: 'ServerPlaceholder', diff --git a/packages/nuxt/src/core/plugins/dev-only.ts b/packages/nuxt/src/core/plugins/dev-only.ts new file mode 100644 index 00000000000..84d88e8f69c --- /dev/null +++ b/packages/nuxt/src/core/plugins/dev-only.ts @@ -0,0 +1,45 @@ +import { pathToFileURL } from 'node:url' +import { stripLiteral } from 'strip-literal' +import { parseQuery, parseURL } from 'ufo' +import MagicString from 'magic-string' +import { createUnplugin } from 'unplugin' + +interface DevOnlyPluginOptions { + sourcemap?: boolean +} + +export const DevOnlyPlugin = createUnplugin((options: DevOnlyPluginOptions) => { + const DEVONLY_COMP_RE = /(:?[\s\S]*)<\/dev-?only>/gmi + + return { + name: 'nuxt:server-devonly:transfrom', + enforce: 'pre', + transformInclude (id) { + const { pathname, search } = parseURL(decodeURIComponent(pathToFileURL(id).href)) + const { type } = parseQuery(search) + + // vue files + if (pathname.endsWith('.vue') && (type === 'template' || !search)) { + return true + } + }, + transform (code, id) { + if (!code.match(DEVONLY_COMP_RE)) { return } + + const s = new MagicString(code) + const strippedCode = stripLiteral(code) + for (const match of strippedCode.matchAll(DEVONLY_COMP_RE) || []) { + s.remove(match.index!, match.index! + match[0].length) + } + + if (s.hasChanged()) { + return { + code: s.toString(), + map: options.sourcemap + ? s.generateMap({ source: id, includeContent: true }) + : undefined + } + } + } + } +})