diff --git a/extensions/vscode/syntaxes/vue.tmLanguage.json b/extensions/vscode/syntaxes/vue.tmLanguage.json index 71109bc144..05d00ef84d 100644 --- a/extensions/vscode/syntaxes/vue.tmLanguage.json +++ b/extensions/vscode/syntaxes/vue.tmLanguage.json @@ -3,6 +3,9 @@ "name": "Vue", "scopeName": "source.vue", "patterns": [ + { + "include": "#vue-comments" + }, { "include": "text.html.basic#comment" }, @@ -1278,6 +1281,39 @@ ] } ] + }, + "vue-comments": { + "patterns": [ + { + "include": "#vue-comments-key-value" + } + ] + }, + "vue-comments-key-value": { + "begin": "()", + "endCaptures": { + "1": { + "name": "punctuation.definition.comment.vue" + } + }, + "name": "comment.block.vue", + "patterns": [ + { + "include": "source.json#value" + } + ] } } } \ No newline at end of file diff --git a/packages/language-core/lib/codegen/script/index.ts b/packages/language-core/lib/codegen/script/index.ts index d805ec1ef3..17e939a046 100644 --- a/packages/language-core/lib/codegen/script/index.ts +++ b/packages/language-core/lib/codegen/script/index.ts @@ -84,7 +84,7 @@ export function* generateScript(options: ScriptCodegenOptions): Generator -): Generator { - 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 { + const definePropProposalA = options.vueCompilerOptions.experimentalDefinePropProposal === 'kevinEdition'; + const definePropProposalB = options.vueCompilerOptions.experimentalDefinePropProposal === 'johnsonEdition'; if (definePropProposalA || definePropProposalB) { yield `type __VLS_PropOptions = Exclude, import('${options.vueCompilerOptions.lib}').PropType>${endOfLine}`; diff --git a/packages/language-core/lib/parsers/scriptSetupRanges.ts b/packages/language-core/lib/parsers/scriptSetupRanges.ts index 7c1b40bd14..c084687599 100644 --- a/packages/language-core/lib/parsers/scriptSetupRanges.ts +++ b/packages/language-core/lib/parsers/scriptSetupRanges.ts @@ -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(); diff --git a/packages/language-core/lib/parsers/vueCompilerOptions.ts b/packages/language-core/lib/parsers/vueCompilerOptions.ts new file mode 100644 index 0000000000..5524ccad5e --- /dev/null +++ b/packages/language-core/lib/parsers/vueCompilerOptions.ts @@ -0,0 +1,22 @@ +import type { Sfc, VueCompilerOptions } from '../types'; + +const syntaxReg = /^\s*@(?.+?)\s+(?.+?)\s*$/m; + +export function parseVueCompilerOptions(sfc: Sfc): Partial | 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); + } +} diff --git a/packages/language-core/lib/plugins/file-html.ts b/packages/language-core/lib/plugins/file-html.ts index 4c548dcf40..60f9965b8c 100644 --- a/packages/language-core/lib/plugins/file-html.ts +++ b/packages/language-core/lib/plugins/file-html.ts @@ -29,6 +29,7 @@ const plugin: VueLanguagePlugin = ({ vueCompilerOptions }) => { descriptor: { filename: fileName, source: content, + comments: [], template: null, script: null, scriptSetup: null, diff --git a/packages/language-core/lib/plugins/vue-tsx.ts b/packages/language-core/lib/plugins/vue-tsx.ts index 65ba00edc4..fabc1f96c5 100644 --- a/packages/language-core/lib/plugins/vue-tsx.ts +++ b/packages/language-core/lib/plugins/vue-tsx.ts @@ -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>(); @@ -77,6 +79,12 @@ 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) @@ -84,7 +92,7 @@ function createTsx( ); const scriptSetupRanges = computed(() => _sfc.scriptSetup - ? parseScriptSetupRanges(ts, _sfc.scriptSetup.ast, ctx.vueCompilerOptions) + ? parseScriptSetupRanges(ts, _sfc.scriptSetup.ast, vueCompilerOptions.get()) : undefined ); const scriptSetupBindingNames = Unstable.computedSet( @@ -134,7 +142,7 @@ function createTsx( }); const generatedTemplate = computed(() => { - if (ctx.vueCompilerOptions.skipTemplateCodegen || !_sfc.template) { + if (vueCompilerOptions.get().skipTemplateCodegen || !_sfc.template) { return; } @@ -142,9 +150,9 @@ function createTsx( 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(), @@ -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(), diff --git a/packages/language-core/lib/types.ts b/packages/language-core/lib/types.ts index bcee20d441..fc2ae2d83d 100644 --- a/packages/language-core/lib/types.ts +++ b/packages/language-core/lib/types.ts @@ -105,6 +105,7 @@ export interface SfcBlock { export interface Sfc { content: string; + comments: string[], template: SfcBlock & { ast: CompilerDOM.RootNode | undefined; errors: CompilerDOM.CompilerError[]; diff --git a/packages/language-core/lib/utils/parseSfc.ts b/packages/language-core/lib/utils/parseSfc.ts index b4c032e9b1..750297cd23 100644 --- a/packages/language-core/lib/utils/parseSfc.ts +++ b/packages/language-core/lib/utils/parseSfc.ts @@ -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[] = []; @@ -19,6 +25,7 @@ export function parse(source: string): SFCParseResult { const descriptor: SFCDescriptor = { filename: 'anonymous.vue', source, + comments: [], template: null, script: null, scriptSetup: null, @@ -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) { diff --git a/packages/language-core/lib/utils/ts.ts b/packages/language-core/lib/utils/ts.ts index e3607ba6bb..103c045728 100644 --- a/packages/language-core/lib/utils/ts.ts +++ b/packages/language-core/lib/utils/ts.ts @@ -220,27 +220,24 @@ function getPartialVueCompilerOptions( } } -export function resolveVueCompilerOptions(vueOptions: Partial): VueCompilerOptions { - const target = vueOptions.target ?? 3.3; - const lib = vueOptions.lib ?? 'vue'; +function getDefaultOptions(options: Partial): 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'], @@ -249,24 +246,40 @@ export function resolveVueCompilerOptions(vueOptions: Partial, + 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 }, @@ -301,4 +314,4 @@ export function setupGlobalTypes(rootDir: string, vueOptions: VueCompilerOptions host.writeFile(globalTypesPath, globalTypesContents); return { absolutePath: globalTypesPath }; } catch { } -} \ No newline at end of file +} diff --git a/packages/language-core/lib/virtualFile/computedSfc.ts b/packages/language-core/lib/virtualFile/computedSfc.ts index beb3279614..d0965e1e5f 100644 --- a/packages/language-core/lib/virtualFile/computedSfc.ts +++ b/packages/language-core/lib/virtualFile/computedSfc.ts @@ -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', @@ -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(); }, diff --git a/test-workspace/tsc/passedFixtures/vue3.4/defineProp_B/script-setup-generic.vue b/test-workspace/tsc/passedFixtures/vue3.4/defineProp_B/script-setup-generic.vue index 87f322d69d..4917e39f20 100644 --- a/test-workspace/tsc/passedFixtures/vue3.4/defineProp_B/script-setup-generic.vue +++ b/test-workspace/tsc/passedFixtures/vue3.4/defineProp_B/script-setup-generic.vue @@ -1,5 +1,6 @@ + + diff --git a/test-workspace/tsc/passedFixtures/vue3/#4649/prop-comp.vue b/test-workspace/tsc/passedFixtures/vue3/#4649/prop-comp.vue index 3f483c1427..cdb53572e6 100644 --- a/test-workspace/tsc/passedFixtures/vue3/#4649/prop-comp.vue +++ b/test-workspace/tsc/passedFixtures/vue3/#4649/prop-comp.vue @@ -1,5 +1,6 @@ + +