diff --git a/server/src/modes/script/childComponents.ts b/server/src/modes/script/childComponents.ts index e0ef7d075d..62331302d7 100644 --- a/server/src/modes/script/childComponents.ts +++ b/server/src/modes/script/childComponents.ts @@ -10,6 +10,7 @@ import { T_TypeScript } from '../../services/dependencyService'; import { kebabCase } from 'lodash'; interface InternalChildComponent { + rawName: string; name: string; documentation?: string; definition?: { @@ -57,11 +58,6 @@ export function getChildComponents( return; } - let componentName = s.name; - if (tagCasing === 'kebab') { - componentName = kebabCase(s.name); - } - let objectLiteralSymbol: ts.Symbol | undefined; if (s.valueDeclaration.kind === tsModule.SyntaxKind.PropertyAssignment) { objectLiteralSymbol = @@ -85,7 +81,8 @@ export function getChildComponents( return; } result.push({ - name: componentName, + rawName: s.name, + name: tagCasing === 'kebab' ? kebabCase(s.name) : s.name, documentation: buildDocumentation(tsModule, definitionSymbol, checker), definition: { path: sourceFile.fileName, diff --git a/server/src/modes/script/componentInfo.ts b/server/src/modes/script/componentInfo.ts index 1529f2d5f6..7dc6be2898 100644 --- a/server/src/modes/script/componentInfo.ts +++ b/server/src/modes/script/componentInfo.ts @@ -5,7 +5,8 @@ import { ComputedInfo, DataInfo, MethodInfo, - ChildComponent + ChildComponent, + ComponentInfo } from '../../services/vueInfoService'; import { getChildComponents } from './childComponents'; import { T_TypeScript } from '../../services/dependencyService'; @@ -33,7 +34,10 @@ export function getComponentInfo( return undefined; } - const vueFileInfo = analyzeDefaultExportExpr(tsModule, defaultExportNode, checker); + const componentInfo = analyzeDefaultExportExpr(tsModule, defaultExportNode, checker); + const vueFileInfo: VueFileInfo = { + componentInfo + }; const defaultExportType = checker.getTypeAtLocation(defaultExportNode); const internalChildComponents = getChildComponents( @@ -47,15 +51,26 @@ export function getComponentInfo( const childComponents: ChildComponent[] = []; internalChildComponents.forEach(c => { childComponents.push({ + rawName: c.rawName, name: c.name, documentation: c.documentation, definition: c.definition, - info: c.defaultExportNode ? analyzeDefaultExportExpr(tsModule, c.defaultExportNode, checker) : undefined + info: c.defaultExportNode + ? { componentInfo: analyzeDefaultExportExpr(tsModule, c.defaultExportNode, checker) } + : undefined }); }); vueFileInfo.componentInfo.childComponents = childComponents; } + const importStatements = sourceFile.statements.filter(s => { + return tsModule.isImportDeclaration(s); + }); + + vueFileInfo.importStatementSrcs = importStatements.map(s => { + return s.getText(); + }); + return vueFileInfo; } @@ -63,7 +78,7 @@ export function analyzeDefaultExportExpr( tsModule: T_TypeScript, defaultExportNode: ts.Node, checker: ts.TypeChecker -): VueFileInfo { +): ComponentInfo { const defaultExportType = checker.getTypeAtLocation(defaultExportNode); const props = getProps(tsModule, defaultExportType, checker); @@ -72,12 +87,10 @@ export function analyzeDefaultExportExpr( const methods = getMethods(tsModule, defaultExportType, checker); return { - componentInfo: { - props, - data, - computed, - methods - } + props, + data, + computed, + methods }; } diff --git a/server/src/modes/template/interpolationMode.ts b/server/src/modes/template/interpolationMode.ts index 155ebb3da3..db99f185b3 100644 --- a/server/src/modes/template/interpolationMode.ts +++ b/server/src/modes/template/interpolationMode.ts @@ -74,13 +74,9 @@ export class VueInterpolationMode implements LanguageMode { document.getText() ); - const childComponents = this.config.vetur.validation.templateProps - ? this.vueInfoService && this.vueInfoService.getInfo(document)?.componentInfo.childComponents - : []; - const { templateService, templateSourceMap } = this.serviceHost.updateCurrentVirtualVueTextDocument( templateDoc, - childComponents + this.vueInfoService && this.vueInfoService.getInfo(document) ); if (!languageServiceIncludesFile(templateService, templateDoc.uri)) { diff --git a/server/src/services/typescriptService/bridge.ts b/server/src/services/typescriptService/bridge.ts index 4ad8202317..9f17ef69c4 100644 --- a/server/src/services/typescriptService/bridge.ts +++ b/server/src/services/typescriptService/bridge.ts @@ -1,4 +1,10 @@ -import { renderHelperName, componentHelperName, iterationHelperName, componentDataName } from './transformTemplate'; +import { + renderHelperName, + componentHelperName, + iterationHelperName, + componentDataName, + injectComponentDataName +} from './transformTemplate'; // This bridge file will be injected into TypeScript language service // it enable type checking and completion, yet still preserve precise option type @@ -38,24 +44,64 @@ export declare const ${iterationHelperName}: { export const preVue25Content = ` import Vue from 'vue'; +import type { ExtendedVue } from 'vue/types/vue' export interface GeneralOption extends Vue.ComponentOptions { [key: string]: any; } export default function bridge(t: T & GeneralOption): T { return t; } + +export const ${injectComponentDataName} = ( + instance: ExtendedVue +) => { + return instance as ExtendedVue & { + __vlsComponentData: { + props: Props & { [other: string]: any } + on: ComponentListeners> + directives: any[] + } + } +} ` + renderHelpers; export const vue25Content = ` import Vue from 'vue'; +import type { ExtendedVue } from 'vue/types/vue' const func = Vue.extend; export default func; + +export const ${injectComponentDataName} = ( + instance: ExtendedVue +) => { + return instance as ExtendedVue & { + __vlsComponentData: { + props: Props & { [other: string]: any } + on: ComponentListeners> + directives: any[] + } + } +} ` + renderHelpers; export const vue30Content = ` import { defineComponent } from 'vue'; +import type { Component, ComputedOptions, MethodOptions } from 'vue' + const func = defineComponent; export default func; + +export const ${injectComponentDataName} = ( + instance: Component +) => { + return instance as Component & { + __vlsComponentData: { + props: Props & { [other: string]: any } + on: ComponentListeners> + directives: any[] + } + } +} ` + renderHelpers; diff --git a/server/src/services/typescriptService/preprocess.ts b/server/src/services/typescriptService/preprocess.ts index 1dfe374f2d..07fbc38806 100644 --- a/server/src/services/typescriptService/preprocess.ts +++ b/server/src/services/typescriptService/preprocess.ts @@ -11,13 +11,14 @@ import { componentHelperName, iterationHelperName, renderHelperName, - componentDataName + componentDataName, + injectComponentDataName } from './transformTemplate'; import { templateSourceMap } from './serviceHost'; import { generateSourceMap } from './sourceMap'; import { isVirtualVueTemplateFile, isVueFile } from './util'; -import { ChildComponent } from '../vueInfoService'; -import { kebabCase, snakeCase } from 'lodash'; +import { ChildComponent, VueFileInfo } from '../vueInfoService'; +import { snakeCase } from 'lodash'; const importedComponentName = '__vlsComponent'; @@ -50,7 +51,7 @@ export function parseVueTemplate(text: string): string { return rawText.replace(/ {10}/, ''; } -export function createUpdater(tsModule: T_TypeScript, allChildComponentsInfo: Map) { +export function createUpdater(tsModule: T_TypeScript, allFileInfo: Map) { const clssf = tsModule.createLanguageServiceSourceFile; const ulssf = tsModule.updateLanguageServiceSourceFile; const scriptKindTracker = new WeakMap(); @@ -95,7 +96,10 @@ export function createUpdater(tsModule: T_TypeScript, allChildComponentsInfo: Ma const scriptSrc = parseVueScriptSrc(vueText); const program = parse(templateCode, { sourceType: 'module' }); - const childComponentNames = allChildComponentsInfo.get(vueTemplateFileName)?.map(c => snakeCase(c.name)); + const fileInfo = allFileInfo.get(vueTemplateFileName); + + const childComponentNames = fileInfo?.componentInfo?.childComponents?.map(c => snakeCase(c.name)) ?? []; + let expressions: ts.Expression[] = []; try { expressions = getTemplateTransformFunctions(tsModule, childComponentNames).transformTemplate( @@ -110,8 +114,12 @@ export function createUpdater(tsModule: T_TypeScript, allChildComponentsInfo: Ma let newText = printer.printFile(sourceFile); - if (allChildComponentsInfo.has(vueTemplateFileName)) { - const childComponents = allChildComponentsInfo.get(vueTemplateFileName)!; + if (fileInfo?.importStatementSrcs) { + newText += fileInfo.importStatementSrcs.join('\n'); + } + + if (allFileInfo.has(vueTemplateFileName)) { + const childComponents = allFileInfo.get(vueTemplateFileName)?.componentInfo?.childComponents ?? []; newText += convertChildComponentsInfoToSource(childComponents); } @@ -256,7 +264,8 @@ export function injectVueTemplate( tsModule.createImportSpecifier(undefined, tsModule.createIdentifier(renderHelperName)), tsModule.createImportSpecifier(undefined, tsModule.createIdentifier(componentHelperName)), tsModule.createImportSpecifier(undefined, tsModule.createIdentifier(iterationHelperName)), - tsModule.createImportSpecifier(undefined, tsModule.createIdentifier(componentDataName)) + tsModule.createImportSpecifier(undefined, tsModule.createIdentifier(componentDataName)), + tsModule.createImportSpecifier(undefined, tsModule.createIdentifier(injectComponentDataName)) ]) ), tsModule.createLiteral('vue-editor-bridge') @@ -302,39 +311,22 @@ function getWrapperRangeSetter( function convertChildComponentsInfoToSource(childComponents: ChildComponent[]) { let src = ''; childComponents.forEach(c => { - const componentDataInterfaceName = componentDataName + '__' + snakeCase(c.name); - const componentHelperInterfaceName = componentHelperName + '__' + snakeCase(c.name); - - const propTypeStrings: string[] = []; - c.info?.componentInfo.props?.forEach(p => { - let typeKey = kebabCase(p.name); - if (typeKey.includes('-')) { - typeKey = `'` + typeKey + `'`; - } - if (!p.required) { - typeKey += '?'; - } - - if (p.typeString) { - propTypeStrings.push(`${typeKey}: ${p.typeString}`); - } else { - propTypeStrings.push(`${typeKey}: any`); - } - }); - propTypeStrings.push('[other: string]: any'); + const snakeRawName = snakeCase(c.rawName); src += ` -interface ${componentDataInterfaceName} extends ${componentDataName} { - props: { ${propTypeStrings.join(', ')} } -} -declare const ${componentHelperInterfaceName}: { +const __wrapeedComponent__${snakeRawName} = __vlsInjectComponentData(${c.rawName}) +type __wrappedComponentDataType__${snakeRawName} = typeof __wrapeedComponent__${snakeRawName}.__vlsComponentData + +declare const ${componentHelperName}__${snakeRawName}: { ( vm: T, tag: string, - data: ${componentDataInterfaceName}> & ThisType, + data: __wrappedComponentDataType__${snakeRawName} & ThisType, children: any[] ): any -}`; +} + +`; }); return src; diff --git a/server/src/services/typescriptService/serviceHost.ts b/server/src/services/typescriptService/serviceHost.ts index 9b53203c0b..4ebb4d9b17 100644 --- a/server/src/services/typescriptService/serviceHost.ts +++ b/server/src/services/typescriptService/serviceHost.ts @@ -16,22 +16,19 @@ import { logger } from '../../log'; import { ModuleResolutionCache } from './moduleResolutionCache'; import { globalScope } from './transformTemplate'; import { inferVueVersion, VueVersion } from './vueVersion'; -import { ChildComponent } from '../vueInfoService'; +import { VueFileInfo } from '../vueInfoService'; const NEWLINE = process.platform === 'win32' ? '\r\n' : '\n'; /** * For prop validation */ -const allChildComponentsInfo = new Map(); +const allFileInfo = new Map(); function patchTS(tsModule: T_TypeScript) { // Patch typescript functions to insert `import Vue from 'vue'` and `new Vue` around export default. // NOTE: this is a global hack that all ts instances after is changed - const { createLanguageServiceSourceFile, updateLanguageServiceSourceFile } = createUpdater( - tsModule, - allChildComponentsInfo - ); + const { createLanguageServiceSourceFile, updateLanguageServiceSourceFile } = createUpdater(tsModule, allFileInfo); (tsModule as any).createLanguageServiceSourceFile = createLanguageServiceSourceFile; (tsModule as any).updateLanguageServiceSourceFile = updateLanguageServiceSourceFile; } @@ -58,7 +55,7 @@ export interface IServiceHost { queryVirtualFileInfo(fileName: string, currFileText: string): { source: string; sourceMapNodesString: string }; updateCurrentVirtualVueTextDocument( doc: TextDocument, - childComponents?: ChildComponent[] + fileInfo?: VueFileInfo ): { templateService: ts.LanguageService; templateSourceMap: TemplateSourceMap; @@ -141,7 +138,7 @@ export function getServiceHost( }; } - function updateCurrentVirtualVueTextDocument(doc: TextDocument, childComponents?: ChildComponent[]) { + function updateCurrentVirtualVueTextDocument(doc: TextDocument, fileInfo?: VueFileInfo) { const fileFsPath = getFileFsPath(doc.uri); const filePath = getFilePath(doc.uri); // When file is not in language service, add it @@ -154,8 +151,8 @@ export function getServiceHost( if (isVirtualVueTemplateFile(fileFsPath)) { localScriptRegionDocuments.set(fileFsPath, doc); scriptFileNameSet.add(filePath); - if (childComponents) { - allChildComponentsInfo.set(filePath, childComponents); + if (fileInfo) { + allFileInfo.set(filePath, fileInfo); } versions.set(fileFsPath, (versions.get(fileFsPath) || 0) + 1); projectVersion++; diff --git a/server/src/services/typescriptService/transformTemplate.ts b/server/src/services/typescriptService/transformTemplate.ts index 57f65e8bf5..ba6823506f 100644 --- a/server/src/services/typescriptService/transformTemplate.ts +++ b/server/src/services/typescriptService/transformTemplate.ts @@ -8,6 +8,7 @@ export const renderHelperName = '__vlsRenderHelper'; export const componentHelperName = '__vlsComponentHelper'; export const iterationHelperName = '__vlsIterationHelper'; export const componentDataName = '__vlsComponentData'; +export const injectComponentDataName = '__vlsInjectComponentData'; /** * Allowed global variables in templates. @@ -75,7 +76,7 @@ export function getTemplateTransformFunctions(ts: T_TypeScript, childComponentNa !hasUnhandledAttributes && childComponentNamesInSnakeCase && childComponentNamesInSnakeCase.indexOf(snakeCase(node.rawName)) !== -1 - ? ts.createIdentifier(componentHelperName + '__' + snakeCase(node.rawName)) + ? ts.createIdentifier(`${componentHelperName}__${snakeCase(node.rawName)}`) : ts.createIdentifier(componentHelperName); return ts.createCall(identifier, undefined, [ diff --git a/server/src/services/vueInfoService.ts b/server/src/services/vueInfoService.ts index aae61b58f9..35067fc9b4 100644 --- a/server/src/services/vueInfoService.ts +++ b/server/src/services/vueInfoService.ts @@ -9,9 +9,14 @@ import { LanguageModes } from '../embeddedSupport/languageModes'; */ export interface VueFileInfo { /** - * The defualt export component info from script section + * The default export component info from script section */ componentInfo: ComponentInfo; + + /** + * All imports in `