Skip to content

Commit

Permalink
feat(language-core): add options for fine-grained configuration of st…
Browse files Browse the repository at this point in the history
…rictTemplates (#5138)

Co-authored-by: Johnson Chu <[email protected]>
  • Loading branch information
KazariEX and johnsoncodehk authored Jan 22, 2025
1 parent 3f138d6 commit a0511b8
Show file tree
Hide file tree
Showing 9 changed files with 111 additions and 37 deletions.
4 changes: 2 additions & 2 deletions packages/component-meta/lib/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
29 changes: 23 additions & 6 deletions packages/language-core/lib/codegen/globalTypes.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>'}`;
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<string, unknown>'}`;
let text = ``;
if (target < 3.5) {
text += `
Expand Down Expand Up @@ -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<T, K> =
'__ctx' extends keyof __VLS_PickNotAny<K, {}> ? K extends { __ctx?: { props?: infer P } } ? NonNullable<P> : never
: T extends (props: infer P, ...args: any) => any ? P :
Expand All @@ -69,7 +86,7 @@ export function generateGlobalTypes(lib: string, target: number, strictTemplates
: __VLS_IsFunction<Events, CamelizedEvent> extends true
? { [K in onEvent]?: Events[CamelizedEvent] }
: Props
)${strictTemplates ? '' : ' & Record<string, unknown>'};
)${checkUnknownEvents ? '' : ' & Record<string, unknown>'};
// fix https://github.com/vuejs/language-tools/issues/926
type __VLS_UnionToIntersection<U> = (U extends unknown ? (arg: U) => unknown : never) extends ((arg: infer P) => unknown) ? P : never;
type __VLS_OverloadUnionInner<T, U = unknown> = U & T extends (...args: infer A) => infer R
Expand Down Expand Up @@ -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>
: T extends (...args: any) => any ? T
: (_: {}${strictTemplates ? '' : ' & Record<string, unknown>'}, ctx?: any) => { __ctx?: { attrs?: any, expose?: any, slots?: any, emit?: any, props?: {}${strictTemplates ? '' : ' & Record<string, unknown>'} } };
function __VLS_asFunctionalElement<T>(tag: T, endTag?: T): (_: T${strictTemplates ? '' : ' & Record<string, unknown>'}) => void;
: (_: {}${checkUnknownProps ? '' : ' & Record<string, unknown>'}, ctx?: any) => { __ctx?: { attrs?: any, expose?: any, slots?: any, emit?: any, props?: {}${checkUnknownProps ? '' : ' & Record<string, unknown>'} } };
function __VLS_asFunctionalElement<T>(tag: T, endTag?: T): (_: T${checkUnknownComponents ? '' : ' & Record<string, unknown>'}) => void;
function __VLS_functionalComponentArgsRest<T extends (...args: any) => any>(t: T): 2 extends Parameters<T>['length'] ? [any] : [];
function __VLS_normalizeSlot<S>(s: S): S extends () => infer R ? (props: {}) => R : S;
function __VLS_tryAsConstant<const T>(t: T): T;
Expand Down
6 changes: 3 additions & 3 deletions packages/language-core/lib/codegen/script/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -68,7 +68,7 @@ export function* generateScript(options: ScriptCodegenOptions): Generator<Code,
yield `/// <reference types="${relativePath}" />${newLine}`;
}
else {
yield `/// <reference types=".vue-global-types/${options.vueCompilerOptions.lib}_${options.vueCompilerOptions.target}_${options.vueCompilerOptions.strictTemplates}.d.ts" />${newLine}`;
yield `/// <reference types=".vue-global-types/${getGlobalTypesFileName(options.vueCompilerOptions)}" />${newLine}`;
}
}
else {
Expand Down Expand Up @@ -163,7 +163,7 @@ export function* generateScript(options: ScriptCodegenOptions): Generator<Code,
}
yield* ctx.localTypes.generate([...ctx.localTypes.getUsedNames()]);
if (options.appendGlobalTypes) {
yield generateGlobalTypes(options.vueCompilerOptions.lib, options.vueCompilerOptions.target, options.vueCompilerOptions.strictTemplates);
yield generateGlobalTypes(options.vueCompilerOptions);
}

if (options.sfc.scriptSetup) {
Expand Down
29 changes: 26 additions & 3 deletions packages/language-core/lib/codegen/template/element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,14 @@ export function* generateComponent(

yield `// @ts-ignore${newLine}`;
yield `const ${var_functionalComponent} = __VLS_asFunctionalComponent(${var_originalComponent}, new ${var_originalComponent}({${newLine}`;
yield* generateElementProps(options, ctx, node, props, options.vueCompilerOptions.strictTemplates, false);
yield* generateElementProps(
options,
ctx,
node,
props,
options.vueCompilerOptions.checkUnknownProps,
false
);
yield `}))${endOfLine}`;

yield `const `;
Expand All @@ -237,7 +244,15 @@ export function* generateComponent(
tagOffsets[0] + node.tag.length,
ctx.codeFeatures.verification,
`{${newLine}`,
...generateElementProps(options, ctx, node, props, options.vueCompilerOptions.strictTemplates, true, failedPropExps),
...generateElementProps(
options,
ctx,
node,
props,
options.vueCompilerOptions.checkUnknownProps,
true,
failedPropExps
),
`}`
);
yield `, ...__VLS_functionalComponentArgsRest(${var_functionalComponent}))${endOfLine}`;
Expand Down Expand Up @@ -330,7 +345,15 @@ export function* generateElement(
startTagOffset + node.tag.length,
ctx.codeFeatures.verification,
`{${newLine}`,
...generateElementProps(options, ctx, node, node.props, options.vueCompilerOptions.strictTemplates, true, failedPropExps),
...generateElementProps(
options,
ctx,
node,
node.props,
options.vueCompilerOptions.checkUnknownProps,
true,
failedPropExps
),
`}`
);
yield `)${endOfLine}`;
Expand Down
18 changes: 16 additions & 2 deletions packages/language-core/lib/codegen/template/slotOutlet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,28 @@ export function* generateSlotOutlet(
startTagOffset + node.tag.length,
ctx.codeFeatures.verification,
`{${newLine}`,
...generateElementProps(options, ctx, node, node.props.filter(prop => 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 (
Expand Down
5 changes: 4 additions & 1 deletion packages/language-core/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export type { SFCParseResult } from '@vue/compiler-sfc';
export { VueEmbeddedCode };

export type RawVueCompilerOptions = Partial<Omit<VueCompilerOptions, 'target' | 'plugins'>> & {
strictTemplates?: boolean;
target?: 'auto' | 2 | 2.7 | 3 | 3.3 | 3.5 | 99 | number;
plugins?: string[];
};
Expand All @@ -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[];
Expand Down
36 changes: 19 additions & 17 deletions packages/language-core/lib/utils/ts.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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,
Expand All @@ -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])),
};
}
Expand Down Expand Up @@ -263,15 +254,17 @@ 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,
extensions: ['.vue'],
vitePressExtensions: [],
petiteVueExtensions: [],
jsxSlots: false,
strictTemplates: false,
checkUnknownProps: strictTemplates,
checkUnknownEvents: strictTemplates,
checkUnknownComponents: strictTemplates,
skipTemplateCodegen: false,
fallthroughAttributes: false,
dataAttributes: [],
Expand All @@ -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
}
},
};
}

Expand Down Expand Up @@ -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 { }
Expand Down
15 changes: 15 additions & 0 deletions packages/language-core/schemas/vue-tsconfig.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 3 additions & 3 deletions packages/language-server/lib/initialize.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit a0511b8

Please sign in to comment.