Skip to content

Commit 5f11c44

Browse files
feat(language-core): support the use of sfc root comment to configure vueCompilerOptions (#4987)
Co-authored-by: Johnson Chu <[email protected]>
1 parent 83b9c52 commit 5f11c44

File tree

17 files changed

+156
-50
lines changed

17 files changed

+156
-50
lines changed

extensions/vscode/syntaxes/vue.tmLanguage.json

+36
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
"name": "Vue",
44
"scopeName": "source.vue",
55
"patterns": [
6+
{
7+
"include": "#vue-comments"
8+
},
69
{
710
"include": "text.html.basic#comment"
811
},
@@ -1278,6 +1281,39 @@
12781281
]
12791282
}
12801283
]
1284+
},
1285+
"vue-comments": {
1286+
"patterns": [
1287+
{
1288+
"include": "#vue-comments-key-value"
1289+
}
1290+
]
1291+
},
1292+
"vue-comments-key-value": {
1293+
"begin": "(<!--)\\s*(@)([\\w$]+)(?=\\s)",
1294+
"beginCaptures": {
1295+
"1": {
1296+
"name": "punctuation.definition.comment.vue"
1297+
},
1298+
"2": {
1299+
"name": "punctuation.definition.block.tag.comment.vue"
1300+
},
1301+
"3": {
1302+
"name": "storage.type.class.comment.vue"
1303+
}
1304+
},
1305+
"end": "(-->)",
1306+
"endCaptures": {
1307+
"1": {
1308+
"name": "punctuation.definition.comment.vue"
1309+
}
1310+
},
1311+
"name": "comment.block.vue",
1312+
"patterns": [
1313+
{
1314+
"include": "source.json#value"
1315+
}
1316+
]
12811317
}
12821318
}
12831319
}

packages/language-core/lib/codegen/script/scriptSetup.ts

