Skip to content

Commit df10153

Browse files
committed
feat(plugin-vue): add forced vapor mode option
Add features.vapor to force Vue SFC files to compile in Vapor mode on Vue 3.6+.
1 parent 51dbf4b commit df10153

6 files changed

Lines changed: 144 additions & 7 deletions

File tree

packages/plugin-vue/__tests__/template-only-vapor.spec.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { SFCDescriptor } from 'vue/compiler-sfc'
33
import type { ResolvedOptions } from '../src/index'
44
import { resolveCompiler } from '../src/compiler'
55
import { transformMain } from '../src/main'
6+
import { clearScriptCache, resolveScript } from '../src/script'
67
import { transformTemplateAsModule } from '../src/template'
78
import { createDescriptor } from '../src/utils/descriptorCache'
89

@@ -18,6 +19,15 @@ function createOptions(): ResolvedOptions {
1819
} as ResolvedOptions
1920
}
2021

22+
function createForcedVaporOptions(): ResolvedOptions {
23+
return {
24+
...createOptions(),
25+
features: {
26+
vapor: true,
27+
},
28+
}
29+
}
30+
2131
function parseDescriptor(
2232
filename: string,
2333
source: string,
@@ -126,3 +136,93 @@ describe.todo('template-only vapor __multiRoot', () => {
126136
expect(result?.code).not.toContain('multiRoot as _sfc_multiRoot')
127137
})
128138
})
139+
140+
// TODO: remove todo in v3.6
141+
describe.todo('features.vapor', () => {
142+
it('forces a template-only Vue SFC to compile in vapor mode', async () => {
143+
const filename = '/root/Forced.vue'
144+
const source = '<template><div /><div /></template>'
145+
const options = createForcedVaporOptions()
146+
147+
const result = await transformMain(
148+
source,
149+
filename,
150+
options,
151+
createPluginContext(),
152+
false,
153+
false,
154+
)
155+
156+
expect(result?.code).toContain('const _sfc_main = { __vapor: true }')
157+
expect(result?.code).toContain('_sfc_main.__multiRoot = true')
158+
})
159+
160+
it('forces external template modules to compile in vapor mode', async () => {
161+
const filename = '/root/ForcedExternal.vue'
162+
const source = '<template lang="pug">div\ndiv</template>'
163+
const options = createForcedVaporOptions()
164+
const descriptor = parseDescriptor(filename, source, options)
165+
166+
const mainResult = await transformMain(
167+
source,
168+
filename,
169+
options,
170+
createPluginContext(),
171+
false,
172+
false,
173+
)
174+
const templateResult = await transformTemplateAsModule(
175+
descriptor.template!.content,
176+
filename,
177+
descriptor,
178+
options,
179+
createPluginContext(),
180+
false,
181+
false,
182+
)
183+
184+
expect(mainResult?.code).toContain(
185+
'import { render as _sfc_render, multiRoot as _sfc_multiRoot }',
186+
)
187+
expect(mainResult?.code).toContain('_sfc_main.__multiRoot = _sfc_multiRoot')
188+
expect(templateResult.code).toContain('export const multiRoot = true')
189+
})
190+
191+
it('passes forced vapor mode to compileScript', () => {
192+
const filename = '/root/ForcedScript.vue'
193+
const options = createForcedVaporOptions()
194+
const descriptor = parseDescriptor(
195+
filename,
196+
'<script setup>const msg = "hi"</script><template>{{ msg }}</template>',
197+
options,
198+
)
199+
const compileScript = vi.fn(() => ({
200+
...descriptor.scriptSetup!,
201+
content: 'const _sfc_main = {}',
202+
}))
203+
204+
clearScriptCache()
205+
resolveScript(
206+
descriptor,
207+
{
208+
...options,
209+
compiler: {
210+
...compiler,
211+
compileScript,
212+
},
213+
},
214+
false,
215+
false,
216+
)
217+
218+
expect(compileScript).toHaveBeenCalledWith(
219+
descriptor,
220+
expect.objectContaining({
221+
vapor: true,
222+
templateOptions: expect.objectContaining({
223+
vapor: true,
224+
}),
225+
}),
226+
)
227+
})
228+
})

