Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(nuxt): use build plugin to detect usage of <NuxtPage> and <NuxtLayout> #26289

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
e89240c
fix(nuxt): avoid warning about unused page/layout components when mou…
IonianPlayboy Mar 15, 2024
dc46308
Revert "fix(nuxt): avoid warning about unused page/layout components …
IonianPlayboy Mar 18, 2024
e94e25e
feat(nuxt): add _hasLayouts property on NuxtApp
IonianPlayboy Mar 18, 2024
b8ea5de
refactor(nuxt): emit component usage warnings at compile time
IonianPlayboy Mar 18, 2024
cefa4c0
refactor(nuxt): remove plugins with runtime component usage checks
IonianPlayboy Mar 18, 2024
488f2bb
[autofix.ci] apply automated fixes
autofix-ci[bot] Mar 18, 2024
6f54ebb
refactor(nuxt): use addBuildPlugin to add DetectComponentUsagePlugin
IonianPlayboy Mar 18, 2024
2330bc9
chore: remove old code
danielroe Mar 18, 2024
10fbc4c
fix: use `app.layouts` as source of truth
danielroe Mar 18, 2024
8163fe4
refactor(nuxt): use check-component-usage plugin to log warnings at r…
IonianPlayboy Mar 18, 2024
06ff4df
refactor(nuxt): use absolute path with distDir for detect-component-u…
IonianPlayboy Mar 18, 2024
aa4eda9
refactor(nuxt): hardcode sep character
IonianPlayboy Mar 18, 2024
9e58137
test: fix runtime-compiler fixture
IonianPlayboy Mar 18, 2024
0f986b0
test: fix basic fixture
IonianPlayboy Mar 18, 2024
4a9609d
Revert "test: fix runtime-compiler fixture" and "test: fix basic fixt…
IonianPlayboy Mar 19, 2024
4132d38
fix(nuxt): include the generated runtime app.vue for component usage …
IonianPlayboy Mar 19, 2024
8145b40
refactor: simplify template
danielroe Mar 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 0 additions & 7 deletions 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<boolean> = Symbol.for('nuxt:client-only')
Expand All @@ -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 `<ClientOnly>` πŸ€·β€β™‚οΈ
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?.() }
Expand Down
4 changes: 0 additions & 4 deletions packages/nuxt/src/app/components/nuxt-layout.ts
Expand Up @@ -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
Expand Down
26 changes: 26 additions & 0 deletions 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<string>()

nuxtApp.hook('app:mounted', () => {
if (Object.keys(layouts).length > 0 && !isNuxtLayoutUsed && !cache.has('NuxtLayout')) {
console.warn('[nuxt] Your project has layouts but the `<NuxtLayout />` component has not been used.')
cache.add('NuxtLayout')
}

if (hasPages && !isNuxtPageUsed && !cache.has('NuxtPage')) {
console.warn('[nuxt] Your project has pages but the `<NuxtPage />` component has not been used.' +
' You might be using the `<RouterView />` 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')
}
})
}
})
28 changes: 0 additions & 28 deletions packages/nuxt/src/app/plugins/check-if-layout-used.ts

This file was deleted.

34 changes: 31 additions & 3 deletions 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'
Expand All @@ -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'
Expand Down Expand Up @@ -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<string>()

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) {
Expand Down
43 changes: 43 additions & 0 deletions 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<RegExp | string>
include?: Array<RegExp | string>
detectedComponents: Set<string>
}

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)
}
Comment on lines +27 to +29
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic doesn't quite look right... What about directories beginning with . - and if it's a relative path surely we could just do join(importer, id) rather than inserting an extra level up?

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 }
Comment on lines +30 to +32
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need the include/exclude pattern? (If we do need it for some reason, we would probably need to add quite a few more directories, including all layer root directories, and the paths to all auto-imported components.)


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
}
}
})
5 changes: 0 additions & 5 deletions packages/nuxt/src/pages/module.ts
Expand Up @@ -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)
Expand Down
4 changes: 0 additions & 4 deletions packages/nuxt/src/pages/runtime/page.ts
Expand Up @@ -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) => {
Expand Down
30 changes: 0 additions & 30 deletions packages/nuxt/src/pages/runtime/plugins/check-if-page-unused.ts

This file was deleted.