From e98740d0a0de8205b127df8215d3f3f84523b549 Mon Sep 17 00:00:00 2001 From: ktsn Date: Tue, 14 Nov 2017 23:08:33 +0900 Subject: [PATCH 01/49] Move script mode integration test --- .../script/{test.ts => test/script-integration.ts} | 10 +++++----- server/test/mocha.opts | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) rename server/src/modes/script/{test.ts => test/script-integration.ts} (85%) diff --git a/server/src/modes/script/test.ts b/server/src/modes/script/test/script-integration.ts similarity index 85% rename from server/src/modes/script/test.ts rename to server/src/modes/script/test/script-integration.ts index f02caba372..bc92bbf4fb 100644 --- a/server/src/modes/script/test.ts +++ b/server/src/modes/script/test/script-integration.ts @@ -5,12 +5,12 @@ import * as fs from 'fs'; import { TextDocument } from 'vscode-languageserver-types'; import Uri from 'vscode-uri'; -import { getJavascriptMode } from './javascript'; -import { getLanguageModelCache } from '../languageModelCache'; -import { getDocumentRegions } from '../embeddedSupport'; -import { ComponentInfo } from './findComponents'; +import { getJavascriptMode } from '../javascript'; +import { getLanguageModelCache } from '../../languageModelCache'; +import { getDocumentRegions } from '../../embeddedSupport'; +import { ComponentInfo } from '../findComponents'; -const workspace = path.resolve(__dirname, '../../../test/fixtures/'); +const workspace = path.resolve(__dirname, '../../../../test/fixtures/'); const documentRegions = getLanguageModelCache(10, 60, document => getDocumentRegions(document)); const scriptMode = getJavascriptMode(documentRegions, workspace); diff --git a/server/test/mocha.opts b/server/test/mocha.opts index 13bcd1092a..a46ca1ee4d 100644 --- a/server/test/mocha.opts +++ b/server/test/mocha.opts @@ -5,4 +5,4 @@ ./dist/modes/template/test ./dist/modes/style/stylus/test ./dist/modes/test/ -./dist/modes/script/ +./dist/modes/script/test/ From e4b1549da7a20453ea5f051438d7861b45b1f6f5 Mon Sep 17 00:00:00 2001 From: ktsn Date: Sun, 26 Nov 2017 22:23:53 +0900 Subject: [PATCH 02/49] Provide diagnostics for template by using type info --- server/package.json | 4 +- server/src/modes/script/bridge.ts | 25 +- server/src/modes/script/javascript.ts | 35 ++- server/src/modes/script/preprocess.ts | 215 +++++++++++++++--- server/src/modes/script/serviceHost.ts | 30 ++- .../modes/script/test/script-integration.ts | 4 +- .../modes/script/test/template-integration.ts | 21 ++ server/src/typing.d.ts | 2 + server/test/fixtures/app.vue | 2 +- server/test/fixtures/component/comp3.vue | 17 ++ .../node_modules/@types/vue/index.d.ts | 41 +++- .../node_modules/@types/vue/options.d.ts | 178 +++++++++++++++ .../node_modules/@types/vue/plugin.d.ts | 8 + .../node_modules/@types/vue/vnode.d.ts | 69 ++++++ .../fixtures/node_modules/@types/vue/vue.d.ts | 122 ++++++++++ server/test/fixtures/package.json | 2 +- server/yarn.lock | 17 +- 17 files changed, 738 insertions(+), 54 deletions(-) create mode 100644 server/src/modes/script/test/template-integration.ts create mode 100644 server/test/fixtures/component/comp3.vue create mode 100644 server/test/fixtures/node_modules/@types/vue/options.d.ts create mode 100644 server/test/fixtures/node_modules/@types/vue/plugin.d.ts create mode 100644 server/test/fixtures/node_modules/@types/vue/vnode.d.ts create mode 100644 server/test/fixtures/node_modules/@types/vue/vue.d.ts diff --git a/server/package.json b/server/package.json index e04a7d15f2..31a2b1d697 100644 --- a/server/package.json +++ b/server/package.json @@ -39,7 +39,9 @@ "vscode-languageserver-types": "^3.5.0", "vscode-uri": "^1.0.1", "vue-onsenui-helper-json": "^1.0.2", - "vuetify-helper-json": "^1.0.0" + "vuetify-helper-json": "^1.0.0", + "vue-template-compiler": "^2.5.3", + "vue-template-es2015-compiler": "^1.6.0" }, "devDependencies": { "@types/glob": "^5.0.34", diff --git a/server/src/modes/script/bridge.ts b/server/src/modes/script/bridge.ts index 59002182c1..c8e810d34e 100644 --- a/server/src/modes/script/bridge.ts +++ b/server/src/modes/script/bridge.ts @@ -5,6 +5,26 @@ export const moduleName = 'vue-editor-bridge'; export const fileName = 'vue-temp/vue-editor-bridge.ts'; +const renderHelpers = ` +export interface RenderHelpers { + _o: Function + _n: Function + _s: Function + _l: Function + _t: Function + _q: Function + _i: Function + _m: Function + _f: Function + _k: Function + _b: Function + _v: Function + _e: Function + _u: Function + _c: Function + _self: this +}`; + export const oldContent = ` import Vue from 'vue'; export interface GeneralOption extends Vue.ComponentOptions { @@ -12,10 +32,11 @@ export interface GeneralOption extends Vue.ComponentOptions { } export default function bridge(t: T & GeneralOption): T { return t; -}`; +} +` + renderHelpers; export const content = ` import Vue from 'vue'; const func = Vue.extend; export default func; -`; +` + renderHelpers; diff --git a/server/src/modes/script/javascript.ts b/server/src/modes/script/javascript.ts index 81aea909d3..2f05d588f6 100644 --- a/server/src/modes/script/javascript.ts +++ b/server/src/modes/script/javascript.ts @@ -37,6 +37,7 @@ import { nullMode, NULL_SIGNATURE, NULL_COMPLETION } from '../nullMode'; export interface ScriptMode extends LanguageMode { findComponents(document: TextDocument): ComponentInfo[]; + doTemplateValidation(document: TextDocument): Diagnostic[]; } export function getJavascriptMode( @@ -44,7 +45,11 @@ export function getJavascriptMode( workspacePath: string | null | undefined ): ScriptMode { if (!workspacePath) { - return { ...nullMode, findComponents: () => [] }; + return { + ...nullMode, + findComponents: () => [], + doTemplateValidation: () => [] + }; } const jsDocuments = getLanguageModelCache(10, 60, document => { const vueDocument = documentRegions.get(document); @@ -89,6 +94,34 @@ export function getJavascriptMode( }; }); }, + doTemplateValidation(doc: TextDocument): Diagnostic[] { + // Add suffix to process this doc as vue template. + const templateDoc = TextDocument.create( + doc.uri + '.template', + doc.languageId, + doc.version, + doc.getText() + ); + + const { service } = updateCurrentTextDocument(templateDoc); + if (!languageServiceIncludesFile(service, templateDoc.uri)) { + return []; + } + + const fileFsPath = getFileFsPath(templateDoc.uri); + // We don't need syntactic diagnostics because + // compiled template is always valid JavaScript syntax. + const diagnostics = service.getSemanticDiagnostics(fileFsPath); + + return diagnostics.map(diag => { + return { + // TODO: provide correct position + range: Range.create(templateDoc.positionAt(0), templateDoc.positionAt(templateDoc.getText().length - 1)), + severity: DiagnosticSeverity.Error, + message: ts.flattenDiagnosticMessageText(diag.messageText, '\n') + }; + }); + }, doComplete(doc: TextDocument, position: Position): CompletionList { const { scriptDoc, service } = updateCurrentTextDocument(doc); if (!languageServiceIncludesFile(service, doc.uri)) { diff --git a/server/src/modes/script/preprocess.ts b/server/src/modes/script/preprocess.ts index 31ca0a34e0..d361494f77 100644 --- a/server/src/modes/script/preprocess.ts +++ b/server/src/modes/script/preprocess.ts @@ -1,6 +1,9 @@ import * as ts from 'typescript'; import * as path from 'path'; +import * as templateCompiler from 'vue-template-compiler'; +import * as templateTranspiler from 'vue-template-es2015-compiler'; + import { getDocumentRegions } from '../embeddedSupport'; import { TextDocument } from 'vscode-languageserver-types'; @@ -8,13 +11,29 @@ export function isVue(filename: string): boolean { return path.extname(filename) === '.vue'; } -export function parseVue(text: string): string { +export function isVueTemplate(path: string) { + return path.endsWith('.vue.template'); +} + +export function parseVueScript(text: string): string { const doc = TextDocument.create('test://test/test.vue', 'vue', 0, text); const regions = getDocumentRegions(doc); const script = regions.getEmbeddedDocumentByType('script'); return script.getText() || 'export default {};'; } +export function parseVueTemplate(text: string): string { + const doc = TextDocument.create('test://test/test.vue', 'vue', 0, text); + const regions = getDocumentRegions(doc); + const template = regions.getEmbeddedDocumentByType('template'); + + // TODO: support other template format + if (template.languageId !== 'vue-html') { + return ''; + } + return transformVueTemplate(template.getText()); +} + function isTSLike(scriptKind: ts.ScriptKind | undefined) { return scriptKind === ts.ScriptKind.TS || scriptKind === ts.ScriptKind.TSX; } @@ -22,44 +41,56 @@ function isTSLike(scriptKind: ts.ScriptKind | undefined) { export function createUpdater() { const clssf = ts.createLanguageServiceSourceFile; const ulssf = ts.updateLanguageServiceSourceFile; - return { - createLanguageServiceSourceFile( - fileName: string, - scriptSnapshot: ts.IScriptSnapshot, - scriptTarget: ts.ScriptTarget, - version: string, - setNodeParents: boolean, - scriptKind?: ts.ScriptKind - ): ts.SourceFile { - const sourceFile = clssf(fileName, scriptSnapshot, scriptTarget, version, setNodeParents, scriptKind); - // store scriptKind info on sourceFile - const hackSourceFile: any = sourceFile; - hackSourceFile.__scriptKind = scriptKind; + + function modifySourceFile(fileName: string, sourceFile: ts.SourceFile, scriptKind?: ts.ScriptKind): void { + // store scriptKind info on sourceFile + const hackSourceFile: any = sourceFile; + hackSourceFile.__scriptKind = scriptKind; + + if (!hackSourceFile.__modified) { if (isVue(fileName) && !isTSLike(scriptKind)) { - modifyVueSource(sourceFile); - } - return sourceFile; - }, - updateLanguageServiceSourceFile( - sourceFile: ts.SourceFile, - scriptSnapshot: ts.IScriptSnapshot, - version: string, - textChangeRange: ts.TextChangeRange, - aggressiveChecks?: boolean - ): ts.SourceFile { - let hackSourceFile: any = sourceFile; - const scriptKind = hackSourceFile.__scriptKind; - sourceFile = hackSourceFile = ulssf(sourceFile, scriptSnapshot, version, textChangeRange, aggressiveChecks); - if (isVue(sourceFile.fileName) && !isTSLike(scriptKind)) { - modifyVueSource(sourceFile); + modifyVueScript(sourceFile); + } else if (isVueTemplate(fileName)) { + modifyRender(sourceFile); } - hackSourceFile.__scriptKind = scriptKind; - return sourceFile; + hackSourceFile.__modified = true; } + } + + function createLanguageServiceSourceFile( + fileName: string, + scriptSnapshot: ts.IScriptSnapshot, + scriptTarget: ts.ScriptTarget, + version: string, + setNodeParents: boolean, + scriptKind?: ts.ScriptKind + ): ts.SourceFile { + const sourceFile = clssf(fileName, scriptSnapshot, scriptTarget, version, setNodeParents, scriptKind); + modifySourceFile(fileName, sourceFile, scriptKind); + return sourceFile; + } + + function updateLanguageServiceSourceFile( + sourceFile: ts.SourceFile, + scriptSnapshot: ts.IScriptSnapshot, + version: string, + textChangeRange: ts.TextChangeRange, + aggressiveChecks?: boolean + ): ts.SourceFile { + const hackSourceFile: any = sourceFile; + const scriptKind = hackSourceFile.__scriptKind; + sourceFile = ulssf(sourceFile, scriptSnapshot, version, textChangeRange, aggressiveChecks); + modifySourceFile(sourceFile.fileName, sourceFile, scriptKind); + return sourceFile; + } + + return { + createLanguageServiceSourceFile, + updateLanguageServiceSourceFile }; } -function modifyVueSource(sourceFile: ts.SourceFile): void { +function modifyVueScript(sourceFile: ts.SourceFile): void { const exportDefaultObject = sourceFile.statements.find( st => st.kind === ts.SyntaxKind.ExportAssignment && @@ -93,6 +124,124 @@ function modifyVueSource(sourceFile: ts.SourceFile): void { } } +/** + * Wrap render function with component options in the script block + * to validate its types + */ +function modifyRender(sourceFile: ts.SourceFile): void { + annotateArguments(sourceFile); + + // 1. add import statement for corresponding Vue file + // so that we acquire the component type from it. + const setZeroPos = getWrapperRangeSetter({ pos: 0, end: 0 }); + const vueFilePath = './' + path.basename(sourceFile.fileName.slice(0, -9)); + const componentImport = setZeroPos(ts.createImportDeclaration(undefined, + undefined, + setZeroPos(ts.createImportClause(ts.createIdentifier('__Component'), undefined)), + setZeroPos(ts.createLiteral(vueFilePath)) + )); + + // import helper type to handle Vue's private methods + const helperImport = setZeroPos(ts.createImportDeclaration(undefined, + undefined, + setZeroPos(ts.createImportClause(undefined, + setZeroPos(ts.createNamedImports([ + setZeroPos(ts.createImportSpecifier( + setZeroPos(ts.createIdentifier('RenderHelpers')), + setZeroPos(ts.createIdentifier('__VueRenderHelpers')) + )) + ])) + )), + setZeroPos(ts.createLiteral('vue-editor-bridge')) + )); + + // 2. add a variable declaration of the component instance + const setMinPos = getWrapperRangeSetter({ pos: 0, end: 1 }); + const component = setZeroPos(ts.createVariableStatement(undefined, [ + setZeroPos(ts.createVariableDeclaration('__component', undefined, + setZeroPos(ts.createNew( + // we need set 1 or more length for identifier node to acquire a type from it. + setMinPos(ts.createIdentifier('__Component')), + undefined, + undefined + )) + )) + ])); + + // 3. wrap render code with a function decralation + // with `this` type of component. + const setRenderPos = getWrapperRangeSetter(sourceFile); + const renderElement = setRenderPos(ts.createFunctionDeclaration(undefined, undefined, undefined, + '__render', + undefined, + [setZeroPos(ts.createParameter(undefined, undefined, undefined, + 'this', + undefined, + setZeroPos(ts.createIntersectionTypeNode([ + setZeroPos(ts.createTypeReferenceNode( + setMinPos(ts.createIdentifier('__VueRenderHelpers')), + undefined + )), + setZeroPos(ts.createTypeQueryNode( + setMinPos(ts.createIdentifier('__component')) + )) + ])) + ))], + undefined, + setRenderPos(ts.createBlock(sourceFile.statements)) + )); + + // 4. replace the original statements with wrapped code. + sourceFile.statements = setRenderPos(ts.createNodeArray([ + componentImport, + helperImport, + component, + renderElement + ])); +} + +/** + * Transform Vue template block to JavaScript code + * to analyze template expression with type information. + */ +function transformVueTemplate(template: string): string { + const compiled = templateCompiler.compile(template); + + // TODO: handle errors + if (compiled.errors.length > 0) { + throw compiled.errors; + } + + // We only need render function to type check. + return transpileWithWrap(compiled.render); +} + +/** + * Annotate all function argument type with `any` + * to avoid implicit any error. + */ +function annotateArguments(node: ts.Node): void { + ts.forEachChild(node, function next(node) { + switch (node.kind) { + case ts.SyntaxKind.FunctionExpression: + const fn = node as ts.FunctionExpression; + fn.parameters.forEach(param => { + param.type = ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword); + }); + default: + ts.forEachChild(node, next); + } + }); +} + +function transpileWithWrap(code: string): string { + const pre = '(function(){'; + const post = '})()'; + return templateTranspiler(pre + code + post) + .slice(pre.length) + .slice(0, -post.length); +} + /** Create a function that calls setTextRange on synthetic wrapper nodes that need a valid range */ function getWrapperRangeSetter(wrapped: ts.TextRange): (wrapperNode: T) => T { return (wrapperNode: T) => ts.setTextRange(wrapperNode, wrapped); diff --git a/server/src/modes/script/serviceHost.ts b/server/src/modes/script/serviceHost.ts index 2f1e79a7a0..4f7230900c 100644 --- a/server/src/modes/script/serviceHost.ts +++ b/server/src/modes/script/serviceHost.ts @@ -5,7 +5,7 @@ import { TextDocument } from 'vscode-languageserver-types'; import * as parseGitIgnore from 'parse-gitignore'; import { LanguageModelCache } from '../languageModelCache'; -import { createUpdater, parseVue, isVue } from './preprocess'; +import { createUpdater, parseVueScript, parseVueTemplate, isVue, isVueTemplate } from './preprocess'; import { getFileFsPath, getFilePath } from '../../utils/paths'; import * as bridge from './bridge'; @@ -30,16 +30,22 @@ const vueSys: ts.System = { if (isVueProject(path)) { return ts.sys.fileExists(path.slice(0, -3)); } + if (isVueTemplate(path)) { + return ts.sys.fileExists(path.slice(0, -9)); + } return ts.sys.fileExists(path); }, readFile(path, encoding) { if (isVueProject(path)) { const fileText = ts.sys.readFile(path.slice(0, -3), encoding); - return fileText ? parseVue(fileText) : fileText; - } else { - const fileText = ts.sys.readFile(path, encoding); - return fileText; + return fileText ? parseVueScript(fileText) : fileText; + } + if (isVueTemplate(path)) { + const fileText = ts.sys.readFile(path.slice(0, -9), encoding); + return fileText ? parseVueTemplate(fileText) : fileText; } + const fileText = ts.sys.readFile(path, encoding); + return fileText; } }; @@ -49,6 +55,9 @@ if (ts.sys.realpath) { if (isVueProject(path)) { return realpath(path.slice(0, -3)) + '.ts'; } + if (isVueTemplate(path)) { + return realpath(path.slice(0, -9)) + '.ts'; + } return realpath(path); }; } @@ -110,11 +119,14 @@ export function getServiceHost(workspacePath: string, jsDocuments: LanguageModel const filePath = getFilePath(doc.uri); // When file is not in language service, add it if (!scriptDocs.has(fileFsPath)) { - if (fileFsPath.endsWith('.vue')) { + if (fileFsPath.endsWith('.vue') || fileFsPath.endsWith('.vue.template')) { files.push(filePath); } } - if (!currentScriptDoc || doc.uri !== currentScriptDoc.uri || doc.version !== currentScriptDoc.version) { + if (isVueTemplate(fileFsPath)) { + scriptDocs.set(fileFsPath, doc); + versions.set(fileFsPath, (versions.get(fileFsPath) || 0) + 1); + } else if (!currentScriptDoc || doc.uri !== currentScriptDoc.uri || doc.version !== currentScriptDoc.version) { currentScriptDoc = jsDocuments.get(doc); const lastDoc = scriptDocs.get(fileFsPath); if (lastDoc && currentScriptDoc.languageId !== lastDoc.languageId) { @@ -217,7 +229,9 @@ export function getServiceHost(workspacePath: string, jsDocuments: LanguageModel if (!doc && isVue(fileName)) { // Note: This is required in addition to the parsing in embeddedSupport because // this works for .vue files that aren't even loaded by VS Code yet. - fileText = parseVue(fileText); + fileText = parseVueScript(fileText); + } else if (isVueTemplate(fileName)) { + fileText = parseVueTemplate(fileText); } return { getText: (start, end) => fileText.substring(start, end), diff --git a/server/src/modes/script/test/script-integration.ts b/server/src/modes/script/test/script-integration.ts index bc92bbf4fb..85693759ec 100644 --- a/server/src/modes/script/test/script-integration.ts +++ b/server/src/modes/script/test/script-integration.ts @@ -14,7 +14,7 @@ const workspace = path.resolve(__dirname, '../../../../test/fixtures/'); const documentRegions = getLanguageModelCache(10, 60, document => getDocumentRegions(document)); const scriptMode = getJavascriptMode(documentRegions, workspace); -suite('integrated test', () => { +suite('script integrated test', () => { const filenames = glob.sync(workspace + '/**/*.vue'); for (const filename of filenames) { const doc = createTextDocument(filename); @@ -47,7 +47,7 @@ function testProps(components: ComponentInfo[]) { assert.deepEqual(comp4.props, [{ name: 'inline', doc: 'Number' }]); } -function createTextDocument(filename: string): TextDocument { +export function createTextDocument(filename: string): TextDocument { const uri = Uri.file(filename).toString(); const content = fs.readFileSync(filename, 'utf-8'); return TextDocument.create(uri, 'vue', 0, content); diff --git a/server/src/modes/script/test/template-integration.ts b/server/src/modes/script/test/template-integration.ts new file mode 100644 index 0000000000..c8a88308ce --- /dev/null +++ b/server/src/modes/script/test/template-integration.ts @@ -0,0 +1,21 @@ +import * as assert from 'assert'; +import * as path from 'path'; + +import { getJavascriptMode } from '../javascript'; +import { getLanguageModelCache } from '../../languageModelCache'; +import { getDocumentRegions } from '../../embeddedSupport'; +import { createTextDocument } from './script-integration'; + +const workspace = path.resolve(__dirname, '../../../../test/fixtures/'); +const documentRegions = getLanguageModelCache(10, 60, document => getDocumentRegions(document)); +const scriptMode = getJavascriptMode(documentRegions, workspace); + +suite('template integrated test', () => { + test('validate: comp3.vue', () => { + const filename = path.join(workspace + '/component/comp3.vue'); + const doc = createTextDocument(filename); + const diagnostics = scriptMode.doTemplateValidation(doc); + assert.equal(diagnostics.length, 1, 'diagnostic count'); + assert(/Property 'messaage' does not exist/.test(diagnostics[0].message), 'diagnostic message'); + }); +}); diff --git a/server/src/typing.d.ts b/server/src/typing.d.ts index 42ea40bfc9..6c05238633 100644 --- a/server/src/typing.d.ts +++ b/server/src/typing.d.ts @@ -21,6 +21,8 @@ declare module 'eslint' { } } +declare module 'vue-template-compiler'; +declare module 'vue-template-es2015-compiler'; declare module 'eslint-plugin-vue'; declare module 'vscode-emmet-helper'; declare module 'parse-gitignore'; diff --git a/server/test/fixtures/app.vue b/server/test/fixtures/app.vue index d3d3ad2892..b5bdfcdb32 100644 --- a/server/test/fixtures/app.vue +++ b/server/test/fixtures/app.vue @@ -4,7 +4,7 @@ import Comp2 from '@comp/comp2.vue'; import test from 'mod'; Comp.data(); -Comp2.props; +Comp2.nextTick(); test.test.toFixed(); myGlobal.toFixed(); diff --git a/server/test/fixtures/component/comp3.vue b/server/test/fixtures/component/comp3.vue new file mode 100644 index 0000000000..a852d4d831 --- /dev/null +++ b/server/test/fixtures/component/comp3.vue @@ -0,0 +1,17 @@ + + + diff --git a/server/test/fixtures/node_modules/@types/vue/index.d.ts b/server/test/fixtures/node_modules/@types/vue/index.d.ts index 7bbda6c0d1..720180d2a5 100644 --- a/server/test/fixtures/node_modules/@types/vue/index.d.ts +++ b/server/test/fixtures/node_modules/@types/vue/index.d.ts @@ -1,4 +1,37 @@ -declare module 'vue' { - var exp: any; - export default exp; -} +import { Vue } from "./vue"; + +export default Vue; + +export { + CreateElement, + VueConstructor +} from "./vue"; + +export { + Component, + AsyncComponent, + ComponentOptions, + FunctionalComponentOptions, + RenderContext, + PropOptions, + ComputedOptions, + WatchHandler, + WatchOptions, + WatchOptionsWithHandler, + DirectiveFunction, + DirectiveOptions +} from "./options"; + +export { + PluginFunction, + PluginObject +} from "./plugin"; + +export { + VNodeChildren, + VNodeChildrenArrayContents, + VNode, + VNodeComponentOptions, + VNodeData, + VNodeDirective +} from "./vnode"; diff --git a/server/test/fixtures/node_modules/@types/vue/options.d.ts b/server/test/fixtures/node_modules/@types/vue/options.d.ts new file mode 100644 index 0000000000..c4d822f69e --- /dev/null +++ b/server/test/fixtures/node_modules/@types/vue/options.d.ts @@ -0,0 +1,178 @@ +import { Vue, CreateElement, CombinedVueInstance } from "./vue"; +import { VNode, VNodeData, VNodeDirective } from "./vnode"; + +type Constructor = { + new (...args: any[]): any; +} + +// we don't support infer props in async component +export type Component, Methods=DefaultMethods, Computed=DefaultComputed, Props=DefaultProps> = + | typeof Vue + | FunctionalComponentOptions + | ThisTypedComponentOptionsWithArrayProps + | ThisTypedComponentOptionsWithRecordProps; + +interface EsModuleComponent { + default: Component +} + +export type AsyncComponent, Methods=DefaultMethods, Computed=DefaultComputed, Props=DefaultProps> = ( + resolve: (component: Component) => void, + reject: (reason?: any) => void +) => Promise | void; + +/** + * When the `Computed` type parameter on `ComponentOptions` is inferred, + * it should have a property with the return type of every get-accessor. + * Since there isn't a way to query for the return type of a function, we allow TypeScript + * to infer from the shape of `Accessors` and work backwards. + */ +export type Accessors = { + [K in keyof T]: (() => T[K]) | ComputedOptions +} + +/** + * This type should be used when an array of strings is used for a component's `props` value. + */ +export type ThisTypedComponentOptionsWithArrayProps = + object & + ComponentOptions> & V) => Data), Methods, Computed, PropNames[]> & + ThisType>>>; + +/** + * This type should be used when an object mapped to `PropOptions` is used for a component's `props` value. + */ +export type ThisTypedComponentOptionsWithRecordProps = + object & + ComponentOptions & V) => Data), Methods, Computed, RecordPropsDefinition> & + ThisType>>; + +type DefaultData = object | ((this: V) => object); +type DefaultProps = Record; +type DefaultMethods = { [key: string]: (this: V, ...args: any[]) => any }; +type DefaultComputed = { [key: string]: any }; +export interface ComponentOptions< + V extends Vue, + Data=DefaultData, + Methods=DefaultMethods, + Computed=DefaultComputed, + PropsDef=PropsDefinition> { + data?: Data; + props?: PropsDef; + propsData?: Object; + computed?: Accessors; + methods?: Methods; + watch?: Record | WatchHandler | string>; + + el?: Element | String; + template?: string; + render?(createElement: CreateElement): VNode; + renderError?: (h: () => VNode, err: Error) => VNode; + staticRenderFns?: ((createElement: CreateElement) => VNode)[]; + + beforeCreate?(this: V): void; + created?(): void; + beforeDestroy?(): void; + destroyed?(): void; + beforeMount?(): void; + mounted?(): void; + beforeUpdate?(): void; + updated?(): void; + activated?(): void; + deactivated?(): void; + errorCaptured?(): boolean | void; + + directives?: { [key: string]: DirectiveFunction | DirectiveOptions }; + components?: { [key: string]: Component | AsyncComponent }; + transitions?: { [key: string]: Object }; + filters?: { [key: string]: Function }; + + provide?: Object | (() => Object); + inject?: InjectOptions; + + model?: { + prop?: string; + event?: string; + }; + + parent?: Vue; + mixins?: (ComponentOptions | typeof Vue)[]; + name?: string; + // TODO: support properly inferred 'extends' + extends?: ComponentOptions | typeof Vue; + delimiters?: [string, string]; + comments?: boolean; + inheritAttrs?: boolean; +} + +export interface FunctionalComponentOptions> { + name?: string; + props?: PropDefs; + inject?: InjectOptions; + functional: boolean; + render(this: undefined, createElement: CreateElement, context: RenderContext): VNode; +} + +export interface RenderContext { + props: Props; + children: VNode[]; + slots(): any; + data: VNodeData; + parent: Vue; + injections: any +} + +export type Prop = { (): T } | { new (...args: any[]): T & object } + +export type PropValidator = PropOptions | Prop | Prop[]; + +export interface PropOptions { + type?: Prop | Prop[]; + required?: boolean; + default?: T | null | undefined | (() => object); + validator?(value: T): boolean; +} + +export type RecordPropsDefinition = { + [K in keyof T]: PropValidator +} +export type ArrayPropsDefinition = (keyof T)[]; +export type PropsDefinition = ArrayPropsDefinition | RecordPropsDefinition; + +export interface ComputedOptions { + get?(): T; + set?(value: T): void; + cache?: boolean; +} + +export type WatchHandler = (val: T, oldVal: T) => void; + +export interface WatchOptions { + deep?: boolean; + immediate?: boolean; +} + +export interface WatchOptionsWithHandler extends WatchOptions { + handler: WatchHandler; +} + +export type DirectiveFunction = ( + el: HTMLElement, + binding: VNodeDirective, + vnode: VNode, + oldVnode: VNode +) => void; + +export interface DirectiveOptions { + bind?: DirectiveFunction; + inserted?: DirectiveFunction; + update?: DirectiveFunction; + componentUpdated?: DirectiveFunction; + unbind?: DirectiveFunction; +} + +export type InjectKey = string | symbol; + +export type InjectOptions = { + [key: string]: InjectKey | { from?: InjectKey, default?: any } +} | string[]; diff --git a/server/test/fixtures/node_modules/@types/vue/plugin.d.ts b/server/test/fixtures/node_modules/@types/vue/plugin.d.ts new file mode 100644 index 0000000000..5741f862c5 --- /dev/null +++ b/server/test/fixtures/node_modules/@types/vue/plugin.d.ts @@ -0,0 +1,8 @@ +import { Vue as _Vue } from "./vue"; + +export type PluginFunction = (Vue: typeof _Vue, options?: T) => void; + +export interface PluginObject { + install: PluginFunction; + [key: string]: any; +} diff --git a/server/test/fixtures/node_modules/@types/vue/vnode.d.ts b/server/test/fixtures/node_modules/@types/vue/vnode.d.ts new file mode 100644 index 0000000000..ae72065f9b --- /dev/null +++ b/server/test/fixtures/node_modules/@types/vue/vnode.d.ts @@ -0,0 +1,69 @@ +import { Vue } from "./vue"; + +export type ScopedSlot = (props: any) => VNodeChildrenArrayContents | string; + +export type VNodeChildren = VNodeChildrenArrayContents | [ScopedSlot] | string; +export interface VNodeChildrenArrayContents { + [x: number]: VNode | string | VNodeChildren; +} + +export interface VNode { + tag?: string; + data?: VNodeData; + children?: VNode[]; + text?: string; + elm?: Node; + ns?: string; + context?: Vue; + key?: string | number; + componentOptions?: VNodeComponentOptions; + componentInstance?: Vue; + parent?: VNode; + raw?: boolean; + isStatic?: boolean; + isRootInsert: boolean; + isComment: boolean; +} + +export interface VNodeComponentOptions { + Ctor: typeof Vue; + propsData?: Object; + listeners?: Object; + children?: VNodeChildren; + tag?: string; +} + +export interface VNodeData { + key?: string | number; + slot?: string; + scopedSlots?: { [key: string]: ScopedSlot }; + ref?: string; + tag?: string; + staticClass?: string; + class?: any; + staticStyle?: { [key: string]: any }; + style?: Object[] | Object; + props?: { [key: string]: any }; + attrs?: { [key: string]: any }; + domProps?: { [key: string]: any }; + hook?: { [key: string]: Function }; + on?: { [key: string]: Function | Function[] }; + nativeOn?: { [key: string]: Function | Function[] }; + transition?: Object; + show?: boolean; + inlineTemplate?: { + render: Function; + staticRenderFns: Function[]; + }; + directives?: VNodeDirective[]; + keepAlive?: boolean; +} + +export interface VNodeDirective { + readonly name: string; + readonly value: any; + readonly oldValue: any; + readonly expression: any; + readonly arg: string; + readonly modifiers: { [key: string]: boolean }; +} diff --git a/server/test/fixtures/node_modules/@types/vue/vue.d.ts b/server/test/fixtures/node_modules/@types/vue/vue.d.ts new file mode 100644 index 0000000000..2b025150bc --- /dev/null +++ b/server/test/fixtures/node_modules/@types/vue/vue.d.ts @@ -0,0 +1,122 @@ +import { + Component, + AsyncComponent, + ComponentOptions, + FunctionalComponentOptions, + WatchOptionsWithHandler, + WatchHandler, + DirectiveOptions, + DirectiveFunction, + RecordPropsDefinition, + ThisTypedComponentOptionsWithArrayProps, + ThisTypedComponentOptionsWithRecordProps, + WatchOptions, +} from "./options"; +import { VNode, VNodeData, VNodeChildren, ScopedSlot } from "./vnode"; +import { PluginFunction, PluginObject } from "./plugin"; + +export interface CreateElement { + (tag?: string | Component | AsyncComponent, children?: VNodeChildren): VNode; + (tag?: string | Component | AsyncComponent, data?: VNodeData, children?: VNodeChildren): VNode; +} + +export interface Vue { + readonly $el: HTMLElement; + readonly $options: ComponentOptions; + readonly $parent: Vue; + readonly $root: Vue; + readonly $children: Vue[]; + readonly $refs: { [key: string]: Vue | Element | Vue[] | Element[] }; + readonly $slots: { [key: string]: VNode[] }; + readonly $scopedSlots: { [key: string]: ScopedSlot }; + readonly $isServer: boolean; + readonly $data: Record; + readonly $props: Record; + readonly $ssrContext: any; + readonly $vnode: VNode; + readonly $attrs: Record; + readonly $listeners: Record; + + $mount(elementOrSelector?: Element | String, hydrating?: boolean): this; + $forceUpdate(): void; + $destroy(): void; + $set: typeof Vue.set; + $delete: typeof Vue.delete; + $watch( + expOrFn: string, + callback: (this: this, n: any, o: any) => void, + options?: WatchOptions + ): (() => void); + $watch( + expOrFn: (this: this) => T, + callback: (this: this, n: T, o: T) => void, + options?: WatchOptions + ): (() => void); + $on(event: string | string[], callback: Function): this; + $once(event: string, callback: Function): this; + $off(event?: string | string[], callback?: Function): this; + $emit(event: string, ...args: any[]): this; + $nextTick(callback: (this: this) => void): void; + $nextTick(): Promise; + $createElement: CreateElement; +} + +export type CombinedVueInstance = Data & Methods & Computed & Props & Instance; +export type ExtendedVue = VueConstructor & Vue>; + +export interface VueConstructor { + new (options?: ThisTypedComponentOptionsWithArrayProps): CombinedVueInstance>; + // ideally, the return type should just contains Props, not Record. But TS requires Base constructors must all have the same return type. + new (options?: ThisTypedComponentOptionsWithRecordProps): CombinedVueInstance>; + new (options?: ComponentOptions): CombinedVueInstance>; + + extend(definition: FunctionalComponentOptions, PropNames[]>): ExtendedVue>; + extend(definition: FunctionalComponentOptions>): ExtendedVue; + extend(options?: ThisTypedComponentOptionsWithArrayProps): ExtendedVue>; + extend(options?: ThisTypedComponentOptionsWithRecordProps): ExtendedVue; + extend(options?: ComponentOptions): ExtendedVue; + + nextTick(callback: () => void, context?: any[]): void; + nextTick(): Promise + set(object: Object, key: string, value: T): T; + set(array: T[], key: number, value: T): T; + delete(object: Object, key: string): void; + delete(array: T[], key: number): void; + + directive( + id: string, + definition?: DirectiveOptions | DirectiveFunction + ): DirectiveOptions; + filter(id: string, definition?: Function): Function; + + component(id: string): VueConstructor; + component(id: string, constructor: VC): VC; + component(id: string, definition: AsyncComponent): ExtendedVue; + component(id: string, definition: FunctionalComponentOptions, PropNames[]>): ExtendedVue>; + component(id: string, definition: FunctionalComponentOptions>): ExtendedVue; + component(id: string, definition?: ThisTypedComponentOptionsWithArrayProps): ExtendedVue>; + component(id: string, definition?: ThisTypedComponentOptionsWithRecordProps): ExtendedVue; + component(id: string, definition?: ComponentOptions): ExtendedVue; + + use(plugin: PluginObject | PluginFunction, options?: T): void; + use(plugin: PluginObject | PluginFunction, ...options: any[]): void; + mixin(mixin: VueConstructor | ComponentOptions): void; + compile(template: string): { + render(createElement: typeof Vue.prototype.$createElement): VNode; + staticRenderFns: (() => VNode)[]; + }; + + config: { + silent: boolean; + optionMergeStrategies: any; + devtools: boolean; + productionTip: boolean; + performance: boolean; + errorHandler(err: Error, vm: Vue, info: string): void; + warnHandler(msg: string, vm: Vue, trace: string): void; + ignoredElements: (string | RegExp)[]; + keyCodes: { [key: string]: number | number[] }; + } +} + +export const Vue: VueConstructor; diff --git a/server/test/fixtures/package.json b/server/test/fixtures/package.json index 03bf75d776..ab12da4503 100644 --- a/server/test/fixtures/package.json +++ b/server/test/fixtures/package.json @@ -4,7 +4,7 @@ "description": "", "main": "index.js", "dependencies": { - "vue": "^2.4.0" + "vue": "^2.5.0" }, "devDependencies": {}, "scripts": { diff --git a/server/yarn.lock b/server/yarn.lock index fd348cae85..5b6383d18d 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -464,6 +464,10 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" +de-indent@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d" + debug@*, debug@^2.6.8: version "2.6.8" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc" @@ -988,7 +992,7 @@ hawk@3.1.3, hawk@~3.1.3: hoek "2.x.x" sntp "1.x.x" -he@1.1.1: +he@1.1.1, he@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" @@ -2265,6 +2269,17 @@ vue-onsenui-helper-json@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/vue-onsenui-helper-json/-/vue-onsenui-helper-json-1.0.2.tgz#b8c900fe3f89ba6a318335de73a55dee99a1846e" +vue-template-compiler@^2.5.3: + version "2.5.3" + resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.5.3.tgz#ab631b0694e211a6aaf0d800102b37836aae36a4" + dependencies: + de-indent "^1.0.2" + he "^1.1.0" + +vue-template-es2015-compiler@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.6.0.tgz#dc42697133302ce3017524356a6c61b7b69b4a18" + vuetify-helper-json@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/vuetify-helper-json/-/vuetify-helper-json-1.0.0.tgz#f429a6e6156bab865f9d6c79aa6739eb187dd778" From dc9d8c18d42387e79a5dde254a8b4948a1c79e6c Mon Sep 17 00:00:00 2001 From: ktsn Date: Tue, 12 Dec 2017 15:29:04 +0900 Subject: [PATCH 03/49] [wip] template diagnostics --- server/src/modes/languageModes.ts | 8 +- server/src/modes/script/bridge.ts | 24 +-- server/src/modes/script/javascript.ts | 4 +- server/src/modes/script/preprocess.ts | 96 ++++-------- server/src/modes/script/serviceHost.ts | 20 ++- .../modes/script/test/script-integration.ts | 4 +- .../modes/script/test/template-integration.ts | 4 +- server/src/modes/script/transformTemplate.ts | 140 ++++++++++++++++++ server/src/modes/template/index.ts | 5 +- server/src/typing.d.ts | 2 - server/src/utils/paths.ts | 4 + 11 files changed, 208 insertions(+), 103 deletions(-) create mode 100644 server/src/modes/script/transformTemplate.ts diff --git a/server/src/modes/languageModes.ts b/server/src/modes/languageModes.ts index def5dd5412..7215b7cc8e 100644 --- a/server/src/modes/languageModes.ts +++ b/server/src/modes/languageModes.ts @@ -28,6 +28,7 @@ import { getJavascriptMode } from './script/javascript'; import { getVueHTMLMode } from './template'; import { getStylusMode } from './style/stylus'; +import { parseHTMLDocument, HTMLDocument } from './template/parser/htmlParser'; export interface LanguageMode { getId(): string; @@ -67,14 +68,15 @@ export interface LanguageModeRange extends Range { export function getLanguageModes(workspacePath: string | null | undefined): LanguageModes { const documentRegions = getLanguageModelCache(10, 60, document => getDocumentRegions(document)); + const vueDocuments = getLanguageModelCache(10, 60, document => parseHTMLDocument(document)); let modelCaches: LanguageModelCache[] = []; modelCaches.push(documentRegions); - const jsMode = getJavascriptMode(documentRegions, workspacePath); - let modes: { [k: string]: LanguageMode } = { + const jsMode = getJavascriptMode(documentRegions, vueDocuments, workspacePath); + let modes: {[k: string]: LanguageMode} = { vue: getVueMode(), - 'vue-html': getVueHTMLMode(documentRegions, workspacePath, jsMode), + 'vue-html': getVueHTMLMode(documentRegions, vueDocuments, workspacePath, jsMode), css: getCSSMode(documentRegions), postcss: getPostCSSMode(documentRegions), scss: getSCSSMode(documentRegions), diff --git a/server/src/modes/script/bridge.ts b/server/src/modes/script/bridge.ts index c8e810d34e..05749fd041 100644 --- a/server/src/modes/script/bridge.ts +++ b/server/src/modes/script/bridge.ts @@ -1,3 +1,5 @@ +import { componentHelperName } from './transformTemplate'; + // this bridge file will be injected into TypeScript service // it enable type checking and completion, yet still preserve precise option type @@ -6,24 +8,10 @@ export const moduleName = 'vue-editor-bridge'; export const fileName = 'vue-temp/vue-editor-bridge.ts'; const renderHelpers = ` -export interface RenderHelpers { - _o: Function - _n: Function - _s: Function - _l: Function - _t: Function - _q: Function - _i: Function - _m: Function - _f: Function - _k: Function - _b: Function - _v: Function - _e: Function - _u: Function - _c: Function - _self: this -}`; +export interface ${componentHelperName} { + (tag: string, data: any, children: any[]): any; +} +`; export const oldContent = ` import Vue from 'vue'; diff --git a/server/src/modes/script/javascript.ts b/server/src/modes/script/javascript.ts index 2f05d588f6..12b43a7c89 100644 --- a/server/src/modes/script/javascript.ts +++ b/server/src/modes/script/javascript.ts @@ -34,6 +34,7 @@ import * as ts from 'typescript'; import * as _ from 'lodash'; import { nullMode, NULL_SIGNATURE, NULL_COMPLETION } from '../nullMode'; +import { HTMLDocument } from '../template/parser/htmlParser'; export interface ScriptMode extends LanguageMode { findComponents(document: TextDocument): ComponentInfo[]; @@ -42,6 +43,7 @@ export interface ScriptMode extends LanguageMode { export function getJavascriptMode( documentRegions: LanguageModelCache, + vueDocuments: LanguageModelCache, workspacePath: string | null | undefined ): ScriptMode { if (!workspacePath) { @@ -61,7 +63,7 @@ export function getJavascriptMode( return vueDocument.getLanguageRangeByType('script'); }); - const serviceHost = getServiceHost(workspacePath, jsDocuments); + const serviceHost = getServiceHost(workspacePath, jsDocuments, vueDocuments); const { updateCurrentTextDocument, getScriptDocByFsPath } = serviceHost; let config: any = {}; diff --git a/server/src/modes/script/preprocess.ts b/server/src/modes/script/preprocess.ts index d361494f77..258b7796e8 100644 --- a/server/src/modes/script/preprocess.ts +++ b/server/src/modes/script/preprocess.ts @@ -1,11 +1,12 @@ import * as ts from 'typescript'; import * as path from 'path'; -import * as templateCompiler from 'vue-template-compiler'; -import * as templateTranspiler from 'vue-template-es2015-compiler'; - import { getDocumentRegions } from '../embeddedSupport'; import { TextDocument } from 'vscode-languageserver-types'; +import { getFileUri } from '../../utils/paths'; +import { HTMLDocument } from '../template/parser/htmlParser'; +import { LanguageModelCache } from '../languageModelCache'; +import { transformTemplate, componentHelperName } from './transformTemplate'; export function isVue(filename: string): boolean { return path.extname(filename) === '.vue'; @@ -31,18 +32,24 @@ export function parseVueTemplate(text: string): string { if (template.languageId !== 'vue-html') { return ''; } - return transformVueTemplate(template.getText()); + return template.getText(); } function isTSLike(scriptKind: ts.ScriptKind | undefined) { return scriptKind === ts.ScriptKind.TS || scriptKind === ts.ScriptKind.TSX; } -export function createUpdater() { +export function createUpdater(vueDocuments: LanguageModelCache) { const clssf = ts.createLanguageServiceSourceFile; const ulssf = ts.updateLanguageServiceSourceFile; - function modifySourceFile(fileName: string, sourceFile: ts.SourceFile, scriptKind?: ts.ScriptKind): void { + function modifySourceFile( + fileName: string, + sourceFile: ts.SourceFile, + scriptSnapshot: ts.IScriptSnapshot, + version: string, + scriptKind?: ts.ScriptKind + ): void { // store scriptKind info on sourceFile const hackSourceFile: any = sourceFile; hackSourceFile.__scriptKind = scriptKind; @@ -51,7 +58,13 @@ export function createUpdater() { if (isVue(fileName) && !isTSLike(scriptKind)) { modifyVueScript(sourceFile); } else if (isVueTemplate(fileName)) { - modifyRender(sourceFile); + const doc = TextDocument.create( + getFileUri(fileName), + 'vue', + Number(version), + scriptSnapshot.getText(0, scriptSnapshot.getLength()) + ); + injectVueTemplate(sourceFile, vueDocuments.get(doc)); } hackSourceFile.__modified = true; } @@ -66,7 +79,7 @@ export function createUpdater() { scriptKind?: ts.ScriptKind ): ts.SourceFile { const sourceFile = clssf(fileName, scriptSnapshot, scriptTarget, version, setNodeParents, scriptKind); - modifySourceFile(fileName, sourceFile, scriptKind); + modifySourceFile(fileName, sourceFile, scriptSnapshot, version, scriptKind); return sourceFile; } @@ -80,7 +93,7 @@ export function createUpdater() { const hackSourceFile: any = sourceFile; const scriptKind = hackSourceFile.__scriptKind; sourceFile = ulssf(sourceFile, scriptSnapshot, version, textChangeRange, aggressiveChecks); - modifySourceFile(sourceFile.fileName, sourceFile, scriptKind); + modifySourceFile(sourceFile.fileName, sourceFile, scriptSnapshot, version, scriptKind); return sourceFile; } @@ -128,9 +141,7 @@ function modifyVueScript(sourceFile: ts.SourceFile): void { * Wrap render function with component options in the script block * to validate its types */ -function modifyRender(sourceFile: ts.SourceFile): void { - annotateArguments(sourceFile); - +function injectVueTemplate(sourceFile: ts.SourceFile, html: HTMLDocument): void { // 1. add import statement for corresponding Vue file // so that we acquire the component type from it. const setZeroPos = getWrapperRangeSetter({ pos: 0, end: 0 }); @@ -147,8 +158,8 @@ function modifyRender(sourceFile: ts.SourceFile): void { setZeroPos(ts.createImportClause(undefined, setZeroPos(ts.createNamedImports([ setZeroPos(ts.createImportSpecifier( - setZeroPos(ts.createIdentifier('RenderHelpers')), - setZeroPos(ts.createIdentifier('__VueRenderHelpers')) + undefined, + setZeroPos(ts.createIdentifier(componentHelperName)) )) ])) )), @@ -171,24 +182,19 @@ function modifyRender(sourceFile: ts.SourceFile): void { // 3. wrap render code with a function decralation // with `this` type of component. const setRenderPos = getWrapperRangeSetter(sourceFile); + const statements = transformTemplate(html).map(exp => ts.createStatement(exp)); const renderElement = setRenderPos(ts.createFunctionDeclaration(undefined, undefined, undefined, '__render', undefined, [setZeroPos(ts.createParameter(undefined, undefined, undefined, 'this', undefined, - setZeroPos(ts.createIntersectionTypeNode([ - setZeroPos(ts.createTypeReferenceNode( - setMinPos(ts.createIdentifier('__VueRenderHelpers')), - undefined - )), - setZeroPos(ts.createTypeQueryNode( - setMinPos(ts.createIdentifier('__component')) - )) - ])) + setZeroPos(setZeroPos(ts.createTypeQueryNode( + setMinPos(ts.createIdentifier('__component')) + ))) ))], undefined, - setRenderPos(ts.createBlock(sourceFile.statements)) + setRenderPos(ts.createBlock(statements)) )); // 4. replace the original statements with wrapped code. @@ -200,48 +206,6 @@ function modifyRender(sourceFile: ts.SourceFile): void { ])); } -/** - * Transform Vue template block to JavaScript code - * to analyze template expression with type information. - */ -function transformVueTemplate(template: string): string { - const compiled = templateCompiler.compile(template); - - // TODO: handle errors - if (compiled.errors.length > 0) { - throw compiled.errors; - } - - // We only need render function to type check. - return transpileWithWrap(compiled.render); -} - -/** - * Annotate all function argument type with `any` - * to avoid implicit any error. - */ -function annotateArguments(node: ts.Node): void { - ts.forEachChild(node, function next(node) { - switch (node.kind) { - case ts.SyntaxKind.FunctionExpression: - const fn = node as ts.FunctionExpression; - fn.parameters.forEach(param => { - param.type = ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword); - }); - default: - ts.forEachChild(node, next); - } - }); -} - -function transpileWithWrap(code: string): string { - const pre = '(function(){'; - const post = '})()'; - return templateTranspiler(pre + code + post) - .slice(pre.length) - .slice(0, -post.length); -} - /** Create a function that calls setTextRange on synthetic wrapper nodes that need a valid range */ function getWrapperRangeSetter(wrapped: ts.TextRange): (wrapperNode: T) => T { return (wrapperNode: T) => ts.setTextRange(wrapperNode, wrapped); diff --git a/server/src/modes/script/serviceHost.ts b/server/src/modes/script/serviceHost.ts index 4f7230900c..469f5c60fd 100644 --- a/server/src/modes/script/serviceHost.ts +++ b/server/src/modes/script/serviceHost.ts @@ -5,9 +5,10 @@ import { TextDocument } from 'vscode-languageserver-types'; import * as parseGitIgnore from 'parse-gitignore'; import { LanguageModelCache } from '../languageModelCache'; -import { createUpdater, parseVueScript, parseVueTemplate, isVue, isVueTemplate } from './preprocess'; +import { createUpdater, parseVueScript, isVue, isVueTemplate } from './preprocess'; import { getFileFsPath, getFilePath } from '../../utils/paths'; import * as bridge from './bridge'; +import { HTMLDocument } from '../template/parser/htmlParser'; function isVueProject(path: string) { return path.endsWith('.vue.ts') && !path.includes('node_modules'); @@ -41,8 +42,8 @@ const vueSys: ts.System = { return fileText ? parseVueScript(fileText) : fileText; } if (isVueTemplate(path)) { - const fileText = ts.sys.readFile(path.slice(0, -9), encoding); - return fileText ? parseVueTemplate(fileText) : fileText; + // The template is parsed in the preprocess phase + return ts.sys.readFile(path.slice(0, -9), encoding); } const fileText = ts.sys.readFile(path, encoding); return fileText; @@ -80,7 +81,11 @@ function inferIsOldVersion(workspacePath: string): boolean { } } -export function getServiceHost(workspacePath: string, jsDocuments: LanguageModelCache) { +export function getServiceHost( + workspacePath: string, + jsDocuments: LanguageModelCache, + vueDocuments: LanguageModelCache +) { let compilerOptions: ts.CompilerOptions = { allowNonTsExtensions: true, allowJs: true, @@ -97,7 +102,7 @@ export function getServiceHost(workspacePath: string, jsDocuments: LanguageModel // Patch typescript functions to insert `import Vue from 'vue'` and `new Vue` around export default. // NOTE: Typescript 2.3 should add an API to allow this, and then this code should use that API. - const { createLanguageServiceSourceFile, updateLanguageServiceSourceFile } = createUpdater(); + const { createLanguageServiceSourceFile, updateLanguageServiceSourceFile } = createUpdater(vueDocuments); (ts as any).createLanguageServiceSourceFile = createLanguageServiceSourceFile; (ts as any).updateLanguageServiceSourceFile = updateLanguageServiceSourceFile; const configFilename = @@ -125,7 +130,8 @@ export function getServiceHost(workspacePath: string, jsDocuments: LanguageModel } if (isVueTemplate(fileFsPath)) { scriptDocs.set(fileFsPath, doc); - versions.set(fileFsPath, (versions.get(fileFsPath) || 0) + 1); + // The version must be the same as doc version + versions.set(fileFsPath, doc.version); } else if (!currentScriptDoc || doc.uri !== currentScriptDoc.uri || doc.version !== currentScriptDoc.version) { currentScriptDoc = jsDocuments.get(doc); const lastDoc = scriptDocs.get(fileFsPath); @@ -230,8 +236,6 @@ export function getServiceHost(workspacePath: string, jsDocuments: LanguageModel // Note: This is required in addition to the parsing in embeddedSupport because // this works for .vue files that aren't even loaded by VS Code yet. fileText = parseVueScript(fileText); - } else if (isVueTemplate(fileName)) { - fileText = parseVueTemplate(fileText); } return { getText: (start, end) => fileText.substring(start, end), diff --git a/server/src/modes/script/test/script-integration.ts b/server/src/modes/script/test/script-integration.ts index 85693759ec..d40cec6fa4 100644 --- a/server/src/modes/script/test/script-integration.ts +++ b/server/src/modes/script/test/script-integration.ts @@ -9,10 +9,12 @@ import { getJavascriptMode } from '../javascript'; import { getLanguageModelCache } from '../../languageModelCache'; import { getDocumentRegions } from '../../embeddedSupport'; import { ComponentInfo } from '../findComponents'; +import { parseHTMLDocument } from '../../template/parser/htmlParser'; const workspace = path.resolve(__dirname, '../../../../test/fixtures/'); const documentRegions = getLanguageModelCache(10, 60, document => getDocumentRegions(document)); -const scriptMode = getJavascriptMode(documentRegions, workspace); +const vueDocuments = getLanguageModelCache(10, 60, document => parseHTMLDocument(document)); +const scriptMode = getJavascriptMode(documentRegions, vueDocuments, workspace); suite('script integrated test', () => { const filenames = glob.sync(workspace + '/**/*.vue'); diff --git a/server/src/modes/script/test/template-integration.ts b/server/src/modes/script/test/template-integration.ts index c8a88308ce..d67bc5bac2 100644 --- a/server/src/modes/script/test/template-integration.ts +++ b/server/src/modes/script/test/template-integration.ts @@ -5,10 +5,12 @@ import { getJavascriptMode } from '../javascript'; import { getLanguageModelCache } from '../../languageModelCache'; import { getDocumentRegions } from '../../embeddedSupport'; import { createTextDocument } from './script-integration'; +import { parseHTMLDocument } from '../../template/parser/htmlParser'; const workspace = path.resolve(__dirname, '../../../../test/fixtures/'); const documentRegions = getLanguageModelCache(10, 60, document => getDocumentRegions(document)); -const scriptMode = getJavascriptMode(documentRegions, workspace); +const vueDocuments = getLanguageModelCache(10, 60, document => parseHTMLDocument(document)); +const scriptMode = getJavascriptMode(documentRegions, vueDocuments, workspace); suite('template integrated test', () => { test('validate: comp3.vue', () => { diff --git a/server/src/modes/script/transformTemplate.ts b/server/src/modes/script/transformTemplate.ts new file mode 100644 index 0000000000..84d36ce292 --- /dev/null +++ b/server/src/modes/script/transformTemplate.ts @@ -0,0 +1,140 @@ +import * as ts from 'typescript'; +import { HTMLDocument, Node, Attribute, Directive, Expression, Range } from '../template/parser/htmlParser'; + +export const componentHelperName = '__veturComponentHelper'; + +/** + * Transform template AST to TypeScript AST. + * Note: The returned TS AST is not compatible with + * the regular Vue render function and does not work on runtime + * because we just need type information for the template. + */ +export function transformTemplate(html: HTMLDocument): ts.Expression[] { + const template = html.roots.find(node => { + return node.tag === 'template'; + }); + return template ? template.children.map(transformNode) : []; +} + +function transformNode(node: Node): ts.Expression { + return setTextRange(ts.createCall( + ts.setTextRange(ts.createIdentifier(componentHelperName), { pos: 0, end: 0 }), + undefined, + [ + // Element / Component name + ts.createLiteral(JSON.stringify(node.tag)), + + // Attributes / Directives + transformAttributes(node.attributes, node.directives), + + // Children + transformChildren(node.children) + ] + ), node); +} + +function transformAttributes( + attributes: Record | undefined, + directives: Record | undefined +): ts.Expression { + const literalProps = !attributes ? [] : Object.keys(attributes).map(key => { + const attr = attributes[key]; + return setTextRange(ts.createPropertyAssignment( + setTextRange(ts.createIdentifier(attr.name.text), attr.name), + attr.value + ? setTextRange(ts.createLiteral(JSON.stringify(attr.value.text)), attr.value) + : ts.createLiteral('true') + ), attr); + }); + + + const boundProps = (!directives || !directives['v-bind']) ? [] : directives['v-bind'].map(bind => { + const name = bind.key.argument; + const exp = bind.value ? parseExpression(bind.value) : ts.createLiteral('true'); + + if (name) { + return setTextRange(ts.createPropertyAssignment( + setTextRange(ts.createIdentifier(name), bind.key), + exp + ), bind); + } else { + return setTextRange(ts.createSpreadAssignment(exp), bind); + } + }); + + + const listeners = (!directives || !directives['v-on']) ? [] : directives['v-on'].map(listener => { + const name = listener.key.argument; + let exp = listener.value ? parseExpression(listener.value) : ts.createLiteral('true'); + + if (exp.kind === ts.SyntaxKind.CallExpression) { + exp = ts.createFunctionExpression(undefined, undefined, undefined, undefined, + [ts.createParameter(undefined, undefined, undefined, + '$event', + undefined, + ts.createTypeReferenceNode('Event', undefined) + )], + undefined, + ts.createBlock([ + ts.createReturn(exp) + ]) + ); + } + + if (name) { + return setTextRange(ts.createPropertyAssignment( + setTextRange(ts.createIdentifier(name), listener.key), + exp + ), listener); + } else { + return setTextRange(ts.createSpreadAssignment(exp), listener); + } + }); + + return ts.createObjectLiteral([ + ts.createPropertyAssignment('props', ts.createObjectLiteral( + [...literalProps, ...boundProps] + )), + ts.createPropertyAssignment('on', ts.createObjectLiteral(listeners)) + ]); +} + +function transformChildren(children: Node[]): ts.Expression { + return ts.createArrayLiteral(children.map(transformNode)); +} + +function parseExpression(expression: Expression): ts.Expression { + const source = ts.createSourceFile('/tmp/parsed.ts', expression.expression, ts.ScriptTarget.Latest); + const statement = source.statements[0]; + + if (statement.kind !== ts.SyntaxKind.ExpressionStatement) { + console.error('Unexpected statement kind:', statement.kind); + return ts.createLiteral('""'); + } + + ts.forEachChild(statement, function next(node) { + ts.setTextRange(node, { + pos: expression.start + node.pos, + end: expression.start + node.end + }); + ts.forEachChild(node, next); + }); + + return injectThisForIdentifier((statement as ts.ExpressionStatement).expression, []); +} + +function injectThisForIdentifier(expression: ts.Expression, scope: ts.Identifier[]): ts.Expression { + switch (expression.kind) { + case ts.SyntaxKind.Identifier: + return ts.createPropertyAccess(ts.createThis(), expression as ts.Identifier); + default: + return expression; + } +} + +function setTextRange(range: T, location: Range): T { + return ts.setTextRange(range, { + pos: location.start, + end: location.end + }); +} diff --git a/server/src/modes/template/index.ts b/server/src/modes/template/index.ts index fd21f35248..215e02c103 100644 --- a/server/src/modes/template/index.ts +++ b/server/src/modes/template/index.ts @@ -11,7 +11,6 @@ import { findDocumentHighlights } from './services/htmlHighlighting'; import { findDocumentLinks } from './services/htmlLinks'; import { findDocumentSymbols } from './services/htmlSymbolsProvider'; import { htmlFormat } from './services/htmlFormat'; -import { parseHTMLDocument } from './parser/htmlParser'; import { doValidation, createLintEngine } from './services/htmlValidation'; import { findDefinition } from './services/htmlDefinition'; import { getTagProviderSettings } from './tagProviders'; @@ -24,6 +23,7 @@ type DocumentRegionCache = LanguageModelCache; export function getVueHTMLMode( documentRegions: DocumentRegionCache, + vueDocuments: LanguageModelCache, workspacePath: string | null | undefined, scriptMode: ScriptMode ): LanguageMode { @@ -32,7 +32,6 @@ export function getVueHTMLMode( const embeddedDocuments = getLanguageModelCache(10, 60, document => documentRegions.get(document).getEmbeddedDocument('vue-html') ); - const vueDocuments = getLanguageModelCache(10, 60, document => parseHTMLDocument(document)); const lintEngine = createLintEngine(); let config: any = {}; @@ -47,7 +46,7 @@ export function getVueHTMLMode( }, doValidation(document) { const embedded = embeddedDocuments.get(document); - return doValidation(embedded, lintEngine); + return doValidation(embedded, lintEngine).concat(scriptMode.doTemplateValidation(document)); }, doComplete(document: TextDocument, position: Position) { const embedded = embeddedDocuments.get(document); diff --git a/server/src/typing.d.ts b/server/src/typing.d.ts index 6c05238633..42ea40bfc9 100644 --- a/server/src/typing.d.ts +++ b/server/src/typing.d.ts @@ -21,8 +21,6 @@ declare module 'eslint' { } } -declare module 'vue-template-compiler'; -declare module 'vue-template-es2015-compiler'; declare module 'eslint-plugin-vue'; declare module 'vscode-emmet-helper'; declare module 'parse-gitignore'; diff --git a/server/src/utils/paths.ts b/server/src/utils/paths.ts index f46213e7f1..50aa87319e 100644 --- a/server/src/utils/paths.ts +++ b/server/src/utils/paths.ts @@ -14,3 +14,7 @@ export function getFilePath(documentUri: string): string { return Uri.parse(documentUri).path; } } + +export function getFileUri(documentPath: string): string { + return Uri.file(documentPath).toString(); +} \ No newline at end of file From 912b1342d1eeac2746b6aaffb2977f6329c1f58f Mon Sep 17 00:00:00 2001 From: ktsn Date: Fri, 15 Dec 2017 14:12:19 +0900 Subject: [PATCH 04/49] Use vue-eslint-parser --- server/package.json | 2 ++ server/yarn.lock | 14 ++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/server/package.json b/server/package.json index 31a2b1d697..74bcab1362 100644 --- a/server/package.json +++ b/server/package.json @@ -40,10 +40,12 @@ "vscode-uri": "^1.0.1", "vue-onsenui-helper-json": "^1.0.2", "vuetify-helper-json": "^1.0.0", + "vue-eslint-parser": "^2.0.1-beta.2", "vue-template-compiler": "^2.5.3", "vue-template-es2015-compiler": "^1.6.0" }, "devDependencies": { + "@types/estree": "^0.0.38", "@types/glob": "^5.0.34", "@types/js-beautify": "0.0.31", "@types/lodash": "^4.14.91", diff --git a/server/yarn.lock b/server/yarn.lock index 5b6383d18d..8a379a9372 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -6,6 +6,10 @@ version "0.1.2" resolved "https://registry.yarnpkg.com/@emmetio/extract-abbreviation/-/extract-abbreviation-0.1.2.tgz#e1f1c06349f4b1a00241ba1ba8a719062f9194b0" +"@types/estree@^0.0.38": + version "0.0.38" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.38.tgz#c1be40aa933723c608820a99a373a16d215a1ca2" + "@types/events@*": version "1.1.0" resolved "https://registry.yarnpkg.com/@types/events/-/events-1.1.0.tgz#93b1be91f63c184450385272c47b6496fd028e02" @@ -2265,6 +2269,16 @@ vue-eslint-parser@^2.0.1: esquery "^1.0.0" lodash "^4.17.4" +vue-eslint-parser@^2.0.1-beta.2: + version "2.0.1-beta.2" + resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-2.0.1-beta.2.tgz#82e5130ac18ae4b0c894a7c831b55ec92478fa2f" + dependencies: + debug "^3.1.0" + eslint-scope "^3.7.1" + espree "^3.5.1" + esquery "^1.0.0" + lodash "^4.17.4" + vue-onsenui-helper-json@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/vue-onsenui-helper-json/-/vue-onsenui-helper-json-1.0.2.tgz#b8c900fe3f89ba6a318335de73a55dee99a1846e" From a68726e3dec6a284054590cecd5b692db2e26cbf Mon Sep 17 00:00:00 2001 From: ktsn Date: Sun, 7 Jan 2018 16:21:24 +0900 Subject: [PATCH 05/49] transform vue-eslint-parser ast to ts ast --- server/src/modes/languageModes.ts | 8 +- server/src/modes/script/javascript.ts | 4 +- server/src/modes/script/preprocess.ts | 24 ++- server/src/modes/script/serviceHost.ts | 6 +- .../modes/script/test/script-integration.ts | 4 +- .../modes/script/test/template-integration.ts | 4 +- server/src/modes/script/transformTemplate.ts | 167 ++++++++++++------ server/src/modes/template/index.ts | 4 +- 8 files changed, 133 insertions(+), 88 deletions(-) diff --git a/server/src/modes/languageModes.ts b/server/src/modes/languageModes.ts index 7215b7cc8e..def5dd5412 100644 --- a/server/src/modes/languageModes.ts +++ b/server/src/modes/languageModes.ts @@ -28,7 +28,6 @@ import { getJavascriptMode } from './script/javascript'; import { getVueHTMLMode } from './template'; import { getStylusMode } from './style/stylus'; -import { parseHTMLDocument, HTMLDocument } from './template/parser/htmlParser'; export interface LanguageMode { getId(): string; @@ -68,15 +67,14 @@ export interface LanguageModeRange extends Range { export function getLanguageModes(workspacePath: string | null | undefined): LanguageModes { const documentRegions = getLanguageModelCache(10, 60, document => getDocumentRegions(document)); - const vueDocuments = getLanguageModelCache(10, 60, document => parseHTMLDocument(document)); let modelCaches: LanguageModelCache[] = []; modelCaches.push(documentRegions); - const jsMode = getJavascriptMode(documentRegions, vueDocuments, workspacePath); - let modes: {[k: string]: LanguageMode} = { + const jsMode = getJavascriptMode(documentRegions, workspacePath); + let modes: { [k: string]: LanguageMode } = { vue: getVueMode(), - 'vue-html': getVueHTMLMode(documentRegions, vueDocuments, workspacePath, jsMode), + 'vue-html': getVueHTMLMode(documentRegions, workspacePath, jsMode), css: getCSSMode(documentRegions), postcss: getPostCSSMode(documentRegions), scss: getSCSSMode(documentRegions), diff --git a/server/src/modes/script/javascript.ts b/server/src/modes/script/javascript.ts index 12b43a7c89..2f05d588f6 100644 --- a/server/src/modes/script/javascript.ts +++ b/server/src/modes/script/javascript.ts @@ -34,7 +34,6 @@ import * as ts from 'typescript'; import * as _ from 'lodash'; import { nullMode, NULL_SIGNATURE, NULL_COMPLETION } from '../nullMode'; -import { HTMLDocument } from '../template/parser/htmlParser'; export interface ScriptMode extends LanguageMode { findComponents(document: TextDocument): ComponentInfo[]; @@ -43,7 +42,6 @@ export interface ScriptMode extends LanguageMode { export function getJavascriptMode( documentRegions: LanguageModelCache, - vueDocuments: LanguageModelCache, workspacePath: string | null | undefined ): ScriptMode { if (!workspacePath) { @@ -63,7 +61,7 @@ export function getJavascriptMode( return vueDocument.getLanguageRangeByType('script'); }); - const serviceHost = getServiceHost(workspacePath, jsDocuments, vueDocuments); + const serviceHost = getServiceHost(workspacePath, jsDocuments); const { updateCurrentTextDocument, getScriptDocByFsPath } = serviceHost; let config: any = {}; diff --git a/server/src/modes/script/preprocess.ts b/server/src/modes/script/preprocess.ts index 258b7796e8..4754549345 100644 --- a/server/src/modes/script/preprocess.ts +++ b/server/src/modes/script/preprocess.ts @@ -1,11 +1,9 @@ import * as ts from 'typescript'; import * as path from 'path'; +import { parse } from 'vue-eslint-parser'; import { getDocumentRegions } from '../embeddedSupport'; import { TextDocument } from 'vscode-languageserver-types'; -import { getFileUri } from '../../utils/paths'; -import { HTMLDocument } from '../template/parser/htmlParser'; -import { LanguageModelCache } from '../languageModelCache'; import { transformTemplate, componentHelperName } from './transformTemplate'; export function isVue(filename: string): boolean { @@ -39,7 +37,7 @@ function isTSLike(scriptKind: ts.ScriptKind | undefined) { return scriptKind === ts.ScriptKind.TS || scriptKind === ts.ScriptKind.TSX; } -export function createUpdater(vueDocuments: LanguageModelCache) { +export function createUpdater() { const clssf = ts.createLanguageServiceSourceFile; const ulssf = ts.updateLanguageServiceSourceFile; @@ -57,16 +55,14 @@ export function createUpdater(vueDocuments: LanguageModelCache) { if (!hackSourceFile.__modified) { if (isVue(fileName) && !isTSLike(scriptKind)) { modifyVueScript(sourceFile); + hackSourceFile.__modified = true; } else if (isVueTemplate(fileName)) { - const doc = TextDocument.create( - getFileUri(fileName), - 'vue', - Number(version), - scriptSnapshot.getText(0, scriptSnapshot.getLength()) - ); - injectVueTemplate(sourceFile, vueDocuments.get(doc)); + const code = scriptSnapshot.getText(0, scriptSnapshot.getLength()); + const program = parse(code, { sourceType: 'module' }); + const tsCode = transformTemplate(program, code); + injectVueTemplate(sourceFile, tsCode); + hackSourceFile.__modified = true; } - hackSourceFile.__modified = true; } } @@ -141,7 +137,7 @@ function modifyVueScript(sourceFile: ts.SourceFile): void { * Wrap render function with component options in the script block * to validate its types */ -function injectVueTemplate(sourceFile: ts.SourceFile, html: HTMLDocument): void { +function injectVueTemplate(sourceFile: ts.SourceFile, renderBlock: ts.Expression[]): void { // 1. add import statement for corresponding Vue file // so that we acquire the component type from it. const setZeroPos = getWrapperRangeSetter({ pos: 0, end: 0 }); @@ -182,7 +178,7 @@ function injectVueTemplate(sourceFile: ts.SourceFile, html: HTMLDocument): void // 3. wrap render code with a function decralation // with `this` type of component. const setRenderPos = getWrapperRangeSetter(sourceFile); - const statements = transformTemplate(html).map(exp => ts.createStatement(exp)); + const statements = renderBlock.map(exp => ts.createStatement(exp)); const renderElement = setRenderPos(ts.createFunctionDeclaration(undefined, undefined, undefined, '__render', undefined, diff --git a/server/src/modes/script/serviceHost.ts b/server/src/modes/script/serviceHost.ts index 469f5c60fd..c45f644722 100644 --- a/server/src/modes/script/serviceHost.ts +++ b/server/src/modes/script/serviceHost.ts @@ -8,7 +8,6 @@ import { LanguageModelCache } from '../languageModelCache'; import { createUpdater, parseVueScript, isVue, isVueTemplate } from './preprocess'; import { getFileFsPath, getFilePath } from '../../utils/paths'; import * as bridge from './bridge'; -import { HTMLDocument } from '../template/parser/htmlParser'; function isVueProject(path: string) { return path.endsWith('.vue.ts') && !path.includes('node_modules'); @@ -83,8 +82,7 @@ function inferIsOldVersion(workspacePath: string): boolean { export function getServiceHost( workspacePath: string, - jsDocuments: LanguageModelCache, - vueDocuments: LanguageModelCache + jsDocuments: LanguageModelCache ) { let compilerOptions: ts.CompilerOptions = { allowNonTsExtensions: true, @@ -102,7 +100,7 @@ export function getServiceHost( // Patch typescript functions to insert `import Vue from 'vue'` and `new Vue` around export default. // NOTE: Typescript 2.3 should add an API to allow this, and then this code should use that API. - const { createLanguageServiceSourceFile, updateLanguageServiceSourceFile } = createUpdater(vueDocuments); + const { createLanguageServiceSourceFile, updateLanguageServiceSourceFile } = createUpdater(); (ts as any).createLanguageServiceSourceFile = createLanguageServiceSourceFile; (ts as any).updateLanguageServiceSourceFile = updateLanguageServiceSourceFile; const configFilename = diff --git a/server/src/modes/script/test/script-integration.ts b/server/src/modes/script/test/script-integration.ts index d40cec6fa4..85693759ec 100644 --- a/server/src/modes/script/test/script-integration.ts +++ b/server/src/modes/script/test/script-integration.ts @@ -9,12 +9,10 @@ import { getJavascriptMode } from '../javascript'; import { getLanguageModelCache } from '../../languageModelCache'; import { getDocumentRegions } from '../../embeddedSupport'; import { ComponentInfo } from '../findComponents'; -import { parseHTMLDocument } from '../../template/parser/htmlParser'; const workspace = path.resolve(__dirname, '../../../../test/fixtures/'); const documentRegions = getLanguageModelCache(10, 60, document => getDocumentRegions(document)); -const vueDocuments = getLanguageModelCache(10, 60, document => parseHTMLDocument(document)); -const scriptMode = getJavascriptMode(documentRegions, vueDocuments, workspace); +const scriptMode = getJavascriptMode(documentRegions, workspace); suite('script integrated test', () => { const filenames = glob.sync(workspace + '/**/*.vue'); diff --git a/server/src/modes/script/test/template-integration.ts b/server/src/modes/script/test/template-integration.ts index d67bc5bac2..c8a88308ce 100644 --- a/server/src/modes/script/test/template-integration.ts +++ b/server/src/modes/script/test/template-integration.ts @@ -5,12 +5,10 @@ import { getJavascriptMode } from '../javascript'; import { getLanguageModelCache } from '../../languageModelCache'; import { getDocumentRegions } from '../../embeddedSupport'; import { createTextDocument } from './script-integration'; -import { parseHTMLDocument } from '../../template/parser/htmlParser'; const workspace = path.resolve(__dirname, '../../../../test/fixtures/'); const documentRegions = getLanguageModelCache(10, 60, document => getDocumentRegions(document)); -const vueDocuments = getLanguageModelCache(10, 60, document => parseHTMLDocument(document)); -const scriptMode = getJavascriptMode(documentRegions, vueDocuments, workspace); +const scriptMode = getJavascriptMode(documentRegions, workspace); suite('template integrated test', () => { test('validate: comp3.vue', () => { diff --git a/server/src/modes/script/transformTemplate.ts b/server/src/modes/script/transformTemplate.ts index 84d36ce292..7948ce4372 100644 --- a/server/src/modes/script/transformTemplate.ts +++ b/server/src/modes/script/transformTemplate.ts @@ -1,5 +1,5 @@ import * as ts from 'typescript'; -import { HTMLDocument, Node, Attribute, Directive, Expression, Range } from '../template/parser/htmlParser'; +import { AST } from 'vue-eslint-parser'; export const componentHelperName = '__veturComponentHelper'; @@ -9,85 +9,104 @@ export const componentHelperName = '__veturComponentHelper'; * the regular Vue render function and does not work on runtime * because we just need type information for the template. */ -export function transformTemplate(html: HTMLDocument): ts.Expression[] { - const template = html.roots.find(node => { - return node.tag === 'template'; - }); - return template ? template.children.map(transformNode) : []; +export function transformTemplate(program: AST.ESLintProgram, code: string): ts.Expression[] { + const template = program.templateBody; + + if (!template) { + return []; + } + + return template.children.map(c => transformChild(c, code)); } -function transformNode(node: Node): ts.Expression { +function transformElement(node: AST.VElement, code: string): ts.Expression { return setTextRange(ts.createCall( ts.setTextRange(ts.createIdentifier(componentHelperName), { pos: 0, end: 0 }), undefined, [ // Element / Component name - ts.createLiteral(JSON.stringify(node.tag)), + ts.createLiteral(node.name), // Attributes / Directives - transformAttributes(node.attributes, node.directives), + transformAttributes(node.startTag.attributes, code), // Children - transformChildren(node.children) + ts.createArrayLiteral(node.children.map(c => transformChild(c, code))) ] ), node); } -function transformAttributes( - attributes: Record | undefined, - directives: Record | undefined -): ts.Expression { - const literalProps = !attributes ? [] : Object.keys(attributes).map(key => { - const attr = attributes[key]; +function transformAttributes(attrs: (AST.VAttribute | AST.VDirective)[], code: string): ts.Expression { + const literalProps = attrs.filter(isVAttribute).map(attr => { return setTextRange(ts.createPropertyAssignment( - setTextRange(ts.createIdentifier(attr.name.text), attr.name), + setTextRange(ts.createIdentifier(attr.key.name), attr.key), attr.value - ? setTextRange(ts.createLiteral(JSON.stringify(attr.value.text)), attr.value) + ? setTextRange(ts.createLiteral(attr.value.value), attr.value) : ts.createLiteral('true') ), attr); }); - const boundProps = (!directives || !directives['v-bind']) ? [] : directives['v-bind'].map(bind => { - const name = bind.key.argument; - const exp = bind.value ? parseExpression(bind.value) : ts.createLiteral('true'); + const boundProps = attrs.filter(isVBind).map(attr => { + const name = attr.key.argument; + const exp = (attr.value && attr.value.expression) + ? parseExpression(attr.value.expression as AST.ESLintExpression, code) + : ts.createLiteral('true'); if (name) { return setTextRange(ts.createPropertyAssignment( - setTextRange(ts.createIdentifier(name), bind.key), + setTextRange(ts.createIdentifier(name), attr.key), exp - ), bind); + ), attr); } else { - return setTextRange(ts.createSpreadAssignment(exp), bind); + return setTextRange(ts.createSpreadAssignment(exp), attr); } }); - const listeners = (!directives || !directives['v-on']) ? [] : directives['v-on'].map(listener => { - const name = listener.key.argument; - let exp = listener.value ? parseExpression(listener.value) : ts.createLiteral('true'); + const listeners = attrs.filter(isVOn).map(attr => { + const name = attr.key.argument; - if (exp.kind === ts.SyntaxKind.CallExpression) { - exp = ts.createFunctionExpression(undefined, undefined, undefined, undefined, - [ts.createParameter(undefined, undefined, undefined, - '$event', - undefined, - ts.createTypeReferenceNode('Event', undefined) - )], - undefined, - ts.createBlock([ - ts.createReturn(exp) - ]) - ); + let statements: ts.Statement[] = []; + if (attr.value && attr.value.expression) { + const exp = attr.value.expression as AST.VOnExpression; + statements = exp.body.map(st => transformStatement(st, code)); } + if (statements.length === 1) { + const first = statements[0]; + + if ( + ts.isExpressionStatement(first) && + ts.isIdentifier(first.expression) + ) { + statements[0] = ts.setTextRange(ts.createStatement( + ts.setTextRange(ts.createCall( + first.expression, + undefined, + [ts.setTextRange(ts.createIdentifier('$event'), first)] + ), first) + ), first); + } + } + + const exp = ts.createFunctionExpression(undefined, undefined, undefined, undefined, + [ts.createParameter(undefined, undefined, undefined, + '$event', + undefined, + ts.createTypeReferenceNode('Event', undefined) + )], + undefined, + ts.createBlock(statements) + ); + if (name) { return setTextRange(ts.createPropertyAssignment( - setTextRange(ts.createIdentifier(name), listener.key), + setTextRange(ts.createIdentifier(name), attr.key), exp - ), listener); + ), attr); } else { - return setTextRange(ts.createSpreadAssignment(exp), listener); + return setTextRange(ts.createSpreadAssignment(exp), attr); } }); @@ -99,42 +118,82 @@ function transformAttributes( ]); } -function transformChildren(children: Node[]): ts.Expression { - return ts.createArrayLiteral(children.map(transformNode)); +function transformChild(child: AST.VElement | AST.VExpressionContainer | AST.VText, code: string): ts.Expression { + switch (child.type) { + case 'VElement': + return transformElement(child, code); + case 'VExpressionContainer': + // Never appear v-for / v-on expression here + const exp = child.expression as AST.ESLintExpression | null; + return exp ? parseExpression(exp, code) : ts.createLiteral('""'); + case 'VText': + return ts.createLiteral(child.value); + } +} + +function transformStatement(statement: AST.ESLintStatement, code: string): ts.Statement { + if (statement.type !== 'ExpressionStatement') { + console.error('Unexpected statement type:', statement.type); + return ts.createStatement(ts.createLiteral('""')); + } + + return setTextRange(ts.createStatement( + parseExpression(statement.expression, code) + ), statement); } -function parseExpression(expression: Expression): ts.Expression { - const source = ts.createSourceFile('/tmp/parsed.ts', expression.expression, ts.ScriptTarget.Latest); +function parseExpression(expression: AST.ESLintExpression, code: string): ts.Expression { + const [start, end] = expression.range; + const expStr = code.slice(start, end); + const source = ts.createSourceFile('/tmp/parsed.ts', expStr, ts.ScriptTarget.Latest); const statement = source.statements[0]; - if (statement.kind !== ts.SyntaxKind.ExpressionStatement) { + if (!statement || !ts.isExpressionStatement(statement)) { console.error('Unexpected statement kind:', statement.kind); return ts.createLiteral('""'); } ts.forEachChild(statement, function next(node) { ts.setTextRange(node, { - pos: expression.start + node.pos, - end: expression.start + node.end + pos: start + node.pos, + end: start + node.end }); ts.forEachChild(node, next); }); - return injectThisForIdentifier((statement as ts.ExpressionStatement).expression, []); + return injectThisForIdentifier(statement.expression, []); } function injectThisForIdentifier(expression: ts.Expression, scope: ts.Identifier[]): ts.Expression { + let res; switch (expression.kind) { case ts.SyntaxKind.Identifier: - return ts.createPropertyAccess(ts.createThis(), expression as ts.Identifier); + res = ts.createPropertyAccess( + ts.setTextRange(ts.createThis(), expression), + expression as ts.Identifier + ); + break; default: return expression; } + return ts.setTextRange(res, expression); +} + +function isVAttribute(node: AST.VAttribute | AST.VDirective): node is AST.VAttribute { + return !node.directive; +} + +function isVBind(node: AST.VAttribute | AST.VDirective): node is AST.VDirective { + return node.directive && node.key.name === 'bind'; +} + +function isVOn(node: AST.VAttribute | AST.VDirective): node is AST.VDirective { + return node.directive && node.key.name === 'on'; } -function setTextRange(range: T, location: Range): T { +function setTextRange(range: T, location: AST.HasLocation): T { return ts.setTextRange(range, { - pos: location.start, - end: location.end + pos: location.range[0], + end: location.range[1] }); } diff --git a/server/src/modes/template/index.ts b/server/src/modes/template/index.ts index 215e02c103..0b3c71a778 100644 --- a/server/src/modes/template/index.ts +++ b/server/src/modes/template/index.ts @@ -4,7 +4,7 @@ import { TextDocument, Position, Range, FormattingOptions } from 'vscode-languag import { LanguageMode } from '../languageModes'; import { VueDocumentRegions } from '../embeddedSupport'; -import { HTMLDocument } from './parser/htmlParser'; +import { parseHTMLDocument, HTMLDocument } from './parser/htmlParser'; import { doComplete } from './services/htmlCompletion'; import { doHover } from './services/htmlHover'; import { findDocumentHighlights } from './services/htmlHighlighting'; @@ -23,7 +23,6 @@ type DocumentRegionCache = LanguageModelCache; export function getVueHTMLMode( documentRegions: DocumentRegionCache, - vueDocuments: LanguageModelCache, workspacePath: string | null | undefined, scriptMode: ScriptMode ): LanguageMode { @@ -32,6 +31,7 @@ export function getVueHTMLMode( const embeddedDocuments = getLanguageModelCache(10, 60, document => documentRegions.get(document).getEmbeddedDocument('vue-html') ); + const vueDocuments = getLanguageModelCache(10, 60, document => parseHTMLDocument(document)); const lintEngine = createLintEngine(); let config: any = {}; From a66b78c52d48009232e0097d41c916aef5a1d213 Mon Sep 17 00:00:00 2001 From: ktsn Date: Sun, 7 Jan 2018 16:36:43 +0900 Subject: [PATCH 06/49] format codes --- server/src/utils/paths.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/server/src/utils/paths.ts b/server/src/utils/paths.ts index 50aa87319e..f46213e7f1 100644 --- a/server/src/utils/paths.ts +++ b/server/src/utils/paths.ts @@ -14,7 +14,3 @@ export function getFilePath(documentUri: string): string { return Uri.parse(documentUri).path; } } - -export function getFileUri(documentPath: string): string { - return Uri.file(documentPath).toString(); -} \ No newline at end of file From a10d4927d9e5a8795612faccc188ee33dbac148d Mon Sep 17 00:00:00 2001 From: ktsn Date: Sun, 7 Jan 2018 16:44:36 +0900 Subject: [PATCH 07/49] template diagnostics provide correct error positions --- server/src/modes/script/javascript.ts | 5 +++-- server/src/modes/script/test/template-integration.ts | 4 ++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/server/src/modes/script/javascript.ts b/server/src/modes/script/javascript.ts index 2f05d588f6..cf53eecc44 100644 --- a/server/src/modes/script/javascript.ts +++ b/server/src/modes/script/javascript.ts @@ -114,9 +114,10 @@ export function getJavascriptMode( const diagnostics = service.getSemanticDiagnostics(fileFsPath); return diagnostics.map(diag => { + // syntactic/semantic diagnostic always has start and length + // so we can safely cast diag to TextSpan return { - // TODO: provide correct position - range: Range.create(templateDoc.positionAt(0), templateDoc.positionAt(templateDoc.getText().length - 1)), + range: convertRange(templateDoc, diag as ts.TextSpan), severity: DiagnosticSeverity.Error, message: ts.flattenDiagnosticMessageText(diag.messageText, '\n') }; diff --git a/server/src/modes/script/test/template-integration.ts b/server/src/modes/script/test/template-integration.ts index c8a88308ce..76b00cfad4 100644 --- a/server/src/modes/script/test/template-integration.ts +++ b/server/src/modes/script/test/template-integration.ts @@ -16,6 +16,10 @@ suite('template integrated test', () => { const doc = createTextDocument(filename); const diagnostics = scriptMode.doTemplateValidation(doc); assert.equal(diagnostics.length, 1, 'diagnostic count'); + assert.deepEqual(diagnostics[0].range, { + start: { line: 1, character: 8 }, + end: { line: 1, character: 16 } + }); assert(/Property 'messaage' does not exist/.test(diagnostics[0].message), 'diagnostic message'); }); }); From 6865c304905969df3713e913f2902512e98c1344 Mon Sep 17 00:00:00 2001 From: ktsn Date: Sun, 4 Feb 2018 00:48:03 +0900 Subject: [PATCH 08/49] Inject this expression for template identifiers --- .../script/test/transformTemplate-unit.ts | 165 ++++++++++++++++++ server/src/modes/script/transformTemplate.ts | 143 +++++++++++++-- 2 files changed, 298 insertions(+), 10 deletions(-) create mode 100644 server/src/modes/script/test/transformTemplate-unit.ts diff --git a/server/src/modes/script/test/transformTemplate-unit.ts b/server/src/modes/script/test/transformTemplate-unit.ts new file mode 100644 index 0000000000..8f5f198843 --- /dev/null +++ b/server/src/modes/script/test/transformTemplate-unit.ts @@ -0,0 +1,165 @@ +import * as assert from 'assert'; +import * as ts from 'typescript'; +import { injectThis, globalScope } from '../transformTemplate'; + +suite('transformTemplate', () => { + suite('`this` injection', () => { + function check( + inputTsCode: string, + expectedTsCode: string, + scope: string[] = [] + ): void { + const source = ts.createSourceFile('test.ts', inputTsCode, ts.ScriptTarget.Latest); + const st = source.statements[0] as ts.ExpressionStatement; + assert.equal(st.kind, ts.SyntaxKind.ExpressionStatement, 'Input ts code must be an expression'); + + const exp = st.expression; + const output = injectThis(exp, globalScope.concat(scope)); + + const printer = ts.createPrinter(); + const outputStr = printer.printNode(ts.EmitHint.Expression, output, source); + assert.equal(outputStr.trim(), expectedTsCode.trim()); + } + + test('Identifier', () => { + check( + 'foo', + 'this.foo' + ); + }); + + test('Identifier: in scope', () => { + check( + 'foo', + 'foo', + ['foo'] + ); + }); + + test('Identifier: global variables', () => { + check( + 'String(undefined)', + 'String(undefined)' + ); + }); + + test('ThisExpression', () => { + check( + 'this.foo', + 'this.foo' + ); + }); + + test('TypeOfExpression', () => { + check( + 'typeof foo === "string"', + 'typeof this.foo === "string"' + ); + }); + + test('DeleteExpression', () => { + check( + 'delete foo.bar', + 'delete this.foo.bar' + ); + }); + + test('VoidExpression', () => { + check( + 'void foo()', + 'void this.foo()' + ); + }); + + test('PropertyAccessExpression', () => { + check( + 'foo.bar', + 'this.foo.bar' + ); + }); + + test('PrefixUnaryExpression', () => { + check( + '!foo', + '!this.foo' + ); + }); + + test('PostfixUnaryExpression', () => { + check( + 'foo++', + 'this.foo++' + ); + }); + + test('BinaryExpression', () => { + check( + 'foo + bar', + 'this.foo + this.bar' + ); + }); + + test('ConditionalExpression', () => { + check( + 'foo ? bar : baz', + 'this.foo ? this.bar : this.baz' + ); + }); + + test('CallExpression', () => { + check( + 'foo(bar)', + 'this.foo(this.bar)' + ); + }); + + test('ParenthesizedExpression', () => { + check( + '(foo)', + '(this.foo)' + ); + }); + + test('ObjectLiteralExpression', () => { + check( + '({ foo: bar })', + '({ foo: this.bar })' + ); + }); + + test('ObjectLiteralExpression: shorthand', () => { + check( + '({ foo })', + '({ foo: this.foo })' + ); + }); + + test('ObjectLiteralExpression: spread', () => { + check( + '({ ...foo })', + '({ ...this.foo })' + ); + }); + + test('ArrowFunction', () => { + check( + '(event) => foo(event)', + '(event) => this.foo(event)' + ); + }); + + test('ArrowFunction: rest spread', () => { + check( + '(...args) => test(args)', + '(...args) => this.test(args)' + ); + }); + + test('ArrowFunction: patterns', () => { + check( + '({ foo: bar, baz }, [qux, ...tail]) => tail.concat(foo(bar + baz) + qux)', + '({ foo: bar, baz }, [qux, ...tail]) => tail.concat(this.foo(bar + baz) + qux)' + ); + }); + }); +}); \ No newline at end of file diff --git a/server/src/modes/script/transformTemplate.ts b/server/src/modes/script/transformTemplate.ts index 7948ce4372..137088483b 100644 --- a/server/src/modes/script/transformTemplate.ts +++ b/server/src/modes/script/transformTemplate.ts @@ -3,6 +3,15 @@ import { AST } from 'vue-eslint-parser'; export const componentHelperName = '__veturComponentHelper'; +// Allowed global variables in templates. +// From: https://github.com/vuejs/vue/blob/dev/src/core/instance/proxy.js +export const globalScope = ( + 'Infinity,undefined,NaN,isFinite,isNaN,' + + 'parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,' + + 'Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,' + + 'require' +).split(','); + /** * Transform template AST to TypeScript AST. * Note: The returned TS AST is not compatible with @@ -161,22 +170,130 @@ function parseExpression(expression: AST.ESLintExpression, code: string): ts.Exp ts.forEachChild(node, next); }); - return injectThisForIdentifier(statement.expression, []); + return injectThis(statement.expression, []); } -function injectThisForIdentifier(expression: ts.Expression, scope: ts.Identifier[]): ts.Expression { +export function injectThis(exp: ts.Expression, scope: string[]): ts.Expression { let res; - switch (expression.kind) { - case ts.SyntaxKind.Identifier: + if (ts.isIdentifier(exp)) { + if (scope.find(id => id === exp.text) === undefined) { res = ts.createPropertyAccess( - ts.setTextRange(ts.createThis(), expression), - expression as ts.Identifier + ts.setTextRange(ts.createThis(), exp), + exp ); - break; - default: - return expression; + } else { + return exp; + } + } else if (ts.isPropertyAccessExpression(exp)) { + res = ts.createPropertyAccess( + injectThis(exp.expression, scope), + exp.name + ); + } else if (ts.isPrefixUnaryExpression(exp)) { + res = ts.createPrefix( + exp.operator, + injectThis(exp.operand, scope) + ); + } else if (ts.isPostfixUnaryExpression(exp)) { + res = ts.createPostfix( + injectThis(exp.operand, scope), + exp.operator + ); + } else if (exp.kind === ts.SyntaxKind.TypeOfExpression) { + // Manually check `kind` for typeof expression + // since ts.isTypeOfExpression is not working. + res = ts.createTypeOf( + injectThis((exp as ts.TypeOfExpression).expression, scope) + ); + } else if (ts.isDeleteExpression(exp)) { + res = ts.createDelete( + injectThis(exp.expression, scope) + ); + } else if (ts.isVoidExpression(exp)) { + res = ts.createVoid( + injectThis(exp.expression, scope) + ); + } else if (ts.isBinaryExpression(exp)) { + res = ts.createBinary( + injectThis(exp.left, scope), + exp.operatorToken, + injectThis(exp.right, scope) + ); + } else if (ts.isConditionalExpression(exp)) { + res = ts.createConditional( + injectThis(exp.condition, scope), + injectThis(exp.whenTrue, scope), + injectThis(exp.whenFalse, scope) + ); + } else if (ts.isCallExpression(exp)) { + res = ts.createCall( + injectThis(exp.expression, scope), + exp.typeArguments, + exp.arguments.map(arg => injectThis(arg, scope)) + ); + } else if (ts.isParenthesizedExpression(exp)) { + res = ts.createParen( + injectThis(exp.expression, scope) + ); + } else if (ts.isObjectLiteralExpression(exp)) { + res = ts.createObjectLiteral( + exp.properties.map(p => injectThisForObjectLiteralElement(p, scope)) + ); + } else if (ts.isArrowFunction(exp) && !ts.isBlock(exp.body)) { + res = ts.createArrowFunction( + exp.modifiers, + exp.typeParameters, + exp.parameters, + exp.type, + exp.equalsGreaterThanToken, + injectThis( + exp.body, + scope.concat(flatMap(exp.parameters, collectScope)) + ) + ); + } else { + return exp; + } + return ts.setTextRange(res, exp); +} + +function injectThisForObjectLiteralElement( + el: ts.ObjectLiteralElementLike, + scope: string[] +): ts.ObjectLiteralElementLike { + let res; + if (ts.isPropertyAssignment(el)) { + res = ts.createPropertyAssignment( + el.name, + injectThis(el.initializer, scope) + ); + } else if (ts.isShorthandPropertyAssignment(el)) { + res = ts.createPropertyAssignment( + el.name, + injectThis(el.name, scope) + ); + } else if (ts.isSpreadAssignment(el)) { + res = ts.createSpreadAssignment( + injectThis(el.expression, scope) + ); + } else { + return el; + } + return ts.setTextRange(res, el); +} + +function collectScope(param: ts.ParameterDeclaration | ts.BindingElement): string[] { + const binding = param.name; + if (ts.isIdentifier(binding)) { + return [binding.text]; + } else if (ts.isObjectBindingPattern(binding)) { + return flatMap(binding.elements, collectScope); + } else if (ts.isArrayBindingPattern(binding)) { + const filtered = binding.elements.filter(ts.isBindingElement); + return flatMap(filtered, collectScope); + } else { + return []; } - return ts.setTextRange(res, expression); } function isVAttribute(node: AST.VAttribute | AST.VDirective): node is AST.VAttribute { @@ -191,6 +308,12 @@ function isVOn(node: AST.VAttribute | AST.VDirective): node is AST.VDirective { return node.directive && node.key.name === 'on'; } +function flatMap(list: ReadonlyArray, fn: (value: T) => R[]): R[] { + return list.reduce((acc, item) => { + return acc.concat(fn(item)); + }, []); +} + function setTextRange(range: T, location: AST.HasLocation): T { return ts.setTextRange(range, { pos: location.range[0], From 34a58dccf91e794e50c18a9df13bb58747a4cc5b Mon Sep 17 00:00:00 2001 From: ktsn Date: Sun, 4 Feb 2018 11:40:51 +0900 Subject: [PATCH 09/49] check v-for expression --- server/src/modes/script/bridge.ts | 9 +- server/src/modes/script/preprocess.ts | 6 +- .../modes/script/test/template-integration.ts | 7 ++ server/src/modes/script/transformTemplate.ts | 100 ++++++++++++++---- server/test/fixtures/component/comp4.vue | 24 +++++ 5 files changed, 123 insertions(+), 23 deletions(-) create mode 100644 server/test/fixtures/component/comp4.vue diff --git a/server/src/modes/script/bridge.ts b/server/src/modes/script/bridge.ts index 05749fd041..4e70739b4f 100644 --- a/server/src/modes/script/bridge.ts +++ b/server/src/modes/script/bridge.ts @@ -1,4 +1,4 @@ -import { componentHelperName } from './transformTemplate'; +import { componentHelperName, iterationHelperName } from './transformTemplate'; // this bridge file will be injected into TypeScript service // it enable type checking and completion, yet still preserve precise option type @@ -8,9 +8,14 @@ export const moduleName = 'vue-editor-bridge'; export const fileName = 'vue-temp/vue-editor-bridge.ts'; const renderHelpers = ` -export interface ${componentHelperName} { +export declare const ${componentHelperName}: { (tag: string, data: any, children: any[]): any; } +export declare const ${iterationHelperName}: { + (list: T[], fn: (value: T, index: number) => any): any; + (obj: { [key: string]: T }, fn: (value: T, key: string, index: number) => any): any; + (obj: object, fn: (value: any, key: string, index: number) => any): any; +} `; export const oldContent = ` diff --git a/server/src/modes/script/preprocess.ts b/server/src/modes/script/preprocess.ts index 4754549345..e0ca66e081 100644 --- a/server/src/modes/script/preprocess.ts +++ b/server/src/modes/script/preprocess.ts @@ -4,7 +4,7 @@ import { parse } from 'vue-eslint-parser'; import { getDocumentRegions } from '../embeddedSupport'; import { TextDocument } from 'vscode-languageserver-types'; -import { transformTemplate, componentHelperName } from './transformTemplate'; +import { transformTemplate, componentHelperName, iterationHelperName } from './transformTemplate'; export function isVue(filename: string): boolean { return path.extname(filename) === '.vue'; @@ -156,6 +156,10 @@ function injectVueTemplate(sourceFile: ts.SourceFile, renderBlock: ts.Expression setZeroPos(ts.createImportSpecifier( undefined, setZeroPos(ts.createIdentifier(componentHelperName)) + )), + setZeroPos(ts.createImportSpecifier( + undefined, + setZeroPos(ts.createIdentifier(iterationHelperName)) )) ])) )), diff --git a/server/src/modes/script/test/template-integration.ts b/server/src/modes/script/test/template-integration.ts index 76b00cfad4..3d2b0f58a5 100644 --- a/server/src/modes/script/test/template-integration.ts +++ b/server/src/modes/script/test/template-integration.ts @@ -22,4 +22,11 @@ suite('template integrated test', () => { }); assert(/Property 'messaage' does not exist/.test(diagnostics[0].message), 'diagnostic message'); }); + + test('validate: comp4.vue', () => { + const filename = path.join(workspace + '/component/comp4.vue'); + const doc = createTextDocument(filename); + const diagnostics = scriptMode.doTemplateValidation(doc); + assert.equal(diagnostics.length, 0, 'diagnostic count'); + }); }); diff --git a/server/src/modes/script/transformTemplate.ts b/server/src/modes/script/transformTemplate.ts index 137088483b..f91a149dd8 100644 --- a/server/src/modes/script/transformTemplate.ts +++ b/server/src/modes/script/transformTemplate.ts @@ -2,10 +2,11 @@ import * as ts from 'typescript'; import { AST } from 'vue-eslint-parser'; export const componentHelperName = '__veturComponentHelper'; +export const iterationHelperName = '__veturIterationHelper'; // Allowed global variables in templates. // From: https://github.com/vuejs/vue/blob/dev/src/core/instance/proxy.js -export const globalScope = ( +const globalScope = ( 'Infinity,undefined,NaN,isFinite,isNaN,' + 'parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,' + 'Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,' + @@ -25,11 +26,12 @@ export function transformTemplate(program: AST.ESLintProgram, code: string): ts. return []; } - return template.children.map(c => transformChild(c, code)); + return template.children.map(c => transformChild(c, code, globalScope)); } -function transformElement(node: AST.VElement, code: string): ts.Expression { - return setTextRange(ts.createCall( +function transformElement(node: AST.VElement, code: string, scope: string[]): ts.Expression { + const newScope = scope.concat(node.variables.map(v => v.id.name)); + const element = setTextRange(ts.createCall( ts.setTextRange(ts.createIdentifier(componentHelperName), { pos: 0, end: 0 }), undefined, [ @@ -37,15 +39,46 @@ function transformElement(node: AST.VElement, code: string): ts.Expression { ts.createLiteral(node.name), // Attributes / Directives - transformAttributes(node.startTag.attributes, code), + transformAttributes(node.startTag.attributes, code, newScope), // Children - ts.createArrayLiteral(node.children.map(c => transformChild(c, code))) + ts.createArrayLiteral(node.children.map(c => transformChild(c, code, newScope))) ] ), node); + + const vFor = node.startTag.attributes.find(isVFor); + if (!vFor || !vFor.value) { + return element; + } else { + // Convert v-for directive to the iteration helper + const exp = vFor.value.expression as AST.VForExpression; + + return setTextRange(ts.createCall( + setTextRange(ts.createIdentifier(iterationHelperName), exp.right), + undefined, + [ + // Iteration target + parseExpression(exp.right, code, scope), + + // Callback + setTextRange(ts.createArrowFunction( + undefined, + undefined, + parseParams(exp.left, code, scope), + undefined, + setTextRange(ts.createToken(ts.SyntaxKind.EqualsGreaterThanToken), exp), + element + ), exp) + ] + ), exp); + } } -function transformAttributes(attrs: (AST.VAttribute | AST.VDirective)[], code: string): ts.Expression { +function transformAttributes( + attrs: (AST.VAttribute | AST.VDirective)[], + code: string, + scope: string[] +): ts.Expression { const literalProps = attrs.filter(isVAttribute).map(attr => { return setTextRange(ts.createPropertyAssignment( setTextRange(ts.createIdentifier(attr.key.name), attr.key), @@ -59,7 +92,7 @@ function transformAttributes(attrs: (AST.VAttribute | AST.VDirective)[], code: s const boundProps = attrs.filter(isVBind).map(attr => { const name = attr.key.argument; const exp = (attr.value && attr.value.expression) - ? parseExpression(attr.value.expression as AST.ESLintExpression, code) + ? parseExpression(attr.value.expression as AST.ESLintExpression, code, scope) : ts.createLiteral('true'); if (name) { @@ -79,7 +112,7 @@ function transformAttributes(attrs: (AST.VAttribute | AST.VDirective)[], code: s let statements: ts.Statement[] = []; if (attr.value && attr.value.expression) { const exp = attr.value.expression as AST.VOnExpression; - statements = exp.body.map(st => transformStatement(st, code)); + statements = exp.body.map(st => transformStatement(st, code, scope)); } if (statements.length === 1) { @@ -127,34 +160,57 @@ function transformAttributes(attrs: (AST.VAttribute | AST.VDirective)[], code: s ]); } -function transformChild(child: AST.VElement | AST.VExpressionContainer | AST.VText, code: string): ts.Expression { +function transformChild( + child: AST.VElement | AST.VExpressionContainer | AST.VText, + code: string, + scope: string[] +): ts.Expression { switch (child.type) { case 'VElement': - return transformElement(child, code); + return transformElement(child, code, scope); case 'VExpressionContainer': // Never appear v-for / v-on expression here const exp = child.expression as AST.ESLintExpression | null; - return exp ? parseExpression(exp, code) : ts.createLiteral('""'); + return exp ? parseExpression(exp, code, scope) : ts.createLiteral('""'); case 'VText': return ts.createLiteral(child.value); } } -function transformStatement(statement: AST.ESLintStatement, code: string): ts.Statement { +function transformStatement(statement: AST.ESLintStatement, code: string, scope: string[]): ts.Statement { if (statement.type !== 'ExpressionStatement') { console.error('Unexpected statement type:', statement.type); return ts.createStatement(ts.createLiteral('""')); } return setTextRange(ts.createStatement( - parseExpression(statement.expression, code) + parseExpression(statement.expression, code, scope) ), statement); } -function parseExpression(expression: AST.ESLintExpression, code: string): ts.Expression { +function parseExpression(expression: AST.ESLintExpression, code: string, scope: string[]): ts.Expression { const [start, end] = expression.range; const expStr = code.slice(start, end); - const source = ts.createSourceFile('/tmp/parsed.ts', expStr, ts.ScriptTarget.Latest); + return parseExpressionImpl(expStr, start, scope); +} + +function parseParams( + params: AST.ESLintPattern[], + code: string, + scope: string[] +): ts.NodeArray { + const start = params[0].range[0]; + const end = params[params.length - 1].range[1]; + const paramsStr = code.slice(start, end); + // Wrap parameters with an arrow function to extract them as ts parameter declarations. + const arrowFnStr = '(' + paramsStr + ') => {}'; + + const exp = parseExpressionImpl(arrowFnStr, start, scope) as ts.ArrowFunction; + return exp.parameters; +} + +function parseExpressionImpl(exp: string, offset: number, scope: string[]): ts.Expression { + const source = ts.createSourceFile('/tmp/parsed.ts', exp, ts.ScriptTarget.Latest); const statement = source.statements[0]; if (!statement || !ts.isExpressionStatement(statement)) { @@ -164,19 +220,19 @@ function parseExpression(expression: AST.ESLintExpression, code: string): ts.Exp ts.forEachChild(statement, function next(node) { ts.setTextRange(node, { - pos: start + node.pos, - end: start + node.end + pos: offset + node.pos, + end: offset + node.end }); ts.forEachChild(node, next); }); - return injectThis(statement.expression, []); + return injectThis(statement.expression, scope); } export function injectThis(exp: ts.Expression, scope: string[]): ts.Expression { let res; if (ts.isIdentifier(exp)) { - if (scope.find(id => id === exp.text) === undefined) { + if (scope.indexOf(exp.text) < 0) { res = ts.createPropertyAccess( ts.setTextRange(ts.createThis(), exp), exp @@ -308,6 +364,10 @@ function isVOn(node: AST.VAttribute | AST.VDirective): node is AST.VDirective { return node.directive && node.key.name === 'on'; } +function isVFor(node: AST.VAttribute | AST.VDirective): node is AST.VDirective { + return node.directive && node.key.name === 'for'; +} + function flatMap(list: ReadonlyArray, fn: (value: T) => R[]): R[] { return list.reduce((acc, item) => { return acc.concat(fn(item)); diff --git a/server/test/fixtures/component/comp4.vue b/server/test/fixtures/component/comp4.vue new file mode 100644 index 0000000000..965432e9b4 --- /dev/null +++ b/server/test/fixtures/component/comp4.vue @@ -0,0 +1,24 @@ + + + From 3b852e4132c99414b148c326f83a1699aecf6fa1 Mon Sep 17 00:00:00 2001 From: ktsn Date: Sun, 4 Feb 2018 11:41:19 +0900 Subject: [PATCH 10/49] remove global scope test of injectThis --- .../src/modes/script/test/transformTemplate-unit.ts | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/server/src/modes/script/test/transformTemplate-unit.ts b/server/src/modes/script/test/transformTemplate-unit.ts index 8f5f198843..08e6a16338 100644 --- a/server/src/modes/script/test/transformTemplate-unit.ts +++ b/server/src/modes/script/test/transformTemplate-unit.ts @@ -1,6 +1,6 @@ import * as assert from 'assert'; import * as ts from 'typescript'; -import { injectThis, globalScope } from '../transformTemplate'; +import { injectThis } from '../transformTemplate'; suite('transformTemplate', () => { suite('`this` injection', () => { @@ -14,7 +14,7 @@ suite('transformTemplate', () => { assert.equal(st.kind, ts.SyntaxKind.ExpressionStatement, 'Input ts code must be an expression'); const exp = st.expression; - const output = injectThis(exp, globalScope.concat(scope)); + const output = injectThis(exp, scope); const printer = ts.createPrinter(); const outputStr = printer.printNode(ts.EmitHint.Expression, output, source); @@ -36,13 +36,6 @@ suite('transformTemplate', () => { ); }); - test('Identifier: global variables', () => { - check( - 'String(undefined)', - 'String(undefined)' - ); - }); - test('ThisExpression', () => { check( 'this.foo', From de2e8595e5c23940f2af8d3111758f3c6acd7ce5 Mon Sep 17 00:00:00 2001 From: ktsn Date: Sun, 4 Feb 2018 12:20:08 +0900 Subject: [PATCH 11/49] fix the position of v-for expression --- server/src/modes/script/transformTemplate.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/server/src/modes/script/transformTemplate.ts b/server/src/modes/script/transformTemplate.ts index f91a149dd8..c762b01303 100644 --- a/server/src/modes/script/transformTemplate.ts +++ b/server/src/modes/script/transformTemplate.ts @@ -1,6 +1,7 @@ import * as ts from 'typescript'; import { AST } from 'vue-eslint-parser'; +export const renderHelperName = '__veturRenderHelper'; export const componentHelperName = '__veturComponentHelper'; export const iterationHelperName = '__veturIterationHelper'; @@ -47,7 +48,7 @@ function transformElement(node: AST.VElement, code: string, scope: string[]): ts ), node); const vFor = node.startTag.attributes.find(isVFor); - if (!vFor || !vFor.value) { + if (!vFor || !vFor.value || !vFor.value.expression) { return element; } else { // Convert v-for directive to the iteration helper @@ -205,7 +206,8 @@ function parseParams( // Wrap parameters with an arrow function to extract them as ts parameter declarations. const arrowFnStr = '(' + paramsStr + ') => {}'; - const exp = parseExpressionImpl(arrowFnStr, start, scope) as ts.ArrowFunction; + // Decrement the offset since the expression now has the open parenthesis. + const exp = parseExpressionImpl(arrowFnStr, start - 1, scope) as ts.ArrowFunction; return exp.parameters; } From c6a38a0616452f09a5cd684edb3c984c48416b0b Mon Sep 17 00:00:00 2001 From: ktsn Date: Sun, 4 Feb 2018 12:20:53 +0900 Subject: [PATCH 12/49] rewrite template render function --- server/src/modes/script/bridge.ts | 9 ++++--- server/src/modes/script/preprocess.ts | 36 ++++++++++++++++++--------- 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/server/src/modes/script/bridge.ts b/server/src/modes/script/bridge.ts index 4e70739b4f..59f938e666 100644 --- a/server/src/modes/script/bridge.ts +++ b/server/src/modes/script/bridge.ts @@ -1,4 +1,4 @@ -import { componentHelperName, iterationHelperName } from './transformTemplate'; +import { renderHelperName, componentHelperName, iterationHelperName } from './transformTemplate'; // this bridge file will be injected into TypeScript service // it enable type checking and completion, yet still preserve precise option type @@ -8,14 +8,17 @@ export const moduleName = 'vue-editor-bridge'; export const fileName = 'vue-temp/vue-editor-bridge.ts'; const renderHelpers = ` +export declare const ${renderHelperName}: { + (component: T, fn: (this: T) => any): any; +}; export declare const ${componentHelperName}: { (tag: string, data: any, children: any[]): any; -} +}; export declare const ${iterationHelperName}: { (list: T[], fn: (value: T, index: number) => any): any; (obj: { [key: string]: T }, fn: (value: T, key: string, index: number) => any): any; (obj: object, fn: (value: any, key: string, index: number) => any): any; -} +}; `; export const oldContent = ` diff --git a/server/src/modes/script/preprocess.ts b/server/src/modes/script/preprocess.ts index e0ca66e081..ffdd0f8257 100644 --- a/server/src/modes/script/preprocess.ts +++ b/server/src/modes/script/preprocess.ts @@ -4,7 +4,7 @@ import { parse } from 'vue-eslint-parser'; import { getDocumentRegions } from '../embeddedSupport'; import { TextDocument } from 'vscode-languageserver-types'; -import { transformTemplate, componentHelperName, iterationHelperName } from './transformTemplate'; +import { transformTemplate, componentHelperName, iterationHelperName, renderHelperName } from './transformTemplate'; export function isVue(filename: string): boolean { return path.extname(filename) === '.vue'; @@ -153,6 +153,10 @@ function injectVueTemplate(sourceFile: ts.SourceFile, renderBlock: ts.Expression undefined, setZeroPos(ts.createImportClause(undefined, setZeroPos(ts.createNamedImports([ + setZeroPos(ts.createImportSpecifier( + undefined, + setZeroPos(ts.createIdentifier(renderHelperName)) + )), setZeroPos(ts.createImportSpecifier( undefined, setZeroPos(ts.createIdentifier(componentHelperName)) @@ -183,18 +187,26 @@ function injectVueTemplate(sourceFile: ts.SourceFile, renderBlock: ts.Expression // with `this` type of component. const setRenderPos = getWrapperRangeSetter(sourceFile); const statements = renderBlock.map(exp => ts.createStatement(exp)); - const renderElement = setRenderPos(ts.createFunctionDeclaration(undefined, undefined, undefined, - '__render', - undefined, - [setZeroPos(ts.createParameter(undefined, undefined, undefined, - 'this', + const renderElement = setRenderPos(ts.createStatement( + setRenderPos(ts.createCall( + setRenderPos(ts.createIdentifier(renderHelperName)), undefined, - setZeroPos(setZeroPos(ts.createTypeQueryNode( - setMinPos(ts.createIdentifier('__component')) - ))) - ))], - undefined, - setRenderPos(ts.createBlock(statements)) + [ + // Reference to the component + setRenderPos(ts.createIdentifier('__component')), + + // A function simulating the render function + setRenderPos(ts.createFunctionExpression( + undefined, + undefined, + undefined, + undefined, + [], + undefined, + setRenderPos(ts.createBlock(statements)) + )) + ] + )) )); // 4. replace the original statements with wrapped code. From b90f96a631f66eaf6294c7e23762deebf238d3f8 Mon Sep 17 00:00:00 2001 From: ktsn Date: Mon, 5 Feb 2018 18:03:34 +0900 Subject: [PATCH 13/49] Avoid parsing error of script block --- server/src/modes/script/preprocess.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/server/src/modes/script/preprocess.ts b/server/src/modes/script/preprocess.ts index ffdd0f8257..3fcca6155b 100644 --- a/server/src/modes/script/preprocess.ts +++ b/server/src/modes/script/preprocess.ts @@ -21,16 +21,20 @@ export function parseVueScript(text: string): string { return script.getText() || 'export default {};'; } -export function parseVueTemplate(text: string): string { +function parseVueTemplate(text: string): string { const doc = TextDocument.create('test://test/test.vue', 'vue', 0, text); const regions = getDocumentRegions(doc); const template = regions.getEmbeddedDocumentByType('template'); - // TODO: support other template format if (template.languageId !== 'vue-html') { return ''; } - return template.getText(); + const rawText = template.getText(); + // skip checking on empty template + if (rawText.replace(/\s/g, '') === '') { + return ''; + } + return rawText.replace(/^\s*\n/, ''); } function isTSLike(scriptKind: ts.ScriptKind | undefined) { @@ -57,7 +61,9 @@ export function createUpdater() { modifyVueScript(sourceFile); hackSourceFile.__modified = true; } else if (isVueTemplate(fileName)) { - const code = scriptSnapshot.getText(0, scriptSnapshot.getLength()); + // TODO: share the logic of transforming the code into AST + // with the template mode + const code = parseVueTemplate(scriptSnapshot.getText(0, scriptSnapshot.getLength())); const program = parse(code, { sourceType: 'module' }); const tsCode = transformTemplate(program, code); injectVueTemplate(sourceFile, tsCode); From 1f02005c33b01766698c70e5c875db497a877e5f Mon Sep 17 00:00:00 2001 From: ktsn Date: Mon, 5 Feb 2018 18:15:13 +0900 Subject: [PATCH 14/49] Handle object literal expression properly --- .../modes/script/test/template-integration.ts | 8 ++++++++ server/src/modes/script/transformTemplate.ts | 12 +++++++---- server/test/fixtures/component/comp5.vue | 20 +++++++++++++++++++ 3 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 server/test/fixtures/component/comp5.vue diff --git a/server/src/modes/script/test/template-integration.ts b/server/src/modes/script/test/template-integration.ts index 3d2b0f58a5..ca1457db61 100644 --- a/server/src/modes/script/test/template-integration.ts +++ b/server/src/modes/script/test/template-integration.ts @@ -29,4 +29,12 @@ suite('template integrated test', () => { const diagnostics = scriptMode.doTemplateValidation(doc); assert.equal(diagnostics.length, 0, 'diagnostic count'); }); + + test('validate: comp5.vue', () => { + const filename = path.join(workspace + '/component/comp5.vue'); + const doc = createTextDocument(filename); + const diagnostics = scriptMode.doTemplateValidation(doc); + assert.equal(diagnostics.length, 1, 'diagnostic count'); + assert(/Property 'bar' does not exist/.test(diagnostics[0].message), 'diagnostic message'); + }); }); diff --git a/server/src/modes/script/transformTemplate.ts b/server/src/modes/script/transformTemplate.ts index c762b01303..4bddd1dfbd 100644 --- a/server/src/modes/script/transformTemplate.ts +++ b/server/src/modes/script/transformTemplate.ts @@ -212,7 +212,9 @@ function parseParams( } function parseExpressionImpl(exp: string, offset: number, scope: string[]): ts.Expression { - const source = ts.createSourceFile('/tmp/parsed.ts', exp, ts.ScriptTarget.Latest); + // Add parenthesis to deal with object literal expression + const wrappedExp = '(' + exp + ')'; + const source = ts.createSourceFile('/tmp/parsed.ts', wrappedExp, ts.ScriptTarget.Latest); const statement = source.statements[0]; if (!statement || !ts.isExpressionStatement(statement)) { @@ -221,14 +223,16 @@ function parseExpressionImpl(exp: string, offset: number, scope: string[]): ts.E } ts.forEachChild(statement, function next(node) { + // Decrement offset for added parenthesis ts.setTextRange(node, { - pos: offset + node.pos, - end: offset + node.end + pos: offset - 1 + node.pos, + end: offset - 1 + node.end }); ts.forEachChild(node, next); }); - return injectThis(statement.expression, scope); + const parenthesis = statement.expression as ts.ParenthesizedExpression; + return injectThis(parenthesis.expression, scope); } export function injectThis(exp: ts.Expression, scope: string[]): ts.Expression { diff --git a/server/test/fixtures/component/comp5.vue b/server/test/fixtures/component/comp5.vue new file mode 100644 index 0000000000..fa7cc11669 --- /dev/null +++ b/server/test/fixtures/component/comp5.vue @@ -0,0 +1,20 @@ + + + From deaa8a7d0b10bb9e535d88399968e7a10f10a9f7 Mon Sep 17 00:00:00 2001 From: ktsn Date: Mon, 5 Feb 2018 18:19:14 +0900 Subject: [PATCH 15/49] Move template type checking fixtures --- .../src/modes/script/test/template-integration.ts | 15 ++++++++------- .../expression.vue} | 0 .../object-literal.vue} | 0 .../{comp4.vue => template-checking/v-for.vue} | 5 ++++- 4 files changed, 12 insertions(+), 8 deletions(-) rename server/test/fixtures/component/{comp3.vue => template-checking/expression.vue} (100%) rename server/test/fixtures/component/{comp5.vue => template-checking/object-literal.vue} (100%) rename server/test/fixtures/component/{comp4.vue => template-checking/v-for.vue} (70%) diff --git a/server/src/modes/script/test/template-integration.ts b/server/src/modes/script/test/template-integration.ts index ca1457db61..16428ad6a9 100644 --- a/server/src/modes/script/test/template-integration.ts +++ b/server/src/modes/script/test/template-integration.ts @@ -11,8 +11,8 @@ const documentRegions = getLanguageModelCache(10, 60, document => getDocumentReg const scriptMode = getJavascriptMode(documentRegions, workspace); suite('template integrated test', () => { - test('validate: comp3.vue', () => { - const filename = path.join(workspace + '/component/comp3.vue'); + test('validate: expression.vue', () => { + const filename = path.join(workspace + '/component/template-checking/expression.vue'); const doc = createTextDocument(filename); const diagnostics = scriptMode.doTemplateValidation(doc); assert.equal(diagnostics.length, 1, 'diagnostic count'); @@ -23,15 +23,16 @@ suite('template integrated test', () => { assert(/Property 'messaage' does not exist/.test(diagnostics[0].message), 'diagnostic message'); }); - test('validate: comp4.vue', () => { - const filename = path.join(workspace + '/component/comp4.vue'); + test('validate: v-for.vue', () => { + const filename = path.join(workspace + '/component/template-checking/v-for.vue'); const doc = createTextDocument(filename); const diagnostics = scriptMode.doTemplateValidation(doc); - assert.equal(diagnostics.length, 0, 'diagnostic count'); + assert.equal(diagnostics.length, 1, 'diagnostic count'); + assert(/Property 'notExists' does not exist/.test(diagnostics[0].message), 'diagnostic message'); }); - test('validate: comp5.vue', () => { - const filename = path.join(workspace + '/component/comp5.vue'); + test('validate: object-literal.vue', () => { + const filename = path.join(workspace + '/component/template-checking/object-literal.vue'); const doc = createTextDocument(filename); const diagnostics = scriptMode.doTemplateValidation(doc); assert.equal(diagnostics.length, 1, 'diagnostic count'); diff --git a/server/test/fixtures/component/comp3.vue b/server/test/fixtures/component/template-checking/expression.vue similarity index 100% rename from server/test/fixtures/component/comp3.vue rename to server/test/fixtures/component/template-checking/expression.vue diff --git a/server/test/fixtures/component/comp5.vue b/server/test/fixtures/component/template-checking/object-literal.vue similarity index 100% rename from server/test/fixtures/component/comp5.vue rename to server/test/fixtures/component/template-checking/object-literal.vue diff --git a/server/test/fixtures/component/comp4.vue b/server/test/fixtures/component/template-checking/v-for.vue similarity index 70% rename from server/test/fixtures/component/comp4.vue rename to server/test/fixtures/component/template-checking/v-for.vue index 965432e9b4..9a5c793722 100644 --- a/server/test/fixtures/component/comp4.vue +++ b/server/test/fixtures/component/template-checking/v-for.vue @@ -1,7 +1,10 @@ From 68b8200c5730cccc14aad47c5bd1859b719127d4 Mon Sep 17 00:00:00 2001 From: ktsn Date: Mon, 5 Feb 2018 18:58:05 +0900 Subject: [PATCH 16/49] Process template code as JS to avoid unnecessary errors --- server/src/modes/script/javascript.ts | 6 +- server/src/modes/script/serviceHost.ts | 189 +++++++++++++------------ 2 files changed, 105 insertions(+), 90 deletions(-) diff --git a/server/src/modes/script/javascript.ts b/server/src/modes/script/javascript.ts index cf53eecc44..be54a1c56d 100644 --- a/server/src/modes/script/javascript.ts +++ b/server/src/modes/script/javascript.ts @@ -103,15 +103,15 @@ export function getJavascriptMode( doc.getText() ); - const { service } = updateCurrentTextDocument(templateDoc); - if (!languageServiceIncludesFile(service, templateDoc.uri)) { + const { templateService } = updateCurrentTextDocument(templateDoc); + if (!languageServiceIncludesFile(templateService, templateDoc.uri)) { return []; } const fileFsPath = getFileFsPath(templateDoc.uri); // We don't need syntactic diagnostics because // compiled template is always valid JavaScript syntax. - const diagnostics = service.getSemanticDiagnostics(fileFsPath); + const diagnostics = templateService.getSemanticDiagnostics(fileFsPath); return diagnostics.map(diag => { // syntactic/semantic diagnostic always has start and length diff --git a/server/src/modes/script/serviceHost.ts b/server/src/modes/script/serviceHost.ts index c45f644722..c6f8f5cf52 100644 --- a/server/src/modes/script/serviceHost.ts +++ b/server/src/modes/script/serviceHost.ts @@ -128,21 +128,21 @@ export function getServiceHost( } if (isVueTemplate(fileFsPath)) { scriptDocs.set(fileFsPath, doc); - // The version must be the same as doc version - versions.set(fileFsPath, doc.version); + versions.set(fileFsPath, (versions.get(fileFsPath) || 0) + 1); } else if (!currentScriptDoc || doc.uri !== currentScriptDoc.uri || doc.version !== currentScriptDoc.version) { currentScriptDoc = jsDocuments.get(doc); const lastDoc = scriptDocs.get(fileFsPath); if (lastDoc && currentScriptDoc.languageId !== lastDoc.languageId) { // if languageId changed, restart the language service; it can't handle file type changes jsLanguageService.dispose(); - jsLanguageService = ts.createLanguageService(host); + jsLanguageService = ts.createLanguageService(jsHost); } scriptDocs.set(fileFsPath, currentScriptDoc); versions.set(fileFsPath, (versions.get(fileFsPath) || 0) + 1); } return { service: jsLanguageService, + templateService: templateLanguageSerivice, scriptDoc: currentScriptDoc }; } @@ -151,102 +151,117 @@ export function getServiceHost( return scriptDocs.get(fsPath); } - const host: ts.LanguageServiceHost = { - getCompilationSettings: () => compilerOptions, - getScriptFileNames: () => files, - getScriptVersion(fileName) { - if (fileName === bridge.fileName) { - return '0'; - } - const normalizedFileFsPath = getNormalizedFileFsPath(fileName); - const version = versions.get(normalizedFileFsPath); - return version ? version.toString() : '0'; - }, - getScriptKind(fileName) { - if (isVue(fileName)) { - const uri = Uri.file(fileName); - fileName = uri.fsPath; - const doc = - scriptDocs.get(fileName) || - jsDocuments.get(TextDocument.create(uri.toString(), 'vue', 0, ts.sys.readFile(fileName) || '')); - return getScriptKind(doc.languageId); - } else { + function createLanguageServiceHost(options: ts.CompilerOptions): ts.LanguageServiceHost { + return { + getCompilationSettings: () => options, + getScriptFileNames: () => files, + getScriptVersion(fileName) { if (fileName === bridge.fileName) { - return ts.Extension.Ts; + return '0'; } - // NOTE: Typescript 2.3 should export getScriptKindFromFileName. Then this cast should be removed. - return (ts as any).getScriptKindFromFileName(fileName); - } - }, + const normalizedFileFsPath = getNormalizedFileFsPath(fileName); + const version = versions.get(normalizedFileFsPath); + return version ? version.toString() : '0'; + }, + getScriptKind(fileName) { + if (isVue(fileName)) { + const uri = Uri.file(fileName); + fileName = uri.fsPath; + const doc = + scriptDocs.get(fileName) || + jsDocuments.get(TextDocument.create(uri.toString(), 'vue', 0, ts.sys.readFile(fileName) || '')); + return getScriptKind(doc.languageId); + } else if (isVueTemplate(fileName)) { + return ts.Extension.Js; + } else { + if (fileName === bridge.fileName) { + return ts.Extension.Ts; + } + // NOTE: Typescript 2.3 should export getScriptKindFromFileName. Then this cast should be removed. + return (ts as any).getScriptKindFromFileName(fileName); + } + }, - // resolve @types, see https://github.com/Microsoft/TypeScript/issues/16772 - getDirectories: vueSys.getDirectories, - directoryExists: vueSys.directoryExists, - fileExists: vueSys.fileExists, - readFile: vueSys.readFile, - readDirectory: vueSys.readDirectory, + // resolve @types, see https://github.com/Microsoft/TypeScript/issues/16772 + getDirectories: vueSys.getDirectories, + directoryExists: vueSys.directoryExists, + fileExists: vueSys.fileExists, + readFile: vueSys.readFile, + readDirectory: vueSys.readDirectory, - resolveModuleNames(moduleNames: string[], containingFile: string): ts.ResolvedModule[] { - // in the normal case, delegate to ts.resolveModuleName - // in the relative-imported.vue case, manually build a resolved filename - return moduleNames.map(name => { - if (name === bridge.moduleName) { + resolveModuleNames(moduleNames: string[], containingFile: string): ts.ResolvedModule[] { + // in the normal case, delegate to ts.resolveModuleName + // in the relative-imported.vue case, manually build a resolved filename + return moduleNames.map(name => { + if (name === bridge.moduleName) { + return { + resolvedFileName: bridge.fileName, + extension: ts.Extension.Ts + }; + } + if (path.isAbsolute(name) || !isVue(name)) { + return ts.resolveModuleName(name, containingFile, options, ts.sys).resolvedModule; + } + const resolved = ts.resolveModuleName(name, containingFile, options, vueSys).resolvedModule; + if (!resolved) { + return undefined as any; + } + if (!resolved.resolvedFileName.endsWith('.vue.ts')) { + return resolved; + } + const resolvedFileName = resolved.resolvedFileName.slice(0, -3); + const uri = Uri.file(resolvedFileName); + const doc = + scriptDocs.get(resolvedFileName) || + jsDocuments.get(TextDocument.create(uri.toString(), 'vue', 0, ts.sys.readFile(resolvedFileName) || '')); + const extension = + doc.languageId === 'typescript' + ? ts.Extension.Ts + : doc.languageId === 'tsx' ? ts.Extension.Tsx : ts.Extension.Js; + return { resolvedFileName, extension }; + }); + }, + getScriptSnapshot: (fileName: string) => { + if (fileName === bridge.fileName) { + const text = isOldVersion ? bridge.oldContent : bridge.content; return { - resolvedFileName: bridge.fileName, - extension: ts.Extension.Ts + getText: (start, end) => text.substring(start, end), + getLength: () => text.length, + getChangeRange: () => void 0 }; } - if (path.isAbsolute(name) || !isVue(name)) { - return ts.resolveModuleName(name, containingFile, compilerOptions, ts.sys).resolvedModule; - } - const resolved = ts.resolveModuleName(name, containingFile, compilerOptions, vueSys).resolvedModule; - if (!resolved) { - return undefined as any; - } - if (!resolved.resolvedFileName.endsWith('.vue.ts')) { - return resolved; + const normalizedFileFsPath = getNormalizedFileFsPath(fileName); + const doc = scriptDocs.get(normalizedFileFsPath); + let fileText = doc ? doc.getText() : ts.sys.readFile(normalizedFileFsPath) || ''; + if (!doc && isVue(fileName)) { + // Note: This is required in addition to the parsing in embeddedSupport because + // this works for .vue files that aren't even loaded by VS Code yet. + fileText = parseVueScript(fileText); } - const resolvedFileName = resolved.resolvedFileName.slice(0, -3); - const uri = Uri.file(resolvedFileName); - const doc = - scriptDocs.get(resolvedFileName) || - jsDocuments.get(TextDocument.create(uri.toString(), 'vue', 0, ts.sys.readFile(resolvedFileName) || '')); - const extension = - doc.languageId === 'typescript' - ? ts.Extension.Ts - : doc.languageId === 'tsx' ? ts.Extension.Tsx : ts.Extension.Js; - return { resolvedFileName, extension }; - }); - }, - getScriptSnapshot: (fileName: string) => { - if (fileName === bridge.fileName) { - const text = isOldVersion ? bridge.oldContent : bridge.content; return { - getText: (start, end) => text.substring(start, end), - getLength: () => text.length, + getText: (start, end) => fileText.substring(start, end), + getLength: () => fileText.length, getChangeRange: () => void 0 }; - } - const normalizedFileFsPath = getNormalizedFileFsPath(fileName); - const doc = scriptDocs.get(normalizedFileFsPath); - let fileText = doc ? doc.getText() : ts.sys.readFile(normalizedFileFsPath) || ''; - if (!doc && isVue(fileName)) { - // Note: This is required in addition to the parsing in embeddedSupport because - // this works for .vue files that aren't even loaded by VS Code yet. - fileText = parseVueScript(fileText); - } - return { - getText: (start, end) => fileText.substring(start, end), - getLength: () => fileText.length, - getChangeRange: () => void 0 - }; - }, - getCurrentDirectory: () => workspacePath, - getDefaultLibFileName: ts.getDefaultLibFilePath, - getNewLine: () => '\n' - }; + }, + getCurrentDirectory: () => workspacePath, + getDefaultLibFileName: ts.getDefaultLibFilePath, + getNewLine: () => '\n' + }; + } + + const jsHost = createLanguageServiceHost(compilerOptions); + const templateHost = createLanguageServiceHost({ + ...compilerOptions, + noUnusedLocals: false, + noUnusedParameters: false, + allowJs: true, + checkJs: true + }); - let jsLanguageService = ts.createLanguageService(host); + const registry = ts.createDocumentRegistry(); + let jsLanguageService = ts.createLanguageService(jsHost, registry); + const templateLanguageSerivice = ts.createLanguageService(templateHost, registry); return { updateCurrentTextDocument, getScriptDocByFsPath, From 6fc1e5251463264298d47c850f2dcb5cd6c1cc81 Mon Sep 17 00:00:00 2001 From: ktsn Date: Mon, 5 Feb 2018 19:43:16 +0900 Subject: [PATCH 17/49] Handle v-on statement properly --- .../modes/script/test/template-integration.ts | 10 +++++ server/src/modes/script/transformTemplate.ts | 38 ++++++++++++------- .../component/template-checking/v-on.vue | 34 +++++++++++++++++ 3 files changed, 69 insertions(+), 13 deletions(-) create mode 100644 server/test/fixtures/component/template-checking/v-on.vue diff --git a/server/src/modes/script/test/template-integration.ts b/server/src/modes/script/test/template-integration.ts index 16428ad6a9..cf09dfade8 100644 --- a/server/src/modes/script/test/template-integration.ts +++ b/server/src/modes/script/test/template-integration.ts @@ -38,4 +38,14 @@ suite('template integrated test', () => { assert.equal(diagnostics.length, 1, 'diagnostic count'); assert(/Property 'bar' does not exist/.test(diagnostics[0].message), 'diagnostic message'); }); + + test('validate: v-on.vue', () => { + const filename = path.join(workspace + '/component/template-checking/v-on.vue'); + const doc = createTextDocument(filename); + const diagnostics = scriptMode.doTemplateValidation(doc); + assert.equal(diagnostics.length, 3, 'diagnostic count'); + assert(/Argument of type 'Event' is not assignable to parameter of type 'string'/.test(diagnostics[0].message)); + assert(/Argument of type '123' is not assignable to parameter of type 'string'/.test(diagnostics[1].message)); + assert(/Type '"test"' is not assignable to type 'number'/.test(diagnostics[2].message)); + }); }); diff --git a/server/src/modes/script/transformTemplate.ts b/server/src/modes/script/transformTemplate.ts index 4bddd1dfbd..ef827db819 100644 --- a/server/src/modes/script/transformTemplate.ts +++ b/server/src/modes/script/transformTemplate.ts @@ -14,6 +14,8 @@ const globalScope = ( 'require' ).split(','); +const vOnScope = ['$event', 'arguments']; + /** * Transform template AST to TypeScript AST. * Note: The returned TS AST is not compatible with @@ -113,16 +115,14 @@ function transformAttributes( let statements: ts.Statement[] = []; if (attr.value && attr.value.expression) { const exp = attr.value.expression as AST.VOnExpression; - statements = exp.body.map(st => transformStatement(st, code, scope)); + const newScope = scope.concat(vOnScope); + statements = exp.body.map(st => transformStatement(st, code, newScope)); } if (statements.length === 1) { const first = statements[0]; - if ( - ts.isExpressionStatement(first) && - ts.isIdentifier(first.expression) - ) { + if (isPathToIdentifier(first)) { statements[0] = ts.setTextRange(ts.createStatement( ts.setTextRange(ts.createCall( first.expression, @@ -133,15 +133,18 @@ function transformAttributes( } } - const exp = ts.createFunctionExpression(undefined, undefined, undefined, undefined, - [ts.createParameter(undefined, undefined, undefined, - '$event', - undefined, - ts.createTypeReferenceNode('Event', undefined) - )], + const exp = setTextRange(ts.createArrowFunction(undefined, undefined, + [ + setTextRange(ts.createParameter(undefined, undefined, undefined, + '$event', + undefined, + setTextRange(ts.createTypeReferenceNode('Event', undefined), attr) + ), attr) + ], undefined, - ts.createBlock(statements) - ); + setTextRange(ts.createToken(ts.SyntaxKind.EqualsGreaterThanToken), attr), + setTextRange(ts.createBlock(statements), attr) + ), attr); if (name) { return setTextRange(ts.createPropertyAssignment( @@ -358,6 +361,15 @@ function collectScope(param: ts.ParameterDeclaration | ts.BindingElement): strin } } +function isPathToIdentifier(statement: ts.Statement): statement is ts.ExpressionStatement { + if (ts.isExpressionStatement(statement)) { + const exp = statement.expression; + return ts.isIdentifier(exp) || ts.isPropertyAccessExpression(exp); + } else { + return false; + } +} + function isVAttribute(node: AST.VAttribute | AST.VDirective): node is AST.VAttribute { return !node.directive; } diff --git a/server/test/fixtures/component/template-checking/v-on.vue b/server/test/fixtures/component/template-checking/v-on.vue new file mode 100644 index 0000000000..b87ffbfc4f --- /dev/null +++ b/server/test/fixtures/component/template-checking/v-on.vue @@ -0,0 +1,34 @@ + + + From 60562ef4c447cf0bbda42c926fef73b28b34669f Mon Sep 17 00:00:00 2001 From: ktsn Date: Mon, 5 Feb 2018 21:45:20 +0900 Subject: [PATCH 18/49] Extract common logic of template checking test --- .../modes/script/test/template-integration.ts | 95 ++++++++++++++----- 1 file changed, 69 insertions(+), 26 deletions(-) diff --git a/server/src/modes/script/test/template-integration.ts b/server/src/modes/script/test/template-integration.ts index cf09dfade8..592e27bdf9 100644 --- a/server/src/modes/script/test/template-integration.ts +++ b/server/src/modes/script/test/template-integration.ts @@ -5,47 +5,90 @@ import { getJavascriptMode } from '../javascript'; import { getLanguageModelCache } from '../../languageModelCache'; import { getDocumentRegions } from '../../embeddedSupport'; import { createTextDocument } from './script-integration'; +import { Range } from 'vscode-languageserver'; const workspace = path.resolve(__dirname, '../../../../test/fixtures/'); const documentRegions = getLanguageModelCache(10, 60, document => getDocumentRegions(document)); const scriptMode = getJavascriptMode(documentRegions, workspace); +interface Expected { + includes: string; + range: Range; +} + +function check(file: string, expected: Expected[]): void { + const filename = path.join(workspace + '/component/template-checking/', file); + const doc = createTextDocument(filename); + const diagnostics = scriptMode.doTemplateValidation(doc); + assert.equal(diagnostics.length, expected.length, 'diagnostic count'); + + diagnostics.forEach((diag, i) => { + const e = expected[i]; + assert(diag.message.includes(e.includes), 'diagnostic message - index: ' + i); + assert.deepEqual(diag.range, e.range, 'diagnostic range of \'' + diag.message + '\''); + }); +} + suite('template integrated test', () => { test('validate: expression.vue', () => { - const filename = path.join(workspace + '/component/template-checking/expression.vue'); - const doc = createTextDocument(filename); - const diagnostics = scriptMode.doTemplateValidation(doc); - assert.equal(diagnostics.length, 1, 'diagnostic count'); - assert.deepEqual(diagnostics[0].range, { - start: { line: 1, character: 8 }, - end: { line: 1, character: 16 } - }); - assert(/Property 'messaage' does not exist/.test(diagnostics[0].message), 'diagnostic message'); + check('expression.vue', [ + { + includes: 'Property \'messaage\' does not exist', + range: { + start: { line: 1, character: 8 }, + end: { line: 1, character: 16 } + } + } + ]); }); test('validate: v-for.vue', () => { - const filename = path.join(workspace + '/component/template-checking/v-for.vue'); - const doc = createTextDocument(filename); - const diagnostics = scriptMode.doTemplateValidation(doc); - assert.equal(diagnostics.length, 1, 'diagnostic count'); - assert(/Property 'notExists' does not exist/.test(diagnostics[0].message), 'diagnostic message'); + check('v-for.vue', [ + { + includes: 'Property \'notExists\' does not exist', + range: { + start: { line: 5, character: 15 }, + end: { line: 5, character: 24 } + } + } + ]); }); test('validate: object-literal.vue', () => { - const filename = path.join(workspace + '/component/template-checking/object-literal.vue'); - const doc = createTextDocument(filename); - const diagnostics = scriptMode.doTemplateValidation(doc); - assert.equal(diagnostics.length, 1, 'diagnostic count'); - assert(/Property 'bar' does not exist/.test(diagnostics[0].message), 'diagnostic message'); + check('object-literal.vue', [ + { + includes: 'Property \'bar\' does not exist', + range: { + start: { line: 3, character: 9 }, + end: { line: 3, character: 12 } + } + } + ]); }); test('validate: v-on.vue', () => { - const filename = path.join(workspace + '/component/template-checking/v-on.vue'); - const doc = createTextDocument(filename); - const diagnostics = scriptMode.doTemplateValidation(doc); - assert.equal(diagnostics.length, 3, 'diagnostic count'); - assert(/Argument of type 'Event' is not assignable to parameter of type 'string'/.test(diagnostics[0].message)); - assert(/Argument of type '123' is not assignable to parameter of type 'string'/.test(diagnostics[1].message)); - assert(/Type '"test"' is not assignable to type 'number'/.test(diagnostics[2].message)); + check('v-on.vue', [ + { + includes: 'Argument of type \'Event\' is not assignable to parameter of type \'string\'', + range: { + start: { line: 9, character: 20 }, + end: { line: 9, character: 30 } + } + }, + { + includes: 'Argument of type \'123\' is not assignable to parameter of type \'string\'', + range: { + start: { line: 10, character: 31 }, + end: { line: 10, character: 34 } + } + }, + { + includes: 'Type \'"test"\' is not assignable to type \'number\'', + range: { + start: { line: 11, character: 20 }, + end: { line: 11, character: 24 } + } + } + ]); }); }); From 3138243c128678adbf00d7b75a429b5f646cf64a Mon Sep 17 00:00:00 2001 From: ktsn Date: Mon, 5 Feb 2018 23:07:00 +0900 Subject: [PATCH 19/49] Refactoring transformTemplate --- server/src/modes/script/transformTemplate.ts | 201 ++++++++++++------- 1 file changed, 129 insertions(+), 72 deletions(-) diff --git a/server/src/modes/script/transformTemplate.ts b/server/src/modes/script/transformTemplate.ts index ef827db819..7de08084a5 100644 --- a/server/src/modes/script/transformTemplate.ts +++ b/server/src/modes/script/transformTemplate.ts @@ -5,8 +5,10 @@ export const renderHelperName = '__veturRenderHelper'; export const componentHelperName = '__veturComponentHelper'; export const iterationHelperName = '__veturIterationHelper'; -// Allowed global variables in templates. -// From: https://github.com/vuejs/vue/blob/dev/src/core/instance/proxy.js +/** + * Allowed global variables in templates. + * Borrowed from: https://github.com/vuejs/vue/blob/dev/src/core/instance/proxy.js + */ const globalScope = ( 'Infinity,undefined,NaN,isFinite,isNaN,' + 'parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,' + @@ -21,6 +23,9 @@ const vOnScope = ['$event', 'arguments']; * Note: The returned TS AST is not compatible with * the regular Vue render function and does not work on runtime * because we just need type information for the template. + * Each TypeScript node should be set a range because + * the compiler may clash or do incorrect type inference + * when it has an invalid range. */ export function transformTemplate(program: AST.ESLintProgram, code: string): ts.Expression[] { const template = program.templateBody; @@ -32,6 +37,12 @@ export function transformTemplate(program: AST.ESLintProgram, code: string): ts. return template.children.map(c => transformChild(c, code, globalScope)); } +/** + * Transform an HTML to TypeScript AST. + * It will be a call expression like Vue's $createElement. + * e.g. + * __veturComponentHelper('div', { props: { title: this.foo } }, [ ...children... ]); + */ function transformElement(node: AST.VElement, code: string, scope: string[]): ts.Expression { const newScope = scope.concat(node.variables.map(v => v.id.name)); const element = setTextRange(ts.createCall( @@ -82,86 +93,116 @@ function transformAttributes( code: string, scope: string[] ): ts.Expression { - const literalProps = attrs.filter(isVAttribute).map(attr => { - return setTextRange(ts.createPropertyAssignment( - setTextRange(ts.createIdentifier(attr.key.name), attr.key), - attr.value - ? setTextRange(ts.createLiteral(attr.value.value), attr.value) - : ts.createLiteral('true') - ), attr); - }); + // Normal attributes + // e.g. class="title" + const literalProps = attrs + .filter(isVAttribute) + .map(transformNativeAttribute); + + // v-bind directives + // e.g. :class="{ selected: foo }" + const boundProps = attrs + .filter(isVBind) + .map(vBind => transformVBind(vBind, code, scope)); + + // v-on directives + // e.g. @click="onClick" + const listeners = attrs + .filter(isVOn) + .map(vOn => transformVOn(vOn, code, scope)); + + // Fold all AST into VNodeData-like object + // example output: + // { + // props: { class: 'title' }, + // on: { click: ($event) => onClick($event) } + // } + return ts.createObjectLiteral([ + ts.createPropertyAssignment('props', ts.createObjectLiteral( + [...literalProps, ...boundProps] + )), + ts.createPropertyAssignment('on', ts.createObjectLiteral(listeners)) + ]); +} +function transformNativeAttribute(attr: AST.VAttribute): ts.ObjectLiteralElementLike { + return setTextRange(ts.createPropertyAssignment( + setTextRange(ts.createIdentifier(attr.key.name), attr.key), + attr.value + ? setTextRange(ts.createLiteral(attr.value.value), attr.value) + : ts.createLiteral('true') + ), attr); +} - const boundProps = attrs.filter(isVBind).map(attr => { - const name = attr.key.argument; - const exp = (attr.value && attr.value.expression) - ? parseExpression(attr.value.expression as AST.ESLintExpression, code, scope) - : ts.createLiteral('true'); +function transformVBind(vBind: AST.VDirective, code: string, scope: string[]): ts.ObjectLiteralElementLike { + const name = vBind.key.argument; + const exp = (vBind.value && vBind.value.expression) + ? parseExpression(vBind.value.expression as AST.ESLintExpression, code, scope) + : ts.createLiteral('true'); - if (name) { - return setTextRange(ts.createPropertyAssignment( - setTextRange(ts.createIdentifier(name), attr.key), - exp - ), attr); - } else { - return setTextRange(ts.createSpreadAssignment(exp), attr); - } - }); + if (name) { + // Attribute name is specified + // e.g. :value="foo" + return setTextRange(ts.createPropertyAssignment( + setTextRange(ts.createIdentifier(name), vBind.key), + exp + ), vBind); + } else { + // Attribute name is omitted + // e.g. v-bind="{ value: foo }" + return setTextRange(ts.createSpreadAssignment(exp), vBind); + } +} +function transformVOn(vOn: AST.VDirective, code: string, scope: string[]): ts.ObjectLiteralElementLike { + const name = vOn.key.argument; - const listeners = attrs.filter(isVOn).map(attr => { - const name = attr.key.argument; + let statements: ts.Statement[] = []; + if (vOn.value && vOn.value.expression) { + const exp = vOn.value.expression as AST.VOnExpression; + const newScope = scope.concat(vOnScope); + statements = exp.body.map(st => transformStatement(st, code, newScope)); + } - let statements: ts.Statement[] = []; - if (attr.value && attr.value.expression) { - const exp = attr.value.expression as AST.VOnExpression; - const newScope = scope.concat(vOnScope); - statements = exp.body.map(st => transformStatement(st, code, newScope)); - } + if (statements.length === 1) { + const first = statements[0]; - if (statements.length === 1) { - const first = statements[0]; - - if (isPathToIdentifier(first)) { - statements[0] = ts.setTextRange(ts.createStatement( - ts.setTextRange(ts.createCall( - first.expression, - undefined, - [ts.setTextRange(ts.createIdentifier('$event'), first)] - ), first) - ), first); - } - } - - const exp = setTextRange(ts.createArrowFunction(undefined, undefined, - [ - setTextRange(ts.createParameter(undefined, undefined, undefined, - '$event', + if (isPathToIdentifier(first)) { + statements[0] = ts.setTextRange(ts.createStatement( + ts.setTextRange(ts.createCall( + first.expression, undefined, - setTextRange(ts.createTypeReferenceNode('Event', undefined), attr) - ), attr) - ], - undefined, - setTextRange(ts.createToken(ts.SyntaxKind.EqualsGreaterThanToken), attr), - setTextRange(ts.createBlock(statements), attr) - ), attr); - - if (name) { - return setTextRange(ts.createPropertyAssignment( - setTextRange(ts.createIdentifier(name), attr.key), - exp - ), attr); - } else { - return setTextRange(ts.createSpreadAssignment(exp), attr); + [ts.setTextRange(ts.createIdentifier('$event'), first)] + ), first) + ), first); } - }); + } - return ts.createObjectLiteral([ - ts.createPropertyAssignment('props', ts.createObjectLiteral( - [...literalProps, ...boundProps] - )), - ts.createPropertyAssignment('on', ts.createObjectLiteral(listeners)) - ]); + const exp = setTextRange(ts.createArrowFunction(undefined, undefined, + [ + setTextRange(ts.createParameter(undefined, undefined, undefined, + '$event', + undefined, + setTextRange(ts.createTypeReferenceNode('Event', undefined), vOn) + ), vOn) + ], + undefined, + setTextRange(ts.createToken(ts.SyntaxKind.EqualsGreaterThanToken), vOn), + setTextRange(ts.createBlock(statements), vOn) + ), vOn); + + if (name) { + // Event name is specified + // e.g. @click="onClick" + return setTextRange(ts.createPropertyAssignment( + setTextRange(ts.createIdentifier(name), vOn.key), + exp + ), vOn); + } else { + // Event name is omitted + // e.g. v-on="{ click: onClick }" + return setTextRange(ts.createSpreadAssignment(exp), vOn); + } } function transformChild( @@ -347,6 +388,14 @@ function injectThisForObjectLiteralElement( return ts.setTextRange(res, el); } +/** + * Collect newly added variable names from function parameters. + * e.g. + * If the function parameters look like following: + * (foo, { bar, baz: qux }) => { ... } + * The output should be: + * ['foo', 'bar', 'qux'] + */ function collectScope(param: ts.ParameterDeclaration | ts.BindingElement): string[] { const binding = param.name; if (ts.isIdentifier(binding)) { @@ -361,6 +410,14 @@ function collectScope(param: ts.ParameterDeclaration | ts.BindingElement): strin } } +/** + * Return `true` if the statement is a simple path to the identifier. + * Examples of `simple path`: + * foo + * this.foo.bar + * list[1] + * record['key'] + */ function isPathToIdentifier(statement: ts.Statement): statement is ts.ExpressionStatement { if (ts.isExpressionStatement(statement)) { const exp = statement.expression; From 76960404bba5d48cdc190af6c2b9262ad8c14038 Mon Sep 17 00:00:00 2001 From: ktsn Date: Mon, 5 Feb 2018 23:14:43 +0900 Subject: [PATCH 20/49] Remove unused @types/estree --- server/package.json | 7 +++---- server/yarn.lock | 15 +++++++++++---- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/server/package.json b/server/package.json index 74bcab1362..5a471e184c 100644 --- a/server/package.json +++ b/server/package.json @@ -38,14 +38,13 @@ "vscode-languageserver": "^3.5.0", "vscode-languageserver-types": "^3.5.0", "vscode-uri": "^1.0.1", - "vue-onsenui-helper-json": "^1.0.2", - "vuetify-helper-json": "^1.0.0", "vue-eslint-parser": "^2.0.1-beta.2", + "vue-onsenui-helper-json": "^1.0.2", "vue-template-compiler": "^2.5.3", - "vue-template-es2015-compiler": "^1.6.0" + "vue-template-es2015-compiler": "^1.6.0", + "vuetify-helper-json": "^1.0.0" }, "devDependencies": { - "@types/estree": "^0.0.38", "@types/glob": "^5.0.34", "@types/js-beautify": "0.0.31", "@types/lodash": "^4.14.91", diff --git a/server/yarn.lock b/server/yarn.lock index 8a379a9372..3eabcb1606 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -6,10 +6,6 @@ version "0.1.2" resolved "https://registry.yarnpkg.com/@emmetio/extract-abbreviation/-/extract-abbreviation-0.1.2.tgz#e1f1c06349f4b1a00241ba1ba8a719062f9194b0" -"@types/estree@^0.0.38": - version "0.0.38" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.38.tgz#c1be40aa933723c608820a99a373a16d215a1ca2" - "@types/events@*": version "1.1.0" resolved "https://registry.yarnpkg.com/@types/events/-/events-1.1.0.tgz#93b1be91f63c184450385272c47b6496fd028e02" @@ -68,6 +64,10 @@ acorn@^5.2.1: version "5.2.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.2.1.tgz#317ac7821826c22c702d66189ab8359675f135d7" +acorn@^5.4.0: + version "5.4.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.4.1.tgz#fdc58d9d17f4a4e98d102ded826a9b9759125102" + ajv-keywords@^1.0.0: version "1.5.1" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.5.1.tgz#314dd0a4b3368fad3dfcdc54ede6171b886daf3c" @@ -687,6 +687,13 @@ espree@^3.5.0: acorn "^5.1.1" acorn-jsx "^3.0.0" +espree@^3.5.1: + version "3.5.3" + resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.3.tgz#931e0af64e7fbbed26b050a29daad1fc64799fa6" + dependencies: + acorn "^5.4.0" + acorn-jsx "^3.0.0" + espree@^3.5.2: version "3.5.2" resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.2.tgz#756ada8b979e9dcfcdb30aad8d1a9304a905e1ca" From b7a805bca851527a6659adbada65bd28298830f0 Mon Sep 17 00:00:00 2001 From: ktsn Date: Mon, 5 Feb 2018 23:18:14 +0900 Subject: [PATCH 21/49] Use component constructor directly in generated template ts code --- server/src/modes/script/bridge.ts | 2 +- server/src/modes/script/preprocess.ts | 26 ++++++-------------------- 2 files changed, 7 insertions(+), 21 deletions(-) diff --git a/server/src/modes/script/bridge.ts b/server/src/modes/script/bridge.ts index 59f938e666..1f06b17834 100644 --- a/server/src/modes/script/bridge.ts +++ b/server/src/modes/script/bridge.ts @@ -9,7 +9,7 @@ export const fileName = 'vue-temp/vue-editor-bridge.ts'; const renderHelpers = ` export declare const ${renderHelperName}: { - (component: T, fn: (this: T) => any): any; + (Component: (new (...args: any[]) => T), fn: (this: T) => any): any; }; export declare const ${componentHelperName}: { (tag: string, data: any, children: any[]): any; diff --git a/server/src/modes/script/preprocess.ts b/server/src/modes/script/preprocess.ts index 3fcca6155b..2a7bfe457e 100644 --- a/server/src/modes/script/preprocess.ts +++ b/server/src/modes/script/preprocess.ts @@ -144,8 +144,8 @@ function modifyVueScript(sourceFile: ts.SourceFile): void { * to validate its types */ function injectVueTemplate(sourceFile: ts.SourceFile, renderBlock: ts.Expression[]): void { - // 1. add import statement for corresponding Vue file - // so that we acquire the component type from it. + // add import statement for corresponding Vue file + // so that we acquire the component type from it. const setZeroPos = getWrapperRangeSetter({ pos: 0, end: 0 }); const vueFilePath = './' + path.basename(sourceFile.fileName.slice(0, -9)); const componentImport = setZeroPos(ts.createImportDeclaration(undefined, @@ -176,21 +176,8 @@ function injectVueTemplate(sourceFile: ts.SourceFile, renderBlock: ts.Expression setZeroPos(ts.createLiteral('vue-editor-bridge')) )); - // 2. add a variable declaration of the component instance - const setMinPos = getWrapperRangeSetter({ pos: 0, end: 1 }); - const component = setZeroPos(ts.createVariableStatement(undefined, [ - setZeroPos(ts.createVariableDeclaration('__component', undefined, - setZeroPos(ts.createNew( - // we need set 1 or more length for identifier node to acquire a type from it. - setMinPos(ts.createIdentifier('__Component')), - undefined, - undefined - )) - )) - ])); - - // 3. wrap render code with a function decralation - // with `this` type of component. + // wrap render code with a function decralation + // with `this` type of component. const setRenderPos = getWrapperRangeSetter(sourceFile); const statements = renderBlock.map(exp => ts.createStatement(exp)); const renderElement = setRenderPos(ts.createStatement( @@ -199,7 +186,7 @@ function injectVueTemplate(sourceFile: ts.SourceFile, renderBlock: ts.Expression undefined, [ // Reference to the component - setRenderPos(ts.createIdentifier('__component')), + setRenderPos(ts.createIdentifier('__Component')), // A function simulating the render function setRenderPos(ts.createFunctionExpression( @@ -215,11 +202,10 @@ function injectVueTemplate(sourceFile: ts.SourceFile, renderBlock: ts.Expression )) )); - // 4. replace the original statements with wrapped code. + // replace the original statements with wrapped code. sourceFile.statements = setRenderPos(ts.createNodeArray([ componentImport, helperImport, - component, renderElement ])); } From a8ef51ecaff6b30d1f75a513e2971610212c55f8 Mon Sep 17 00:00:00 2001 From: ktsn Date: Mon, 5 Feb 2018 23:31:26 +0900 Subject: [PATCH 22/49] Bump Vue typings for testing --- .../template-checking/expression.vue | 2 - .../template-checking/object-literal.vue | 2 - .../component/template-checking/v-for.vue | 2 - .../component/template-checking/v-on.vue | 2 - .../node_modules/@types/vue/options.d.ts | 11 +++--- .../node_modules/@types/vue/vnode.d.ts | 8 ++-- .../fixtures/node_modules/@types/vue/vue.d.ts | 38 ++++++++++--------- server/test/fixtures/package.json | 2 +- 8 files changed, 30 insertions(+), 37 deletions(-) diff --git a/server/test/fixtures/component/template-checking/expression.vue b/server/test/fixtures/component/template-checking/expression.vue index a852d4d831..537e069c56 100644 --- a/server/test/fixtures/component/template-checking/expression.vue +++ b/server/test/fixtures/component/template-checking/expression.vue @@ -6,8 +6,6 @@ import Vue from 'vue'; export default Vue.extend({ - props: {}, - data() { return { message: 'Hello' diff --git a/server/test/fixtures/component/template-checking/object-literal.vue b/server/test/fixtures/component/template-checking/object-literal.vue index fa7cc11669..a7740ab65e 100644 --- a/server/test/fixtures/component/template-checking/object-literal.vue +++ b/server/test/fixtures/component/template-checking/object-literal.vue @@ -9,8 +9,6 @@ import Vue from 'vue' export default Vue.extend({ - props: {}, - data() { return { foo: true diff --git a/server/test/fixtures/component/template-checking/v-for.vue b/server/test/fixtures/component/template-checking/v-for.vue index 9a5c793722..1dbcc6e6c3 100644 --- a/server/test/fixtures/component/template-checking/v-for.vue +++ b/server/test/fixtures/component/template-checking/v-for.vue @@ -12,8 +12,6 @@ import Vue from 'vue'; export default Vue.extend({ - props: {}, - data() { return { list: ['foo', 'bar'], diff --git a/server/test/fixtures/component/template-checking/v-on.vue b/server/test/fixtures/component/template-checking/v-on.vue index b87ffbfc4f..a8f6097571 100644 --- a/server/test/fixtures/component/template-checking/v-on.vue +++ b/server/test/fixtures/component/template-checking/v-on.vue @@ -17,8 +17,6 @@ import Vue from 'vue' export default Vue.extend({ - props: {}, - data() { return { test: 0 diff --git a/server/test/fixtures/node_modules/@types/vue/options.d.ts b/server/test/fixtures/node_modules/@types/vue/options.d.ts index c4d822f69e..8401a93b98 100644 --- a/server/test/fixtures/node_modules/@types/vue/options.d.ts +++ b/server/test/fixtures/node_modules/@types/vue/options.d.ts @@ -9,8 +9,7 @@ type Constructor = { export type Component, Methods=DefaultMethods, Computed=DefaultComputed, Props=DefaultProps> = | typeof Vue | FunctionalComponentOptions - | ThisTypedComponentOptionsWithArrayProps - | ThisTypedComponentOptionsWithRecordProps; + | ComponentOptions interface EsModuleComponent { default: Component @@ -59,12 +58,12 @@ export interface ComponentOptions< PropsDef=PropsDefinition> { data?: Data; props?: PropsDef; - propsData?: Object; + propsData?: object; computed?: Accessors; methods?: Methods; watch?: Record | WatchHandler | string>; - el?: Element | String; + el?: Element | string; template?: string; render?(createElement: CreateElement): VNode; renderError?: (h: () => VNode, err: Error) => VNode; @@ -84,10 +83,10 @@ export interface ComponentOptions< directives?: { [key: string]: DirectiveFunction | DirectiveOptions }; components?: { [key: string]: Component | AsyncComponent }; - transitions?: { [key: string]: Object }; + transitions?: { [key: string]: object }; filters?: { [key: string]: Function }; - provide?: Object | (() => Object); + provide?: object | (() => object); inject?: InjectOptions; model?: { diff --git a/server/test/fixtures/node_modules/@types/vue/vnode.d.ts b/server/test/fixtures/node_modules/@types/vue/vnode.d.ts index ae72065f9b..2fd2ef13c5 100644 --- a/server/test/fixtures/node_modules/@types/vue/vnode.d.ts +++ b/server/test/fixtures/node_modules/@types/vue/vnode.d.ts @@ -27,8 +27,8 @@ export interface VNode { export interface VNodeComponentOptions { Ctor: typeof Vue; - propsData?: Object; - listeners?: Object; + propsData?: object; + listeners?: object; children?: VNodeChildren; tag?: string; } @@ -42,14 +42,14 @@ export interface VNodeData { staticClass?: string; class?: any; staticStyle?: { [key: string]: any }; - style?: Object[] | Object; + style?: object[] | object; props?: { [key: string]: any }; attrs?: { [key: string]: any }; domProps?: { [key: string]: any }; hook?: { [key: string]: Function }; on?: { [key: string]: Function | Function[] }; nativeOn?: { [key: string]: Function | Function[] }; - transition?: Object; + transition?: object; show?: boolean; inlineTemplate?: { render: Function; diff --git a/server/test/fixtures/node_modules/@types/vue/vue.d.ts b/server/test/fixtures/node_modules/@types/vue/vue.d.ts index 2b025150bc..045644e0a9 100644 --- a/server/test/fixtures/node_modules/@types/vue/vue.d.ts +++ b/server/test/fixtures/node_modules/@types/vue/vue.d.ts @@ -16,8 +16,8 @@ import { VNode, VNodeData, VNodeChildren, ScopedSlot } from "./vnode"; import { PluginFunction, PluginObject } from "./plugin"; export interface CreateElement { - (tag?: string | Component | AsyncComponent, children?: VNodeChildren): VNode; - (tag?: string | Component | AsyncComponent, data?: VNodeData, children?: VNodeChildren): VNode; + (tag?: string | Component | AsyncComponent | (() => Component), children?: VNodeChildren): VNode; + (tag?: string | Component | AsyncComponent | (() => Component), data?: VNodeData, children?: VNodeChildren): VNode; } export interface Vue { @@ -37,7 +37,7 @@ export interface Vue { readonly $attrs: Record; readonly $listeners: Record; - $mount(elementOrSelector?: Element | String, hydrating?: boolean): this; + $mount(elementOrSelector?: Element | string, hydrating?: boolean): this; $forceUpdate(): void; $destroy(): void; $set: typeof Vue.set; @@ -64,6 +64,18 @@ export interface Vue { export type CombinedVueInstance = Data & Methods & Computed & Props & Instance; export type ExtendedVue = VueConstructor & Vue>; +export interface VueConfiguration { + silent: boolean; + optionMergeStrategies: any; + devtools: boolean; + productionTip: boolean; + performance: boolean; + errorHandler(err: Error, vm: Vue, info: string): void; + warnHandler(msg: string, vm: Vue, trace: string): void; + ignoredElements: (string | RegExp)[]; + keyCodes: { [key: string]: number | number[] }; +} + export interface VueConstructor { new (options?: ThisTypedComponentOptionsWithArrayProps): CombinedVueInstance>; // ideally, the return type should just contains Props, not Record. But TS requires Base constructors must all have the same return type. @@ -72,15 +84,15 @@ export interface VueConstructor { extend(definition: FunctionalComponentOptions, PropNames[]>): ExtendedVue>; extend(definition: FunctionalComponentOptions>): ExtendedVue; - extend(options?: ThisTypedComponentOptionsWithArrayProps): ExtendedVue>; + extend(options?: ThisTypedComponentOptionsWithArrayProps): ExtendedVue>; extend(options?: ThisTypedComponentOptionsWithRecordProps): ExtendedVue; extend(options?: ComponentOptions): ExtendedVue; nextTick(callback: () => void, context?: any[]): void; nextTick(): Promise - set(object: Object, key: string, value: T): T; + set(object: object, key: string, value: T): T; set(array: T[], key: number, value: T): T; - delete(object: Object, key: string): void; + delete(object: object, key: string): void; delete(array: T[], key: number): void; directive( @@ -94,7 +106,7 @@ export interface VueConstructor { component(id: string, definition: AsyncComponent): ExtendedVue; component(id: string, definition: FunctionalComponentOptions, PropNames[]>): ExtendedVue>; component(id: string, definition: FunctionalComponentOptions>): ExtendedVue; - component(id: string, definition?: ThisTypedComponentOptionsWithArrayProps): ExtendedVue>; + component(id: string, definition?: ThisTypedComponentOptionsWithArrayProps): ExtendedVue>; component(id: string, definition?: ThisTypedComponentOptionsWithRecordProps): ExtendedVue; component(id: string, definition?: ComponentOptions): ExtendedVue; @@ -106,17 +118,7 @@ export interface VueConstructor { staticRenderFns: (() => VNode)[]; }; - config: { - silent: boolean; - optionMergeStrategies: any; - devtools: boolean; - productionTip: boolean; - performance: boolean; - errorHandler(err: Error, vm: Vue, info: string): void; - warnHandler(msg: string, vm: Vue, trace: string): void; - ignoredElements: (string | RegExp)[]; - keyCodes: { [key: string]: number | number[] }; - } + config: VueConfiguration; } export const Vue: VueConstructor; diff --git a/server/test/fixtures/package.json b/server/test/fixtures/package.json index ab12da4503..9249d6b231 100644 --- a/server/test/fixtures/package.json +++ b/server/test/fixtures/package.json @@ -4,7 +4,7 @@ "description": "", "main": "index.js", "dependencies": { - "vue": "^2.5.0" + "vue": "^2.5.13" }, "devDependencies": {}, "scripts": { From 0c97e7585ae696138e512271e9c73e4498e20976 Mon Sep 17 00:00:00 2001 From: ktsn Date: Mon, 5 Feb 2018 23:51:51 +0900 Subject: [PATCH 23/49] Remove unused packages --- server/package.json | 2 -- server/yarn.lock | 17 +---------------- 2 files changed, 1 insertion(+), 18 deletions(-) diff --git a/server/package.json b/server/package.json index 5a471e184c..9ec60126dc 100644 --- a/server/package.json +++ b/server/package.json @@ -40,8 +40,6 @@ "vscode-uri": "^1.0.1", "vue-eslint-parser": "^2.0.1-beta.2", "vue-onsenui-helper-json": "^1.0.2", - "vue-template-compiler": "^2.5.3", - "vue-template-es2015-compiler": "^1.6.0", "vuetify-helper-json": "^1.0.0" }, "devDependencies": { diff --git a/server/yarn.lock b/server/yarn.lock index 3eabcb1606..aa1ac4ac11 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -468,10 +468,6 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" -de-indent@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d" - debug@*, debug@^2.6.8: version "2.6.8" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc" @@ -1003,7 +999,7 @@ hawk@3.1.3, hawk@~3.1.3: hoek "2.x.x" sntp "1.x.x" -he@1.1.1, he@^1.1.0: +he@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" @@ -2290,17 +2286,6 @@ vue-onsenui-helper-json@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/vue-onsenui-helper-json/-/vue-onsenui-helper-json-1.0.2.tgz#b8c900fe3f89ba6a318335de73a55dee99a1846e" -vue-template-compiler@^2.5.3: - version "2.5.3" - resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.5.3.tgz#ab631b0694e211a6aaf0d800102b37836aae36a4" - dependencies: - de-indent "^1.0.2" - he "^1.1.0" - -vue-template-es2015-compiler@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.6.0.tgz#dc42697133302ce3017524356a6c61b7b69b4a18" - vuetify-helper-json@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/vuetify-helper-json/-/vuetify-helper-json-1.0.0.tgz#f429a6e6156bab865f9d6c79aa6739eb187dd778" From 1642733a2b09a64959b68b3dd7319cef92fa32f1 Mon Sep 17 00:00:00 2001 From: ktsn Date: Thu, 8 Feb 2018 11:28:17 +0900 Subject: [PATCH 24/49] Rename internal template helpers --- server/src/modes/script/transformTemplate.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/server/src/modes/script/transformTemplate.ts b/server/src/modes/script/transformTemplate.ts index 7de08084a5..20cb823000 100644 --- a/server/src/modes/script/transformTemplate.ts +++ b/server/src/modes/script/transformTemplate.ts @@ -1,9 +1,9 @@ import * as ts from 'typescript'; import { AST } from 'vue-eslint-parser'; -export const renderHelperName = '__veturRenderHelper'; -export const componentHelperName = '__veturComponentHelper'; -export const iterationHelperName = '__veturIterationHelper'; +export const renderHelperName = '__vlsRenderHelper'; +export const componentHelperName = '__vlsComponentHelper'; +export const iterationHelperName = '__vlsIterationHelper'; /** * Allowed global variables in templates. @@ -41,7 +41,7 @@ export function transformTemplate(program: AST.ESLintProgram, code: string): ts. * Transform an HTML to TypeScript AST. * It will be a call expression like Vue's $createElement. * e.g. - * __veturComponentHelper('div', { props: { title: this.foo } }, [ ...children... ]); + * __vlsComponentHelper('div', { props: { title: this.foo } }, [ ...children... ]); */ function transformElement(node: AST.VElement, code: string, scope: string[]): ts.Expression { const newScope = scope.concat(node.variables.map(v => v.id.name)); From ce43c75584b7a6ffa97f51a0c9d676a33be2c804 Mon Sep 17 00:00:00 2001 From: ktsn Date: Fri, 9 Feb 2018 03:40:07 +0900 Subject: [PATCH 25/49] Simplify v-on transformation --- .../modes/script/test/template-integration.ts | 15 ++--- server/src/modes/script/transformTemplate.ts | 55 ++++++++++--------- .../component/template-checking/v-on.vue | 1 - 3 files changed, 32 insertions(+), 39 deletions(-) diff --git a/server/src/modes/script/test/template-integration.ts b/server/src/modes/script/test/template-integration.ts index 592e27bdf9..0beef9a102 100644 --- a/server/src/modes/script/test/template-integration.ts +++ b/server/src/modes/script/test/template-integration.ts @@ -68,25 +68,18 @@ suite('template integrated test', () => { test('validate: v-on.vue', () => { check('v-on.vue', [ - { - includes: 'Argument of type \'Event\' is not assignable to parameter of type \'string\'', - range: { - start: { line: 9, character: 20 }, - end: { line: 9, character: 30 } - } - }, { includes: 'Argument of type \'123\' is not assignable to parameter of type \'string\'', range: { - start: { line: 10, character: 31 }, - end: { line: 10, character: 34 } + start: { line: 9, character: 31 }, + end: { line: 9, character: 34 } } }, { includes: 'Type \'"test"\' is not assignable to type \'number\'', range: { - start: { line: 11, character: 20 }, - end: { line: 11, character: 24 } + start: { line: 10, character: 20 }, + end: { line: 10, character: 24 } } } ]); diff --git a/server/src/modes/script/transformTemplate.ts b/server/src/modes/script/transformTemplate.ts index 20cb823000..e6bd900f9e 100644 --- a/server/src/modes/script/transformTemplate.ts +++ b/server/src/modes/script/transformTemplate.ts @@ -157,40 +157,41 @@ function transformVBind(vBind: AST.VDirective, code: string, scope: string[]): t function transformVOn(vOn: AST.VDirective, code: string, scope: string[]): ts.ObjectLiteralElementLike { const name = vOn.key.argument; - let statements: ts.Statement[] = []; + let exp: ts.Expression; if (vOn.value && vOn.value.expression) { - const exp = vOn.value.expression as AST.VOnExpression; + const vOnExp = vOn.value.expression as AST.VOnExpression; const newScope = scope.concat(vOnScope); - statements = exp.body.map(st => transformStatement(st, code, newScope)); - } + const statements = vOnExp.body.map(st => transformStatement(st, code, newScope)); - if (statements.length === 1) { const first = statements[0]; - - if (isPathToIdentifier(first)) { - statements[0] = ts.setTextRange(ts.createStatement( - ts.setTextRange(ts.createCall( - first.expression, - undefined, - [ts.setTextRange(ts.createIdentifier('$event'), first)] - ), first) - ), first); + if (statements.length === 1 && isPathToIdentifier(first)) { + // The v-on expression is simple path to a method + // e.g. @click="onClick" + exp = first.expression; + } else { + // The v-on has some complex expressions or statements. + // Then wrap them with a function so that they can use `$event` and `arguments`. + // e.g. + // @click="onClick($event, 'test')" + // @click="value = "foo"" + exp = setTextRange(ts.createArrowFunction(undefined, undefined, + [ + setTextRange(ts.createParameter(undefined, undefined, undefined, + '$event', + undefined, + setTextRange(ts.createTypeReferenceNode('Event', undefined), vOn) + ), vOn) + ], + undefined, + setTextRange(ts.createToken(ts.SyntaxKind.EqualsGreaterThanToken), vOn), + setTextRange(ts.createBlock(statements), vOn) + ), vOn); } + } else { + // There are no statement in v-on value + exp = ts.createLiteral(true); } - const exp = setTextRange(ts.createArrowFunction(undefined, undefined, - [ - setTextRange(ts.createParameter(undefined, undefined, undefined, - '$event', - undefined, - setTextRange(ts.createTypeReferenceNode('Event', undefined), vOn) - ), vOn) - ], - undefined, - setTextRange(ts.createToken(ts.SyntaxKind.EqualsGreaterThanToken), vOn), - setTextRange(ts.createBlock(statements), vOn) - ), vOn); - if (name) { // Event name is specified // e.g. @click="onClick" diff --git a/server/test/fixtures/component/template-checking/v-on.vue b/server/test/fixtures/component/template-checking/v-on.vue index a8f6097571..f6acdf7cdc 100644 --- a/server/test/fixtures/component/template-checking/v-on.vue +++ b/server/test/fixtures/component/template-checking/v-on.vue @@ -7,7 +7,6 @@ - From f2f32366b7cb615ef126a4036173477401946fbe Mon Sep 17 00:00:00 2001 From: ktsn Date: Tue, 11 Dec 2018 17:20:15 +0800 Subject: [PATCH 26/49] support literal iteration of v-for --- server/src/modes/script/bridge.ts | 1 + server/test/fixtures/component/template-checking/v-for.vue | 3 +++ 2 files changed, 4 insertions(+) diff --git a/server/src/modes/script/bridge.ts b/server/src/modes/script/bridge.ts index 1f06b17834..b6ea0263dd 100644 --- a/server/src/modes/script/bridge.ts +++ b/server/src/modes/script/bridge.ts @@ -17,6 +17,7 @@ export declare const ${componentHelperName}: { export declare const ${iterationHelperName}: { (list: T[], fn: (value: T, index: number) => any): any; (obj: { [key: string]: T }, fn: (value: T, key: string, index: number) => any): any; + (num: number, fn: (value: number) => any): any; (obj: object, fn: (value: any, key: string, index: number) => any): any; }; `; diff --git a/server/test/fixtures/component/template-checking/v-for.vue b/server/test/fixtures/component/template-checking/v-for.vue index 1dbcc6e6c3..753a239203 100644 --- a/server/test/fixtures/component/template-checking/v-for.vue +++ b/server/test/fixtures/component/template-checking/v-for.vue @@ -5,6 +5,9 @@ {{ value + key + i }} {{ notExists }}

+

+ {{ i }} +

From bca4ad489f029b310df01eadf3746f771796fd59 Mon Sep 17 00:00:00 2001 From: ktsn Date: Tue, 11 Dec 2018 17:30:56 +0800 Subject: [PATCH 27/49] add a flag to control template type check --- package.json | 5 +++++ server/src/modes/script/javascript.ts | 5 +++++ server/src/modes/script/test/template-integration.ts | 8 ++++++++ 3 files changed, 18 insertions(+) diff --git a/package.json b/package.json index f70b02dbdc..e61f21fa1b 100644 --- a/package.json +++ b/package.json @@ -359,6 +359,11 @@ ], "default": "off", "description": "Traces the communication between VS Code and Vue Language Server." + }, + "vetur.experimental.templateTypeCheck": { + "type": "boolean", + "default": false, + "description": "Type check expressions in