packages/plugin-vue/src/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,14 @@ export interface Options {
122122
* - **default:** /\.ce\.vue$/
123123
*/
124124
customElement?: boolean | string | RegExp | (string | RegExp)[]
125+
/**
126+
* Force all Vue SFC (`.vue`) files to compile in Vapor mode.
127+
* When enabled, this acts as a plugin-level fallback for SFCs without the
128+
* per-file `vapor` marker.
129+
* - Available in Vue 3.6 and later.
130+
* - **default:** `false`
131+
*/
132+
vapor?: boolean
125133
/**
126134
* Set to `false` to disable Options API support and allow related code in
127135
* Vue core to be dropped via dead-code elimination in production builds,

packages/plugin-vue/src/main.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { transformTemplateInMain } from './template'
2424
import { isEqualBlock, isOnlyTemplateChanged } from './handleHotUpdate'
2525
import { createRollupError } from './utils/error'
2626
import { EXPORT_HELPER_ID } from './helper'
27+
import { isVaporMode } from './utils/vapor'
2728
import type { ResolvedOptions } from './index'
2829

2930
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
@@ -67,9 +68,10 @@ export async function transformMain(
6768
// feature information
6869
const attachedProps: [string, string][] = []
6970
const hasScoped = descriptor.styles.some((s) => s.scoped)
70-
// @ts-expect-error TODO remove when 3.6 is out
7171
const isTemplateOnlyVapor =
72-
!descriptor.script && !descriptor.scriptSetup && descriptor.vapor
72+
!descriptor.script &&
73+
!descriptor.scriptSetup &&
74+
isVaporMode(descriptor, options)
7375

7476
// script
7577
const { code: scriptCode, map: scriptMap } = await genScriptCode(
@@ -345,9 +347,10 @@ async function genTemplateCode(
345347
}> {
346348
const template = descriptor.template!
347349
const hasScoped = descriptor.styles.some((style) => style.scoped)
348-
// @ts-expect-error TODO remove when 3.6 is out
349350
const needsMultiRoot =
350-
!descriptor.script && !descriptor.scriptSetup && descriptor.vapor
351+
!descriptor.script &&
352+
!descriptor.scriptSetup &&
353+
isVaporMode(descriptor, options)
351354

352355
// If the template is not using pre-processor AND is not using external src,
353356
// compile and inline it directly in the main module. When served in vite this
@@ -404,8 +407,7 @@ async function genScriptCode(
404407
code: string
405408
map: RawSourceMap | undefined
406409
}> {
407-
// @ts-expect-error TODO remove when 3.6 is out
408-
const vaporFlag = descriptor.vapor ? '__vapor: true' : ''
410+
const vaporFlag = isVaporMode(descriptor, options) ? '__vapor: true' : ''
409411
let scriptCode = `const ${scriptIdentifier} = { ${vaporFlag} }`
410412
let map: RawSourceMap | undefined
411413

packages/plugin-vue/src/script.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { SFCDescriptor, SFCScriptBlock } from 'vue/compiler-sfc'
22
import { resolveTemplateCompilerOptions } from './template'
33
import { cache as descriptorCache } from './utils/descriptorCache'
4+
import { isVaporMode } from './utils/vapor'
45
import type { ResolvedOptions } from './index'
56

67
// ssr and non ssr builds would output different script content
@@ -85,6 +86,8 @@ export function resolveScript(
8586
? scriptIdentifier
8687
: undefined,
8788
customElement,
89+
// @ts-expect-error TODO remove when 3.6 is out
90+
vapor: isVaporMode(descriptor, options),
8891
propsDestructure:
8992
options.features?.propsDestructure ?? options.script?.propsDestructure,
9093
})

packages/plugin-vue/src/template.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type {
99
import type { Rollup } from 'vite'
1010
import { getResolvedScript, resolveScript } from './script'
1111
import { createRollupError } from './utils/error'
12+
import { isVaporMode } from './utils/vapor'
1213
import type { ResolvedOptions } from './index'
1314

1415
export async function transformTemplateAsModule(
@@ -193,7 +194,8 @@ export function resolveTemplateCompilerOptions(
193194
return {
194195
...options.template,
195196
// @ts-expect-error TODO remove when 3.6 is out
196-
vapor: descriptor.vapor,
197+
198+
vapor: isVaporMode(descriptor, options),
197199
id,
198200
ast: canReuseAST(options.compiler.version)
199201
? descriptor.template?.ast
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type { SFCDescriptor } from 'vue/compiler-sfc'
2+
3+
interface VaporModeOptions {
4+
features?: {
5+
vapor?: boolean
6+
}
7+
}
8+
9+
/**
10+
* Resolve the effective Vapor mode for a Vue SFC (`.vue`) file.
11+
*
12+
* `descriptor.vapor` is the per-file opt-in marker. `features.vapor` is the
13+
* plugin-level force switch: when enabled, every Vue SFC handled by this plugin
14+
* is compiled as Vapor mode even if the descriptor itself is not marked.
15+
*/
16+
export function isVaporMode(
17+
descriptor: SFCDescriptor,
18+
options: VaporModeOptions,
19+
): boolean {
20+
// @ts-expect-error TODO remove when 3.6 is out
21+
return !!(descriptor.vapor || options.features?.vapor)
22+
}

0 commit comments

Comments
 (0)