diff --git a/packages/nuxt/src/app/components/client-only.ts b/packages/nuxt/src/app/components/client-only.ts index ec002d460d27..d3f19cd5a268 100644 --- a/packages/nuxt/src/app/components/client-only.ts +++ b/packages/nuxt/src/app/components/client-only.ts @@ -1,6 +1,5 @@ import { cloneVNode, createElementBlock, createStaticVNode, defineComponent, getCurrentInstance, h, onMounted, provide, ref } from 'vue' import type { ComponentInternalInstance, ComponentOptions, InjectionKey } from 'vue' -import { useNuxtApp } from '../nuxt' import { getFragmentHTML } from './utils' export const clientOnlySymbol: InjectionKey = Symbol.for('nuxt:client-only') @@ -13,12 +12,6 @@ export default defineComponent({ setup (_, { slots, attrs }) { const mounted = ref(false) onMounted(() => { mounted.value = true }) - // Bail out of checking for pages/layouts as they might be included under `` 🤷‍♂️ - if (import.meta.dev) { - const nuxtApp = useNuxtApp() - nuxtApp._isNuxtPageUsed = true - nuxtApp._isNuxtLayoutUsed = true - } provide(clientOnlySymbol, true) return (props: any) => { if (mounted.value) { return slots.default?.() } diff --git a/packages/nuxt/src/app/components/nuxt-layout.ts b/packages/nuxt/src/app/components/nuxt-layout.ts index 7509f2f9ff4f..64e7c840eeab 100644 --- a/packages/nuxt/src/app/components/nuxt-layout.ts +++ b/packages/nuxt/src/app/components/nuxt-layout.ts @@ -76,10 +76,6 @@ export default defineComponent({ useRouter().beforeEach(removeErrorHook) } - if (import.meta.dev) { - nuxtApp._isNuxtLayoutUsed = true - } - return () => { const hasLayout = layout.value && layout.value in layouts const transitionProps = route.meta.layoutTransition ?? defaultLayoutTransition diff --git a/packages/nuxt/src/app/plugins/check-component-usage.ts b/packages/nuxt/src/app/plugins/check-component-usage.ts new file mode 100644 index 000000000000..8c8da20123f3 --- /dev/null +++ b/packages/nuxt/src/app/plugins/check-component-usage.ts @@ -0,0 +1,26 @@ +import { defineNuxtPlugin } from '../nuxt' +// @ts-expect-error virtual file +import { hasPages, isNuxtLayoutUsed, isNuxtPageUsed } from '#build/detected-component-usage.mjs' +// @ts-expect-error virtual file +import layouts from '#build/layouts' + +export default defineNuxtPlugin({ + name: 'nuxt:check-component-usage', + setup (nuxtApp) { + const cache = new Set() + + nuxtApp.hook('app:mounted', () => { + if (Object.keys(layouts).length > 0 && !isNuxtLayoutUsed && !cache.has('NuxtLayout')) { + console.warn('[nuxt] Your project has layouts but the `` component has not been used.') + cache.add('NuxtLayout') + } + + if (hasPages && !isNuxtPageUsed && !cache.has('NuxtPage')) { + console.warn('[nuxt] Your project has pages but the `` component has not been used.' + + ' You might be using the `` component instead, which will not work correctly in Nuxt.' + + ' You can set `pages: false` in `nuxt.config` if you do not wish to use the Nuxt `vue-router` integration.') + cache.add('NuxtPage') + } + }) + } +}) diff --git a/packages/nuxt/src/app/plugins/check-if-layout-used.ts b/packages/nuxt/src/app/plugins/check-if-layout-used.ts deleted file mode 100644 index 9cf7192ea98c..000000000000 --- a/packages/nuxt/src/app/plugins/check-if-layout-used.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { nextTick } from 'vue' -import { defineNuxtPlugin } from '../nuxt' -import { onNuxtReady } from '../composables/ready' -import { useError } from '../composables/error' - -// @ts-expect-error virtual file -import layouts from '#build/layouts' - -export default defineNuxtPlugin({ - name: 'nuxt:checkIfLayoutUsed', - setup (nuxtApp) { - const error = useError() - - function checkIfLayoutUsed () { - if (!error.value && !nuxtApp._isNuxtLayoutUsed && Object.keys(layouts).length > 0) { - console.warn('[nuxt] Your project has layouts but the `` component has not been used.') - } - } - if (import.meta.server) { - nuxtApp.hook('app:rendered', ({ renderResult }) => { renderResult?.html && nextTick(checkIfLayoutUsed) }) - } else { - onNuxtReady(checkIfLayoutUsed) - } - }, - env: { - islands: false - } -}) diff --git a/packages/nuxt/src/core/nuxt.ts b/packages/nuxt/src/core/nuxt.ts index ab8f93c79475..effbd1cae4ad 100644 --- a/packages/nuxt/src/core/nuxt.ts +++ b/packages/nuxt/src/core/nuxt.ts @@ -1,7 +1,7 @@ import { dirname, join, normalize, relative, resolve } from 'pathe' import { createDebugger, createHooks } from 'hookable' import type { LoadNuxtOptions } from '@nuxt/kit' -import { addBuildPlugin, addComponent, addPlugin, addRouteMiddleware, addServerPlugin, addVitePlugin, addWebpackPlugin, installModule, loadNuxtConfig, logger, nuxtCtx, resolveAlias, resolveFiles, resolvePath, tryResolveModule, useNitro } from '@nuxt/kit' +import { addBuildPlugin, addComponent, addPlugin, addRouteMiddleware, addServerPlugin, addTemplate, addVitePlugin, addWebpackPlugin, installModule, loadNuxtConfig, logger, nuxtCtx, resolveAlias, resolveFiles, resolvePath, tryResolveModule, useNitro } from '@nuxt/kit' import { resolvePath as _resolvePath } from 'mlly' import type { Nuxt, NuxtHooks, NuxtOptions } from 'nuxt/schema' import type { PackageJson } from 'pkg-types' @@ -25,6 +25,7 @@ import { UnctxTransformPlugin } from './plugins/unctx' import type { TreeShakeComposablesPluginOptions } from './plugins/tree-shake' import { TreeShakeComposablesPlugin } from './plugins/tree-shake' import { DevOnlyPlugin } from './plugins/dev-only' +import { DetectComponentUsagePlugin } from './plugins/detect-component-usage' import { LayerAliasingPlugin } from './plugins/layer-aliasing' import { addModuleTranspiles } from './modules' import { initNitro } from './nitro' @@ -197,8 +198,35 @@ async function initNuxt (nuxt: Nuxt) { } if (nuxt.options.dev) { - // Add plugin to check if layouts are defined without NuxtLayout being instantiated - addPlugin(resolve(nuxt.options.appDir, 'plugins/check-if-layout-used')) + // Add component usage detection + const detectedComponents = new Set() + + addBuildPlugin(DetectComponentUsagePlugin({ + rootDir: nuxt.options.rootDir, + exclude: [ + // Exclude top-level resolutions by plugins + join(nuxt.options.rootDir, 'index.html'), + // Keep only imports coming from the user's project (inside the rootDir) + new RegExp(`^(?!${escapeRE(nuxt.options.rootDir)}/).+[^\n]+$`) + ], + include: [ + // Keep the imports coming from the auto-generated runtime app.vue + resolve(distDir, 'pages/runtime/app.vue') + ], + detectedComponents + })) + + addTemplate({ + filename: 'detected-component-usage.mjs', + getContents: ({ nuxt }) => + [ + `export const hasPages = ${nuxt.options.pages}`, + `export const isNuxtLayoutUsed = ${detectedComponents.has('NuxtLayout')}`, + `export const isNuxtPageUsed = ${detectedComponents.has('NuxtPage')}` + ].join('\n') + }) + + addPlugin(resolve(nuxt.options.appDir, 'plugins/check-component-usage')) } if (nuxt.options.dev && nuxt.options.features.devLogs) { diff --git a/packages/nuxt/src/core/plugins/detect-component-usage.ts b/packages/nuxt/src/core/plugins/detect-component-usage.ts new file mode 100644 index 000000000000..84711a6eb090 --- /dev/null +++ b/packages/nuxt/src/core/plugins/detect-component-usage.ts @@ -0,0 +1,43 @@ +import { createUnplugin } from 'unplugin' +import { join, resolve } from 'pathe' +import { updateTemplates } from '@nuxt/kit' +import { distDir } from '../../dirs' + +interface DetectComponentUsageOptions { + rootDir: string + exclude?: Array + include?: Array + detectedComponents: Set +} + +export const DetectComponentUsagePlugin = (options: DetectComponentUsageOptions) => createUnplugin(() => { + const importersToExclude = options?.exclude || [] + const importersToInclude = options?.include || [] + + const detectComponentUsagePatterns: Array<[importPattern: string | RegExp, name: string]> = [ + [resolve(distDir, 'pages/runtime/page'), 'NuxtPage'], + [resolve(distDir, 'app/components/nuxt-layout'), 'NuxtLayout'] + ] + + return { + name: 'nuxt:detect-component-usage', + enforce: 'pre', + resolveId (id, importer) { + if (!importer) { return } + if (id[0] === '.') { + id = join(importer, '..', id) + } + const isExcludedImporter = importersToExclude.some(p => typeof p === 'string' ? importer === p : p.test(importer)) + const isIncludedImporter = importersToInclude.some(p => typeof p === 'string' ? importer === p : p.test(importer)) + if (isExcludedImporter && !isIncludedImporter) { return } + + for (const [pattern, name] of detectComponentUsagePatterns) { + if (pattern instanceof RegExp ? pattern.test(id) : pattern === id) { + options.detectedComponents.add(name) + updateTemplates({ filter: template => template.filename === 'detected-component-usage.mjs' }) + } + } + return null + } + } +}) diff --git a/packages/nuxt/src/pages/module.ts b/packages/nuxt/src/pages/module.ts index 25d5c95ab9cf..885fe27f96fd 100644 --- a/packages/nuxt/src/pages/module.ts +++ b/packages/nuxt/src/pages/module.ts @@ -71,11 +71,6 @@ export default defineNuxtModule({ } nuxt.options.pages = await isPagesEnabled() - if (nuxt.options.dev && nuxt.options.pages) { - // Add plugin to check if pages are enabled without NuxtPage being instantiated - addPlugin(resolve(runtimeDir, 'plugins/check-if-page-unused')) - } - nuxt.hook('app:templates', async (app) => { app.pages = await resolvePagesRoutes() await nuxt.callHook('pages:extend', app.pages) diff --git a/packages/nuxt/src/pages/runtime/page.ts b/packages/nuxt/src/pages/runtime/page.ts index aafaeb8e893a..66c7ba69c4da 100644 --- a/packages/nuxt/src/pages/runtime/page.ts +++ b/packages/nuxt/src/pages/runtime/page.ts @@ -62,10 +62,6 @@ export default defineComponent({ }) } - if (import.meta.dev) { - nuxtApp._isNuxtPageUsed = true - } - return () => { return h(RouterView, { name: props.name, route: props.route, ...attrs }, { default: (routeProps: RouterViewSlotProps) => { diff --git a/packages/nuxt/src/pages/runtime/plugins/check-if-page-unused.ts b/packages/nuxt/src/pages/runtime/plugins/check-if-page-unused.ts deleted file mode 100644 index 57ae91fe6e63..000000000000 --- a/packages/nuxt/src/pages/runtime/plugins/check-if-page-unused.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { nextTick } from 'vue' -import { defineNuxtPlugin } from '#app/nuxt' -import { onNuxtReady } from '#app/composables/ready' -import { useError } from '#app/composables/error' - -export default defineNuxtPlugin({ - name: 'nuxt:checkIfPageUnused', - setup (nuxtApp) { - const error = useError() - - function checkIfPageUnused () { - if (!error.value && !nuxtApp._isNuxtPageUsed) { - console.warn( - '[nuxt] Your project has pages but the `` component has not been used.' + - ' You might be using the `` component instead, which will not work correctly in Nuxt.' + - ' You can set `pages: false` in `nuxt.config` if you do not wish to use the Nuxt `vue-router` integration.' - ) - } - } - - if (import.meta.server) { - nuxtApp.hook('app:rendered', ({ renderResult }) => { renderResult?.html && nextTick(checkIfPageUnused) }) - } else { - onNuxtReady(checkIfPageUnused) - } - }, - env: { - islands: false - } -})