mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-25 07:05:11 +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}
|
||||
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
|
||||
|
@ -2,6 +2,7 @@ import { statSync } from 'node:fs'
|
||||
import { relative, resolve } from 'pathe'
|
||||
import { defineNuxtModule, resolveAlias, addTemplate, addPluginTemplate } from '@nuxt/kit'
|
||||
import type { Component, ComponentsDir, ComponentsOptions } from '@nuxt/schema'
|
||||
import { distDir } from '../dirs'
|
||||
import { componentsPluginTemplate, componentsTemplate, componentsTypeTemplate } from './templates'
|
||||
import { scanComponents } from './scan'
|
||||
import { loaderPlugin } from './loader'
|
||||
@ -146,6 +147,17 @@ export default defineNuxtModule<ComponentsOptions>({
|
||||
nuxt.hook('app:templates', async () => {
|
||||
const newComponents = await scanComponents(componentDirs, nuxt.options.srcDir!)
|
||||
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
|
||||
})
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { isAbsolute, relative } from 'pathe'
|
||||
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 {
|
||||
nuxt: Nuxt
|
||||
@ -53,17 +53,31 @@ export default defineNuxtPlugin(nuxtApp => {
|
||||
export const componentsTemplate: NuxtTemplate<ComponentsTemplateContext> = {
|
||||
// components.[server|client].mjs'
|
||||
getContents ({ options }) {
|
||||
return [
|
||||
'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)
|
||||
const imports = new Set<string>()
|
||||
imports.add('import { defineAsyncComponent } from \'vue\'')
|
||||
|
||||
return [
|
||||
genExport(c.filePath, [{ name: c.export, as: c.pascalName }]),
|
||||
`export const Lazy${c.pascalName} = defineAsyncComponent(${genDynamicImport(c.filePath, { comment })}.then(c => ${exp}))`
|
||||
]
|
||||
}),
|
||||
let num = 0
|
||||
const components = options.getComponents(options.mode).flatMap((c) => {
|
||||
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))}`
|
||||
].join('\n')
|
||||
}
|
||||
|
@ -219,6 +219,17 @@ describe('pages', () => {
|
||||
await Promise.all(hiddenSelectors.map(selector => page.locator(selector).isVisible()))
|
||||
.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', () => {
|
||||
|
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">
|
||||
onMounted(() => import('~/components/BreaksServer'))
|
||||
onBeforeMount(() => import('~/components/BreaksServer'))
|
||||
onBeforeUpdate(() => import('~/components/BreaksServer'))
|
||||
onRenderTracked(() => import('~/components/BreaksServer'))
|
||||
onRenderTriggered(() => import('~/components/BreaksServer'))
|
||||
onActivated(() => import('~/components/BreaksServer'))
|
||||
onDeactivated(() => import('~/components/BreaksServer'))
|
||||
onBeforeUnmount(() => import('~/components/BreaksServer'))
|
||||
// explicit import to bypass client import protection
|
||||
import BreaksServer from '../components/BreaksServer.client'
|
||||
|
||||
onMounted(() => import('~/components/BreaksServer.client'))
|
||||
onBeforeMount(() => import('~/components/BreaksServer.client'))
|
||||
onBeforeUpdate(() => import('~/components/BreaksServer.client'))
|
||||
onRenderTracked(() => import('~/components/BreaksServer.client'))
|
||||
onRenderTriggered(() => import('~/components/BreaksServer.client'))
|
||||
onActivated(() => import('~/components/BreaksServer.client'))
|
||||
onDeactivated(() => import('~/components/BreaksServer.client'))
|
||||
onBeforeUnmount(() => import('~/components/BreaksServer.client'))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
Loading…
Reference in New Issue
Block a user