From 7da99fc49e0dffe06758c356e73635c9332ae046 Mon Sep 17 00:00:00 2001 From: julien huang Date: Sun, 11 Sep 2022 14:07:08 +0200 Subject: [PATCH 01/13] feat(nuxt): convert .client component with createClient only in #components --- packages/nuxt/src/components/scan.ts | 15 +++++++++- packages/nuxt/src/components/templates.ts | 36 ++++++++++++++++------- 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/packages/nuxt/src/components/scan.ts b/packages/nuxt/src/components/scan.ts index 4cad3c587d6..1e23546386f 100644 --- a/packages/nuxt/src/components/scan.ts +++ b/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 { pkgDir } from '../dirs' /** * Scan the components inside different components folders @@ -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(pkgDir, 'src/app/components/server-placeholder.ts'), + chunkName: 'components/' + component.kebabName + }) + } + }) + return components } diff --git a/packages/nuxt/src/components/templates.ts b/packages/nuxt/src/components/templates.ts index d74119e1060..6db5d4b536a 100644 --- a/packages/nuxt/src/components/templates.ts +++ b/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 @@ -53,17 +53,31 @@ export default defineNuxtPlugin(nuxtApp => { export const componentsTemplate: NuxtTemplate = { // 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() + 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} = 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') } From c1a03232b394df569ac41e9308558c394f550b97 Mon Sep 17 00:00:00 2001 From: julien huang Date: Sun, 11 Sep 2022 14:49:57 +0200 Subject: [PATCH 02/13] docs: update .client docs --- docs/content/2.guide/3.directory-structure/4.components.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/2.guide/3.directory-structure/4.components.md b/docs/content/2.guide/3.directory-structure/4.components.md index f74e63a3660..1b1ee38a81b 100644 --- a/docs/content/2.guide/3.directory-structure/4.components.md +++ b/docs/content/2.guide/3.directory-structure/4.components.md @@ -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 From a1fb4a48b89c2482cb01578ceb423a969012eaae Mon Sep 17 00:00:00 2001 From: julien huang Date: Mon, 12 Sep 2022 00:52:14 +0200 Subject: [PATCH 03/13] fix(nuxt): fix lazy import not being chunked --- packages/nuxt/src/components/templates.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/nuxt/src/components/templates.ts b/packages/nuxt/src/components/templates.ts index 6db5d4b536a..7de7e36fa09 100644 --- a/packages/nuxt/src/components/templates.ts +++ b/packages/nuxt/src/components/templates.ts @@ -55,8 +55,7 @@ export const componentsTemplate: NuxtTemplate = { getContents ({ options }) { const imports = new Set() 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) @@ -64,13 +63,10 @@ export const componentsTemplate: NuxtTemplate = { 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} = createClientOnly(${identifier})`) + definitions.push(`export const ${c.pascalName} = createClientOnly(${genDynamicImport(c.filePath, { comment })}.then(c => ${exp}))`) } else { - definitions.push(genExport(c.filePath, [{ name: c.export, as: c.pascalName }])) + definitions.push(`export const ${c.pascalName} = ${genDynamicImport(c.filePath, { comment })}.then(c => ${exp})`) } definitions.push(`export const Lazy${c.pascalName} = defineAsyncComponent(${genDynamicImport(c.filePath, { comment })}.then(c => ${isClient ? `createClientOnly(${exp})` : exp}))`) return definitions From 3ef7980455f8d86ea0b7dac166852b448a3580f7 Mon Sep 17 00:00:00 2001 From: julien huang Date: Mon, 12 Sep 2022 00:55:08 +0200 Subject: [PATCH 04/13] style: lint --- packages/nuxt/src/components/templates.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nuxt/src/components/templates.ts b/packages/nuxt/src/components/templates.ts index 7de7e36fa09..0b39e3cf36d 100644 --- a/packages/nuxt/src/components/templates.ts +++ b/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, genImport, genObjectFromRawEntries } from 'knitwork' +import { genDynamicImport, genImport, genObjectFromRawEntries } from 'knitwork' export interface ComponentsTemplateContext { nuxt: Nuxt @@ -55,7 +55,7 @@ export const componentsTemplate: NuxtTemplate = { getContents ({ options }) { const imports = new Set() imports.add('import { defineAsyncComponent } from \'vue\'') - + const components = options.getComponents(options.mode).flatMap((c) => { const exp = c.export === 'default' ? 'c.default || c' : `c['${c.export}']` const comment = createImportMagicComments(c) From 8fe530b06540e8b1cd5245dbeeb65e79bec2f809 Mon Sep 17 00:00:00 2001 From: julien huang Date: Mon, 12 Sep 2022 01:10:06 +0200 Subject: [PATCH 05/13] revert : revert hydration issue --- packages/nuxt/src/components/templates.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/nuxt/src/components/templates.ts b/packages/nuxt/src/components/templates.ts index 0b39e3cf36d..6db5d4b536a 100644 --- a/packages/nuxt/src/components/templates.ts +++ b/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, genImport, genObjectFromRawEntries } from 'knitwork' +import { genDynamicImport, genExport, genImport, genObjectFromRawEntries } from 'knitwork' export interface ComponentsTemplateContext { nuxt: Nuxt @@ -56,6 +56,7 @@ export const componentsTemplate: NuxtTemplate = { const imports = new Set() 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) @@ -63,10 +64,13 @@ export const componentsTemplate: NuxtTemplate = { const isClient = c.mode === 'client' const definitions = [] if (isClient) { + num++ + const identifier = `__nuxt_component_${num}` imports.add(genImport('#app/components/client-only', [{ name: 'createClientOnly' }])) - definitions.push(`export const ${c.pascalName} = createClientOnly(${genDynamicImport(c.filePath, { comment })}.then(c => ${exp}))`) + imports.add(genImport(c.filePath, [{ name: c.export, as: identifier }])) + definitions.push(`export const ${c.pascalName} = createClientOnly(${identifier})`) } else { - definitions.push(`export const ${c.pascalName} = ${genDynamicImport(c.filePath, { comment })}.then(c => ${exp})`) + 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 From 36df3b59c502d892a1659c3b948c54d019d1b5cb Mon Sep 17 00:00:00 2001 From: "jhuang@hsk-partners.com" Date: Mon, 12 Sep 2022 13:22:32 +0200 Subject: [PATCH 06/13] fix(nuxt): avoid side-effect at not lazy loaded version --- packages/nuxt/src/components/templates.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nuxt/src/components/templates.ts b/packages/nuxt/src/components/templates.ts index 6db5d4b536a..c79b3ad5727 100644 --- a/packages/nuxt/src/components/templates.ts +++ b/packages/nuxt/src/components/templates.ts @@ -68,7 +68,7 @@ export const componentsTemplate: NuxtTemplate = { 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} = createClientOnly(${identifier})`) + definitions.push(`export const ${c.pascalName} = /*#__PURE__*/ createClientOnly(${identifier})`) } else { definitions.push(genExport(c.filePath, [{ name: c.export, as: c.pascalName }])) } From 966d6ab98f8007686a9b668f6ae3cf367ec5d3bc Mon Sep 17 00:00:00 2001 From: julien huang Date: Tue, 13 Sep 2022 22:53:22 +0200 Subject: [PATCH 07/13] fix(nuxt): fix server-placeholder filePath --- packages/nuxt/src/components/scan.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nuxt/src/components/scan.ts b/packages/nuxt/src/components/scan.ts index 1e23546386f..a2273db8460 100644 --- a/packages/nuxt/src/components/scan.ts +++ b/packages/nuxt/src/components/scan.ts @@ -5,7 +5,7 @@ import type { Component, ComponentsDir } from '@nuxt/schema' import { isIgnored } from '@nuxt/kit' import { hyphenate } from '@vue/shared' import { withTrailingSlash } from 'ufo' -import { pkgDir } from '../dirs' +import { distDir } from '../dirs' /** * Scan the components inside different components folders @@ -137,7 +137,7 @@ export async function scanComponents (dirs: ComponentsDir[], srcDir: string): Pr components.push({ ...component, mode: 'server', - filePath: resolve(pkgDir, 'src/app/components/server-placeholder.ts'), + filePath: resolve(distDir, 'app/components/server-placeholder'), chunkName: 'components/' + component.kebabName }) } From 6c1330b4f92f32b396348f951b1d82fd13ff4155 Mon Sep 17 00:00:00 2001 From: julien huang Date: Tue, 4 Oct 2022 00:03:31 +0200 Subject: [PATCH 08/13] test(nuxt): add test for #component .client import --- test/basic.test.ts | 11 +++++++++++ .../basic/pages/client-only-explicit-import.vue | 11 +++++++++++ 2 files changed, 22 insertions(+) create mode 100644 test/fixtures/basic/pages/client-only-explicit-import.vue diff --git a/test/basic.test.ts b/test/basic.test.ts index 083bf889030..adbb3bc89d6 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -140,6 +140,17 @@ describe('pages', () => { await expectNoClientErrors('/client-only-components') }) + + 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('
') + expect(html).toContain('
') + // ensure components are not rendered server-side + expect(html).not.toContain('client only script') + await expectNoClientErrors('/client-only-components') + }) }) describe('head tags', () => { diff --git a/test/fixtures/basic/pages/client-only-explicit-import.vue b/test/fixtures/basic/pages/client-only-explicit-import.vue new file mode 100644 index 00000000000..23463cd4019 --- /dev/null +++ b/test/fixtures/basic/pages/client-only-explicit-import.vue @@ -0,0 +1,11 @@ + + + + From 98851186691ed028289a9cd8fba594b029fdf78e Mon Sep 17 00:00:00 2001 From: julien huang Date: Tue, 4 Oct 2022 21:42:32 +0200 Subject: [PATCH 09/13] test: change BreakServer to BreakServer.client to avoid test break in tests --- .../{BreaksServer.ts => BreaksServer.client.ts} | 0 test/fixtures/basic/pages/client.vue | 16 ++++++++-------- 2 files changed, 8 insertions(+), 8 deletions(-) rename test/fixtures/basic/components/{BreaksServer.ts => BreaksServer.client.ts} (100%) diff --git a/test/fixtures/basic/components/BreaksServer.ts b/test/fixtures/basic/components/BreaksServer.client.ts similarity index 100% rename from test/fixtures/basic/components/BreaksServer.ts rename to test/fixtures/basic/components/BreaksServer.client.ts diff --git a/test/fixtures/basic/pages/client.vue b/test/fixtures/basic/pages/client.vue index 95da86f78d4..c6d0495aeb6 100644 --- a/test/fixtures/basic/pages/client.vue +++ b/test/fixtures/basic/pages/client.vue @@ -1,12 +1,12 @@