mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-29 00:52:01 +00:00
feat(nuxt): wrap #components
client exports with createClientOnly (#7412)
Co-authored-by: jhuang@hsk-partners.com <jhuang@hsk-partners.com>
This commit is contained in:
parent
2aa097310c
commit
ee41bb6d5d
@ -201,7 +201,7 @@ If a component is meant to be rendered only client-side, you can add the `.clien
|
|||||||
```
|
```
|
||||||
|
|
||||||
::alert{type=warning}
|
::alert{type=warning}
|
||||||
This feature only works with Nuxt auto-imports. Explicitly importing these components does not convert them into client-only components.
|
This feature only works with Nuxt auto-imports and `#components` imports. Explicitly importing these components from their real paths does not convert them into client-only components.
|
||||||
::
|
::
|
||||||
|
|
||||||
## .server Components
|
## .server Components
|
||||||
|
@ -2,6 +2,7 @@ import { statSync } from 'node:fs'
|
|||||||
import { relative, resolve } from 'pathe'
|
import { relative, resolve } from 'pathe'
|
||||||
import { defineNuxtModule, resolveAlias, addTemplate, addPluginTemplate } from '@nuxt/kit'
|
import { defineNuxtModule, resolveAlias, addTemplate, addPluginTemplate } from '@nuxt/kit'
|
||||||
import type { Component, ComponentsDir, ComponentsOptions } from '@nuxt/schema'
|
import type { Component, ComponentsDir, ComponentsOptions } from '@nuxt/schema'
|
||||||
|
import { distDir } from '../dirs'
|
||||||
import { componentsPluginTemplate, componentsTemplate, componentsTypeTemplate } from './templates'
|
import { componentsPluginTemplate, componentsTemplate, componentsTypeTemplate } from './templates'
|
||||||
import { scanComponents } from './scan'
|
import { scanComponents } from './scan'
|
||||||
import { loaderPlugin } from './loader'
|
import { loaderPlugin } from './loader'
|
||||||
@ -146,6 +147,17 @@ export default defineNuxtModule<ComponentsOptions>({
|
|||||||
nuxt.hook('app:templates', async () => {
|
nuxt.hook('app:templates', async () => {
|
||||||
const newComponents = await scanComponents(componentDirs, nuxt.options.srcDir!)
|
const newComponents = await scanComponents(componentDirs, nuxt.options.srcDir!)
|
||||||
await nuxt.callHook('components:extend', newComponents)
|
await nuxt.callHook('components:extend', newComponents)
|
||||||
|
// add server placeholder for .client components server side. issue: #7085
|
||||||
|
for (const component of newComponents) {
|
||||||
|
if (component.mode === 'client' && !newComponents.some(c => c.pascalName === component.pascalName && c.mode === 'server')) {
|
||||||
|
newComponents.push({
|
||||||
|
...component,
|
||||||
|
mode: 'server',
|
||||||
|
filePath: resolve(distDir, 'app/components/server-placeholder'),
|
||||||
|
chunkName: 'components/' + component.kebabName
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
context.components = newComponents
|
context.components = newComponents
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { isAbsolute, relative } from 'pathe'
|
import { isAbsolute, relative } from 'pathe'
|
||||||
import type { Component, Nuxt, NuxtPluginTemplate, NuxtTemplate } from '@nuxt/schema'
|
import type { Component, Nuxt, NuxtPluginTemplate, NuxtTemplate } from '@nuxt/schema'
|
||||||
import { genDynamicImport, genExport, genObjectFromRawEntries } from 'knitwork'
|
import { genDynamicImport, genExport, genImport, genObjectFromRawEntries } from 'knitwork'
|
||||||
|
|
||||||
export interface ComponentsTemplateContext {
|
export interface ComponentsTemplateContext {
|
||||||
nuxt: Nuxt
|
nuxt: Nuxt
|
||||||
@ -53,17 +53,31 @@ export default defineNuxtPlugin(nuxtApp => {
|
|||||||
export const componentsTemplate: NuxtTemplate<ComponentsTemplateContext> = {
|
export const componentsTemplate: NuxtTemplate<ComponentsTemplateContext> = {
|
||||||
// components.[server|client].mjs'
|
// components.[server|client].mjs'
|
||||||
getContents ({ options }) {
|
getContents ({ options }) {
|
||||||
return [
|
const imports = new Set<string>()
|
||||||
'import { defineAsyncComponent } from \'vue\'',
|
imports.add('import { defineAsyncComponent } from \'vue\'')
|
||||||
...options.getComponents(options.mode).flatMap((c) => {
|
|
||||||
const exp = c.export === 'default' ? 'c.default || c' : `c['${c.export}']`
|
|
||||||
const comment = createImportMagicComments(c)
|
|
||||||
|
|
||||||
return [
|
let num = 0
|
||||||
genExport(c.filePath, [{ name: c.export, as: c.pascalName }]),
|
const components = options.getComponents(options.mode).flatMap((c) => {
|
||||||
`export const Lazy${c.pascalName} = defineAsyncComponent(${genDynamicImport(c.filePath, { comment })}.then(c => ${exp}))`
|
const exp = c.export === 'default' ? 'c.default || c' : `c['${c.export}']`
|
||||||
]
|
const comment = createImportMagicComments(c)
|
||||||
}),
|
|
||||||
|
const isClient = c.mode === 'client'
|
||||||
|
const definitions = []
|
||||||
|
if (isClient) {
|
||||||
|
num++
|
||||||
|
const identifier = `__nuxt_component_${num}`
|
||||||
|
imports.add(genImport('#app/components/client-only', [{ name: 'createClientOnly' }]))
|
||||||
|
imports.add(genImport(c.filePath, [{ name: c.export, as: identifier }]))
|
||||||
|
definitions.push(`export const ${c.pascalName} = /*#__PURE__*/ createClientOnly(${identifier})`)
|
||||||
|
} else {
|
||||||
|
definitions.push(genExport(c.filePath, [{ name: c.export, as: c.pascalName }]))
|
||||||
|
}
|
||||||
|
definitions.push(`export const Lazy${c.pascalName} = defineAsyncComponent(${genDynamicImport(c.filePath, { comment })}.then(c => ${isClient ? `createClientOnly(${exp})` : exp}))`)
|
||||||
|
return definitions
|
||||||
|
})
|
||||||
|
return [
|
||||||
|
...imports,
|
||||||
|
...components,
|
||||||
`export const componentNames = ${JSON.stringify(options.getComponents().map(c => c.pascalName))}`
|
`export const componentNames = ${JSON.stringify(options.getComponents().map(c => c.pascalName))}`
|
||||||
].join('\n')
|
].join('\n')
|
||||||
}
|
}
|
||||||
|
@ -219,6 +219,17 @@ describe('pages', () => {
|
|||||||
await Promise.all(hiddenSelectors.map(selector => page.locator(selector).isVisible()))
|
await Promise.all(hiddenSelectors.map(selector => page.locator(selector).isVisible()))
|
||||||
.then(results => results.forEach(isVisible => expect(isVisible).toBeTruthy()))
|
.then(results => results.forEach(isVisible => expect(isVisible).toBeTruthy()))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('/client-only-explicit-import', async () => {
|
||||||
|
const html = await $fetch('/client-only-explicit-import')
|
||||||
|
|
||||||
|
// ensure fallbacks with classes and arbitrary attributes are rendered
|
||||||
|
expect(html).toContain('<div class="client-only-script" foo="bar">')
|
||||||
|
expect(html).toContain('<div class="lazy-client-only-script-setup" foo="hello">')
|
||||||
|
// ensure components are not rendered server-side
|
||||||
|
expect(html).not.toContain('client only script')
|
||||||
|
await expectNoClientErrors('/client-only-components')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('head tags', () => {
|
describe('head tags', () => {
|
||||||
|
11
test/fixtures/basic/pages/client-only-explicit-import.vue
vendored
Normal file
11
test/fixtures/basic/pages/client-only-explicit-import.vue
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<ClientOnlyScript class="client-only-script" foo="bar" />
|
||||||
|
<LazyClientOnlySetupScript class="lazy-client-only-script-setup" foo="hello" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ClientOnlyScript, LazyClientOnlySetupScript } from '#components'
|
||||||
|
</script>
|
19
test/fixtures/basic/pages/client.vue
vendored
19
test/fixtures/basic/pages/client.vue
vendored
@ -1,12 +1,15 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
onMounted(() => import('~/components/BreaksServer'))
|
// explicit import to bypass client import protection
|
||||||
onBeforeMount(() => import('~/components/BreaksServer'))
|
import BreaksServer from '../components/BreaksServer.client'
|
||||||
onBeforeUpdate(() => import('~/components/BreaksServer'))
|
|
||||||
onRenderTracked(() => import('~/components/BreaksServer'))
|
onMounted(() => import('~/components/BreaksServer.client'))
|
||||||
onRenderTriggered(() => import('~/components/BreaksServer'))
|
onBeforeMount(() => import('~/components/BreaksServer.client'))
|
||||||
onActivated(() => import('~/components/BreaksServer'))
|
onBeforeUpdate(() => import('~/components/BreaksServer.client'))
|
||||||
onDeactivated(() => import('~/components/BreaksServer'))
|
onRenderTracked(() => import('~/components/BreaksServer.client'))
|
||||||
onBeforeUnmount(() => import('~/components/BreaksServer'))
|
onRenderTriggered(() => import('~/components/BreaksServer.client'))
|
||||||
|
onActivated(() => import('~/components/BreaksServer.client'))
|
||||||
|
onDeactivated(() => import('~/components/BreaksServer.client'))
|
||||||
|
onBeforeUnmount(() => import('~/components/BreaksServer.client'))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
Loading…
Reference in New Issue
Block a user