From f46634c7e5a330032b40d224cf72dd1c0b6640d5 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Sun, 21 Apr 2024 17:53:41 +0900 Subject: [PATCH] refactor(language-core): improve maintainability of codegen (#4276) --- extensions/vscode/src/nodeClientMain.ts | 1 - packages/component-meta/lib/base.ts | 4 +- packages/language-core/index.ts | 2 +- packages/language-core/lib/codegen/common.ts | 84 + .../lib/codegen/script/component.ts | 116 + .../lib/codegen/script/context.ts | 132 ++ .../lib/codegen/script/globalTypes.ts | 131 ++ .../language-core/lib/codegen/script/index.ts | 154 ++ .../lib/codegen/script/internalComponent.ts | 64 + .../lib/codegen/script/scriptSetup.ts | 380 +++ .../language-core/lib/codegen/script/src.ts | 54 + .../lib/codegen/script/template.ts | 245 ++ .../lib/codegen/template/camelized.ts | 29 + .../lib/codegen/template/context.ts | 190 ++ .../lib/codegen/template/element.ts | 464 ++++ .../lib/codegen/template/elementChildren.ts | 33 + .../lib/codegen/template/elementDirectives.ts | 92 + .../lib/codegen/template/elementEvents.ts | 215 ++ .../lib/codegen/template/elementProps.ts | 363 +++ .../lib/codegen/template/index.ts | 283 +++ .../template/interpolation.ts} | 152 +- .../lib/codegen/template/objectProperty.ts | 45 + .../lib/codegen/template/propertyAccess.ts | 39 + .../lib/codegen/template/slotOutlet.ts | 113 + .../lib/codegen/template/stringLiteralKey.ts | 18 + .../lib/codegen/template/templateChild.ts | 182 ++ .../lib/codegen/template/vFor.ts | 87 + .../language-core/lib/codegen/template/vIf.ts | 76 + .../lib/generators/globalTypes.ts | 131 -- .../language-core/lib/generators/inlineCss.ts | 38 - .../language-core/lib/generators/script.ts | 1117 --------- .../language-core/lib/generators/template.ts | 2047 ----------------- .../language-core/lib/generators/utils.ts | 51 - packages/language-core/lib/languageModule.ts | 11 +- packages/language-core/lib/plugins/shared.ts | 10 + .../lib/plugins/vue-sfc-customblocks.ts | 4 +- .../lib/plugins/vue-sfc-scripts.ts | 5 +- .../lib/plugins/vue-sfc-styles.ts | 6 +- .../lib/plugins/vue-sfc-template.ts | 4 +- .../lib/plugins/vue-template-inline-css.ts | 43 +- .../lib/plugins/vue-template-inline-ts.ts | 11 +- packages/language-core/lib/plugins/vue-tsx.ts | 82 +- packages/language-core/lib/types.ts | 5 +- packages/language-core/lib/utils/ts.ts | 33 - .../lib/virtualFile/computedFiles.ts | 36 +- .../lib/virtualFile/computedMappings.ts | 6 +- .../lib/virtualFile/computedSfc.ts | 23 +- .../lib/virtualFile/embeddedFile.ts | 3 +- .../language-core/lib/virtualFile/vueFile.ts | 8 +- packages/language-server/lib/types.ts | 2 +- packages/language-server/node.ts | 1 - .../lib/ideFeatures/nameCasing.ts | 12 +- .../lib/plugins/vue-codelens-references.ts | 4 +- .../lib/plugins/vue-document-drop.ts | 4 +- .../lib/plugins/vue-extract-file.ts | 6 +- .../language-service/lib/plugins/vue-sfc.ts | 8 +- .../lib/plugins/vue-template.ts | 16 +- .../plugins/vue-toggle-v-bind-codeaction.ts | 4 +- .../lib/plugins/vue-twoslash-queries.ts | 2 +- .../tests/format/1210.spec.ts | 2 +- packages/tsc/index.ts | 1 - .../tsc/tests/__snapshots__/dts.spec.ts.snap | 35 +- packages/tsc/tests/dts.spec.ts | 1 - packages/typescript-plugin/lib/common.ts | 11 +- .../lib/requests/collectExtractProps.ts | 4 +- .../lib/requests/componentInfos.ts | 30 +- .../component-meta/options-api/component.ts | 2 +- .../component-meta/ts-component/component.ts | 4 +- .../component-meta/ts-component/component.tsx | 4 +- .../ts-named-export/component.ts | 2 +- test-workspace/tsc/vue3/#2399/main.vue | 6 +- .../tsc/vue3/defineProp_A/script-setup.vue | 2 +- .../tsc/vue3/dynamic-component/main.vue | 4 +- test-workspace/tsc/vue3/input-radio/main.vue | 2 +- 74 files changed, 3929 insertions(+), 3662 deletions(-) create mode 100644 packages/language-core/lib/codegen/common.ts create mode 100644 packages/language-core/lib/codegen/script/component.ts create mode 100644 packages/language-core/lib/codegen/script/context.ts create mode 100644 packages/language-core/lib/codegen/script/globalTypes.ts create mode 100644 packages/language-core/lib/codegen/script/index.ts create mode 100644 packages/language-core/lib/codegen/script/internalComponent.ts create mode 100644 packages/language-core/lib/codegen/script/scriptSetup.ts create mode 100644 packages/language-core/lib/codegen/script/src.ts create mode 100644 packages/language-core/lib/codegen/script/template.ts create mode 100644 packages/language-core/lib/codegen/template/camelized.ts create mode 100644 packages/language-core/lib/codegen/template/context.ts create mode 100644 packages/language-core/lib/codegen/template/element.ts create mode 100644 packages/language-core/lib/codegen/template/elementChildren.ts create mode 100644 packages/language-core/lib/codegen/template/elementDirectives.ts create mode 100644 packages/language-core/lib/codegen/template/elementEvents.ts create mode 100644 packages/language-core/lib/codegen/template/elementProps.ts create mode 100644 packages/language-core/lib/codegen/template/index.ts rename packages/language-core/lib/{utils/transform.ts => codegen/template/interpolation.ts} (65%) create mode 100644 packages/language-core/lib/codegen/template/objectProperty.ts create mode 100644 packages/language-core/lib/codegen/template/propertyAccess.ts create mode 100644 packages/language-core/lib/codegen/template/slotOutlet.ts create mode 100644 packages/language-core/lib/codegen/template/stringLiteralKey.ts create mode 100644 packages/language-core/lib/codegen/template/templateChild.ts create mode 100644 packages/language-core/lib/codegen/template/vFor.ts create mode 100644 packages/language-core/lib/codegen/template/vIf.ts delete mode 100644 packages/language-core/lib/generators/globalTypes.ts delete mode 100644 packages/language-core/lib/generators/inlineCss.ts delete mode 100644 packages/language-core/lib/generators/script.ts delete mode 100644 packages/language-core/lib/generators/template.ts delete mode 100644 packages/language-core/lib/generators/utils.ts create mode 100644 packages/language-core/lib/plugins/shared.ts diff --git a/extensions/vscode/src/nodeClientMain.ts b/extensions/vscode/src/nodeClientMain.ts index bbcb01c098..a5f0cf8a05 100644 --- a/extensions/vscode/src/nodeClientMain.ts +++ b/extensions/vscode/src/nodeClientMain.ts @@ -10,7 +10,6 @@ import { middleware } from './middleware'; export async function activate(context: vscode.ExtensionContext) { const volarLabs = createLabsInfo(serverLib); - volarLabs.extensionExports.volarLabs.codegenStackSupport = true; await commonActivate(context, ( id, diff --git a/packages/component-meta/lib/base.ts b/packages/component-meta/lib/base.ts index 2cdf071171..4e959b11aa 100644 --- a/packages/component-meta/lib/base.ts +++ b/packages/component-meta/lib/base.ts @@ -314,7 +314,7 @@ ${vueCompilerOptions.target < 3 ? vue2TypeHelpersCode : typeHelpersCode} const vueFile = language.scripts.get(componentPath)?.generated?.root; const vueDefaults = vueFile && exportName === 'default' - ? (vueFile instanceof vue.VueGeneratedCode ? readVueComponentDefaultProps(vueFile, printer, ts, vueCompilerOptions) : {}) + ? (vueFile instanceof vue.VueVirtualCode ? readVueComponentDefaultProps(vueFile, printer, ts, vueCompilerOptions) : {}) : {}; const tsDefaults = !vueFile ? readTsComponentDefaultProps( componentPath.substring(componentPath.lastIndexOf('.') + 1), // ts | js | tsx | jsx @@ -684,7 +684,7 @@ function createSchemaResolvers( } function readVueComponentDefaultProps( - vueSourceFile: vue.VueGeneratedCode, + vueSourceFile: vue.VueVirtualCode, printer: ts.Printer | undefined, ts: typeof import('typescript'), vueCompilerOptions: vue.VueCompilerOptions, diff --git a/packages/language-core/index.ts b/packages/language-core/index.ts index 79909946ca..390a66f6aa 100644 --- a/packages/language-core/index.ts +++ b/packages/language-core/index.ts @@ -1,4 +1,4 @@ -export * from './lib/generators/template'; +export * from './lib/codegen/template'; export * from './lib/languageModule'; export * from './lib/parsers/scriptSetupRanges'; export * from './lib/plugins'; diff --git a/packages/language-core/lib/codegen/common.ts b/packages/language-core/lib/codegen/common.ts new file mode 100644 index 0000000000..58389a1931 --- /dev/null +++ b/packages/language-core/lib/codegen/common.ts @@ -0,0 +1,84 @@ +import type * as ts from 'typescript'; +import { getNodeText } from '../parsers/scriptSetupRanges'; +import type { Code, SfcBlock, VueCodeInformation } from '../types'; + +export const newLine = '\n'; +export const endOfLine = `;${newLine}`; +export const combineLastMapping: VueCodeInformation = { __combineLastMapping: true }; +export const variableNameRegex = /^[a-zA-Z_$][0-9a-zA-Z_$]*$/; + +export function* conditionWrapWith( + condition: boolean, + startOffset: number, + endOffset: number, + features: VueCodeInformation, + ...wrapCodes: Code[] +): Generator { + if (condition) { + yield* wrapWith(startOffset, endOffset, features, ...wrapCodes); + } + else { + for (const wrapCode of wrapCodes) { + yield wrapCode; + } + } +} + +export function* wrapWith( + startOffset: number, + endOffset: number, + features: VueCodeInformation, + ...wrapCodes: Code[] +): Generator { + yield ['', 'template', startOffset, features]; + let offset = 1; + for (const wrapCode of wrapCodes) { + if (typeof wrapCode !== 'string') { + offset++; + } + yield wrapCode; + } + yield ['', 'template', endOffset, { __combineOffsetMapping: offset }]; +} + +export function collectVars( + ts: typeof import('typescript'), + node: ts.Node, + ast: ts.SourceFile, + result: string[], +) { + if (ts.isIdentifier(node)) { + result.push(getNodeText(ts, node, ast)); + } + else if (ts.isObjectBindingPattern(node)) { + for (const el of node.elements) { + collectVars(ts, el.name, ast, result); + } + } + else if (ts.isArrayBindingPattern(node)) { + for (const el of node.elements) { + if (ts.isBindingElement(el)) { + collectVars(ts, el.name, ast, result); + } + } + } + else { + ts.forEachChild(node, node => collectVars(ts, node, ast, result)); + } +} +export function createTsAst(ts: typeof import('typescript'), astHolder: any, text: string) { + if (astHolder.__volar_ast_text !== text) { + astHolder.__volar_ast_text = text; + astHolder.__volar_ast = ts.createSourceFile('/a.ts', text, 99 satisfies ts.ScriptTarget.ESNext); + } + return astHolder.__volar_ast as ts.SourceFile; +} + +export function generateSfcBlockSection(block: SfcBlock, start: number, end: number, features: VueCodeInformation): Code { + return [ + block.content.substring(start, end), + block.name, + start, + features, + ]; +} diff --git a/packages/language-core/lib/codegen/script/component.ts b/packages/language-core/lib/codegen/script/component.ts new file mode 100644 index 0000000000..d76c4ccd9d --- /dev/null +++ b/packages/language-core/lib/codegen/script/component.ts @@ -0,0 +1,116 @@ +import type { ScriptRanges } from '../../parsers/scriptRanges'; +import type { ScriptSetupRanges } from '../../parsers/scriptSetupRanges'; +import type { Code, Sfc } from '../../types'; +import { endOfLine, generateSfcBlockSection, newLine } from '../common'; +import type { ScriptCodegenContext } from './context'; +import { ScriptCodegenOptions, codeFeatures } from './index'; + +export function* generateComponent( + options: ScriptCodegenOptions, + ctx: ScriptCodegenContext, + scriptSetup: NonNullable, + scriptSetupRanges: ScriptSetupRanges, +): Generator { + if (options.sfc.script && options.scriptRanges?.exportDefault && options.scriptRanges.exportDefault.expression.start !== options.scriptRanges.exportDefault.args.start) { + // use defineComponent() from user space code if it exist + yield generateSfcBlockSection(options.sfc.script, options.scriptRanges.exportDefault.expression.start, options.scriptRanges.exportDefault.args.start, codeFeatures.all); + yield `{${newLine}`; + } + else { + yield `(await import('${options.vueCompilerOptions.lib}')).defineComponent({${newLine}`; + } + + yield `setup() {${newLine}`; + yield `return {${newLine}`; + if (ctx.bypassDefineComponent) { + yield* generateComponentSetupReturns(scriptSetupRanges); + } + if (scriptSetupRanges.expose.define) { + yield `...__VLS_exposed,${newLine}`; + } + yield `}${endOfLine}`; + yield `},${newLine}`; + if (!ctx.bypassDefineComponent) { + yield* generateScriptSetupOptions(ctx, scriptSetup, scriptSetupRanges); + } + if (options.sfc.script && options.scriptRanges) { + yield* generateScriptOptions(options.sfc.script, options.scriptRanges); + } + yield `})`; +} + +export function* generateComponentSetupReturns(scriptSetupRanges: ScriptSetupRanges): Generator { + // fill $props + if (scriptSetupRanges.props.define) { + // NOTE: defineProps is inaccurate for $props + yield `$props: __VLS_makeOptional(${scriptSetupRanges.props.name ?? `__VLS_props`}),${newLine}`; + yield `...${scriptSetupRanges.props.name ?? `__VLS_props`},${newLine}`; + } + // fill $emit + if (scriptSetupRanges.emits.define) { + yield `$emit: ${scriptSetupRanges.emits.name ?? '__VLS_emit'},${newLine}`; + } +} + +export function* generateScriptOptions( + script: NonNullable, + scriptRanges: ScriptRanges, +): Generator { + if (scriptRanges.exportDefault?.args) { + yield generateSfcBlockSection(script, scriptRanges.exportDefault.args.start + 1, scriptRanges.exportDefault.args.end - 1, codeFeatures.all); + } +} + +export function* generateScriptSetupOptions( + ctx: ScriptCodegenContext, + scriptSetup: NonNullable, + scriptSetupRanges: ScriptSetupRanges, +): Generator { + const propsCodegens: (() => Generator)[] = []; + + if (ctx.generatedPropsType) { + propsCodegens.push(function* () { + yield `{} as `; + if (scriptSetupRanges.props.withDefaults?.arg) { + yield `${ctx.helperTypes.WithDefaults.name}<`; + } + yield `${ctx.helperTypes.TypePropsToOption.name}<`; + yield `typeof __VLS_componentProps`; + yield `>`; + if (scriptSetupRanges.props.withDefaults?.arg) { + yield `, typeof __VLS_withDefaultsArg>`; + } + }); + } + if (scriptSetupRanges.props.define?.arg) { + const { arg } = scriptSetupRanges.props.define; + propsCodegens.push(function* () { + yield generateSfcBlockSection(scriptSetup, arg.start, arg.end, codeFeatures.navigation); + }); + } + + if (propsCodegens.length === 1) { + yield `props: `; + for (const generate of propsCodegens) { + yield* generate(); + } + yield `,${newLine}`; + } + else if (propsCodegens.length >= 2) { + yield `props: {${newLine}`; + for (const generate of propsCodegens) { + yield `...`; + yield* generate(); + yield `,${newLine}`; + } + yield `},${newLine}`; + } + if (scriptSetupRanges.defineProp.filter(p => p.isModel).length || scriptSetupRanges.emits.define) { + yield `emits: ({} as __VLS_NormalizeEmits),${newLine}`; + } +} diff --git a/packages/language-core/lib/codegen/script/context.ts b/packages/language-core/lib/codegen/script/context.ts new file mode 100644 index 0000000000..a78d05a82d --- /dev/null +++ b/packages/language-core/lib/codegen/script/context.ts @@ -0,0 +1,132 @@ +import { getSlotsPropertyName } from '../../utils/shared'; +import { newLine } from '../common'; +import type { ScriptCodegenOptions } from './index'; + +interface HelperType { + name: string; + used?: boolean; + generated?: boolean; + code: string; +} + +export type ScriptCodegenContext = ReturnType; + +export function createScriptCodegenContext(options: ScriptCodegenOptions) { + const helperTypes = { + OmitKeepDiscriminatedUnion: { + get name() { + this.used = true; + return `__VLS_OmitKeepDiscriminatedUnion`; + }, + get code() { + return `type __VLS_OmitKeepDiscriminatedUnion = T extends any + ? Pick> + : never;`; + }, + } satisfies HelperType as HelperType, + WithDefaults: { + get name() { + this.used = true; + return `__VLS_WithDefaults`; + }, + get code(): string { + return `type __VLS_WithDefaults = { + [K in keyof Pick]: K extends keyof D + ? ${helperTypes.Prettify.name} + : P[K] + };`; + }, + } satisfies HelperType as HelperType, + Prettify: { + get name() { + this.used = true; + return `__VLS_Prettify`; + }, + get code() { + return `type __VLS_Prettify = { [K in keyof T]: T[K]; } & {};`; + }, + } satisfies HelperType as HelperType, + WithTemplateSlots: { + get name() { + this.used = true; + return `__VLS_WithTemplateSlots`; + }, + get code(): string { + return `type __VLS_WithTemplateSlots = T & { + new(): { + ${getSlotsPropertyName(options.vueCompilerOptions.target)}: S; + ${options.vueCompilerOptions.jsxSlots ? `$props: ${helperTypes.PropsChildren.name};` : ''} + } + };`; + }, + } satisfies HelperType as HelperType, + PropsChildren: { + get name() { + this.used = true; + return `__VLS_PropsChildren`; + }, + get code() { + return `type __VLS_PropsChildren = { + [K in keyof ( + boolean extends ( + // @ts-ignore + JSX.ElementChildrenAttribute extends never + ? true + : false + ) + ? never + // @ts-ignore + : JSX.ElementChildrenAttribute + )]?: S; + };`; + }, + } satisfies HelperType as HelperType, + TypePropsToOption: { + get name() { + this.used = true; + return `__VLS_TypePropsToOption`; + }, + get code() { + return options.compilerOptions.exactOptionalPropertyTypes ? + `type __VLS_TypePropsToOption = { + [K in keyof T]-?: {} extends Pick + ? { type: import('${options.vueCompilerOptions.lib}').PropType } + : { type: import('${options.vueCompilerOptions.lib}').PropType, required: true } + };` : + `type __VLS_NonUndefinedable = T extends undefined ? never : T; + type __VLS_TypePropsToOption = { + [K in keyof T]-?: {} extends Pick + ? { type: import('${options.vueCompilerOptions.lib}').PropType<__VLS_NonUndefinedable> } + : { type: import('${options.vueCompilerOptions.lib}').PropType, required: true } + };`; + }, + } satisfies HelperType as HelperType, + }; + + return { + generatedTemplate: false, + generatedPropsType: false, + scriptSetupGeneratedOffset: undefined as number | undefined, + bypassDefineComponent: options.lang === 'js' || options.lang === 'jsx', + bindingNames: new Set([ + ...options.scriptRanges?.bindings.map(range => options.sfc.script!.content.substring(range.start, range.end)) ?? [], + ...options.scriptSetupRanges?.bindings.map(range => options.sfc.scriptSetup!.content.substring(range.start, range.end)) ?? [], + ]), + helperTypes, + generateHelperTypes, + }; + + function* generateHelperTypes() { + let shouldCheck = true; + while (shouldCheck) { + shouldCheck = false; + for (const helperType of Object.values(helperTypes)) { + if (helperType.used && !helperType.generated) { + shouldCheck = true; + helperType.generated = true; + yield newLine + helperType.code + newLine; + } + } + } + } +} diff --git a/packages/language-core/lib/codegen/script/globalTypes.ts b/packages/language-core/lib/codegen/script/globalTypes.ts new file mode 100644 index 0000000000..8e6cb7007d --- /dev/null +++ b/packages/language-core/lib/codegen/script/globalTypes.ts @@ -0,0 +1,131 @@ +import type { VueCompilerOptions } from '../../types'; +import { getSlotsPropertyName } from '../../utils/shared'; + +export function generateGlobalTypes(vueCompilerOptions: VueCompilerOptions) { + const fnPropsType = `(K extends { $props: infer Props } ? Props : any)${vueCompilerOptions.strictTemplates ? '' : ' & Record'}`; + return ` +; export const __VLS_globalTypesStart = {}; +declare global { + // @ts-ignore + type __VLS_IntrinsicElements = __VLS_PickNotAny>>; + // @ts-ignore + type __VLS_Element = __VLS_PickNotAny; + // @ts-ignore + type __VLS_GlobalComponents = ${[ + `__VLS_PickNotAny`, + `__VLS_PickNotAny`, + `__VLS_PickNotAny`, + `Pick` + ].join(' & ')}; + type __VLS_IsAny = 0 extends 1 & T ? true : false; + type __VLS_PickNotAny = __VLS_IsAny extends true ? B : A; + + const __VLS_intrinsicElements: __VLS_IntrinsicElements; + + // v-for + function __VLS_getVForSourceType(source: number): [number, number, number][]; + function __VLS_getVForSourceType(source: string): [string, number, number][]; + function __VLS_getVForSourceType(source: T): [ + item: T[number], + key: number, + index: number, + ][]; + function __VLS_getVForSourceType }>(source: T): [ + item: T extends { [Symbol.iterator](): Iterator } ? T1 : never, + key: number, + index: undefined, + ][]; + // #3845 + function __VLS_getVForSourceType }>(source: T): [ + item: number | (Exclude extends { [Symbol.iterator](): Iterator } ? T1 : never), + key: number, + index: undefined, + ][]; + function __VLS_getVForSourceType(source: T): [ + item: T[keyof T], + key: keyof T, + index: number, + ][]; + + // @ts-ignore + function __VLS_getSlotParams(slot: T): Parameters<__VLS_PickNotAny, (...args: any[]) => any>>; + // @ts-ignore + function __VLS_getSlotParam(slot: T): Parameters<__VLS_PickNotAny, (...args: any[]) => any>>[0]; + function __VLS_directiveFunction(dir: T): + T extends import('${vueCompilerOptions.lib}').ObjectDirective | import('${vueCompilerOptions.lib}').FunctionDirective ? (value: V) => void + : T; + function __VLS_withScope(ctx: T, scope: K): ctx is T & K; + function __VLS_makeOptional(t: T): { [K in keyof T]?: T[K] }; + + type __VLS_SelfComponent = string extends N ? {} : N extends string ? { [P in N]: C } : {}; + type __VLS_WithComponent = + N1 extends keyof LocalComponents ? N1 extends N0 ? Pick : { [K in N0]: LocalComponents[N1] } : + N2 extends keyof LocalComponents ? N2 extends N0 ? Pick : { [K in N0]: LocalComponents[N2] } : + N3 extends keyof LocalComponents ? N3 extends N0 ? Pick : { [K in N0]: LocalComponents[N3] } : + 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] } : + ${vueCompilerOptions.strictTemplates ? '{}' : '{ [K in N0]: unknown }'} + + type __VLS_FillingEventArg_ParametersLength any> = __VLS_IsAny> extends true ? -1 : Parameters['length']; + type __VLS_FillingEventArg = E extends (...args: any) => any ? __VLS_FillingEventArg_ParametersLength extends 0 ? ($event?: undefined) => ReturnType : E : E; + function __VLS_asFunctionalComponent any ? InstanceType : unknown>(t: T, instance?: K): + T extends new (...args: any) => any + ? (props: ${fnPropsType}, ctx?: any) => __VLS_Element & { __ctx?: { + attrs?: any, + slots?: K extends { ${getSlotsPropertyName(vueCompilerOptions.target)}: infer Slots } ? Slots : any, + emit?: K extends { $emit: infer Emit } ? Emit : any + } & { props?: ${fnPropsType}; expose?(exposed: K): void; } } + : T extends () => any ? (props: {}, ctx?: any) => ReturnType + : T extends (...args: any) => any ? T + : (_: {}${vueCompilerOptions.strictTemplates ? '' : ' & Record'}, ctx?: any) => { __ctx?: { attrs?: any, expose?: any, slots?: any, emit?: any, props?: {}${vueCompilerOptions.strictTemplates ? '' : ' & Record'} } }; + function __VLS_elementAsFunctionalComponent(t: T): (_: T${vueCompilerOptions.strictTemplates ? '' : ' & Record'}, ctx?: any) => { __ctx?: { attrs?: any, expose?: any, slots?: any, emit?: any, props?: T${vueCompilerOptions.strictTemplates ? '' : ' & Record'} } }; + function __VLS_functionalComponentArgsRest any>(t: T): Parameters['length'] extends 2 ? [any] : []; + function __VLS_pickEvent(emitEvent: E1, propEvent: E2): __VLS_FillingEventArg< + __VLS_PickNotAny< + __VLS_AsFunctionOrAny, + __VLS_AsFunctionOrAny + > + > | undefined; + function __VLS_pickFunctionalComponentCtx(comp: T, compInstance: K): __VLS_PickNotAny< + '__ctx' extends keyof __VLS_PickNotAny ? K extends { __ctx?: infer Ctx } ? Ctx : never : any + , T extends (props: any, ctx: infer Ctx) => any ? Ctx : any + >; + 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 : + {}; + type __VLS_AsFunctionOrAny = unknown extends F ? any : ((...args: any) => any) extends F ? F : any; + + function __VLS_normalizeSlot(s: S): S extends () => infer R ? (props: {}) => R : S; + + /** + * emit + */ + // 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 + ? U extends T + ? never + : __VLS_OverloadUnionInner & U & ((...args: A) => R)> | ((...args: A) => R) + : never; + type __VLS_OverloadUnion = Exclude< + __VLS_OverloadUnionInner<(() => never) & T>, + T extends () => never ? never : () => never + >; + type __VLS_ConstructorOverloads = __VLS_OverloadUnion extends infer F + ? F extends (event: infer E, ...args: infer A) => any + ? { [K in E & string]: (...args: A) => void; } + : never + : never; + type __VLS_NormalizeEmits = __VLS_PrettifyGlobal< + __VLS_UnionToIntersection< + __VLS_ConstructorOverloads & { + [K in keyof T]: T[K] extends any[] ? { (...args: T[K]): void } : never + } + > + >; + type __VLS_PrettifyGlobal = { [K in keyof T]: T[K]; } & {}; +} +export const __VLS_globalTypesEnd = {};`; +}; diff --git a/packages/language-core/lib/codegen/script/index.ts b/packages/language-core/lib/codegen/script/index.ts new file mode 100644 index 0000000000..5bf02b8690 --- /dev/null +++ b/packages/language-core/lib/codegen/script/index.ts @@ -0,0 +1,154 @@ +import type { Mapping } from '@volar/language-core'; +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 { endOfLine, generateSfcBlockSection, newLine } from '../common'; +import type { TemplateCodegenContext } from '../template/context'; +import { createScriptCodegenContext } from './context'; +import { generateGlobalTypes } from './globalTypes'; +import { generateScriptSetup, generateScriptSetupImports } from './scriptSetup'; +import { generateSrc } from './src'; +import { generateTemplate } from './template'; + +export const codeFeatures = { + all: { + verification: true, + completion: true, + semantic: true, + navigation: true, + } as VueCodeInformation, + none: {} as VueCodeInformation, + verification: { + verification: true, + } as VueCodeInformation, + navigation: { + navigation: true, + } as VueCodeInformation, + referencesCodeLens: { + navigation: true, + __referencesCodeLens: true, + } as VueCodeInformation, + cssClassNavigation: { + navigation: { + resolveRenameNewName: normalizeCssRename, + resolveRenameEditText: applyCssRename, + }, + } as VueCodeInformation, +}; + +export interface ScriptCodegenOptions { + fileBaseName: string; + ts: typeof ts; + compilerOptions: ts.CompilerOptions; + vueCompilerOptions: VueCompilerOptions; + sfc: Sfc; + lang: string; + scriptRanges: ScriptRanges | undefined; + scriptSetupRanges: ScriptSetupRanges | undefined; + templateCodegen: { + tsCodes: Code[]; + ctx: TemplateCodegenContext; + hasSlot: boolean; + } | undefined; + globalTypes: boolean; + getGeneratedLength: () => number; + linkedCodeMappings: Mapping[]; +} + +export function* generateScript(options: ScriptCodegenOptions): Generator { + const ctx = createScriptCodegenContext(options); + + yield `/* __placeholder__ */${newLine}`; + if (options.sfc.script?.src) { + yield* generateSrc(options.sfc.script, options.sfc.script.src); + } + if (options.sfc.script && options.scriptRanges) { + const { exportDefault } = options.scriptRanges; + const isExportRawObject = exportDefault + && options.sfc.script.content[exportDefault.expression.start] === '{'; + if (options.sfc.scriptSetup && options.scriptSetupRanges) { + yield generateScriptSetupImports(options.sfc.scriptSetup, options.scriptSetupRanges); + if (exportDefault) { + yield generateSfcBlockSection(options.sfc.script, 0, exportDefault.expression.start, codeFeatures.all); + yield* generateScriptSetup(options, ctx, options.sfc.scriptSetup, options.scriptSetupRanges); + yield generateSfcBlockSection(options.sfc.script, exportDefault.expression.end, options.sfc.script.content.length, codeFeatures.all); + } + else { + yield generateSfcBlockSection(options.sfc.script, 0, options.sfc.script.content.length, codeFeatures.all); + yield* generateScriptSetup(options, ctx, options.sfc.scriptSetup, options.scriptSetupRanges); + } + } + else if (exportDefault && isExportRawObject && options.vueCompilerOptions.optionsWrapper.length) { + yield generateSfcBlockSection(options.sfc.script, 0, exportDefault.expression.start, codeFeatures.all); + yield options.vueCompilerOptions.optionsWrapper[0]; + yield [ + '', + 'script', + exportDefault.expression.start, + { + __hint: { + setting: 'vue.inlayHints.optionsWrapper', + label: options.vueCompilerOptions.optionsWrapper.length + ? options.vueCompilerOptions.optionsWrapper[0] + : '[Missing optionsWrapper]', + tooltip: [ + 'This is virtual code that is automatically wrapped for type support, it does not affect your runtime behavior, you can customize it via `vueCompilerOptions.optionsWrapper` option in tsconfig / jsconfig.', + 'To hide it, you can set `"vue.inlayHints.optionsWrapper": false` in IDE settings.', + ].join('\n\n'), + } + }, + ]; + yield generateSfcBlockSection(options.sfc.script, exportDefault.expression.start, exportDefault.expression.end, codeFeatures.all); + yield [ + '', + 'script', + exportDefault.expression.end, + { + __hint: { + setting: 'vue.inlayHints.optionsWrapper', + label: options.vueCompilerOptions.optionsWrapper.length === 2 + ? options.vueCompilerOptions.optionsWrapper[1] + : '[Missing optionsWrapper]', + tooltip: '', + } + }, + ]; + yield options.vueCompilerOptions.optionsWrapper[1]; + yield generateSfcBlockSection(options.sfc.script, exportDefault.expression.end, options.sfc.script.content.length, codeFeatures.all); + } + else { + yield generateSfcBlockSection(options.sfc.script, 0, options.sfc.script.content.length, codeFeatures.all); + } + } + else if (options.sfc.scriptSetup && options.scriptSetupRanges) { + yield generateScriptSetupImports(options.sfc.scriptSetup, options.scriptSetupRanges); + yield* generateScriptSetup(options, ctx, options.sfc.scriptSetup, options.scriptSetupRanges); + } + if (options.globalTypes) { + yield generateGlobalTypes(options.vueCompilerOptions); + } + yield* ctx.generateHelperTypes(); + yield `\ntype __VLS_IntrinsicElementsCompletion = __VLS_IntrinsicElements${endOfLine}`; + + if (!ctx.generatedTemplate) { + yield* generateTemplate(options, ctx); + } + + if (options.sfc.scriptSetup) { + yield [ + '', + 'scriptSetup', + options.sfc.scriptSetup.content.length, + codeFeatures.verification, + ]; + } +} + +function normalizeCssRename(newName: string) { + return newName.startsWith('.') ? newName.slice(1) : newName; +} + +function applyCssRename(newName: string) { + return '.' + newName; +} diff --git a/packages/language-core/lib/codegen/script/internalComponent.ts b/packages/language-core/lib/codegen/script/internalComponent.ts new file mode 100644 index 0000000000..b517b32260 --- /dev/null +++ b/packages/language-core/lib/codegen/script/internalComponent.ts @@ -0,0 +1,64 @@ +import type { Code } from '../../types'; +import { endOfLine, newLine } from '../common'; +import type { TemplateCodegenContext } from '../template/context'; +import { generateComponentSetupReturns, generateScriptOptions, generateScriptSetupOptions } from './component'; +import type { ScriptCodegenContext } from './context'; +import type { ScriptCodegenOptions } from './index'; +import { getTemplateUsageVars } from './template'; + +export function* generateInternalComponent( + options: ScriptCodegenOptions, + ctx: ScriptCodegenContext, + templateCodegenCtx: TemplateCodegenContext, +): Generator { + if (options.sfc.scriptSetup && options.scriptSetupRanges) { + yield `const __VLS_internalComponent = (await import('${options.vueCompilerOptions.lib}')).defineComponent({${newLine}`; + yield `setup() {${newLine}`; + yield `return {${newLine}`; + if (ctx.bypassDefineComponent) { + yield* generateComponentSetupReturns(options.scriptSetupRanges); + } + // bindings + const templateUsageVars = getTemplateUsageVars(options, ctx); + for (const [content, bindings] of [ + [options.sfc.scriptSetup.content, options.scriptSetupRanges.bindings] as const, + options.sfc.script && options.scriptRanges + ? [options.sfc.script.content, options.scriptRanges.bindings] as const + : ['', []] as const, + ]) { + for (const expose of bindings) { + const varName = content.substring(expose.start, expose.end); + if (!templateUsageVars.has(varName) && !templateCodegenCtx.accessGlobalVariables.has(varName)) { + continue; + } + const templateOffset = options.getGeneratedLength(); + yield `${varName}: ${varName} as typeof `; + + const scriptOffset = options.getGeneratedLength(); + yield `${varName},${newLine}`; + + options.linkedCodeMappings.push({ + sourceOffsets: [scriptOffset], + generatedOffsets: [templateOffset], + lengths: [varName.length], + data: undefined, + }); + } + } + yield `}${endOfLine}`; // return { + yield `},${newLine}`; // setup() { + if (options.sfc.scriptSetup && options.scriptSetupRanges && !ctx.bypassDefineComponent) { + yield* generateScriptSetupOptions(ctx, options.sfc.scriptSetup, options.scriptSetupRanges); + } + if (options.sfc.script && options.scriptRanges) { + yield* generateScriptOptions(options.sfc.script, options.scriptRanges); + } + yield `})${endOfLine}`; // defineComponent { + } + else if (options.sfc.script) { + yield `const __VLS_internalComponent = (await import('./${options.fileBaseName}')).default${endOfLine}`; + } + else { + yield `const __VLS_internalComponent = (await import('${options.vueCompilerOptions.lib}')).defineComponent({})${endOfLine}`; + } +} diff --git a/packages/language-core/lib/codegen/script/scriptSetup.ts b/packages/language-core/lib/codegen/script/scriptSetup.ts new file mode 100644 index 0000000000..b19c64ce16 --- /dev/null +++ b/packages/language-core/lib/codegen/script/scriptSetup.ts @@ -0,0 +1,380 @@ +import type { ScriptSetupRanges } from '../../parsers/scriptSetupRanges'; +import type { Code, Sfc } from '../../types'; +import { endOfLine, generateSfcBlockSection, newLine } from '../common'; +import { generateComponent } from './component'; +import type { ScriptCodegenContext } from './context'; +import { ScriptCodegenOptions, codeFeatures } from './index'; +import { generateTemplate } from './template'; + +export function generateScriptSetupImports( + scriptSetup: NonNullable, + scriptSetupRanges: ScriptSetupRanges, +): Code { + return [ + scriptSetup.content.substring(0, Math.max(scriptSetupRanges.importSectionEndOffset, scriptSetupRanges.leadingCommentEndOffset)) + newLine, + 'scriptSetup', + 0, + codeFeatures.all, + ]; +} + +export function* generateScriptSetup( + options: ScriptCodegenOptions, + ctx: ScriptCodegenContext, + scriptSetup: NonNullable, + scriptSetupRanges: ScriptSetupRanges, +): Generator { + const definePropMirrors = new Map(); + + if (scriptSetup.generic) { + if (!options.scriptRanges?.exportDefault) { + yield `export default `; + } + yield `(<`; + yield [ + scriptSetup.generic, + scriptSetup.name, + scriptSetup.genericOffset, + codeFeatures.all, + ]; + if (!scriptSetup.generic.endsWith(`,`)) { + yield `,`; + } + yield `>(${newLine}` + + ` __VLS_props: Awaited['props'],${newLine}` + + ` __VLS_ctx?: ${ctx.helperTypes.Prettify.name}, 'attrs' | 'emit' | 'slots'>>,${newLine}` // use __VLS_Prettify for less dts code + + ` __VLS_expose?: NonNullable>['expose'],${newLine}` + + ` __VLS_setup = (async () => {${newLine}`; + yield* generateSetupFunction(options, ctx, scriptSetup, scriptSetupRanges, undefined, definePropMirrors); + yield ` return {} as {${newLine}` + + ` props: ${ctx.helperTypes.Prettify.name} & typeof __VLS_publicProps,${newLine}` + + ` expose(exposed: import('${options.vueCompilerOptions.lib}').ShallowUnwrapRef<${scriptSetupRanges.expose.define ? 'typeof __VLS_exposed' : '{}'}>): void,${newLine}` + + ` attrs: any,${newLine}` + + ` slots: ReturnType,${newLine}` + + ` emit: typeof ${scriptSetupRanges.emits.name ?? '__VLS_emit'} & typeof __VLS_modelEmitsType,${newLine}` + + ` }${endOfLine}`; + yield ` })(),${newLine}`; // __VLS_setup = (async () => { + yield `) => ({} as import('${options.vueCompilerOptions.lib}').VNode & { __ctx?: Awaited }))`; + } + else if (!options.sfc.script) { + // no script block, generate script setup code at root + yield* generateSetupFunction(options, ctx, scriptSetup, scriptSetupRanges, 'export default', definePropMirrors); + } + else { + if (!options.scriptRanges?.exportDefault) { + yield `export default `; + } + yield `await (async () => {${newLine}`; + yield* generateSetupFunction(options, ctx, scriptSetup, scriptSetupRanges, 'return', definePropMirrors); + yield `})()`; + } + + if (ctx.scriptSetupGeneratedOffset !== undefined) { + for (const defineProp of scriptSetupRanges.defineProp) { + if (!defineProp.name) { + continue; + } + const propName = scriptSetup.content.substring(defineProp.name.start, defineProp.name.end); + const propMirror = definePropMirrors.get(propName); + if (propMirror !== undefined) { + options.linkedCodeMappings.push({ + sourceOffsets: [defineProp.name.start + ctx.scriptSetupGeneratedOffset], + generatedOffsets: [propMirror], + lengths: [defineProp.name.end - defineProp.name.start], + data: undefined, + }); + } + } + } +} + +function* generateSetupFunction( + options: ScriptCodegenOptions, + ctx: ScriptCodegenContext, + scriptSetup: NonNullable, + scriptSetupRanges: ScriptSetupRanges, + syntax: 'return' | 'export default' | undefined, + definePropMirrors: Map, +): 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'; + + if (options.vueCompilerOptions.target >= 3.3) { + yield `const { `; + for (const macro of Object.keys(options.vueCompilerOptions.macros)) { + if (!ctx.bindingNames.has(macro)) { + yield macro + `, `; + } + } + yield `} = await import('${options.vueCompilerOptions.lib}')${endOfLine}`; + } + if (definePropProposalA) { + yield `declare function defineProp(name: string, options: { required: true } & Record): import('${options.vueCompilerOptions.lib}').ComputedRef${endOfLine}`; + yield `declare function defineProp(name: string, options: { default: any } & Record): import('${options.vueCompilerOptions.lib}').ComputedRef${endOfLine}`; + yield `declare function defineProp(name?: string, options?: any): import('${options.vueCompilerOptions.lib}').ComputedRef${endOfLine}`; + } + if (definePropProposalB) { + yield `declare function defineProp(value: T | (() => T), required?: boolean, rest?: any): import('${options.vueCompilerOptions.lib}').ComputedRef${endOfLine}`; + yield `declare function defineProp(value: T | (() => T) | undefined, required: true, rest?: any): import('${options.vueCompilerOptions.lib}').ComputedRef${endOfLine}`; + yield `declare function defineProp(value?: T | (() => T), required?: boolean, rest?: any): import('${options.vueCompilerOptions.lib}').ComputedRef${endOfLine}`; + } + + ctx.scriptSetupGeneratedOffset = options.getGeneratedLength() - scriptSetupRanges.importSectionEndOffset; + + let setupCodeModifies: [Code[], number, number][] = []; + if (scriptSetupRanges.props.define && !scriptSetupRanges.props.name) { + const range = scriptSetupRanges.props.withDefaults ?? scriptSetupRanges.props.define; + const statement = scriptSetupRanges.props.define.statement; + if (statement.start === range.start && statement.end === range.end) { + setupCodeModifies.push([[`const __VLS_props = `], range.start, range.start]); + } + else { + setupCodeModifies.push([[ + `const __VLS_props = `, + generateSfcBlockSection(scriptSetup, range.start, range.end, codeFeatures.all), + `${endOfLine}`, + generateSfcBlockSection(scriptSetup, statement.start, range.start, codeFeatures.all), + `__VLS_props`, + ], statement.start, range.end]); + } + } + if (scriptSetupRanges.slots.define && !scriptSetupRanges.slots.name) { + setupCodeModifies.push([[`const __VLS_slots = `], scriptSetupRanges.slots.define.start, scriptSetupRanges.slots.define.start]); + } + if (scriptSetupRanges.emits.define && !scriptSetupRanges.emits.name) { + setupCodeModifies.push([[`const __VLS_emit = `], scriptSetupRanges.emits.define.start, scriptSetupRanges.emits.define.start]); + } + if (scriptSetupRanges.expose.define) { + if (scriptSetupRanges.expose.define?.typeArg) { + setupCodeModifies.push([ + [ + `let __VLS_exposed!: `, + generateSfcBlockSection(scriptSetup, scriptSetupRanges.expose.define.typeArg.start, scriptSetupRanges.expose.define.typeArg.end, codeFeatures.navigation), + `${endOfLine}`, + ], + scriptSetupRanges.expose.define.start, + scriptSetupRanges.expose.define.start, + ]); + } + else if (scriptSetupRanges.expose.define?.arg) { + setupCodeModifies.push([ + [ + `const __VLS_exposed = `, + generateSfcBlockSection(scriptSetup, scriptSetupRanges.expose.define.arg.start, scriptSetupRanges.expose.define.arg.end, codeFeatures.navigation), + `${endOfLine}`, + ], + scriptSetupRanges.expose.define.start, + scriptSetupRanges.expose.define.start, + ]); + } + else { + setupCodeModifies.push([ + [`const __VLS_exposed = {}${endOfLine}`], + scriptSetupRanges.expose.define.start, + scriptSetupRanges.expose.define.start, + ]); + } + } + setupCodeModifies = setupCodeModifies.sort((a, b) => a[1] - b[1]); + + if (setupCodeModifies.length) { + yield generateSfcBlockSection(scriptSetup, scriptSetupRanges.importSectionEndOffset, setupCodeModifies[0][1], codeFeatures.all); + while (setupCodeModifies.length) { + const [codes, _start, end] = setupCodeModifies.shift()!; + for (const code of codes) { + yield code; + } + if (setupCodeModifies.length) { + const nextStart = setupCodeModifies[0][1]; + yield generateSfcBlockSection(scriptSetup, end, nextStart, codeFeatures.all); + } + else { + yield generateSfcBlockSection(scriptSetup, end, scriptSetup.content.length, codeFeatures.all); + } + } + } + else { + yield generateSfcBlockSection(scriptSetup, scriptSetupRanges.importSectionEndOffset, scriptSetup.content.length, codeFeatures.all); + } + + if (scriptSetupRanges.props.define?.typeArg && scriptSetupRanges.props.withDefaults?.arg) { + // fix https://github.com/vuejs/language-tools/issues/1187 + yield `const __VLS_withDefaultsArg = (function (t: T) { return t })(`; + yield generateSfcBlockSection(scriptSetup, scriptSetupRanges.props.withDefaults.arg.start, scriptSetupRanges.props.withDefaults.arg.end, codeFeatures.navigation); + yield `)${endOfLine}`; + } + + yield* generateComponentProps(options, ctx, scriptSetup, scriptSetupRanges, definePropMirrors); + yield* generateModelEmits(options, scriptSetup, scriptSetupRanges); + yield* generateTemplate(options, ctx); + + if (syntax) { + if (!options.vueCompilerOptions.skipTemplateCodegen && (options.templateCodegen?.hasSlot || scriptSetupRanges?.slots.define)) { + yield `const __VLS_component = `; + yield* generateComponent(options, ctx, scriptSetup, scriptSetupRanges); + yield endOfLine; + yield `${syntax} `; + yield `{} as ${ctx.helperTypes.WithTemplateSlots.name}>${endOfLine}`; + } + else { + yield `${syntax} `; + yield* generateComponent(options, ctx, scriptSetup, scriptSetupRanges); + yield endOfLine; + } + } +} + +function* generateComponentProps( + options: ScriptCodegenOptions, + ctx: ScriptCodegenContext, + scriptSetup: NonNullable, + scriptSetupRanges: ScriptSetupRanges, + definePropMirrors: Map, +): Generator { + if (scriptSetupRanges.props.define?.arg || scriptSetupRanges.emits.define) { + yield `const __VLS_fnComponent = ` + + `(await import('${options.vueCompilerOptions.lib}')).defineComponent({${newLine}`; + if (scriptSetupRanges.props.define?.arg) { + yield ` props: `; + yield generateSfcBlockSection(scriptSetup, scriptSetupRanges.props.define.arg.start, scriptSetupRanges.props.define.arg.end, codeFeatures.navigation); + yield `,${newLine}`; + } + if (scriptSetupRanges.emits.define) { + yield ` emits: ({} as __VLS_NormalizeEmits),${newLine}`; + } + yield `})${endOfLine}`; + yield `let __VLS_functionalComponentProps!: `; + yield `${ctx.helperTypes.OmitKeepDiscriminatedUnion.name}['$props'], keyof typeof __VLS_publicProps>`; + yield endOfLine; + } + else { + yield `let __VLS_functionalComponentProps!: {}${endOfLine}`; + } + + yield `let __VLS_publicProps!:${newLine}` + + ` import('${options.vueCompilerOptions.lib}').VNodeProps${newLine}` + + ` & import('${options.vueCompilerOptions.lib}').AllowedComponentProps${newLine}` + + ` & import('${options.vueCompilerOptions.lib}').ComponentCustomProps${endOfLine}`; + + if (scriptSetupRanges.defineProp.length) { + yield `const __VLS_defaults = {${newLine}`; + for (const defineProp of scriptSetupRanges.defineProp) { + if (defineProp.defaultValue) { + if (defineProp.name) { + yield scriptSetup.content.substring(defineProp.name.start, defineProp.name.end); + } + else { + yield `modelValue`; + } + yield `: `; + yield scriptSetup.content.substring(defineProp.defaultValue.start, defineProp.defaultValue.end); + yield `,${newLine}`; + } + } + yield `}${endOfLine}`; + } + + yield `let __VLS_componentProps!: `; + if (scriptSetupRanges.slots.define && options.vueCompilerOptions.jsxSlots) { + if (ctx.generatedPropsType) { + yield ` & `; + } + ctx.generatedPropsType = true; + yield `${ctx.helperTypes.PropsChildren.name}`; + } + if (scriptSetupRanges.defineProp.length) { + if (ctx.generatedPropsType) { + yield ` & `; + } + ctx.generatedPropsType = true; + yield `{${newLine}`; + for (const defineProp of scriptSetupRanges.defineProp) { + let propName = 'modelValue'; + if (defineProp.name && defineProp.nameIsString) { + // renaming support + yield generateSfcBlockSection(scriptSetup, defineProp.name.start, defineProp.name.end, codeFeatures.navigation); + } + else if (defineProp.name) { + propName = scriptSetup.content.substring(defineProp.name.start, defineProp.name.end); + definePropMirrors.set(propName, options.getGeneratedLength()); + yield propName; + } + else { + yield propName; + } + yield defineProp.required + ? `: ` + : `?: `; + if (defineProp.type) { + yield scriptSetup.content.substring(defineProp.type.start, defineProp.type.end); + } + else if (!defineProp.nameIsString) { + yield `NonNullable`; + } + else if (defineProp.defaultValue) { + yield `typeof __VLS_defaults['${propName}']`; + } + else { + yield `any`; + } + yield `,${newLine}`; + + if (defineProp.modifierType) { + let propModifierName = 'modelModifiers'; + if (defineProp.name) { + propModifierName = `${scriptSetup.content.substring(defineProp.name.start + 1, defineProp.name.end - 1)}Modifiers`; + } + const modifierType = scriptSetup.content.substring(defineProp.modifierType.start, defineProp.modifierType.end); + definePropMirrors.set(propModifierName, options.getGeneratedLength()); + yield `${propModifierName}?: Record<${modifierType}, true>,${endOfLine}`; + } + } + yield `}`; + } + if (scriptSetupRanges.props.define?.typeArg) { + if (ctx.generatedPropsType) { + yield ` & `; + } + ctx.generatedPropsType = true; + yield generateSfcBlockSection(scriptSetup, scriptSetupRanges.props.define.typeArg.start, scriptSetupRanges.props.define.typeArg.end, codeFeatures.all); + } + if (!ctx.generatedPropsType) { + yield `{}`; + } + yield endOfLine; +} + +function* generateModelEmits( + options: ScriptCodegenOptions, + scriptSetup: NonNullable, + scriptSetupRanges: ScriptSetupRanges, +): Generator { + yield `let __VLS_modelEmitsType!: {}`; + + if (scriptSetupRanges.defineProp.length) { + yield ` & ReturnType>`; + } + yield endOfLine; +} diff --git a/packages/language-core/lib/codegen/script/src.ts b/packages/language-core/lib/codegen/script/src.ts new file mode 100644 index 0000000000..1f03f672d5 --- /dev/null +++ b/packages/language-core/lib/codegen/script/src.ts @@ -0,0 +1,54 @@ +import type { Code, Sfc } from '../../types'; +import { endOfLine } from '../common'; +import { codeFeatures } from './index'; + +export function* generateSrc( + script: NonNullable, + src: string, +): Generator { + if (src.endsWith('.d.ts')) { + src = src.substring(0, src.length - '.d.ts'.length); + } + else if (src.endsWith('.ts')) { + src = src.substring(0, src.length - '.ts'.length); + } + else if (src.endsWith('.tsx')) { + src = src.substring(0, src.length - '.tsx'.length) + '.jsx'; + } + + if (!src.endsWith('.js') && !src.endsWith('.jsx')) { + src = src + '.js'; + } + + yield `export * from `; + yield [ + `'${src}'`, + 'script', + script.srcOffset - 1, + { + ...codeFeatures.all, + navigation: src === script.src + ? true + : { + shouldRename: () => false, + resolveRenameEditText(newName) { + if (newName.endsWith('.jsx') || newName.endsWith('.js')) { + newName = newName.split('.').slice(0, -1).join('.'); + } + if (script?.src?.endsWith('.d.ts')) { + newName = newName + '.d.ts'; + } + else if (script?.src?.endsWith('.ts')) { + newName = newName + '.ts'; + } + else if (script?.src?.endsWith('.tsx')) { + newName = newName + '.tsx'; + } + return newName; + }, + }, + }, + ]; + yield endOfLine; + yield `export { default } from '${src}'${endOfLine}`; +} diff --git a/packages/language-core/lib/codegen/script/template.ts b/packages/language-core/lib/codegen/script/template.ts new file mode 100644 index 0000000000..e2c12a9ac0 --- /dev/null +++ b/packages/language-core/lib/codegen/script/template.ts @@ -0,0 +1,245 @@ +import type * as ts from 'typescript'; +import type { Code } from '../../types'; +import { getSlotsPropertyName, hyphenateTag } from '../../utils/shared'; +import { endOfLine, newLine } from '../common'; +import { TemplateCodegenContext, createTemplateCodegenContext } from '../template/context'; +import { forEachInterpolationSegment } from '../template/interpolation'; +import type { ScriptCodegenContext } from './context'; +import { codeFeatures, type ScriptCodegenOptions } from './index'; +import { generateInternalComponent } from './internalComponent'; +import { combineLastMapping } from '../common'; + +export function* generateTemplate( + options: ScriptCodegenOptions, + ctx: ScriptCodegenContext, +): Generator { + + ctx.generatedTemplate = true; + + if (!options.vueCompilerOptions.skipTemplateCodegen) { + yield* generateExportOptions(options); + yield* generateConstNameOption(options); + yield `function __VLS_template() {${newLine}`; + const templateCodegenCtx = createTemplateCodegenContext(); + yield* generateTemplateContext(options, ctx, templateCodegenCtx); + yield `}${newLine}`; + yield* generateInternalComponent(options, ctx, templateCodegenCtx); + } + else { + yield `function __VLS_template() {${newLine}`; + const templateUsageVars = [...getTemplateUsageVars(options, ctx)]; + yield `// @ts-ignore${newLine}`; + yield `[${templateUsageVars.join(', ')}]${newLine}`; + yield `return {}${endOfLine}`; + yield `}${newLine}`; + } +} + +function* generateExportOptions(options: ScriptCodegenOptions): Generator { + yield newLine; + yield `const __VLS_componentsOption = `; + if (options.sfc.script && options.scriptRanges?.exportDefault?.componentsOption) { + const componentsOption = options.scriptRanges.exportDefault.componentsOption; + yield [ + options.sfc.script.content.substring(componentsOption.start, componentsOption.end), + 'script', + componentsOption.start, + codeFeatures.navigation, + ]; + } + else { + yield `{}`; + } + yield endOfLine; +} + +function* generateConstNameOption(options: ScriptCodegenOptions): Generator { + yield newLine; + if (options.sfc.script && options.scriptRanges?.exportDefault?.nameOption) { + const nameOption = options.scriptRanges.exportDefault.nameOption; + yield `const __VLS_name = `; + yield `${options.sfc.script.content.substring(nameOption.start, nameOption.end)} as const`; + yield endOfLine; + } + else if (options.sfc.scriptSetup) { + yield `let __VLS_name!: '${options.fileBaseName.substring(0, options.fileBaseName.lastIndexOf('.'))}'${endOfLine}`; + } + else { + yield `const __VLS_name = undefined${endOfLine}`; + } +} + +function* generateTemplateContext( + options: ScriptCodegenOptions, + ctx: ScriptCodegenContext, + templateCodegenCtx: TemplateCodegenContext, +): Generator { + + const useGlobalThisTypeInCtx = options.fileBaseName.endsWith('.html'); + + yield `let __VLS_ctx!: ${useGlobalThisTypeInCtx ? 'typeof globalThis &' : ''}`; + yield `InstanceType<__VLS_PickNotAny {}>> & {${newLine}`; + + /* CSS Module */ + for (let i = 0; i < options.sfc.styles.length; i++) { + const style = options.sfc.styles[i]; + if (style.module) { + yield `${style.module}: Record & ${ctx.helperTypes.Prettify.name}<{}`; + for (const className of style.classNames) { + yield* generateCssClassProperty( + i, + className.text, + className.offset, + 'string', + false, + true, + ); + } + yield `>${endOfLine}`; + } + } + yield `}${endOfLine}`; + + /* Components */ + yield `/* Components */${newLine}`; + yield `let __VLS_otherComponents!: NonNullable & typeof __VLS_componentsOption${endOfLine}`; + yield `let __VLS_own!: __VLS_SelfComponent { ${getSlotsPropertyName(options.vueCompilerOptions.target)}: typeof ${options.scriptSetupRanges?.slots?.name ?? '__VLS_slots'} })>${endOfLine}`; + yield `let __VLS_localComponents!: typeof __VLS_otherComponents & Omit${endOfLine}`; + yield `let __VLS_components!: typeof __VLS_localComponents & __VLS_GlobalComponents & typeof __VLS_ctx${endOfLine}`; // for html completion, TS references... + + /* Style Scoped */ + yield `/* Style Scoped */${newLine}`; + yield `type __VLS_StyleScopedClasses = {}`; + for (let i = 0; i < options.sfc.styles.length; i++) { + const style = options.sfc.styles[i]; + const option = options.vueCompilerOptions.experimentalResolveStyleCssClasses; + if (option === 'always' || (option === 'scoped' && style.scoped)) { + for (const className of style.classNames) { + yield* generateCssClassProperty( + i, + className.text, + className.offset, + 'boolean', + true, + !style.module, + ); + } + } + } + yield endOfLine; + yield `let __VLS_styleScopedClasses!: __VLS_StyleScopedClasses | keyof __VLS_StyleScopedClasses | (keyof __VLS_StyleScopedClasses)[]${endOfLine}`; + + yield `/* CSS variable injection */${newLine}`; + yield* generateCssVars(options, templateCodegenCtx); + yield `/* CSS variable injection end */${newLine}`; + + if (options.templateCodegen) { + for (const code of options.templateCodegen.tsCodes) { + yield code; + } + } + else { + yield `// no template${newLine}`; + if (!options.scriptSetupRanges?.slots.define) { + yield `const __VLS_slots = {}${endOfLine}`; + } + } + + yield `return ${options.scriptSetupRanges?.slots.name ?? '__VLS_slots'}${endOfLine}`; +} + +function* generateCssClassProperty( + styleIndex: number, + classNameWithDot: string, + offset: number, + propertyType: string, + optional: boolean, + referencesCodeLens: boolean +): Generator { + yield `${newLine} & { `; + yield [ + '', + 'style_' + styleIndex, + offset, + referencesCodeLens + ? codeFeatures.navigation + : codeFeatures.referencesCodeLens, + ]; + yield `'`; + yield [ + '', + 'style_' + styleIndex, + offset, + codeFeatures.cssClassNavigation, + ]; + yield [ + classNameWithDot.substring(1), + 'style_' + styleIndex, + offset + 1, + combineLastMapping, + ]; + yield `'`; + yield [ + '', + 'style_' + styleIndex, + offset + classNameWithDot.length, + codeFeatures.none, + ]; + yield `${optional ? '?' : ''}: ${propertyType}`; + yield ` }`; +} + +function* generateCssVars(options: ScriptCodegenOptions, ctx: TemplateCodegenContext): Generator { + for (const style of options.sfc.styles) { + for (const cssBind of style.cssVars) { + for (const [segment, offset, onlyError] of forEachInterpolationSegment( + options.ts, + options.vueCompilerOptions, + ctx, + cssBind.text, + cssBind.offset, + options.ts.createSourceFile('/a.txt', cssBind.text, 99 satisfies ts.ScriptTarget.ESNext), + )) { + if (offset === undefined) { + yield segment; + } + else { + yield [ + segment, + style.name, + cssBind.offset + offset, + onlyError + ? codeFeatures.navigation + : codeFeatures.all, + ]; + } + } + yield endOfLine; + } + } +} + +export function getTemplateUsageVars(options: ScriptCodegenOptions, ctx: ScriptCodegenContext) { + + const usageVars = new Set(); + const components = new Set(options.sfc.template?.ast?.components); + + if (options.templateCodegen) { + // fix import components unused report + for (const varName of ctx.bindingNames) { + if (components.has(varName) || components.has(hyphenateTag(varName))) { + usageVars.add(varName); + } + } + for (const component of components) { + if (component.indexOf('.') >= 0) { + usageVars.add(component.split('.')[0]); + } + } + for (const [varName] of options.templateCodegen.ctx.accessGlobalVariables) { + usageVars.add(varName); + } + } + + return usageVars; +} diff --git a/packages/language-core/lib/codegen/template/camelized.ts b/packages/language-core/lib/codegen/template/camelized.ts new file mode 100644 index 0000000000..d0aaab5b10 --- /dev/null +++ b/packages/language-core/lib/codegen/template/camelized.ts @@ -0,0 +1,29 @@ +import { capitalize } from '@vue/shared'; +import type { Code, VueCodeInformation } from '../../types'; +import { combineLastMapping } from '../common'; + +export function* generateCamelized(code: string, offset: number, info: VueCodeInformation): Generator { + const parts = code.split('-'); + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + if (part !== '') { + if (i === 0) { + yield [ + part, + 'template', + offset, + info, + ]; + } + else { + yield [ + capitalize(part), + 'template', + offset, + combineLastMapping, + ]; + } + } + offset += part.length + 1; + } +} diff --git a/packages/language-core/lib/codegen/template/context.ts b/packages/language-core/lib/codegen/template/context.ts new file mode 100644 index 0000000000..25aaf784fa --- /dev/null +++ b/packages/language-core/lib/codegen/template/context.ts @@ -0,0 +1,190 @@ +import type * as CompilerDOM from '@vue/compiler-dom'; +import type { Code, VueCodeInformation } from '../../types'; +import { endOfLine, newLine, wrapWith } from '../common'; + +const _codeFeatures = { + all: { + verification: true, + completion: true, + semantic: true, + navigation: true, + } as VueCodeInformation, + verification: { + verification: true, + } as VueCodeInformation, + completion: { + completion: true, + } as VueCodeInformation, + additionalCompletion: { + completion: { isAdditional: true }, + } as VueCodeInformation, + navigation: { + navigation: true, + } as VueCodeInformation, + navigationAndCompletion: { + navigation: true, + } as VueCodeInformation, + withoutHighlight: { + semantic: { shouldHighlight: () => false }, + verification: true, + navigation: true, + completion: true, + } as VueCodeInformation, + withoutHighlightAndCompletion: { + semantic: { shouldHighlight: () => false }, + verification: true, + navigation: true, + } as VueCodeInformation, + withoutHighlightAndCompletionAndNavigation: { + semantic: { shouldHighlight: () => false }, + verification: true, + } as VueCodeInformation, +}; + +export type TemplateCodegenContext = ReturnType; + +export function createTemplateCodegenContext() { + let ignoredError = false; + let expectErrorToken: { + errors: number; + node: CompilerDOM.CommentNode; + } | undefined; + let variableId = 0; + + const codeFeatures = new Proxy(_codeFeatures, { + get(target, key: keyof typeof _codeFeatures) { + const data = target[key]; + if (data.verification) { + if (ignoredError) { + return { + ...data, + verification: false, + }; + } + if (expectErrorToken) { + const token = expectErrorToken; + if (typeof data.verification !== 'object' || !data.verification.shouldReport) { + return { + ...data, + verification: { + shouldReport: () => { + token.errors++; + return false; + }, + }, + }; + } + } + } + return data; + }, + }); + const localVars = new Map(); + const accessGlobalVariables = new Map>(); + const slots: { + name: string; + loc?: number; + tagRange: [number, number]; + varName: string; + nodeLoc: any; + }[] = []; + const dynamicSlots: { + expVar: string; + varName: string; + }[] = []; + const hasSlotElements = new Set();; + const blockConditions: string[] = []; + const usedComponentCtxVars = new Set(); + const scopedClasses: { className: string, offset: number; }[] = []; + + return { + slots, + dynamicSlots, + codeFeatures, + accessGlobalVariables, + hasSlotElements, + blockConditions, + usedComponentCtxVars, + scopedClasses, + accessGlobalVariable(name: string, offset?: number) { + let arr = accessGlobalVariables.get(name); + if (!arr) { + accessGlobalVariables.set(name, arr = new Set()); + } + if (offset !== undefined) { + arr.add(offset); + } + }, + hasLocalVariable: (name: string) => { + return !!localVars.get(name); + }, + addLocalVariable: (name: string) => { + localVars.set(name, (localVars.get(name) ?? 0) + 1); + }, + removeLocalVariable: (name: string) => { + localVars.set(name, localVars.get(name)! - 1); + }, + getInternalVariable: () => { + return `__VLS_${variableId++}`; + }, + ignoreError: function* (): Generator { + if (!ignoredError) { + ignoredError = true; + yield `// @vue-ignore start${newLine}`; + } + }, + expectError: function* (prevNode: CompilerDOM.CommentNode): Generator { + if (!expectErrorToken) { + expectErrorToken = { + errors: 0, + node: prevNode, + }; + yield `// @vue-expect-error start${newLine}`; + } + }, + resetDirectiveComments: function* (endStr: string): Generator { + if (expectErrorToken) { + const token = expectErrorToken; + yield* wrapWith( + expectErrorToken.node.loc.start.offset, + expectErrorToken.node.loc.end.offset, + { + verification: { + shouldReport: () => token.errors === 0, + }, + }, + `// @ts-expect-error __VLS_TS_EXPECT_ERROR`, + ); + yield `${newLine}${endOfLine}`; + expectErrorToken = undefined; + yield `// @vue-expect-error ${endStr}${newLine}`; + } + if (ignoredError) { + ignoredError = false; + yield `// @vue-ignore ${endStr}${newLine}`; + } + }, + generateAutoImportCompletion: function* (): Generator { + const all = [...accessGlobalVariables.entries()]; + if (!all.some(([_, offsets]) => offsets.size)) { + yield `// no auto imports${endOfLine}`; + return; + } + yield `// @ts-ignore${newLine}`; // #2304 + yield `[`; + for (const [varName, offsets] of all) { + for (const offset of offsets) { + yield [ + varName, + 'template', + offset, + codeFeatures.additionalCompletion, + ]; + yield `,`; + } + offsets.clear(); + } + yield `]${endOfLine}`; + } + }; +} diff --git a/packages/language-core/lib/codegen/template/element.ts b/packages/language-core/lib/codegen/template/element.ts new file mode 100644 index 0000000000..1b0062ad8d --- /dev/null +++ b/packages/language-core/lib/codegen/template/element.ts @@ -0,0 +1,464 @@ +import * as CompilerDOM from '@vue/compiler-dom'; +import { camelize, capitalize } from '@vue/shared'; +import type { Code, VueCodeInformation } from '../../types'; +import { collectVars, createTsAst, endOfLine, newLine, variableNameRegex, wrapWith } from '../common'; +import { generateCamelized } from './camelized'; +import type { TemplateCodegenContext } from './context'; +import { generateElementChildren } from './elementChildren'; +import { generateElementDirectives } from './elementDirectives'; +import { generateElementEvents } from './elementEvents'; +import { generateElementProps } from './elementProps'; +import type { TemplateCodegenOptions } from './index'; +import { generateInterpolation } from './interpolation'; +import { generatePropertyAccess } from './propertyAccess'; +import { generateStringLiteralKey } from './stringLiteralKey'; +import { generateTemplateChild } from './templateChild'; + +const colonReg = /:/g; + +export function* generateElement( + options: TemplateCodegenOptions, + ctx: TemplateCodegenContext, + node: CompilerDOM.ElementNode, + currentElement: CompilerDOM.ElementNode | undefined, + componentCtxVar: string | undefined, +): Generator { + const startTagOffset = node.loc.start.offset + options.template.content.substring(node.loc.start.offset).indexOf(node.tag); + const propsFailedExps: CompilerDOM.SimpleExpressionNode[] = []; + const var_originalComponent = ctx.getInternalVariable(); + const var_functionalComponent = ctx.getInternalVariable(); + const var_componentInstance = ctx.getInternalVariable(); + const isIntrinsicElement = node.tagType === CompilerDOM.ElementTypes.ELEMENT + || node.tagType === CompilerDOM.ElementTypes.TEMPLATE; + const isComponentTag = node.tag.toLowerCase() === 'component'; + + let endTagOffset = !node.isSelfClosing && options.template.lang === 'html' ? node.loc.start.offset + node.loc.source.lastIndexOf(node.tag) : undefined; + let tag = node.tag; + let tagOffsets = endTagOffset !== undefined + ? [startTagOffset, endTagOffset] + : [startTagOffset]; + let props = node.props; + let dynamicTagInfo: { + exp: string; + offset: number; + astHolder: any; + } | undefined; + + if (isComponentTag) { + for (const prop of node.props) { + if (prop.type === CompilerDOM.NodeTypes.DIRECTIVE && prop.name === 'bind' && prop.arg?.loc.source === 'is' && prop.exp) { + dynamicTagInfo = { + exp: prop.exp.loc.source, + offset: prop.exp.loc.start.offset, + astHolder: prop.exp.loc, + }; + props = props.filter(p => p !== prop); + break; + } + } + } + else if (tag.includes('.')) { + // namespace tag + dynamicTagInfo = { + exp: tag, + astHolder: node.loc, + offset: startTagOffset, + }; + } + + if (isIntrinsicElement) { + yield `const ${var_originalComponent} = __VLS_intrinsicElements[`; + yield* generateStringLiteralKey( + tag, + startTagOffset, + ctx.codeFeatures.verification, + ); + yield `]${endOfLine}`; + } + else if (dynamicTagInfo) { + yield `const ${var_originalComponent} = `; + yield* generateInterpolation( + options, + ctx, + dynamicTagInfo.exp, + dynamicTagInfo.astHolder, + dynamicTagInfo.offset, + ctx.codeFeatures.all, + '(', + ')', + ); + yield endOfLine; + } + else if (!isComponentTag) { + yield `const ${var_originalComponent} = ({} as `; + for (const componentName of getPossibleOriginalComponentNames(tag, true)) { + yield `'${componentName}' extends keyof typeof __VLS_ctx ? { '${getCanonicalComponentName(tag)}': typeof __VLS_ctx`; + yield* generatePropertyAccess(options, ctx, componentName); + yield ` }: `; + } + yield `typeof __VLS_resolvedLocalAndGlobalComponents)`; + yield* generatePropertyAccess( + options, + ctx, + getCanonicalComponentName(tag), + startTagOffset, + ctx.codeFeatures.verification, + ); + yield endOfLine; + } + else { + yield `const ${var_originalComponent} = {} as any${endOfLine}`; + } + + if (isIntrinsicElement) { + yield `const ${var_functionalComponent} = __VLS_elementAsFunctionalComponent(${var_originalComponent})${endOfLine}`; + } + else { + yield `const ${var_functionalComponent} = __VLS_asFunctionalComponent(${var_originalComponent}, new ${var_originalComponent}({`; + yield* generateElementProps(options, ctx, node, props, 'navigationOnly'); + yield `}))${endOfLine}`; + } + + if ( + !dynamicTagInfo + && !isIntrinsicElement + && !isComponentTag + ) { + for (const offset of tagOffsets) { + yield `({} as { ${getCanonicalComponentName(tag)}: typeof ${var_originalComponent} }).`; + yield* generateCanonicalComponentName( + tag, + offset, + ctx.codeFeatures.withoutHighlightAndCompletionAndNavigation, + ); + yield endOfLine; + } + } + + if (options.vueCompilerOptions.strictTemplates) { + // with strictTemplates, generate once for props type-checking + instance type + yield `const ${var_componentInstance} = ${var_functionalComponent}(`; + yield ['', 'template', startTagOffset, ctx.codeFeatures.verification]; + yield `{`; + yield* generateElementProps(options, ctx, node, props, 'normal', propsFailedExps); + yield `}`; + yield ['', 'template', startTagOffset + tag.length, ctx.codeFeatures.verification]; + yield `, ...__VLS_functionalComponentArgsRest(${var_functionalComponent}))${endOfLine}`; + } + else { + // without strictTemplates, this only for instacne type + yield `const ${var_componentInstance} = ${var_functionalComponent}({`; + yield* generateElementProps(options, ctx, node, props, 'navigationOnly'); + yield `}, ...__VLS_functionalComponentArgsRest(${var_functionalComponent}))${endOfLine}`; + // and this for props type-checking + yield `({} as (props: __VLS_FunctionalComponentProps & Record) => void)(`; + yield ['', 'template', startTagOffset, ctx.codeFeatures.verification]; + yield `{`; + yield* generateElementProps(options, ctx, node, props, 'normal', propsFailedExps); + yield `}`; + yield ['', 'template', startTagOffset + tag.length, ctx.codeFeatures.verification]; + yield `)${endOfLine}`; + } + + let defineComponentCtxVar: string | undefined; + + if (node.tagType !== CompilerDOM.ElementTypes.TEMPLATE) { + defineComponentCtxVar = ctx.getInternalVariable(); + componentCtxVar = defineComponentCtxVar; + currentElement = node; + } + + const componentEventsVar = ctx.getInternalVariable(); + + let usedComponentEventsVar = false; + + //#region + // fix https://github.com/vuejs/language-tools/issues/1775 + for (const failedExp of propsFailedExps) { + yield* generateInterpolation( + options, + ctx, + failedExp.loc.source, + failedExp.loc, + failedExp.loc.start.offset, + ctx.codeFeatures.all, + '(', + ')', + ); + yield endOfLine; + } + + const vScope = props.find(prop => prop.type === CompilerDOM.NodeTypes.DIRECTIVE && (prop.name === 'scope' || prop.name === 'data')); + let inScope = false; + let originalConditionsNum = ctx.blockConditions.length; + + if (vScope?.type === CompilerDOM.NodeTypes.DIRECTIVE && vScope.exp) { + + const scopeVar = ctx.getInternalVariable(); + const condition = `__VLS_withScope(__VLS_ctx, ${scopeVar})`; + + yield `const ${scopeVar} = `; + yield [ + vScope.exp.loc.source, + 'template', + vScope.exp.loc.start.offset, + ctx.codeFeatures.all, + ]; + yield endOfLine; + yield `if (${condition}) {${newLine}`; + ctx.blockConditions.push(condition); + inScope = true; + } + + yield* generateElementDirectives(options, ctx, node); + yield* generateReferencesForElements(options, ctx, node); // + if (options.shouldGenerateScopedClasses) { + yield* generateReferencesForScopedCssClasses(ctx, node); + } + if (componentCtxVar) { + ctx.usedComponentCtxVars.add(componentCtxVar); + yield* generateElementEvents(options, ctx, node, var_functionalComponent, var_componentInstance, componentEventsVar, () => usedComponentEventsVar = true); + } + + if (inScope) { + yield `}${newLine}`; + ctx.blockConditions.length = originalConditionsNum; + } + //#endregion + + const slotDir = node.props.find(p => p.type === CompilerDOM.NodeTypes.DIRECTIVE && p.name === 'slot') as CompilerDOM.DirectiveNode; + if (slotDir && componentCtxVar) { + yield* generateComponentSlot(options, ctx, node, slotDir, currentElement, componentCtxVar); + } + else { + yield* generateElementChildren(options, ctx, node, currentElement, componentCtxVar); + } + + if (defineComponentCtxVar && ctx.usedComponentCtxVars.has(defineComponentCtxVar)) { + yield `const ${componentCtxVar} = __VLS_pickFunctionalComponentCtx(${var_originalComponent}, ${var_componentInstance})!${endOfLine}`; + } + if (usedComponentEventsVar) { + yield `let ${componentEventsVar}!: __VLS_NormalizeEmits${endOfLine}`; + } +} + +export function getCanonicalComponentName(tagText: string) { + return variableNameRegex.test(tagText) + ? tagText + : capitalize(camelize(tagText.replace(colonReg, '-'))); +} + +export function getPossibleOriginalComponentNames(tagText: string, deduplicate: boolean) { + const name1 = capitalize(camelize(tagText)); + const name2 = camelize(tagText); + const name3 = tagText; + const names: string[] = [name1]; + if (!deduplicate || name2 !== name1) { + names.push(name2); + } + if (!deduplicate || name3 !== name2) { + names.push(name3); + } + return names; +} + +function* generateCanonicalComponentName(tagText: string, offset: number, features: VueCodeInformation): Generator { + if (variableNameRegex.test(tagText)) { + yield [tagText, 'template', offset, features]; + } + else { + yield* generateCamelized( + capitalize(tagText.replace(colonReg, '-')), + offset, + features + ); + } +} + +function* generateComponentSlot( + options: TemplateCodegenOptions, + ctx: TemplateCodegenContext, + node: CompilerDOM.ElementNode, + slotDir: CompilerDOM.DirectiveNode, + currentElement: CompilerDOM.ElementNode | undefined, + componentCtxVar: string, +): Generator { + yield `{${newLine}`; + ctx.usedComponentCtxVars.add(componentCtxVar); + if (currentElement) { + ctx.hasSlotElements.add(currentElement); + } + const slotBlockVars: string[] = []; + let hasProps = false; + if (slotDir?.exp?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION) { + + const slotAst = createTsAst(options.ts, slotDir, `(${slotDir.exp.content}) => {}`); + collectVars(options.ts, slotAst, slotAst, slotBlockVars); + hasProps = true; + if (!slotDir.exp.content.includes(':')) { + yield `const [`; + yield [ + slotDir.exp.content, + 'template', + slotDir.exp.loc.start.offset, + ctx.codeFeatures.all, + ]; + yield `] = __VLS_getSlotParams(`; + } + else { + yield `const `; + yield [ + slotDir.exp.content, + 'template', + slotDir.exp.loc.start.offset, + ctx.codeFeatures.all, + ]; + yield ` = __VLS_getSlotParam(`; + } + } + yield* wrapWith( + (slotDir.arg ?? slotDir).loc.start.offset, + (slotDir.arg ?? slotDir).loc.end.offset, + ctx.codeFeatures.verification, + `(${componentCtxVar}.slots!)`, + ...( + slotDir?.arg?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION && slotDir.arg.content + ? generatePropertyAccess( + options, + ctx, + slotDir.arg.loc.source, + slotDir.arg.loc.start.offset, + slotDir.arg.isStatic ? ctx.codeFeatures.withoutHighlight : ctx.codeFeatures.all, + slotDir.arg.loc + ) + : [ + `.`, + ...wrapWith( + slotDir.loc.start.offset, + slotDir.loc.start.offset + ( + slotDir.loc.source.startsWith('#') + ? '#'.length + : slotDir.loc.source.startsWith('v-slot:') + ? 'v-slot:'.length + : 0 + ), + ctx.codeFeatures.withoutHighlightAndCompletion, + `default`, + ) + ] + ) + ); + if (hasProps) { + yield `)`; + } + yield endOfLine; + + for (const varName of slotBlockVars) { + ctx.addLocalVariable(varName); + } + + yield* ctx.resetDirectiveComments('end of slot children start'); + + let prev: CompilerDOM.TemplateChildNode | undefined; + for (const childNode of node.children) { + yield* generateTemplateChild(options, ctx, childNode, currentElement, prev, componentCtxVar); + prev = childNode; + } + + for (const varName of slotBlockVars) { + ctx.removeLocalVariable(varName); + } + let isStatic = true; + if (slotDir?.arg?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION) { + isStatic = slotDir.arg.isStatic; + } + if (isStatic && slotDir && !slotDir.arg) { + yield `${componentCtxVar}.slots!['`; + yield [ + '', + 'template', + slotDir.loc.start.offset + ( + slotDir.loc.source.startsWith('#') + ? '#'.length : slotDir.loc.source.startsWith('v-slot:') + ? 'v-slot:'.length + : 0 + ), + ctx.codeFeatures.completion, + ]; + yield `'/* empty slot name completion */]${newLine}`; + } + + yield* ctx.generateAutoImportCompletion(); + yield `}${newLine}`; +} + +function* generateReferencesForElements( + options: TemplateCodegenOptions, + ctx: TemplateCodegenContext, + node: CompilerDOM.ElementNode, +): Generator { + for (const prop of node.props) { + if ( + prop.type === CompilerDOM.NodeTypes.ATTRIBUTE + && prop.name === 'ref' + && prop.value + ) { + yield `// @ts-ignore${newLine}`; + yield* generateInterpolation( + options, + ctx, + prop.value.content, + prop.value.loc, + prop.value.loc.start.offset + 1, + ctx.codeFeatures.navigation, + '(', + ')', + ); + yield endOfLine; + } + } +} + +function* generateReferencesForScopedCssClasses( + ctx: TemplateCodegenContext, + node: CompilerDOM.ElementNode, +): Generator { + for (const prop of node.props) { + if ( + prop.type === CompilerDOM.NodeTypes.ATTRIBUTE + && prop.name === 'class' + && prop.value + ) { + let startOffset = prop.value.loc.start.offset; + let tempClassName = ''; + for (const char of (prop.value.loc.source + ' ')) { + if (char.trim() === '' || char === '"' || char === "'") { + if (tempClassName !== '') { + ctx.scopedClasses.push({ className: tempClassName, offset: startOffset }); + startOffset += tempClassName.length; + tempClassName = ''; + } + startOffset += char.length; + } + else { + tempClassName += char; + } + } + } + else if ( + prop.type === CompilerDOM.NodeTypes.DIRECTIVE + && prop.arg?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION + && prop.exp?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION + && prop.arg.content === 'class' + ) { + yield `__VLS_styleScopedClasses = (`; + yield [ + prop.exp.content, + 'template', + prop.exp.loc.start.offset, + ctx.codeFeatures.navigationAndCompletion, + ]; + yield `)${endOfLine}`; + } + } +} diff --git a/packages/language-core/lib/codegen/template/elementChildren.ts b/packages/language-core/lib/codegen/template/elementChildren.ts new file mode 100644 index 0000000000..eb0a850b75 --- /dev/null +++ b/packages/language-core/lib/codegen/template/elementChildren.ts @@ -0,0 +1,33 @@ +import type * as CompilerDOM from '@vue/compiler-dom'; +import type { Code } from '../../types'; +import { endOfLine, wrapWith } from '../common'; +import type { TemplateCodegenContext } from './context'; +import type { TemplateCodegenOptions } from './index'; +import { generateTemplateChild } from './templateChild'; + +export function* generateElementChildren( + options: TemplateCodegenOptions, + ctx: TemplateCodegenContext, + node: CompilerDOM.ElementNode, + currentElement: CompilerDOM.ElementNode | undefined, + componentCtxVar: string | undefined, +): Generator { + yield* ctx.resetDirectiveComments('end of element children start'); + let prev: CompilerDOM.TemplateChildNode | undefined; + for (const childNode of node.children) { + yield* generateTemplateChild(options, ctx, childNode, currentElement, prev, componentCtxVar); + prev = childNode; + } + + // fix https://github.com/vuejs/language-tools/issues/932 + if (!ctx.hasSlotElements.has(node) && node.children.length) { + yield `(${componentCtxVar}.slots!).`; + yield* wrapWith( + node.children[0].loc.start.offset, + node.children[node.children.length - 1].loc.end.offset, + ctx.codeFeatures.navigation, + `default`, + ); + yield endOfLine; + } +} diff --git a/packages/language-core/lib/codegen/template/elementDirectives.ts b/packages/language-core/lib/codegen/template/elementDirectives.ts new file mode 100644 index 0000000000..e70dbab994 --- /dev/null +++ b/packages/language-core/lib/codegen/template/elementDirectives.ts @@ -0,0 +1,92 @@ +import * as CompilerDOM from '@vue/compiler-dom'; +import { camelize } from '@vue/shared'; +import type { Code } from '../../types'; +import { hyphenateAttr } from '../../utils/shared'; +import { endOfLine, wrapWith } from '../common'; +import { generateCamelized } from './camelized'; +import type { TemplateCodegenContext } from './context'; +import type { TemplateCodegenOptions } from './index'; +import { generateInterpolation } from './interpolation'; + +export function* generateElementDirectives( + options: TemplateCodegenOptions, + ctx: TemplateCodegenContext, + node: CompilerDOM.ElementNode, +): Generator { + for (const prop of node.props) { + if ( + prop.type === CompilerDOM.NodeTypes.DIRECTIVE + && prop.name !== 'slot' + && prop.name !== 'on' + && prop.name !== 'model' + && prop.name !== 'bind' + && prop.name !== 'scope' + && prop.name !== 'data' + ) { + ctx.accessGlobalVariable(camelize('v-' + prop.name), prop.loc.start.offset); + + if (prop.arg?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION && !prop.arg.isStatic) { + yield* generateInterpolation( + options, + ctx, + prop.arg.content, + prop.arg.loc, + prop.arg.loc.start.offset + prop.arg.loc.source.indexOf(prop.arg.content), + ctx.codeFeatures.all, + '(', + ')', + ); + yield endOfLine; + } + + yield* wrapWith( + prop.loc.start.offset, + prop.loc.end.offset, + ctx.codeFeatures.verification, + `__VLS_directiveFunction(__VLS_ctx.`, + ...generateCamelized( + 'v-' + prop.name, + prop.loc.start.offset, + { + ...ctx.codeFeatures.all, + verification: false, + completion: { + // fix https://github.com/vuejs/language-tools/issues/1905 + isAdditional: true, + }, + navigation: { + resolveRenameNewName: camelize, + resolveRenameEditText: getPropRenameApply(prop.name), + }, + } + ), + `)(`, + ...( + prop.exp?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION + ? wrapWith( + prop.exp.loc.start.offset, + prop.exp.loc.end.offset, + ctx.codeFeatures.verification, + ...generateInterpolation( + options, + ctx, + prop.exp.content, + prop.exp.loc, + prop.exp.loc.start.offset, + ctx.codeFeatures.all, + '(', + ')', + ), + ) + : [`undefined`] + ), + `)`, + ); + yield endOfLine; + } + } +} + +function getPropRenameApply(oldName: string) { + return oldName === hyphenateAttr(oldName) ? hyphenateAttr : undefined; +} diff --git a/packages/language-core/lib/codegen/template/elementEvents.ts b/packages/language-core/lib/codegen/template/elementEvents.ts new file mode 100644 index 0000000000..ce74849690 --- /dev/null +++ b/packages/language-core/lib/codegen/template/elementEvents.ts @@ -0,0 +1,215 @@ +import * as CompilerDOM from '@vue/compiler-dom'; +import { camelize, capitalize } from '@vue/shared'; +import type * as ts from 'typescript'; +import type { Code, VueCodeInformation } from '../../types'; +import { hyphenateAttr } from '../../utils/shared'; +import { combineLastMapping, createTsAst, endOfLine, newLine, variableNameRegex, wrapWith } from '../common'; +import { generateCamelized } from './camelized'; +import type { TemplateCodegenContext } from './context'; +import type { TemplateCodegenOptions } from './index'; +import { generateInterpolation } from './interpolation'; +import { generateObjectProperty } from './objectProperty'; + +export function* generateElementEvents( + options: TemplateCodegenOptions, + ctx: TemplateCodegenContext, + node: CompilerDOM.ElementNode, + componentVar: string, + componentInstanceVar: string, + eventsVar: string, + used: () => void, +): Generator { + for (const prop of node.props) { + if ( + prop.type === CompilerDOM.NodeTypes.DIRECTIVE + && prop.name === 'on' + && prop.arg?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION + ) { + used(); + const eventVar = ctx.getInternalVariable(); + yield `let ${eventVar} = { '${prop.arg.loc.source}': __VLS_pickEvent(`; + yield `${eventsVar}['${prop.arg.loc.source}'], `; + yield `({} as __VLS_FunctionalComponentProps)`; + const startMappingFeatures: VueCodeInformation = { + navigation: { + // @click-outside -> onClickOutside + resolveRenameNewName(newName) { + return camelize('on-' + newName); + }, + // onClickOutside -> @click-outside + resolveRenameEditText(newName) { + const hName = hyphenateAttr(newName); + if (hyphenateAttr(newName).startsWith('on-')) { + return camelize(hName.slice('on-'.length)); + } + return newName; + }, + }, + }; + if (variableNameRegex.test(camelize(prop.arg.loc.source))) { + yield `.`; + yield ['', 'template', prop.arg.loc.start.offset, startMappingFeatures]; + yield `on`; + yield* generateCamelized( + capitalize(prop.arg.loc.source), + prop.arg.loc.start.offset, + combineLastMapping, + ); + } + else { + yield `[`; + yield* wrapWith( + prop.arg.loc.start.offset, + prop.arg.loc.end.offset, + startMappingFeatures, + `'`, + ['', 'template', prop.arg.loc.start.offset, combineLastMapping], + 'on', + ...generateCamelized( + capitalize(prop.arg.loc.source), + prop.arg.loc.start.offset, + combineLastMapping, + ), + `'`, + ); + yield `]`; + } + yield `) }${endOfLine}`; + yield `${eventVar} = { `; + if (prop.arg.loc.source.startsWith('[') && prop.arg.loc.source.endsWith(']')) { + yield `[(`; + yield* generateInterpolation( + options, + ctx, + prop.arg.loc.source.slice(1, -1), + prop.arg.loc, + prop.arg.loc.start.offset + 1, + ctx.codeFeatures.all, + '', + '', + ); + yield `)!]`; + } + else { + yield* generateObjectProperty( + options, + ctx, + prop.arg.loc.source, + prop.arg.loc.start.offset, + ctx.codeFeatures.withoutHighlightAndCompletionAndNavigation, + prop.arg.loc + ); + } + yield `: `; + yield* appendExpressionNode(options, ctx, prop); + yield ` }${endOfLine}`; + } + else if ( + prop.type === CompilerDOM.NodeTypes.DIRECTIVE + && prop.name === 'on' + && prop.exp?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION + ) { + // for vue 2 nameless event + // https://github.com/johnsoncodehk/vue-tsc/issues/67 + yield* generateInterpolation( + options, + ctx, + prop.exp.content, + prop.exp.loc, + prop.exp.loc.start.offset, + ctx.codeFeatures.all, + '$event => {(', + ')}', + ); + yield endOfLine; + } + } +} + +function* appendExpressionNode( + options: TemplateCodegenOptions, + ctx: TemplateCodegenContext, + prop: CompilerDOM.DirectiveNode, +): Generator { + if (prop.exp?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION) { + let prefix = '('; + let suffix = ')'; + let isFirstMapping = true; + + const ast = createTsAst(options.ts, prop.exp, prop.exp.content); + const _isCompoundExpression = isCompoundExpression(options.ts, ast); + if (_isCompoundExpression) { + + yield `$event => {${newLine}`; + ctx.addLocalVariable('$event'); + + prefix = ''; + suffix = ''; + for (const blockCondition of ctx.blockConditions) { + prefix += `if (!(${blockCondition})) return${endOfLine}`; + } + } + + yield* generateInterpolation( + options, + ctx, + prop.exp.content, + prop.exp.loc, + prop.exp.loc.start.offset, + () => { + if (_isCompoundExpression && isFirstMapping) { + isFirstMapping = false; + return { + ...ctx.codeFeatures.all, + __hint: { + setting: 'vue.inlayHints.inlineHandlerLeading', + label: '$event =>', + tooltip: [ + '`$event` is a hidden parameter, you can use it in this callback.', + 'To hide this hint, set `vue.inlayHints.inlineHandlerLeading` to `false` in IDE settings.', + '[More info](https://github.com/vuejs/language-tools/issues/2445#issuecomment-1444771420)', + ].join('\n\n'), + paddingRight: true, + }, + }; + } + return ctx.codeFeatures.all; + }, + prefix, + suffix, + ); + + if (_isCompoundExpression) { + ctx.removeLocalVariable('$event'); + + yield endOfLine; + yield* ctx.generateAutoImportCompletion(); + yield `}${newLine}`; + } + } + else { + yield `() => {}`; + } +} + +export function isCompoundExpression(ts: typeof import('typescript'), ast: ts.SourceFile) { + let result = true; + if (ast.statements.length === 1) { + ts.forEachChild(ast, child_1 => { + if (ts.isExpressionStatement(child_1)) { + ts.forEachChild(child_1, child_2 => { + if (ts.isArrowFunction(child_2)) { + result = false; + } + else if (ts.isIdentifier(child_2)) { + result = false; + } + }); + } + else if (ts.isFunctionDeclaration(child_1)) { + result = false; + } + }); + } + return result; +} diff --git a/packages/language-core/lib/codegen/template/elementProps.ts b/packages/language-core/lib/codegen/template/elementProps.ts new file mode 100644 index 0000000000..92d414a3e4 --- /dev/null +++ b/packages/language-core/lib/codegen/template/elementProps.ts @@ -0,0 +1,363 @@ +import * as CompilerDOM from '@vue/compiler-dom'; +import { camelize } from '@vue/shared'; +import { minimatch } from 'minimatch'; +import type { Code, VueCodeInformation, VueCompilerOptions } from '../../types'; +import { hyphenateAttr, hyphenateTag } from '../../utils/shared'; +import { conditionWrapWith, variableNameRegex, wrapWith } from '../common'; +import { generateCamelized } from './camelized'; +import type { TemplateCodegenContext } from './context'; +import type { TemplateCodegenOptions } from './index'; +import { generateInterpolation } from './interpolation'; +import { generateObjectProperty } from './objectProperty'; + +export function* generateElementProps( + options: TemplateCodegenOptions, + ctx: TemplateCodegenContext, + node: CompilerDOM.ElementNode, + props: CompilerDOM.ElementNode['props'], + mode: 'normal' | 'navigationOnly', + propsFailedExps?: CompilerDOM.SimpleExpressionNode[], +): Generator { + + let styleAttrNum = 0; + let classAttrNum = 0; + let defaultCodeFeatures: VueCodeInformation = ctx.codeFeatures.all; + let attrCodeFeatures: VueCodeInformation = ctx.codeFeatures.withoutHighlightAndCompletion; + + const canCamelize = node.tagType === CompilerDOM.ElementTypes.COMPONENT; + + if (props.some(prop => + prop.type === CompilerDOM.NodeTypes.DIRECTIVE + && prop.name === 'bind' + && !prop.arg + && prop.exp?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION + )) { + // fix https://github.com/vuejs/language-tools/issues/2166 + styleAttrNum++; + classAttrNum++; + } + + if (mode === 'navigationOnly') { + defaultCodeFeatures = ctx.codeFeatures.navigation; + attrCodeFeatures = ctx.codeFeatures.navigation; + } + + yield `...{ `; + for (const prop of props) { + if ( + prop.type === CompilerDOM.NodeTypes.DIRECTIVE + && prop.name === 'on' + && prop.arg?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION + ) { + yield `'${camelize('on-' + prop.arg.loc.source)}': {} as any, `; + } + } + yield `}, `; + + for (const prop of props) { + if ( + prop.type === CompilerDOM.NodeTypes.DIRECTIVE + && (prop.name === 'bind' || prop.name === 'model') + && (prop.name === 'model' || prop.arg?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION) + && (!prop.exp || prop.exp.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION) + ) { + let propName = + prop.arg?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION + ? prop.arg.constType === CompilerDOM.ConstantTypes.CAN_STRINGIFY + ? prop.arg.content + : prop.arg.loc.source + : getModelValuePropName(node, options.vueCompilerOptions.target, options.vueCompilerOptions); + + if (prop.modifiers.some(m => m === 'prop' || m === 'attr')) { + propName = propName?.substring(1); + } + + if ( + propName === undefined + || options.vueCompilerOptions.dataAttributes.some(pattern => minimatch(propName!, pattern)) + || (propName === 'style' && ++styleAttrNum >= 2) + || (propName === 'class' && ++classAttrNum >= 2) + || (propName === 'name' && node.tagType === CompilerDOM.ElementTypes.SLOT) // #2308 + ) { + if (prop.exp && prop.exp.constType !== CompilerDOM.ConstantTypes.CAN_STRINGIFY) { + propsFailedExps?.push(prop.exp); + } + continue; + } + + const shouldCamelize = canCamelize + && (!prop.arg || (prop.arg.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION && prop.arg.isStatic)) // isStatic + && hyphenateAttr(propName) === propName + && !options.vueCompilerOptions.htmlAttributes.some(pattern => minimatch(propName!, pattern)); + + yield* wrapWith( + prop.loc.start.offset, + prop.loc.end.offset, + ctx.codeFeatures.verification, + ...generateObjectProperty( + options, + ctx, + propName, + prop.arg + ? prop.arg.loc.start.offset + : prop.loc.start.offset, + prop.arg + ? { + ...attrCodeFeatures, + navigation: attrCodeFeatures.navigation + ? { + resolveRenameNewName: camelize, + resolveRenameEditText: shouldCamelize ? hyphenateAttr : undefined, + } + : false, + } + : attrCodeFeatures, + (prop.loc as any).name_2 ?? ((prop.loc as any).name_2 = {}), + shouldCamelize, + ), + `: (`, + ...genereatePropExp( + options, + ctx, + prop.exp, + attrCodeFeatures, + prop.arg?.loc.start.offset === prop.exp?.loc.start.offset, + mode === 'normal', + ), + `)`, + ); + yield `, `; + } + else if (prop.type === CompilerDOM.NodeTypes.ATTRIBUTE) { + if ( + options.vueCompilerOptions.dataAttributes.some(pattern => minimatch(prop.name, pattern)) + || (prop.name === 'style' && ++styleAttrNum >= 2) + || (prop.name === 'class' && ++classAttrNum >= 2) + || (prop.name === 'name' && node.tagType === CompilerDOM.ElementTypes.SLOT) // #2308 + ) { + continue; + } + + if ( + options.vueCompilerOptions.target < 3 + && prop.name === 'persisted' + && node.tag.toLowerCase() === 'transition' + ) { + // Vue 2 Transition doesn't support "persisted" property but `@vue/compiler-dom always adds it (#3881) + continue; + } + + const shouldCamelize = canCamelize + && hyphenateAttr(prop.name) === prop.name + && !options.vueCompilerOptions.htmlAttributes.some(pattern => minimatch(prop.name, pattern)); + + yield* conditionWrapWith( + mode === 'normal', + prop.loc.start.offset, + prop.loc.end.offset, + ctx.codeFeatures.verification, + ...generateObjectProperty( + options, + ctx, + prop.name, + prop.loc.start.offset, + shouldCamelize + ? { + ...attrCodeFeatures, + navigation: attrCodeFeatures.navigation + ? { + resolveRenameNewName: camelize, + resolveRenameEditText: hyphenateAttr, + } + : false, + } + : attrCodeFeatures, + (prop.loc as any).name_1 ?? ((prop.loc as any).name_1 = {}), + shouldCamelize, + ), + `: (`, + ...( + prop.value + ? generateAttrValue(prop.value, defaultCodeFeatures) + : [`true`] + ), + `)`, + ); + yield `, `; + } + else if ( + prop.type === CompilerDOM.NodeTypes.DIRECTIVE + && prop.name === 'bind' + && !prop.arg + && prop.exp?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION + ) { + yield* conditionWrapWith( + mode === 'normal', + prop.exp.loc.start.offset, + prop.exp.loc.end.offset, + ctx.codeFeatures.verification, + `...`, + ...generateInterpolation( + options, + ctx, + prop.exp.content, + prop.exp.loc, + prop.exp.loc.start.offset, + defaultCodeFeatures, + '(', + ')', + ), + ); + yield `, `; + } + else { + // comment this line to avoid affecting comments in prop expressions + // tsCodeGen.addText("/* " + [prop.type, prop.name, prop.arg?.loc.source, prop.exp?.loc.source, prop.loc.source].join(", ") + " */ "); + } + } +} + +function* genereatePropExp( + options: TemplateCodegenOptions, + ctx: TemplateCodegenContext, + exp: CompilerDOM.SimpleExpressionNode | undefined, + features: VueCodeInformation, + isShorthand: boolean, + inlayHints: boolean, +): Generator { + if (exp && exp.constType !== CompilerDOM.ConstantTypes.CAN_STRINGIFY) { // style='z-index: 2' will compile to {'z-index':'2'} + if (!isShorthand) { // vue 3.4+ + yield* generateInterpolation( + options, + ctx, + exp.loc.source, + exp.loc, + exp.loc.start.offset, + features, + '(', + ')', + ); + } else { + const propVariableName = camelize(exp.loc.source); + + if (variableNameRegex.test(propVariableName)) { + if (!ctx.hasLocalVariable(propVariableName)) { + ctx.accessGlobalVariable(propVariableName, exp.loc.start.offset); + yield `__VLS_ctx.`; + } + yield* generateCamelized( + exp.loc.source, + exp.loc.start.offset, + features, + ); + if (inlayHints) { + yield [ + '', + 'template', + exp.loc.end.offset, + { + __hint: { + setting: 'vue.inlayHints.vBindShorthand', + label: `="${propVariableName}"`, + tooltip: [ + `This is a shorthand for \`${exp.loc.source}="${propVariableName}"\`.`, + 'To hide this hint, set `vue.inlayHints.vBindShorthand` to `false` in IDE settings.', + '[More info](https://github.com/vuejs/core/pull/9451)', + ].join('\n\n'), + }, + } as VueCodeInformation, + ]; + } + } + } + } + else { + yield `{}`; + } +} + +function* generateAttrValue(attrNode: CompilerDOM.TextNode, features: VueCodeInformation): Generator { + const char = attrNode.loc.source.startsWith("'") ? "'" : '"'; + yield char; + let start = attrNode.loc.start.offset; + let end = attrNode.loc.end.offset; + let content = attrNode.loc.source; + if ( + (content.startsWith('"') && content.endsWith('"')) + || (content.startsWith("'") && content.endsWith("'")) + ) { + start++; + end--; + content = content.slice(1, -1); + } + if (needToUnicode(content)) { + yield ['', 'template', start, features]; + yield* wrapWith( + start, + end, + features, + toUnicode(content), + ); + } + else { + yield [content, 'template', start, features]; + } + yield char; +} + +function needToUnicode(str: string) { + return str.includes('\\') || str.includes('\n'); +} + +function toUnicode(str: string) { + return str.split('').map(value => { + const temp = value.charCodeAt(0).toString(16).padStart(4, '0'); + if (temp.length > 2) { + return '\\u' + temp; + } + return value; + }).join(''); +} + +function getModelValuePropName(node: CompilerDOM.ElementNode, vueVersion: number, vueCompilerOptions: VueCompilerOptions) { + + for (const modelName in vueCompilerOptions.experimentalModelPropName) { + const tags = vueCompilerOptions.experimentalModelPropName[modelName]; + for (const tag in tags) { + if (node.tag === tag || node.tag === hyphenateTag(tag)) { + const v = tags[tag]; + if (typeof v === 'object') { + const arr = Array.isArray(v) ? v : [v]; + for (const attrs of arr) { + let failed = false; + for (const attr in attrs) { + const attrNode = node.props.find(prop => prop.type === CompilerDOM.NodeTypes.ATTRIBUTE && prop.name === attr) as CompilerDOM.AttributeNode | undefined; + if (!attrNode || attrNode.value?.content !== attrs[attr]) { + failed = true; + break; + } + } + if (!failed) { + // all match + return modelName || undefined; + } + } + } + } + } + } + + for (const modelName in vueCompilerOptions.experimentalModelPropName) { + const tags = vueCompilerOptions.experimentalModelPropName[modelName]; + for (const tag in tags) { + if (node.tag === tag || node.tag === hyphenateTag(tag)) { + const attrs = tags[tag]; + if (attrs === true) { + return modelName || undefined; + } + } + } + } + + return vueVersion < 3 ? 'value' : 'modelValue'; +} diff --git a/packages/language-core/lib/codegen/template/index.ts b/packages/language-core/lib/codegen/template/index.ts new file mode 100644 index 0000000000..f347c215b3 --- /dev/null +++ b/packages/language-core/lib/codegen/template/index.ts @@ -0,0 +1,283 @@ +import * as CompilerDOM from '@vue/compiler-dom'; +import { camelize, capitalize } from '@vue/shared'; +import type * as ts from 'typescript'; +import type { Code, Sfc, VueCodeInformation, VueCompilerOptions } from '../../types'; +import { hyphenateTag } from '../../utils/shared'; +import { endOfLine, newLine, variableNameRegex, wrapWith } from '../common'; +import { generateCamelized } from './camelized'; +import { createTemplateCodegenContext } from './context'; +import { getCanonicalComponentName, getPossibleOriginalComponentNames } from './element'; +import { generateObjectProperty } from './objectProperty'; +import { generatePropertyAccess } from './propertyAccess'; +import { generateStringLiteralKey } from './stringLiteralKey'; +import { generateTemplateChild, getVForNode } from './templateChild'; + +export interface TemplateCodegenOptions { + ts: typeof ts; + compilerOptions: ts.CompilerOptions; + vueCompilerOptions: VueCompilerOptions; + template: NonNullable; + shouldGenerateScopedClasses?: boolean; + stylesScopedClasses: Set; + hasDefineSlots?: boolean; + slotsAssignName?: string; + propsAssignName?: string; +} + +export function* generateTemplate(options: TemplateCodegenOptions) { + const ctx = createTemplateCodegenContext(); + const { componentTagNameOffsets, elementTagNameOffsets } = collectTagOffsets(); + + let hasSlot = false; + + if (options.slotsAssignName) { + ctx.addLocalVariable(options.slotsAssignName); + } + if (options.propsAssignName) { + ctx.addLocalVariable(options.propsAssignName); + } + + yield* generatePreResolveComponents(); + + if (options.template.ast) { + yield* generateTemplateChild(options, ctx, options.template.ast, undefined, undefined, undefined); + } + + yield* generateStyleScopedClasses(); + + if (!options.hasDefineSlots) { + yield `var __VLS_slots!:`; + yield* generateSlotsType(); + yield endOfLine; + } + + yield* ctx.generateAutoImportCompletion(); + + return { + ctx, + hasSlot, + }; + + function collectTagOffsets() { + + const componentTagNameOffsets = new Map(); + const elementTagNameOffsets = new Map(); + + if (!options.template.ast) { + return { + componentTagNameOffsets, + elementTagNameOffsets, + }; + } + + for (const node of forEachElementNode(options.template.ast)) { + if (node.tagType === CompilerDOM.ElementTypes.SLOT) { + // ignore + continue; + } + if (node.tag === 'component' || node.tag === 'Component') { + // ignore + continue; + } + const map = node.tagType === CompilerDOM.ElementTypes.COMPONENT + ? componentTagNameOffsets + : elementTagNameOffsets; + let offsets = map.get(node.tag); + if (!offsets) { + map.set(node.tag, offsets = []); + } + const source = options.template.content.substring(node.loc.start.offset); + const startTagOffset = node.loc.start.offset + source.indexOf(node.tag); + + offsets.push(startTagOffset); // start tag + if (!node.isSelfClosing && options.template.lang === 'html') { + const endTagOffset = node.loc.start.offset + node.loc.source.lastIndexOf(node.tag); + if (endTagOffset !== startTagOffset) { + offsets.push(endTagOffset); // end tag + } + } + } + + return { + componentTagNameOffsets, + elementTagNameOffsets, + }; + } + + function* generateSlotsType(): Generator { + for (const { expVar, varName } of ctx.dynamicSlots) { + hasSlot = true; + yield `Partial, (_: typeof ${varName}) => any>> &${newLine}`; + } + yield `{${newLine}`; + for (const slot of ctx.slots) { + hasSlot = true; + if (slot.name && slot.loc !== undefined) { + yield* generateObjectProperty( + options, + ctx, + slot.name, + slot.loc, + { + ...ctx.codeFeatures.withoutHighlightAndCompletion, + __referencesCodeLens: true, + }, + slot.nodeLoc + ); + } + else { + yield* wrapWith( + slot.tagRange[0], + slot.tagRange[1], + { + ...ctx.codeFeatures.withoutHighlightAndCompletion, + __referencesCodeLens: true, + }, + `default`, + ); + } + yield `?(_: typeof ${slot.varName}): any,${newLine}`; + } + yield `}`; + } + + function* generateStyleScopedClasses(): Generator { + yield `if (typeof __VLS_styleScopedClasses === 'object' && !Array.isArray(__VLS_styleScopedClasses)) {${newLine}`; + for (const { className, offset } of ctx.scopedClasses) { + yield `__VLS_styleScopedClasses[`; + yield* generateStringLiteralKey( + className, + offset, + { + ...ctx.codeFeatures.navigationAndCompletion, + __displayWithLink: options.stylesScopedClasses.has(className), + }, + ); + yield `]${endOfLine}`; + } + yield `}${newLine}`; + } + + function* generatePreResolveComponents(): Generator { + + yield `let __VLS_resolvedLocalAndGlobalComponents!: {}`; + + for (const [tagName] of componentTagNameOffsets) { + + const isNamespacedTag = tagName.includes('.'); + if (isNamespacedTag) { + continue; + } + + yield ` & __VLS_WithComponent<'${getCanonicalComponentName(tagName)}', typeof __VLS_localComponents, `; + yield getPossibleOriginalComponentNames(tagName, false) + .map(name => `"${name}"`) + .join(', '); + yield `>`; + } + + yield endOfLine; + + for (const [tagName, offsets] of elementTagNameOffsets) { + for (const tagOffset of offsets) { + yield `__VLS_intrinsicElements`; + yield* generatePropertyAccess( + options, + ctx, + tagName, + tagOffset, + ctx.codeFeatures.withoutHighlightAndCompletion, + ); + yield `;`; + } + yield `${newLine}`; + } + + for (const [tagName, offsets] of componentTagNameOffsets) { + if (!variableNameRegex.test(camelize(tagName))) { + continue; + } + for (const tagOffset of offsets) { + for (const shouldCapitalize of (tagName[0] === tagName[0].toUpperCase() ? [false] : [true, false])) { + const expectName = shouldCapitalize ? capitalize(camelize(tagName)) : camelize(tagName); + yield `__VLS_components.`; + yield* generateCamelized( + shouldCapitalize ? capitalize(tagName) : tagName, + tagOffset, + { + navigation: { + resolveRenameNewName: tagName !== expectName ? camelizeComponentName : undefined, + resolveRenameEditText: getTagRenameApply(tagName), + }, + } as VueCodeInformation, + ); + yield `;`; + } + } + yield `${newLine}`; + yield `// @ts-ignore${newLine}`; // #2304 + yield `[`; + for (const tagOffset of offsets) { + yield* generateCamelized( + capitalize(tagName), + tagOffset, + { + completion: { + isAdditional: true, + onlyImport: true, + }, + } as VueCodeInformation, + ); + yield `,`; + } + yield `]${endOfLine}`; + } + } +} + +export function* forEachElementNode(node: CompilerDOM.RootNode | CompilerDOM.TemplateChildNode): Generator { + if (node.type === CompilerDOM.NodeTypes.ROOT) { + for (const child of node.children) { + yield* forEachElementNode(child); + } + } + else if (node.type === CompilerDOM.NodeTypes.ELEMENT) { + const patchForNode = getVForNode(node); + if (patchForNode) { + yield* forEachElementNode(patchForNode); + } + else { + yield node; + for (const child of node.children) { + yield* forEachElementNode(child); + } + } + } + else if (node.type === CompilerDOM.NodeTypes.IF) { + // v-if / v-else-if / v-else + for (let i = 0; i < node.branches.length; i++) { + const branch = node.branches[i]; + for (const childNode of branch.children) { + yield* forEachElementNode(childNode); + } + } + } + else if (node.type === CompilerDOM.NodeTypes.FOR) { + // v-for + for (const child of node.children) { + yield* forEachElementNode(child); + } + } +} + +function camelizeComponentName(newName: string) { + return camelize('-' + newName); +} + +function getTagRenameApply(oldName: string) { + return oldName === hyphenateTag(oldName) ? hyphenateTag : undefined; +} + +export function isFragment(node: CompilerDOM.IfNode | CompilerDOM.ForNode) { + return node.codegenNode && 'consequent' in node.codegenNode && 'tag' in node.codegenNode.consequent && node.codegenNode.consequent.tag === CompilerDOM.FRAGMENT; +} diff --git a/packages/language-core/lib/utils/transform.ts b/packages/language-core/lib/codegen/template/interpolation.ts similarity index 65% rename from packages/language-core/lib/utils/transform.ts rename to packages/language-core/lib/codegen/template/interpolation.ts index ea404bafbf..6f90c64757 100644 --- a/packages/language-core/lib/utils/transform.ts +++ b/packages/language-core/lib/codegen/template/interpolation.ts @@ -1,26 +1,93 @@ import { isGloballyWhitelisted } from '@vue/shared'; import type * as ts from 'typescript'; -import { getNodeText, getStartEnd } from '../parsers/scriptSetupRanges'; -import type { VueCompilerOptions } from '../types'; +import { getNodeText, getStartEnd } from '../../parsers/scriptSetupRanges'; +import type { Code, VueCodeInformation, VueCompilerOptions } from '../../types'; +import { collectVars, createTsAst } from '../common'; +import type { TemplateCodegenContext } from './context'; +import type { TemplateCodegenOptions } from './index'; -export function* eachInterpolationSegment( +export function* generateInterpolation( + options: TemplateCodegenOptions, + ctx: TemplateCodegenContext, + _code: string, + astHolder: any, + start: number | undefined, + data: VueCodeInformation | (() => VueCodeInformation) | undefined, + prefix: string, + suffix: string, +): Generator { + const code = prefix + _code + suffix; + const ast = createTsAst(options.ts, astHolder, code); + const vars: { + text: string, + isShorthand: boolean, + offset: number, + }[] = []; + for (let [section, offset, onlyError] of forEachInterpolationSegment( + options.ts, + options.vueCompilerOptions, + ctx, + code, + start !== undefined ? start - prefix.length : undefined, + ast, + )) { + if (offset === undefined) { + yield section; + } + else { + offset -= prefix.length; + let addSuffix = ''; + const overLength = offset + section.length - _code.length; + if (overLength > 0) { + addSuffix = section.substring(section.length - overLength); + section = section.substring(0, section.length - overLength); + } + if (offset < 0) { + yield section.substring(0, -offset); + section = section.substring(-offset); + offset = 0; + } + if (start !== undefined && data !== undefined) { + yield [ + section, + 'template', + start + offset, + onlyError + ? ctx.codeFeatures.verification + : typeof data === 'function' ? data() : data, + ]; + } + else { + yield section; + } + yield addSuffix; + } + } + if (start !== undefined) { + for (const v of vars) { + v.offset = start + v.offset - prefix.length; + } + } +} + +export function* forEachInterpolationSegment( ts: typeof import('typescript'), + vueOptions: VueCompilerOptions, + ctx: TemplateCodegenContext, code: string, + offset: number | undefined, ast: ts.SourceFile, - localVars: Map, - identifiers: Set, - vueOptions: VueCompilerOptions, - ctxVars: { +): Generator<[fragment: string, offset: number | undefined, isJustForErrorMapping?: boolean]> { + let ctxVars: { text: string, isShorthand: boolean, offset: number, - }[] = [] -): Generator<[fragment: string, offset: number | undefined, isJustForErrorMapping?: boolean]> { + }[] = []; const varCb = (id: ts.Identifier, isShorthand: boolean) => { const text = getNodeText(ts, id, ast); if ( - localVars.get(text) || + ctx.hasLocalVariable(text) || // https://github.com/vuejs/core/blob/245230e135152900189f13a4281302de45fdcfaa/packages/compiler-core/src/transforms/transformExpression.ts#L342-L352 isGloballyWhitelisted(text) || text === 'require' || @@ -34,10 +101,15 @@ export function* eachInterpolationSegment( isShorthand: isShorthand, offset: getStartEnd(ts, id, ast).start, }); - identifiers.add(text); + if (offset !== undefined) { + ctx.accessGlobalVariable(text, offset + getStartEnd(ts, id, ast).start); + } + else { + ctx.accessGlobalVariable(text); + } } }; - ts.forEachChild(ast, node => walkIdentifiers(ts, node, ast, varCb, localVars)); + ts.forEachChild(ast, node => walkIdentifiers(ts, node, ast, varCb, ctx)); ctxVars = ctxVars.sort((a, b) => a.offset - b.offset); @@ -114,7 +186,7 @@ function walkIdentifiers( node: ts.Node, ast: ts.SourceFile, cb: (varNode: ts.Identifier, isShorthand: boolean) => void, - localVars: Map, + ctx: TemplateCodegenContext, blockVars: string[] = [], isRoot: boolean = true, ) { @@ -126,18 +198,18 @@ function walkIdentifiers( cb(node.name, true); } else if (ts.isPropertyAccessExpression(node)) { - walkIdentifiers(ts, node.expression, ast, cb, localVars, blockVars, false); + walkIdentifiers(ts, node.expression, ast, cb, ctx, blockVars, false); } else if (ts.isVariableDeclaration(node)) { collectVars(ts, node.name, ast, blockVars); for (const varName of blockVars) { - localVars.set(varName, (localVars.get(varName) ?? 0) + 1); + ctx.addLocalVariable(varName); } if (node.initializer) { - walkIdentifiers(ts, node.initializer, ast, cb, localVars, blockVars, false); + walkIdentifiers(ts, node.initializer, ast, cb, ctx, blockVars, false); } } else if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) { @@ -147,18 +219,18 @@ function walkIdentifiers( for (const param of node.parameters) { collectVars(ts, param.name, ast, functionArgs); if (param.type) { - walkIdentifiers(ts, param.type, ast, cb, localVars, blockVars, false); + walkIdentifiers(ts, param.type, ast, cb, ctx, blockVars, false); } } for (const varName of functionArgs) { - localVars.set(varName, (localVars.get(varName) ?? 0) + 1); + ctx.addLocalVariable(varName); } - walkIdentifiers(ts, node.body, ast, cb, localVars, blockVars, false); + walkIdentifiers(ts, node.body, ast, cb, ctx, blockVars, false); for (const varName of functionArgs) { - localVars.set(varName, localVars.get(varName)! - 1); + ctx.removeLocalVariable(varName); } } else if (ts.isObjectLiteralExpression(node)) { @@ -166,18 +238,18 @@ function walkIdentifiers( if (ts.isPropertyAssignment(prop)) { // fix https://github.com/vuejs/language-tools/issues/1176 if (ts.isComputedPropertyName(prop.name)) { - walkIdentifiers(ts, prop.name.expression, ast, cb, localVars, blockVars, false); + walkIdentifiers(ts, prop.name.expression, ast, cb, ctx, blockVars, false); } - walkIdentifiers(ts, prop.initializer, ast, cb, localVars, blockVars, false); + walkIdentifiers(ts, prop.initializer, ast, cb, ctx, blockVars, false); } // fix https://github.com/vuejs/language-tools/issues/1156 else if (ts.isShorthandPropertyAssignment(prop)) { - walkIdentifiers(ts, prop, ast, cb, localVars, blockVars, false); + walkIdentifiers(ts, prop, ast, cb, ctx, blockVars, false); } // fix https://github.com/vuejs/language-tools/issues/1148#issuecomment-1094378126 else if (ts.isSpreadAssignment(prop)) { // TODO: cannot report "Spread types may only be created from object types.ts(2698)" - walkIdentifiers(ts, prop.expression, ast, cb, localVars, blockVars, false); + walkIdentifiers(ts, prop.expression, ast, cb, ctx, blockVars, false); } } } @@ -190,10 +262,10 @@ function walkIdentifiers( if (ts.isBlock(node)) { blockVars = []; } - ts.forEachChild(node, node => walkIdentifiers(ts, node, ast, cb, localVars, blockVars, false)); + ts.forEachChild(node, node => walkIdentifiers(ts, node, ast, cb, ctx, blockVars, false)); if (ts.isBlock(node)) { for (const varName of blockVars) { - localVars.set(varName, localVars.get(varName)! - 1); + ctx.removeLocalVariable(varName); } } blockVars = _blockVars; @@ -201,7 +273,7 @@ function walkIdentifiers( if (isRoot) { for (const varName of blockVars) { - localVars.set(varName, localVars.get(varName)! - 1); + ctx.removeLocalVariable(varName); } } } @@ -218,29 +290,3 @@ function walkIdentifiersInTypeReference( ts.forEachChild(node, node => walkIdentifiersInTypeReference(ts, node, cb)); } } - -export function collectVars( - ts: typeof import('typescript'), - node: ts.Node, - ast: ts.SourceFile, - result: string[], -) { - if (ts.isIdentifier(node)) { - result.push(getNodeText(ts, node, ast)); - } - else if (ts.isObjectBindingPattern(node)) { - for (const el of node.elements) { - collectVars(ts, el.name, ast, result); - } - } - else if (ts.isArrayBindingPattern(node)) { - for (const el of node.elements) { - if (ts.isBindingElement(el)) { - collectVars(ts, el.name, ast, result); - } - } - } - else { - ts.forEachChild(node, node => collectVars(ts, node, ast, result)); - } -} diff --git a/packages/language-core/lib/codegen/template/objectProperty.ts b/packages/language-core/lib/codegen/template/objectProperty.ts new file mode 100644 index 0000000000..c016115316 --- /dev/null +++ b/packages/language-core/lib/codegen/template/objectProperty.ts @@ -0,0 +1,45 @@ +import { camelize } from '@vue/shared'; +import type { Code, VueCodeInformation } from '../../types'; +import { combineLastMapping, variableNameRegex, wrapWith } from '../common'; +import { generateCamelized } from './camelized'; +import type { TemplateCodegenContext } from './context'; +import type { TemplateCodegenOptions } from './index'; +import { generateInterpolation } from './interpolation'; +import { generateStringLiteralKey } from './stringLiteralKey'; + +export function* generateObjectProperty( + options: TemplateCodegenOptions, + ctx: TemplateCodegenContext, + code: string, + offset: number, + features: VueCodeInformation, + astHolder?: any, + shouldCamelize = false, +): Generator { + if (code.startsWith('[') && code.endsWith(']') && astHolder) { + yield* generateInterpolation(options, ctx, code, astHolder, offset, features, '', ''); + } + else if (shouldCamelize) { + if (variableNameRegex.test(camelize(code))) { + yield* generateCamelized(code, offset, features); + } + else { + yield* wrapWith( + offset, + offset + code.length, + features, + `"`, + ...generateCamelized(code, offset, combineLastMapping), + `"`, + ); + } + } + else { + if (variableNameRegex.test(code)) { + yield [code, 'template', offset, features]; + } + else { + yield* generateStringLiteralKey(code, offset, features); + } + } +} diff --git a/packages/language-core/lib/codegen/template/propertyAccess.ts b/packages/language-core/lib/codegen/template/propertyAccess.ts new file mode 100644 index 0000000000..5db813a848 --- /dev/null +++ b/packages/language-core/lib/codegen/template/propertyAccess.ts @@ -0,0 +1,39 @@ +import type { Code, VueCodeInformation } from '../../types'; +import { variableNameRegex } from '../common'; +import type { TemplateCodegenContext } from './context'; +import type { TemplateCodegenOptions } from './index'; +import { generateInterpolation } from './interpolation'; +import { generateStringLiteralKey } from './stringLiteralKey'; + +export function* generatePropertyAccess( + options: TemplateCodegenOptions, + ctx: TemplateCodegenContext, + code: string, + offset?: number, + features?: VueCodeInformation, + astHolder?: any, +): Generator { + if (!options.compilerOptions.noPropertyAccessFromIndexSignature && variableNameRegex.test(code)) { + yield `.`; + yield offset !== undefined && features + ? [code, 'template', offset, features] + : code; + } + else if (code.startsWith('[') && code.endsWith(']')) { + yield* generateInterpolation( + options, + ctx, + code, + astHolder, + offset, + features, + '', + '', + ); + } + else { + yield `[`; + yield* generateStringLiteralKey(code, offset, features); + yield `]`; + } +} diff --git a/packages/language-core/lib/codegen/template/slotOutlet.ts b/packages/language-core/lib/codegen/template/slotOutlet.ts new file mode 100644 index 0000000000..f58832fda8 --- /dev/null +++ b/packages/language-core/lib/codegen/template/slotOutlet.ts @@ -0,0 +1,113 @@ +import * as CompilerDOM from '@vue/compiler-dom'; +import type { Code } from '../../types'; +import { endOfLine, newLine, wrapWith } from '../common'; +import type { TemplateCodegenContext } from './context'; +import { generateElementChildren } from './elementChildren'; +import { generateElementProps } from './elementProps'; +import type { TemplateCodegenOptions } from './index'; +import { generateInterpolation } from './interpolation'; + +export function* generateSlotOutlet( + options: TemplateCodegenOptions, + ctx: TemplateCodegenContext, + node: CompilerDOM.SlotOutletNode, + currentElement: CompilerDOM.ElementNode | undefined, + componentCtxVar: string | undefined, +): Generator { + const startTagOffset = node.loc.start.offset + options.template.content.substring(node.loc.start.offset).indexOf(node.tag); + const varSlot = ctx.getInternalVariable(); + const nameProp = node.props.find(prop => { + if (prop.type === CompilerDOM.NodeTypes.ATTRIBUTE) { + return prop.name === 'name'; + } + if ( + prop.type === CompilerDOM.NodeTypes.DIRECTIVE + && prop.name === 'bind' + && prop.arg?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION + ) { + return prop.arg.content === 'name'; + } + }); + + if (options.hasDefineSlots) { + yield `__VLS_normalizeSlot(`; + yield* wrapWith( + node.loc.start.offset, + node.loc.end.offset, + ctx.codeFeatures.verification, + `${options.slotsAssignName ?? '__VLS_slots'}[`, + ...wrapWith( + node.loc.start.offset, + node.loc.end.offset, + ctx.codeFeatures.verification, + nameProp?.type === CompilerDOM.NodeTypes.ATTRIBUTE && nameProp.value + ? `'${nameProp.value.content}'` + : nameProp?.type === CompilerDOM.NodeTypes.DIRECTIVE && nameProp.exp?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION + ? nameProp.exp.content + : `('default' as const)` + ), + `]`, + ); + yield `)?.(`; + yield* wrapWith( + startTagOffset, + startTagOffset + node.tag.length, + ctx.codeFeatures.verification, + `{${newLine}`, + ...generateElementProps(options, ctx, node, node.props.filter(prop => prop !== nameProp), 'normal'), + `}`, + ); + yield `)${endOfLine}`; + return; + } + else { + yield `var ${varSlot} = {${newLine}`; + yield* generateElementProps(options, ctx, node, node.props.filter(prop => prop !== nameProp), 'normal'); + yield `}${endOfLine}`; + } + + if ( + nameProp?.type === CompilerDOM.NodeTypes.ATTRIBUTE + && nameProp.value + ) { + ctx.slots.push({ + name: nameProp.value.content, + loc: nameProp.loc.start.offset + nameProp.loc.source.indexOf(nameProp.value.content, nameProp.name.length), + tagRange: [startTagOffset, startTagOffset + node.tag.length], + varName: varSlot, + nodeLoc: node.loc, + }); + } + else if ( + nameProp?.type === CompilerDOM.NodeTypes.DIRECTIVE + && nameProp.exp?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION + ) { + const slotExpVar = ctx.getInternalVariable(); + yield `var ${slotExpVar} = `; + yield* generateInterpolation( + options, + ctx, + nameProp.exp.content, + nameProp.exp, + nameProp.exp.loc.start.offset, + ctx.codeFeatures.all, + '(', + ')', + ); + yield ` as const${endOfLine}`; + ctx.dynamicSlots.push({ + expVar: slotExpVar, + varName: varSlot, + }); + } + else { + ctx.slots.push({ + name: 'default', + tagRange: [startTagOffset, startTagOffset + node.tag.length], + varName: varSlot, + nodeLoc: node.loc, + }); + } + yield* ctx.generateAutoImportCompletion(); + yield* generateElementChildren(options, ctx, node, currentElement, componentCtxVar); +} diff --git a/packages/language-core/lib/codegen/template/stringLiteralKey.ts b/packages/language-core/lib/codegen/template/stringLiteralKey.ts new file mode 100644 index 0000000000..ea7d169fc9 --- /dev/null +++ b/packages/language-core/lib/codegen/template/stringLiteralKey.ts @@ -0,0 +1,18 @@ +import type { Code, VueCodeInformation } from '../../types'; +import { combineLastMapping, wrapWith } from '../common'; + +export function* generateStringLiteralKey(code: string, offset?: number, info?: VueCodeInformation): Generator { + if (offset === undefined || !info) { + yield `"${code}"`; + } + else { + yield* wrapWith( + offset, + offset + code.length, + info, + `"`, + [code, 'template', offset, combineLastMapping], + `"`, + ); + } +} diff --git a/packages/language-core/lib/codegen/template/templateChild.ts b/packages/language-core/lib/codegen/template/templateChild.ts new file mode 100644 index 0000000000..37b8cb122f --- /dev/null +++ b/packages/language-core/lib/codegen/template/templateChild.ts @@ -0,0 +1,182 @@ +import * as CompilerDOM from '@vue/compiler-dom'; +import type { Code } from '../../types'; +import { endOfLine, newLine } from '../common'; +import type { TemplateCodegenContext } from './context'; +import { generateElement } from './element'; +import type { TemplateCodegenOptions } from './index'; +import { generateInterpolation } from './interpolation'; +import { generateSlotOutlet } from './slotOutlet'; +import { generateVFor } from './vFor'; +import { generateVIf } from './vIf'; + +// @ts-ignore +const transformContext: CompilerDOM.TransformContext = { + onError: () => { }, + helperString: str => str.toString(), + replaceNode: () => { }, + cacheHandlers: false, + prefixIdentifiers: false, + scopes: { + vFor: 0, + vOnce: 0, + vPre: 0, + vSlot: 0, + }, + expressionPlugins: ['typescript'], +}; + +export function* generateTemplateChild( + options: TemplateCodegenOptions, + ctx: TemplateCodegenContext, + node: CompilerDOM.RootNode | CompilerDOM.TemplateChildNode | CompilerDOM.SimpleExpressionNode, + currentElement: CompilerDOM.ElementNode | undefined, + prevNode: CompilerDOM.TemplateChildNode | undefined, + componentCtxVar: string | undefined, +): Generator { + if (prevNode?.type === CompilerDOM.NodeTypes.COMMENT) { + const commentText = prevNode.content.trim().split(' ')[0]; + if (commentText.match(/^@vue-skip\b[\s\S]*/)) { + yield `// @vue-skip${newLine}`; + return; + } + else if (commentText.match(/^@vue-ignore\b[\s\S]*/)) { + yield* ctx.ignoreError(); + } + else if (commentText.match(/^@vue-expect-error\b[\s\S]*/)) { + yield* ctx.expectError(prevNode); + } + } + + if (node.type === CompilerDOM.NodeTypes.ROOT) { + let prev: CompilerDOM.TemplateChildNode | undefined; + for (const childNode of node.children) { + yield* generateTemplateChild(options, ctx, childNode, currentElement, prev, componentCtxVar); + prev = childNode; + } + yield* ctx.resetDirectiveComments('end of root'); + } + else if (node.type === CompilerDOM.NodeTypes.ELEMENT) { + const vForNode = getVForNode(node); + const vIfNode = getVIfNode(node); + if (vForNode) { + yield* generateVFor(options, ctx, vForNode, currentElement, componentCtxVar); + } + else if (vIfNode) { + yield* generateVIf(options, ctx, vIfNode, currentElement, componentCtxVar); + } + else { + if (node.tagType === CompilerDOM.ElementTypes.SLOT) { + yield* generateSlotOutlet(options, ctx, node, currentElement, componentCtxVar); + } + else { + yield* generateElement(options, ctx, node, currentElement, componentCtxVar); + } + } + } + else if (node.type === CompilerDOM.NodeTypes.TEXT_CALL) { + // {{ var }} + yield* generateTemplateChild(options, ctx, node.content, currentElement, undefined, componentCtxVar); + } + else if (node.type === CompilerDOM.NodeTypes.COMPOUND_EXPRESSION) { + // {{ ... }} {{ ... }} + for (const childNode of node.children) { + if (typeof childNode === 'object') { + yield* generateTemplateChild(options, ctx, childNode, currentElement, undefined, componentCtxVar); + } + } + } + else if (node.type === CompilerDOM.NodeTypes.INTERPOLATION) { + // {{ ... }} + const [content, start] = parseInterpolationNode(node, options.template.content); + yield* generateInterpolation( + options, + ctx, + content, + node.content.loc, + start, + ctx.codeFeatures.all, + `(`, + `)${endOfLine}`, + ); + yield* ctx.resetDirectiveComments('end of INTERPOLATION'); + } + else if (node.type === CompilerDOM.NodeTypes.IF) { + // v-if / v-else-if / v-else + yield* generateVIf(options, ctx, node, currentElement, componentCtxVar); + } + else if (node.type === CompilerDOM.NodeTypes.FOR) { + // v-for + yield* generateVFor(options, ctx, node, currentElement, componentCtxVar); + } + else if (node.type === CompilerDOM.NodeTypes.TEXT) { + // not needed progress + } +} + +// TODO: track https://github.com/vuejs/vue-next/issues/3498 +export function getVForNode(node: CompilerDOM.ElementNode) { + const forDirective = node.props.find( + (prop): prop is CompilerDOM.DirectiveNode => + prop.type === CompilerDOM.NodeTypes.DIRECTIVE + && prop.name === 'for' + ); + if (forDirective) { + let forNode: CompilerDOM.ForNode | undefined; + CompilerDOM.processFor(node, forDirective, transformContext, _forNode => { + forNode = { ..._forNode }; + return undefined; + }); + if (forNode) { + forNode.children = [{ + ...node, + props: node.props.filter(prop => prop !== forDirective), + }]; + return forNode; + } + } +} + +function getVIfNode(node: CompilerDOM.ElementNode) { + const forDirective = node.props.find( + (prop): prop is CompilerDOM.DirectiveNode => + prop.type === CompilerDOM.NodeTypes.DIRECTIVE + && prop.name === 'if' + ); + if (forDirective) { + let ifNode: CompilerDOM.IfNode | undefined; + CompilerDOM.processIf(node, forDirective, transformContext, _ifNode => { + ifNode = { ..._ifNode }; + return undefined; + }); + if (ifNode) { + for (const branch of ifNode.branches) { + branch.children = [{ + ...node, + props: node.props.filter(prop => prop !== forDirective), + }]; + } + return ifNode; + } + } +} + +export function parseInterpolationNode(node: CompilerDOM.InterpolationNode, template: string) { + let content = node.content.loc.source; + let start = node.content.loc.start.offset; + let leftCharacter: string; + let rightCharacter: string; + + // fix https://github.com/vuejs/language-tools/issues/1787 + while ((leftCharacter = template.substring(start - 1, start)).trim() === '' && leftCharacter.length) { + start--; + content = leftCharacter + content; + } + while ((rightCharacter = template.substring(start + content.length, start + content.length + 1)).trim() === '' && rightCharacter.length) { + content = content + rightCharacter; + } + + return [ + content, + start, + ] as const; +} diff --git a/packages/language-core/lib/codegen/template/vFor.ts b/packages/language-core/lib/codegen/template/vFor.ts new file mode 100644 index 0000000000..517beb1aaf --- /dev/null +++ b/packages/language-core/lib/codegen/template/vFor.ts @@ -0,0 +1,87 @@ +import * as CompilerDOM from '@vue/compiler-dom'; +import type { Code } from '../../types'; +import { collectVars, createTsAst, newLine } from '../common'; +import type { TemplateCodegenContext } from './context'; +import type { TemplateCodegenOptions } from './index'; +import { isFragment } from './index'; +import { generateInterpolation } from './interpolation'; +import { generateTemplateChild } from './templateChild'; + +export function* generateVFor( + options: TemplateCodegenOptions, + ctx: TemplateCodegenContext, + node: CompilerDOM.ForNode, + currentElement: CompilerDOM.ElementNode | undefined, + componentCtxVar: string | undefined, +): Generator { + const { source } = node.parseResult; + const { leftExpressionRange, leftExpressionText } = parseVForNode(node); + const forBlockVars: string[] = []; + + yield `for (const [`; + if (leftExpressionRange && leftExpressionText) { + const collectAst = createTsAst(options.ts, node.parseResult, `const [${leftExpressionText}]`); + collectVars(options.ts, collectAst, collectAst, forBlockVars); + yield [ + leftExpressionText, + 'template', + leftExpressionRange.start, + ctx.codeFeatures.all, + ]; + } + yield `] of `; + if (source.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION) { + yield `__VLS_getVForSourceType(`; + yield* generateInterpolation( + options, + ctx, + source.content, + source.loc, + source.loc.start.offset, + ctx.codeFeatures.all, + '(', + ')', + ); + yield `!)`; // #3102 + } + else { + yield `{} as any`; + } + yield `) {${newLine}`; + if (isFragment(node)) { + yield* ctx.resetDirectiveComments('end of v-for start'); + } + for (const varName of forBlockVars) { + ctx.addLocalVariable(varName); + } + let prev: CompilerDOM.TemplateChildNode | undefined; + for (const childNode of node.children) { + yield* generateTemplateChild(options, ctx, childNode, currentElement, prev, componentCtxVar); + prev = childNode; + } + for (const varName of forBlockVars) { + ctx.removeLocalVariable(varName); + } + yield* ctx.generateAutoImportCompletion(); + yield `}${newLine}`; +} + +export function parseVForNode(node: CompilerDOM.ForNode) { + const { value, key, index } = node.parseResult; + const leftExpressionRange = (value || key || index) + ? { + start: (value ?? key ?? index)!.loc.start.offset, + end: (index ?? key ?? value)!.loc.end.offset, + } + : undefined; + const leftExpressionText = leftExpressionRange + ? node.loc.source.substring( + leftExpressionRange.start - node.loc.start.offset, + leftExpressionRange.end - node.loc.start.offset + ) + : undefined; + return { + leftExpressionRange, + leftExpressionText, + }; +} diff --git a/packages/language-core/lib/codegen/template/vIf.ts b/packages/language-core/lib/codegen/template/vIf.ts new file mode 100644 index 0000000000..032c311cbc --- /dev/null +++ b/packages/language-core/lib/codegen/template/vIf.ts @@ -0,0 +1,76 @@ +import { toString } from '@volar/language-core'; +import * as CompilerDOM from '@vue/compiler-dom'; +import type { Code } from '../../types'; +import { newLine } from '../common'; +import type { TemplateCodegenContext } from './context'; +import type { TemplateCodegenOptions } from './index'; +import { isFragment } from './index'; +import { generateInterpolation } from './interpolation'; +import { generateTemplateChild } from './templateChild'; + +export function* generateVIf( + options: TemplateCodegenOptions, + ctx: TemplateCodegenContext, + node: CompilerDOM.IfNode, + currentElement: CompilerDOM.ElementNode | undefined, + componentCtxVar: string | undefined, +): Generator { + + let originalBlockConditionsLength = ctx.blockConditions.length; + + for (let i = 0; i < node.branches.length; i++) { + + const branch = node.branches[i]; + + if (i === 0) { + yield `if `; + } + else if (branch.condition) { + yield `else if `; + } + else { + yield `else `; + } + + let addedBlockCondition = false; + + if (branch.condition?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION) { + const codes = [ + ...generateInterpolation( + options, + ctx, + branch.condition.content, + branch.condition.loc, + branch.condition.loc.start.offset, + ctx.codeFeatures.all, + '(', + ')', + ), + ]; + for (const code of codes) { + yield code; + } + ctx.blockConditions.push(toString(codes)); + addedBlockCondition = true; + yield ` `; + } + + yield `{${newLine}`; + if (isFragment(node)) { + yield* ctx.resetDirectiveComments('end of v-if start'); + } + let prev: CompilerDOM.TemplateChildNode | undefined; + for (const childNode of branch.children) { + yield* generateTemplateChild(options, ctx, childNode, currentElement, prev, componentCtxVar); + prev = childNode; + } + yield* ctx.generateAutoImportCompletion(); + yield `}${newLine}`; + + if (addedBlockCondition) { + ctx.blockConditions[ctx.blockConditions.length - 1] = `!(${ctx.blockConditions[ctx.blockConditions.length - 1]})`; + } + } + + ctx.blockConditions.length = originalBlockConditionsLength; +} diff --git a/packages/language-core/lib/generators/globalTypes.ts b/packages/language-core/lib/generators/globalTypes.ts deleted file mode 100644 index 4a235750f6..0000000000 --- a/packages/language-core/lib/generators/globalTypes.ts +++ /dev/null @@ -1,131 +0,0 @@ -import type { VueCompilerOptions } from '../types'; -import { getSlotsPropertyName } from '../utils/shared'; - -export function generateGlobalTypes(vueCompilerOptions: VueCompilerOptions) { - const fnPropsType = `(K extends { $props: infer Props } ? Props : any)${vueCompilerOptions.strictTemplates ? '' : ' & Record'}`; - return ` -; export const __VLS_globalTypesStart = {}; -declare global { -// @ts-ignore -type __VLS_IntrinsicElements = __VLS_PickNotAny>>; -// @ts-ignore -type __VLS_Element = __VLS_PickNotAny; -// @ts-ignore -type __VLS_GlobalComponents = ${[ - `__VLS_PickNotAny`, - `__VLS_PickNotAny`, - `__VLS_PickNotAny`, - `Pick` - ].join(' & ')}; -type __VLS_IsAny = 0 extends 1 & T ? true : false; -type __VLS_PickNotAny = __VLS_IsAny extends true ? B : A; - -const __VLS_intrinsicElements: __VLS_IntrinsicElements; - -// v-for -function __VLS_getVForSourceType(source: number): [number, number, number][]; -function __VLS_getVForSourceType(source: string): [string, number, number][]; -function __VLS_getVForSourceType(source: T): [ - item: T[number], - key: number, - index: number, -][]; -function __VLS_getVForSourceType }>(source: T): [ - item: T extends { [Symbol.iterator](): Iterator } ? T1 : never, - key: number, - index: undefined, -][]; -// #3845 -function __VLS_getVForSourceType }>(source: T): [ - item: number | (Exclude extends { [Symbol.iterator](): Iterator } ? T1 : never), - key: number, - index: undefined, -][]; -function __VLS_getVForSourceType(source: T): [ - item: T[keyof T], - key: keyof T, - index: number, -][]; - -// @ts-ignore -function __VLS_getSlotParams(slot: T): Parameters<__VLS_PickNotAny, (...args: any[]) => any>>; -// @ts-ignore -function __VLS_getSlotParam(slot: T): Parameters<__VLS_PickNotAny, (...args: any[]) => any>>[0]; -function __VLS_directiveFunction(dir: T): - T extends import('${vueCompilerOptions.lib}').ObjectDirective | import('${vueCompilerOptions.lib}').FunctionDirective ? (value: V) => void - : T; -function __VLS_withScope(ctx: T, scope: K): ctx is T & K; -function __VLS_makeOptional(t: T): { [K in keyof T]?: T[K] }; - -type __VLS_SelfComponent = string extends N ? {} : N extends string ? { [P in N]: C } : {}; -type __VLS_WithComponent = - N1 extends keyof LocalComponents ? N1 extends N0 ? Pick : { [K in N0]: LocalComponents[N1] } : - N2 extends keyof LocalComponents ? N2 extends N0 ? Pick : { [K in N0]: LocalComponents[N2] } : - N3 extends keyof LocalComponents ? N3 extends N0 ? Pick : { [K in N0]: LocalComponents[N3] } : - 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] } : - ${vueCompilerOptions.strictTemplates ? '{}' : '{ [K in N0]: unknown }'} - -type __VLS_FillingEventArg_ParametersLength any> = __VLS_IsAny> extends true ? -1 : Parameters['length']; -type __VLS_FillingEventArg = E extends (...args: any) => any ? __VLS_FillingEventArg_ParametersLength extends 0 ? ($event?: undefined) => ReturnType : E : E; -function __VLS_asFunctionalComponent any ? InstanceType : unknown>(t: T, instance?: K): - T extends new (...args: any) => any - ? (props: ${fnPropsType}, ctx?: any) => __VLS_Element & { __ctx?: { - attrs?: any, - slots?: K extends { ${getSlotsPropertyName(vueCompilerOptions.target)}: infer Slots } ? Slots : any, - emit?: K extends { $emit: infer Emit } ? Emit : any - } & { props?: ${fnPropsType}; expose?(exposed: K): void; } } - : T extends () => any ? (props: {}, ctx?: any) => ReturnType - : T extends (...args: any) => any ? T - : (_: {}${vueCompilerOptions.strictTemplates ? '' : ' & Record'}, ctx?: any) => { __ctx?: { attrs?: any, expose?: any, slots?: any, emit?: any, props?: {}${vueCompilerOptions.strictTemplates ? '' : ' & Record'} } }; -function __VLS_elementAsFunctionalComponent(t: T): (_: T${vueCompilerOptions.strictTemplates ? '' : ' & Record'}, ctx?: any) => { __ctx?: { attrs?: any, expose?: any, slots?: any, emit?: any, props?: T${vueCompilerOptions.strictTemplates ? '' : ' & Record'} } }; -function __VLS_functionalComponentArgsRest any>(t: T): Parameters['length'] extends 2 ? [any] : []; -function __VLS_pickEvent(emitEvent: E1, propEvent: E2): __VLS_FillingEventArg< - __VLS_PickNotAny< - __VLS_AsFunctionOrAny, - __VLS_AsFunctionOrAny - > -> | undefined; -function __VLS_pickFunctionalComponentCtx(comp: T, compInstance: K): __VLS_PickNotAny< - '__ctx' extends keyof __VLS_PickNotAny ? K extends { __ctx?: infer Ctx } ? Ctx : never : any - , T extends (props: any, ctx: infer Ctx) => any ? Ctx : any ->; -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 : - {}; -type __VLS_AsFunctionOrAny = unknown extends F ? any : ((...args: any) => any) extends F ? F : any; - -function __VLS_normalizeSlot(s: S): S extends () => infer R ? (props: {}) => R : S; - -/** - * emit - */ -// 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 - ? U extends T - ? never - : __VLS_OverloadUnionInner & U & ((...args: A) => R)> | ((...args: A) => R) - : never; -type __VLS_OverloadUnion = Exclude< - __VLS_OverloadUnionInner<(() => never) & T>, - T extends () => never ? never : () => never ->; -type __VLS_ConstructorOverloads = __VLS_OverloadUnion extends infer F - ? F extends (event: infer E, ...args: infer A) => any - ? { [K in E & string]: (...args: A) => void; } - : never - : never; -type __VLS_NormalizeEmits = __VLS_PrettifyGlobal< - __VLS_UnionToIntersection< - __VLS_ConstructorOverloads & { - [K in keyof T]: T[K] extends any[] ? { (...args: T[K]): void } : never - } - > ->; -type __VLS_PrettifyGlobal = { [K in keyof T]: T[K]; } & {}; -} -export const __VLS_globalTypesEnd = {};`; -}; diff --git a/packages/language-core/lib/generators/inlineCss.ts b/packages/language-core/lib/generators/inlineCss.ts deleted file mode 100644 index 19aef52e32..0000000000 --- a/packages/language-core/lib/generators/inlineCss.ts +++ /dev/null @@ -1,38 +0,0 @@ -import * as CompilerDOM from '@vue/compiler-dom'; -import { forEachElementNode } from './template'; -import { enableAllFeatures } from './utils'; -import type { Code } from '../types'; - -const codeFeatures = enableAllFeatures({ - format: false, - structure: false, -}); - -export function* generate(templateAst: NonNullable): Generator { - for (const node of forEachElementNode(templateAst)) { - for (const prop of node.props) { - if ( - prop.type === CompilerDOM.NodeTypes.DIRECTIVE - && prop.name === 'bind' - && prop.arg?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION - && prop.exp?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION - && prop.arg.content === 'style' - && prop.exp.constType === CompilerDOM.ConstantTypes.CAN_STRINGIFY - ) { - const endCrt = prop.arg.loc.source[prop.arg.loc.source.length - 1]; // " | ' - const start = prop.arg.loc.source.indexOf(endCrt) + 1; - const end = prop.arg.loc.source.lastIndexOf(endCrt); - const content = prop.arg.loc.source.substring(start, end); - - yield `x { `; - yield [ - content, - 'template', - prop.arg.loc.start.offset + start, - codeFeatures, - ]; - yield ` }\n`; - } - } - } -} diff --git a/packages/language-core/lib/generators/script.ts b/packages/language-core/lib/generators/script.ts deleted file mode 100644 index e0a21abaf5..0000000000 --- a/packages/language-core/lib/generators/script.ts +++ /dev/null @@ -1,1117 +0,0 @@ -import type { Mapping } from '@volar/language-core'; -import * as path from 'path-browserify'; -import type * as ts from 'typescript'; -import type { ScriptRanges } from '../parsers/scriptRanges'; -import type { ScriptSetupRanges } from '../parsers/scriptSetupRanges'; -import type { Code, CodeAndStack, Sfc, SfcBlock, VueCompilerOptions } from '../types'; -import { getSlotsPropertyName, hyphenateTag } from '../utils/shared'; -import { eachInterpolationSegment } from '../utils/transform'; -import { disableAllFeatures, enableAllFeatures, getStack } from './utils'; -import { generateGlobalTypes } from './globalTypes'; - -interface HelperType { - name: string; - usage?: boolean; - generated?: boolean; - code: string; -} - -export function* generate( - ts: typeof import('typescript'), - fileName: string, - script: Sfc['script'], - scriptSetup: Sfc['scriptSetup'], - styles: Sfc['styles'], // TODO: computed it - lang: string, - scriptRanges: ScriptRanges | undefined, - scriptSetupRanges: ScriptSetupRanges | undefined, - templateCodegen: { - tsCodes: Code[]; - tsCodegenStacks: string[]; - tagNames: Set; - accessedGlobalVariables: Set; - hasSlot: boolean; - } | undefined, - compilerOptions: ts.CompilerOptions, - vueCompilerOptions: VueCompilerOptions, - globalTypesHolder: string | undefined, - getGeneratedLength: () => number, - linkedCodeMappings: Mapping[] = [], - codegenStack: boolean, -): Generator { - - //#region monkey fix: https://github.com/vuejs/language-tools/pull/2113 - if (!script && !scriptSetup) { - scriptSetup = { - content: '', - lang: 'ts', - name: '', - start: 0, - end: 0, - startTagEnd: 0, - endTagStart: 0, - generic: undefined, - genericOffset: 0, - attrs: {}, - ast: ts.createSourceFile('', '', 99 satisfies ts.ScriptTarget.Latest, false, ts.ScriptKind.TS), - }; - scriptSetupRanges = { - bindings: [], - props: {}, - emits: {}, - expose: {}, - slots: {}, - defineProp: [], - importSectionEndOffset: 0, - leadingCommentEndOffset: 0, - }; - } - //#endregion - - const bindingNames = new Set([ - ...scriptRanges?.bindings.map(range => script!.content.substring(range.start, range.end)) ?? [], - ...scriptSetupRanges?.bindings.map(range => scriptSetup!.content.substring(range.start, range.end)) ?? [], - ]); - const bypassDefineComponent = lang === 'js' || lang === 'jsx'; - const _ = (code: Code): CodeAndStack => { - if (typeof code !== 'string') { - code[3].structure = false; - code[3].format = false; - } - if (!codegenStack) { - return [code, '']; - } - else { - return [code, getStack()]; - } - }; - const helperTypes = { - OmitKeepDiscriminatedUnion: { - get name() { - this.usage = true; - return `__VLS_OmitKeepDiscriminatedUnion`; - }, - get code() { - return `type __VLS_OmitKeepDiscriminatedUnion = T extends any - ? Pick> - : never;`; - }, - } satisfies HelperType as HelperType, - WithDefaults: { - get name() { - this.usage = true; - return `__VLS_WithDefaults`; - }, - get code(): string { - return `type __VLS_WithDefaults = { - [K in keyof Pick]: K extends keyof D - ? ${helperTypes.Prettify.name} - : P[K] - };`; - }, - } satisfies HelperType as HelperType, - Prettify: { - get name() { - this.usage = true; - return `__VLS_Prettify`; - }, - get code() { - return `type __VLS_Prettify = { [K in keyof T]: T[K]; } & {};`; - }, - } satisfies HelperType as HelperType, - WithTemplateSlots: { - get name() { - this.usage = true; - return `__VLS_WithTemplateSlots`; - }, - get code(): string { - return `type __VLS_WithTemplateSlots = T & { - new(): { - ${getSlotsPropertyName(vueCompilerOptions.target)}: S; - ${vueCompilerOptions.jsxSlots ? `$props: ${helperTypes.PropsChildren.name};` : ''} - } - };`; - }, - } satisfies HelperType as HelperType, - PropsChildren: { - get name() { - this.usage = true; - return `__VLS_PropsChildren`; - }, - get code() { - return `type __VLS_PropsChildren = { - [K in keyof ( - boolean extends ( - // @ts-ignore - JSX.ElementChildrenAttribute extends never - ? true - : false - ) - ? never - // @ts-ignore - : JSX.ElementChildrenAttribute - )]?: S; - };`; - }, - } satisfies HelperType as HelperType, - TypePropsToOption: { - get name() { - this.usage = true; - return `__VLS_TypePropsToOption`; - }, - get code() { - return compilerOptions.exactOptionalPropertyTypes ? - `type __VLS_TypePropsToOption = { - [K in keyof T]-?: {} extends Pick - ? { type: import('${vueCompilerOptions.lib}').PropType } - : { type: import('${vueCompilerOptions.lib}').PropType, required: true } - };` : - `type __VLS_NonUndefinedable = T extends undefined ? never : T; - type __VLS_TypePropsToOption = { - [K in keyof T]-?: {} extends Pick - ? { type: import('${vueCompilerOptions.lib}').PropType<__VLS_NonUndefinedable> } - : { type: import('${vueCompilerOptions.lib}').PropType, required: true } - };`; - }, - } satisfies HelperType as HelperType, - }; - - let generatedTemplate = false; - let scriptSetupGeneratedOffset: number | undefined; - - yield _(`/* __placeholder__ */\n`); - yield* generateSrc(); - yield* generateScriptSetupImports(); - yield* generateScriptContentBeforeExportDefault(); - yield* generateScriptSetupAndTemplate(); - yield* generateScriptContentAfterExportDefault(); - if (globalTypesHolder === fileName) { - yield _(generateGlobalTypes(vueCompilerOptions)); - } - yield* generateLocalHelperTypes(); - yield _(`\ntype __VLS_IntrinsicElementsCompletion = __VLS_IntrinsicElements;\n`); - - if (!generatedTemplate) { - yield* generateTemplate(false); - } - - if (scriptSetup) { - yield _(['', 'scriptSetup', scriptSetup.content.length, disableAllFeatures({ verification: true })]); - } - - function* generateLocalHelperTypes(): Generator { - let shouldCheck = true; - while (shouldCheck) { - shouldCheck = false; - for (const helperType of Object.values(helperTypes)) { - if (helperType.usage && !helperType.generated) { - shouldCheck = true; - helperType.generated = true; - yield _('\n' + helperType.code + '\n'); - } - } - } - } - function* generateSrc(): Generator { - if (!script?.src) { - return; - } - - let src = script.src; - - if (src.endsWith('.d.ts')) { - src = src.substring(0, src.length - '.d.ts'.length); - } - else if (src.endsWith('.ts')) { - src = src.substring(0, src.length - '.ts'.length); - } - else if (src.endsWith('.tsx')) { - src = src.substring(0, src.length - '.tsx'.length) + '.jsx'; - } - - if (!src.endsWith('.js') && !src.endsWith('.jsx')) { - src = src + '.js'; - } - - yield _(`export * from `); - yield _([ - `'${src}'`, - 'script', - script.srcOffset - 1, - enableAllFeatures({ - navigation: src === script.src ? true : { - shouldRename: () => false, - resolveRenameEditText(newName) { - if (newName.endsWith('.jsx') || newName.endsWith('.js')) { - newName = newName.split('.').slice(0, -1).join('.'); - } - if (script?.src?.endsWith('.d.ts')) { - newName = newName + '.d.ts'; - } - else if (script?.src?.endsWith('.ts')) { - newName = newName + '.ts'; - } - else if (script?.src?.endsWith('.tsx')) { - newName = newName + '.tsx'; - } - return newName; - }, - }, - }), - ]); - yield _(`;\n`); - yield _(`export { default } from '${src}';\n`); - } - function* generateScriptContentBeforeExportDefault(): Generator { - if (!script) { - return; - } - - if (!!scriptSetup && scriptRanges?.exportDefault) { - return yield _(generateSourceCode(script, 0, scriptRanges.exportDefault.expression.start)); - } - - let isExportRawObject = false; - if (scriptRanges?.exportDefault) { - isExportRawObject = script.content.substring(scriptRanges.exportDefault.expression.start, scriptRanges.exportDefault.expression.end).startsWith('{'); - } - - if (!isExportRawObject || !vueCompilerOptions.optionsWrapper.length || !scriptRanges?.exportDefault) { - return yield _(generateSourceCode(script, 0, script.content.length)); - } - - yield _(generateSourceCode(script, 0, scriptRanges.exportDefault.expression.start)); - yield _(vueCompilerOptions.optionsWrapper[0]); - yield _(['', 'script', scriptRanges.exportDefault.expression.start, disableAllFeatures({ - __hint: { - setting: 'vue.inlayHints.optionsWrapper', - label: vueCompilerOptions.optionsWrapper[0], - tooltip: [ - 'This is virtual code that is automatically wrapped for type support, it does not affect your runtime behavior, you can customize it via `vueCompilerOptions.optionsWrapper` option in tsconfig / jsconfig.', - 'To hide it, you can set `"vue.inlayHints.optionsWrapper": false` in IDE settings.', - ].join('\n\n'), - } - })]); - yield _(generateSourceCode(script, scriptRanges.exportDefault.expression.start, scriptRanges.exportDefault.expression.end)); - yield _(['', 'script', scriptRanges.exportDefault.expression.end, disableAllFeatures({ - __hint: { - setting: 'vue.inlayHints.optionsWrapper', - label: vueCompilerOptions.optionsWrapper[1], - tooltip: '', - } - })]); - yield _(vueCompilerOptions.optionsWrapper[1]); - yield _(generateSourceCode(script, scriptRanges.exportDefault.expression.end, script.content.length)); - } - function* generateScriptContentAfterExportDefault(): Generator { - if (!script) { - return; - } - - if (!!scriptSetup && scriptRanges?.exportDefault) { - yield _(generateSourceCode(script, scriptRanges.exportDefault.expression.end, script.content.length)); - } - } - function* generateScriptSetupImports(): Generator { - if (!scriptSetup || !scriptSetupRanges) { - return; - } - - yield _([ - scriptSetup.content.substring(0, Math.max(scriptSetupRanges.importSectionEndOffset, scriptSetupRanges.leadingCommentEndOffset)) + '\n', - 'scriptSetup', - 0, - enableAllFeatures({}), - ]); - } - function* generateModelEmits(): Generator { - if (!scriptSetup || !scriptSetupRanges) { - return; - } - - yield _(`let __VLS_modelEmitsType!: {}`); - - if (scriptSetupRanges.defineProp.length) { - yield _(` & ReturnType>`); - } - yield _(`;\n`); - } - function* generateScriptSetupAndTemplate(): Generator { - if (!scriptSetup || !scriptSetupRanges) { - return; - } - - const definePropMirrors = new Map(); - - if (scriptSetup.generic) { - if (!scriptRanges?.exportDefault) { - yield _(`export default `); - } - yield _(`(<`); - yield _([ - scriptSetup.generic, - scriptSetup.name, - scriptSetup.genericOffset, - enableAllFeatures({}), - ]); - if (!scriptSetup.generic.endsWith(`,`)) { - yield _(`,`); - } - yield _(`>`); - yield _(`(\n` - + ` __VLS_props: Awaited['props'],\n` - + ` __VLS_ctx?: ${helperTypes.Prettify.name}, 'attrs' | 'emit' | 'slots'>>,\n` // use __VLS_Prettify for less dts code - + ` __VLS_expose?: NonNullable>['expose'],\n` - + ` __VLS_setup = (async () => {\n`); - - yield* generateSetupFunction(true, 'none', definePropMirrors); - - //#region props - yield _(`const __VLS_fnComponent = ` - + `(await import('${vueCompilerOptions.lib}')).defineComponent({\n`); - if (scriptSetupRanges.props.define?.arg) { - yield _(` props: `); - yield _(generateSourceCodeForExtraReference(scriptSetup, scriptSetupRanges.props.define.arg.start, scriptSetupRanges.props.define.arg.end)); - yield _(`,\n`); - } - if (scriptSetupRanges.emits.define) { - yield _(` emits: ({} as __VLS_NormalizeEmits),\n`); - } - yield _(`});\n`); - - if (scriptSetupRanges.defineProp.length) { - yield _(`const __VLS_defaults = {\n`); - for (const defineProp of scriptSetupRanges.defineProp) { - if (defineProp.defaultValue) { - if (defineProp.name) { - yield _(scriptSetup.content.substring(defineProp.name.start, defineProp.name.end)); - } - else { - yield _('modelValue'); - } - yield _(`: `); - yield _(scriptSetup.content.substring(defineProp.defaultValue.start, defineProp.defaultValue.end)); - yield _(`,\n`); - } - } - yield _(`};\n`); - } - - yield _(`let __VLS_fnPropsTypeOnly!: {}`); // TODO: reuse __VLS_fnPropsTypeOnly even without generic, and remove __VLS_propsOption_defineProp - if (scriptSetupRanges.props.define?.typeArg) { - yield _(` & `); - yield _(generateSourceCode(scriptSetup, scriptSetupRanges.props.define.typeArg.start, scriptSetupRanges.props.define.typeArg.end)); - } - if (scriptSetupRanges.defineProp.length) { - yield _(` & {\n`); - for (const defineProp of scriptSetupRanges.defineProp) { - let propName = 'modelValue'; - if (defineProp.name) { - propName = scriptSetup.content.substring(defineProp.name.start, defineProp.name.end); - definePropMirrors.set(propName, getGeneratedLength()); - } - yield _(`${propName}${defineProp.required ? '' : '?'}: `); - if (defineProp.type) { - yield _(scriptSetup.content.substring(defineProp.type.start, defineProp.type.end)); - } - else if (defineProp.defaultValue) { - yield _(`typeof __VLS_defaults['`); - yield _(propName); - yield _(`']`); - } - else { - yield _(`any`); - } - yield _(',\n'); - } - yield _(`}`); - } - yield _(`;\n`); - - yield* generateModelEmits(); - - yield _(`let __VLS_fnPropsDefineComponent!: InstanceType['$props'];\n`); - yield _(`let __VLS_fnPropsSlots!: `); - if (scriptSetupRanges.slots.define && vueCompilerOptions.jsxSlots) { - yield _(`${helperTypes.PropsChildren.name}`); - } - else { - yield _(`{}`); - } - yield _(`;\n`); - - yield _(`let __VLS_defaultProps!:\n` - + ` import('${vueCompilerOptions.lib}').VNodeProps\n` - + ` & import('${vueCompilerOptions.lib}').AllowedComponentProps\n` - + ` & import('${vueCompilerOptions.lib}').ComponentCustomProps;\n`); - //#endregion - - yield _(` return {} as {\n` - + ` props: ${helperTypes.Prettify.name}<${helperTypes.OmitKeepDiscriminatedUnion.name}> & typeof __VLS_fnPropsSlots & typeof __VLS_defaultProps,\n` - + ` expose(exposed: import('${vueCompilerOptions.lib}').ShallowUnwrapRef<${scriptSetupRanges.expose.define ? 'typeof __VLS_exposed' : '{}'}>): void,\n` - + ` attrs: any,\n` - + ` slots: ReturnType,\n` - + ` emit: typeof ${scriptSetupRanges.emits.name ?? '__VLS_emit'} & typeof __VLS_modelEmitsType,\n` - + ` };\n`); - yield _(` })(),\n`); // __VLS_setup = (async () => { - yield _(`) => ({} as import('${vueCompilerOptions.lib}').VNode & { __ctx?: Awaited }))`); - } - else if (!script) { - // no script block, generate script setup code at root - yield* generateSetupFunction(false, 'export', definePropMirrors); - } - else { - if (!scriptRanges?.exportDefault) { - yield _(`export default `); - } - yield _(`await (async () => {\n`); - yield* generateSetupFunction(false, 'return', definePropMirrors); - yield _(`})()`); - } - - if (scriptSetupGeneratedOffset !== undefined) { - for (const defineProp of scriptSetupRanges.defineProp) { - if (!defineProp.name) { - continue; - } - const propName = scriptSetup.content.substring(defineProp.name.start, defineProp.name.end); - const propMirror = definePropMirrors.get(propName); - if (propMirror !== undefined) { - linkedCodeMappings.push({ - sourceOffsets: [defineProp.name.start + scriptSetupGeneratedOffset], - generatedOffsets: [propMirror], - lengths: [defineProp.name.end - defineProp.name.start], - data: undefined, - }); - } - } - } - } - function* generateSetupFunction(functional: boolean, mode: 'return' | 'export' | 'none', definePropMirrors: Map): Generator { - if (!scriptSetupRanges || !scriptSetup) { - return; - } - - const definePropProposalA = scriptSetup.content.trimStart().startsWith('// @experimentalDefinePropProposal=kevinEdition') || vueCompilerOptions.experimentalDefinePropProposal === 'kevinEdition'; - const definePropProposalB = scriptSetup.content.trimStart().startsWith('// @experimentalDefinePropProposal=johnsonEdition') || vueCompilerOptions.experimentalDefinePropProposal === 'johnsonEdition'; - - if (vueCompilerOptions.target >= 3.3) { - yield _(`const { `); - for (const macro of Object.keys(vueCompilerOptions.macros)) { - if (!bindingNames.has(macro)) { - yield _(macro + `, `); - } - } - yield _(`} = await import('${vueCompilerOptions.lib}');\n`); - } - if (definePropProposalA) { - yield _(`declare function defineProp(name: string, options: { required: true } & Record): import('${vueCompilerOptions.lib}').ComputedRef;\n`); - yield _(`declare function defineProp(name: string, options: { default: any } & Record): import('${vueCompilerOptions.lib}').ComputedRef;\n`); - yield _(`declare function defineProp(name?: string, options?: any): import('${vueCompilerOptions.lib}').ComputedRef;\n`); - } - if (definePropProposalB) { - yield _(`declare function defineProp(value: T | (() => T), required?: boolean, rest?: any): import('${vueCompilerOptions.lib}').ComputedRef;\n`); - yield _(`declare function defineProp(value: T | (() => T) | undefined, required: true, rest?: any): import('${vueCompilerOptions.lib}').ComputedRef;\n`); - yield _(`declare function defineProp(value?: T | (() => T), required?: boolean, rest?: any): import('${vueCompilerOptions.lib}').ComputedRef;\n`); - } - - scriptSetupGeneratedOffset = getGeneratedLength() - scriptSetupRanges.importSectionEndOffset; - - let setupCodeModifies: [Code[], number, number][] = []; - if (scriptSetupRanges.props.define && !scriptSetupRanges.props.name) { - const range = scriptSetupRanges.props.withDefaults ?? scriptSetupRanges.props.define; - const statement = scriptSetupRanges.props.define.statement; - if (statement.start === range.start && statement.end === range.end) { - setupCodeModifies.push([[`const __VLS_props = `], range.start, range.start]); - } - else { - setupCodeModifies.push([[ - `const __VLS_props = `, - generateSourceCode(scriptSetup, range.start, range.end), - `;\n`, - generateSourceCode(scriptSetup, statement.start, range.start), - `__VLS_props`, - ], statement.start, range.end]); - } - } - if (scriptSetupRanges.slots.define && !scriptSetupRanges.slots.name) { - setupCodeModifies.push([[`const __VLS_slots = `], scriptSetupRanges.slots.define.start, scriptSetupRanges.slots.define.start]); - } - if (scriptSetupRanges.emits.define && !scriptSetupRanges.emits.name) { - setupCodeModifies.push([[`const __VLS_emit = `], scriptSetupRanges.emits.define.start, scriptSetupRanges.emits.define.start]); - } - if (scriptSetupRanges.expose.define) { - if (scriptSetupRanges.expose.define?.typeArg) { - setupCodeModifies.push([ - [ - `let __VLS_exposed!: `, - generateSourceCodeForExtraReference(scriptSetup, scriptSetupRanges.expose.define.typeArg.start, scriptSetupRanges.expose.define.typeArg.end), - `;\n`, - ], - scriptSetupRanges.expose.define.start, - scriptSetupRanges.expose.define.start, - ]); - } - else if (scriptSetupRanges.expose.define?.arg) { - setupCodeModifies.push([ - [ - `const __VLS_exposed = `, - generateSourceCodeForExtraReference(scriptSetup, scriptSetupRanges.expose.define.arg.start, scriptSetupRanges.expose.define.arg.end), - `;\n`, - ], - scriptSetupRanges.expose.define.start, - scriptSetupRanges.expose.define.start, - ]); - } - else { - setupCodeModifies.push([ - [`const __VLS_exposed = {};\n`], - scriptSetupRanges.expose.define.start, - scriptSetupRanges.expose.define.start, - ]); - } - } - setupCodeModifies = setupCodeModifies.sort((a, b) => a[1] - b[1]); - - if (setupCodeModifies.length) { - yield _(generateSourceCode(scriptSetup, scriptSetupRanges.importSectionEndOffset, setupCodeModifies[0][1])); - while (setupCodeModifies.length) { - const [codes, _start, end] = setupCodeModifies.shift()!; - for (const code of codes) { - yield _(code); - } - if (setupCodeModifies.length) { - const nextStart = setupCodeModifies[0][1]; - yield _(generateSourceCode(scriptSetup, end, nextStart)); - } - else { - yield _(generateSourceCode(scriptSetup, end)); - } - } - } - else { - yield _(generateSourceCode(scriptSetup, scriptSetupRanges.importSectionEndOffset)); - } - - if (scriptSetupRanges.props.define?.typeArg && scriptSetupRanges.props.withDefaults?.arg) { - // fix https://github.com/vuejs/language-tools/issues/1187 - yield _(`const __VLS_withDefaultsArg = (function (t: T) { return t })(`); - yield _(generateSourceCodeForExtraReference(scriptSetup, scriptSetupRanges.props.withDefaults.arg.start, scriptSetupRanges.props.withDefaults.arg.end)); - yield _(`);\n`); - } - - if (!functional && scriptSetupRanges.defineProp.length) { - yield _(`let __VLS_propsOption_defineProp!: {\n`); - for (const defineProp of scriptSetupRanges.defineProp) { - - let propName = 'modelValue'; - - if (defineProp.name && defineProp.nameIsString) { - // renaming support - yield _(generateSourceCodeForExtraReference(scriptSetup, defineProp.name.start, defineProp.name.end)); - } - else if (defineProp.name) { - propName = scriptSetup.content.substring(defineProp.name.start, defineProp.name.end); - const start = getGeneratedLength(); - definePropMirrors.set(propName, start); - yield _(propName); - } - else { - yield _(propName); - } - yield _(`: `); - - let type = 'any'; - if (!defineProp.nameIsString) { - type = `NonNullable`; - } - else if (defineProp.type) { - type = scriptSetup.content.substring(defineProp.type.start, defineProp.type.end); - } - - if (defineProp.required) { - yield _(`{ required: true, type: import('${vueCompilerOptions.lib}').PropType<${type}> },\n`); - } - else { - yield _(`import('${vueCompilerOptions.lib}').PropType<${type}>,\n`); - } - - if (defineProp.modifierType) { - let propModifierName = 'modelModifiers'; - - if (defineProp.name) { - propModifierName = `${scriptSetup.content.substring(defineProp.name.start + 1, defineProp.name.end - 1)}Modifiers`; - } - - const modifierType = scriptSetup.content.substring(defineProp.modifierType.start, defineProp.modifierType.end); - - const start = getGeneratedLength(); - definePropMirrors.set(propModifierName, start); - yield _(propModifierName); - yield _(`: `); - yield _(`import('${vueCompilerOptions.lib}').PropType>,\n`); - } - } - yield _(`};\n`); - } - - yield* generateModelEmits(); - yield* generateTemplate(functional); - - if (mode === 'return' || mode === 'export') { - if (!vueCompilerOptions.skipTemplateCodegen && (templateCodegen?.hasSlot || scriptSetupRanges?.slots.define)) { - yield _(`const __VLS_component = `); - yield* generateComponent(functional); - yield _(`;\n`); - yield _(mode === 'return' ? 'return ' : 'export default '); - yield _(`{} as ${helperTypes.WithTemplateSlots.name}>;\n`); - } - else { - yield _(mode === 'return' ? 'return ' : 'export default '); - yield* generateComponent(functional); - yield _(`;\n`); - } - } - } - function* generateComponent(functional: boolean): Generator { - if (!scriptSetupRanges) { - return; - } - - if (script && scriptRanges?.exportDefault && scriptRanges.exportDefault.expression.start !== scriptRanges.exportDefault.args.start) { - // use defineComponent() from user space code if it exist - yield _(generateSourceCode(script, scriptRanges.exportDefault.expression.start, scriptRanges.exportDefault.args.start)); - yield _(`{\n`); - } - else { - yield _(`(await import('${vueCompilerOptions.lib}')).defineComponent({\n`); - } - - yield _(`setup() {\n`); - yield _(`return {\n`); - yield* generateSetupReturns(); - if (scriptSetupRanges.expose.define) { - yield _(`...__VLS_exposed,\n`); - } - yield _(`};\n`); - yield _(`},\n`); - yield* generateComponentOptions(functional); - yield _(`})`); - } - function* generateComponentOptions(functional: boolean): Generator { - if (scriptSetup && scriptSetupRanges && !bypassDefineComponent) { - - const ranges = scriptSetupRanges; - const propsCodegens: (() => Generator)[] = []; - - if (ranges.props.define?.arg) { - const arg = ranges.props.define.arg; - propsCodegens.push(function* () { - yield _(generateSourceCodeForExtraReference(scriptSetup!, arg.start, arg.end)); - }); - } - if (ranges.props.define?.typeArg) { - const typeArg = ranges.props.define.typeArg; - propsCodegens.push(function* () { - - yield _(`{} as `); - - if (ranges.props.withDefaults?.arg) { - yield _(`${helperTypes.WithDefaults.name}<`); - } - - yield _(`${helperTypes.TypePropsToOption.name}<`); - if (functional) { - yield _(`typeof __VLS_fnPropsTypeOnly`); - } - else { - yield _(generateSourceCodeForExtraReference(scriptSetup!, typeArg.start, typeArg.end)); - } - yield _(`>`); - - if (ranges.props.withDefaults?.arg) { - yield _(`, typeof __VLS_withDefaultsArg`); - yield _(`>`); - } - }); - } - if (!functional && ranges.defineProp.length) { - propsCodegens.push(function* () { - yield _(`__VLS_propsOption_defineProp`); - }); - } - - if (propsCodegens.length === 1) { - yield _(`props: `); - for (const generate of propsCodegens) { - yield* generate(); - } - yield _(`,\n`); - } - else if (propsCodegens.length >= 2) { - yield _(`props: {\n`); - for (const generate of propsCodegens) { - yield _(`...`); - yield* generate(); - yield _(`,\n`); - } - yield _(`},\n`); - } - if (ranges.defineProp.filter(p => p.isModel).length || ranges.emits.define) { - yield _(`emits: ({} as __VLS_NormalizeEmits),\n`); - } - } - if (script && scriptRanges?.exportDefault?.args) { - yield _(generateSourceCode(script, scriptRanges.exportDefault.args.start + 1, scriptRanges.exportDefault.args.end - 1)); - } - } - function* generateSetupReturns(): Generator { - if (scriptSetupRanges && bypassDefineComponent) { - // fill $props - if (scriptSetupRanges.props.define) { - // NOTE: defineProps is inaccurate for $props - yield _(`$props: __VLS_makeOptional(${scriptSetupRanges.props.name ?? `__VLS_props`}),\n`); - yield _(`...${scriptSetupRanges.props.name ?? `__VLS_props`},\n`); - } - // fill $emit - if (scriptSetupRanges.emits.define) { - yield _(`$emit: ${scriptSetupRanges.emits.name ?? '__VLS_emit'},\n`); - } - } - } - function* generateTemplate(functional: boolean): Generator { - - generatedTemplate = true; - - if (!vueCompilerOptions.skipTemplateCodegen) { - yield* generateExportOptions(); - yield* generateConstNameOption(); - yield _(`function __VLS_template() {\n`); - const cssIds = new Set(); - yield* generateTemplateContext(cssIds); - yield _(`}\n`); - yield* generateComponentForTemplateUsage(functional, cssIds); - } - else { - yield _(`function __VLS_template() {\n`); - const templateUsageVars = [...getTemplateUsageVars()]; - yield _(`// @ts-ignore\n`); - yield _(`[${templateUsageVars.join(', ')}]\n`); - yield _(`return {};\n`); - yield _(`}\n`); - } - } - function* generateComponentForTemplateUsage(functional: boolean, cssIds: Set): Generator { - - if (scriptSetup && scriptSetupRanges) { - - yield _(`const __VLS_internalComponent = (await import('${vueCompilerOptions.lib}')).defineComponent({\n`); - yield _(`setup() {\n`); - yield _(`return {\n`); - yield* generateSetupReturns(); - // bindings - const templateUsageVars = getTemplateUsageVars(); - for (const [content, bindings] of [ - [scriptSetup.content, scriptSetupRanges.bindings] as const, - scriptRanges && script - ? [script.content, scriptRanges.bindings] as const - : ['', []] as const, - ]) { - for (const expose of bindings) { - const varName = content.substring(expose.start, expose.end); - if (!templateUsageVars.has(varName) && !cssIds.has(varName)) { - continue; - } - const templateOffset = getGeneratedLength(); - yield _(varName); - yield _(`: ${varName} as typeof `); - - const scriptOffset = getGeneratedLength(); - yield _(varName); - yield _(`,\n`); - - linkedCodeMappings.push({ - sourceOffsets: [scriptOffset], - generatedOffsets: [templateOffset], - lengths: [varName.length], - data: undefined, - }); - } - } - yield _(`};\n`); // return { - yield _(`},\n`); // setup() { - yield* generateComponentOptions(functional); - yield _(`});\n`); // defineComponent { - } - else if (script) { - yield _(`let __VLS_internalComponent!: typeof import('./${path.basename(fileName)}')['default'];\n`); - } - else { - yield _(`const __VLS_internalComponent = (await import('${vueCompilerOptions.lib}')).defineComponent({});\n`); - } - } - function* generateExportOptions(): Generator { - yield _(`\n`); - yield _(`const __VLS_componentsOption = `); - if (script && scriptRanges?.exportDefault?.componentsOption) { - const componentsOption = scriptRanges.exportDefault.componentsOption; - yield _([ - script.content.substring(componentsOption.start, componentsOption.end), - 'script', - componentsOption.start, - disableAllFeatures({ - navigation: true, - }), - ]); - } - else { - yield _(`{}`); - } - yield _(`;\n`); - } - function* generateConstNameOption(): Generator { - yield _(`\n`); - if (script && scriptRanges?.exportDefault?.nameOption) { - const nameOption = scriptRanges.exportDefault.nameOption; - yield _(`const __VLS_name = `); - yield _(`${script.content.substring(nameOption.start, nameOption.end)} as const`); - yield _(`;\n`); - } - else if (scriptSetup) { - yield _(`let __VLS_name!: '${path.basename(fileName.substring(0, fileName.lastIndexOf('.')))}';\n`); - } - else { - yield _(`const __VLS_name = undefined;\n`); - } - } - function* generateTemplateContext(cssIds = new Set()): Generator { - - const useGlobalThisTypeInCtx = fileName.endsWith('.html'); - - yield _(`let __VLS_ctx!: ${useGlobalThisTypeInCtx ? 'typeof globalThis &' : ''}`); - yield _(`InstanceType<__VLS_PickNotAny {}>> & {\n`); - - /* CSS Module */ - for (let i = 0; i < styles.length; i++) { - const style = styles[i]; - if (style.module) { - yield _(`${style.module}: Record & ${helperTypes.Prettify.name}<{}`); - for (const className of style.classNames) { - yield* generateCssClassProperty( - i, - className.text, - className.offset, - 'string', - false, - true, - ); - } - yield _(`>;\n`); - } - } - yield _(`};\n`); - - /* Components */ - yield _(`/* Components */\n`); - yield _(`let __VLS_otherComponents!: NonNullable & typeof __VLS_componentsOption;\n`); - yield _(`let __VLS_own!: __VLS_SelfComponent { ${getSlotsPropertyName(vueCompilerOptions.target)}: typeof ${scriptSetupRanges?.slots?.name ?? '__VLS_slots'} })>;\n`); - yield _(`let __VLS_localComponents!: typeof __VLS_otherComponents & Omit;\n`); - yield _(`let __VLS_components!: typeof __VLS_localComponents & __VLS_GlobalComponents & typeof __VLS_ctx;\n`); // for html completion, TS references... - - /* Style Scoped */ - yield _(`/* Style Scoped */\n`); - yield _(`type __VLS_StyleScopedClasses = {}`); - for (let i = 0; i < styles.length; i++) { - const style = styles[i]; - const option = vueCompilerOptions.experimentalResolveStyleCssClasses; - if (option === 'always' || (option === 'scoped' && style.scoped)) { - for (const className of style.classNames) { - yield* generateCssClassProperty( - i, - className.text, - className.offset, - 'boolean', - true, - !style.module, - ); - } - } - } - yield _(`;\n`); - yield _('let __VLS_styleScopedClasses!: __VLS_StyleScopedClasses | keyof __VLS_StyleScopedClasses | (keyof __VLS_StyleScopedClasses)[];\n'); - - yield _(`/* CSS variable injection */\n`); - yield* generateCssVars(cssIds); - yield _(`/* CSS variable injection end */\n`); - - if (templateCodegen) { - for (let i = 0; i < templateCodegen.tsCodes.length; i++) { - yield [ - templateCodegen.tsCodes[i], - templateCodegen.tsCodegenStacks[i], - ]; - } - } - else { - yield _(`// no template\n`); - if (!scriptSetupRanges?.slots.define) { - yield _(`const __VLS_slots = {};\n`); - } - } - - yield _(`return ${scriptSetupRanges?.slots.name ?? '__VLS_slots'};\n`); - - } - function* generateCssClassProperty( - styleIndex: number, - classNameWithDot: string, - offset: number, - propertyType: string, - optional: boolean, - referencesCodeLens: boolean - ): Generator { - yield _(`\n & { `); - yield _([ - '', - 'style_' + styleIndex, - offset, - disableAllFeatures({ - navigation: true, - __referencesCodeLens: referencesCodeLens, - }), - ]); - yield _(`'`); - yield _([ - '', - 'style_' + styleIndex, - offset, - disableAllFeatures({ - navigation: { - resolveRenameNewName: normalizeCssRename, - resolveRenameEditText: applyCssRename, - }, - }), - ]); - yield _([ - classNameWithDot.substring(1), - 'style_' + styleIndex, - offset + 1, - disableAllFeatures({ __combineLastMapping: true }), - ]); - yield _(`'`); - yield _([ - '', - 'style_' + styleIndex, - offset + classNameWithDot.length, - disableAllFeatures({}), - ]); - yield _(`${optional ? '?' : ''}: ${propertyType}`); - yield _(` }`); - } - function* generateCssVars(cssIds: Set): Generator { - - const emptyLocalVars = new Map(); - - for (const style of styles) { - for (const cssBind of style.cssVars) { - for (const [segment, offset, onlyError] of eachInterpolationSegment( - ts, - cssBind.text, - ts.createSourceFile('/a.txt', cssBind.text, 99 satisfies ts.ScriptTarget.ESNext), - emptyLocalVars, - cssIds, - vueCompilerOptions, - )) { - if (offset === undefined) { - yield _(segment); - } - else { - yield _([ - segment, - style.name, - cssBind.offset + offset, - onlyError - ? disableAllFeatures({ verification: true }) - : enableAllFeatures({}), - ]); - } - } - yield _(`;\n`); - } - } - } - function getTemplateUsageVars() { - - const usageVars = new Set(); - - if (templateCodegen) { - // fix import components unused report - for (const varName of bindingNames) { - if (templateCodegen.tagNames.has(varName) || templateCodegen.tagNames.has(hyphenateTag(varName))) { - usageVars.add(varName); - } - } - for (const tag of Object.keys(templateCodegen.tagNames)) { - if (tag.indexOf('.') >= 0) { - usageVars.add(tag.split('.')[0]); - } - } - for (const _id of templateCodegen.accessedGlobalVariables) { - usageVars.add(_id); - } - } - - return usageVars; - } - function generateSourceCode(block: SfcBlock, start: number, end?: number): Code { - return [ - block.content.substring(start, end), - block.name, - start, - enableAllFeatures({}), // diagnostic also working for setup() returns unused in template checking - ]; - } - function generateSourceCodeForExtraReference(block: SfcBlock, start: number, end: number): Code { - return [ - block.content.substring(start, end), - block.name, - start, - disableAllFeatures({ navigation: true }), - ]; - } -} - -function normalizeCssRename(newName: string) { - return newName.startsWith('.') ? newName.slice(1) : newName; -} - -function applyCssRename(newName: string) { - return '.' + newName; -} diff --git a/packages/language-core/lib/generators/template.ts b/packages/language-core/lib/generators/template.ts deleted file mode 100644 index 07ee00be7a..0000000000 --- a/packages/language-core/lib/generators/template.ts +++ /dev/null @@ -1,2047 +0,0 @@ -import { toString } from '@volar/language-core'; -import * as CompilerDOM from '@vue/compiler-dom'; -import { camelize, capitalize } from '@vue/shared'; -import { minimatch } from 'minimatch'; -import type * as ts from 'typescript'; -import type { Code, CodeAndStack, Sfc, VueCodeInformation, VueCompilerOptions } from '../types'; -import { hyphenateAttr, hyphenateTag } from '../utils/shared'; -import { collectVars, eachInterpolationSegment } from '../utils/transform'; -import { disableAllFeatures, enableAllFeatures, getStack, mergeFeatureSettings } from './utils'; - -const presetInfos = { - disabledAll: disableAllFeatures({}), - all: enableAllFeatures({}), - allWithHiddenParam: enableAllFeatures({ - __hint: { - setting: 'vue.inlayHints.inlineHandlerLeading', - label: '$event =>', - tooltip: [ - '`$event` is a hidden parameter, you can use it in this callback.', - 'To hide this hint, set `vue.inlayHints.inlineHandlerLeading` to `false` in IDE settings.', - '[More info](https://github.com/vuejs/language-tools/issues/2445#issuecomment-1444771420)', - ].join('\n\n'), - paddingRight: true, - } - }), - noDiagnostics: enableAllFeatures({ verification: false }), - diagnosticOnly: disableAllFeatures({ verification: true }), - tagHover: disableAllFeatures({ semantic: { shouldHighlight: () => false } }), - event: disableAllFeatures({ semantic: { shouldHighlight: () => false }, verification: true }), - tagReference: disableAllFeatures({ navigation: { shouldRename: () => false } }), - attr: disableAllFeatures({ semantic: { shouldHighlight: () => false }, verification: true, navigation: true }), - attrReference: disableAllFeatures({ navigation: true }), - slotProp: disableAllFeatures({ navigation: true, verification: true }), - scopedClassName: disableAllFeatures({ navigation: true, completion: true }), - slotName: disableAllFeatures({ semantic: { shouldHighlight: () => false }, verification: true, navigation: true, completion: true }), - slotNameExport: disableAllFeatures({ semantic: { shouldHighlight: () => false }, verification: true, navigation: true, /* __navigationCodeLens: true */ }), - refAttr: disableAllFeatures({ navigation: true }), -}; -const validTsVarReg = /^[a-zA-Z_$][0-9a-zA-Z_$]*$/; -const colonReg = /:/g; -// @ts-ignore -const transformContext: CompilerDOM.TransformContext = { - onError: () => { }, - helperString: str => str.toString(), - replaceNode: () => { }, - cacheHandlers: false, - prefixIdentifiers: false, - scopes: { - vFor: 0, - vOnce: 0, - vPre: 0, - vSlot: 0, - }, - expressionPlugins: ['typescript'], -}; - -export function* generate( - ts: typeof import('typescript'), - compilerOptions: ts.CompilerOptions, - vueCompilerOptions: VueCompilerOptions, - template: NonNullable, - shouldGenerateScopedClasses: boolean, - stylesScopedClasses: Set, - hasScriptSetupSlots: boolean, - slotsAssignName: string | undefined, - propsAssignName: string | undefined, - codegenStack: boolean, -) { - - const processDirectiveComment = (code: Code) => { - if (typeof code !== 'string') { - if (ignoreError) { - const data = code[3]; - if (data.verification) { - code[3] = { - ...data, - verification: false, - }; - } - } - if (expectErrorToken) { - const token = expectErrorToken; - const data = code[3]; - if (data.verification && (typeof data.verification !== 'object' || !data.verification.shouldReport)) { - code[3] = { - ...data, - verification: { - shouldReport: () => { - token.errors++; - return false; - }, - }, - }; - } - } - code[3].structure = false; - code[3].format = false; - } - return code; - }; - const _ts = codegenStack - ? (code: Code): CodeAndStack => [processDirectiveComment(code), getStack()] - : (code: Code): CodeAndStack => [processDirectiveComment(code), '']; - const nativeTags = new Set(vueCompilerOptions.nativeTags); - const slots = new Map(); - const slotExps = new Map(); - const tagOffsetsMap = collectTagOffsets(); - const localVars = new Map(); - const tempVars: { - text: string, - isShorthand: boolean, - offset: number, - }[][] = []; - const accessedGlobalVariables = new Set(); - const scopedClasses: { className: string, offset: number; }[] = []; - const blockConditions: string[] = []; - const hasSlotElements = new Set(); - const usedComponentCtxVars = new Set(); - - let hasSlot = false; - let ignoreError = false; - let expectErrorToken: { - errors: number; - node: CompilerDOM.CommentNode; - } | undefined; - let elementIndex = 0; - - if (slotsAssignName) { - localVars.set(slotsAssignName, 1); - } - - if (propsAssignName) { - localVars.set(propsAssignName, 1); - } - - yield* generatePreResolveComponents(); - - if (template.ast) { - yield* generateAstNode(template.ast, undefined, undefined, undefined); - } - - yield* generateStyleScopedClasses(); - - if (!hasScriptSetupSlots) { - yield _ts('var __VLS_slots!:'); - yield* generateSlotsType(); - yield _ts(';\n'); - } - - yield* generateExtraAutoImport(); - - return { - tagOffsetsMap, - accessedGlobalVariables, - hasSlot, - }; - - function collectTagOffsets() { - - const tagOffsetsMap = new Map(); - - if (!template.ast) { - return tagOffsetsMap; - } - - for (const node of forEachElementNode(template.ast)) { - if (node.tag === 'slot') { - // ignore - } - else if (node.tag === 'component' || node.tag === 'Component') { - for (const prop of node.props) { - if (prop.type === CompilerDOM.NodeTypes.ATTRIBUTE && prop.name === 'is' && prop.value) { - const tag = prop.value.content; - let offsets = tagOffsetsMap.get(tag); - if (!offsets) { - tagOffsetsMap.set(tag, offsets = []); - } - offsets.push(prop.value.loc.start.offset + prop.value.loc.source.lastIndexOf(tag)); - break; - } - } - } - else { - let offsets = tagOffsetsMap.get(node.tag); - if (!offsets) { - tagOffsetsMap.set(node.tag, offsets = []); - } - const source = template.content.substring(node.loc.start.offset); - const startTagOffset = node.loc.start.offset + source.indexOf(node.tag); - - offsets.push(startTagOffset); // start tag - if (!node.isSelfClosing && template.lang === 'html') { - const endTagOffset = node.loc.start.offset + node.loc.source.lastIndexOf(node.tag); - if (endTagOffset !== startTagOffset) { - offsets.push(endTagOffset); // end tag - } - } - } - } - - return tagOffsetsMap; - } - - function* resetDirectiveComments(endStr: string): Generator { - if (expectErrorToken) { - const token = expectErrorToken; - yield _ts([ - '', - 'template', - expectErrorToken.node.loc.start.offset, - disableAllFeatures({ - verification: { - shouldReport: () => token.errors === 0, - }, - }), - ]); - yield _ts('// @ts-expect-error __VLS_TS_EXPECT_ERROR'); - yield _ts([ - '', - 'template', - expectErrorToken.node.loc.end.offset, - disableAllFeatures({ __combineLastMapping: true }), - ]); - yield _ts('\n;\n'); - expectErrorToken = undefined; - yield _ts(`// @vue-expect-error ${endStr}\n`); - } - if (ignoreError) { - ignoreError = false; - yield _ts(`// @vue-ignore ${endStr}\n`); - } - } - - function* generateCanonicalComponentName(tagText: string, offset: number, info: VueCodeInformation): Generator { - if (validTsVarReg.test(tagText)) { - yield _ts([tagText, 'template', offset, info]); - } - else { - yield* generateCamelized( - capitalize(tagText.replace(colonReg, '-')), - offset, - info - ); - } - } - - function* generateSlotsType(): Generator { - for (const [exp, slot] of slotExps) { - hasSlot = true; - yield _ts(`Partial, (_: typeof ${slot.varName}) => any>> &\n`); - } - yield _ts(`{\n`); - for (const [_, slot] of slots) { - hasSlot = true; - if (slot.name && slot.loc !== undefined) { - yield* generateObjectProperty( - slot.name, - slot.loc, - mergeFeatureSettings(presetInfos.slotNameExport, disableAllFeatures({ __referencesCodeLens: true })), - slot.nodeLoc - ); - } - else { - yield _ts(['', 'template', slot.tagRange[0], mergeFeatureSettings(presetInfos.slotNameExport, disableAllFeatures({ __referencesCodeLens: true }))]); - yield _ts('default'); - yield _ts(['', 'template', slot.tagRange[1], disableAllFeatures({ __combineLastMapping: true })]); - } - yield _ts(`?(_: typeof ${slot.varName}): any,\n`); - } - yield _ts(`}`); - } - - function* generateStyleScopedClasses(): Generator { - yield _ts(`if (typeof __VLS_styleScopedClasses === 'object' && !Array.isArray(__VLS_styleScopedClasses)) {\n`); - for (const { className, offset } of scopedClasses) { - yield _ts(`__VLS_styleScopedClasses[`); - yield* generateStringLiteralKey( - className, - offset, - mergeFeatureSettings( - presetInfos.scopedClassName, - disableAllFeatures({ __displayWithLink: stylesScopedClasses.has(className) }), - ), - ); - yield _ts(`];\n`); - } - yield _ts('}\n'); - } - - function* generatePreResolveComponents(): Generator { - - yield _ts(`let __VLS_resolvedLocalAndGlobalComponents!: {}\n`); - - for (const [tagName] of tagOffsetsMap) { - - if (nativeTags.has(tagName)) { - continue; - } - - const isNamespacedTag = tagName.indexOf('.') >= 0; - if (isNamespacedTag) { - continue; - } - - yield _ts(`& __VLS_WithComponent<'${getCanonicalComponentName(tagName)}', typeof __VLS_localComponents, `); - // order is important: https://github.com/vuejs/language-tools/issues/2010 - yield _ts(`"${capitalize(camelize(tagName))}", `); - yield _ts(`"${camelize(tagName)}", `); - yield _ts(`"${tagName}"`); - yield _ts('>\n'); - } - - yield _ts(`;\n`); - - for (const [tagName, tagOffsets] of tagOffsetsMap) { - - for (const tagOffset of tagOffsets) { - if (nativeTags.has(tagName)) { - yield _ts(`__VLS_intrinsicElements`); - yield* generatePropertyAccess( - tagName, - tagOffset, - mergeFeatureSettings( - presetInfos.tagReference, - { - navigation: true - }, - ...[ - presetInfos.tagHover, - presetInfos.diagnosticOnly, - ], - ), - ); - yield _ts(';'); - } - else if (validTsVarReg.test(camelize(tagName))) { - for (const shouldCapitalize of tagName[0] === tagName.toUpperCase() ? [false] : [true, false]) { - const expectName = shouldCapitalize ? capitalize(camelize(tagName)) : camelize(tagName); - yield _ts('__VLS_components.'); - yield* generateCamelized( - shouldCapitalize ? capitalize(tagName) : tagName, - tagOffset, - mergeFeatureSettings( - presetInfos.tagReference, - { - navigation: { - resolveRenameNewName: tagName !== expectName ? camelizeComponentName : undefined, - resolveRenameEditText: getTagRenameApply(tagName), - } - }, - ), - ); - yield _ts(';'); - } - } - } - yield _ts('\n'); - - if ( - !nativeTags.has(tagName) - && validTsVarReg.test(camelize(tagName)) - ) { - yield _ts('// @ts-ignore\n'); // #2304 - yield _ts('['); - for (const tagOffset of tagOffsets) { - yield* generateCamelized( - capitalize(tagName), - tagOffset, - disableAllFeatures({ - completion: { - isAdditional: true, - onlyImport: true, - }, - }), - ); - yield _ts(','); - } - yield _ts(`];\n`); - } - } - } - - function* generateAstNode( - node: CompilerDOM.RootNode | CompilerDOM.TemplateChildNode | CompilerDOM.InterpolationNode | CompilerDOM.CompoundExpressionNode | CompilerDOM.TextNode | CompilerDOM.SimpleExpressionNode, - parentEl: CompilerDOM.ElementNode | undefined, - prevNode: CompilerDOM.TemplateChildNode | undefined, - componentCtxVar: string | undefined, - ): Generator { - if (prevNode?.type === CompilerDOM.NodeTypes.COMMENT) { - const commentText = prevNode.content.trim().split(' ')[0]; - if (commentText.match(/^@vue-skip\b[\s\S]*/)) { - yield _ts('// @vue-skip\n'); - return; - } - else if (commentText.match(/^@vue-ignore\b[\s\S]*/) && !ignoreError) { - ignoreError = true; - yield _ts('// @vue-ignore start\n'); - } - else if (commentText.match(/^@vue-expect-error\b[\s\S]*/) && !expectErrorToken) { - expectErrorToken = { - errors: 0, - node: prevNode, - }; - yield _ts('// @vue-expect-error start\n'); - } - } - - if (node.type === CompilerDOM.NodeTypes.ROOT) { - let prev: CompilerDOM.TemplateChildNode | undefined; - for (const childNode of node.children) { - yield* generateAstNode(childNode, parentEl, prev, componentCtxVar); - prev = childNode; - } - yield* resetDirectiveComments('end of root'); - } - else if (node.type === CompilerDOM.NodeTypes.ELEMENT) { - const vForNode = getVForNode(node); - const vIfNode = getVIfNode(node); - if (vForNode) { - yield* generateVFor(vForNode, parentEl, componentCtxVar); - } - else if (vIfNode) { - yield* generateVIf(vIfNode, parentEl, componentCtxVar); - } - else { - yield* generateElement(node, parentEl, componentCtxVar); - } - } - else if (node.type === CompilerDOM.NodeTypes.TEXT_CALL) { - // {{ var }} - yield* generateAstNode(node.content, parentEl, undefined, componentCtxVar); - } - else if (node.type === CompilerDOM.NodeTypes.COMPOUND_EXPRESSION) { - // {{ ... }} {{ ... }} - for (const childNode of node.children) { - if (typeof childNode === 'object') { - yield* generateAstNode(childNode, parentEl, undefined, componentCtxVar); - } - } - } - else if (node.type === CompilerDOM.NodeTypes.INTERPOLATION) { - // {{ ... }} - const [content, start] = parseInterpolationNode(node, template.content); - yield* generateInterpolation( - content, - node.content.loc, - start, - presetInfos.all, - '(', - ');\n', - ); - yield* resetDirectiveComments('end of INTERPOLATION'); - } - else if (node.type === CompilerDOM.NodeTypes.IF) { - // v-if / v-else-if / v-else - yield* generateVIf(node, parentEl, componentCtxVar); - } - else if (node.type === CompilerDOM.NodeTypes.FOR) { - // v-for - yield* generateVFor(node, parentEl, componentCtxVar); - } - else if (node.type === CompilerDOM.NodeTypes.TEXT) { - // not needed progress - } - } - - function* generateVIf(node: CompilerDOM.IfNode, parentEl: CompilerDOM.ElementNode | undefined, componentCtxVar: string | undefined): Generator { - - let originalBlockConditionsLength = blockConditions.length; - - for (let i = 0; i < node.branches.length; i++) { - - const branch = node.branches[i]; - - if (i === 0) { - yield _ts('if'); - } - else if (branch.condition) { - yield _ts('else if'); - } - else { - yield _ts('else'); - } - - let addedBlockCondition = false; - - if (branch.condition?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION) { - yield _ts(` `); - yield* generateInterpolation( - branch.condition.content, - branch.condition.loc, - branch.condition.loc.start.offset, - presetInfos.all, - '(', - ')', - ); - blockConditions.push( - toString( - [...generateInterpolation(branch.condition.content, branch.condition.loc, undefined, undefined, '(', ')')] - .map(([code]) => code) - ) - ); - addedBlockCondition = true; - } - - yield _ts(` {\n`); - if (isFragment(node)) { - yield* resetDirectiveComments('end of v-if start'); - } - let prev: CompilerDOM.TemplateChildNode | undefined; - for (const childNode of branch.children) { - yield* generateAstNode(childNode, parentEl, prev, componentCtxVar); - prev = childNode; - } - yield* generateExtraAutoImport(); - yield _ts('}\n'); - - if (addedBlockCondition) { - blockConditions[blockConditions.length - 1] = `!(${blockConditions[blockConditions.length - 1]})`; - } - } - - blockConditions.length = originalBlockConditionsLength; - } - - function* generateVFor(node: CompilerDOM.ForNode, parentEl: CompilerDOM.ElementNode | undefined, componentCtxVar: string | undefined): Generator { - const { source } = node.parseResult; - const { leftExpressionRange, leftExpressionText } = parseVForNode(node); - const forBlockVars: string[] = []; - - yield _ts(`for (const [`); - if (leftExpressionRange && leftExpressionText) { - - const collectAst = createTsAst(ts, node.parseResult, `const [${leftExpressionText}]`); - collectVars(ts, collectAst, collectAst, forBlockVars); - - for (const varName of forBlockVars) { - localVars.set(varName, (localVars.get(varName) ?? 0) + 1); - } - - yield _ts([leftExpressionText, 'template', leftExpressionRange.start, presetInfos.all]); - } - yield _ts(`] of __VLS_getVForSourceType`); - if (source.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION) { - yield _ts('('); - yield* generateInterpolation( - source.content, - source.loc, - source.loc.start.offset, - presetInfos.all, - '(', - ')', - ); - yield _ts('!)'); // #3102 - yield _ts(') {\n'); - if (isFragment(node)) { - yield* resetDirectiveComments('end of v-for start'); - } - let prev: CompilerDOM.TemplateChildNode | undefined; - for (const childNode of node.children) { - yield* generateAstNode(childNode, parentEl, prev, componentCtxVar); - prev = childNode; - } - yield* generateExtraAutoImport(); - yield _ts('}\n'); - } - - for (const varName of forBlockVars) { - localVars.set(varName, localVars.get(varName)! - 1); - } - } - - function* generateElement(node: CompilerDOM.ElementNode, parentEl: CompilerDOM.ElementNode | undefined, componentCtxVar: string | undefined): Generator { - - yield _ts(`{\n`); - - const startTagOffset = node.loc.start.offset + template.content.substring(node.loc.start.offset).indexOf(node.tag); - let endTagOffset = !node.isSelfClosing && template.lang === 'html' ? node.loc.start.offset + node.loc.source.lastIndexOf(node.tag) : undefined; - - if (endTagOffset === startTagOffset) { - endTagOffset = undefined; - } - - let tag = node.tag; - let tagOffsets = endTagOffset !== undefined ? [startTagOffset, endTagOffset] : [startTagOffset]; - let props = node.props; - - const propsFailedExps: CompilerDOM.SimpleExpressionNode[] = []; - const isNamespacedTag = tag.indexOf('.') >= 0; - const var_originalComponent = `__VLS_${elementIndex++}`; - const var_functionalComponent = `__VLS_${elementIndex++}`; - const var_componentInstance = `__VLS_${elementIndex++}`; - - let dynamicTagExp: CompilerDOM.ExpressionNode | undefined; - - if (tag === 'slot') { - tagOffsets.length = 0; - } - else if (tag === 'component' || tag === 'Component') { - tagOffsets.length = 0; - for (const prop of node.props) { - if (prop.type === CompilerDOM.NodeTypes.ATTRIBUTE && prop.name === 'is' && prop.value) { - tag = prop.value.content; - tagOffsets = [prop.value.loc.start.offset + prop.value.loc.source.lastIndexOf(tag)]; - props = props.filter(p => p !== prop); - break; - } - else if (prop.type === CompilerDOM.NodeTypes.DIRECTIVE && prop.name === 'bind' && prop.arg?.loc.source === 'is' && prop.exp) { - dynamicTagExp = prop.exp; - props = props.filter(p => p !== prop); - break; - } - } - } - - const isIntrinsicElement = nativeTags.has(tag) && tagOffsets.length; - - if (isIntrinsicElement) { - yield _ts('const '); - yield _ts(var_originalComponent); - yield _ts(` = __VLS_intrinsicElements[`); - yield* generateStringLiteralKey( - tag, - tagOffsets[0], - presetInfos.diagnosticOnly, - ); - yield _ts('];\n'); - } - else if (isNamespacedTag) { - yield _ts(`const ${var_originalComponent} = `); - yield* generateInterpolation(tag, node.loc, startTagOffset, presetInfos.all, '', ''); - yield _ts(';\n'); - } - else if (dynamicTagExp) { - yield _ts(`const ${var_originalComponent} = `); - yield* generateInterpolation(dynamicTagExp.loc.source, dynamicTagExp.loc, dynamicTagExp.loc.start.offset, presetInfos.all, '(', ')'); - yield _ts(';\n'); - } - else { - yield _ts(`const ${var_originalComponent} = ({} as `); - for (const componentName of getPossibleOriginalComponentNames(tag)) { - yield _ts(`'${componentName}' extends keyof typeof __VLS_ctx ? `); - yield _ts(`{ '${getCanonicalComponentName(tag)}': typeof __VLS_ctx`); - yield* generatePropertyAccess(componentName); - yield _ts(` }: `); - } - yield _ts(`typeof __VLS_resolvedLocalAndGlobalComponents)`); - if (tagOffsets.length) { - yield* generatePropertyAccess( - getCanonicalComponentName(tag), - tagOffsets[0], - presetInfos.diagnosticOnly, - ); - } - else { - yield* generatePropertyAccess(getCanonicalComponentName(tag)); - } - yield _ts(';\n'); - } - - if (isIntrinsicElement) { - yield _ts(`const ${var_functionalComponent} = __VLS_elementAsFunctionalComponent(${var_originalComponent});\n`); - } - else { - yield _ts(`const ${var_functionalComponent} = __VLS_asFunctionalComponent(`); - yield _ts(`${var_originalComponent}, `); - yield _ts(`new ${var_originalComponent}({`); - yield* generateProps(node, props, 'extraReferences'); - yield _ts('})'); - yield _ts(');\n'); - } - - for (const offset of tagOffsets) { - if (isNamespacedTag || dynamicTagExp || isIntrinsicElement) { - continue; - } - yield _ts(`({} as { ${getCanonicalComponentName(tag)}: typeof ${var_originalComponent} }).`); - yield* generateCanonicalComponentName( - tag, - offset, - mergeFeatureSettings( - presetInfos.tagHover, - presetInfos.diagnosticOnly, - ), - ); - yield _ts(';\n'); - } - - if (vueCompilerOptions.strictTemplates) { - // with strictTemplates, generate once for props type-checking + instance type - yield _ts(`const ${var_componentInstance} = ${var_functionalComponent}(`); - // diagnostic start - yield _ts( - tagOffsets.length ? ['', 'template', tagOffsets[0], presetInfos.diagnosticOnly] - : dynamicTagExp ? ['', 'template', startTagOffset, presetInfos.diagnosticOnly] - : '' - ); - yield _ts('{ '); - yield* generateProps(node, props, 'normal', propsFailedExps); - yield _ts('}'); - // diagnostic end - yield _ts( - tagOffsets.length ? ['', 'template', tagOffsets[0] + tag.length, presetInfos.diagnosticOnly] - : dynamicTagExp ? ['', 'template', startTagOffset + tag.length, presetInfos.diagnosticOnly] - : '' - ); - yield _ts(`, ...__VLS_functionalComponentArgsRest(${var_functionalComponent}));\n`); - } - else { - // without strictTemplates, this only for instacne type - yield _ts(`const ${var_componentInstance} = ${var_functionalComponent}(`); - yield _ts('{ '); - yield* generateProps(node, props, 'extraReferences'); - yield _ts('}'); - yield _ts(`, ...__VLS_functionalComponentArgsRest(${var_functionalComponent}));\n`); - // and this for props type-checking - yield _ts(`({} as (props: __VLS_FunctionalComponentProps & Record) => void)(`); - // diagnostic start - yield _ts( - tagOffsets.length ? ['', 'template', tagOffsets[0], presetInfos.diagnosticOnly] - : dynamicTagExp ? ['', 'template', startTagOffset, presetInfos.diagnosticOnly] - : '' - ); - yield _ts('{ '); - yield* generateProps(node, props, 'normal', propsFailedExps); - yield _ts('}'); - // diagnostic end - yield _ts( - tagOffsets.length ? ['', 'template', tagOffsets[0] + tag.length, presetInfos.diagnosticOnly] - : dynamicTagExp ? ['', 'template', startTagOffset + tag.length, presetInfos.diagnosticOnly] - : '' - ); - yield _ts(`);\n`); - } - - let defineComponentCtxVar: string | undefined; - - if (tag !== 'template' && tag !== 'slot') { - defineComponentCtxVar = `__VLS_${elementIndex++}`; - componentCtxVar = defineComponentCtxVar; - parentEl = node; - } - - const componentEventsVar = `__VLS_${elementIndex++}`; - - let usedComponentEventsVar = false; - - //#region - // fix https://github.com/vuejs/language-tools/issues/1775 - for (const failedExp of propsFailedExps) { - yield* generateInterpolation( - failedExp.loc.source, - failedExp.loc, - failedExp.loc.start.offset, - presetInfos.all, - '(', - ')', - ); - yield _ts(';\n'); - } - - const vScope = props.find(prop => prop.type === CompilerDOM.NodeTypes.DIRECTIVE && (prop.name === 'scope' || prop.name === 'data')); - let inScope = false; - let originalConditionsNum = blockConditions.length; - - if (vScope?.type === CompilerDOM.NodeTypes.DIRECTIVE && vScope.exp) { - - const scopeVar = `__VLS_${elementIndex++}`; - const condition = `__VLS_withScope(__VLS_ctx, ${scopeVar})`; - - yield _ts(`const ${scopeVar} = `); - yield _ts([ - vScope.exp.loc.source, - 'template', - vScope.exp.loc.start.offset, - presetInfos.all, - ]); - yield _ts(';\n'); - yield _ts(`if (${condition}) {\n`); - blockConditions.push(condition); - inScope = true; - } - - yield* generateDirectives(node); - yield* generateReferencesForElements(node); // - if (shouldGenerateScopedClasses) { - yield* generateReferencesForScopedCssClasses(node); - } - if (componentCtxVar) { - usedComponentCtxVars.add(componentCtxVar); - yield* generateEvents(node, var_functionalComponent, var_componentInstance, componentEventsVar, () => usedComponentEventsVar = true); - } - if (node.tag === 'slot') { - yield* generateSlot(node, startTagOffset); - } - - if (inScope) { - yield _ts('}\n'); - blockConditions.length = originalConditionsNum; - } - //#endregion - - const slotDir = node.props.find(p => p.type === CompilerDOM.NodeTypes.DIRECTIVE && p.name === 'slot') as CompilerDOM.DirectiveNode; - if (slotDir && componentCtxVar) { - usedComponentCtxVars.add(componentCtxVar); - if (parentEl) { - hasSlotElements.add(parentEl); - } - const slotBlockVars: string[] = []; - yield _ts(`{\n`); - let hasProps = false; - if (slotDir?.exp?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION) { - - const slotAst = createTsAst(ts, slotDir, `(${slotDir.exp.content}) => {}`); - collectVars(ts, slotAst, slotAst, slotBlockVars); - hasProps = true; - if (slotDir.exp.content.indexOf(':') === -1) { - yield _ts('const ['); - yield _ts([ - slotDir.exp.content, - 'template', - slotDir.exp.loc.start.offset, - presetInfos.all, - ]); - yield _ts(`] = __VLS_getSlotParams(`); - } - else { - yield _ts('const '); - yield _ts([ - slotDir.exp.content, - 'template', - slotDir.exp.loc.start.offset, - presetInfos.all, - ]); - yield _ts(` = __VLS_getSlotParam(`); - } - } - yield _ts(['', 'template', (slotDir.arg ?? slotDir).loc.start.offset, presetInfos.diagnosticOnly]); - yield _ts(`(${componentCtxVar}.slots!)`); - if (slotDir?.arg?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION && slotDir.arg.content) { - yield* generatePropertyAccess( - slotDir.arg.loc.source, - slotDir.arg.loc.start.offset, - slotDir.arg.isStatic ? presetInfos.slotName : presetInfos.all, - slotDir.arg.loc - ); - } - else { - yield _ts('.'); - yield _ts(['', 'template', slotDir.loc.start.offset, { ...presetInfos.slotName, completion: false }] satisfies Code); - yield _ts('default'); - yield _ts(['', 'template', slotDir.loc.start.offset + (slotDir.loc.source.startsWith('#') ? '#'.length : slotDir.loc.source.startsWith('v-slot:') ? 'v-slot:'.length : 0), disableAllFeatures({ __combineLastMapping: true })] satisfies Code); - } - yield _ts(['', 'template', (slotDir.arg ?? slotDir).loc.end.offset, presetInfos.diagnosticOnly]); - if (hasProps) { - yield _ts(')'); - } - yield _ts(';\n'); - - slotBlockVars.forEach(varName => { - localVars.set(varName, (localVars.get(varName) ?? 0) + 1); - }); - - yield* resetDirectiveComments('end of slot children start'); - - let prev: CompilerDOM.TemplateChildNode | undefined; - for (const childNode of node.children) { - yield* generateAstNode(childNode, parentEl, prev, componentCtxVar); - prev = childNode; - } - - slotBlockVars.forEach(varName => { - localVars.set(varName, localVars.get(varName)! - 1); - }); - let isStatic = true; - if (slotDir?.arg?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION) { - isStatic = slotDir.arg.isStatic; - } - if (isStatic && slotDir && !slotDir.arg) { - yield _ts(`${componentCtxVar}.slots!['`); - yield _ts([ - '', - 'template', - slotDir.loc.start.offset + ( - slotDir.loc.source.startsWith('#') - ? '#'.length : slotDir.loc.source.startsWith('v-slot:') - ? 'v-slot:'.length - : 0 - ), - disableAllFeatures({ completion: true }), - ]); - yield _ts(`'/* empty slot name completion */]\n`); - } - - yield* generateExtraAutoImport(); - yield _ts(`}\n`); - } - else { - yield* resetDirectiveComments('end of element children start'); - let prev: CompilerDOM.TemplateChildNode | undefined; - for (const childNode of node.children) { - yield* generateAstNode(childNode, parentEl, prev, componentCtxVar); - prev = childNode; - } - - // fix https://github.com/vuejs/language-tools/issues/932 - if (!hasSlotElements.has(node) && node.children.length) { - yield _ts(`(${componentCtxVar}.slots!).`); - yield _ts(['', 'template', node.children[0].loc.start.offset, disableAllFeatures({ navigation: true })]); - yield _ts('default'); - yield _ts(['', 'template', node.children[node.children.length - 1].loc.end.offset, disableAllFeatures({ __combineLastMapping: true })]); - yield _ts(';\n'); - } - } - - if (defineComponentCtxVar && usedComponentCtxVars.has(defineComponentCtxVar)) { - yield _ts(`const ${componentCtxVar} = __VLS_pickFunctionalComponentCtx(${var_originalComponent}, ${var_componentInstance})!;\n`); - } - if (usedComponentEventsVar) { - yield _ts(`let ${componentEventsVar}!: __VLS_NormalizeEmits;\n`); - } - - yield _ts(`}\n`); - } - - function* generateEvents(node: CompilerDOM.ElementNode, componentVar: string, componentInstanceVar: string, eventsVar: string, used: () => void): Generator { - - for (const prop of node.props) { - if ( - prop.type === CompilerDOM.NodeTypes.DIRECTIVE - && prop.name === 'on' - && prop.arg?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION - ) { - used(); - const eventVar = `__VLS_${elementIndex++}`; - yield _ts(`let ${eventVar} = { '${prop.arg.loc.source}': `); - yield _ts(`__VLS_pickEvent(`); - yield _ts(`${eventsVar}['${prop.arg.loc.source}'], `); - yield _ts(`({} as __VLS_FunctionalComponentProps)`); - const startCode: Code = [ - '', - 'template', - prop.arg.loc.start.offset, - mergeFeatureSettings( - presetInfos.attrReference, - { - navigation: { - // @click-outside -> onClickOutside - resolveRenameNewName(newName) { - return camelize('on-' + newName); - }, - // onClickOutside -> @click-outside - resolveRenameEditText(newName) { - const hName = hyphenateAttr(newName); - if (hyphenateAttr(newName).startsWith('on-')) { - return camelize(hName.slice('on-'.length)); - } - return newName; - }, - }, - }, - ), - ]; - if (validTsVarReg.test(camelize(prop.arg.loc.source))) { - yield _ts(`.`); - yield _ts(startCode); - yield _ts(`on`); - yield* generateCamelized( - capitalize(prop.arg.loc.source), - prop.arg.loc.start.offset, - disableAllFeatures({ __combineLastMapping: true }), - ); - } - else { - yield _ts(`[`); - yield _ts(startCode); - yield _ts(`'`); - yield _ts(['', 'template', prop.arg.loc.start.offset, disableAllFeatures({ __combineLastMapping: true })]); - yield _ts('on'); - yield* generateCamelized( - capitalize(prop.arg.loc.source), - prop.arg.loc.start.offset, - disableAllFeatures({ __combineLastMapping: true }), - ); - yield _ts(`'`); - yield _ts(['', 'template', prop.arg.loc.end.offset, disableAllFeatures({ __combineLastMapping: true })]); - yield _ts(`]`); - } - yield _ts(`) };\n`); - yield _ts(`${eventVar} = { `); - if (prop.arg.loc.source.startsWith('[') && prop.arg.loc.source.endsWith(']')) { - yield _ts('[('); - yield* generateInterpolation( - prop.arg.loc.source.slice(1, -1), - prop.arg.loc, - prop.arg.loc.start.offset + 1, - presetInfos.all, - '', - '', - ); - yield _ts(')!]'); - } - else { - yield* generateObjectProperty( - prop.arg.loc.source, - prop.arg.loc.start.offset, - presetInfos.event, - prop.arg.loc - ); - } - yield _ts(`: `); - yield* appendExpressionNode(prop); - yield _ts(` };\n`); - } - else if ( - prop.type === CompilerDOM.NodeTypes.DIRECTIVE - && prop.name === 'on' - && prop.exp?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION - ) { - // for vue 2 nameless event - // https://github.com/johnsoncodehk/vue-tsc/issues/67 - yield* generateInterpolation( - prop.exp.content, - prop.exp.loc, - prop.exp.loc.start.offset, - presetInfos.all, - '$event => {(', - ')}', - ); - yield _ts(';\n'); - } - } - } - - function* appendExpressionNode(prop: CompilerDOM.DirectiveNode): Generator { - if (prop.exp?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION) { - let prefix = '('; - let suffix = ')'; - let isFirstMapping = true; - - const ast = createTsAst(ts, prop.exp, prop.exp.content); - const _isCompoundExpression = isCompoundExpression(ts, ast); - if (_isCompoundExpression) { - - yield _ts('$event => {\n'); - localVars.set('$event', (localVars.get('$event') ?? 0) + 1); - - prefix = ''; - suffix = ''; - for (const blockCondition of blockConditions) { - prefix += `if (!(${blockCondition})) return;\n`; - } - } - - yield* generateInterpolation( - prop.exp.content, - prop.exp.loc, - prop.exp.loc.start.offset, - () => { - if (_isCompoundExpression && isFirstMapping) { - isFirstMapping = false; - return presetInfos.allWithHiddenParam; - } - return presetInfos.all; - }, - prefix, - suffix, - ); - - if (_isCompoundExpression) { - localVars.set('$event', localVars.get('$event')! - 1); - - yield _ts(';\n'); - yield* generateExtraAutoImport(); - yield _ts('}\n'); - } - } - else { - yield _ts(`() => {}`); - } - } - - function* generateProps(node: CompilerDOM.ElementNode, props: CompilerDOM.ElementNode['props'], mode: 'normal' | 'extraReferences', propsFailedExps?: CompilerDOM.SimpleExpressionNode[]): Generator { - - let styleAttrNum = 0; - let classAttrNum = 0; - - if (props.some(prop => - prop.type === CompilerDOM.NodeTypes.DIRECTIVE - && prop.name === 'bind' - && !prop.arg - && prop.exp?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION - )) { - // fix https://github.com/vuejs/language-tools/issues/2166 - styleAttrNum++; - classAttrNum++; - } - - let caps_all: VueCodeInformation = presetInfos.all; - let caps_diagnosticOnly: VueCodeInformation = presetInfos.diagnosticOnly; - let caps_attr: VueCodeInformation = presetInfos.attr; - - if (mode === 'extraReferences') { - caps_all = disableAllFeatures({ navigation: caps_all.navigation }); - caps_diagnosticOnly = disableAllFeatures({ navigation: caps_diagnosticOnly.navigation }); - caps_attr = disableAllFeatures({ navigation: caps_attr.navigation }); - } - - yield _ts(`...{ `); - for (const prop of props) { - if ( - prop.type === CompilerDOM.NodeTypes.DIRECTIVE - && prop.name === 'on' - && prop.arg?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION - ) { - yield _ts(`'${camelize('on-' + prop.arg.loc.source)}': {} as any, `); - } - } - yield _ts(`}, `); - - const canCamelize = !nativeTags.has(node.tag) || node.tagType === CompilerDOM.ElementTypes.COMPONENT; - - for (const prop of props) { - if ( - prop.type === CompilerDOM.NodeTypes.DIRECTIVE - && (prop.name === 'bind' || prop.name === 'model') - && (prop.name === 'model' || prop.arg?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION) - && (!prop.exp || prop.exp.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION) - ) { - - let propName = - prop.arg?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION - ? prop.arg.constType === CompilerDOM.ConstantTypes.CAN_STRINGIFY - ? prop.arg.content - : prop.arg.loc.source - : getModelValuePropName(node, vueCompilerOptions.target, vueCompilerOptions); - - if (prop.modifiers.some(m => m === 'prop' || m === 'attr')) { - propName = propName?.substring(1); - } - - if ( - propName === undefined - || vueCompilerOptions.dataAttributes.some(pattern => minimatch(propName!, pattern)) - || (propName === 'style' && ++styleAttrNum >= 2) - || (propName === 'class' && ++classAttrNum >= 2) - || (propName === 'name' && node.tag === 'slot') // #2308 - ) { - if (prop.exp && prop.exp.constType !== CompilerDOM.ConstantTypes.CAN_STRINGIFY) { - propsFailedExps?.push(prop.exp); - } - continue; - } - - const shouldCamelize = canCamelize - && (!prop.arg || (prop.arg.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION && prop.arg.isStatic)) // isStatic - && hyphenateAttr(propName) === propName - && !vueCompilerOptions.htmlAttributes.some(pattern => minimatch(propName!, pattern)); - - yield _ts(['', 'template', prop.loc.start.offset, caps_diagnosticOnly]); - yield* generateObjectProperty( - propName, - prop.arg - ? prop.arg.loc.start.offset - : prop.loc.start.offset, - prop.arg - ? mergeFeatureSettings( - caps_attr, - { - navigation: caps_attr.navigation ? { - resolveRenameNewName: camelize, - resolveRenameEditText: shouldCamelize ? hyphenateAttr : undefined, - } : undefined, - }, - ) - : caps_attr, - (prop.loc as any).name_2 ?? ((prop.loc as any).name_2 = {}), - shouldCamelize, - ); - yield _ts(': ('); - if (prop.exp && prop.exp.constType !== CompilerDOM.ConstantTypes.CAN_STRINGIFY) { // style='z-index: 2' will compile to {'z-index':'2'} - const isShorthand = prop.arg?.loc.start.offset === prop.exp?.loc.start.offset; // vue 3.4+ - if (!isShorthand) { - yield* generateInterpolation( - prop.exp.loc.source, - prop.exp.loc, - prop.exp.loc.start.offset, - caps_all, - '(', - ')', - ); - } else { - const propVariableName = camelize(prop.exp.loc.source); - - if (validTsVarReg.test(propVariableName)) { - if (!localVars.has(propVariableName)) { - accessedGlobalVariables.add(propVariableName); - yield _ts('__VLS_ctx.'); - } - yield* generateCamelized( - prop.exp.loc.source, - prop.exp.loc.start.offset, - caps_all, - ); - if (mode === 'normal') { - yield _ts([ - '', - 'template', - prop.exp.loc.end.offset, - disableAllFeatures({ - __hint: { - setting: 'vue.inlayHints.vBindShorthand', - label: `="${propVariableName}"`, - tooltip: [ - `This is a shorthand for \`${prop.exp.loc.source}="${propVariableName}"\`.`, - 'To hide this hint, set `vue.inlayHints.vBindShorthand` to `false` in IDE settings.', - '[More info](https://github.com/vuejs/core/pull/9451)', - ].join('\n\n'), - }, - }) - ]); - } - } - } - } - else { - yield _ts('{}'); - } - yield _ts(')'); - yield _ts([ - '', - 'template', - prop.loc.end.offset, - caps_diagnosticOnly, - ]); - yield _ts(', '); - } - else if (prop.type === CompilerDOM.NodeTypes.ATTRIBUTE) { - - if ( - vueCompilerOptions.dataAttributes.some(pattern => minimatch(prop.name, pattern)) - || (prop.name === 'style' && ++styleAttrNum >= 2) - || (prop.name === 'class' && ++classAttrNum >= 2) - || (prop.name === 'name' && node.tag === 'slot') // #2308 - ) { - continue; - } - - if ( - vueCompilerOptions.target < 3 - && (node.tag === 'transition' || node.tag === 'Transition') - && prop.name === 'persisted' - ) { - // Vue 2 Transition doesn't support "persisted" property but `@vue/compiler-dom always adds it (#3881) - continue; - } - - const shouldCamelize = canCamelize - && hyphenateAttr(prop.name) === prop.name - && !vueCompilerOptions.htmlAttributes.some(pattern => minimatch(prop.name, pattern)); - - yield _ts(['', 'template', prop.loc.start.offset, caps_diagnosticOnly]); - yield* generateObjectProperty( - prop.name, - prop.loc.start.offset, - shouldCamelize - ? mergeFeatureSettings(caps_attr, { - navigation: caps_attr.navigation ? { - resolveRenameNewName: camelize, - resolveRenameEditText: hyphenateAttr, - } : undefined, - }) - : caps_attr, - (prop.loc as any).name_1 ?? ((prop.loc as any).name_1 = {}), - shouldCamelize, - ); - yield _ts(': ('); - if (prop.value) { - yield* generateAttrValue(prop.value, caps_all); - } - else { - yield _ts('true'); - } - yield _ts(')'); - yield _ts(['', 'template', prop.loc.end.offset, caps_diagnosticOnly]); - yield _ts(', '); - } - else if ( - prop.type === CompilerDOM.NodeTypes.DIRECTIVE - && prop.name === 'bind' - && !prop.arg - && prop.exp?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION - ) { - yield _ts(['', 'template', prop.exp.loc.start.offset, presetInfos.diagnosticOnly]); - yield _ts('...'); - yield* generateInterpolation( - prop.exp.content, - prop.exp.loc, - prop.exp.loc.start.offset, - caps_all, - '(', - ')', - ); - yield _ts(['', 'template', prop.exp.loc.end.offset, presetInfos.diagnosticOnly]); - yield _ts(', '); - } - else { - // comment this line to avoid affecting comments in prop expressions - // tsCodeGen.addText("/* " + [prop.type, prop.name, prop.arg?.loc.source, prop.exp?.loc.source, prop.loc.source].join(", ") + " */ "); - } - } - } - - function* generateDirectives(node: CompilerDOM.ElementNode): Generator { - for (const prop of node.props) { - if ( - prop.type === CompilerDOM.NodeTypes.DIRECTIVE - && prop.name !== 'slot' - && prop.name !== 'on' - && prop.name !== 'model' - && prop.name !== 'bind' - && (prop.name !== 'scope' && prop.name !== 'data') - ) { - - accessedGlobalVariables.add(camelize('v-' + prop.name)); - - if (prop.arg?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION && !prop.arg.isStatic) { - yield* generateInterpolation( - prop.arg.content, - prop.arg.loc, - prop.arg.loc.start.offset + prop.arg.loc.source.indexOf(prop.arg.content), - presetInfos.all, - '(', - ')', - ); - yield _ts(';\n'); - } - - yield _ts(['', 'template', prop.loc.start.offset, presetInfos.diagnosticOnly]); - yield _ts(`__VLS_directiveFunction(__VLS_ctx.`); - yield* generateCamelized( - 'v-' + prop.name, - prop.loc.start.offset, - mergeFeatureSettings( - presetInfos.noDiagnostics, - { - completion: { - // fix https://github.com/vuejs/language-tools/issues/1905 - isAdditional: true, - }, - navigation: { - resolveRenameNewName: camelize, - resolveRenameEditText: getPropRenameApply(prop.name), - }, - }, - ), - ); - yield _ts(')'); - yield _ts('('); - - if (prop.exp?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION) { - yield _ts(['', 'template', prop.exp.loc.start.offset, presetInfos.diagnosticOnly]); - yield* generateInterpolation( - prop.exp.content, - prop.exp.loc, - prop.exp.loc.start.offset, - presetInfos.all, - '(', - ')', - ); - yield _ts(['', 'template', prop.exp.loc.end.offset, presetInfos.diagnosticOnly]); - } - else { - yield _ts('undefined'); - } - yield _ts(')'); - yield _ts(['', 'template', prop.loc.end.offset, presetInfos.diagnosticOnly]); - yield _ts(';\n'); - } - } - } - - function* generateReferencesForElements(node: CompilerDOM.ElementNode): Generator { - for (const prop of node.props) { - if ( - prop.type === CompilerDOM.NodeTypes.ATTRIBUTE - && prop.name === 'ref' - && prop.value - ) { - yield _ts('// @ts-ignore\n'); - yield* generateInterpolation( - prop.value.content, - prop.value.loc, - prop.value.loc.start.offset + 1, - presetInfos.refAttr, - '(', - ')', - ); - yield _ts(';\n'); - } - } - } - - function* generateReferencesForScopedCssClasses(node: CompilerDOM.ElementNode): Generator { - for (const prop of node.props) { - if ( - prop.type === CompilerDOM.NodeTypes.ATTRIBUTE - && prop.name === 'class' - && prop.value - ) { - let startOffset = prop.value.loc.start.offset; - let tempClassName = ''; - for (const char of (prop.value.loc.source + ' ')) { - if (char.trim() === '' || char === '"' || char === "'") { - if (tempClassName !== '') { - scopedClasses.push({ className: tempClassName, offset: startOffset }); - startOffset += tempClassName.length; - tempClassName = ''; - } - startOffset += char.length; - } - else { - tempClassName += char; - } - } - } - else if ( - prop.type === CompilerDOM.NodeTypes.DIRECTIVE - && prop.arg?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION - && prop.exp?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION - && prop.arg.content === 'class' - ) { - yield _ts(`__VLS_styleScopedClasses = (`); - yield _ts([ - prop.exp.content, - 'template', - prop.exp.loc.start.offset, - presetInfos.scopedClassName, - ]); - yield _ts(`);\n`); - } - } - } - - function* generateSlot(node: CompilerDOM.ElementNode, startTagOffset: number): Generator { - - const varSlot = `__VLS_${elementIndex++}`; - const slotNameExpNode = getSlotNameExpNode(); - - if (hasScriptSetupSlots) { - yield _ts('__VLS_normalizeSlot('); - yield _ts(['', 'template', node.loc.start.offset, presetInfos.diagnosticOnly]); - yield _ts(`${slotsAssignName ?? '__VLS_slots'}[`); - yield _ts(['', 'template', node.loc.start.offset, disableAllFeatures({ __combineLastMapping: true })]); - yield _ts(slotNameExpNode?.content ?? `('${getSlotName()?.[0] ?? 'default'}' as const)`); - yield _ts(['', 'template', node.loc.end.offset, disableAllFeatures({ __combineLastMapping: true })]); - yield _ts(']'); - yield _ts(['', 'template', node.loc.end.offset, disableAllFeatures({ __combineLastMapping: true })]); - yield _ts(')?.('); - yield _ts(['', 'template', startTagOffset, disableAllFeatures({ __combineLastMapping: true })]); - yield _ts('{\n'); - } - else { - yield _ts(`var ${varSlot} = {\n`); - } - for (const prop of node.props) { - if ( - prop.type === CompilerDOM.NodeTypes.DIRECTIVE - && !prop.arg - && prop.exp?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION - ) { - yield _ts('...'); - yield* generateInterpolation( - prop.exp.content, - prop.exp.loc, - prop.exp.loc.start.offset, - presetInfos.attrReference, - '(', - ')', - ); - yield _ts(',\n'); - } - else if ( - prop.type === CompilerDOM.NodeTypes.DIRECTIVE - && prop.arg?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION - && prop.exp?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION - && prop.arg.content !== 'name' - ) { - yield* generateObjectProperty( - prop.arg.content, - prop.arg.loc.start.offset, - mergeFeatureSettings( - presetInfos.slotProp, - { - navigation: { - resolveRenameNewName: camelize, - resolveRenameEditText: getPropRenameApply(prop.arg.content), - }, - }, - ), - prop.arg.loc - ); - yield _ts(': '); - yield* generateInterpolation( - prop.exp.content, - prop.exp.loc, - prop.exp.loc.start.offset, - presetInfos.attrReference, - '(', - ')', - ); - yield _ts(',\n'); - } - else if ( - prop.type === CompilerDOM.NodeTypes.ATTRIBUTE - && prop.name !== 'name' // slot name - ) { - yield* generateObjectProperty( - prop.name, - prop.loc.start.offset, - mergeFeatureSettings( - presetInfos.attr, - { - navigation: { - resolveRenameNewName: camelize, - resolveRenameEditText: getPropRenameApply(prop.name), - }, - }, - ), - prop.loc - ); - yield _ts(': ('); - yield _ts( - prop.value !== undefined - ? `"${needToUnicode(prop.value.content) ? toUnicode(prop.value.content) : prop.value.content}"` - : 'true' - ); - yield _ts('),\n'); - } - } - yield _ts('}'); - if (hasScriptSetupSlots) { - yield _ts(['', 'template', startTagOffset + node.tag.length, presetInfos.diagnosticOnly]); - yield _ts(`)`); - } - yield _ts(`;\n`); - - if (hasScriptSetupSlots) { - return; - } - - if (slotNameExpNode) { - const varSlotExp = `__VLS_${elementIndex++}`; - yield _ts(`var ${varSlotExp} = `); - if (typeof slotNameExpNode === 'string') { - yield _ts(slotNameExpNode); - } - else { - yield* generateInterpolation( - slotNameExpNode.content, - slotNameExpNode, - undefined, undefined, - '(', - ')', - ); - } - yield _ts(` as const;\n`); - slotExps.set(varSlotExp, { - varName: varSlot, - }); - } - else { - const slotName = getSlotName(); - slots.set(slotName?.[0] ?? 'default', { - name: slotName?.[0], - loc: slotName?.[1], - tagRange: [startTagOffset, startTagOffset + node.tag.length], - varName: varSlot, - nodeLoc: node.loc, - }); - } - - function getSlotName() { - for (const prop2 of node.props) { - if (prop2.name === 'name' && prop2.type === CompilerDOM.NodeTypes.ATTRIBUTE && prop2.value) { - if (prop2.value.content) { - return [ - prop2.value.content, - prop2.loc.start.offset + prop2.loc.source.indexOf(prop2.value.content, prop2.name.length), - ] as const; - } - } - } - } - function getSlotNameExpNode() { - for (const prop2 of node.props) { - if (prop2.type === CompilerDOM.NodeTypes.DIRECTIVE && prop2.name === 'bind' && prop2.arg?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION && prop2.arg.content === 'name') { - if (prop2.exp?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION) { - return prop2.exp; - } - } - } - } - } - - function* generateExtraAutoImport(): Generator { - - if (!tempVars.length) { - return; - } - - yield _ts('// @ts-ignore\n'); // #2304 - yield _ts('['); - const visited = new Set(); - for (const _vars of tempVars) { - for (const v of _vars) { - if (visited.has(v.offset)) { - continue; - } - visited.add(v.offset); - yield _ts([ - v.text, - 'template', - v.offset, - disableAllFeatures({ completion: { isAdditional: true }, }), - ]); - yield _ts(','); - } - } - yield _ts('];\n'); - tempVars.length = 0; - } - - function* generateAttrValue(attrNode: CompilerDOM.TextNode, info: VueCodeInformation): Generator { - const char = attrNode.loc.source.startsWith("'") ? "'" : '"'; - yield _ts(char); - let start = attrNode.loc.start.offset; - let end = attrNode.loc.end.offset; - let content = attrNode.loc.source; - if ( - (content.startsWith('"') && content.endsWith('"')) - || (content.startsWith("'") && content.endsWith("'")) - ) { - start++; - end--; - content = content.slice(1, -1); - } - if (needToUnicode(content)) { - yield _ts(['', 'template', start, info]); - yield _ts(toUnicode(content)); - yield _ts(['', 'template', end, disableAllFeatures({ __combineLastMapping: true })]); - } - else { - yield _ts([content, 'template', start, info]); - } - yield _ts(char); - } - - function* generateCamelized(code: string, offset: number, info: VueCodeInformation): Generator { - const parts = code.split('-'); - for (let i = 0; i < parts.length; i++) { - const part = parts[i]; - if (part !== '') { - yield _ts([ - i === 0 - ? part - : capitalize(part), - 'template', - offset, - i === 0 - ? info - : disableAllFeatures({ __combineLastMapping: true }), - ]); - } - offset += part.length + 1; - } - } - - function* generateObjectProperty(code: string, offset: number, info: VueCodeInformation, astHolder?: any, shouldCamelize = false): Generator { - if (code.startsWith('[') && code.endsWith(']') && astHolder) { - yield* generateInterpolation(code, astHolder, offset, info, '', ''); - } - else if (shouldCamelize) { - if (validTsVarReg.test(camelize(code))) { - yield* generateCamelized(code, offset, info); - } - else { - yield _ts(['', 'template', offset, info]); - yield _ts('"'); - yield* generateCamelized(code, offset, disableAllFeatures({ __combineLastMapping: true })); - yield _ts('"'); - yield _ts(['', 'template', offset + code.length, disableAllFeatures({ __combineLastMapping: true })]); - } - } - else { - if (validTsVarReg.test(code)) { - yield _ts([code, 'template', offset, info]); - } - else { - yield* generateStringLiteralKey(code, offset, info); - } - } - } - - function* generateInterpolation( - _code: string, - astHolder: any, - start: number | undefined, - data: VueCodeInformation | (() => VueCodeInformation) | undefined, - prefix: string, - suffix: string, - ): Generator { - const code = prefix + _code + suffix; - const ast = createTsAst(ts, astHolder, code); - const vars: { - text: string, - isShorthand: boolean, - offset: number, - }[] = []; - for (let [section, offset, onlyError] of eachInterpolationSegment( - ts, - code, - ast, - localVars, - accessedGlobalVariables, - vueCompilerOptions, - vars, - )) { - if (offset === undefined) { - yield _ts(section); - } - else { - offset -= prefix.length; - let addSuffix = ''; - const overLength = offset + section.length - _code.length; - if (overLength > 0) { - addSuffix = section.substring(section.length - overLength); - section = section.substring(0, section.length - overLength); - } - if (offset < 0) { - yield _ts(section.substring(0, -offset)); - section = section.substring(-offset); - offset = 0; - } - if (start !== undefined && data !== undefined) { - yield _ts([ - section, - 'template', - start + offset, - onlyError - ? presetInfos.diagnosticOnly - : typeof data === 'function' ? data() : data, - ]); - } - else { - yield _ts(section); - } - yield _ts(addSuffix); - } - } - if (start !== undefined) { - for (const v of vars) { - v.offset = start + v.offset - prefix.length; - } - if (vars.length) { - tempVars.push(vars); - } - } - } - - function* generatePropertyAccess(code: string, offset?: number, info?: VueCodeInformation, astHolder?: any): Generator { - if (!compilerOptions.noPropertyAccessFromIndexSignature && validTsVarReg.test(code)) { - yield _ts('.'); - yield _ts(offset !== undefined && info - ? [code, 'template', offset, info] - : code); - } - else if (code.startsWith('[') && code.endsWith(']')) { - yield* generateInterpolation(code, astHolder, offset, info, '', ''); - } - else { - yield _ts('['); - yield* generateStringLiteralKey(code, offset, info); - yield _ts(']'); - } - } - - function* generateStringLiteralKey(code: string, offset?: number, info?: VueCodeInformation): Generator { - if (offset === undefined || !info) { - yield _ts(`"${code}"`); - } - else { - yield _ts(['', 'template', offset, info]); - yield _ts('"'); - yield _ts([code, 'template', offset, disableAllFeatures({ __combineLastMapping: true })]); - yield _ts('"'); - yield _ts(['', 'template', offset + code.length, disableAllFeatures({ __combineLastMapping: true })]); - } - } -} - -function isFragment(node: CompilerDOM.IfNode | CompilerDOM.ForNode) { - return node.codegenNode && 'consequent' in node.codegenNode && 'tag' in node.codegenNode.consequent && node.codegenNode.consequent.tag === CompilerDOM.FRAGMENT; -} - -export function createTsAst(ts: typeof import('typescript'), astHolder: any, text: string) { - if (astHolder.__volar_ast_text !== text) { - astHolder.__volar_ast_text = text; - astHolder.__volar_ast = ts.createSourceFile('/a.ts', text, 99 satisfies ts.ScriptTarget.ESNext); - } - return astHolder.__volar_ast as ts.SourceFile; -} - -export function isCompoundExpression(ts: typeof import('typescript'), ast: ts.SourceFile,) { - let result = true; - if (ast.statements.length === 1) { - ts.forEachChild(ast, child_1 => { - if (ts.isExpressionStatement(child_1)) { - ts.forEachChild(child_1, child_2 => { - if (ts.isArrowFunction(child_2)) { - result = false; - } - else if (ts.isIdentifier(child_2)) { - result = false; - } - }); - } - else if (ts.isFunctionDeclaration(child_1)) { - result = false; - } - }); - } - return result; -} - -export function parseInterpolationNode(node: CompilerDOM.InterpolationNode, template: string) { - let content = node.content.loc.source; - let start = node.content.loc.start.offset; - let leftCharacter: string; - let rightCharacter: string; - - // fix https://github.com/vuejs/language-tools/issues/1787 - while ((leftCharacter = template.substring(start - 1, start)).trim() === '' && leftCharacter.length) { - start--; - content = leftCharacter + content; - } - while ((rightCharacter = template.substring(start + content.length, start + content.length + 1)).trim() === '' && rightCharacter.length) { - content = content + rightCharacter; - } - - return [ - content, - start, - ] as const; -} - -export function parseVForNode(node: CompilerDOM.ForNode) { - const { value, key, index } = node.parseResult; - const leftExpressionRange = (value || key || index) - ? { - start: (value ?? key ?? index)!.loc.start.offset, - end: (index ?? key ?? value)!.loc.end.offset, - } - : undefined; - const leftExpressionText = leftExpressionRange - ? node.loc.source.substring( - leftExpressionRange.start - node.loc.start.offset, - leftExpressionRange.end - node.loc.start.offset - ) - : undefined; - return { - leftExpressionRange, - leftExpressionText, - }; -} - -function getCanonicalComponentName(tagText: string) { - return validTsVarReg.test(tagText) - ? tagText - : capitalize(camelize(tagText.replace(colonReg, '-'))); -} - -function getPossibleOriginalComponentNames(tagText: string) { - return [...new Set([ - // order is important: https://github.com/vuejs/language-tools/issues/2010 - capitalize(camelize(tagText)), - camelize(tagText), - tagText, - ])]; -} - -export function* forEachElementNode(node: CompilerDOM.RootNode | CompilerDOM.TemplateChildNode): Generator { - if (node.type === CompilerDOM.NodeTypes.ROOT) { - for (const child of node.children) { - yield* forEachElementNode(child); - } - } - else if (node.type === CompilerDOM.NodeTypes.ELEMENT) { - const patchForNode = getVForNode(node); - if (patchForNode) { - yield* forEachElementNode(patchForNode); - } - else { - yield node; - for (const child of node.children) { - yield* forEachElementNode(child); - } - } - } - else if (node.type === CompilerDOM.NodeTypes.IF) { - // v-if / v-else-if / v-else - for (let i = 0; i < node.branches.length; i++) { - const branch = node.branches[i]; - for (const childNode of branch.children) { - yield* forEachElementNode(childNode); - } - } - } - else if (node.type === CompilerDOM.NodeTypes.FOR) { - // v-for - for (const child of node.children) { - yield* forEachElementNode(child); - } - } -} - -function needToUnicode(str: string) { - return str.indexOf('\\') >= 0 || str.indexOf('\n') >= 0; -} - -function toUnicode(str: string) { - return str.split('').map(value => { - const temp = value.charCodeAt(0).toString(16).padStart(4, '0'); - if (temp.length > 2) { - return '\\u' + temp; - } - return value; - }).join(''); -} - -function camelizeComponentName(newName: string) { - return camelize('-' + newName); -} - -function getTagRenameApply(oldName: string) { - return oldName === hyphenateTag(oldName) ? hyphenateTag : undefined; -} - -function getPropRenameApply(oldName: string) { - return oldName === hyphenateAttr(oldName) ? hyphenateAttr : undefined; -} - -function getModelValuePropName(node: CompilerDOM.ElementNode, vueVersion: number, vueCompilerOptions: VueCompilerOptions) { - - for (const modelName in vueCompilerOptions.experimentalModelPropName) { - const tags = vueCompilerOptions.experimentalModelPropName[modelName]; - for (const tag in tags) { - if (node.tag === tag || node.tag === hyphenateTag(tag)) { - const v = tags[tag]; - if (typeof v === 'object') { - const arr = Array.isArray(v) ? v : [v]; - for (const attrs of arr) { - let failed = false; - for (const attr in attrs) { - const attrNode = node.props.find(prop => prop.type === CompilerDOM.NodeTypes.ATTRIBUTE && prop.name === attr) as CompilerDOM.AttributeNode | undefined; - if (!attrNode || attrNode.value?.content !== attrs[attr]) { - failed = true; - break; - } - } - if (!failed) { - // all match - return modelName || undefined; - } - } - } - } - } - } - - for (const modelName in vueCompilerOptions.experimentalModelPropName) { - const tags = vueCompilerOptions.experimentalModelPropName[modelName]; - for (const tag in tags) { - if (node.tag === tag || node.tag === hyphenateTag(tag)) { - const attrs = tags[tag]; - if (attrs === true) { - return modelName || undefined; - } - } - } - } - - return vueVersion < 3 ? 'value' : 'modelValue'; -} - -// TODO: track https://github.com/vuejs/vue-next/issues/3498 -function getVForNode(node: CompilerDOM.ElementNode) { - const forDirective = node.props.find( - (prop): prop is CompilerDOM.DirectiveNode => - prop.type === CompilerDOM.NodeTypes.DIRECTIVE - && prop.name === 'for' - ); - if (forDirective) { - let forNode: CompilerDOM.ForNode | undefined; - CompilerDOM.processFor(node, forDirective, transformContext, _forNode => { - forNode = { ..._forNode }; - return undefined; - }); - if (forNode) { - forNode.children = [{ - ...node, - props: node.props.filter(prop => prop !== forDirective), - }]; - return forNode; - } - } -} - -function getVIfNode(node: CompilerDOM.ElementNode) { - const forDirective = node.props.find( - (prop): prop is CompilerDOM.DirectiveNode => - prop.type === CompilerDOM.NodeTypes.DIRECTIVE - && prop.name === 'if' - ); - if (forDirective) { - let ifNode: CompilerDOM.IfNode | undefined; - CompilerDOM.processIf(node, forDirective, transformContext, _ifNode => { - ifNode = { ..._ifNode }; - return undefined; - }); - if (ifNode) { - for (const branch of ifNode.branches) { - branch.children = [{ - ...node, - props: node.props.filter(prop => prop !== forDirective), - }]; - } - return ifNode; - } - } -} diff --git a/packages/language-core/lib/generators/utils.ts b/packages/language-core/lib/generators/utils.ts deleted file mode 100644 index 1ce49274bc..0000000000 --- a/packages/language-core/lib/generators/utils.ts +++ /dev/null @@ -1,51 +0,0 @@ -import type { VueCodeInformation } from '../types'; - -// TODO: import from muggle-string -export function getStack() { - const stack = new Error().stack!; - let source = stack.split('\n')[3].trim(); - if (source.endsWith(')')) { - source = source.slice(source.lastIndexOf('(') + 1, -1); - } - else { - source = source.slice(source.lastIndexOf(' ') + 1); - } - return source; -} - -export function disableAllFeatures(override: Partial): VueCodeInformation { - return { - verification: false, - completion: false, - semantic: false, - navigation: false, - structure: false, - format: false, - ...override, - }; -} - -export function enableAllFeatures(override: Partial): VueCodeInformation { - return { - verification: true, - completion: true, - semantic: true, - navigation: true, - structure: true, - format: true, - ...override, - }; -} - -export function mergeFeatureSettings(base: VueCodeInformation, ...others: Partial[]): VueCodeInformation { - const result: VueCodeInformation = { ...base }; - for (const info of others) { - for (const key in info) { - const value = info[key as keyof VueCodeInformation]; - if (value) { - result[key as keyof VueCodeInformation] = value as any; - } - } - } - return result; -} diff --git a/packages/language-core/lib/languageModule.ts b/packages/language-core/lib/languageModule.ts index 058642414a..bedeab4a68 100644 --- a/packages/language-core/lib/languageModule.ts +++ b/packages/language-core/lib/languageModule.ts @@ -2,14 +2,14 @@ import { forEachEmbeddedCode, type LanguagePlugin } from '@volar/language-core'; import type * as ts from 'typescript'; import { getDefaultVueLanguagePlugins } from './plugins'; import type { VueCompilerOptions, VueLanguagePlugin } from './types'; -import { VueGeneratedCode } from './virtualFile/vueFile'; +import { VueVirtualCode } from './virtualFile/vueFile'; import * as CompilerDOM from '@vue/compiler-dom'; import * as CompilerVue2 from './utils/vue2TemplateCompiler'; const normalFileRegistries: { key: string; plugins: VueLanguagePlugin[]; - files: Map; + files: Map; }[] = []; const holderFileRegistries: typeof normalFileRegistries = []; @@ -48,7 +48,7 @@ function getFileRegistryKey( return JSON.stringify(values); } -interface _Plugin extends LanguagePlugin { +interface _Plugin extends LanguagePlugin { getCanonicalFileName: (fileName: string) => string; pluginContext: Parameters[0]; } @@ -61,7 +61,6 @@ export function createVueLanguagePlugin( getScriptFileNames: () => string[] | Set, compilerOptions: ts.CompilerOptions, vueCompilerOptions: VueCompilerOptions, - codegenStack: boolean = false, ): _Plugin { const allowLanguageIds = new Set(['vue']); const pluginContext: Parameters[0] = { @@ -76,7 +75,6 @@ export function createVueLanguagePlugin( }, compilerOptions, vueCompilerOptions, - codegenStack, globalTypesHolder: undefined, }; const plugins = getDefaultVueLanguagePlugins(pluginContext); @@ -115,14 +113,13 @@ export function createVueLanguagePlugin( return code; } else { - const code = new VueGeneratedCode( + const code = new VueVirtualCode( fileName, languageId, snapshot, vueCompilerOptions, plugins, ts, - codegenStack, ); fileRegistry.set(fileId, code); return code; diff --git a/packages/language-core/lib/plugins/shared.ts b/packages/language-core/lib/plugins/shared.ts new file mode 100644 index 0000000000..90855a5a97 --- /dev/null +++ b/packages/language-core/lib/plugins/shared.ts @@ -0,0 +1,10 @@ +import type { CodeInformation } from '@volar/language-core'; + +export const allCodeFeatures: CodeInformation = { + verification: true, + completion: true, + semantic: true, + navigation: true, + structure: true, + format: true, +}; diff --git a/packages/language-core/lib/plugins/vue-sfc-customblocks.ts b/packages/language-core/lib/plugins/vue-sfc-customblocks.ts index de08db5b78..738ae30cc3 100644 --- a/packages/language-core/lib/plugins/vue-sfc-customblocks.ts +++ b/packages/language-core/lib/plugins/vue-sfc-customblocks.ts @@ -1,5 +1,5 @@ -import { enableAllFeatures } from '../generators/utils'; import type { VueLanguagePlugin } from '../types'; +import { allCodeFeatures } from './shared'; const plugin: VueLanguagePlugin = () => { @@ -23,7 +23,7 @@ const plugin: VueLanguagePlugin = () => { customBlock.content, customBlock.name, 0, - enableAllFeatures({}), + allCodeFeatures, ]); } }, diff --git a/packages/language-core/lib/plugins/vue-sfc-scripts.ts b/packages/language-core/lib/plugins/vue-sfc-scripts.ts index 655bffa8b7..4a32ce1040 100644 --- a/packages/language-core/lib/plugins/vue-sfc-scripts.ts +++ b/packages/language-core/lib/plugins/vue-sfc-scripts.ts @@ -1,4 +1,3 @@ -import { disableAllFeatures } from '../generators/utils'; import type { VueLanguagePlugin } from '../types'; const plugin: VueLanguagePlugin = () => { @@ -30,10 +29,10 @@ const plugin: VueLanguagePlugin = () => { script.content, script.name, 0, - disableAllFeatures({ + { structure: true, format: true, - }), + }, ]); } }, diff --git a/packages/language-core/lib/plugins/vue-sfc-styles.ts b/packages/language-core/lib/plugins/vue-sfc-styles.ts index ac76f9d05f..44d3979676 100644 --- a/packages/language-core/lib/plugins/vue-sfc-styles.ts +++ b/packages/language-core/lib/plugins/vue-sfc-styles.ts @@ -1,5 +1,5 @@ -import { enableAllFeatures } from '../generators/utils'; import type { VueLanguagePlugin } from '../types'; +import { allCodeFeatures } from './shared'; const plugin: VueLanguagePlugin = () => { @@ -43,7 +43,7 @@ const plugin: VueLanguagePlugin = () => { cssVar.text, style.name, cssVar.offset, - enableAllFeatures({}), + allCodeFeatures, ], ');\n', ); @@ -54,7 +54,7 @@ const plugin: VueLanguagePlugin = () => { style.content, style.name, 0, - enableAllFeatures({}), + allCodeFeatures, ]); } } diff --git a/packages/language-core/lib/plugins/vue-sfc-template.ts b/packages/language-core/lib/plugins/vue-sfc-template.ts index 3686f6fa34..3350e9b7a5 100644 --- a/packages/language-core/lib/plugins/vue-sfc-template.ts +++ b/packages/language-core/lib/plugins/vue-sfc-template.ts @@ -1,5 +1,5 @@ -import { enableAllFeatures } from '../generators/utils'; import type { VueLanguagePlugin } from '../types'; +import { allCodeFeatures } from './shared'; const plugin: VueLanguagePlugin = () => { @@ -23,7 +23,7 @@ const plugin: VueLanguagePlugin = () => { sfc.template.content, sfc.template.name, 0, - enableAllFeatures({}), + allCodeFeatures, ]); } }, diff --git a/packages/language-core/lib/plugins/vue-template-inline-css.ts b/packages/language-core/lib/plugins/vue-template-inline-css.ts index 58f31a1e8a..f8d147720b 100644 --- a/packages/language-core/lib/plugins/vue-template-inline-css.ts +++ b/packages/language-core/lib/plugins/vue-template-inline-css.ts @@ -1,5 +1,13 @@ -import { generate as generateInlineCss } from '../generators/inlineCss'; -import type { VueLanguagePlugin } from '../types'; +import * as CompilerDOM from '@vue/compiler-dom'; +import { forEachElementNode } from '../codegen/template'; +import type { Code, VueLanguagePlugin } from '../types'; +import { allCodeFeatures } from './shared'; + +const codeFeatures = { + ...allCodeFeatures, + format: false, + structure: false, +}; const plugin: VueLanguagePlugin = () => { @@ -19,9 +27,38 @@ const plugin: VueLanguagePlugin = () => { return; } embeddedFile.parentCodeId = 'template'; - embeddedFile.content.push(...generateInlineCss(sfc.template.ast)); + embeddedFile.content.push(...generate(sfc.template.ast)); }, }; }; export default plugin; + +function* generate(templateAst: NonNullable): Generator { + for (const node of forEachElementNode(templateAst)) { + for (const prop of node.props) { + if ( + prop.type === CompilerDOM.NodeTypes.DIRECTIVE + && prop.name === 'bind' + && prop.arg?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION + && prop.exp?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION + && prop.arg.content === 'style' + && prop.exp.constType === CompilerDOM.ConstantTypes.CAN_STRINGIFY + ) { + const endCrt = prop.arg.loc.source[prop.arg.loc.source.length - 1]; // " | ' + const start = prop.arg.loc.source.indexOf(endCrt) + 1; + const end = prop.arg.loc.source.lastIndexOf(endCrt); + const content = prop.arg.loc.source.substring(start, end); + + yield `x { `; + yield [ + content, + 'template', + prop.arg.loc.start.offset + start, + codeFeatures, + ]; + yield ` }\n`; + } + } + } +} diff --git a/packages/language-core/lib/plugins/vue-template-inline-ts.ts b/packages/language-core/lib/plugins/vue-template-inline-ts.ts index 6dd91652f1..0501f8884e 100644 --- a/packages/language-core/lib/plugins/vue-template-inline-ts.ts +++ b/packages/language-core/lib/plugins/vue-template-inline-ts.ts @@ -1,12 +1,15 @@ -import { createTsAst, isCompoundExpression, parseVForNode, parseInterpolationNode } from '../generators/template'; -import { disableAllFeatures } from '../generators/utils'; +import type { CodeInformation } from '@volar/language-core'; +import { createTsAst } from '../codegen/common'; +import { isCompoundExpression } from '../codegen/template/elementEvents'; +import { parseInterpolationNode } from '../codegen/template/templateChild'; +import { parseVForNode } from '../codegen/template/vFor'; import type { Code, Sfc, VueLanguagePlugin } from '../types'; import * as CompilerDOM from '@vue/compiler-dom'; -const codeFeatures = disableAllFeatures({ +const codeFeatures: CodeInformation = { format: true, // autoInserts: true, // TODO: support vue-autoinsert-parentheses -}); +}; const formatBrackets = { normal: ['`${', '}`;'] as [string, string], if: ['if (', ') { }'] as [string, string], diff --git a/packages/language-core/lib/plugins/vue-tsx.ts b/packages/language-core/lib/plugins/vue-tsx.ts index 84a10e6601..db9870a87d 100644 --- a/packages/language-core/lib/plugins/vue-tsx.ts +++ b/packages/language-core/lib/plugins/vue-tsx.ts @@ -1,7 +1,8 @@ -import { Mapping, StackNode, track } from '@volar/language-core'; +import type { Mapping } from '@volar/language-core'; import { computed, computedSet } from 'computeds'; -import { generate as generateScript } from '../generators/script'; -import { generate as generateTemplate } from '../generators/template'; +import * as path from 'path-browserify'; +import { generateScript } from '../codegen/script'; +import { generateTemplate } from '../codegen/template'; import { parseScriptRanges } from '../parsers/scriptRanges'; import { parseScriptSetupRanges } from '../parsers/scriptSetupRanges'; import type { Code, Sfc, VueLanguagePlugin } from '../types'; @@ -38,9 +39,8 @@ const plugin: VueLanguagePlugin = ctx => { if (embeddedFile.id.startsWith('script_')) { const tsx = _tsx.generatedScript(); if (tsx) { - const [content, contentStacks] = ctx.codegenStack ? track([...tsx.codes], [...tsx.codeStacks]) : [[...tsx.codes], [...tsx.codeStacks]]; + const content: Code[] = [...tsx.codes]; embeddedFile.content = content; - embeddedFile.contentStacks = contentStacks; embeddedFile.linkedCodeMappings = [...tsx.linkedCodeMappings]; } } @@ -111,81 +111,65 @@ function createTsx( return; } - const tsCodes: Code[] = []; - const tsCodegenStacks: string[] = []; - const codegen = generateTemplate( + const codes: Code[] = []; + const codegen = generateTemplate({ ts, - ctx.compilerOptions, - ctx.vueCompilerOptions, - _sfc.template, - shouldGenerateScopedClasses(), - stylesScopedClasses(), - hasScriptSetupSlots(), - slotsAssignName(), - propsAssignName(), - ctx.codegenStack, - ); + compilerOptions: ctx.compilerOptions, + vueCompilerOptions: ctx.vueCompilerOptions, + template: _sfc.template, + shouldGenerateScopedClasses: shouldGenerateScopedClasses(), + stylesScopedClasses: stylesScopedClasses(), + hasDefineSlots: hasDefineSlots(), + slotsAssignName: slotsAssignName(), + propsAssignName: propsAssignName(), + }); let current = codegen.next(); while (!current.done) { - const [code, stack] = current.value; - tsCodes.push(code); - if (ctx.codegenStack) { - tsCodegenStacks.push(stack); - } + const code = current.value; + codes.push(code); current = codegen.next(); } return { ...current.value, - codes: tsCodes, - codeStacks: tsCodegenStacks, + codes: codes, }; }); - const hasScriptSetupSlots = computed(() => !!scriptSetupRanges()?.slots.define); + const hasDefineSlots = computed(() => !!scriptSetupRanges()?.slots.define); const slotsAssignName = computed(() => scriptSetupRanges()?.slots.name); const propsAssignName = computed(() => scriptSetupRanges()?.props.name); const generatedScript = computed(() => { const codes: Code[] = []; - const codeStacks: StackNode[] = []; const linkedCodeMappings: Mapping[] = []; const _template = generatedTemplate(); let generatedLength = 0; - for (const [code, stack] of generateScript( + for (const code of generateScript({ ts, - fileName, - _sfc.script, - _sfc.scriptSetup, - _sfc.styles, - lang(), - scriptRanges(), - scriptSetupRanges(), - _template ? { + fileBaseName: path.basename(fileName), + globalTypes: ctx.globalTypesHolder === fileName, + sfc: _sfc, + lang: lang(), + scriptRanges: scriptRanges(), + scriptSetupRanges: scriptSetupRanges(), + templateCodegen: _template ? { tsCodes: _template.codes, - tsCodegenStacks: _template.codeStacks, - accessedGlobalVariables: _template.accessedGlobalVariables, + ctx: _template.ctx, hasSlot: _template.hasSlot, - tagNames: new Set(_template.tagOffsetsMap.keys()), } : undefined, - ctx.compilerOptions, - ctx.vueCompilerOptions, - ctx.globalTypesHolder, - () => generatedLength, + compilerOptions: ctx.compilerOptions, + vueCompilerOptions: ctx.vueCompilerOptions, + getGeneratedLength: () => generatedLength, linkedCodeMappings, - ctx.codegenStack, - )) { + })) { codes.push(code); - if (ctx.codegenStack) { - codeStacks.push({ stack, length: 1 }); - } generatedLength += typeof code === 'string' ? code.length : code[0].length; }; return { codes, - codeStacks, linkedCodeMappings, }; }); diff --git a/packages/language-core/lib/types.ts b/packages/language-core/lib/types.ts index 42900598a7..b30b3b443d 100644 --- a/packages/language-core/lib/types.ts +++ b/packages/language-core/lib/types.ts @@ -22,10 +22,9 @@ export interface VueCodeInformation extends CodeInformation { paddingLeft?: boolean; }; __combineLastMapping?: boolean; + __combineOffsetMapping?: number; } -export type CodeAndStack = [code: Code, stack: string]; - export type Code = Segment; export interface VueCompilerOptions { @@ -35,7 +34,6 @@ export interface VueCompilerOptions { jsxSlots: boolean; strictTemplates: boolean; skipTemplateCodegen: boolean; - nativeTags: string[]; dataAttributes: string[]; htmlAttributes: string[]; optionsWrapper: [string, string] | []; @@ -66,7 +64,6 @@ export type VueLanguagePlugin = (ctx: { }; compilerOptions: ts.CompilerOptions; vueCompilerOptions: VueCompilerOptions; - codegenStack: boolean; globalTypesHolder: string | undefined; }) => { version: typeof pluginVersion; diff --git a/packages/language-core/lib/utils/ts.ts b/packages/language-core/lib/utils/ts.ts index 8d5cf2a54d..54b1d778ce 100644 --- a/packages/language-core/lib/utils/ts.ts +++ b/packages/language-core/lib/utils/ts.ts @@ -200,31 +200,6 @@ function getPartialVueCompilerOptions( } } -// https://developer.mozilla.org/en-US/docs/Web/HTML/Element -const HTML_TAGS = - 'html,body,base,head,link,meta,style,title,address,article,aside,footer,' + - 'header,hgroup,h1,h2,h3,h4,h5,h6,nav,section,search,div,dd,dl,dt,figcaption,' + - 'figure,picture,hr,img,li,main,ol,p,pre,ul,a,b,abbr,bdi,bdo,br,cite,code,' + - 'data,dfn,em,i,kbd,mark,q,rp,rt,ruby,s,samp,small,span,strong,sub,sup,' + - 'time,u,var,wbr,area,audio,map,track,video,embed,object,param,source,' + - 'canvas,script,noscript,del,ins,caption,col,colgroup,table,thead,tbody,td,' + - 'th,tr,button,datalist,fieldset,form,input,label,legend,meter,optgroup,' + - 'option,output,progress,select,textarea,details,dialog,menu,' + - 'summary,template,blockquote,iframe,tfoot'; - -// https://developer.mozilla.org/en-US/docs/Web/SVG/Element -const SVG_TAGS = - 'svg,animate,animateMotion,animateTransform,circle,clipPath,color-profile,' + - 'defs,desc,discard,ellipse,feBlend,feColorMatrix,feComponentTransfer,' + - 'feComposite,feConvolveMatrix,feDiffuseLighting,feDisplacementMap,' + - 'feDistanceLight,feDropShadow,feFlood,feFuncA,feFuncB,feFuncG,feFuncR,' + - 'feGaussianBlur,feImage,feMerge,feMergeNode,feMorphology,feOffset,' + - 'fePointLight,feSpecularLighting,feSpotLight,feTile,feTurbulence,filter,' + - 'foreignObject,g,hatch,hatchpath,image,line,linearGradient,marker,mask,' + - 'mesh,meshgradient,meshpatch,meshrow,metadata,mpath,path,pattern,' + - 'polygon,polyline,radialGradient,rect,set,solidcolor,stop,switch,symbol,' + - 'text,textPath,title,tspan,unknown,use,view'; - export function resolveVueCompilerOptions(vueOptions: Partial): VueCompilerOptions { const target = vueOptions.target ?? 3.3; const lib = vueOptions.lib || (target < 2.7 ? '@vue/runtime-dom' : 'vue'); @@ -236,14 +211,6 @@ export function resolveVueCompilerOptions(vueOptions: Partial[], fileName: string, sfc: Sfc, - codegenStack: boolean ) { const nameToBlock = computed(() => { @@ -30,7 +29,7 @@ export function computedFiles( } return blocks; }); - const pluginsResult = plugins.map(plugin => computedPluginFiles(plugins, plugin, fileName, sfc, nameToBlock, codegenStack)); + const pluginsResult = plugins.map(plugin => computedPluginFiles(plugins, plugin, fileName, sfc, nameToBlock)); const flatResult = computed(() => pluginsResult.map(r => r()).flat()); const structuredResult = computed(() => { @@ -54,7 +53,7 @@ export function computedFiles( function consumeRemain() { for (let i = remain.length - 1; i >= 0; i--) { - const { file, snapshot, mappings, codegenStacks } = remain[i]; + const { file, snapshot, mappings } = remain[i]; if (!file.parentCodeId) { embeddedCodes.push({ id: file.id, @@ -62,7 +61,6 @@ export function computedFiles( linkedCodeMappings: file.linkedCodeMappings, snapshot, mappings, - codegenStacks, embeddedCodes: [], }); remain.splice(i, 1); @@ -77,7 +75,6 @@ export function computedFiles( linkedCodeMappings: file.linkedCodeMappings, snapshot, mappings, - codegenStacks, embeddedCodes: [], }); remain.splice(i, 1); @@ -107,7 +104,6 @@ function computedPluginFiles( fileName: string, sfc: Sfc, nameToBlock: () => Record, - codegenStack: boolean ) { const embeddedFiles: Record { file: VueEmbeddedCode; snapshot: ts.IScriptSnapshot; }> = {}; const files = computed(() => { @@ -124,8 +120,8 @@ function computedPluginFiles( for (const fileInfo of fileInfos) { if (!embeddedFiles[fileInfo.id]) { embeddedFiles[fileInfo.id] = computed(() => { - const [content, stacks] = codegenStack ? track([]) : [[], []]; - const file = new VueEmbeddedCode(fileInfo.id, fileInfo.lang, content, stacks); + const content: Code[] = []; + const file = new VueEmbeddedCode(fileInfo.id, fileInfo.lang, content); for (const plugin of plugins) { if (!plugin.resolveEmbeddedCode) { continue; @@ -173,9 +169,11 @@ function computedPluginFiles( const { file, snapshot } = _file(); const mappings = buildMappings(file.content); + const newMappings: typeof mappings = []; let lastValidMapping: typeof mappings[number] | undefined; - for (const mapping of mappings) { + for (let i = 0; i < mappings.length; i++) { + const mapping = mappings[i]; if (mapping.source !== undefined) { const block = nameToBlock()[mapping.source]; if (block) { @@ -186,7 +184,17 @@ function computedPluginFiles( } mapping.source = undefined; } - if (mapping.data.__combineLastMapping) { + if (mapping.data.__combineOffsetMapping !== undefined) { + const offsetMapping = mappings[i - mapping.data.__combineOffsetMapping]; + if (typeof offsetMapping === 'string' || !offsetMapping) { + throw new Error('Invalid offset mapping, mappings: ' + mappings.length + ', i: ' + i + ', offset: ' + mapping.data.__combineOffsetMapping); + } + offsetMapping.sourceOffsets.push(...mapping.sourceOffsets); + offsetMapping.generatedOffsets.push(...mapping.generatedOffsets); + offsetMapping.lengths.push(...mapping.lengths); + continue; + } + else if (mapping.data.__combineLastMapping) { lastValidMapping!.sourceOffsets.push(...mapping.sourceOffsets); lastValidMapping!.generatedOffsets.push(...mapping.generatedOffsets); lastValidMapping!.lengths.push(...mapping.lengths); @@ -195,13 +203,13 @@ function computedPluginFiles( else { lastValidMapping = mapping; } + newMappings.push(mapping); } return { file, snapshot, - mappings: mappings.filter(mapping => !mapping.data.__combineLastMapping), - codegenStacks: buildStacks(file.content, file.contentStacks), + mappings: newMappings, }; }); }); diff --git a/packages/language-core/lib/virtualFile/computedMappings.ts b/packages/language-core/lib/virtualFile/computedMappings.ts index d1b02ac486..90df46b412 100644 --- a/packages/language-core/lib/virtualFile/computedMappings.ts +++ b/packages/language-core/lib/virtualFile/computedMappings.ts @@ -1,15 +1,15 @@ import { CodeMapping, Segment, replaceSourceRange } from '@volar/language-core'; import { computed } from 'computeds'; import type * as ts from 'typescript'; -import { disableAllFeatures, enableAllFeatures } from '../generators/utils'; import type { Sfc, VueCodeInformation } from '../types'; +import { allCodeFeatures } from '../plugins/shared'; export function computedMappings( snapshot: () => ts.IScriptSnapshot, sfc: Sfc ) { return computed(() => { - const str: Segment[] = [[snapshot().getText(0, snapshot().getLength()), undefined, 0, enableAllFeatures({})]]; + const str: Segment[] = [[snapshot().getText(0, snapshot().getLength()), undefined, 0, allCodeFeatures]]; for (const block of [ sfc.script, sfc.scriptSetup, @@ -59,7 +59,7 @@ export function computedMappings( sourceOffsets: offsets, generatedOffsets: offsets, lengths: offsets.map(() => 0), - data: disableAllFeatures({ structure: true }), + data: { structure: true }, }); } } diff --git a/packages/language-core/lib/virtualFile/computedSfc.ts b/packages/language-core/lib/virtualFile/computedSfc.ts index 78ca2ff2e1..71fc283c9f 100644 --- a/packages/language-core/lib/virtualFile/computedSfc.ts +++ b/packages/language-core/lib/virtualFile/computedSfc.ts @@ -51,7 +51,7 @@ export function computedSfc( }); }, ); - const scriptSetup = computedNullableSfcBlock( + const scriptSetupOriginal = computedNullableSfcBlock( 'scriptSetup', 'js', computed(() => parsed()?.descriptor.scriptSetup ?? undefined), @@ -72,6 +72,27 @@ export function computedSfc( }); }, ); + const hasScript = computed(() => !!parsed()?.descriptor.script); + const hasScriptSetup = computed(() => !!parsed()?.descriptor.scriptSetup); + const scriptSetup = computed(() => { + if (!hasScript() && !hasScriptSetup()) { + //#region monkey fix: https://github.com/vuejs/language-tools/pull/2113 + return { + content: '', + lang: 'ts', + name: '', + start: 0, + end: 0, + startTagEnd: 0, + endTagStart: 0, + generic: undefined, + genericOffset: 0, + attrs: {}, + ast: ts.createSourceFile('', '', 99 satisfies ts.ScriptTarget.Latest, false, ts.ScriptKind.TS), + }; + } + return scriptSetupOriginal(); + }); const styles = computedArray( computed(() => parsed()?.descriptor.styles ?? []), (block, i) => { diff --git a/packages/language-core/lib/virtualFile/embeddedFile.ts b/packages/language-core/lib/virtualFile/embeddedFile.ts index 4307ce6f29..d3e881e3fa 100644 --- a/packages/language-core/lib/virtualFile/embeddedFile.ts +++ b/packages/language-core/lib/virtualFile/embeddedFile.ts @@ -1,4 +1,4 @@ -import type { Mapping, StackNode } from '@volar/language-core'; +import type { Mapping } from '@volar/language-core'; import type { Code } from '../types'; export class VueEmbeddedCode { @@ -11,6 +11,5 @@ export class VueEmbeddedCode { public id: string, public lang: string, public content: Code[], - public contentStacks: StackNode[], ) { } } diff --git a/packages/language-core/lib/virtualFile/vueFile.ts b/packages/language-core/lib/virtualFile/vueFile.ts index 1753832cda..c1e6211eb5 100644 --- a/packages/language-core/lib/virtualFile/vueFile.ts +++ b/packages/language-core/lib/virtualFile/vueFile.ts @@ -1,4 +1,4 @@ -import type { Stack, VirtualCode } from '@volar/language-core'; +import type { VirtualCode } from '@volar/language-core'; import { Signal, signal } from 'computeds'; import type * as ts from 'typescript'; import type { VueCompilerOptions, VueLanguagePlugin } from '../types'; @@ -7,7 +7,7 @@ import { computedMappings } from './computedMappings'; import { computedSfc } from './computedSfc'; import { computedVueSfc } from './computedVueSfc'; -export class VueGeneratedCode implements VirtualCode { +export class VueVirtualCode implements VirtualCode { // sources @@ -20,11 +20,10 @@ export class VueGeneratedCode implements VirtualCode { getVueSfc = computedVueSfc(this.plugins, this.fileName, () => this._snapshot()); sfc = computedSfc(this.ts, this.plugins, this.fileName, () => this._snapshot(), this.getVueSfc); getMappings = computedMappings(() => this._snapshot(), this.sfc); - getEmbeddedCodes = computedFiles(this.plugins, this.fileName, this.sfc, this.codegenStack); + getEmbeddedCodes = computedFiles(this.plugins, this.fileName, this.sfc); // others - codegenStacks: Stack[] = []; get embeddedCodes() { return this.getEmbeddedCodes(); } @@ -42,7 +41,6 @@ export class VueGeneratedCode implements VirtualCode { public vueCompilerOptions: VueCompilerOptions, public plugins: ReturnType[], public ts: typeof import('typescript'), - public codegenStack: boolean, ) { this._snapshot = signal(initSnapshot); } diff --git a/packages/language-server/lib/types.ts b/packages/language-server/lib/types.ts index 0148645ca0..2536ab2d60 100644 --- a/packages/language-server/lib/types.ts +++ b/packages/language-server/lib/types.ts @@ -1,4 +1,4 @@ -import type { InitializationOptions } from "@volar/language-server"; +import type { InitializationOptions } from '@volar/language-server'; export type VueInitializationOptions = InitializationOptions & { typescript: { diff --git a/packages/language-server/node.ts b/packages/language-server/node.ts index 7a781636a7..9cbb66dbb7 100644 --- a/packages/language-server/node.ts +++ b/packages/language-server/node.ts @@ -85,7 +85,6 @@ connection.onInitialize(async params => { () => projectContext.typescript?.host.getScriptFileNames() ?? [], commandLine?.options ?? {}, vueOptions, - options.codegenStack, ); envToVueOptions.set(serviceEnv, vueOptions); diff --git a/packages/language-service/lib/ideFeatures/nameCasing.ts b/packages/language-service/lib/ideFeatures/nameCasing.ts index 68785bc80d..f2c2707ce0 100644 --- a/packages/language-service/lib/ideFeatures/nameCasing.ts +++ b/packages/language-service/lib/ideFeatures/nameCasing.ts @@ -1,7 +1,7 @@ import type { ServiceContext, VirtualCode } from '@volar/language-service'; import type { CompilerDOM } from '@vue/language-core'; import * as vue from '@vue/language-core'; -import { VueGeneratedCode, hyphenateAttr, hyphenateTag } from '@vue/language-core'; +import { VueVirtualCode, hyphenateAttr, hyphenateTag } from '@vue/language-core'; import { computed } from 'computeds'; import type * as vscode from 'vscode-languageserver-protocol'; import { AttrNameCasing, TagNameCasing } from '../types'; @@ -19,7 +19,7 @@ export async function convertTagName( } const rootCode = sourceFile?.generated?.root; - if (!(rootCode instanceof VueGeneratedCode)) { + if (!(rootCode instanceof VueVirtualCode)) { return; } @@ -67,7 +67,7 @@ export async function convertAttrName( } const rootCode = sourceFile?.generated?.root; - if (!(rootCode instanceof VueGeneratedCode)) { + if (!(rootCode instanceof VueVirtualCode)) { return; } @@ -138,7 +138,7 @@ export async function detect( }> { const rootFile = context.language.scripts.get(uri)?.generated?.root; - if (!(rootFile instanceof VueGeneratedCode)) { + if (!(rootFile instanceof VueVirtualCode)) { return { tag: [], attr: [], @@ -174,7 +174,7 @@ export async function detect( return result; } - async function getTagNameCase(file: VueGeneratedCode): Promise { + async function getTagNameCase(file: VueVirtualCode): Promise { const components = await tsPluginClient?.getComponentNames(file.fileName) ?? []; const tagNames = getTemplateTagsAndAttrs(file); @@ -225,7 +225,7 @@ function getTemplateTagsAndAttrs(sourceFile: VirtualCode): Tags { if (!map.has(sourceFile)) { const getter = computed(() => { - if (!(sourceFile instanceof vue.VueGeneratedCode)) { + if (!(sourceFile instanceof vue.VueVirtualCode)) { return; } const ast = sourceFile.sfc.template?.ast; diff --git a/packages/language-service/lib/plugins/vue-codelens-references.ts b/packages/language-service/lib/plugins/vue-codelens-references.ts index 9cc7b4b87d..14ad82a0a3 100644 --- a/packages/language-service/lib/plugins/vue-codelens-references.ts +++ b/packages/language-service/lib/plugins/vue-codelens-references.ts @@ -1,5 +1,5 @@ import type { LanguageServicePlugin, LanguageServicePluginInstance } from '@volar/language-service'; -import { SourceScript, VirtualCode, VueCodeInformation, VueGeneratedCode } from '@vue/language-core'; +import { SourceScript, VirtualCode, VueCodeInformation, VueVirtualCode } from '@vue/language-core'; import type * as vscode from 'vscode-languageserver-protocol'; export function create(): LanguageServicePlugin { @@ -40,7 +40,7 @@ export function create(): LanguageServicePlugin { const decoded = context.decodeEmbeddedDocumentUri(uri); const sourceScript = decoded && context.language.scripts.get(decoded[0]); const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]); - if (!virtualCode || !(sourceScript?.generated?.root instanceof VueGeneratedCode) || !sourceScript) { + if (!virtualCode || !(sourceScript?.generated?.root instanceof VueVirtualCode) || !sourceScript) { return; } diff --git a/packages/language-service/lib/plugins/vue-document-drop.ts b/packages/language-service/lib/plugins/vue-document-drop.ts index 6e706a4dd9..46a83819a0 100644 --- a/packages/language-service/lib/plugins/vue-document-drop.ts +++ b/packages/language-service/lib/plugins/vue-document-drop.ts @@ -1,4 +1,4 @@ -import { VueGeneratedCode, forEachEmbeddedCode } from '@vue/language-core'; +import { VueVirtualCode, forEachEmbeddedCode } from '@vue/language-core'; import { camelize, capitalize, hyphenate } from '@vue/shared'; import * as path from 'path-browserify'; import type * as vscode from 'vscode-languageserver-protocol'; @@ -29,7 +29,7 @@ export function create( const sourceScript = decoded && context.language.scripts.get(decoded[0]); const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]); const vueVirtualCode = sourceScript?.generated?.root; - if (!sourceScript || !virtualCode || !(vueVirtualCode instanceof VueGeneratedCode)) { + if (!sourceScript || !virtualCode || !(vueVirtualCode instanceof VueVirtualCode)) { return; } diff --git a/packages/language-service/lib/plugins/vue-extract-file.ts b/packages/language-service/lib/plugins/vue-extract-file.ts index 6e17d9c6db..01036402a6 100644 --- a/packages/language-service/lib/plugins/vue-extract-file.ts +++ b/packages/language-service/lib/plugins/vue-extract-file.ts @@ -1,6 +1,6 @@ import type { CreateFile, ServiceContext, LanguageServicePlugin, TextDocumentEdit, TextEdit } from '@volar/language-service'; import type { ExpressionNode, TemplateChildNode } from '@vue/compiler-dom'; -import { Sfc, VueGeneratedCode, scriptRanges } from '@vue/language-core'; +import { Sfc, VueVirtualCode, scriptRanges } from '@vue/language-core'; import type * as ts from 'typescript'; import type * as vscode from 'vscode-languageserver-protocol'; @@ -32,7 +32,7 @@ export function create( const decoded = context.decodeEmbeddedDocumentUri(document.uri); const sourceScript = decoded && context.language.scripts.get(decoded[0]); const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]); - if (!(sourceScript?.generated?.root instanceof VueGeneratedCode) || virtualCode?.id !== 'template') { + if (!(sourceScript?.generated?.root instanceof VueVirtualCode) || virtualCode?.id !== 'template') { return; } @@ -68,7 +68,7 @@ export function create( const decoded = context.decodeEmbeddedDocumentUri(uri); const sourceScript = decoded && context.language.scripts.get(decoded[0]); const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]); - if (!(sourceScript?.generated?.root instanceof VueGeneratedCode) || virtualCode?.id !== 'template') { + if (!(sourceScript?.generated?.root instanceof VueVirtualCode) || virtualCode?.id !== 'template') { return codeAction; } diff --git a/packages/language-service/lib/plugins/vue-sfc.ts b/packages/language-service/lib/plugins/vue-sfc.ts index 823f3d7d32..3fe001d4a6 100644 --- a/packages/language-service/lib/plugins/vue-sfc.ts +++ b/packages/language-service/lib/plugins/vue-sfc.ts @@ -9,7 +9,7 @@ import { loadLanguageBlocks } from './data'; let sfcDataProvider: html.IHTMLDataProvider | undefined; export interface Provide { - 'vue/vueFile': (document: TextDocument) => vue.VueGeneratedCode | undefined; + 'vue/vueFile': (document: TextDocument) => vue.VueVirtualCode | undefined; } export function create(): LanguageServicePlugin { @@ -60,7 +60,7 @@ export function create(): LanguageServicePlugin { }, async resolveEmbeddedCodeFormattingOptions(sourceScript, virtualCode, options) { - if (sourceScript.generated?.root instanceof vue.VueGeneratedCode) { + if (sourceScript.generated?.root instanceof vue.VueVirtualCode) { if (virtualCode.id === 'scriptFormat' || virtualCode.id === 'scriptSetupFormat') { if (await context.env.getConfiguration?.('vue.format.script.initialIndent') ?? false) { options.initialIndentLevel++; @@ -171,11 +171,11 @@ export function create(): LanguageServicePlugin { }, }; - function worker(document: TextDocument, callback: (vueSourceFile: vue.VueGeneratedCode) => T) { + function worker(document: TextDocument, callback: (vueSourceFile: vue.VueVirtualCode) => T) { const decoded = context.decodeEmbeddedDocumentUri(document.uri); const sourceScript = decoded && context.language.scripts.get(decoded[0]); const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]); - if (virtualCode instanceof vue.VueGeneratedCode) { + if (virtualCode instanceof vue.VueVirtualCode) { return callback(virtualCode); } } diff --git a/packages/language-service/lib/plugins/vue-template.ts b/packages/language-service/lib/plugins/vue-template.ts index ba9c512818..b24eda61ab 100644 --- a/packages/language-service/lib/plugins/vue-template.ts +++ b/packages/language-service/lib/plugins/vue-template.ts @@ -1,5 +1,5 @@ import type { Disposable, ServiceContext, ServiceEnvironment, LanguageServicePluginInstance } from '@volar/language-service'; -import { VueGeneratedCode, hyphenateAttr, hyphenateTag, parseScriptSetupRanges, tsCodegen } from '@vue/language-core'; +import { VueVirtualCode, hyphenateAttr, hyphenateTag, parseScriptSetupRanges, tsCodegen } from '@vue/language-core'; import { camelize, capitalize } from '@vue/shared'; import { create as createHtmlService } from 'volar-service-html'; import { create as createPugService } from 'volar-service-pug'; @@ -113,7 +113,7 @@ export function create( const decoded = context.decodeEmbeddedDocumentUri(document.uri); const sourceScript = decoded && context.language.scripts.get(decoded[0]); - if (sourceScript?.generated?.root instanceof VueGeneratedCode) { + if (sourceScript?.generated?.root instanceof VueVirtualCode) { sync = (await provideHtmlData(sourceScript.id, sourceScript.generated.root)).sync; currentVersion = await sync(); } @@ -126,7 +126,7 @@ export function create( return; } - if (sourceScript?.generated?.root instanceof VueGeneratedCode) { + if (sourceScript?.generated?.root instanceof VueVirtualCode) { await afterHtmlCompletion( htmlComplete, context.documents.get(sourceScript.id, sourceScript.languageId, sourceScript.snapshot), @@ -161,7 +161,7 @@ export function create( const code = context.language.scripts.get(map.sourceDocument.uri)?.generated?.root; const scanner = getScanner(baseServiceInstance, document); - if (code instanceof VueGeneratedCode && scanner) { + if (code instanceof VueVirtualCode && scanner) { // visualize missing required props const casing = await getNameCasing(context, map.sourceDocument.uri, tsPluginClient); @@ -288,7 +288,7 @@ export function create( for (const map of context.documents.getMaps(virtualCode)) { const code = context.language.scripts.get(map.sourceDocument.uri)?.generated?.root; - if (!(code instanceof VueGeneratedCode)) { + if (!(code instanceof VueVirtualCode)) { continue; } @@ -345,7 +345,7 @@ export function create( const sourceScript = decoded && context.language.scripts.get(decoded[0]); if ( !sourceScript - || !(sourceScript.generated?.root instanceof VueGeneratedCode) + || !(sourceScript.generated?.root instanceof VueVirtualCode) || !sourceScript.generated.root.sfc.template ) { return []; @@ -378,7 +378,7 @@ export function create( }, }; - async function provideHtmlData(sourceDocumentUri: string, vueCode: VueGeneratedCode) { + async function provideHtmlData(sourceDocumentUri: string, vueCode: VueVirtualCode) { await (initializing ??= initialize()); @@ -621,7 +621,7 @@ export function create( }; } - async function afterHtmlCompletion(completionList: vscode.CompletionList, sourceDocument: TextDocument, code: VueGeneratedCode) { + async function afterHtmlCompletion(completionList: vscode.CompletionList, sourceDocument: TextDocument, code: VueVirtualCode) { const replacement = getReplacement(completionList, sourceDocument); const componentNames = new Set( diff --git a/packages/language-service/lib/plugins/vue-toggle-v-bind-codeaction.ts b/packages/language-service/lib/plugins/vue-toggle-v-bind-codeaction.ts index 961ebe4fec..a88f1762d9 100644 --- a/packages/language-service/lib/plugins/vue-toggle-v-bind-codeaction.ts +++ b/packages/language-service/lib/plugins/vue-toggle-v-bind-codeaction.ts @@ -1,5 +1,5 @@ import type { LanguageServicePlugin, LanguageServicePluginInstance } from '@volar/language-service'; -import { VueGeneratedCode, forEachElementNode, type CompilerDOM } from '@vue/language-core'; +import { VueVirtualCode, forEachElementNode, type CompilerDOM } from '@vue/language-core'; import type * as vscode from 'vscode-languageserver-protocol'; export function create(ts: typeof import('typescript')): LanguageServicePlugin { @@ -14,7 +14,7 @@ export function create(ts: typeof import('typescript')): LanguageServicePlugin { const decoded = context.decodeEmbeddedDocumentUri(document.uri); const sourceScript = decoded && context.language.scripts.get(decoded[0]); const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]); - if (!(virtualCode instanceof VueGeneratedCode)) { + if (!(virtualCode instanceof VueVirtualCode)) { return; } diff --git a/packages/language-service/lib/plugins/vue-twoslash-queries.ts b/packages/language-service/lib/plugins/vue-twoslash-queries.ts index 15ad153f44..093943a392 100644 --- a/packages/language-service/lib/plugins/vue-twoslash-queries.ts +++ b/packages/language-service/lib/plugins/vue-twoslash-queries.ts @@ -18,7 +18,7 @@ export function create( const decoded = context.decodeEmbeddedDocumentUri(document.uri); const sourceScript = decoded && context.language.scripts.get(decoded[0]); const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]); - if (!(sourceScript?.generated?.root instanceof vue.VueGeneratedCode) || virtualCode?.id !== 'template') { + if (!(sourceScript?.generated?.root instanceof vue.VueVirtualCode) || virtualCode?.id !== 'template') { return; } diff --git a/packages/language-service/tests/format/1210.spec.ts b/packages/language-service/tests/format/1210.spec.ts index 2d19cb4782..d79e577ec5 100644 --- a/packages/language-service/tests/format/1210.spec.ts +++ b/packages/language-service/tests/format/1210.spec.ts @@ -5,7 +5,7 @@ defineFormatTest({ languageId: 'vue', input: ` diff --git a/packages/tsc/index.ts b/packages/tsc/index.ts index fe62066b49..1d1485722b 100644 --- a/packages/tsc/index.ts +++ b/packages/tsc/index.ts @@ -39,7 +39,6 @@ export function run() { () => options.rootNames.map(rootName => rootName.replace(windowsPathReg, '/')), options.options, vueOptions, - false, ); return [vueLanguagePlugin]; } diff --git a/packages/tsc/tests/__snapshots__/dts.spec.ts.snap b/packages/tsc/tests/__snapshots__/dts.spec.ts.snap index 8e9c19bd90..fb7a1f43f8 100644 --- a/packages/tsc/tests/__snapshots__/dts.spec.ts.snap +++ b/packages/tsc/tests/__snapshots__/dts.spec.ts.snap @@ -90,7 +90,6 @@ exports[`vue-tsc-dts > Input: generic/component.vue, Output: generic/component.v }; }; export default _default; -type __VLS_OmitKeepDiscriminatedUnion = T extends any ? Pick> : never; type __VLS_Prettify = { [K in keyof T]: T[K]; } & {}; @@ -159,7 +158,6 @@ exports[`vue-tsc-dts > Input: generic/custom-extension-component.cext, Output: g }; }; export default _default; -type __VLS_OmitKeepDiscriminatedUnion = T extends any ? Pick> : never; type __VLS_Prettify = { [K in keyof T]: T[K]; } & {}; @@ -314,26 +312,35 @@ export default _default; `; exports[`vue-tsc-dts > Input: reference-type-model/component.vue, Output: reference-type-model/component.vue.d.ts 1`] = ` -"declare const _default: import("vue").DefineComponent<{ - foo: import("vue").PropType; - bar: import("vue").PropType; - qux: import("vue").PropType; - quxModifiers: import("vue").PropType>; -}, {}, unknown, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, { +"declare const _default: import("vue").DefineComponent<__VLS_TypePropsToOption<{ + foo?: number; + bar?: string[]; + qux?: string; + quxModifiers?: Record<"trim" | "lazy", true>; +}>, {}, unknown, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, { "update:foo": (foo: number) => void; "update:bar": (bar: string[]) => void; "update:qux": (qux: string) => void; -}, string, import("vue").PublicProps, Readonly; - bar: import("vue").PropType; - qux: import("vue").PropType; - quxModifiers: import("vue").PropType>; -}>> & { +}, string, import("vue").PublicProps, Readonly; +}>>> & { "onUpdate:foo"?: (foo: number) => any; "onUpdate:bar"?: (bar: string[]) => any; "onUpdate:qux"?: (qux: string) => 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; + }; +}; " `; diff --git a/packages/tsc/tests/dts.spec.ts b/packages/tsc/tests/dts.spec.ts index d2ad168b24..7b0eb5d1bc 100644 --- a/packages/tsc/tests/dts.spec.ts +++ b/packages/tsc/tests/dts.spec.ts @@ -40,7 +40,6 @@ describe('vue-tsc-dts', () => { () => options.rootNames.map(rootName => rootName.replace(windowsPathReg, '/')), options.options, vueOptions, - false, ); return [vueLanguagePlugin]; }, fileName => { diff --git a/packages/typescript-plugin/lib/common.ts b/packages/typescript-plugin/lib/common.ts index 1d08350568..520d58c463 100644 --- a/packages/typescript-plugin/lib/common.ts +++ b/packages/typescript-plugin/lib/common.ts @@ -81,7 +81,7 @@ export function decorateLanguageServiceForVue( languageService.getEncodedSemanticClassifications = (fileName, span, format) => { const result = getEncodedSemanticClassifications(fileName, span, format); const file = language.scripts.get(fileName); - if (file?.generated?.root instanceof vue.VueGeneratedCode) { + if (file?.generated?.root instanceof vue.VueVirtualCode) { const { template } = file.generated.root.sfc; if (template) { for (const componentSpan of getComponentSpans.call( @@ -110,15 +110,14 @@ export function getComponentSpans( this: { typescript: typeof import('typescript'); languageService: ts.LanguageService; - vueOptions: vue.VueCompilerOptions; }, - vueCode: vue.VueGeneratedCode, - template: NonNullable, + vueCode: vue.VueVirtualCode, + template: NonNullable, spanTemplateRange: ts.TextSpan, ) { - const { typescript: ts, languageService, vueOptions } = this; + const { typescript: ts, languageService } = this; const result: ts.TextSpan[] = []; - const validComponentNames = _getComponentNames(ts, languageService, vueCode, vueOptions); + const validComponentNames = _getComponentNames(ts, languageService, vueCode); const components = new Set([ ...validComponentNames, ...validComponentNames.map(vue.hyphenateTag), diff --git a/packages/typescript-plugin/lib/requests/collectExtractProps.ts b/packages/typescript-plugin/lib/requests/collectExtractProps.ts index 13fb3afdf4..0ebdf05a89 100644 --- a/packages/typescript-plugin/lib/requests/collectExtractProps.ts +++ b/packages/typescript-plugin/lib/requests/collectExtractProps.ts @@ -1,4 +1,4 @@ -import { Language, VueGeneratedCode, isSemanticTokensEnabled } from '@vue/language-core'; +import { Language, VueVirtualCode, isSemanticTokensEnabled } from '@vue/language-core'; import type * as ts from 'typescript'; export function collectExtractProps( @@ -15,7 +15,7 @@ export function collectExtractProps( const { typescript: ts, languageService, language, isTsPlugin, getFileId } = this; const volarFile = language.scripts.get(getFileId(fileName)); - if (!(volarFile?.generated?.root instanceof VueGeneratedCode)) { + if (!(volarFile?.generated?.root instanceof VueVirtualCode)) { return; } diff --git a/packages/typescript-plugin/lib/requests/componentInfos.ts b/packages/typescript-plugin/lib/requests/componentInfos.ts index b6b92bfaff..c216ed8694 100644 --- a/packages/typescript-plugin/lib/requests/componentInfos.ts +++ b/packages/typescript-plugin/lib/requests/componentInfos.ts @@ -7,16 +7,15 @@ export function getComponentProps( typescript: typeof import('typescript'); languageService: ts.LanguageService; language: vue.Language; - vueOptions: vue.VueCompilerOptions, getFileId: (fileName: string) => string, }, fileName: string, tag: string, requiredOnly = false, ) { - const { typescript: ts, language, vueOptions, languageService, getFileId } = this; + const { typescript: ts, language, languageService, getFileId } = this; const volarFile = language.scripts.get(getFileId(fileName)); - if (!(volarFile?.generated?.root instanceof vue.VueGeneratedCode)) { + if (!(volarFile?.generated?.root instanceof vue.VueVirtualCode)) { return; } const vueCode = volarFile.generated.root; @@ -35,7 +34,7 @@ export function getComponentProps( let componentSymbol = components.type.getProperty(name[0]); - if (!componentSymbol && !vueOptions.nativeTags.includes(name[0])) { + if (!componentSymbol) { componentSymbol = components.type.getProperty(camelize(name[0])) ?? components.type.getProperty(capitalize(camelize(name[0]))); } @@ -96,15 +95,14 @@ export function getComponentEvents( typescript: typeof import('typescript'); languageService: ts.LanguageService; language: vue.Language; - vueOptions: vue.VueCompilerOptions, getFileId: (fileName: string) => string, }, fileName: string, tag: string, ) { - const { typescript: ts, language, vueOptions, languageService, getFileId } = this; + const { typescript: ts, language, languageService, getFileId } = this; const volarFile = language.scripts.get(getFileId(fileName)); - if (!(volarFile?.generated?.root instanceof vue.VueGeneratedCode)) { + if (!(volarFile?.generated?.root instanceof vue.VueVirtualCode)) { return; } const vueCode = volarFile.generated.root; @@ -123,7 +121,7 @@ export function getComponentEvents( let componentSymbol = components.type.getProperty(name[0]); - if (!componentSymbol && !vueOptions.nativeTags.includes(name[0])) { + if (!componentSymbol) { componentSymbol = components.type.getProperty(camelize(name[0])) ?? components.type.getProperty(capitalize(camelize(name[0]))); } @@ -184,7 +182,7 @@ export function getTemplateContextProps( ) { const { typescript: ts, language, languageService, getFileId } = this; const volarFile = language.scripts.get(getFileId(fileName)); - if (!(volarFile?.generated?.root instanceof vue.VueGeneratedCode)) { + if (!(volarFile?.generated?.root instanceof vue.VueVirtualCode)) { return; } const vueCode = volarFile.generated.root; @@ -200,14 +198,13 @@ export function getComponentNames( typescript: typeof import('typescript'); languageService: ts.LanguageService; language: vue.Language; - vueOptions: vue.VueCompilerOptions, getFileId: (fileName: string) => string, }, fileName: string, ) { - const { typescript: ts, language, vueOptions, languageService, getFileId } = this; + const { typescript: ts, language, languageService, getFileId } = this; const volarFile = language.scripts.get(getFileId(fileName)); - if (!(volarFile?.generated?.root instanceof vue.VueGeneratedCode)) { + if (!(volarFile?.generated?.root instanceof vue.VueVirtualCode)) { return; } const vueCode = volarFile.generated.root; @@ -217,22 +214,19 @@ export function getComponentNames( ?.getProperties() .map(c => c.name) .filter(entry => entry.indexOf('$') === -1 && !entry.startsWith('_')) - .filter(entry => !vueOptions.nativeTags.includes(entry)) ?? []; } export function _getComponentNames( ts: typeof import('typescript'), tsLs: ts.LanguageService, - vueCode: vue.VueGeneratedCode, - vueOptions: vue.VueCompilerOptions, + vueCode: vue.VueVirtualCode, ) { return getVariableType(ts, tsLs, vueCode, '__VLS_components') ?.type ?.getProperties() .map(c => c.name) .filter(entry => entry.indexOf('$') === -1 && !entry.startsWith('_')) - .filter(entry => !vueOptions.nativeTags.includes(entry)) ?? []; } @@ -248,7 +242,7 @@ export function getElementAttrs( ) { const { typescript: ts, language, languageService, getFileId } = this; const volarFile = language.scripts.get(getFileId(fileName)); - if (!(volarFile?.generated?.root instanceof vue.VueGeneratedCode)) { + if (!(volarFile?.generated?.root instanceof vue.VueVirtualCode)) { return; } const program: ts.Program = (languageService as any).getCurrentProgram(); @@ -282,7 +276,7 @@ export function getElementAttrs( function getVariableType( ts: typeof import('typescript'), tsLs: ts.LanguageService, - vueCode: vue.VueGeneratedCode, + vueCode: vue.VueVirtualCode, name: string, ) { const program: ts.Program = (tsLs as any).getCurrentProgram(); diff --git a/test-workspace/component-meta/options-api/component.ts b/test-workspace/component-meta/options-api/component.ts index 19a04f1e57..4bddfe432c 100644 --- a/test-workspace/component-meta/options-api/component.ts +++ b/test-workspace/component-meta/options-api/component.ts @@ -1,4 +1,4 @@ -import { defineComponent } from "vue"; +import { defineComponent } from 'vue'; interface SubmitPayload { /** diff --git a/test-workspace/component-meta/ts-component/component.ts b/test-workspace/component-meta/ts-component/component.ts index 55062a45f1..bfb249a65f 100644 --- a/test-workspace/component-meta/ts-component/component.ts +++ b/test-workspace/component-meta/ts-component/component.ts @@ -1,5 +1,5 @@ -import { h, defineComponent } from "vue"; -import { MyProps } from "./PropDefinitions"; +import { h, defineComponent } from 'vue'; +import { MyProps } from './PropDefinitions'; export default defineComponent((props: MyProps) => { return () => h('pre', JSON.stringify(props, null, 2)); diff --git a/test-workspace/component-meta/ts-component/component.tsx b/test-workspace/component-meta/ts-component/component.tsx index 0ceaa9e012..d3e4e41633 100644 --- a/test-workspace/component-meta/ts-component/component.tsx +++ b/test-workspace/component-meta/ts-component/component.tsx @@ -1,5 +1,5 @@ -import { defineComponent } from "vue"; -import { MyProps } from "./PropDefinitions"; +import { defineComponent } from 'vue'; +import { MyProps } from './PropDefinitions'; export default defineComponent((props: MyProps) => { return () =>

diff --git a/test-workspace/component-meta/ts-named-export/component.ts b/test-workspace/component-meta/ts-named-export/component.ts
index cf55be5cb0..1913ecb269 100644
--- a/test-workspace/component-meta/ts-named-export/component.ts
+++ b/test-workspace/component-meta/ts-named-export/component.ts
@@ -1,4 +1,4 @@
-import { defineComponent } from "vue";
+import { defineComponent } from 'vue';
 
 export const Foo = defineComponent((_: { foo: string; }) => ()=> { });
 
diff --git a/test-workspace/tsc/vue3/#2399/main.vue b/test-workspace/tsc/vue3/#2399/main.vue
index 849d1c76b6..f1b0fb604c 100644
--- a/test-workspace/tsc/vue3/#2399/main.vue
+++ b/test-workspace/tsc/vue3/#2399/main.vue
@@ -3,13 +3,13 @@
 
 
 
 
diff --git a/test-workspace/tsc/vue3/dynamic-component/main.vue b/test-workspace/tsc/vue3/dynamic-component/main.vue
index f92986b92c..c7f6283a12 100644
--- a/test-workspace/tsc/vue3/dynamic-component/main.vue
+++ b/test-workspace/tsc/vue3/dynamic-component/main.vue
@@ -6,9 +6,9 @@ let Bar: new () => { $props: { bar: (_: number) => void; }; };
 
 
 
diff --git a/test-workspace/tsc/vue3/input-radio/main.vue b/test-workspace/tsc/vue3/input-radio/main.vue
index 7bd173c67f..ee073c9c48 100644
--- a/test-workspace/tsc/vue3/input-radio/main.vue
+++ b/test-workspace/tsc/vue3/input-radio/main.vue
@@ -4,7 +4,7 @@