diff --git a/packages/component-meta/lib/base.ts b/packages/component-meta/lib/base.ts index c65a7cf43d..5d8ff7ec85 100644 --- a/packages/component-meta/lib/base.ts +++ b/packages/component-meta/lib/base.ts @@ -138,8 +138,8 @@ export function baseCreate( const directoryExists = languageServiceHost.directoryExists?.bind(languageServiceHost); const fileExists = languageServiceHost.fileExists.bind(languageServiceHost); const getScriptSnapshot = languageServiceHost.getScriptSnapshot.bind(languageServiceHost); - const globalTypesName = `${commandLine.vueOptions.lib}_${commandLine.vueOptions.target}_${commandLine.vueOptions.strictTemplates}.d.ts`; - const globalTypesContents = `// @ts-nocheck\nexport {};\n` + vue.generateGlobalTypes(commandLine.vueOptions.lib, commandLine.vueOptions.target, commandLine.vueOptions.strictTemplates); + const globalTypesName = vue.getGlobalTypesFileName(commandLine.vueOptions); + const globalTypesContents = `// @ts-nocheck\nexport {};\n` + vue.generateGlobalTypes(commandLine.vueOptions); const globalTypesSnapshot: ts.IScriptSnapshot = { getText: (start, end) => globalTypesContents.slice(start, end), getLength: () => globalTypesContents.length, diff --git a/packages/language-core/lib/codegen/globalTypes.ts b/packages/language-core/lib/codegen/globalTypes.ts index 8e7b3c65f6..d08d8b02d9 100644 --- a/packages/language-core/lib/codegen/globalTypes.ts +++ b/packages/language-core/lib/codegen/globalTypes.ts @@ -1,7 +1,24 @@ +import type { VueCompilerOptions } from '../types'; import { getSlotsPropertyName } from '../utils/shared'; -export function generateGlobalTypes(lib: string, target: number, strictTemplates: boolean) { - const fnPropsType = `(K extends { $props: infer Props } ? Props : any)${strictTemplates ? '' : ' & Record'}`; +export function getGlobalTypesFileName(options: VueCompilerOptions) { + return [ + options.lib, + options.target, + options.checkUnknownProps, + options.checkUnknownEvents, + options.checkUnknownComponents, + ].map(v => { + if (typeof v === 'boolean') { + return v ? 1 : 0; + } + return v; + }).join('_') + '.d.ts'; +} + +export function generateGlobalTypes(options: VueCompilerOptions) { + const { lib, target, checkUnknownProps, checkUnknownEvents, checkUnknownComponents } = options; + const fnPropsType = `(K extends { $props: infer Props } ? Props : any)${checkUnknownProps ? '' : ' & Record'}`; let text = ``; if (target < 3.5) { text += ` @@ -49,7 +66,7 @@ export function generateGlobalTypes(lib: string, target: number, strictTemplates N1 extends keyof __VLS_GlobalComponents ? N1 extends N0 ? Pick<__VLS_GlobalComponents, N0 extends keyof __VLS_GlobalComponents ? N0 : never> : { [K in N0]: __VLS_GlobalComponents[N1] } : N2 extends keyof __VLS_GlobalComponents ? N2 extends N0 ? Pick<__VLS_GlobalComponents, N0 extends keyof __VLS_GlobalComponents ? N0 : never> : { [K in N0]: __VLS_GlobalComponents[N2] } : N3 extends keyof __VLS_GlobalComponents ? N3 extends N0 ? Pick<__VLS_GlobalComponents, N0 extends keyof __VLS_GlobalComponents ? N0 : never> : { [K in N0]: __VLS_GlobalComponents[N3] } : - ${strictTemplates ? '{}' : '{ [K in N0]: unknown }'}; + ${checkUnknownComponents ? '{}' : '{ [K in N0]: unknown }'}; type __VLS_FunctionalComponentProps = '__ctx' extends keyof __VLS_PickNotAny ? K extends { __ctx?: { props?: infer P } } ? NonNullable

: never : T extends (props: infer P, ...args: any) => any ? P : @@ -69,7 +86,7 @@ export function generateGlobalTypes(lib: string, target: number, strictTemplates : __VLS_IsFunction extends true ? { [K in onEvent]?: Events[CamelizedEvent] } : Props - )${strictTemplates ? '' : ' & Record'}; + )${checkUnknownEvents ? '' : ' & Record'}; // fix https://github.com/vuejs/language-tools/issues/926 type __VLS_UnionToIntersection = (U extends unknown ? (arg: U) => unknown : never) extends ((arg: infer P) => unknown) ? P : never; type __VLS_OverloadUnionInner = U & T extends (...args: infer A) => infer R @@ -143,8 +160,8 @@ export function generateGlobalTypes(lib: string, target: number, strictTemplates } & { props?: ${fnPropsType}; expose?(exposed: K): void; } } : T extends () => any ? (props: {}, ctx?: any) => ReturnType : T extends (...args: any) => any ? T - : (_: {}${strictTemplates ? '' : ' & Record'}, ctx?: any) => { __ctx?: { attrs?: any, expose?: any, slots?: any, emit?: any, props?: {}${strictTemplates ? '' : ' & Record'} } }; - function __VLS_asFunctionalElement(tag: T, endTag?: T): (_: T${strictTemplates ? '' : ' & Record'}) => void; + : (_: {}${checkUnknownProps ? '' : ' & Record'}, ctx?: any) => { __ctx?: { attrs?: any, expose?: any, slots?: any, emit?: any, props?: {}${checkUnknownProps ? '' : ' & Record'} } }; + function __VLS_asFunctionalElement(tag: T, endTag?: T): (_: T${checkUnknownComponents ? '' : ' & Record'}) => void; function __VLS_functionalComponentArgsRest any>(t: T): 2 extends Parameters['length'] ? [any] : []; function __VLS_normalizeSlot(s: S): S extends () => infer R ? (props: {}) => R : S; function __VLS_tryAsConstant(t: T): T; diff --git a/packages/language-core/lib/codegen/script/index.ts b/packages/language-core/lib/codegen/script/index.ts index fd93e9c639..e9e8187e65 100644 --- a/packages/language-core/lib/codegen/script/index.ts +++ b/packages/language-core/lib/codegen/script/index.ts @@ -4,7 +4,7 @@ import type * as ts from 'typescript'; import type { ScriptRanges } from '../../parsers/scriptRanges'; import type { ScriptSetupRanges } from '../../parsers/scriptSetupRanges'; import type { Code, Sfc, VueCodeInformation, VueCompilerOptions } from '../../types'; -import { generateGlobalTypes } from '../globalTypes'; +import { generateGlobalTypes, getGlobalTypesFileName } from '../globalTypes'; import type { TemplateCodegenContext } from '../template/context'; import { endOfLine, generateSfcBlockSection, newLine } from '../utils'; import { generateComponentSelf } from './componentSelf'; @@ -68,7 +68,7 @@ export function* generateScript(options: ScriptCodegenOptions): Generator${newLine}`; } else { - yield `/// ${newLine}`; + yield `/// ${newLine}`; } } else { @@ -163,7 +163,7 @@ export function* generateScript(options: ScriptCodegenOptions): Generator prop !== nameProp), true, true), + ...generateElementProps( + options, + ctx, + node, + node.props.filter(prop => prop !== nameProp), + true, + true + ), `}` ); yield `)${endOfLine}`; } else { yield `var ${varSlot} = {${newLine}`; - yield* generateElementProps(options, ctx, node, node.props.filter(prop => prop !== nameProp), options.vueCompilerOptions.strictTemplates, true); + yield* generateElementProps( + options, + ctx, + node, + node.props.filter(prop => prop !== nameProp), + options.vueCompilerOptions.checkUnknownProps, + true + ); yield `}${endOfLine}`; if ( diff --git a/packages/language-core/lib/types.ts b/packages/language-core/lib/types.ts index 6728cbad41..0a9ea03d88 100644 --- a/packages/language-core/lib/types.ts +++ b/packages/language-core/lib/types.ts @@ -10,6 +10,7 @@ export type { SFCParseResult } from '@vue/compiler-sfc'; export { VueEmbeddedCode }; export type RawVueCompilerOptions = Partial> & { + strictTemplates?: boolean; target?: 'auto' | 2 | 2.7 | 3 | 3.3 | 3.5 | 99 | number; plugins?: string[]; }; @@ -28,7 +29,9 @@ export interface VueCompilerOptions { vitePressExtensions: string[]; petiteVueExtensions: string[]; jsxSlots: boolean; - strictTemplates: boolean; + checkUnknownProps: boolean; + checkUnknownEvents: boolean; + checkUnknownComponents: boolean; skipTemplateCodegen: boolean; fallthroughAttributes: boolean; dataAttributes: string[]; diff --git a/packages/language-core/lib/utils/ts.ts b/packages/language-core/lib/utils/ts.ts index c4cf75d586..d8605b0f75 100644 --- a/packages/language-core/lib/utils/ts.ts +++ b/packages/language-core/lib/utils/ts.ts @@ -1,7 +1,7 @@ import { camelize } from '@vue/shared'; import { posix as path } from 'path-browserify'; import type * as ts from 'typescript'; -import { generateGlobalTypes } from '../codegen/globalTypes'; +import { generateGlobalTypes, getGlobalTypesFileName } from '../codegen/globalTypes'; import { getAllExtensions } from '../languagePlugin'; import type { RawVueCompilerOptions, VueCompilerOptions, VueLanguagePlugin } from '../types'; @@ -206,7 +206,7 @@ export class CompilerOptionsResolver { build(defaults?: VueCompilerOptions): VueCompilerOptions { const target = this.target ?? this.fallbackTarget; - defaults ??= getDefaultCompilerOptions(target, this.options.lib); + defaults ??= getDefaultCompilerOptions(target, this.options.lib, this.options.strictTemplates); return { ...defaults, ...this.options, @@ -222,16 +222,7 @@ export class CompilerOptionsResolver { // 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 - } - } + this.options.experimentalModelPropName ?? defaults.experimentalModelPropName ).map(([k, v]) => [camelize(k), v])), }; } @@ -263,7 +254,7 @@ function resolvePath(scriptPath: string, root: string) { } } -export function getDefaultCompilerOptions(target = 99, lib = 'vue'): VueCompilerOptions { +export function getDefaultCompilerOptions(target = 99, lib = 'vue', strictTemplates = false): VueCompilerOptions { return { target, lib, @@ -271,7 +262,9 @@ export function getDefaultCompilerOptions(target = 99, lib = 'vue'): VueCompiler vitePressExtensions: [], petiteVueExtensions: [], jsxSlots: false, - strictTemplates: false, + checkUnknownProps: strictTemplates, + checkUnknownEvents: strictTemplates, + checkUnknownComponents: strictTemplates, skipTemplateCodegen: false, fallthroughAttributes: false, dataAttributes: [], @@ -297,7 +290,16 @@ export function getDefaultCompilerOptions(target = 99, lib = 'vue'): VueCompiler plugins: [], experimentalDefinePropProposal: false, experimentalResolveStyleCssClasses: 'scoped', - experimentalModelPropName: null! + experimentalModelPropName: { + '': { + input: true + }, + value: { + input: { type: 'text' }, + textarea: true, + select: true + } + }, }; } @@ -327,8 +329,8 @@ export function setupGlobalTypes(rootDir: string, vueOptions: VueCompilerOptions } dir = parentDir; } - const globalTypesPath = path.join(dir, 'node_modules', '.vue-global-types', `${vueOptions.lib}_${vueOptions.target}_${vueOptions.strictTemplates}.d.ts`); - const globalTypesContents = `// @ts-nocheck\nexport {};\n` + generateGlobalTypes(vueOptions.lib, vueOptions.target, vueOptions.strictTemplates); + const globalTypesPath = path.join(dir, 'node_modules', '.vue-global-types', getGlobalTypesFileName(vueOptions)); + const globalTypesContents = `// @ts-nocheck\nexport {};\n` + generateGlobalTypes(vueOptions); host.writeFile(globalTypesPath, globalTypesContents); return { absolutePath: globalTypesPath }; } catch { } diff --git a/packages/language-core/schemas/vue-tsconfig.schema.json b/packages/language-core/schemas/vue-tsconfig.schema.json index 5c6f33426f..d2f8613a01 100644 --- a/packages/language-core/schemas/vue-tsconfig.schema.json +++ b/packages/language-core/schemas/vue-tsconfig.schema.json @@ -45,6 +45,21 @@ "default": false, "markdownDescription": "Strict props, component type-checking in templates." }, + "checkUnknownProps": { + "type": "boolean", + "default": false, + "markdownDescription": "Check unknown props. If not set, uses the 'strictTemplates' value." + }, + "checkUnknownEvents": { + "type": "boolean", + "default": false, + "markdownDescription": "Check unknown events. If not set, uses the 'strictTemplates' value." + }, + "checkUnknownComponents": { + "type": "boolean", + "default": false, + "markdownDescription": "Check unknown components. If not set, uses the 'strictTemplates' value." + }, "skipTemplateCodegen": { "type": "boolean", "default": false, diff --git a/packages/language-server/lib/initialize.ts b/packages/language-server/lib/initialize.ts index 2d33454b3a..776710c6f0 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, getDefaultCompilerOptions, VueCompilerOptions } from '@vue/language-core'; +import { createParsedCommandLine, createVueLanguagePlugin, generateGlobalTypes, getAllExtensions, getDefaultCompilerOptions, getGlobalTypesFileName, VueCompilerOptions } from '@vue/language-core'; import { Disposable, getFullLanguageServicePlugins, InitializeParams } from '@vue/language-service'; import type * as ts from 'typescript'; @@ -55,8 +55,8 @@ export function initialize( const directoryExists = project.typescript.languageServiceHost.directoryExists?.bind(project.typescript.languageServiceHost); const fileExists = project.typescript.languageServiceHost.fileExists.bind(project.typescript.languageServiceHost); const getScriptSnapshot = project.typescript.languageServiceHost.getScriptSnapshot.bind(project.typescript.languageServiceHost); - const globalTypesName = `${vueCompilerOptions.lib}_${vueCompilerOptions.target}_${vueCompilerOptions.strictTemplates}.d.ts`; - const globalTypesContents = `// @ts-nocheck\nexport {};\n` + generateGlobalTypes(vueCompilerOptions.lib, vueCompilerOptions.target, vueCompilerOptions.strictTemplates); + const globalTypesName = getGlobalTypesFileName(vueCompilerOptions); + const globalTypesContents = `// @ts-nocheck\nexport {};\n` + generateGlobalTypes(vueCompilerOptions); const globalTypesSnapshot: ts.IScriptSnapshot = { getText: (start, end) => globalTypesContents.slice(start, end), getLength: () => globalTypesContents.length,