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): handle auto-importing named components #26556

Merged
merged 3 commits into from
Apr 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 9 additions & 6 deletions packages/nuxt/src/components/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { parseURL } from 'ufo'
import { parseQuery } from 'vue-router'
import { normalize, resolve } from 'pathe'
import { genImport } from 'knitwork'
import { distDir } from '../dirs'
import type { getComponentsT } from './module'

Expand All @@ -27,7 +28,7 @@
const components = getComponents(mode)
return components.flatMap((c): Import[] => {
const withMode = (mode: string | undefined) => mode
? `${c.filePath}${c.filePath.includes('?') ? '&' : '?'}nuxt_component=${mode}&nuxt_component_name=${c.pascalName}`
? `${c.filePath}${c.filePath.includes('?') ? '&' : '?'}nuxt_component=${mode}&nuxt_component_name=${c.pascalName}&nuxt_component_export=${c.export || 'default'}`
: c.filePath

const mode = !c._raw && c.mode && ['client', 'server'].includes(c.mode) ? c.mode : undefined
Expand Down Expand Up @@ -60,20 +61,22 @@
const query = parseQuery(search)
const mode = query.nuxt_component
const bare = id.replace(/\?.*/, '')
const componentExport = query.nuxt_component_export as string || 'default'
const exportWording = componentExport === 'default' ? 'export default' : `export const ${componentExport} =`
if (mode === 'async') {
return {
code: [
'import { defineAsyncComponent } from "vue"',
`export default defineAsyncComponent(() => import(${JSON.stringify(bare)}).then(r => r.default))`
`${exportWording} defineAsyncComponent(() => import(${JSON.stringify(bare)}).then(r => r[${JSON.stringify(componentExport)}] || r.default || r))`
Dismissed Show dismissed Hide dismissed
Dismissed Show dismissed Hide dismissed
].join('\n'),
map: null
}
} else if (mode === 'client') {
return {
code: [
`import __component from ${JSON.stringify(bare)}`,
genImport(bare, [{ name: componentExport, as: '__component' }]),
'import { createClientOnly } from "#app/components/client-only"',
'export default createClientOnly(__component)'
`${exportWording} createClientOnly(__component)`
].join('\n'),
map: null
}
Expand All @@ -82,7 +85,7 @@
code: [
'import { defineAsyncComponent } from "vue"',
'import { createClientOnly } from "#app/components/client-only"',
`export default defineAsyncComponent(() => import(${JSON.stringify(bare)}).then(r => createClientOnly(r.default)))`
`${exportWording} defineAsyncComponent(() => import(${JSON.stringify(bare)}).then(r => createClientOnly(r[${JSON.stringify(componentExport)}] || r.default || r)))`
Dismissed Show dismissed Hide dismissed
Dismissed Show dismissed Hide dismissed
].join('\n'),
map: null
}
Expand All @@ -91,7 +94,7 @@
return {
code: [
`import { createServerComponent } from ${JSON.stringify(serverComponentRuntime)}`,
`export default createServerComponent(${JSON.stringify(name)})`
`${exportWording} createServerComponent(${JSON.stringify(name)})`
].join('\n'),
map: null
}
Expand Down
112 changes: 112 additions & 0 deletions packages/nuxt/test/components-transform.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
import type { Component, Nuxt } from '@nuxt/schema'
import { kebabCase } from 'scule'

import { createTransformPlugin } from '../src/components/transform'

describe('components:transform', () => {
it('should transform #components imports', async () => {
const transform = createTransformer([
createComponent('Foo'),
createComponent('Bar', { export: 'Bar' })
])

const code = await transform('import { Foo, Bar } from \'#components\'', '/app.vue')
expect(code).toMatchInlineSnapshot(`
"import Foo from '/Foo.vue';
import { Bar } from '/Bar.vue';
"
`)
})

it('should correctly resolve server-only components', async () => {
const transform = createTransformer([
createComponent('Foo', { mode: 'server' })
])

const code = await transform('import { Foo, LazyFoo } from \'#components\'', '/app.vue')
expect(code).toMatchInlineSnapshot(`
"import Foo from '/Foo.vue?nuxt_component=server&nuxt_component_name=Foo&nuxt_component_export=default';
import LazyFoo from '/Foo.vue?nuxt_component=server,async&nuxt_component_name=Foo&nuxt_component_export=default';
"
`)

expect(await transform('', '/Foo.vue?nuxt_component=server&nuxt_component_name=Foo&nuxt_component_export=default')).toMatchInlineSnapshot(`
"import { createServerComponent } from "<repo>/nuxt/src/components/runtime/server-component"
export default createServerComponent("Foo")"
`)
expect(await transform('', '/Foo.vue?nuxt_component=server,async&nuxt_component_name=Foo&nuxt_component_export=default')).toMatchInlineSnapshot(`
"import { createServerComponent } from "<repo>/nuxt/src/components/runtime/server-component"
export default createServerComponent("Foo")"
`)
expect(await transform('', '/Foo.vue?nuxt_component=server&nuxt_component_name=Foo&nuxt_component_export=Foo')).toMatchInlineSnapshot(`
"import { createServerComponent } from "<repo>/nuxt/src/components/runtime/server-component"
export const Foo = createServerComponent("Foo")"
`)
})

it('should correctly resolve client-only components', async () => {
const transform = createTransformer([
createComponent('Foo', { mode: 'client' })
])

const code = await transform('import { Foo, LazyFoo } from \'#components\'', '/app.vue')
expect(code).toMatchInlineSnapshot(`
"import Foo from '/Foo.vue?nuxt_component=client&nuxt_component_name=Foo&nuxt_component_export=default';
import LazyFoo from '/Foo.vue?nuxt_component=client,async&nuxt_component_name=Foo&nuxt_component_export=default';
"
`)

expect(await transform('', '/Foo.vue?nuxt_component=client&nuxt_component_name=Foo&nuxt_component_export=default')).toMatchInlineSnapshot(`
"import { default as __component } from "/Foo.vue";
import { createClientOnly } from "#app/components/client-only"
export default createClientOnly(__component)"
`)
expect(await transform('', '/Foo.vue?nuxt_component=client,async&nuxt_component_name=Foo&nuxt_component_export=default')).toMatchInlineSnapshot(`
"import { defineAsyncComponent } from "vue"
import { createClientOnly } from "#app/components/client-only"
export default defineAsyncComponent(() => import("/Foo.vue").then(r => createClientOnly(r["default"] || r.default || r)))"
`)
expect(await transform('', '/Foo.vue?nuxt_component=client,async&nuxt_component_name=Foo&nuxt_component_export=Foo')).toMatchInlineSnapshot(`
"import { defineAsyncComponent } from "vue"
import { createClientOnly } from "#app/components/client-only"
export const Foo = defineAsyncComponent(() => import("/Foo.vue").then(r => createClientOnly(r["Foo"] || r.default || r)))"
`)
})
})

const rootDir = fileURLToPath(new URL('../..', import.meta.url))

function createTransformer (components: Component[], mode: 'client' | 'server' | 'all' = 'all') {
const stubNuxt = {
options: {
buildDir: '/',
sourcemap: {
server: false,
client: false
}
}
} as Nuxt
const plugin = createTransformPlugin(stubNuxt, () => components, mode).vite()

return async (code: string, id: string) => {
const result = await (plugin as any).transform!(code, id)
return (typeof result === 'string' ? result : result?.code)?.replaceAll(rootDir, '<repo>/')
}
}

function createComponent (pascalName: string, options: Partial<Component> = {}) {
return {
filePath: `/${pascalName}.vue`,
pascalName,
export: 'default',
chunkName: `components/${pascalName.toLowerCase()}`,
kebabName: kebabCase(pascalName),
mode: 'all',
prefetch: false,
preload: false,
shortPath: `components/${pascalName}.vue`,
...options
} satisfies Component
}