From 93a75dee28f6fb1b5cad780efbb9d852c8147a9d Mon Sep 17 00:00:00 2001 From: KazariEX <1364035137@qq.com> Date: Tue, 5 Nov 2024 19:42:55 +0800 Subject: [PATCH 1/8] feat: support the use of leading JSDoc to configure `vueCompilerOptions` --- .../language-core/lib/codegen/script/index.ts | 13 ++- .../lib/parsers/scriptSetupRanges.ts | 4 +- .../lib/parsers/vueCompilerOptions.ts | 42 +++++++++ packages/language-core/lib/plugins/vue-tsx.ts | 20 +++-- packages/language-core/lib/utils/ts.ts | 85 +++++++++++++------ .../defineProp_B/script-setup-generic.vue | 2 +- .../vue3.4/defineProp_B/script-setup.vue | 2 +- .../tsc/passedFixtures/vue3/#3548/main.vue | 2 +- .../passedFixtures/vue3/#4649/prop-comp.vue | 2 +- .../vue3/defineProp_A/script-setup.vue | 2 +- .../defineProp_B/script-setup-generic.vue | 2 +- .../vue3/defineProp_B/script-setup.vue | 2 +- 12 files changed, 129 insertions(+), 49 deletions(-) create mode 100644 packages/language-core/lib/parsers/vueCompilerOptions.ts 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 433674d2f1..880661e2ef 100644 --- a/packages/language-core/lib/parsers/scriptSetupRanges.ts +++ b/packages/language-core/lib/parsers/scriptSetupRanges.ts @@ -55,8 +55,8 @@ export function parseScriptSetupRanges( name?: string; define: ReturnType; }[] = []; - 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 defineProp: { localName: TextRange | undefined; name: TextRange | undefined; diff --git a/packages/language-core/lib/parsers/vueCompilerOptions.ts b/packages/language-core/lib/parsers/vueCompilerOptions.ts new file mode 100644 index 0000000000..9b755acd4c --- /dev/null +++ b/packages/language-core/lib/parsers/vueCompilerOptions.ts @@ -0,0 +1,42 @@ +import * as ts from 'typescript'; +import { Sfc, VueCompilerOptions } from '../types'; + +export function parseCompilerOptions( + ts: typeof import('typescript'), + sfc: Sfc +): Partial | undefined { + const entries = [ + ...getLeadingComments(sfc.script?.ast), + ...getLeadingComments(sfc.scriptSetup?.ast) + ] + + if (entries.length) { + return Object.fromEntries(entries); + } + + return Object.fromEntries([ + ...getLeadingComments(sfc.script?.ast), + ...getLeadingComments(sfc.scriptSetup?.ast), + ]); + + function getLeadingComments(ast: ts.SourceFile | undefined) { + if (!ast) { + return []; + } + const ranges = ts.getLeadingCommentRanges(ast.text, 0) ?? []; + return ranges + .filter(range => range.kind === 3 satisfies ts.SyntaxKind.MultiLineCommentTrivia) + .map(range => { + try { + const text = ast.text.slice(range.pos, range.end) + const match = text.match(/^\/\*\*\s*@vue\$(?.+) (?.+)\s*\*\/$/); + if (match) { + const { key, value } = match.groups ?? {}; + return [key, JSON.parse(value)] as const; + } + } + catch { }; + }) + .filter(item => !!item); + } +} diff --git a/packages/language-core/lib/plugins/vue-tsx.ts b/packages/language-core/lib/plugins/vue-tsx.ts index 43cad99a2b..582d63623a 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 { parseCompilerOptions } 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 = parseCompilerOptions(ts, _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/utils/ts.ts b/packages/language-core/lib/utils/ts.ts index 4b8ebda122..95f6b63fe7 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,22 +246,38 @@ export function resolveVueCompilerOptions(vueOptions: Partial, + defaults: VueCompilerOptions = getDefaultOptions(options) +): VueCompilerOptions { + return { + ...defaults, + ...options, + macros: { + ...defaults.macros, + ...options.macros, + }, + composibles: { + ...defaults.composibles, + ...options.composibles, + }, - // 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 }, @@ -299,4 +312,24 @@ export function setupGlobalTypes(rootDir: string, vueOptions: VueCompilerOptions host.writeFile(globalTypesPath, globalTypesContents); return { absolutePath: globalTypesPath }; } catch { } -} \ No newline at end of file +} + +// isTrueLiteral is missing in tsc +export function isTrueLiteral(node: ts.Node): node is ts.TrueLiteral { + return node.kind === 112 satisfies ts.SyntaxKind.TrueKeyword; +} + +// isFalseLiteral is missing in tsc +export function isFalseLiteral(node: ts.Node): node is ts.FalseLiteral { + return node.kind === 97 satisfies ts.SyntaxKind.FalseKeyword; +} + +// isTemplateExpression is missing in tsc +export function isTemplateExpression(node: ts.Node): node is ts.TemplateExpression { + return node.kind === 228 satisfies ts.SyntaxKind.TemplateExpression; +} + +// isAsExpression is missing in tsc +export function isAsExpression(node: ts.Node): node is ts.AsExpression { + return node.kind === 234 satisfies ts.SyntaxKind.AsExpression; +} 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..050384e885 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,5 @@ diff --git a/test-workspace/tsc/passedFixtures/vue3/#4649/prop-comp.vue b/test-workspace/tsc/passedFixtures/vue3/#4649/prop-comp.vue index 3f483c1427..5d6452cabc 100644 --- a/test-workspace/tsc/passedFixtures/vue3/#4649/prop-comp.vue +++ b/test-workspace/tsc/passedFixtures/vue3/#4649/prop-comp.vue @@ -1,5 +1,5 @@ diff --git a/test-workspace/tsc/passedFixtures/vue3/#4649/prop-comp.vue b/test-workspace/tsc/passedFixtures/vue3/#4649/prop-comp.vue index 5d6452cabc..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 @@ + +