Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use real proptype for prop type validation. Fix #2343. Fix #2312 #2422

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 3 additions & 6 deletions server/src/modes/script/childComponents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { T_TypeScript } from '../../services/dependencyService';
import { kebabCase } from 'lodash';

interface InternalChildComponent {
rawName: string;
name: string;
documentation?: string;
definition?: {
Expand Down Expand Up @@ -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 =
Expand All @@ -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,
Expand Down
33 changes: 23 additions & 10 deletions server/src/modes/script/componentInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import {
ComputedInfo,
DataInfo,
MethodInfo,
ChildComponent
ChildComponent,
ComponentInfo
} from '../../services/vueInfoService';
import { getChildComponents } from './childComponents';
import { T_TypeScript } from '../../services/dependencyService';
Expand Down Expand Up @@ -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(
Expand All @@ -47,23 +51,34 @@ 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;
}

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);
Expand All @@ -72,12 +87,10 @@ export function analyzeDefaultExportExpr(
const methods = getMethods(tsModule, defaultExportType, checker);

return {
componentInfo: {
props,
data,
computed,
methods
}
props,
data,
computed,
methods
};
}

Expand Down
6 changes: 1 addition & 5 deletions server/src/modes/template/interpolationMode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
48 changes: 47 additions & 1 deletion server/src/services/typescriptService/bridge.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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<Vue> {
[key: string]: any;
}
export default function bridge<T>(t: T & GeneralOption): T {
return t;
}

export const ${injectComponentDataName} = <Instance extends Vue, Data, Methods, Computed, Props>(
instance: ExtendedVue<Instance, Data, Methods, Computed, Props>
) => {
return instance as ExtendedVue<Instance, Data, Methods, Computed, Props> & {
__vlsComponentData: {
props: Props & { [other: string]: any }
on: ComponentListeners<ExtendedVue<Instance, Data, Methods, Computed, Props>>
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 extends Vue, Data, Methods, Computed, Props>(
instance: ExtendedVue<Instance, Data, Methods, Computed, Props>
) => {
return instance as ExtendedVue<Instance, Data, Methods, Computed, Props> & {
__vlsComponentData: {
props: Props & { [other: string]: any }
on: ComponentListeners<ExtendedVue<Instance, Data, Methods, Computed, Props>>
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} = <Props, RawBindings, D, C extends ComputedOptions, M extends MethodOptions>(
instance: Component<Props, RawBindings, D, C, M>
) => {
return instance as Component<Props, RawBindings, D, C, M> & {
__vlsComponentData: {
props: Props & { [other: string]: any }
on: ComponentListeners<Component<Props, RawBindings, D, C, M>>
directives: any[]
}
}
}
` + renderHelpers;
60 changes: 26 additions & 34 deletions server/src/services/typescriptService/preprocess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -50,7 +51,7 @@ export function parseVueTemplate(text: string): string {
return rawText.replace(/ {10}/, '<template>') + '</template>';
}

export function createUpdater(tsModule: T_TypeScript, allChildComponentsInfo: Map<string, ChildComponent[]>) {
export function createUpdater(tsModule: T_TypeScript, allFileInfo: Map<string, VueFileInfo>) {
const clssf = tsModule.createLanguageServiceSourceFile;
const ulssf = tsModule.updateLanguageServiceSourceFile;
const scriptKindTracker = new WeakMap<ts.SourceFile, ts.ScriptKind | undefined>();
Expand Down Expand Up @@ -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(
Expand All @@ -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);
}

Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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}<T> extends ${componentDataName}<T> {
props: { ${propTypeStrings.join(', ')} }
}
declare const ${componentHelperInterfaceName}: {
const __wrapeedComponent__${snakeRawName} = __vlsInjectComponentData(${c.rawName})
type __wrappedComponentDataType__${snakeRawName} = typeof __wrapeedComponent__${snakeRawName}.__vlsComponentData

declare const ${componentHelperName}__${snakeRawName}: {
<T>(
vm: T,
tag: string,
data: ${componentDataInterfaceName}<Record<string, any>> & ThisType<T>,
data: __wrappedComponentDataType__${snakeRawName} & ThisType<T>,
children: any[]
): any
}`;
}

`;
});

return src;
Expand Down
17 changes: 7 additions & 10 deletions server/src/services/typescriptService/serviceHost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, ChildComponent[]>();
const allFileInfo = new Map<string, VueFileInfo>();

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;
}
Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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++;
Expand Down
3 changes: 2 additions & 1 deletion server/src/services/typescriptService/transformTemplate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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, [
Expand Down
Loading