From cda434123e443dceb03f534a09908422af02c1b0 Mon Sep 17 00:00:00 2001 From: David Graham Date: Sun, 17 Dec 2017 07:51:09 -0600 Subject: [PATCH 1/8] add vue functionality, add tests --- .gitignore | 4 +- .travis.yml | 2 +- package.json | 10 +- src/IncrementalChecker.ts | 35 ++++- src/VueProgram.ts | 131 ++++++++++++++++++ src/index.ts | 8 +- src/service.ts | 3 +- test/integration/index.spec.js | 2 + test/integration/vue.spec.js | 147 +++++++++++++++++++++ test/integration/vue/src/example-wild.vue | 22 +++ test/integration/vue/src/example.vue | 22 +++ test/integration/vue/src/index.ts | 8 ++ test/integration/vue/src/syntacticError.ts | 2 + test/integration/vue/tsconfig.json | 12 ++ test/integration/vue/tslint.json | 9 ++ test/unit/VueProgram.spec.js | 56 ++++++++ 16 files changed, 461 insertions(+), 12 deletions(-) create mode 100644 src/VueProgram.ts create mode 100644 test/integration/vue.spec.js create mode 100644 test/integration/vue/src/example-wild.vue create mode 100644 test/integration/vue/src/example.vue create mode 100644 test/integration/vue/src/index.ts create mode 100644 test/integration/vue/src/syntacticError.ts create mode 100644 test/integration/vue/tsconfig.json create mode 100644 test/integration/vue/tslint.json create mode 100644 test/unit/VueProgram.spec.js diff --git a/.gitignore b/.gitignore index df5f1e83..c0b8ab88 100644 --- a/.gitignore +++ b/.gitignore @@ -17,9 +17,11 @@ jspm_packages # Optional npm cache directory .npm +package-lock.json # Optional REPL history .node_repl_history -# IDEA directory +# Editor directories and files .idea +.vscode \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 060b8449..70d05056 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,7 @@ deploy: email: opensource@realytics.io skip_cleanup: true api_key: - secure: RdoLwtLDz3PI8fhTg0wXeRuL9JAffNJSTJvNExk9Q+fcccBl8yaNU4AId5OcuDwO3pnb59nKv8fIidW9VzemZ8/eCqCBbCx4qzGFxN2uCB+aBwQ7Svu6GAJV0JIEgIXG+dkByRawd9hCIyvi6IvhIA/XbVbboV5ngjwSW317SA0lbznGHQmIT4o93+YpUuT+pkGC04eJGIH6cgAe61QGwN6WyGx8hfJ+K+OCksZ5A4RIeSkRHY4zRgmlGGtSwOutUPh3OMIdG40E0UXLn3MRdANzekMFSowMZ8aFliqhDsgk/zk8IC/X6OlPFSTAmO1IkKqZiJsr1rxmeJvC9GDxvRZ3Vyv3GncoteEQVjKFsYRlTvmEouECSxafl2aOqa1i2TqU8if5GjgB6mm+pTjDTzF4KB3QkEtRKgTuB47+EUc5zXIBrgIAb3sWu+QgaSFOreG49cGccCsU0viLe2WHbjzuNUP3IMO0x9tL/rm0RQmP3FjQSWlDg6VXYjJs5UcpiQLabgL3u4tso9aYprqTKihqfQkyfi0R5qH872ncGlASkzwbcZxiUeWau3yx46iZdckPNTAKw0afeodmr7aL6B+xEWHfZv9OBn0UE5I23jvIlI+wzyWHWUgeVikBx1ZZV4OqAHOTdEMHQivbir4eYGGAYtY6yxzLpyfD4g0xIzM= + secure: J4q7IGixHsEp5of9FG/Zefit8ba6bV7T1I0yV8TBNQHfwBaTC344PBhx3jRg8Nhv04vzkzY+s2cJYIuyqlfEeZrsBcFTvPE9LowR/4cMCoFjteb/gDGtW2AFw2M5pfJ6L4PHXtigO5emWs7xeT3R8KxcQn3e2nzJcGXShc0N9vCCDe601MKJFco5svoHqx5cAmHoknIQDPatddob9VdOUvaRGZWWh7Pv8pdDHGMcUk18TL67p55vMDXL8LvtHlOJpg106P0hX8a3NGhD0Oq9jGgmS77JwAOel36gncxou8CbEXlzpNsH65gJ2/tEJ3uSUZdHYQtQCiQtNzFbqI45JTu7UXkwgQv0Wxb7fnnNsz9StYmquVNp7MDw+Ol2/txA55tBBdSCeK+hy/X7cgI4tI/ftZ98c0YQbeUe/fdaV6PMxv0lPzED2We3N2Y080ineOib1ySsjY0diN2NM6NB8+9TJgqyLzd7X3WofJxExMFMRKuApCDCU7Bjw/FlKkISzvUOMmthL91LunQZ4x+ratiYVbz2cKa7PMcP0Qm7cFnosbyQKJWjM1KTOsCnoCS7iLZHxzKKE+prOWrssbcfENvpSTmouWrGppi9D8nwXDEFNyp+pVx8b7gwQ5GR/WjXb1PY/WkjBgNRaZbHzJlJzYTMBjNjlI1Nn0s4ncWhsZE= on: tags: true branch: master diff --git a/package.json b/package.json index 3827210d..85103c6d 100644 --- a/package.json +++ b/package.json @@ -50,8 +50,10 @@ "@types/lodash.startswith": "^4.2.3", "@types/minimatch": "^3.0.1", "@types/node": "^8.0.26", + "@types/resolve": "0.0.4", "@types/webpack": "^3.0.10", "chai": "^3.5.0", + "css-loader": "^0.28.7", "eslint": "^3.19.0", "istanbul": "^0.4.5", "mocha": "^3.4.1", @@ -62,6 +64,10 @@ "ts-loader": "^2.1.0", "tslint": "^5.0.0", "typescript": "^2.1.0", + "vue": "^2.5.9", + "vue-class-component": "^6.1.1", + "vue-loader": "^13.5.0", + "vue-template-compiler": "^2.5.9", "webpack": "^3.0.0" }, "peerDependencies": { @@ -76,6 +82,8 @@ "lodash.isfunction": "^3.0.8", "lodash.isstring": "^4.0.1", "lodash.startswith": "^4.2.1", - "minimatch": "^3.0.4" + "minimatch": "^3.0.4", + "resolve": "^1.5.0", + "vue-parser": "^1.1.3" } } diff --git a/src/IncrementalChecker.ts b/src/IncrementalChecker.ts index a66a67d7..56fdb704 100644 --- a/src/IncrementalChecker.ts +++ b/src/IncrementalChecker.ts @@ -9,6 +9,7 @@ import WorkSet = require('./WorkSet'); import NormalizedMessage = require('./NormalizedMessage'); import CancellationToken = require('./CancellationToken'); import minimatch = require('minimatch'); +import VueProgram = require('./VueProgram'); // Need some augmentation here - linterOptions.exclude is not (yet) part of the official // types for tslint. @@ -36,13 +37,16 @@ class IncrementalChecker { programConfig: ts.ParsedCommandLine; watcher: FilesWatcher; + vue: boolean; + constructor( programConfigFile: string, linterConfigFile: string | false, watchPaths: string[], workNumber: number, workDivision: number, - checkSyntacticErrors: boolean + checkSyntacticErrors: boolean, + vue: boolean ) { this.programConfigFile = programConfigFile; this.linterConfigFile = linterConfigFile; @@ -50,6 +54,7 @@ class IncrementalChecker { this.workNumber = workNumber || 0; this.workDivision = workDivision || 1; this.checkSyntacticErrors = checkSyntacticErrors || false; + this.vue = vue || false; // Use empty array of exclusions in general to avoid having // to check of its existence later on. this.linterExclusions = []; @@ -130,7 +135,8 @@ class IncrementalChecker { nextIteration() { if (!this.watcher) { - this.watcher = new FilesWatcher(this.watchPaths, ['.ts', '.tsx']); + const watchExtensions = this.vue ? ['.ts', '.tsx', '.vue'] : ['.ts', '.tsx']; + this.watcher = new FilesWatcher(this.watchPaths, watchExtensions); // connect watcher with register this.watcher.on('change', (filePath: string, stats: fs.Stats) => { @@ -143,10 +149,6 @@ class IncrementalChecker { this.watcher.watch(); } - if (!this.programConfig) { - this.programConfig = IncrementalChecker.loadProgramConfig(this.programConfigFile); - } - if (!this.linterConfig && this.linterConfigFile) { this.linterConfig = IncrementalChecker.loadLinterConfig(this.linterConfigFile); @@ -158,12 +160,31 @@ class IncrementalChecker { } } - this.program = IncrementalChecker.createProgram(this.programConfig, this.files, this.watcher, this.program); + this.program = this.vue ? this.loadVueProgram() : this.loadDefaultProgram(); + if (this.linterConfig) { this.linter = IncrementalChecker.createLinter(this.program); } } + loadVueProgram() { + this.programConfig = this.programConfig || VueProgram.loadProgramConfig(this.programConfigFile); + + return VueProgram.createProgram( + this.programConfig, + path.dirname(this.programConfigFile), + this.files, + this.watcher, + this.program + ); + } + + loadDefaultProgram() { + this.programConfig = this.programConfig || IncrementalChecker.loadProgramConfig(this.programConfigFile); + + return IncrementalChecker.createProgram(this.programConfig, this.files, this.watcher, this.program); + } + hasLinter() { return this.linter !== undefined; } diff --git a/src/VueProgram.ts b/src/VueProgram.ts new file mode 100644 index 00000000..00425b98 --- /dev/null +++ b/src/VueProgram.ts @@ -0,0 +1,131 @@ +import fs = require('fs'); +import path = require('path'); +import ts = require('typescript'); +import FilesRegister = require('./FilesRegister'); +import FilesWatcher = require('./FilesWatcher'); +import vueParser = require('vue-parser'); + +class VueProgram { + static loadProgramConfig(configFile: string) { + const extraExtensions = ['vue']; + + const parseConfigHost: ts.ParseConfigHost = { + fileExists: ts.sys.fileExists, + readFile: ts.sys.readFile, + useCaseSensitiveFileNames: ts.sys.useCaseSensitiveFileNames, + readDirectory: (rootDir, extensions, excludes, includes, depth) => { + return ts.sys.readDirectory(rootDir, extensions.concat(extraExtensions), excludes, includes, depth); + } + }; + + const parsed = ts.parseJsonConfigFileContent( + // Regardless of the setting in the tsconfig.json we want isolatedModules to be false + Object.assign(ts.readConfigFile(configFile, ts.sys.readFile).config, { isolatedModules: false }), + parseConfigHost, + path.dirname(configFile) + ); + + parsed.options.allowNonTsExtensions = true; + + return parsed; + } + + /** + * Since 99.9% of Vue projects use the wildcard '@/*', we only search for that in tsconfig CompilerOptions.paths. + * The path is resolved with thie given substitution and includes the CompilerOptions.baseUrl (if given). + * If no paths given in tsconfig, then the default substitution is '[tsconfig directory]/src'. + * (This is a fast, simplified inspiration of what's described here: https://github.com/Microsoft/TypeScript/issues/5039) + */ + public static resolveNonTsModuleName(moduleName: string, containingFile: string, basedir: string, options: ts.CompilerOptions) { + const baseUrl = options.baseUrl ? options.baseUrl : basedir; + const pattern = options.paths ? options.paths['@/*'] : undefined; + const substitution = pattern ? options.paths['@/*'][0].replace('*', '') : 'src'; + const isWildcard = moduleName.substr(0, 2) === '@/'; + const isRelative = !path.isAbsolute(moduleName); + + if (isWildcard) { + moduleName = path.resolve(baseUrl, substitution, moduleName.substr(2)); + } else if (isRelative) { + moduleName = path.resolve(path.dirname(containingFile), moduleName); + } + + return moduleName; + } + + static createProgram( + programConfig: ts.ParsedCommandLine, + basedir: string, + files: FilesRegister, + watcher: FilesWatcher, + oldProgram: ts.Program + ) { + const host = ts.createCompilerHost(programConfig.options); + const realGetSourceFile = host.getSourceFile; + + // We need a host that can parse Vue SFCs (single file components). + host.getSourceFile = (filePath, languageVersion, onError) => { + // first check if watcher is watching file - if not - check it's mtime + if (!watcher.isWatchingFile(filePath)) { + try { + const stats = fs.statSync(filePath); + + files.setMtime(filePath, stats.mtime.valueOf()); + } catch (e) { + // probably file does not exists + files.remove(filePath); + } + } + + // get source file only if there is no source in files register + if (!files.has(filePath) || !files.getData(filePath).source) { + files.mutateData(filePath, (data) => { + data.source = realGetSourceFile(filePath, languageVersion, onError); + }); + } + + let source = files.getData(filePath).source; + + // get typescript contents from Vue file + if (source && filePath.substr(-4) === '.vue') { + const parsed = vueParser.parse(source.text, 'script', { lang: ['ts', 'tsx', 'js', 'jsx'] }); + source = ts.createSourceFile(filePath, parsed, languageVersion, true); + } + + return source; + }; + + // We need a host with special module resolution for Vue files. + host.resolveModuleNames = (moduleNames, containingFile) => { + const resolvedModules: ts.ResolvedModule[] = []; + + for (const moduleName of moduleNames) { + // Try to use standard resolution. + const result = ts.resolveModuleName(moduleName, containingFile, programConfig.options, { + fileExists: host.fileExists, + readFile: host.readFile + }); + + if (result.resolvedModule) { + resolvedModules.push(result.resolvedModule); + } else { + // For non-ts extensions. + resolvedModules.push({ + resolvedFileName: VueProgram.resolveNonTsModuleName(moduleName, containingFile, basedir, programConfig.options), + extension: '.ts' + } as ts.ResolvedModuleFull); + } + } + + return resolvedModules; + }; + + return ts.createProgram( + programConfig.fileNames, + programConfig.options, + host, + oldProgram // re-use old program + ); + } +} + +export = VueProgram; diff --git a/src/index.ts b/src/index.ts index b5e9f661..83c0eb33 100644 --- a/src/index.ts +++ b/src/index.ts @@ -30,6 +30,7 @@ interface Options { checkSyntacticErrors: boolean; memoryLimit: number; workers: number; + vue: boolean; } /** @@ -84,6 +85,8 @@ class ForkTsCheckerWebpackPlugin { service: childProcess.ChildProcess; + vue: boolean; + constructor(options: Options) { options = options || {} as Options; this.options = Object.assign({}, options); @@ -126,6 +129,8 @@ class ForkTsCheckerWebpackPlugin { this.typescriptVersion = require('typescript').version; this.tslintVersion = this.tslint ? require('tslint').Linter.VERSION : undefined; + + this.vue = options.vue === true; // default false } static createFormatter(type: 'default' | 'codeframe', options: any) { @@ -313,7 +318,8 @@ class ForkTsCheckerWebpackPlugin { WATCH: this.isWatching ? this.watchPaths.join('|') : '', WORK_DIVISION: Math.max(1, this.workersNumber), MEMORY_LIMIT: this.memoryLimit, - CHECK_SYNTACTIC_ERRORS: this.checkSyntacticErrors + CHECK_SYNTACTIC_ERRORS: this.checkSyntacticErrors, + VUE: this.vue } ), stdio: ['inherit', 'inherit', 'inherit', 'ipc'] diff --git a/src/service.ts b/src/service.ts index 65b37ac9..7d60bd26 100644 --- a/src/service.ts +++ b/src/service.ts @@ -10,7 +10,8 @@ const checker = new IncrementalChecker( process.env.WATCH === '' ? [] : process.env.WATCH.split('|'), parseInt(process.env.WORK_NUMBER, 10), parseInt(process.env.WORK_DIVISION, 10), - process.env.CHECK_SYNTACTIC_ERRORS === 'true' + process.env.CHECK_SYNTACTIC_ERRORS === 'true', + process.env.VUE === 'true' ); function run(cancellationToken: CancellationToken) { diff --git a/test/integration/index.spec.js b/test/integration/index.spec.js index f014748d..f3c8d2bc 100644 --- a/test/integration/index.spec.js +++ b/test/integration/index.spec.js @@ -218,8 +218,10 @@ describe('[INTEGRATION] index', function () { it('should find syntactic errors when checkSyntacticErrors is true', function (callback) { var compiler = createCompiler({ checkSyntacticErrors: true }, true); + var logger = console; compiler.run(function(error, stats) { + // logger.info(stats.compilation.errors); expect(stats.compilation.errors.length).to.be.equal(2); callback(); }); diff --git a/test/integration/vue.spec.js b/test/integration/vue.spec.js new file mode 100644 index 00000000..c95bf8ed --- /dev/null +++ b/test/integration/vue.spec.js @@ -0,0 +1,147 @@ + +var describe = require('mocha').describe; +var it = require('mocha').it; +var expect = require('chai').expect; +var path = require('path'); +var webpack = require('webpack'); +var process = require('process'); +var ForkTsCheckerWebpackPlugin = require('../../lib/index'); +var IncrementalChecker = require('../../lib/IncrementalChecker'); +var CancellationToken = require('../../lib/CancellationToken'); + +describe('[INTEGRATION] vue', function () { + this.timeout(30000); + process.setMaxListeners(20); + var plugin; + var files; + var compiler; + var checker; + + function createCompiler(options) { + plugin = new ForkTsCheckerWebpackPlugin(Object.assign({}, options, { silent: true })); + + compiler = webpack({ + context: path.resolve(__dirname, './vue'), + entry: './src/index.ts', + output: { + path: path.resolve(__dirname, '../../tmp') + }, + resolve: { + extensions: ['.ts', '.js', '.vue', '.json'], + alias: { + '@': path.resolve(__dirname, './vue/src'), + } + }, + module: { + rules: [ + { + test: /\.vue$/, + loader: 'vue-loader' + }, + { + test: /\.ts$/, + loader: 'ts-loader', + options: { + appendTsSuffixTo: [/\.vue$/], + transpileOnly: true, + silent: true + } + } + ] + }, + plugins: [ + plugin + ] + }); + + files = { + 'example.vue': path.resolve(compiler.context, 'src/example.vue'), + 'syntacticError.ts': path.resolve(compiler.context, 'src/syntacticError.ts') + }; + + checker = new IncrementalChecker( + plugin.tsconfigPath, + plugin.tslintPath || false, + [compiler.context], + ForkTsCheckerWebpackPlugin.ONE_CPU, + 1, + plugin.checkSyntacticErrors, + plugin.vue + ); + + checker.nextIteration(); + } + + it('should create a Vue program config if vue=true', function () { + createCompiler({ vue: true }); + + var fileFound; + + fileFound = checker.programConfig.fileNames.indexOf(files['example.vue']) >= 0; + expect(fileFound).to.be.true; + + fileFound = checker.programConfig.fileNames.indexOf(files['syntacticError.ts']) >= 0; + expect(fileFound).to.be.true; + }); + + it('should not create a Vue program config if vue=false', function () { + createCompiler(); + + var fileFound; + + fileFound = checker.programConfig.fileNames.indexOf(files['example.vue']) >= 0; + expect(fileFound).to.be.false; + + fileFound = checker.programConfig.fileNames.indexOf(files['syntacticError.ts']) >= 0; + expect(fileFound).to.be.true; + }); + + it('should create a Vue program if vue=true', function () { + createCompiler({ vue: true }); + + var source; + + source = checker.program.getSourceFile(files['example.vue']); + expect(source).to.not.be.undefined; + + source = checker.program.getSourceFile(files['syntacticError.ts']); + expect(source).to.not.be.undefined; + }); + + it('should not create a Vue program if vue=false', function () { + createCompiler(); + + var source; + + source = checker.program.getSourceFile(files['example.vue']); + expect(source).to.be.undefined; + + source = checker.program.getSourceFile(files['syntacticError.ts']); + expect(source).to.not.be.undefined; + }); + + it('should get syntactic diagnostics from Vue program', function () { + createCompiler({ tslint: true, vue: true }); + + const diagnostics = checker.program.getSyntacticDiagnostics(); + expect(diagnostics.length).to.be.equal(1); + }); + + it('should not find syntactic errors when checkSyntacticErrors is false', function (callback) { + createCompiler({ tslint: true, vue: true }); + + compiler.run(function(error, stats) { + expect(stats.compilation.errors.length).to.be.equal(2); + callback(); + }); + }); + + it('should find syntactic errors when checkSyntacticErrors is true', function (callback) { + createCompiler({ tslint: true, vue: true, checkSyntacticErrors: true }); + + compiler.run(function(error, stats) { + expect(stats.compilation.errors.length).to.be.equal(3); + callback(); + }); + }); +}); \ No newline at end of file diff --git a/test/integration/vue/src/example-wild.vue b/test/integration/vue/src/example-wild.vue new file mode 100644 index 00000000..c64b7c10 --- /dev/null +++ b/test/integration/vue/src/example-wild.vue @@ -0,0 +1,22 @@ + + + + + diff --git a/test/integration/vue/src/example.vue b/test/integration/vue/src/example.vue new file mode 100644 index 00000000..23823834 --- /dev/null +++ b/test/integration/vue/src/example.vue @@ -0,0 +1,22 @@ + + + + + diff --git a/test/integration/vue/src/index.ts b/test/integration/vue/src/index.ts new file mode 100644 index 00000000..ba753b87 --- /dev/null +++ b/test/integration/vue/src/index.ts @@ -0,0 +1,8 @@ +import ExampleWild from "@/example-wild.vue"; +import Example from "./example.vue"; + +const foo = new Example(); +foo.msg = "foo"; + +const bar = new ExampleWild(); +bar.msg = "bar"; diff --git a/test/integration/vue/src/syntacticError.ts b/test/integration/vue/src/syntacticError.ts new file mode 100644 index 00000000..1655a39e --- /dev/null +++ b/test/integration/vue/src/syntacticError.ts @@ -0,0 +1,2 @@ +// Syntactic error +const array = [{} {}]; diff --git a/test/integration/vue/tsconfig.json b/test/integration/vue/tsconfig.json new file mode 100644 index 00000000..6e26e3b4 --- /dev/null +++ b/test/integration/vue/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "experimentalDecorators": true + }, + "include": [ + "src/**/*.ts", + "src/**/*.vue" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/test/integration/vue/tslint.json b/test/integration/vue/tslint.json new file mode 100644 index 00000000..dfff889c --- /dev/null +++ b/test/integration/vue/tslint.json @@ -0,0 +1,9 @@ +{ + "defaultSeverity": "warning", + "extends": [ + "tslint:recommended" + ], + "jsRules": {}, + "rules": {}, + "rulesDirectory": [] +} \ No newline at end of file diff --git a/test/unit/VueProgram.spec.js b/test/unit/VueProgram.spec.js new file mode 100644 index 00000000..73870d1d --- /dev/null +++ b/test/unit/VueProgram.spec.js @@ -0,0 +1,56 @@ +var describe = require('mocha').describe; +var it = require('mocha').it; +var expect = require('chai').expect; +var VueProgram = require('../../lib/VueProgram'); + +describe('[UNIT] VueProgram', function () { + it('should properly resolve relative module names', function() { + var basedir = '/base/dir'; + var containingFile = '/con/tain/ing/main.ts'; + var options = { + baseUrl: '/baseurl', + paths: { + '@/*': [ + 'src/*' + ] + } + } + var moduleNames = [ + './test.vue', + '../test.vue', + '../../test.vue' + ]; + + var resolvedModuleNames = moduleNames.map(function(moduleName) { + return VueProgram.resolveNonTsModuleName(moduleName, containingFile, basedir, options); + }); + + expect(resolvedModuleNames[0]).to.be.equal('/con/tain/ing/test.vue'); + expect(resolvedModuleNames[1]).to.be.equal('/con/tain/test.vue'); + expect(resolvedModuleNames[2]).to.be.equal('/con/test.vue'); + }); + + it('should properly resolve wildcard module names', function() { + var basedir = '/base/dir'; + var containingFile = '/con/tain/ing/main.ts'; + var options = {}; + var moduleName = '@/test.vue'; + + resolvedModuleName = VueProgram.resolveNonTsModuleName(moduleName, containingFile, basedir, options); + expect(resolvedModuleName).to.be.equal('/base/dir/src/test.vue'); + + options.baseUrl = '/baseurl1'; + resolvedModuleName = VueProgram.resolveNonTsModuleName(moduleName, containingFile, basedir, options); + expect(resolvedModuleName).to.be.equal('/baseurl1/src/test.vue'); + + options.baseUrl = '/baseurl2'; + options.paths = { '@/*': ['src1/*'] } + resolvedModuleName = VueProgram.resolveNonTsModuleName(moduleName, containingFile, basedir, options); + expect(resolvedModuleName).to.be.equal('/baseurl2/src1/test.vue'); + + options.baseUrl = '/baseurl3'; + options.paths = { '@/*': ['src1/src2/*'] } + resolvedModuleName = VueProgram.resolveNonTsModuleName(moduleName, containingFile, basedir, options); + expect(resolvedModuleName).to.be.equal('/baseurl3/src1/src2/test.vue'); + }); +}); \ No newline at end of file From 7ae27a824ad0dfd8c79400095e97c828df079287 Mon Sep 17 00:00:00 2001 From: David Graham Date: Sun, 17 Dec 2017 08:04:14 -0600 Subject: [PATCH 2/8] remove unnecessary import --- test/integration/vue.spec.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/integration/vue.spec.js b/test/integration/vue.spec.js index c95bf8ed..3343261e 100644 --- a/test/integration/vue.spec.js +++ b/test/integration/vue.spec.js @@ -7,7 +7,6 @@ var webpack = require('webpack'); var process = require('process'); var ForkTsCheckerWebpackPlugin = require('../../lib/index'); var IncrementalChecker = require('../../lib/IncrementalChecker'); -var CancellationToken = require('../../lib/CancellationToken'); describe('[INTEGRATION] vue', function () { this.timeout(30000); From c0340eda3d8bd3a81f1e94636547d3cc3205c5ca Mon Sep 17 00:00:00 2001 From: David Graham Date: Sun, 17 Dec 2017 08:14:41 -0600 Subject: [PATCH 3/8] remove extra spacing --- src/VueProgram.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/VueProgram.ts b/src/VueProgram.ts index 00425b98..18a8ef12 100644 --- a/src/VueProgram.ts +++ b/src/VueProgram.ts @@ -8,7 +8,7 @@ import vueParser = require('vue-parser'); class VueProgram { static loadProgramConfig(configFile: string) { const extraExtensions = ['vue']; - + const parseConfigHost: ts.ParseConfigHost = { fileExists: ts.sys.fileExists, readFile: ts.sys.readFile, From b3cfe273de2fb91a86365d58bd13e3a69e9427e5 Mon Sep 17 00:00:00 2001 From: David Graham Date: Sun, 17 Dec 2017 08:23:13 -0600 Subject: [PATCH 4/8] remove some debugging --- test/integration/index.spec.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/integration/index.spec.js b/test/integration/index.spec.js index f3c8d2bc..f014748d 100644 --- a/test/integration/index.spec.js +++ b/test/integration/index.spec.js @@ -218,10 +218,8 @@ describe('[INTEGRATION] index', function () { it('should find syntactic errors when checkSyntacticErrors is true', function (callback) { var compiler = createCompiler({ checkSyntacticErrors: true }, true); - var logger = console; compiler.run(function(error, stats) { - // logger.info(stats.compilation.errors); expect(stats.compilation.errors.length).to.be.equal(2); callback(); }); From 45abd0ea6df61305d4b5a132cc495375e98ad70f Mon Sep 17 00:00:00 2001 From: David Graham Date: Mon, 18 Dec 2017 16:04:31 -0600 Subject: [PATCH 5/8] restore original travis key --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 70d05056..060b8449 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,7 @@ deploy: email: opensource@realytics.io skip_cleanup: true api_key: - secure: J4q7IGixHsEp5of9FG/Zefit8ba6bV7T1I0yV8TBNQHfwBaTC344PBhx3jRg8Nhv04vzkzY+s2cJYIuyqlfEeZrsBcFTvPE9LowR/4cMCoFjteb/gDGtW2AFw2M5pfJ6L4PHXtigO5emWs7xeT3R8KxcQn3e2nzJcGXShc0N9vCCDe601MKJFco5svoHqx5cAmHoknIQDPatddob9VdOUvaRGZWWh7Pv8pdDHGMcUk18TL67p55vMDXL8LvtHlOJpg106P0hX8a3NGhD0Oq9jGgmS77JwAOel36gncxou8CbEXlzpNsH65gJ2/tEJ3uSUZdHYQtQCiQtNzFbqI45JTu7UXkwgQv0Wxb7fnnNsz9StYmquVNp7MDw+Ol2/txA55tBBdSCeK+hy/X7cgI4tI/ftZ98c0YQbeUe/fdaV6PMxv0lPzED2We3N2Y080ineOib1ySsjY0diN2NM6NB8+9TJgqyLzd7X3WofJxExMFMRKuApCDCU7Bjw/FlKkISzvUOMmthL91LunQZ4x+ratiYVbz2cKa7PMcP0Qm7cFnosbyQKJWjM1KTOsCnoCS7iLZHxzKKE+prOWrssbcfENvpSTmouWrGppi9D8nwXDEFNyp+pVx8b7gwQ5GR/WjXb1PY/WkjBgNRaZbHzJlJzYTMBjNjlI1Nn0s4ncWhsZE= + secure: RdoLwtLDz3PI8fhTg0wXeRuL9JAffNJSTJvNExk9Q+fcccBl8yaNU4AId5OcuDwO3pnb59nKv8fIidW9VzemZ8/eCqCBbCx4qzGFxN2uCB+aBwQ7Svu6GAJV0JIEgIXG+dkByRawd9hCIyvi6IvhIA/XbVbboV5ngjwSW317SA0lbznGHQmIT4o93+YpUuT+pkGC04eJGIH6cgAe61QGwN6WyGx8hfJ+K+OCksZ5A4RIeSkRHY4zRgmlGGtSwOutUPh3OMIdG40E0UXLn3MRdANzekMFSowMZ8aFliqhDsgk/zk8IC/X6OlPFSTAmO1IkKqZiJsr1rxmeJvC9GDxvRZ3Vyv3GncoteEQVjKFsYRlTvmEouECSxafl2aOqa1i2TqU8if5GjgB6mm+pTjDTzF4KB3QkEtRKgTuB47+EUc5zXIBrgIAb3sWu+QgaSFOreG49cGccCsU0viLe2WHbjzuNUP3IMO0x9tL/rm0RQmP3FjQSWlDg6VXYjJs5UcpiQLabgL3u4tso9aYprqTKihqfQkyfi0R5qH872ncGlASkzwbcZxiUeWau3yx46iZdckPNTAKw0afeodmr7aL6B+xEWHfZv9OBn0UE5I23jvIlI+wzyWHWUgeVikBx1ZZV4OqAHOTdEMHQivbir4eYGGAYtY6yxzLpyfD4g0xIzM= on: tags: true branch: master From 39c9012adcfc9d9fafa3bc02c98b1922bc683ae9 Mon Sep 17 00:00:00 2001 From: David Graham Date: Sat, 23 Dec 2017 11:09:15 -0600 Subject: [PATCH 6/8] fix parsing bug found after modifying non ts-vue files --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 85103c6d..d53e80c3 100644 --- a/package.json +++ b/package.json @@ -84,6 +84,6 @@ "lodash.startswith": "^4.2.1", "minimatch": "^3.0.4", "resolve": "^1.5.0", - "vue-parser": "^1.1.3" + "vue-parser": "^1.1.5" } } From c09b16dc4ea9070fd0ce4a18b28a443b5dd7f784 Mon Sep 17 00:00:00 2001 From: David Graham Date: Sun, 31 Dec 2017 18:56:48 -0600 Subject: [PATCH 7/8] fix import issue, update tests, update README --- README.md | 93 +++++++++++++++++++++++ src/VueProgram.ts | 24 ++++-- test/integration/vue.spec.js | 4 + test/integration/vue/src/css.d.ts | 5 ++ test/integration/vue/src/example-wild.vue | 2 + test/integration/vue/src/example.css | 3 + test/integration/vue/src/example.vue | 2 + test/unit/VueProgram.spec.js | 10 +++ 8 files changed, 138 insertions(+), 5 deletions(-) create mode 100644 test/integration/vue/src/css.d.ts create mode 100644 test/integration/vue/src/example.css diff --git a/README.md b/README.md index e7aaf852..2c1a7719 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,10 @@ should keep free 1 core for *build* and 1 core for a *system* *(for example syst node doesn't share memory between workers - keep in mind that memory usage will increase. Be aware that in some scenarios increasing workers number **can increase checking time**. Default: `ForkTsCheckerWebpackPlugin.ONE_CPU`. +* **vue** `boolean`: +If `true`, the linter and compiler will process VueJs single-file-component (.vue) files. See the +[Vue section](https://github.com/Realytics/fork-ts-checker-webpack-plugin#vue) further down for information on how to correctly setup your project. + ### Pre-computed consts: * `ForkTsCheckerWebpackPlugin.ONE_CPU` - always use one CPU * `ForkTsCheckerWebpackPlugin.ALL_CPUS` - always use all CPUs (will increase build time) @@ -141,5 +145,94 @@ This plugin provides some custom webpack hooks (all are sync): |`fork-ts-checker-emit`| Service will add errors and warnings to webpack compilation ('build' mode) | `diagnostics`, `lints`, `elapsed` | |`fork-ts-checker-done`| Service finished type checking and webpack finished compilation ('watch' mode) | `diagnostics`, `lints`, `elapsed` | +## Vue +1. Turn on the vue option in the plugin in your webpack config: + +``` + new ForkTsCheckerWebpackPlugin({ + tslint: true, + vue: true + }) +``` + +2. To activate TypeScript in your `.vue` files, you need to ensure your script tag's language attribute is set +to `ts` or `tsx` (also make sure you include the `.vue` extension in all your import statements as shown below): + +```html + +``` + +3. Ideally you are also using `ts-loader` (in transpileOnly mode). Your Webpack config rules may look something like this: + +``` +{ + test: /\.ts$/, + loader: 'ts-loader', + include: [resolve('src'), resolve('test')], + options: { + appendTsSuffixTo: [/\.vue$/], + transpileOnly: true + } +}, +{ + test: /\.vue$/, + loader: 'vue-loader', + options: vueLoaderConfig +}, +``` +4. Add rules to your `tslint.json` and they will be applied to Vue files. For example, you could apply the Standard JS rules [tslint-config-standard](https://github.com/blakeembrey/tslint-config-standard) like this: + +```json +{ + "defaultSeverity": "error", + "extends": [ + "tslint-config-standard" + ] +} +``` +5. Ensure your `tsconfig.json` includes .vue files: + +``` +// tsconfig.json +{ + "include": [ + "src/**/*.ts", + "src/**/*.vue" + ], + "exclude": [ + "node_modules" + ] +} +``` + +6. The commonly used `@` path wildcard will work if you set up a `baseUrl` and `paths` (in `compilerOptions`) to include `@/*`. If you don't set this, then +the fallback for the `@` wildcard will be `[tsconfig directory]/src` (we hope to make this more flexible on future releases): +``` +// tsconfig.json +{ + "compilerOptions": { + + // ... + + "baseUrl": ".", + "paths": { + "@/*": [ + "src/*" + ] + } + } +} + +// In a .ts or .vue file... +import Hello from '@/components/hello.vue' +``` + +7. If you are working in **VSCode**, you can get extensions [Vetur](https://marketplace.visualstudio.com/items?itemName=octref.vetur) and [TSLint Vue](https://marketplace.visualstudio.com/items?itemName=prograhammer.tslint-vue) to complete the developer workflow. + ## License MIT diff --git a/src/VueProgram.ts b/src/VueProgram.ts index 18a8ef12..57a2a47e 100644 --- a/src/VueProgram.ts +++ b/src/VueProgram.ts @@ -52,6 +52,10 @@ class VueProgram { return moduleName; } + public static isVue(filePath: string) { + return path.extname(filePath) === '.vue'; + } + static createProgram( programConfig: ts.ParsedCommandLine, basedir: string, @@ -86,7 +90,7 @@ class VueProgram { let source = files.getData(filePath).source; // get typescript contents from Vue file - if (source && filePath.substr(-4) === '.vue') { + if (source && VueProgram.isVue(filePath)) { const parsed = vueParser.parse(source.text, 'script', { lang: ['ts', 'tsx', 'js', 'jsx'] }); source = ts.createSourceFile(filePath, parsed, languageVersion, true); } @@ -109,10 +113,20 @@ class VueProgram { resolvedModules.push(result.resolvedModule); } else { // For non-ts extensions. - resolvedModules.push({ - resolvedFileName: VueProgram.resolveNonTsModuleName(moduleName, containingFile, basedir, programConfig.options), - extension: '.ts' - } as ts.ResolvedModuleFull); + const absolutePath = VueProgram.resolveNonTsModuleName(moduleName, containingFile, basedir, programConfig.options); + + if (VueProgram.isVue(moduleName)) { + resolvedModules.push({ + resolvedFileName: absolutePath, + extension: '.ts' + } as ts.ResolvedModuleFull); + } else { + resolvedModules.push({ + // If the file does exist, return an empty string (because we assume user has provided a ".d.ts" file for it). + resolvedFileName: host.fileExists(absolutePath) ? '' : absolutePath, + extension: '.ts' + } as ts.ResolvedModuleFull); + } } } diff --git a/test/integration/vue.spec.js b/test/integration/vue.spec.js index 3343261e..18eeced6 100644 --- a/test/integration/vue.spec.js +++ b/test/integration/vue.spec.js @@ -45,6 +45,10 @@ describe('[INTEGRATION] vue', function () { transpileOnly: true, silent: true } + }, + { + test: /\.css$/, + loader: 'css-loader' } ] }, diff --git a/test/integration/vue/src/css.d.ts b/test/integration/vue/src/css.d.ts new file mode 100644 index 00000000..ae764235 --- /dev/null +++ b/test/integration/vue/src/css.d.ts @@ -0,0 +1,5 @@ +declare module '*.css' { + const css = ''; + + export default css; +} diff --git a/test/integration/vue/src/example-wild.vue b/test/integration/vue/src/example-wild.vue index c64b7c10..37bf9981 100644 --- a/test/integration/vue/src/example-wild.vue +++ b/test/integration/vue/src/example-wild.vue @@ -7,11 +7,13 @@ diff --git a/test/integration/vue/src/example.css b/test/integration/vue/src/example.css new file mode 100644 index 00000000..da5d2635 --- /dev/null +++ b/test/integration/vue/src/example.css @@ -0,0 +1,3 @@ +.test { + background-color: blue; +} \ No newline at end of file diff --git a/test/integration/vue/src/example.vue b/test/integration/vue/src/example.vue index 23823834..4892c15d 100644 --- a/test/integration/vue/src/example.vue +++ b/test/integration/vue/src/example.vue @@ -7,11 +7,13 @@ diff --git a/test/unit/VueProgram.spec.js b/test/unit/VueProgram.spec.js index 73870d1d..6a2944cd 100644 --- a/test/unit/VueProgram.spec.js +++ b/test/unit/VueProgram.spec.js @@ -4,6 +4,16 @@ var expect = require('chai').expect; var VueProgram = require('../../lib/VueProgram'); describe('[UNIT] VueProgram', function () { + it('should determine if file is a Vue file', function() { + expect(VueProgram.isVue('./test.vue')).to.be.true; + expect(VueProgram.isVue('../test.vue')).to.be.true; + expect(VueProgram.isVue('../../test.vue')).to.be.true; + expect(VueProgram.isVue('@/test.vue')).to.be.true; + expect(VueProgram.isVue('../../.vue')).to.be.false; + expect(VueProgram.isVue('./test.css')).to.be.false; + expect(VueProgram.isVue('./')).to.be.false; + }); + it('should properly resolve relative module names', function() { var basedir = '/base/dir'; var containingFile = '/con/tain/ing/main.ts'; From 8776b9f2cfe3a60d310089caa4dafdeb222b16b4 Mon Sep 17 00:00:00 2001 From: David Graham Date: Sun, 31 Dec 2017 19:26:12 -0600 Subject: [PATCH 8/8] trigger Travis to try again --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2c1a7719..18dfc1c3 100644 --- a/README.md +++ b/README.md @@ -195,7 +195,7 @@ import Hello from '@/components/hello.vue' ] } ``` -5. Ensure your `tsconfig.json` includes .vue files: +5. Ensure your `tsconfig.json` includes .vue files: ``` // tsconfig.json