Skip to content

Commit 9e07ae9

Browse files
authored
feat(plugin-vue): propagate multiRoot for template-only vapor components (#745)
1 parent 050c996 commit 9e07ae9

File tree

4 files changed

+169
-4
lines changed

4 files changed

+169
-4
lines changed
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { describe, expect, it, vi } from 'vitest'
2+
import type { SFCDescriptor } from 'vue/compiler-sfc'
3+
import type { ResolvedOptions } from '../src/index'
4+
import { resolveCompiler } from '../src/compiler'
5+
import { transformMain } from '../src/main'
6+
import { transformTemplateAsModule } from '../src/template'
7+
import { createDescriptor } from '../src/utils/descriptorCache'
8+
9+
const compiler = resolveCompiler(process.cwd())
10+
11+
function createOptions(): ResolvedOptions {
12+
return {
13+
root: '/root',
14+
isProduction: false,
15+
sourceMap: false,
16+
cssDevSourcemap: false,
17+
compiler,
18+
} as ResolvedOptions
19+
}
20+
21+
function parseDescriptor(
22+
filename: string,
23+
source: string,
24+
options: ResolvedOptions,
25+
): SFCDescriptor {
26+
const { descriptor, errors } = createDescriptor(filename, source, options)
27+
if (errors.length) {
28+
throw errors[0]
29+
}
30+
return descriptor
31+
}
32+
33+
function createPluginContext() {
34+
return {
35+
warn: vi.fn(),
36+
error: vi.fn((error: unknown) => {
37+
throw error
38+
}),
39+
} as any
40+
}
41+
42+
// TODO: remove todo in v3.6
43+
describe.todo('template-only vapor __multiRoot', () => {
44+
it('attaches __multiRoot for inline multi-root templates', async () => {
45+
const filename = '/root/Inline.vue'
46+
const source = '<template vapor><div /><div /></template>'
47+
const options = createOptions()
48+
49+
const result = await transformMain(
50+
source,
51+
filename,
52+
options,
53+
createPluginContext(),
54+
false,
55+
false,
56+
)
57+
58+
expect(result?.code).toContain('const _sfc_main = { __vapor: true }')
59+
expect(result?.code).toContain('_sfc_main.__multiRoot = true')
60+
})
61+
62+
it('preserves false multiRoot values for inline single-root templates', async () => {
63+
const filename = '/root/InlineSingle.vue'
64+
const source = '<template vapor><div /></template>'
65+
const options = createOptions()
66+
67+
const result = await transformMain(
68+
source,
69+
filename,
70+
options,
71+
createPluginContext(),
72+
false,
73+
false,
74+
)
75+
76+
expect(result?.code).toContain('_sfc_main.__multiRoot = false')
77+
})
78+
79+
it('re-exports and imports multiRoot for external template modules', async () => {
80+
const filename = '/root/External.vue'
81+
const source = '<template vapor lang="pug">div\ndiv</template>'
82+
const options = createOptions()
83+
const descriptor = parseDescriptor(filename, source, options)
84+
85+
const mainResult = await transformMain(
86+
source,
87+
filename,
88+
options,
89+
createPluginContext(),
90+
false,
91+
false,
92+
)
93+
const templateResult = await transformTemplateAsModule(
94+
descriptor.template!.content,
95+
filename,
96+
descriptor,
97+
options,
98+
createPluginContext(),
99+
false,
100+
false,
101+
)
102+
103+
expect(mainResult?.code).toContain(
104+
'import { render as _sfc_render, multiRoot as _sfc_multiRoot }',
105+
)
106+
expect(mainResult?.code).toContain('_sfc_main.__multiRoot = _sfc_multiRoot')
107+
expect(templateResult.code).toContain('export const multiRoot = true')
108+
})
109+
110+
it('does not attach __multiRoot when the component has script', async () => {
111+
const filename = '/root/WithScript.vue'
112+
const source =
113+
'<script>export default {}</script><template vapor lang="pug">div</template>'
114+
const options = createOptions()
115+
116+
const result = await transformMain(
117+
source,
118+
filename,
119+
options,
120+
createPluginContext(),
121+
false,
122+
false,
123+
)
124+
125+
expect(result?.code).not.toContain('__multiRoot')
126+
expect(result?.code).not.toContain('multiRoot as _sfc_multiRoot')
127+
})
128+
})

packages/plugin-vue/src/compiler.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@ declare module 'vue/compiler-sfc' {
33
interface SFCDescriptor {
44
id: string
55
}
6+
7+
// TODO: remove once Vue 3.6 is officially released and stable compiler-sfc
8+
// types include `multiRoot`.
9+
interface SFCTemplateCompileResults {
10+
multiRoot?: boolean
11+
}
612
}
713

814
import { createRequire } from 'node:module'

packages/plugin-vue/src/main.ts

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ export async function transformMain(
6767
// feature information
6868
const attachedProps: [string, string][] = []
6969
const hasScoped = descriptor.styles.some((s) => s.scoped)
70+
// @ts-expect-error TODO remove when 3.6 is out
71+
const isTemplateOnlyVapor =
72+
!descriptor.script && !descriptor.scriptSetup && descriptor.vapor
7073

7174
// script
7275
const { code: scriptCode, map: scriptMap } = await genScriptCode(
@@ -80,11 +83,20 @@ export async function transformMain(
8083
// template
8184
const hasTemplateImport =
8285
descriptor.template && !isUseInlineTemplate(descriptor, options)
86+
const isTemplateInlined =
87+
!!descriptor.template &&
88+
(!descriptor.template.lang || descriptor.template.lang === 'html') &&
89+
!descriptor.template.src
8390

8491
let templateCode = ''
8592
let templateMap: RawSourceMap | undefined = undefined
93+
let templateMultiRoot: boolean | undefined
8694
if (hasTemplateImport) {
87-
;({ code: templateCode, map: templateMap } = await genTemplateCode(
95+
;({
96+
code: templateCode,
97+
map: templateMap,
98+
multiRoot: templateMultiRoot,
99+
} = await genTemplateCode(
88100
descriptor,
89101
options,
90102
pluginContext,
@@ -122,6 +134,11 @@ export async function transformMain(
122134
const output: string[] = [
123135
scriptCode,
124136
templateCode,
137+
isTemplateOnlyVapor
138+
? `${scriptIdentifier}.__multiRoot = ${
139+
isTemplateInlined ? templateMultiRoot : '_sfc_multiRoot'
140+
}`
141+
: '',
125142
stylesCode,
126143
customBlocksCode,
127144
]
@@ -321,22 +338,33 @@ async function genTemplateCode(
321338
pluginContext: Rollup.PluginContext,
322339
ssr: boolean,
323340
customElement: boolean,
324-
) {
341+
): Promise<{
342+
code: string
343+
map?: RawSourceMap
344+
multiRoot?: boolean
345+
}> {
325346
const template = descriptor.template!
326347
const hasScoped = descriptor.styles.some((style) => style.scoped)
348+
// @ts-expect-error TODO remove when 3.6 is out
349+
const needsMultiRoot =
350+
!descriptor.script && !descriptor.scriptSetup && descriptor.vapor
327351

328352
// If the template is not using pre-processor AND is not using external src,
329353
// compile and inline it directly in the main module. When served in vite this
330354
// saves an extra request per SFC which can improve load performance.
331355
if ((!template.lang || template.lang === 'html') && !template.src) {
332-
return transformTemplateInMain(
356+
const result = transformTemplateInMain(
333357
template.content,
334358
descriptor,
335359
options,
336360
pluginContext,
337361
ssr,
338362
customElement,
339363
)
364+
return {
365+
...result,
366+
multiRoot: needsMultiRoot ? result.multiRoot : undefined,
367+
}
340368
} else {
341369
if (template.src) {
342370
await linkSrcToDescriptor(
@@ -358,7 +386,9 @@ async function genTemplateCode(
358386
const request = JSON.stringify(src + query)
359387
const renderFnName = ssr ? 'ssrRender' : 'render'
360388
return {
361-
code: `import { ${renderFnName} as _sfc_${renderFnName} } from ${request}`,
389+
code: `import { ${renderFnName} as _sfc_${renderFnName}${
390+
needsMultiRoot ? ', multiRoot as _sfc_multiRoot' : ''
391+
} } from ${request}`,
362392
map: undefined,
363393
}
364394
}

packages/plugin-vue/src/template.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export async function transformTemplateAsModule(
3434
)
3535

3636
let returnCode = result.code
37+
returnCode += `\nexport const multiRoot = ${JSON.stringify(result.multiRoot)}`
3738
if (
3839
options.devServer &&
3940
options.devServer.config.server.hmr !== false &&

0 commit comments

Comments
 (0)