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

feat(language-core): support the use of sfc root comment to configure vueCompilerOptions #4987

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions extensions/vscode/syntaxes/vue.tmLanguage.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
"name": "Vue",
"scopeName": "source.vue",
"patterns": [
{
"include": "#vue-comments"
},
{
"include": "text.html.basic#comment"
},
Expand Down Expand Up @@ -1278,6 +1281,39 @@
]
}
]
},
"vue-comments": {
"patterns": [
{
"include": "#vue-comments-key-value"
}
]
},
"vue-comments-key-value": {
"begin": "(<!--)\\s*(@)([\\w$]+)(?=\\s)",
"beginCaptures": {
"1": {
"name": "punctuation.definition.comment.vue"
},
"2": {
"name": "punctuation.definition.block.tag.comment.vue"
},
"3": {
"name": "storage.type.class.comment.vue"
}
},
"end": "(-->)",
"endCaptures": {
"1": {
"name": "punctuation.definition.comment.vue"
}
},
"name": "comment.block.vue",
"patterns": [
{
"include": "source.json#value"
}
]
}
}
}
13 changes: 5 additions & 8 deletions packages/language-core/lib/codegen/script/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export function* generateScript(options: ScriptCodegenOptions): Generator<Code,
&& options.sfc.script.content[exportDefault.expression.start] === '{';
if (options.sfc.scriptSetup && options.scriptSetupRanges) {
yield* generateScriptSetupImports(options.sfc.scriptSetup, options.scriptSetupRanges);
yield* generateDefineProp(options, options.sfc.scriptSetup);
yield* generateDefineProp(options);
if (exportDefault) {
yield generateSfcBlockSection(options.sfc.script, 0, exportDefault.expression.start, codeFeatures.all);
yield* generateScriptSetup(options, ctx, options.sfc.scriptSetup, options.scriptSetupRanges);
Expand Down Expand Up @@ -141,7 +141,7 @@ export function* generateScript(options: ScriptCodegenOptions): Generator<Code,
}
else if (options.sfc.scriptSetup && options.scriptSetupRanges) {
yield* generateScriptSetupImports(options.sfc.scriptSetup, options.scriptSetupRanges);
yield* generateDefineProp(options, options.sfc.scriptSetup);
yield* generateDefineProp(options);
yield* generateScriptSetup(options, ctx, options.sfc.scriptSetup, options.scriptSetupRanges);
}

Expand Down Expand Up @@ -183,12 +183,9 @@ export function* generateScriptSectionPartiallyEnding(source: string, end: numbe
yield `/* PartiallyEnd: ${mark} */${newLine}`;
}

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

if (definePropProposalA || definePropProposalB) {
yield `type __VLS_PropOptions<T> = Exclude<import('${options.vueCompilerOptions.lib}').Prop<T>, import('${options.vueCompilerOptions.lib}').PropType<T>>${endOfLine}`;
Expand Down
4 changes: 2 additions & 2 deletions packages/language-core/lib/parsers/scriptSetupRanges.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@ export function parseScriptSetupRanges(
const useCssModule: UseCssModule[] = [];
const useSlots: UseSlots[] = [];
const useTemplateRef: UseTemplateRef[] = [];
const definePropProposalA = vueCompilerOptions.experimentalDefinePropProposal === 'kevinEdition' || ast.text.trimStart().startsWith('// @experimentalDefinePropProposal=kevinEdition');
const definePropProposalB = vueCompilerOptions.experimentalDefinePropProposal === 'johnsonEdition' || ast.text.trimStart().startsWith('// @experimentalDefinePropProposal=johnsonEdition');
const definePropProposalA = vueCompilerOptions.experimentalDefinePropProposal === 'kevinEdition';
const definePropProposalB = vueCompilerOptions.experimentalDefinePropProposal === 'johnsonEdition';
const text = ast.text;
const importComponentNames = new Set<string>();

Expand Down
22 changes: 22 additions & 0 deletions packages/language-core/lib/parsers/vueCompilerOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { Sfc, VueCompilerOptions } from '../types';

const syntaxReg = /^\s*@(?<key>.+?)\s+(?<value>.+?)\s*$/m;

export function parseVueCompilerOptions(sfc: Sfc): Partial<VueCompilerOptions> | undefined {
const entries = sfc.comments
.map(text => {
try {
const match = text.match(syntaxReg);
if (match) {
const { key, value } = match.groups ?? {};
return [key, JSON.parse(value)] as const;
}
}
catch { };
})
.filter(item => !!item);

if (entries.length) {
return Object.fromEntries(entries);
}
}
1 change: 1 addition & 0 deletions packages/language-core/lib/plugins/file-html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const plugin: VueLanguagePlugin = ({ vueCompilerOptions }) => {
descriptor: {
filename: fileName,
source: content,
comments: [],
template: null,
script: null,
scriptSetup: null,
Expand Down
20 changes: 14 additions & 6 deletions packages/language-core/lib/plugins/vue-tsx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import { generateScript } from '../codegen/script';
import { generateTemplate } from '../codegen/template';
import { parseScriptRanges } from '../parsers/scriptRanges';
import { parseScriptSetupRanges } from '../parsers/scriptSetupRanges';
import { parseVueCompilerOptions } from '../parsers/vueCompilerOptions';
import type { Code, Sfc, VueLanguagePlugin } from '../types';
import { resolveVueCompilerOptions } from '../utils/ts';

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

Expand Down Expand Up @@ -77,14 +79,20 @@ function createTsx(
: _sfc.script && _sfc.script.lang !== 'js' ? _sfc.script.lang
: 'js';
});
const vueCompilerOptions = computed(() => {
const options = parseVueCompilerOptions(_sfc);
return options
? resolveVueCompilerOptions(options, ctx.vueCompilerOptions)
: ctx.vueCompilerOptions;
});
const scriptRanges = computed(() =>
_sfc.script
? parseScriptRanges(ts, _sfc.script.ast, !!_sfc.scriptSetup, false)
: undefined
);
const scriptSetupRanges = computed(() =>
_sfc.scriptSetup
? parseScriptSetupRanges(ts, _sfc.scriptSetup.ast, ctx.vueCompilerOptions)
? parseScriptSetupRanges(ts, _sfc.scriptSetup.ast, vueCompilerOptions.get())
: undefined
);
const scriptSetupBindingNames = Unstable.computedSet(
Expand Down Expand Up @@ -134,17 +142,17 @@ function createTsx(
});
const generatedTemplate = computed(() => {

if (ctx.vueCompilerOptions.skipTemplateCodegen || !_sfc.template) {
if (vueCompilerOptions.get().skipTemplateCodegen || !_sfc.template) {
return;
}

const codes: Code[] = [];
const codegen = generateTemplate({
ts,
compilerOptions: ctx.compilerOptions,
vueCompilerOptions: ctx.vueCompilerOptions,
vueCompilerOptions: vueCompilerOptions.get(),
template: _sfc.template,
edited: ctx.vueCompilerOptions.__test || (fileEditTimes.get(fileName) ?? 0) >= 2,
edited: vueCompilerOptions.get().__test || (fileEditTimes.get(fileName) ?? 0) >= 2,
scriptSetupBindingNames: scriptSetupBindingNames.get(),
scriptSetupImportComponentNames: scriptSetupImportComponentNames.get(),
destructuredPropNames: destructuredPropNames.get(),
Expand Down Expand Up @@ -175,9 +183,9 @@ function createTsx(
const codegen = generateScript({
ts,
compilerOptions: ctx.compilerOptions,
vueCompilerOptions: ctx.vueCompilerOptions,
vueCompilerOptions: vueCompilerOptions.get(),
sfc: _sfc,
edited: ctx.vueCompilerOptions.__test || (fileEditTimes.get(fileName) ?? 0) >= 2,
edited: vueCompilerOptions.get().__test || (fileEditTimes.get(fileName) ?? 0) >= 2,
fileName,
lang: lang.get(),
scriptRanges: scriptRanges.get(),
Expand Down
1 change: 1 addition & 0 deletions packages/language-core/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ export interface SfcBlock {

export interface Sfc {
content: string;
comments: string[],
template: SfcBlock & {
ast: CompilerDOM.RootNode | undefined;
errors: CompilerDOM.CompilerError[];
Expand Down
13 changes: 12 additions & 1 deletion packages/language-core/lib/utils/parseSfc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ import type { ElementNode, SourceLocation } from '@vue/compiler-dom';
import * as compiler from '@vue/compiler-dom';
import type { CompilerError, SFCBlock, SFCDescriptor, SFCParseResult, SFCScriptBlock, SFCStyleBlock, SFCTemplateBlock } from '@vue/compiler-sfc';

declare module '@vue/compiler-sfc' {
interface SFCDescriptor {
comments: string[];
}
}

export function parse(source: string): SFCParseResult {

const errors: CompilerError[] = [];
Expand All @@ -19,6 +25,7 @@ export function parse(source: string): SFCParseResult {
const descriptor: SFCDescriptor = {
filename: 'anonymous.vue',
source,
comments: [],
template: null,
script: null,
scriptSetup: null,
Expand All @@ -29,7 +36,11 @@ export function parse(source: string): SFCParseResult {
shouldForceReload: () => false,
};
ast.children.forEach(node => {
if (node.type !== compiler.NodeTypes.ELEMENT) {
if (node.type === compiler.NodeTypes.COMMENT) {
descriptor.comments.push(node.content);
return;
}
else if (node.type !== compiler.NodeTypes.ELEMENT) {
return;
}
switch (node.tag) {
Expand Down
65 changes: 39 additions & 26 deletions packages/language-core/lib/utils/ts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,27 +220,24 @@ function getPartialVueCompilerOptions(
}
}

export function resolveVueCompilerOptions(vueOptions: Partial<VueCompilerOptions>): VueCompilerOptions {
const target = vueOptions.target ?? 3.3;
const lib = vueOptions.lib ?? 'vue';
function getDefaultOptions(options: Partial<VueCompilerOptions>): VueCompilerOptions {
const target = options.target ?? 3.3;
const lib = options.lib ?? 'vue';
return {
...vueOptions,
target,
extensions: vueOptions.extensions ?? ['.vue'],
vitePressExtensions: vueOptions.vitePressExtensions ?? [],
petiteVueExtensions: vueOptions.petiteVueExtensions ?? [],
lib,
jsxSlots: vueOptions.jsxSlots ?? false,
strictTemplates: vueOptions.strictTemplates ?? false,
skipTemplateCodegen: vueOptions.skipTemplateCodegen ?? false,
fallthroughAttributes: vueOptions.fallthroughAttributes ?? false,
dataAttributes: vueOptions.dataAttributes ?? [],
htmlAttributes: vueOptions.htmlAttributes ?? ['aria-*'],
optionsWrapper: vueOptions.optionsWrapper ?? (
target >= 2.7
? [`(await import('${lib}')).defineComponent(`, `)`]
: [`(await import('${lib}')).default.extend(`, `)`]
),
extensions: ['.vue'],
vitePressExtensions: [],
petiteVueExtensions: [],
jsxSlots: false,
strictTemplates: false,
skipTemplateCodegen: false,
fallthroughAttributes: false,
dataAttributes: [],
htmlAttributes: ['aria-*'],
optionsWrapper: target >= 2.7
? [`(await import('${lib}')).defineComponent(`, `)`]
: [`(await import('${lib}')).default.extend(`, `)`],
macros: {
defineProps: ['defineProps'],
defineSlots: ['defineSlots'],
Expand All @@ -249,24 +246,40 @@ export function resolveVueCompilerOptions(vueOptions: Partial<VueCompilerOptions
defineModel: ['defineModel'],
defineOptions: ['defineOptions'],
withDefaults: ['withDefaults'],
...vueOptions.macros,
},
composables: {
useAttrs: ['useAttrs'],
useCssModule: ['useCssModule'],
useSlots: ['useSlots'],
useTemplateRef: ['useTemplateRef', 'templateRef'],
...vueOptions.composables,
},
plugins: vueOptions.plugins ?? [],
plugins: [],
experimentalDefinePropProposal: false,
experimentalResolveStyleCssClasses: 'scoped',
experimentalModelPropName: null!
}
};

export function resolveVueCompilerOptions(
options: Partial<VueCompilerOptions>,
defaults: VueCompilerOptions = getDefaultOptions(options)
): VueCompilerOptions {
return {
...defaults,
...options,
macros: {
...defaults.macros,
...options.macros,
},
composables: {
...defaults.composables,
...options.composables,
},

// experimental
experimentalDefinePropProposal: vueOptions.experimentalDefinePropProposal ?? false,
experimentalResolveStyleCssClasses: vueOptions.experimentalResolveStyleCssClasses ?? 'scoped',
// https://github.com/vuejs/vue-next/blob/master/packages/compiler-dom/src/transforms/vModel.ts#L49-L51
// https://vuejs.org/guide/essentials/forms.html#form-input-bindings
experimentalModelPropName: Object.fromEntries(Object.entries(
vueOptions.experimentalModelPropName ?? {
options.experimentalModelPropName ?? defaults.experimentalModelPropName ?? {
'': {
input: true
},
Expand Down Expand Up @@ -301,4 +314,4 @@ export function setupGlobalTypes(rootDir: string, vueOptions: VueCompilerOptions
host.writeFile(globalTypesPath, globalTypesContents);
return { absolutePath: globalTypesPath };
} catch { }
}
}
4 changes: 4 additions & 0 deletions packages/language-core/lib/virtualFile/computedSfc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ export function computedSfc(
const content = computed(() => {
return snapshot.get().getText(0, snapshot.get().getLength());
});
const comments = computed(() => {
return parsed.get()?.descriptor.comments ?? [];
});
const template = computedNullableSfcBlock(
'template',
'html',
Expand Down Expand Up @@ -149,6 +152,7 @@ export function computedSfc(

return {
get content() { return content.get(); },
get comments() { return comments.get(); },
get template() { return template.get(); },
get script() { return script.get(); },
get scriptSetup() { return scriptSetup.get(); },
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<!-- @experimentalDefinePropProposal "johnsonEdition" -->

<script setup lang="ts" generic="T">
// @experimentalDefinePropProposal=johnsonEdition
import { exactType } from '../../shared';

const a = defineProp<T>();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<!-- @experimentalDefinePropProposal "johnsonEdition" -->

<script setup lang="ts">
// @experimentalDefinePropProposal=johnsonEdition
import { exactType } from '../../shared';

const a = defineProp<string>();
Expand Down
4 changes: 2 additions & 2 deletions test-workspace/tsc/passedFixtures/vue3/#3548/main.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts" generic="T">
// @experimentalDefinePropProposal=johnsonEdition
<!-- @experimentalDefinePropProposal "johnsonEdition" -->

<script setup lang="ts" generic="T">
defineProp<T>();
</script>
3 changes: 2 additions & 1 deletion test-workspace/tsc/passedFixtures/vue3/#4649/prop-comp.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<!-- @experimentalDefinePropProposal "kevinEdition" -->

<script lang="ts" setup>
// @experimentalDefinePropProposal=kevinEdition
import { exactType } from '../../shared';

const fooAlias = defineProp('foo', {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<!-- @experimentalDefinePropProposal "kevinEdition" -->

<script setup lang="ts">
// @experimentalDefinePropProposal=kevinEdition
import { exactType } from '../../shared';

interface Qux { qux: true };
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<!-- @experimentalDefinePropProposal "johnsonEdition" -->

<script setup lang="ts" generic="T">
// @experimentalDefinePropProposal=johnsonEdition
import { exactType } from '../../shared';

const a = defineProp<T>();
Expand Down
Loading
Loading