Skip to content
This repository has been archived by the owner on Apr 6, 2023. It is now read-only.

feat(nuxt): wrap #components client exports with createClientOnly #7412

Merged
merged 19 commits into from Oct 11, 2022
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
7da99fc
feat(nuxt): convert .client component with createClient only in #comp…
huang-julien Sep 11, 2022
c1a0323
docs: update .client docs
huang-julien Sep 11, 2022
218f3c3
Merge branch 'main' into fix/#components-import
huang-julien Sep 11, 2022
a1fb4a4
fix(nuxt): fix lazy import not being chunked
huang-julien Sep 11, 2022
7c47181
Merge branch 'fix/#components-import' of https://github.com/huang-jul…
huang-julien Sep 11, 2022
3ef7980
style: lint
huang-julien Sep 11, 2022
8fe530b
revert : revert hydration issue
huang-julien Sep 11, 2022
36df3b5
fix(nuxt): avoid side-effect at not lazy loaded version
Sep 12, 2022
b2e9bc2
Merge branch 'main' into fix/#components-import
huang-julien Sep 12, 2022
966d6ab
fix(nuxt): fix server-placeholder filePath
huang-julien Sep 13, 2022
6c1330b
test(nuxt): add test for #component .client import
huang-julien Oct 3, 2022
9885118
test: change BreakServer to BreakServer.client to avoid test break in…
huang-julien Oct 4, 2022
deb883a
refactor(test): use proper path for BreakServer.client
huang-julien Oct 4, 2022
63eb8ba
Merge remote-tracking branch 'origin/main' into fix/#components-import
danielroe Oct 9, 2022
e677619
test: add explicit import to bypass client import protection
danielroe Oct 9, 2022
b37b8cb
fix(nuxt): add server-placeholder .client filler after components:ext…
huang-julien Oct 9, 2022
8a6f0c6
Merge branch 'fix/#components-import' of https://github.com/huang-jul…
huang-julien Oct 9, 2022
6ea0049
Update packages/nuxt/src/components/module.ts
huang-julien Oct 11, 2022
94c6f28
Merge branch 'main' into fix/#components-import
danielroe Oct 11, 2022
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
2 changes: 1 addition & 1 deletion docs/content/2.guide/2.directory-structure/1.components.md
Expand Up @@ -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
Expand Down
15 changes: 14 additions & 1 deletion packages/nuxt/src/components/scan.ts
@@ -1,10 +1,11 @@
import { basename, extname, join, dirname, relative } from 'pathe'
import { basename, extname, join, dirname, relative, resolve } from 'pathe'
import { globby } from 'globby'
import { pascalCase, splitByCase } from 'scule'
import type { Component, ComponentsDir } from '@nuxt/schema'
import { isIgnored } from '@nuxt/kit'
import { hyphenate } from '@vue/shared'
import { withTrailingSlash } from 'ufo'
import { distDir } from '../dirs'

/**
* Scan the components inside different components folders
Expand Down Expand Up @@ -130,5 +131,17 @@ export async function scanComponents (dirs: ComponentsDir[], srcDir: string): Pr
scannedPaths.push(dir.path)
}

// add server placeholder for .client components server side issue: #7085
components.forEach((component) => {
if (component.mode === 'client' && !components.some(c => c.pascalName === component.pascalName && c.mode === 'server')) {
components.push({
...component,
mode: 'server',
filePath: resolve(distDir, 'app/components/server-placeholder'),
chunkName: 'components/' + component.kebabName
})
}
})
danielroe marked this conversation as resolved.
Show resolved Hide resolved

return components
}
36 changes: 25 additions & 11 deletions packages/nuxt/src/components/templates.ts
@@ -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
Expand Down Expand Up @@ -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\'')

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)

return [
genExport(c.filePath, [{ name: c.export, as: c.pascalName }]),
`export const Lazy${c.pascalName} = defineAsyncComponent(${genDynamicImport(c.filePath, { comment })}.then(c => ${exp}))`
]
}),
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')
}
Expand Down
11 changes: 11 additions & 0 deletions test/basic.test.ts
Expand Up @@ -214,6 +214,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', () => {
Expand Down
11 changes: 11 additions & 0 deletions test/fixtures/basic/pages/client-only-explicit-import.vue
@@ -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>
16 changes: 8 additions & 8 deletions test/fixtures/basic/pages/client.vue
@@ -1,12 +1,12 @@
<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'))
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>
Expand Down