From 96b284635fd1db37a2b521845d09ab6a1aaefb8a Mon Sep 17 00:00:00 2001 From: Eugene Zolenko Date: Tue, 14 Mar 2017 19:04:59 -0600 Subject: [PATCH] Watch (#8) * - partial fix for watch mode (#6) * - trying to detect watch mode * - support for watch mode --- .gitignore | 2 +- README.md | 6 ++ dist/rollup-plugin-typescript2.cjs.js | 137 ++++++++++++++++--------- dist/rollup-plugin-typescript2.es.js | 141 +++++++++++++++++--------- package.json | 11 +- rollup.config.js | 2 +- src/host.ts | 10 +- src/icache.ts | 16 +++ src/index.ts | 72 ++++++++++--- src/rollingcache.ts | 37 ++++--- src/{cache.ts => tscache.ts} | 37 +++++-- 11 files changed, 329 insertions(+), 142 deletions(-) create mode 100644 src/icache.ts rename src/{cache.ts => tscache.ts} (87%) diff --git a/.gitignore b/.gitignore index 3c97547..0b325ff 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ /node_modules /npm-debug.log /typings -/.rts2_cache \ No newline at end of file +/.rpt2_cache \ No newline at end of file diff --git a/README.md b/README.md index a44a530..b976c53 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,12 @@ Plugin takes following options: On windows typescript resolver favors POSIX path, while commonjs plugin (and maybe others?) uses native path as module id. This can result in `namedExports` being ignored if rollup happened to use typescript's resolution. Set to true to pass resolved module path through `resolve()` to match up with `rollup-plugin-commonjs`. +### Watch mode + +The way typescript handles type-only imports and ambient types effectively hides them from rollup watch, because import statements are not generated and changing them doesn't trigger a rebuild. + +Otherwise the plugin should work in watch mode. Make sure to run a normal build after watch session to catch any type errors. + ### Version This plugin currently requires TypeScript `2.0+`. diff --git a/dist/rollup-plugin-typescript2.cjs.js b/dist/rollup-plugin-typescript2.cjs.js index a274c4e..0dc6ddb 100644 --- a/dist/rollup-plugin-typescript2.cjs.js +++ b/dist/rollup-plugin-typescript2.cjs.js @@ -113,6 +113,10 @@ var LanguageServiceHost = (function () { this.snapshots = {}; this.versions = {}; } + LanguageServiceHost.prototype.reset = function () { + this.snapshots = {}; + this.versions = {}; + }; LanguageServiceHost.prototype.setSnapshot = function (fileName, data) { var snapshot = ts.ScriptSnapshot.fromString(data); this.snapshots[fileName] = snapshot; @@ -131,8 +135,8 @@ var LanguageServiceHost = (function () { LanguageServiceHost.prototype.getCurrentDirectory = function () { return this.cwd; }; - LanguageServiceHost.prototype.getScriptVersion = function (_fileName) { - return (this.versions[_fileName] || 0).toString(); + LanguageServiceHost.prototype.getScriptVersion = function (fileName) { + return (this.versions[fileName] || 0).toString(); }; LanguageServiceHost.prototype.getScriptFileNames = function () { return this.parsedConfig.fileNames; @@ -158,6 +162,7 @@ var RollingCache = (function () { function RollingCache(cacheRoot, checkNewCache) { this.cacheRoot = cacheRoot; this.checkNewCache = checkNewCache; + this.rolled = false; this.oldCacheRoot = this.cacheRoot + "/cache"; this.newCacheRoot = this.cacheRoot + "/cache_"; fs.emptyDirSync(this.newCacheRoot); @@ -166,6 +171,8 @@ var RollingCache = (function () { * @returns true if name exist in old cache (or either old of new cache if checkNewCache is true) */ RollingCache.prototype.exists = function (name) { + if (this.rolled) + return false; if (this.checkNewCache && fs.existsSync(this.newCacheRoot + "/" + name)) return true; return fs.existsSync(this.oldCacheRoot + "/" + name); @@ -177,6 +184,8 @@ var RollingCache = (function () { * @returns true if old cache contains all names and nothing more */ RollingCache.prototype.match = function (names) { + if (this.rolled) + return false; if (!fs.existsSync(this.oldCacheRoot)) return names.length === 0; // empty folder matches return _.isEqual(fs.readdirSync(this.oldCacheRoot).sort(), names.sort()); @@ -190,27 +199,29 @@ var RollingCache = (function () { return fs.readJsonSync(this.oldCacheRoot + "/" + name, "utf8"); }; RollingCache.prototype.write = function (name, data) { + if (this.rolled) + return; if (data === undefined) return; - if (this.checkNewCache) - fs.writeJsonSync(this.newCacheRoot + "/" + name, data); + if (this.rolled) + fs.writeJsonSync(this.oldCacheRoot + "/" + name, data); else - fs.writeJson(this.newCacheRoot + "/" + name, data, { encoding: "utf8" }, function () { }); + fs.writeJsonSync(this.newCacheRoot + "/" + name, data); }; RollingCache.prototype.touch = function (name) { - if (this.checkNewCache) - fs.ensureFileSync(this.newCacheRoot + "/" + name); - else - fs.ensureFile(this.newCacheRoot + "/" + name, function () { }); + if (this.rolled) + return; + fs.ensureFileSync(this.newCacheRoot + "/" + name); }; /** * clears old cache and moves new in its place */ RollingCache.prototype.roll = function () { - var _this = this; - fs.remove(this.oldCacheRoot, function () { - fs.move(_this.newCacheRoot, _this.oldCacheRoot, function () { }); - }); + if (this.rolled) + return; + this.rolled = true; + fs.removeSync(this.oldCacheRoot); + fs.move(this.newCacheRoot, this.oldCacheRoot, function () { }); }; return RollingCache; }()); @@ -228,13 +239,13 @@ function convertDiagnostic(data) { return entry; }); } -var Cache = (function () { - function Cache(host, cache, options, rootFilenames, context) { +var TsCache = (function () { + function TsCache(host, cache, options, rootFilenames, context) { var _this = this; this.host = host; this.options = options; this.context = context; - this.cacheVersion = "2"; + this.cacheVersion = "3"; this.ambientTypesDirty = false; this.cacheDir = cache + "/" + hash.sha1({ version: this.cacheVersion, @@ -253,25 +264,34 @@ var Cache = (function () { this.init(); this.checkAmbientTypes(); } - Cache.prototype.clean = function () { + TsCache.prototype.clean = function () { this.context.info(colors.blue("cleaning cache: " + this.cacheDir)); fs.emptyDirSync(this.cacheDir); this.init(); }; - Cache.prototype.setDependency = function (importee, importer) { + TsCache.prototype.setDependency = function (importee, importer) { // importee -> importer this.context.debug(colors.blue("dependency") + " '" + importee + "'"); this.context.debug(" imported by '" + importer + "'"); this.dependencyTree.setEdge(importer, importee); }; - Cache.prototype.done = function () { + TsCache.prototype.walkTree = function (cb) { + var acyclic = graph.alg.isAcyclic(this.dependencyTree); + if (acyclic) { + _.each(graph.alg.topsort(this.dependencyTree), function (id) { return cb(id); }); + return; + } + this.context.info(colors.yellow("import tree has cycles")); + _.each(this.dependencyTree.nodes(), function (id) { return cb(id); }); + }; + TsCache.prototype.done = function () { this.context.info(colors.blue("rolling caches")); this.codeCache.roll(); this.semanticDiagnosticsCache.roll(); this.syntacticDiagnosticsCache.roll(); this.typesCache.roll(); }; - Cache.prototype.getCompiled = function (id, snapshot, transform) { + TsCache.prototype.getCompiled = function (id, snapshot, transform) { var name = this.makeName(id, snapshot); this.context.info(colors.blue("transpiling") + " '" + id + "'"); this.context.debug(" cache: '" + this.codeCache.path(name) + "'"); @@ -287,13 +307,13 @@ var Cache = (function () { this.codeCache.write(name, data); return data; }; - Cache.prototype.getSyntacticDiagnostics = function (id, snapshot, check) { + TsCache.prototype.getSyntacticDiagnostics = function (id, snapshot, check) { return this.getDiagnostics(this.syntacticDiagnosticsCache, id, snapshot, check); }; - Cache.prototype.getSemanticDiagnostics = function (id, snapshot, check) { + TsCache.prototype.getSemanticDiagnostics = function (id, snapshot, check) { return this.getDiagnostics(this.semanticDiagnosticsCache, id, snapshot, check); }; - Cache.prototype.checkAmbientTypes = function () { + TsCache.prototype.checkAmbientTypes = function () { var _this = this; this.context.debug(colors.blue("Ambient types:")); var typeNames = _.filter(this.ambientTypes, function (snapshot) { return snapshot.snapshot !== undefined; }) @@ -307,7 +327,7 @@ var Cache = (function () { this.context.info(colors.yellow("ambient types changed, redoing all semantic diagnostics")); _.each(typeNames, function (name) { return _this.typesCache.touch(name); }); }; - Cache.prototype.getDiagnostics = function (cache, id, snapshot, check) { + TsCache.prototype.getDiagnostics = function (cache, id, snapshot, check) { var name = this.makeName(id, snapshot); this.context.debug(" cache: '" + cache.path(name) + "'"); if (!cache.exists(name) || this.isDirty(id, snapshot, true)) { @@ -322,17 +342,17 @@ var Cache = (function () { cache.write(name, data); return data; }; - Cache.prototype.init = function () { + TsCache.prototype.init = function () { this.codeCache = new RollingCache(this.cacheDir + "/code", true); - this.typesCache = new RollingCache(this.cacheDir + "/types", false); - this.syntacticDiagnosticsCache = new RollingCache(this.cacheDir + "/syntacticDiagnostics", false); - this.semanticDiagnosticsCache = new RollingCache(this.cacheDir + "/semanticDiagnostics", false); + this.typesCache = new RollingCache(this.cacheDir + "/types", true); + this.syntacticDiagnosticsCache = new RollingCache(this.cacheDir + "/syntacticDiagnostics", true); + this.semanticDiagnosticsCache = new RollingCache(this.cacheDir + "/semanticDiagnostics", true); }; - Cache.prototype.markAsDirty = function (id, _snapshot) { + TsCache.prototype.markAsDirty = function (id, _snapshot) { this.dependencyTree.setNode(id, { dirty: true }); }; // returns true if node or any of its imports or any of global types changed - Cache.prototype.isDirty = function (id, _snapshot, checkImports) { + TsCache.prototype.isDirty = function (id, _snapshot, checkImports) { var _this = this; var label = this.dependencyTree.node(id); if (!label) @@ -352,11 +372,11 @@ var Cache = (function () { return dirty; }); }; - Cache.prototype.makeName = function (id, snapshot) { + TsCache.prototype.makeName = function (id, snapshot) { var data = snapshot.getText(0, snapshot.getLength()); return hash.sha1({ data: data, id: id }); }; - return Cache; + return TsCache; }()); // tslint:disable-next-line:no-var-requires @@ -432,20 +452,23 @@ function typescript(options) { abortOnError: true, rollupCommonJSResolveHack: false, }); + var watchMode = false; + var round = 0; + var targetCount = 0; var context = new ConsoleContext(options.verbosity, "rpt2: "); context.info("Typescript version: " + ts.version); context.debug("Options: " + JSON.stringify(options, undefined, 4)); var filter$$1 = createFilter(options.include, options.exclude); var parsedConfig = parseTsConfig(context); var servicesHost = new LanguageServiceHost(parsedConfig); - var services = ts.createLanguageService(servicesHost, ts.createDocumentRegistry()); - var cache = new Cache(servicesHost, options.cacheRoot, parsedConfig.options, parsedConfig.fileNames, context); - var cleanTranspile = true; + var service = ts.createLanguageService(servicesHost, ts.createDocumentRegistry()); + var cache = new TsCache(servicesHost, options.cacheRoot, parsedConfig.options, parsedConfig.fileNames, context); + var noErrors = true; if (options.clean) cache.clean(); // printing compiler option errors if (options.check) - printDiagnostics(context, convertDiagnostic(services.getCompilerOptionsDiagnostics())); + printDiagnostics(context, convertDiagnostic(service.getCompilerOptionsDiagnostics())); return { resolveId: function (importee, importer) { if (importee === TSLIB) @@ -481,14 +504,14 @@ function typescript(options) { var snapshot = servicesHost.setSnapshot(id, code); // getting compiled file from cache or from ts var result = cache.getCompiled(id, snapshot, function () { - var output = services.getEmitOutput(id); + var output = service.getEmitOutput(id); if (output.emitSkipped) { - cleanTranspile = false; + noErrors = false; // always checking on fatal errors, even if options.check is set to false var diagnostics = cache.getSyntacticDiagnostics(id, snapshot, function () { - return services.getSyntacticDiagnostics(id); + return service.getSyntacticDiagnostics(id); }).concat(cache.getSemanticDiagnostics(id, snapshot, function () { - return services.getSemanticDiagnostics(id); + return service.getSemanticDiagnostics(id); })); printDiagnostics(contextWrapper, diagnostics); // since no output was generated, aborting compilation @@ -503,20 +526,42 @@ function typescript(options) { }); if (options.check) { var diagnostics = cache.getSyntacticDiagnostics(id, snapshot, function () { - return services.getSyntacticDiagnostics(id); + return service.getSyntacticDiagnostics(id); }).concat(cache.getSemanticDiagnostics(id, snapshot, function () { - return services.getSemanticDiagnostics(id); + return service.getSemanticDiagnostics(id); })); if (diagnostics.length !== 0) - cleanTranspile = false; + noErrors = false; printDiagnostics(contextWrapper, diagnostics); } return result; }, - ongenerate: function () { - cache.done(); - if (!cleanTranspile) + ongenerate: function (bundleOptions) { + if (_.isArray(bundleOptions.targets)) + targetCount = bundleOptions.targets.length; + if (round >= targetCount) { + watchMode = true; + round = 0; + } + context.debug("generating target " + round + " of " + bundleOptions.targets.length); + if (watchMode && round === 0) { + context.debug("running in watch mode"); + // hack to fix ts lagging + servicesHost.reset(); + service.cleanupSemanticCache(); + cache.walkTree(function (id) { + var diagnostics = convertDiagnostic(service.getSyntacticDiagnostics(id)).concat(convertDiagnostic(service.getSemanticDiagnostics(id))); + if (diagnostics.length > 0) + noErrors = false; + printDiagnostics(context, diagnostics); + }); + } + if (!noErrors) { + noErrors = true; context.info(colors.yellow("there were errors or warnings above.")); + } + cache.done(); + round++; }, }; } diff --git a/dist/rollup-plugin-typescript2.es.js b/dist/rollup-plugin-typescript2.es.js index 6c0be4f..d9c7472 100644 --- a/dist/rollup-plugin-typescript2.es.js +++ b/dist/rollup-plugin-typescript2.es.js @@ -1,9 +1,9 @@ /* eslint-disable */ -import { emptyDirSync, ensureFile, ensureFileSync, existsSync, move, readFileSync, readJsonSync, readdirSync, remove, writeJson, writeJsonSync } from 'fs-extra'; +import { emptyDirSync, ensureFileSync, existsSync, move, readFileSync, readJsonSync, readdirSync, removeSync, writeJsonSync } from 'fs-extra'; import * as fs from 'fs-extra'; import { DiagnosticCategory, ModuleKind, ScriptSnapshot, createDocumentRegistry, createLanguageService, findConfigFile, flattenDiagnosticMessageText, getAutomaticTypeDirectiveNames, getDefaultLibFilePath, nodeModuleNameResolver, parseConfigFileTextToJson, parseJsonConfigFileContent, resolveTypeReferenceDirective, sys, version } from 'typescript'; import * as ts from 'typescript'; -import { defaults, each, endsWith, filter, find, has, isEqual, map, some } from 'lodash'; +import { defaults, each, endsWith, filter, find, has, isArray, isEqual, map, some } from 'lodash'; import * as _ from 'lodash'; import { Graph, alg } from 'graphlib'; import * as graph from 'graphlib'; @@ -119,6 +119,10 @@ var LanguageServiceHost = (function () { this.snapshots = {}; this.versions = {}; } + LanguageServiceHost.prototype.reset = function () { + this.snapshots = {}; + this.versions = {}; + }; LanguageServiceHost.prototype.setSnapshot = function (fileName, data) { var snapshot = ScriptSnapshot.fromString(data); this.snapshots[fileName] = snapshot; @@ -137,8 +141,8 @@ var LanguageServiceHost = (function () { LanguageServiceHost.prototype.getCurrentDirectory = function () { return this.cwd; }; - LanguageServiceHost.prototype.getScriptVersion = function (_fileName) { - return (this.versions[_fileName] || 0).toString(); + LanguageServiceHost.prototype.getScriptVersion = function (fileName) { + return (this.versions[fileName] || 0).toString(); }; LanguageServiceHost.prototype.getScriptFileNames = function () { return this.parsedConfig.fileNames; @@ -164,6 +168,7 @@ var RollingCache = (function () { function RollingCache(cacheRoot, checkNewCache) { this.cacheRoot = cacheRoot; this.checkNewCache = checkNewCache; + this.rolled = false; this.oldCacheRoot = this.cacheRoot + "/cache"; this.newCacheRoot = this.cacheRoot + "/cache_"; emptyDirSync(this.newCacheRoot); @@ -172,6 +177,8 @@ var RollingCache = (function () { * @returns true if name exist in old cache (or either old of new cache if checkNewCache is true) */ RollingCache.prototype.exists = function (name) { + if (this.rolled) + return false; if (this.checkNewCache && existsSync(this.newCacheRoot + "/" + name)) return true; return existsSync(this.oldCacheRoot + "/" + name); @@ -183,6 +190,8 @@ var RollingCache = (function () { * @returns true if old cache contains all names and nothing more */ RollingCache.prototype.match = function (names) { + if (this.rolled) + return false; if (!existsSync(this.oldCacheRoot)) return names.length === 0; // empty folder matches return isEqual(readdirSync(this.oldCacheRoot).sort(), names.sort()); @@ -196,27 +205,29 @@ var RollingCache = (function () { return readJsonSync(this.oldCacheRoot + "/" + name, "utf8"); }; RollingCache.prototype.write = function (name, data) { + if (this.rolled) + return; if (data === undefined) return; - if (this.checkNewCache) - writeJsonSync(this.newCacheRoot + "/" + name, data); + if (this.rolled) + writeJsonSync(this.oldCacheRoot + "/" + name, data); else - writeJson(this.newCacheRoot + "/" + name, data, { encoding: "utf8" }, function () { }); + writeJsonSync(this.newCacheRoot + "/" + name, data); }; RollingCache.prototype.touch = function (name) { - if (this.checkNewCache) - ensureFileSync(this.newCacheRoot + "/" + name); - else - ensureFile(this.newCacheRoot + "/" + name, function () { }); + if (this.rolled) + return; + ensureFileSync(this.newCacheRoot + "/" + name); }; /** * clears old cache and moves new in its place */ RollingCache.prototype.roll = function () { - var _this = this; - remove(this.oldCacheRoot, function () { - move(_this.newCacheRoot, _this.oldCacheRoot, function () { }); - }); + if (this.rolled) + return; + this.rolled = true; + removeSync(this.oldCacheRoot); + move(this.newCacheRoot, this.oldCacheRoot, function () { }); }; return RollingCache; }()); @@ -234,13 +245,13 @@ function convertDiagnostic(data) { return entry; }); } -var Cache = (function () { - function Cache(host, cache, options, rootFilenames, context) { +var TsCache = (function () { + function TsCache(host, cache, options, rootFilenames, context) { var _this = this; this.host = host; this.options = options; this.context = context; - this.cacheVersion = "2"; + this.cacheVersion = "3"; this.ambientTypesDirty = false; this.cacheDir = cache + "/" + sha1({ version: this.cacheVersion, @@ -259,25 +270,34 @@ var Cache = (function () { this.init(); this.checkAmbientTypes(); } - Cache.prototype.clean = function () { + TsCache.prototype.clean = function () { this.context.info(blue("cleaning cache: " + this.cacheDir)); emptyDirSync(this.cacheDir); this.init(); }; - Cache.prototype.setDependency = function (importee, importer) { + TsCache.prototype.setDependency = function (importee, importer) { // importee -> importer this.context.debug(blue("dependency") + " '" + importee + "'"); this.context.debug(" imported by '" + importer + "'"); this.dependencyTree.setEdge(importer, importee); }; - Cache.prototype.done = function () { + TsCache.prototype.walkTree = function (cb) { + var acyclic = alg.isAcyclic(this.dependencyTree); + if (acyclic) { + each(alg.topsort(this.dependencyTree), function (id) { return cb(id); }); + return; + } + this.context.info(yellow("import tree has cycles")); + each(this.dependencyTree.nodes(), function (id) { return cb(id); }); + }; + TsCache.prototype.done = function () { this.context.info(blue("rolling caches")); this.codeCache.roll(); this.semanticDiagnosticsCache.roll(); this.syntacticDiagnosticsCache.roll(); this.typesCache.roll(); }; - Cache.prototype.getCompiled = function (id, snapshot, transform) { + TsCache.prototype.getCompiled = function (id, snapshot, transform) { var name = this.makeName(id, snapshot); this.context.info(blue("transpiling") + " '" + id + "'"); this.context.debug(" cache: '" + this.codeCache.path(name) + "'"); @@ -293,13 +313,13 @@ var Cache = (function () { this.codeCache.write(name, data); return data; }; - Cache.prototype.getSyntacticDiagnostics = function (id, snapshot, check) { + TsCache.prototype.getSyntacticDiagnostics = function (id, snapshot, check) { return this.getDiagnostics(this.syntacticDiagnosticsCache, id, snapshot, check); }; - Cache.prototype.getSemanticDiagnostics = function (id, snapshot, check) { + TsCache.prototype.getSemanticDiagnostics = function (id, snapshot, check) { return this.getDiagnostics(this.semanticDiagnosticsCache, id, snapshot, check); }; - Cache.prototype.checkAmbientTypes = function () { + TsCache.prototype.checkAmbientTypes = function () { var _this = this; this.context.debug(blue("Ambient types:")); var typeNames = filter(this.ambientTypes, function (snapshot) { return snapshot.snapshot !== undefined; }) @@ -313,7 +333,7 @@ var Cache = (function () { this.context.info(yellow("ambient types changed, redoing all semantic diagnostics")); each(typeNames, function (name) { return _this.typesCache.touch(name); }); }; - Cache.prototype.getDiagnostics = function (cache, id, snapshot, check) { + TsCache.prototype.getDiagnostics = function (cache, id, snapshot, check) { var name = this.makeName(id, snapshot); this.context.debug(" cache: '" + cache.path(name) + "'"); if (!cache.exists(name) || this.isDirty(id, snapshot, true)) { @@ -328,17 +348,17 @@ var Cache = (function () { cache.write(name, data); return data; }; - Cache.prototype.init = function () { + TsCache.prototype.init = function () { this.codeCache = new RollingCache(this.cacheDir + "/code", true); - this.typesCache = new RollingCache(this.cacheDir + "/types", false); - this.syntacticDiagnosticsCache = new RollingCache(this.cacheDir + "/syntacticDiagnostics", false); - this.semanticDiagnosticsCache = new RollingCache(this.cacheDir + "/semanticDiagnostics", false); + this.typesCache = new RollingCache(this.cacheDir + "/types", true); + this.syntacticDiagnosticsCache = new RollingCache(this.cacheDir + "/syntacticDiagnostics", true); + this.semanticDiagnosticsCache = new RollingCache(this.cacheDir + "/semanticDiagnostics", true); }; - Cache.prototype.markAsDirty = function (id, _snapshot) { + TsCache.prototype.markAsDirty = function (id, _snapshot) { this.dependencyTree.setNode(id, { dirty: true }); }; // returns true if node or any of its imports or any of global types changed - Cache.prototype.isDirty = function (id, _snapshot, checkImports) { + TsCache.prototype.isDirty = function (id, _snapshot, checkImports) { var _this = this; var label = this.dependencyTree.node(id); if (!label) @@ -358,11 +378,11 @@ var Cache = (function () { return dirty; }); }; - Cache.prototype.makeName = function (id, snapshot) { + TsCache.prototype.makeName = function (id, snapshot) { var data = snapshot.getText(0, snapshot.getLength()); return sha1({ data: data, id: id }); }; - return Cache; + return TsCache; }()); // tslint:disable-next-line:no-var-requires @@ -438,20 +458,23 @@ function typescript(options) { abortOnError: true, rollupCommonJSResolveHack: false, }); + var watchMode = false; + var round = 0; + var targetCount = 0; var context = new ConsoleContext(options.verbosity, "rpt2: "); context.info("Typescript version: " + version); context.debug("Options: " + JSON.stringify(options, undefined, 4)); var filter$$1 = createFilter(options.include, options.exclude); var parsedConfig = parseTsConfig(context); var servicesHost = new LanguageServiceHost(parsedConfig); - var services = createLanguageService(servicesHost, createDocumentRegistry()); - var cache = new Cache(servicesHost, options.cacheRoot, parsedConfig.options, parsedConfig.fileNames, context); - var cleanTranspile = true; + var service = createLanguageService(servicesHost, createDocumentRegistry()); + var cache = new TsCache(servicesHost, options.cacheRoot, parsedConfig.options, parsedConfig.fileNames, context); + var noErrors = true; if (options.clean) cache.clean(); // printing compiler option errors if (options.check) - printDiagnostics(context, convertDiagnostic(services.getCompilerOptionsDiagnostics())); + printDiagnostics(context, convertDiagnostic(service.getCompilerOptionsDiagnostics())); return { resolveId: function (importee, importer) { if (importee === TSLIB) @@ -487,14 +510,14 @@ function typescript(options) { var snapshot = servicesHost.setSnapshot(id, code); // getting compiled file from cache or from ts var result = cache.getCompiled(id, snapshot, function () { - var output = services.getEmitOutput(id); + var output = service.getEmitOutput(id); if (output.emitSkipped) { - cleanTranspile = false; + noErrors = false; // always checking on fatal errors, even if options.check is set to false var diagnostics = cache.getSyntacticDiagnostics(id, snapshot, function () { - return services.getSyntacticDiagnostics(id); + return service.getSyntacticDiagnostics(id); }).concat(cache.getSemanticDiagnostics(id, snapshot, function () { - return services.getSemanticDiagnostics(id); + return service.getSemanticDiagnostics(id); })); printDiagnostics(contextWrapper, diagnostics); // since no output was generated, aborting compilation @@ -509,20 +532,42 @@ function typescript(options) { }); if (options.check) { var diagnostics = cache.getSyntacticDiagnostics(id, snapshot, function () { - return services.getSyntacticDiagnostics(id); + return service.getSyntacticDiagnostics(id); }).concat(cache.getSemanticDiagnostics(id, snapshot, function () { - return services.getSemanticDiagnostics(id); + return service.getSemanticDiagnostics(id); })); if (diagnostics.length !== 0) - cleanTranspile = false; + noErrors = false; printDiagnostics(contextWrapper, diagnostics); } return result; }, - ongenerate: function () { - cache.done(); - if (!cleanTranspile) + ongenerate: function (bundleOptions) { + if (isArray(bundleOptions.targets)) + targetCount = bundleOptions.targets.length; + if (round >= targetCount) { + watchMode = true; + round = 0; + } + context.debug("generating target " + round + " of " + bundleOptions.targets.length); + if (watchMode && round === 0) { + context.debug("running in watch mode"); + // hack to fix ts lagging + servicesHost.reset(); + service.cleanupSemanticCache(); + cache.walkTree(function (id) { + var diagnostics = convertDiagnostic(service.getSyntacticDiagnostics(id)).concat(convertDiagnostic(service.getSemanticDiagnostics(id))); + if (diagnostics.length > 0) + noErrors = false; + printDiagnostics(context, diagnostics); + }); + } + if (!noErrors) { + noErrors = true; context.info(yellow("there were errors or warnings above.")); + } + cache.done(); + round++; }, }; } diff --git a/package.json b/package.json index 3f8a978..84d254c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rollup-plugin-typescript2", - "version": "0.3.0", + "version": "0.4.0", "description": "Seamless integration between Rollup and TypeScript. Now with errors.", "main": "dist/rollup-plugin-typescript2.cjs.js", "module": "dist/rollup-plugin-typescript2.es.js", @@ -43,13 +43,14 @@ "@types/node": "^6.0.53", "@types/object-hash": "^0.5.28", "@types/resolve": "0.0.4", + "resolve": "^1.1.7", "rimraf": "^2.5.4", "rollup": "^0.41.4", - "rollup-plugin-typescript2": "^0.2.2", + "rollup-plugin-typescript2": "^0.3.0", + "rollup-watch": "^3.2.2", + "tslib": "^1.6.0", "tslint": "^4.5.1", - "typescript": "^2.2.1", - "resolve": "^1.1.7", - "tslib": "^1.6.0" + "typescript": "^2.2.1" }, "repository": { "type": "git", diff --git a/rollup.config.js b/rollup.config.js index 24c4198..7c41a54 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -19,7 +19,7 @@ export default { ], plugins: [ - ts({ verbosity: 2 }), + ts({ verbosity: 3 }), ], banner: '/* eslint-disable */', diff --git a/src/host.ts b/src/host.ts index a9f873d..f6b7a53 100644 --- a/src/host.ts +++ b/src/host.ts @@ -12,6 +12,12 @@ export class LanguageServiceHost implements ts.LanguageServiceHost { } + public reset() + { + this.snapshots = {}; + this.versions = {}; + } + public setSnapshot(fileName: string, data: string): ts.IScriptSnapshot { let snapshot = ts.ScriptSnapshot.fromString(data); @@ -39,9 +45,9 @@ export class LanguageServiceHost implements ts.LanguageServiceHost return this.cwd; } - public getScriptVersion(_fileName: string) + public getScriptVersion(fileName: string) { - return (this.versions[_fileName] || 0).toString(); + return (this.versions[fileName] || 0).toString(); } public getScriptFileNames() diff --git a/src/icache.ts b/src/icache.ts new file mode 100644 index 0000000..9885a3a --- /dev/null +++ b/src/icache.ts @@ -0,0 +1,16 @@ +export interface ICache +{ + exists(name: string): boolean; + + path(name: string): string; + + match(names: string[]): boolean; + + read(name: string): DataType; + + write(name: string, data: DataType): void; + + touch(name: string): void; + + roll(): void; +}; diff --git a/src/index.ts b/src/index.ts index b575dd0..587ca27 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ import { RollupContext } from "./rollupcontext"; import { IContext, ConsoleContext, IRollupContext, VerbosityLevel } from "./context"; import { LanguageServiceHost } from "./host"; -import { Cache, convertDiagnostic, ICode, IDiagnostics } from "./cache"; +import { TsCache, convertDiagnostic, ICode, IDiagnostics } from "./tscache"; import * as ts from "typescript"; import * as fs from "fs-extra"; import * as path from "path"; @@ -115,6 +115,10 @@ export default function typescript (options: IOptions) rollupCommonJSResolveHack: false, }); + let watchMode = false; + let round = 0; + let targetCount = 0; + const context = new ConsoleContext(options.verbosity, "rpt2: "); context.info(`Typescript version: ${ts.version}`); @@ -124,20 +128,20 @@ export default function typescript (options: IOptions) const parsedConfig = parseTsConfig(context); - const servicesHost = new LanguageServiceHost(parsedConfig); + let servicesHost = new LanguageServiceHost(parsedConfig); - const services = ts.createLanguageService(servicesHost, ts.createDocumentRegistry()); + let service = ts.createLanguageService(servicesHost, ts.createDocumentRegistry()); - const cache = new Cache(servicesHost, options.cacheRoot, parsedConfig.options, parsedConfig.fileNames, context); + const cache = new TsCache(servicesHost, options.cacheRoot, parsedConfig.options, parsedConfig.fileNames, context); - let cleanTranspile = true; + let noErrors = true; if (options.clean) cache.clean(); // printing compiler option errors if (options.check) - printDiagnostics(context, convertDiagnostic(services.getCompilerOptionsDiagnostics())); + printDiagnostics(context, convertDiagnostic(service.getCompilerOptionsDiagnostics())); return { @@ -194,19 +198,19 @@ export default function typescript (options: IOptions) // getting compiled file from cache or from ts const result = cache.getCompiled(id, snapshot, () => { - const output = services.getEmitOutput(id); + const output = service.getEmitOutput(id); if (output.emitSkipped) { - cleanTranspile = false; + noErrors = false; // always checking on fatal errors, even if options.check is set to false const diagnostics = cache.getSyntacticDiagnostics(id, snapshot, () => { - return services.getSyntacticDiagnostics(id); + return service.getSyntacticDiagnostics(id); }).concat(cache.getSemanticDiagnostics(id, snapshot, () => { - return services.getSemanticDiagnostics(id); + return service.getSemanticDiagnostics(id); })); printDiagnostics(contextWrapper, diagnostics); @@ -227,14 +231,14 @@ export default function typescript (options: IOptions) { const diagnostics = cache.getSyntacticDiagnostics(id, snapshot, () => { - return services.getSyntacticDiagnostics(id); + return service.getSyntacticDiagnostics(id); }).concat(cache.getSemanticDiagnostics(id, snapshot, () => { - return services.getSemanticDiagnostics(id); + return service.getSemanticDiagnostics(id); })); if (diagnostics.length !== 0) - cleanTranspile = false; + noErrors = false; printDiagnostics(contextWrapper, diagnostics); } @@ -242,11 +246,47 @@ export default function typescript (options: IOptions) return result; }, - ongenerate(): void + ongenerate(bundleOptions: any): void { - cache.done(); - if (!cleanTranspile) + if (_.isArray(bundleOptions.targets)) + targetCount = bundleOptions.targets.length; + + if (round >= targetCount) // ongenerate() is called for each target + { + watchMode = true; + round = 0; + } + context.debug(`generating target ${round} of ${bundleOptions.targets.length}`); + + if (watchMode && round === 0) + { + context.debug("running in watch mode"); + + // hack to fix ts lagging + servicesHost.reset(); + service.cleanupSemanticCache(); + + cache.walkTree((id) => + { + const diagnostics = convertDiagnostic(service.getSyntacticDiagnostics(id)).concat(convertDiagnostic(service.getSemanticDiagnostics(id))); + + if (diagnostics.length > 0) + noErrors = false; + + printDiagnostics(context, diagnostics); + }); + + } + + if (!noErrors) + { + noErrors = true; context.info(colors.yellow("there were errors or warnings above.")); + } + + cache.done(); + + round++; }, }; } diff --git a/src/rollingcache.ts b/src/rollingcache.ts index 43214b9..d2e2901 100644 --- a/src/rollingcache.ts +++ b/src/rollingcache.ts @@ -1,3 +1,4 @@ +import { ICache } from "./icache"; import * as fs from "fs-extra"; import * as _ from "lodash"; @@ -5,11 +6,13 @@ import * as _ from "lodash"; * Saves data in new cache folder or reads it from old one. * Avoids perpetually growing cache and situations when things need to consider changed and then reverted data to be changed. */ -export class RollingCache +export class RollingCache implements ICache { private oldCacheRoot: string; private newCacheRoot: string; + private rolled: boolean = false; + /** * @param cacheRoot: root folder for the cache * @param checkNewCache: whether to also look in new cache when reading from cache @@ -27,6 +30,9 @@ export class RollingCache */ public exists(name: string): boolean { + if (this.rolled) + return false; + if (this.checkNewCache && fs.existsSync(`${this.newCacheRoot}/${name}`)) return true; @@ -43,6 +49,9 @@ export class RollingCache */ public match(names: string[]): boolean { + if (this.rolled) + return false; + if (!fs.existsSync(this.oldCacheRoot)) return names.length === 0; // empty folder matches @@ -62,21 +71,23 @@ export class RollingCache public write(name: string, data: DataType): void { + if (this.rolled) + return; + if (data === undefined) return; - if (this.checkNewCache) + if (this.rolled) + fs.writeJsonSync(`${this.oldCacheRoot}/${name}`, data); + else fs.writeJsonSync(`${this.newCacheRoot}/${name}`, data); - else // won't be reading it this run - fs.writeJson(`${this.newCacheRoot}/${name}`, data, { encoding: "utf8" }, () => { ; }); } public touch(name: string) { - if (this.checkNewCache) - fs.ensureFileSync(`${this.newCacheRoot}/${name}`); - else // won't be reading it this run - fs.ensureFile(`${this.newCacheRoot}/${name}`, () => { ; }); + if (this.rolled) + return; + fs.ensureFileSync(`${this.newCacheRoot}/${name}`); } /** @@ -84,9 +95,11 @@ export class RollingCache */ public roll() { - fs.remove(this.oldCacheRoot, () => - { - fs.move(this.newCacheRoot, this.oldCacheRoot, () => { ; }); - }); + if (this.rolled) + return; + + this.rolled = true; + fs.removeSync(this.oldCacheRoot); + fs.move(this.newCacheRoot, this.oldCacheRoot, () => { ; }); } } diff --git a/src/cache.ts b/src/tscache.ts similarity index 87% rename from src/cache.ts rename to src/tscache.ts index e850045..67e0b20 100644 --- a/src/cache.ts +++ b/src/tscache.ts @@ -6,6 +6,7 @@ import * as _ from "lodash"; import { RollingCache } from "./rollingcache"; import * as fs from "fs-extra"; import * as colors from "colors/safe"; +import { ICache } from "./icache"; export interface ICode { @@ -51,17 +52,17 @@ export function convertDiagnostic(data: ts.Diagnostic[]): IDiagnostics[] }); } -export class Cache +export class TsCache { - private cacheVersion = "2"; + private cacheVersion = "3"; private dependencyTree: graph.Graph; private ambientTypes: ITypeSnapshot[]; private ambientTypesDirty = false; private cacheDir: string; - private codeCache: RollingCache; - private typesCache: RollingCache; - private semanticDiagnosticsCache: RollingCache; - private syntacticDiagnosticsCache: RollingCache; + private codeCache: ICache; + private typesCache: ICache; + private semanticDiagnosticsCache: ICache; + private syntacticDiagnosticsCache: ICache; constructor(private host: ts.LanguageServiceHost, cache: string, private options: ts.CompilerOptions, rootFilenames: string[], private context: IContext) { @@ -106,6 +107,21 @@ export class Cache this.dependencyTree.setEdge(importer, importee); } + public walkTree(cb: (id: string) => void | false): void + { + const acyclic = graph.alg.isAcyclic(this.dependencyTree); + + if (acyclic) + { + _.each(graph.alg.topsort(this.dependencyTree), (id: string) => cb(id)); + return; + } + + this.context.info(colors.yellow("import tree has cycles")); + + _.each(this.dependencyTree.nodes(), (id: string) => cb(id)); + } + public done() { this.context.info(colors.blue("rolling caches")); @@ -159,7 +175,6 @@ export class Cache this.context.debug(` ${snapshot.id}`); return this.makeName(snapshot.id, snapshot.snapshot!); }); - // types dirty if any d.ts changed, added or removed this.ambientTypesDirty = !this.typesCache.match(typeNames); @@ -169,7 +184,7 @@ export class Cache _.each(typeNames, (name) => this.typesCache.touch(name)); } - private getDiagnostics(cache: RollingCache, id: string, snapshot: ts.IScriptSnapshot, check: () => ts.Diagnostic[]): IDiagnostics[] + private getDiagnostics(cache: ICache, id: string, snapshot: ts.IScriptSnapshot, check: () => ts.Diagnostic[]): IDiagnostics[] { const name = this.makeName(id, snapshot); @@ -195,9 +210,9 @@ export class Cache private init() { this.codeCache = new RollingCache(`${this.cacheDir}/code`, true); - this.typesCache = new RollingCache(`${this.cacheDir}/types`, false); - this.syntacticDiagnosticsCache = new RollingCache(`${this.cacheDir}/syntacticDiagnostics`, false); - this.semanticDiagnosticsCache = new RollingCache(`${this.cacheDir}/semanticDiagnostics`, false); + this.typesCache = new RollingCache(`${this.cacheDir}/types`, true); + this.syntacticDiagnosticsCache = new RollingCache(`${this.cacheDir}/syntacticDiagnostics`, true); + this.semanticDiagnosticsCache = new RollingCache(`${this.cacheDir}/semanticDiagnostics`, true); } private markAsDirty(id: string, _snapshot: ts.IScriptSnapshot): void