diff --git a/packages/language-core/lib/parsers/vueCompilerOptions.ts b/packages/language-core/lib/parsers/vueCompilerOptions.ts index 89450c89bd..33747b84e6 100644 --- a/packages/language-core/lib/parsers/vueCompilerOptions.ts +++ b/packages/language-core/lib/parsers/vueCompilerOptions.ts @@ -1,8 +1,8 @@ -import type { VueCompilerOptions } from '../types'; +import type { RawVueCompilerOptions } from '../types'; const syntaxReg = /^\s*@(?.+?)\s+(?.+?)\s*$/m; -export function parseVueCompilerOptions(comments: string[]): Partial | undefined { +export function parseVueCompilerOptions(comments: string[]): RawVueCompilerOptions | undefined { const entries = comments .map(text => { try { diff --git a/packages/language-core/lib/plugins/vue-tsx.ts b/packages/language-core/lib/plugins/vue-tsx.ts index 48d33f31b6..7bf5e75dfc 100644 --- a/packages/language-core/lib/plugins/vue-tsx.ts +++ b/packages/language-core/lib/plugins/vue-tsx.ts @@ -8,7 +8,7 @@ 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'; +import { CompilerOptionsResolver } from '../utils/ts'; export const tsCodegen = new WeakMap>(); @@ -83,9 +83,12 @@ function createTsx( }); const vueCompilerOptions = computed(() => { const options = parseVueCompilerOptions(_sfc.comments); - return options - ? resolveVueCompilerOptions(options, ctx.vueCompilerOptions) - : ctx.vueCompilerOptions; + if (options) { + const resolver = new CompilerOptionsResolver(); + resolver.addConfig(options, path.dirname(fileName)); + return resolver.build(ctx.vueCompilerOptions); + } + return ctx.vueCompilerOptions; }); const scriptRanges = computed(() => _sfc.script diff --git a/packages/language-core/lib/types.ts b/packages/language-core/lib/types.ts index 6d1e05cac5..6728cbad41 100644 --- a/packages/language-core/lib/types.ts +++ b/packages/language-core/lib/types.ts @@ -10,7 +10,7 @@ export type { SFCParseResult } from '@vue/compiler-sfc'; export { VueEmbeddedCode }; export type RawVueCompilerOptions = Partial> & { - target?: 'auto' | 2 | 2.7 | 3 | 3.3; + target?: 'auto' | 2 | 2.7 | 3 | 3.3 | 3.5 | 99 | number; plugins?: string[]; }; diff --git a/packages/language-core/lib/utils/ts.ts b/packages/language-core/lib/utils/ts.ts index ff9e3471a1..c4cf75d586 100644 --- a/packages/language-core/lib/utils/ts.ts +++ b/packages/language-core/lib/utils/ts.ts @@ -23,18 +23,19 @@ export function createParsedCommandLineByJson( const proxyHost = proxyParseConfigHostForExtendConfigPaths(parseConfigHost); ts.parseJsonConfigFileContent(json, proxyHost.host, rootDir, {}, configFileName); - let vueOptions: Partial = {}; + const resolver = new CompilerOptionsResolver(); for (const extendPath of proxyHost.extendConfigPaths.reverse()) { try { - vueOptions = { - ...vueOptions, - ...getPartialVueCompilerOptions(ts, ts.readJsonConfigFile(extendPath, proxyHost.host.readFile)), - }; + const configFile = ts.readJsonConfigFile(extendPath, proxyHost.host.readFile); + const obj = ts.convertToObject(configFile, []); + const rawOptions: RawVueCompilerOptions = obj?.vueCompilerOptions ?? {}; + resolver.addConfig(rawOptions, path.dirname(configFile.fileName)); } catch (err) { } } - const resolvedVueOptions = resolveVueCompilerOptions(vueOptions); + const resolvedVueOptions = resolver.build(); + if (skipGlobalTypesSetup) { resolvedVueOptions.__setupedGlobalTypes = true; } @@ -78,18 +79,19 @@ export function createParsedCommandLine( const config = ts.readJsonConfigFile(tsConfigPath, proxyHost.host.readFile); ts.parseJsonSourceFileConfigFileContent(config, proxyHost.host, path.dirname(tsConfigPath), {}, tsConfigPath); - let vueOptions: Partial = {}; + const resolver = new CompilerOptionsResolver(); for (const extendPath of proxyHost.extendConfigPaths.reverse()) { try { - vueOptions = { - ...vueOptions, - ...getPartialVueCompilerOptions(ts, ts.readJsonConfigFile(extendPath, proxyHost.host.readFile)), - }; + const configFile = ts.readJsonConfigFile(extendPath, proxyHost.host.readFile); + const obj = ts.convertToObject(configFile, []); + const rawOptions: RawVueCompilerOptions = obj?.vueCompilerOptions ?? {}; + resolver.addConfig(rawOptions, path.dirname(configFile.fileName)); } catch (err) { } } - const resolvedVueOptions = resolveVueCompilerOptions(vueOptions); + const resolvedVueOptions = resolver.build(); + if (skipGlobalTypesSetup) { resolvedVueOptions.__setupedGlobalTypes = true; } @@ -126,7 +128,7 @@ export function createParsedCommandLine( return { fileNames: [], options: {}, - vueOptions: resolveVueCompilerOptions({}), + vueOptions: getDefaultCompilerOptions(), errors: [], }; } @@ -153,76 +155,115 @@ function proxyParseConfigHostForExtendConfigPaths(parseConfigHost: ts.ParseConfi }; } -function getPartialVueCompilerOptions( - ts: typeof import('typescript'), - tsConfigSourceFile: ts.TsConfigSourceFile -) { - - const folder = path.dirname(tsConfigSourceFile.fileName); - const obj = ts.convertToObject(tsConfigSourceFile, []); - const rawOptions: RawVueCompilerOptions = obj?.vueCompilerOptions ?? {}; - const result: Partial = { - ...rawOptions as any, - }; - const target = rawOptions.target ?? 'auto'; +export class CompilerOptionsResolver { + options: Omit = {}; + fallbackTarget: number | undefined; + target: number | undefined; + plugins: VueLanguagePlugin[] = []; - if (target === 'auto') { - const resolvedPath = resolvePath('vue/package.json'); - if (resolvedPath) { - const vuePackageJson = require(resolvedPath); - const versionNumbers = vuePackageJson.version.split('.'); - result.target = Number(versionNumbers[0] + '.' + versionNumbers[1]); + addConfig(options: RawVueCompilerOptions, rootDir: string) { + for (const key in options) { + switch (key) { + case 'target': + const target = options.target!; + if (typeof target === 'string') { + this.target = findVueVersion(rootDir); + } + else { + this.target = target; + } + break; + case 'plugins': + this.plugins = (options.plugins ?? []) + .map((pluginPath: string) => { + try { + const resolvedPath = resolvePath(pluginPath, rootDir); + if (resolvedPath) { + const plugin = require(resolvedPath); + plugin.__moduleName = pluginPath; + return plugin; + } + else { + console.warn('[Vue] Load plugin failed:', pluginPath); + } + } + catch (error) { + console.warn('[Vue] Resolve plugin path failed:', pluginPath, error); + } + return []; + }); + break; + default: + // @ts-expect-error + this.options[key] = options[key]; + break; + } } - else { - // console.warn('Load vue/package.json failed from', folder); + if (this.target === undefined) { + this.fallbackTarget = findVueVersion(rootDir); } } - else { - result.target = target; - } - if (rawOptions.plugins) { - const plugins = rawOptions.plugins - .map((pluginPath: string) => { - try { - const resolvedPath = resolvePath(pluginPath); - if (resolvedPath) { - const plugin = require(resolvedPath); - plugin.__moduleName = pluginPath; - return plugin; - } - else { - console.warn('[Vue] Load plugin failed:', pluginPath); + + build(defaults?: VueCompilerOptions): VueCompilerOptions { + const target = this.target ?? this.fallbackTarget; + defaults ??= getDefaultCompilerOptions(target, this.options.lib); + return { + ...defaults, + ...this.options, + plugins: this.plugins, + macros: { + ...defaults.macros, + ...this.options.macros, + }, + composables: { + ...defaults.composables, + ...this.options.composables, + }, + // 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( + this.options.experimentalModelPropName ?? defaults.experimentalModelPropName ?? { + '': { + input: true + }, + value: { + input: { type: 'text' }, + textarea: true, + select: true } } - catch (error) { - console.warn('[Vue] Resolve plugin path failed:', pluginPath, error); - } - return []; - }); - - result.plugins = plugins; + ).map(([k, v]) => [camelize(k), v])), + }; } +} - return result; +function findVueVersion(rootDir: string) { + const resolvedPath = resolvePath('vue/package.json', rootDir); + if (resolvedPath) { + const vuePackageJson = require(resolvedPath); + const versionNumbers = vuePackageJson.version.split('.'); + return Number(versionNumbers[0] + '.' + versionNumbers[1]); + } + else { + // console.warn('Load vue/package.json failed from', folder); + } +} - function resolvePath(scriptPath: string) { - try { - if (require?.resolve) { - return require.resolve(scriptPath, { paths: [folder] }); - } - else { - // console.warn('failed to resolve path:', scriptPath, 'require.resolve is not supported in web'); - } +function resolvePath(scriptPath: string, root: string) { + try { + if (require?.resolve) { + return require.resolve(scriptPath, { paths: [root] }); } - catch (error) { - // console.warn(error); + else { + // console.warn('failed to resolve path:', scriptPath, 'require.resolve is not supported in web'); } } + catch (error) { + // console.warn(error); + } } -function getDefaultOptions(options: Partial): VueCompilerOptions { - const target = options.target ?? 3.3; - const lib = options.lib ?? 'vue'; +export function getDefaultCompilerOptions(target = 99, lib = 'vue'): VueCompilerOptions { return { target, lib, @@ -258,38 +299,15 @@ function getDefaultOptions(options: Partial): VueCompilerOpt experimentalResolveStyleCssClasses: 'scoped', experimentalModelPropName: null! }; -}; +} -export function resolveVueCompilerOptions( - options: Partial, - defaults: VueCompilerOptions = getDefaultOptions(options) -): VueCompilerOptions { +/** + * @deprecated use `getDefaultCompilerOptions` instead + */ +export function resolveVueCompilerOptions(options: Partial): VueCompilerOptions { return { - ...defaults, + ...getDefaultCompilerOptions(options.target, options.lib), ...options, - macros: { - ...defaults.macros, - ...options.macros, - }, - composables: { - ...defaults.composables, - ...options.composables, - }, - - // 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( - options.experimentalModelPropName ?? defaults.experimentalModelPropName ?? { - '': { - input: true - }, - value: { - input: { type: 'text' }, - textarea: true, - select: true - } - } - ).map(([k, v]) => [camelize(k), v])), }; } diff --git a/packages/language-server/lib/initialize.ts b/packages/language-server/lib/initialize.ts index 88c5c626f1..2d33454b3a 100644 --- a/packages/language-server/lib/initialize.ts +++ b/packages/language-server/lib/initialize.ts @@ -1,6 +1,6 @@ import type { LanguageServer } from '@volar/language-server'; import { createTypeScriptProject } from '@volar/language-server/node'; -import { createParsedCommandLine, createVueLanguagePlugin, generateGlobalTypes, getAllExtensions, resolveVueCompilerOptions, VueCompilerOptions } from '@vue/language-core'; +import { createParsedCommandLine, createVueLanguagePlugin, generateGlobalTypes, getAllExtensions, getDefaultCompilerOptions, VueCompilerOptions } from '@vue/language-core'; import { Disposable, getFullLanguageServicePlugins, InitializeParams } from '@vue/language-service'; import type * as ts from 'typescript'; @@ -35,7 +35,7 @@ export function initialize( } else { compilerOptions = ts.getDefaultCompilerOptions(); - vueCompilerOptions = resolveVueCompilerOptions({}); + vueCompilerOptions = getDefaultCompilerOptions(); } vueCompilerOptions.__test = params.initializationOptions.typescript.disableAutoImportCache; updateFileWatcher(vueCompilerOptions); diff --git a/packages/language-server/node.ts b/packages/language-server/node.ts index 24c37db0ec..20ced42547 100644 --- a/packages/language-server/node.ts +++ b/packages/language-server/node.ts @@ -1,5 +1,5 @@ import { createConnection, createServer, loadTsdkByPath } from '@volar/language-server/node'; -import { createParsedCommandLine, createVueLanguagePlugin, resolveVueCompilerOptions } from '@vue/language-core'; +import { createParsedCommandLine, createVueLanguagePlugin, getDefaultCompilerOptions } from '@vue/language-core'; import { getHybridModeLanguageServicePlugins } from '@vue/language-service'; import * as namedPipeClient from '@vue/typescript-plugin/lib/client'; import { createHybridModeProject } from './lib/hybridModeProject'; @@ -23,7 +23,7 @@ connection.onInitialize(params => { const commandLine = configFileName ? createParsedCommandLine(ts, ts.sys, configFileName) : { - vueOptions: resolveVueCompilerOptions({}), + vueOptions: getDefaultCompilerOptions(), options: ts.getDefaultCompilerOptions(), }; commandLine.vueOptions.__test = params.initializationOptions.typescript.disableAutoImportCache; diff --git a/packages/language-service/tests/utils/format.ts b/packages/language-service/tests/utils/format.ts index 5f6eb36a72..754b4ce463 100644 --- a/packages/language-service/tests/utils/format.ts +++ b/packages/language-service/tests/utils/format.ts @@ -2,9 +2,9 @@ import * as kit from '@volar/kit'; import * as ts from 'typescript'; import { describe, expect, it } from 'vitest'; import type { URI } from 'vscode-uri'; -import { createVueLanguagePlugin, getFullLanguageServicePlugins, resolveVueCompilerOptions } from '../..'; +import { createVueLanguagePlugin, getDefaultCompilerOptions, getFullLanguageServicePlugins } from '../..'; -const resolvedVueOptions = resolveVueCompilerOptions({}); +const resolvedVueOptions = getDefaultCompilerOptions(); const vueLanguagePlugin = createVueLanguagePlugin( ts, {}, diff --git a/packages/tsc/index.ts b/packages/tsc/index.ts index 1ba58bdf9a..21dc1ae028 100644 --- a/packages/tsc/index.ts +++ b/packages/tsc/index.ts @@ -15,7 +15,7 @@ export function run(tscPath = require.resolve('typescript/lib/tsc')) { const { configFilePath } = options.options; const vueOptions = typeof configFilePath === 'string' ? vue.createParsedCommandLine(ts, ts.sys, configFilePath.replace(windowsPathReg, '/')).vueOptions - : vue.resolveVueCompilerOptions({}); + : vue.getDefaultCompilerOptions(); const allExtensions = vue.getAllExtensions(vueOptions); if ( runExtensions.length === allExtensions.length diff --git a/packages/tsc/tests/__snapshots__/dts.spec.ts.snap b/packages/tsc/tests/__snapshots__/dts.spec.ts.snap index 67887bcab3..e321fe1512 100644 --- a/packages/tsc/tests/__snapshots__/dts.spec.ts.snap +++ b/packages/tsc/tests/__snapshots__/dts.spec.ts.snap @@ -8,7 +8,7 @@ declare const _default: (__VLS_props: NonNullable & Omit<{} & import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, never>, never> & { nonGeneric: string; rows: Row[]; - } & Partial<{}>> & (import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps); + } & Partial<{}>> & import("vue").PublicProps; expose(exposed: import("vue").ShallowUnwrapRef<{}>): void; attrs: any; slots: { @@ -56,8 +56,8 @@ export default _default; `; exports[`vue-tsc-dts > Input: events/component-class.vue, Output: events/component-class.vue.d.ts 1`] = ` -"declare const _default: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, { - foo: (value: string) => void; +"declare const _default: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & { + foo: (value: string) => any; }, string, import("vue").PublicProps, Readonly<{}> & Readonly<{ onFoo?: (value: string) => any; }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>; @@ -69,7 +69,7 @@ exports[`vue-tsc-dts > Input: events/component-generic.vue, Output: events/compo "declare const _default: (__VLS_props: NonNullable>["props"], __VLS_ctx?: __VLS_PrettifyLocal>, "attrs" | "emit" | "slots">>, __VLS_expose?: NonNullable>["expose"], __VLS_setup?: Promise<{ props: __VLS_PrettifyLocal & Omit<{ readonly onFoo?: (value: string) => any; - } & import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, never>, "onFoo"> & {} & Partial<{}>> & (import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps); + } & import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, never>, "onFoo"> & {} & Partial<{}>> & import("vue").PublicProps; expose(exposed: import("vue").ShallowUnwrapRef<{}>): void; attrs: any; slots: {}; @@ -93,7 +93,7 @@ exports[`vue-tsc-dts > Input: generic/component.vue, Output: generic/component.v title?: string; } & { foo: number; - }) & Partial<{}>> & (import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps); + }) & Partial<{}>> & import("vue").PublicProps; expose(exposed: import("vue").ShallowUnwrapRef<{ baz: number; }>): void; @@ -127,7 +127,7 @@ exports[`vue-tsc-dts > Input: generic/custom-extension-component.cext, Output: g title?: string; } & { foo: number; - }) & Partial<{}>> & (import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps); + }) & Partial<{}>> & import("vue").PublicProps; expose(exposed: import("vue").ShallowUnwrapRef<{ baz: number; }>): void; @@ -252,15 +252,15 @@ export default _default; `; exports[`vue-tsc-dts > Input: reference-type-events/component.vue, Output: reference-type-events/component.vue.d.ts 1`] = ` -"declare const _default: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, { +"declare const _default: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & { foo: (data?: { foo: string; - }) => void; + }) => any; bar: (value: { arg1: number; arg2?: any; - }) => void; - baz: () => void; + }) => any; + baz: () => any; }, string, import("vue").PublicProps, Readonly<{}> & Readonly<{ onFoo?: (data?: { foo: string; @@ -312,58 +312,26 @@ exports[`vue-tsc-dts > Input: reference-type-model/component.vue, Output: refere "qux"?: string; 'quxModifiers'?: Partial>; }; -declare const _default: import("vue").DefineComponent>, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, { - "update:foo": (value: number) => void; - "update:bar": (value: string[]) => void; - "update:qux": (value: string) => void; -}, string, import("vue").PublicProps, Readonly>> & Readonly<{ +declare const _default: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, { + "update:foo": (value: number) => any; + "update:bar": (value: string[]) => any; + "update:qux": (value: string) => any; +}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{ "onUpdate:foo"?: (value: number) => any; "onUpdate:bar"?: (value: string[]) => any; "onUpdate:qux"?: (value: string) => any; }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>; export default _default; -type __VLS_NonUndefinedable = T extends undefined ? never : T; -type __VLS_TypePropsToOption = { - [K in keyof T]-?: {} extends Pick ? { - type: import('vue').PropType<__VLS_NonUndefinedable>; - } : { - type: import('vue').PropType; - required: true; - }; -}; " `; exports[`vue-tsc-dts > Input: reference-type-props/component.vue, Output: reference-type-props/component.vue.d.ts 1`] = ` "import { MyProps } from './my-props'; -declare const _default: import("vue").DefineComponent, { - bar: number; - baz: () => string[]; -}>>, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly, { - bar: number; - baz: () => string[]; -}>>> & Readonly<{}>, { +declare const _default: import("vue").DefineComponent & Readonly<{}>, { bar: number; baz: string[]; -}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>; +}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>; export default _default; -type __VLS_WithDefaults = { - [K in keyof Pick]: K extends keyof D ? __VLS_PrettifyLocal : P[K]; -}; -type __VLS_NonUndefinedable = T extends undefined ? never : T; -type __VLS_TypePropsToOption = { - [K in keyof T]-?: {} extends Pick ? { - type: import('vue').PropType<__VLS_NonUndefinedable>; - } : { - type: import('vue').PropType; - required: true; - }; -}; -type __VLS_PrettifyLocal = { - [K in keyof T]: T[K]; -} & {}; " `; diff --git a/packages/tsc/tests/dts.spec.ts b/packages/tsc/tests/dts.spec.ts index 50e65e887a..8d1d400bfd 100644 --- a/packages/tsc/tests/dts.spec.ts +++ b/packages/tsc/tests/dts.spec.ts @@ -32,7 +32,8 @@ describe('vue-tsc-dts', () => { vueOptions = vue.createParsedCommandLine(ts, ts.sys, configFilePath.replace(windowsPathReg, '/')).vueOptions; } else { - vueOptions = vue.resolveVueCompilerOptions({ extensions: ['.vue', '.cext'] }); + vueOptions = vue.getDefaultCompilerOptions(); + vueOptions.extensions = ['.vue', '.cext']; vueOptions.__setupedGlobalTypes = vue.setupGlobalTypes(workspace.replace(windowsPathReg, '/'), vueOptions, ts.sys); } const vueLanguagePlugin = vue.createVueLanguagePlugin(