+4-7
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ function* generateSetupFunction(
271271

272272
yield* generateScriptSectionPartiallyEnding(scriptSetup.name, scriptSetup.content.length, '#3632/scriptSetup.vue');
273273
yield* generateMacros(options, ctx);
274-
yield* generateDefineProp(options, scriptSetup);
274+
yield* generateDefineProp(options);
275275

276276
if (scriptSetupRanges.defineProps?.typeArg && scriptSetupRanges.withDefaults?.arg) {
277277
// fix https://github.com/vuejs/language-tools/issues/1187
@@ -324,12 +324,9 @@ function* generateMacros(
324324
}
325325
}
326326

327-
function* generateDefineProp(
328-
options: ScriptCodegenOptions,
329-
scriptSetup: NonNullable<Sfc['scriptSetup']>
330-
): Generator<Code> {
331-
const definePropProposalA = scriptSetup.content.trimStart().startsWith('// @experimentalDefinePropProposal=kevinEdition') || options.vueCompilerOptions.experimentalDefinePropProposal === 'kevinEdition';
332-
const definePropProposalB = scriptSetup.content.trimStart().startsWith('// @experimentalDefinePropProposal=johnsonEdition') || options.vueCompilerOptions.experimentalDefinePropProposal === 'johnsonEdition';
327+
function* generateDefineProp(options: ScriptCodegenOptions): Generator<Code> {
328+
const definePropProposalA = options.vueCompilerOptions.experimentalDefinePropProposal === 'kevinEdition';
329+
const definePropProposalB = options.vueCompilerOptions.experimentalDefinePropProposal === 'johnsonEdition';
333330

334331
if (definePropProposalA || definePropProposalB) {
335332
yield `type __VLS_PropOptions<T> = Exclude<import('${options.vueCompilerOptions.lib}').Prop<T>, import('${options.vueCompilerOptions.lib}').PropType<T>>${endOfLine}`;

packages/language-core/lib/parsers/scriptSetupRanges.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,8 @@ export function parseScriptSetupRanges(
7878
const useCssModule: UseCssModule[] = [];
7979
const useSlots: UseSlots[] = [];
8080
const useTemplateRef: UseTemplateRef[] = [];
81-
const definePropProposalA = vueCompilerOptions.experimentalDefinePropProposal === 'kevinEdition' || ast.text.trimStart().startsWith('// @experimentalDefinePropProposal=kevinEdition');
82-
const definePropProposalB = vueCompilerOptions.experimentalDefinePropProposal === 'johnsonEdition' || ast.text.trimStart().startsWith('// @experimentalDefinePropProposal=johnsonEdition');
81+
const definePropProposalA = vueCompilerOptions.experimentalDefinePropProposal === 'kevinEdition';
82+
const definePropProposalB = vueCompilerOptions.experimentalDefinePropProposal === 'johnsonEdition';
8383
const text = ast.text;
8484

8585
const leadingCommentRanges = ts.getLeadingCommentRanges(text, 0)?.reverse() ?? [];
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type { VueCompilerOptions } from '../types';
2+
3+
const syntaxReg = /^\s*@(?<key>.+?)\s+(?<value>.+?)\s*$/m;
4+
5+
export function parseVueCompilerOptions(comments: string[]): Partial<VueCompilerOptions> | undefined {
6+
const entries = comments
7+
.map(text => {
8+
try {
9+
const match = text.match(syntaxReg);
10+
if (match) {
11+
const { key, value } = match.groups ?? {};
12+
return [key, JSON.parse(value)] as const;
13+
}
14+
}
15+
catch { };
16+
})
17+
.filter(item => !!item);
18+
19+
if (entries.length) {
20+
return Object.fromEntries(entries);
21+
}
22+
}

packages/language-core/lib/plugins/file-html.ts

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const plugin: VueLanguagePlugin = ({ vueCompilerOptions }) => {
2929
descriptor: {
3030
filename: fileName,
3131
source: content,
32+
comments: [],
3233
template: null,
3334
script: null,
3435
scriptSetup: null,

packages/language-core/lib/plugins/vue-tsx.ts

+14-6
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import { generateScript } from '../codegen/script';
44
import { generateTemplate } from '../codegen/template';
55
import { parseScriptRanges } from '../parsers/scriptRanges';
66
import { parseScriptSetupRanges } from '../parsers/scriptSetupRanges';
7+
import { parseVueCompilerOptions } from '../parsers/vueCompilerOptions';
78
import type { Code, Sfc, VueLanguagePlugin } from '../types';
9+
import { resolveVueCompilerOptions } from '../utils/ts';
810

911
export const tsCodegen = new WeakMap<Sfc, ReturnType<typeof createTsx>>();
1012

@@ -77,14 +79,20 @@ function createTsx(
7779
: _sfc.script && _sfc.script.lang !== 'js' ? _sfc.script.lang
7880
: 'js';
7981
});
82+
const vueCompilerOptions = computed(() => {
83+
const options = parseVueCompilerOptions(_sfc.comments);
84+
return options
85+
? resolveVueCompilerOptions(options, ctx.vueCompilerOptions)
86+
: ctx.vueCompilerOptions;
87+
});
8088
const scriptRanges = computed(() =>
8189
_sfc.script
8290
? parseScriptRanges(ts, _sfc.script.ast, !!_sfc.scriptSetup, false)
8391
: undefined
8492
);
8593
const scriptSetupRanges = computed(() =>
8694
_sfc.scriptSetup
87-
? parseScriptSetupRanges(ts, _sfc.scriptSetup.ast, ctx.vueCompilerOptions)
95+
? parseScriptSetupRanges(ts, _sfc.scriptSetup.ast, vueCompilerOptions.get())
8896
: undefined
8997
);
9098
const scriptSetupBindingNames = Unstable.computedSet(
@@ -147,17 +155,17 @@ function createTsx(
147155
});
148156
const generatedTemplate = computed(() => {
149157

150-
if (ctx.vueCompilerOptions.skipTemplateCodegen || !_sfc.template) {
158+
if (vueCompilerOptions.get().skipTemplateCodegen || !_sfc.template) {
151159
return;
152160
}
153161

154162
const codes: Code[] = [];
155163
const codegen = generateTemplate({
156164
ts,
157165
compilerOptions: ctx.compilerOptions,
158-
vueCompilerOptions: ctx.vueCompilerOptions,
166+
vueCompilerOptions: vueCompilerOptions.get(),
159167
template: _sfc.template,
160-
edited: ctx.vueCompilerOptions.__test || (fileEditTimes.get(fileName) ?? 0) >= 2,
168+
edited: vueCompilerOptions.get().__test || (fileEditTimes.get(fileName) ?? 0) >= 2,
161169
scriptSetupBindingNames: scriptSetupBindingNames.get(),
162170
scriptSetupImportComponentNames: scriptSetupImportComponentNames.get(),
163171
destructuredPropNames: destructuredPropNames.get(),
@@ -188,9 +196,9 @@ function createTsx(
188196
const codegen = generateScript({
189197
ts,
190198
compilerOptions: ctx.compilerOptions,
191-
vueCompilerOptions: ctx.vueCompilerOptions,
199+
vueCompilerOptions: vueCompilerOptions.get(),
192200
sfc: _sfc,
193-
edited: ctx.vueCompilerOptions.__test || (fileEditTimes.get(fileName) ?? 0) >= 2,
201+
edited: vueCompilerOptions.get().__test || (fileEditTimes.get(fileName) ?? 0) >= 2,
194202
fileName,
195203
lang: lang.get(),
196204
scriptRanges: scriptRanges.get(),

packages/language-core/lib/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ export interface SfcBlock {
105105

106106
export interface Sfc {
107107
content: string;
108+
comments: string[],
108109
template: SfcBlock & {
109110
ast: CompilerDOM.RootNode | undefined;
110111
errors: CompilerDOM.CompilerError[];

packages/language-core/lib/utils/parseSfc.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@ import type { ElementNode, SourceLocation } from '@vue/compiler-dom';
22
import * as compiler from '@vue/compiler-dom';
33
import type { CompilerError, SFCBlock, SFCDescriptor, SFCParseResult, SFCScriptBlock, SFCStyleBlock, SFCTemplateBlock } from '@vue/compiler-sfc';
44

5+
declare module '@vue/compiler-sfc' {
6+
interface SFCDescriptor {
7+
comments: string[];
8+
}
9+
}
10+
511
export function parse(source: string): SFCParseResult {
612

713
const errors: CompilerError[] = [];
@@ -19,6 +25,7 @@ export function parse(source: string): SFCParseResult {
1925
const descriptor: SFCDescriptor = {
2026
filename: 'anonymous.vue',
2127
source,
28+
comments: [],
2229
template: null,
2330
script: null,
2431
scriptSetup: null,
@@ -29,7 +36,11 @@ export function parse(source: string): SFCParseResult {
2936
shouldForceReload: () => false,
3037
};
3138
ast.children.forEach(node => {
32-
if (node.type !== compiler.NodeTypes.ELEMENT) {
39+
if (node.type === compiler.NodeTypes.COMMENT) {
40+
descriptor.comments.push(node.content);
41+
return;
42+
}
43+
else if (node.type !== compiler.NodeTypes.ELEMENT) {
3344
return;
3445
}
3546
switch (node.tag) {

packages/language-core/lib/utils/ts.ts

+39-26
Original file line numberDiff line numberDiff line change
@@ -220,27 +220,24 @@ function getPartialVueCompilerOptions(
220220
}
221221
}
222222

223-
export function resolveVueCompilerOptions(vueOptions: Partial<VueCompilerOptions>): VueCompilerOptions {
224-
const target = vueOptions.target ?? 3.3;
225-
const lib = vueOptions.lib ?? 'vue';
223+
function getDefaultOptions(options: Partial<VueCompilerOptions>): VueCompilerOptions {
224+
const target = options.target ?? 3.3;
225+
const lib = options.lib ?? 'vue';
226226
return {
227-
...vueOptions,
228227
target,
229-
extensions: vueOptions.extensions ?? ['.vue'],
230-
vitePressExtensions: vueOptions.vitePressExtensions ?? [],
231-
petiteVueExtensions: vueOptions.petiteVueExtensions ?? [],
232228
lib,
233-
jsxSlots: vueOptions.jsxSlots ?? false,
234-
strictTemplates: vueOptions.strictTemplates ?? false,
235-
skipTemplateCodegen: vueOptions.skipTemplateCodegen ?? false,
236-
fallthroughAttributes: vueOptions.fallthroughAttributes ?? false,
237-
dataAttributes: vueOptions.dataAttributes ?? [],
238-
htmlAttributes: vueOptions.htmlAttributes ?? ['aria-*'],
239-
optionsWrapper: vueOptions.optionsWrapper ?? (
240-
target >= 2.7
241-
? [`(await import('${lib}')).defineComponent(`, `)`]
242-
: [`(await import('${lib}')).default.extend(`, `)`]
243-
),
229+
extensions: ['.vue'],
230+
vitePressExtensions: [],
231+
petiteVueExtensions: [],
232+
jsxSlots: false,
233+
strictTemplates: false,
234+
skipTemplateCodegen: false,
235+
fallthroughAttributes: false,
236+
dataAttributes: [],
237+
htmlAttributes: ['aria-*'],
238+
optionsWrapper: target >= 2.7
239+
? [`(await import('${lib}')).defineComponent(`, `)`]
240+
: [`(await import('${lib}')).default.extend(`, `)`],
244241
macros: {
245242
defineProps: ['defineProps'],
246243
defineSlots: ['defineSlots'],
@@ -249,24 +246,40 @@ export function resolveVueCompilerOptions(vueOptions: Partial<VueCompilerOptions
249246
defineModel: ['defineModel'],
250247
defineOptions: ['defineOptions'],
251248
withDefaults: ['withDefaults'],
252-
...vueOptions.macros,
253249
},
254250
composables: {
255251
useAttrs: ['useAttrs'],
256252
useCssModule: ['useCssModule'],
257253
useSlots: ['useSlots'],
258254
useTemplateRef: ['useTemplateRef', 'templateRef'],
259-
...vueOptions.composables,
260255
},
261-
plugins: vueOptions.plugins ?? [],
256+
plugins: [],
257+
experimentalDefinePropProposal: false,
258+
experimentalResolveStyleCssClasses: 'scoped',
259+
experimentalModelPropName: null!
260+
};
261+
};
262+
263+
export function resolveVueCompilerOptions(
264+
options: Partial<VueCompilerOptions>,
265+
defaults: VueCompilerOptions = getDefaultOptions(options)
266+
): VueCompilerOptions {
267+
return {
268+
...defaults,
269+
...options,
270+
macros: {
271+
...defaults.macros,
272+
...options.macros,
273+
},
274+
composables: {
275+
...defaults.composables,
276+
...options.composables,
277+
},
262278

263-
// experimental
264-
experimentalDefinePropProposal: vueOptions.experimentalDefinePropProposal ?? false,
265-
experimentalResolveStyleCssClasses: vueOptions.experimentalResolveStyleCssClasses ?? 'scoped',
266279
// https://github.com/vuejs/vue-next/blob/master/packages/compiler-dom/src/transforms/vModel.ts#L49-L51
267280
// https://vuejs.org/guide/essentials/forms.html#form-input-bindings
268281
experimentalModelPropName: Object.fromEntries(Object.entries(
269-
vueOptions.experimentalModelPropName ?? {
282+
options.experimentalModelPropName ?? defaults.experimentalModelPropName ?? {
270283
'': {
271284
input: true
272285
},
@@ -301,4 +314,4 @@ export function setupGlobalTypes(rootDir: string, vueOptions: VueCompilerOptions
301314
host.writeFile(globalTypesPath, globalTypesContents);
302315
return { absolutePath: globalTypesPath };
303316
} catch { }
304-
}
317+
}

packages/language-core/lib/virtualFile/computedSfc.ts

+11
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,16 @@ export function computedSfc(
2525
const content = computed(() => {
2626
return snapshot.get().getText(0, snapshot.get().getLength());
2727
});
28+
const comments = computed<string[]>(oldValue => {
29+
const newValue = parsed.get()?.descriptor.comments ?? [];
30+
if (
31+
oldValue?.length === newValue.length
32+
&& oldValue.every((v, i) => v === newValue[i])
33+
) {
34+
return oldValue;
35+
}
36+
return newValue;
37+
});
2838
const template = computedNullableSfcBlock(
2939
'template',
3040
'html',
@@ -149,6 +159,7 @@ export function computedSfc(
149159

150160
return {
151161
get content() { return content.get(); },
162+
get comments() { return comments.get(); },
152163
get template() { return template.get(); },
153164
get script() { return script.get(); },
154165
get scriptSetup() { return scriptSetup.get(); },

test-workspace/tsc/passedFixtures/vue3.4/defineProp_B/script-setup-generic.vue

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
<!-- @experimentalDefinePropProposal "johnsonEdition" -->
2+
13
<script setup lang="ts" generic="T">
2-
// @experimentalDefinePropProposal=johnsonEdition
34
import { exactType } from '../../shared';
45
56
const a = defineProp<T>();

test-workspace/tsc/passedFixtures/vue3.4/defineProp_B/script-setup.vue

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
<!-- @experimentalDefinePropProposal "johnsonEdition" -->
2+
13
<script setup lang="ts">
2-
// @experimentalDefinePropProposal=johnsonEdition
34
import { exactType } from '../../shared';
45
56
const a = defineProp<string>();
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
<script setup lang="ts" generic="T">
2-
// @experimentalDefinePropProposal=johnsonEdition
1+
<!-- @experimentalDefinePropProposal "johnsonEdition" -->
32

3+
<script setup lang="ts" generic="T">
44
defineProp<T>();
55
</script>

test-workspace/tsc/passedFixtures/vue3/#4649/prop-comp.vue

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
<!-- @experimentalDefinePropProposal "kevinEdition" -->
2+
13
<script lang="ts" setup>
2-
// @experimentalDefinePropProposal=kevinEdition
34
import { exactType } from '../../shared';
45
56
const fooAlias = defineProp('foo', {

test-workspace/tsc/passedFixtures/vue3/defineProp_A/script-setup.vue

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
<!-- @experimentalDefinePropProposal "kevinEdition" -->
2+
13
<script setup lang="ts">
2-
// @experimentalDefinePropProposal=kevinEdition
34
import { exactType } from '../../shared';
45
56
interface Qux { qux: true };

test-workspace/tsc/passedFixtures/vue3/defineProp_B/script-setup-generic.vue

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
<!-- @experimentalDefinePropProposal "johnsonEdition" -->
2+
13
<script setup lang="ts" generic="T">
2-
// @experimentalDefinePropProposal=johnsonEdition
34
import { exactType } from '../../shared';
45
56
const a = defineProp<T>();

0 commit comments

Comments
 (0)