diff --git a/.vscode/settings.json b/.vscode/settings.json index f015944..90e1422 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,4 @@ { - "typescript.tsdk": "./node_modules/typescript/lib" + "editor.tabSize": 4, + "editor.useTabStops": true } \ No newline at end of file diff --git a/README.md b/README.md index f6068db..94e607b 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Rollup plugin for typescript with compiler errors. This is a rewrite of original rollup-plugin-typescript, starting and borrowing from [this fork](https://github.com/alexlur/rollup-plugin-typescript). -This version is significantly slower than original, but it will print out typescript errors and warnings. +This version is somewhat slower than original, but it will print out typescript syntactic and semantic diagnostic messages (the main reason for using typescript after all). ## Usage @@ -15,11 +15,11 @@ This version is significantly slower than original, but it will print out typesc import typescript from 'rollup-plugin-typescript'; export default { - entry: './main.ts', + entry: './main.ts', - plugins: [ - typescript() - ] + plugins: [ + typescript() + ] } ``` @@ -32,7 +32,19 @@ Following compiler options are forced though: * `importHelpers`: true * `noResolve`: false -Plugin itself takes standard include/exclude options (each a minimatch pattern, or array of minimatch patterns), which determine which files are transpiled by Typescript (all `.ts` and `.tsx` files by default) +Plugin takes following options: +* `check`: true + - set to false to avoid doing any diagnostic checks on the code +* `verbosity`: 2 + - goes up to 3 +* `clean`: false + - set to true for clean build (wipes out cache) +* `cacheRoot`: ".rts2_cache" + - path to cache +* `include`: `[ "*.ts+(|x)", "**/*.ts+(|x)" ]` + - passes all .ts files through typescript compiler. +* `exclude`: `[ "*.d.ts", "**/*.d.ts" ]` + - but not types ### TypeScript version -This plugin currently requires TypeScript > 2.0. +This plugin currently requires TypeScript 2.0+. diff --git a/dist/rollup-plugin-typescript2.cjs.js b/dist/rollup-plugin-typescript2.cjs.js index 5379e1f..b7e697c 100644 --- a/dist/rollup-plugin-typescript2.cjs.js +++ b/dist/rollup-plugin-typescript2.cjs.js @@ -1,11 +1,14 @@ /* eslint-disable */ 'use strict'; +var fs = require('fs-extra'); var ts = require('typescript'); -var rollupPluginutils = require('rollup-pluginutils'); -var fs = require('fs'); -var path = require('path'); var _ = require('lodash'); +var graph = require('graphlib'); +var hash = require('object-hash'); +var rollupPluginutils = require('rollup-pluginutils'); +var path = require('path'); +var colors = require('colors/safe'); const __assign = Object.assign || function (target) { for (var source, i = 1; i < arguments.length; i++) { @@ -19,6 +22,272 @@ const __assign = Object.assign || function (target) { return target; }; +var VerbosityLevel; +(function (VerbosityLevel) { + VerbosityLevel[VerbosityLevel["Error"] = 0] = "Error"; + VerbosityLevel[VerbosityLevel["Warning"] = 1] = "Warning"; + VerbosityLevel[VerbosityLevel["Info"] = 2] = "Info"; + VerbosityLevel[VerbosityLevel["Debug"] = 3] = "Debug"; +})(VerbosityLevel || (VerbosityLevel = {})); +var ConsoleContext = (function () { + function ConsoleContext(verbosity, prefix) { + if (prefix === void 0) { prefix = ""; } + this.verbosity = verbosity; + this.prefix = prefix; + } + ConsoleContext.prototype.warn = function (message) { + if (this.verbosity < VerbosityLevel.Warning) + return; + console.log("" + this.prefix + message); + }; + ConsoleContext.prototype.error = function (message) { + if (this.verbosity < VerbosityLevel.Error) + return; + console.log("" + this.prefix + message); + }; + ConsoleContext.prototype.info = function (message) { + if (this.verbosity < VerbosityLevel.Info) + return; + console.log("" + this.prefix + message); + }; + ConsoleContext.prototype.debug = function (message) { + if (this.verbosity < VerbosityLevel.Debug) + return; + console.log("" + this.prefix + message); + }; + return ConsoleContext; +}()); + +var LanguageServiceHost = (function () { + function LanguageServiceHost(parsedConfig) { + this.parsedConfig = parsedConfig; + this.cwd = process.cwd(); + this.snapshots = {}; + } + LanguageServiceHost.prototype.setSnapshot = function (fileName, data) { + var snapshot = ts.ScriptSnapshot.fromString(data); + this.snapshots[fileName] = snapshot; + return snapshot; + }; + LanguageServiceHost.prototype.getScriptSnapshot = function (fileName) { + if (_.has(this.snapshots, fileName)) + return this.snapshots[fileName]; + if (fs.existsSync(fileName)) { + this.snapshots[fileName] = ts.ScriptSnapshot.fromString(ts.sys.readFile(fileName)); + return this.snapshots[fileName]; + } + return undefined; + }; + LanguageServiceHost.prototype.getCurrentDirectory = function () { + return this.cwd; + }; + LanguageServiceHost.prototype.getScriptVersion = function (_fileName) { + return "0"; + }; + LanguageServiceHost.prototype.getScriptFileNames = function () { + return this.parsedConfig.fileNames; + }; + LanguageServiceHost.prototype.getCompilationSettings = function () { + return this.parsedConfig.options; + }; + LanguageServiceHost.prototype.getDefaultLibFileName = function (opts) { + return ts.getDefaultLibFilePath(opts); + }; + return LanguageServiceHost; +}()); + +/** + * 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. + */ +var RollingCache = (function () { + /** + * @param cacheRoot: root folder for the cache + * @param checkNewCache: whether to also look in new cache when reading from cache + */ + function RollingCache(cacheRoot, checkNewCache) { + this.cacheRoot = cacheRoot; + this.checkNewCache = checkNewCache; + this.oldCacheRoot = this.cacheRoot + "/cache"; + this.newCacheRoot = this.cacheRoot + "/cache_"; + fs.emptyDirSync(this.newCacheRoot); + } + /** + * @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.checkNewCache && fs.existsSync(this.newCacheRoot + "/" + name)) + return true; + return fs.existsSync(this.oldCacheRoot + "/" + name); + }; + /** + * @returns true if old cache contains all names and nothing more + */ + RollingCache.prototype.match = function (names) { + if (!fs.existsSync(this.oldCacheRoot)) + return names.length === 0; // empty folder matches + return _.isEqual(fs.readdirSync(this.oldCacheRoot).sort(), names.sort()); + }; + /** + * @returns data for name, must exist in old cache (or either old of new cache if checkNewCache is true) + */ + RollingCache.prototype.read = function (name) { + if (this.checkNewCache && fs.existsSync(this.newCacheRoot + "/" + name)) + return fs.readJsonSync(this.newCacheRoot + "/" + name, "utf8"); + return fs.readJsonSync(this.oldCacheRoot + "/" + name, "utf8"); + }; + RollingCache.prototype.write = function (name, data) { + if (data === undefined) + return; + if (this.checkNewCache) + fs.writeJsonSync(this.newCacheRoot + "/" + name, data); + else + fs.writeJson(this.newCacheRoot + "/" + name, data, { encoding: "utf8" }, function () { }); + }; + RollingCache.prototype.touch = function (name) { + if (this.checkNewCache) + fs.ensureFileSync(this.newCacheRoot + "/" + name); + else + fs.ensureFile(this.newCacheRoot + "/" + name, function () { }); + }; + /** + * 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 () { }); + }); + }; + return RollingCache; +}()); + +var Cache = (function () { + function Cache(host, cache, options, rootFilenames, context) { + var _this = this; + this.host = host; + this.options = options; + this.context = context; + this.cacheVersion = "1"; + this.ambientTypesDirty = false; + this.cacheDir = cache + "/" + hash.sha1({ version: this.cacheVersion, rootFilenames: rootFilenames, options: this.options }); + this.dependencyTree = new graph.Graph({ directed: true }); + this.dependencyTree.setDefaultNodeLabel(function (_node) { return { dirty: false }; }); + this.ambientTypes = _.filter(rootFilenames, function (file) { return _.endsWith(file, ".d.ts"); }) + .map(function (id) { return { id: id, snapshot: _this.host.getScriptSnapshot(id) }; }); + this.init(); + } + Cache.prototype.clean = function () { + this.context.info("cleaning cache: " + this.cacheDir); + fs.emptyDirSync(this.cacheDir); + this.init(); + }; + Cache.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("import tree has cycles"); + _.each(this.dependencyTree.nodes(), function (id) { return cb(id); }); + }; + Cache.prototype.setDependency = function (importee, importer) { + // importee -> importer + this.context.debug(importee + " -> " + importer); + this.dependencyTree.setEdge(importer, importee); + }; + Cache.prototype.compileDone = function () { + var _this = this; + var typeNames = _.filter(this.ambientTypes, function (snaphot) { return snaphot.snapshot !== undefined; }) + .map(function (snaphot) { return _this.makeName(snaphot.id, snaphot.snapshot); }); + // types dirty if any d.ts changed, added or removed + this.ambientTypesDirty = !this.typesCache.match(typeNames); + if (this.ambientTypesDirty) + this.context.info("ambient types changed, redoing all diagnostics"); + _.each(typeNames, function (name) { return _this.typesCache.touch(name); }); + }; + Cache.prototype.diagnosticsDone = function () { + this.codeCache.roll(); + this.diagnosticsCache.roll(); + this.typesCache.roll(); + }; + Cache.prototype.getCompiled = function (id, snapshot, transform) { + var name = this.makeName(id, snapshot); + if (!this.codeCache.exists(name) || this.isDirty(id, snapshot, false)) { + this.context.debug("fresh transpile for: " + id); + var data_1 = transform(); + this.codeCache.write(name, data_1); + this.markAsDirty(id, snapshot); + return data_1; + } + this.context.debug("old transpile for: " + id); + var data = this.codeCache.read(name); + this.codeCache.write(name, data); + return data; + }; + Cache.prototype.getDiagnostics = function (id, snapshot, check) { + var name = this.makeName(id, snapshot); + if (!this.diagnosticsCache.exists(name) || this.isDirty(id, snapshot, true)) { + this.context.debug("fresh diagnostics for: " + id); + var data_2 = this.convert(check()); + this.diagnosticsCache.write(name, data_2); + this.markAsDirty(id, snapshot); + return data_2; + } + this.context.debug("old diagnostics for: " + id); + var data = this.diagnosticsCache.read(name); + this.diagnosticsCache.write(name, data); + return data; + }; + Cache.prototype.init = function () { + this.codeCache = new RollingCache(this.cacheDir + "/code", true); + this.typesCache = new RollingCache(this.cacheDir + "/types", false); + this.diagnosticsCache = new RollingCache(this.cacheDir + "/diagnostics", false); + }; + Cache.prototype.markAsDirty = function (id, _snapshot) { + this.context.debug("changed: " + id); + 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) { + var _this = this; + var label = this.dependencyTree.node(id); + if (!label) + return false; + if (!checkImports || label.dirty) + return label.dirty; + if (this.ambientTypesDirty) + return true; + var dependencies = graph.alg.dijkstra(this.dependencyTree, id); + return _.some(dependencies, function (dependency, node) { + if (!node || dependency.distance === Infinity) + return false; + var l = _this.dependencyTree.node(node); + var dirty = l === undefined ? true : l.dirty; + if (dirty) + _this.context.debug("import changed: " + id + " -> " + node); + return dirty; + }); + }; + Cache.prototype.makeName = function (id, snapshot) { + var data = snapshot.getText(0, snapshot.getLength()); + return hash.sha1({ data: data, id: id }); + }; + Cache.prototype.convert = function (data) { + return _.map(data, function (diagnostic) { + var entry = { + flatMessage: ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"), + }; + if (diagnostic.file) { + var _a = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start), line = _a.line, character = _a.character; + entry.fileLine = diagnostic.file.fileName + " (" + (line + 1) + "," + (character + 1) + ")"; + } + return entry; + }); + }; + return Cache; +}()); + function getOptionsOverrides() { return { module: ts.ModuleKind.ES2015, @@ -33,17 +302,15 @@ function getOptionsOverrides() { // MIT Licenced function findFile(cwd, filename) { var fp = cwd ? (cwd + "/" + filename) : filename; - if (fs.existsSync(fp)) { + if (fs.existsSync(fp)) return fp; - } var segs = cwd.split(path.sep); var len = segs.length; while (len--) { cwd = segs.slice(0, len).join("/"); fp = cwd + "/" + filename; - if (fs.existsSync(fp)) { + if (fs.existsSync(fp)) return fp; - } } return null; } @@ -61,42 +328,40 @@ catch (e) { } function parseTsConfig() { var fileName = findFile(process.cwd(), "tsconfig.json"); + if (!fileName) + throw new Error("couldn't find 'tsconfig.json' in " + process.cwd()); var text = ts.sys.readFile(fileName); var result = ts.parseConfigFileTextToJson(fileName, text); var configParseResult = ts.parseJsonConfigFileContent(result.config, ts.sys, path.dirname(fileName), getOptionsOverrides(), fileName); return configParseResult; } function printDiagnostics(context, diagnostics) { - diagnostics.forEach(function (diagnostic) { - var message = ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"); - if (diagnostic.file) { - var _a = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start), line = _a.line, character = _a.character; - context.warn({ message: diagnostic.file.fileName + " (" + (line + 1) + "," + (character + 1) + "): " + message }); - } + _.each(diagnostics, function (diagnostic) { + if (diagnostic.fileLine) + context.warn(diagnostic.fileLine + ": " + colors.yellow(diagnostic.flatMessage)); else - context.warn({ message: message }); + context.warn(colors.yellow(diagnostic.flatMessage)); }); } function typescript(options) { options = __assign({}, options); - var filter = rollupPluginutils.createFilter(options.include || ["*.ts+(|x)", "**/*.ts+(|x)"], options.exclude || ["*.d.ts", "**/*.d.ts"]); - delete options.include; - delete options.exclude; + _.defaults(options, { + check: true, + verbosity: VerbosityLevel.Info, + clean: false, + cacheRoot: process.cwd() + "/.rts2_cache", + include: ["*.ts+(|x)", "**/*.ts+(|x)"], + exclude: ["*.d.ts", "**/*.d.ts"], + }); + var filter$$1 = rollupPluginutils.createFilter(options.include, options.exclude); var parsedConfig = parseTsConfig(); - var servicesHost = { - getScriptFileNames: function () { return parsedConfig.fileNames; }, - getScriptVersion: function (_fileName) { return "0"; }, - getScriptSnapshot: function (fileName) { - if (!fs.existsSync(fileName)) - return undefined; - return ts.ScriptSnapshot.fromString(ts.sys.readFile(fileName)); - }, - getCurrentDirectory: function () { return process.cwd(); }, - getCompilationSettings: function () { return parsedConfig.options; }, - getDefaultLibFileName: function (opts) { return ts.getDefaultLibFilePath(opts); }, - }; + var servicesHost = new LanguageServiceHost(parsedConfig); var services = ts.createLanguageService(servicesHost, ts.createDocumentRegistry()); + var context = new ConsoleContext(options.verbosity, "rollup-plugin-typescript2: "); + var cache = new Cache(servicesHost, options.cacheRoot, parsedConfig.options, parsedConfig.fileNames, context); + if (options.clean) + cache.clean(); return { resolveId: function (importee, importer) { if (importee === TSLIB) @@ -106,6 +371,8 @@ function typescript(options) { importer = importer.split("\\").join("/"); var result = ts.nodeModuleNameResolver(importee, importer, parsedConfig.options, ts.sys); if (result.resolvedModule && result.resolvedModule.resolvedFileName) { + if (filter$$1(result.resolvedModule.resolvedFileName)) + cache.setDependency(result.resolvedModule.resolvedFileName, importer); if (_.endsWith(result.resolvedModule.resolvedFileName, ".d.ts")) return null; return result.resolvedModule.resolvedFileName; @@ -115,24 +382,45 @@ function typescript(options) { load: function (id) { if (id === "\0" + TSLIB) return tslibSource; + return undefined; }, - transform: function (_code, id) { - if (!filter(id)) - return null; - var output = services.getEmitOutput(id); - var allDiagnostics = services - .getCompilerOptionsDiagnostics() - .concat(services.getSyntacticDiagnostics(id)) - .concat(services.getSemanticDiagnostics(id)); - printDiagnostics(this, allDiagnostics); - if (output.emitSkipped) - this.error({ message: "failed to transpile " + id }); - var code = _.find(output.outputFiles, function (entry) { return _.endsWith(entry.name, ".js"); }); - var map = _.find(output.outputFiles, function (entry) { return _.endsWith(entry.name, ".map"); }); - return { - code: code ? code.text : undefined, - map: map ? JSON.parse(map.text) : { mappings: "" }, - }; + transform: function (code, id) { + var _this = this; + if (!filter$$1(id)) + return undefined; + var snapshot = servicesHost.setSnapshot(id, code); + var result = cache.getCompiled(id, snapshot, function () { + var output = services.getEmitOutput(id); + if (output.emitSkipped) + _this.error({ message: colors.red("failed to transpile " + id) }); + var transpiled = _.find(output.outputFiles, function (entry) { return _.endsWith(entry.name, ".js"); }); + var map$$1 = _.find(output.outputFiles, function (entry) { return _.endsWith(entry.name, ".map"); }); + return { + code: transpiled ? transpiled.text : undefined, + map: map$$1 ? JSON.parse(map$$1.text) : { mappings: "" }, + }; + }); + return result; + }, + outro: function () { + cache.compileDone(); + if (options.check) { + cache.walkTree(function (id) { + var snapshot = servicesHost.getScriptSnapshot(id); + if (!snapshot) { + context.error(colors.red("failed lo load snapshot for " + id)); + return; + } + var diagnostics = cache.getDiagnostics(id, snapshot, function () { + return services + .getCompilerOptionsDiagnostics() + .concat(services.getSyntacticDiagnostics(id)) + .concat(services.getSemanticDiagnostics(id)); + }); + printDiagnostics(context, diagnostics); + }); + } + cache.diagnosticsDone(); }, }; } diff --git a/dist/rollup-plugin-typescript2.es.js b/dist/rollup-plugin-typescript2.es.js index fb83cb8..9d73f06 100644 --- a/dist/rollup-plugin-typescript2.es.js +++ b/dist/rollup-plugin-typescript2.es.js @@ -1,13 +1,19 @@ /* eslint-disable */ +import { emptyDirSync, ensureFile, ensureFileSync, existsSync, move, readFileSync, readJsonSync, readdirSync, remove, writeJson, writeJsonSync } from 'fs-extra'; +import * as fs from 'fs-extra'; import { ModuleKind, ScriptSnapshot, createDocumentRegistry, createLanguageService, flattenDiagnosticMessageText, getDefaultLibFilePath, nodeModuleNameResolver, parseConfigFileTextToJson, parseJsonConfigFileContent, sys } from 'typescript'; import * as ts from 'typescript'; +import { defaults, each, endsWith, filter, find, has, isEqual, map, some } from 'lodash'; +import * as _ from 'lodash'; +import { Graph, alg } from 'graphlib'; +import * as graph from 'graphlib'; +import { sha1 } from 'object-hash'; +import * as hash from 'object-hash'; import { createFilter } from 'rollup-pluginutils'; -import { existsSync, readFileSync } from 'fs'; -import * as fs from 'fs'; import { dirname, sep } from 'path'; import * as path from 'path'; -import { endsWith, find } from 'lodash'; -import * as _ from 'lodash'; +import { red, yellow } from 'colors/safe'; +import * as colors from 'colors/safe'; const __assign = Object.assign || function (target) { for (var source, i = 1; i < arguments.length; i++) { @@ -21,6 +27,272 @@ const __assign = Object.assign || function (target) { return target; }; +var VerbosityLevel; +(function (VerbosityLevel) { + VerbosityLevel[VerbosityLevel["Error"] = 0] = "Error"; + VerbosityLevel[VerbosityLevel["Warning"] = 1] = "Warning"; + VerbosityLevel[VerbosityLevel["Info"] = 2] = "Info"; + VerbosityLevel[VerbosityLevel["Debug"] = 3] = "Debug"; +})(VerbosityLevel || (VerbosityLevel = {})); +var ConsoleContext = (function () { + function ConsoleContext(verbosity, prefix) { + if (prefix === void 0) { prefix = ""; } + this.verbosity = verbosity; + this.prefix = prefix; + } + ConsoleContext.prototype.warn = function (message) { + if (this.verbosity < VerbosityLevel.Warning) + return; + console.log("" + this.prefix + message); + }; + ConsoleContext.prototype.error = function (message) { + if (this.verbosity < VerbosityLevel.Error) + return; + console.log("" + this.prefix + message); + }; + ConsoleContext.prototype.info = function (message) { + if (this.verbosity < VerbosityLevel.Info) + return; + console.log("" + this.prefix + message); + }; + ConsoleContext.prototype.debug = function (message) { + if (this.verbosity < VerbosityLevel.Debug) + return; + console.log("" + this.prefix + message); + }; + return ConsoleContext; +}()); + +var LanguageServiceHost = (function () { + function LanguageServiceHost(parsedConfig) { + this.parsedConfig = parsedConfig; + this.cwd = process.cwd(); + this.snapshots = {}; + } + LanguageServiceHost.prototype.setSnapshot = function (fileName, data) { + var snapshot = ScriptSnapshot.fromString(data); + this.snapshots[fileName] = snapshot; + return snapshot; + }; + LanguageServiceHost.prototype.getScriptSnapshot = function (fileName) { + if (has(this.snapshots, fileName)) + return this.snapshots[fileName]; + if (existsSync(fileName)) { + this.snapshots[fileName] = ScriptSnapshot.fromString(sys.readFile(fileName)); + return this.snapshots[fileName]; + } + return undefined; + }; + LanguageServiceHost.prototype.getCurrentDirectory = function () { + return this.cwd; + }; + LanguageServiceHost.prototype.getScriptVersion = function (_fileName) { + return "0"; + }; + LanguageServiceHost.prototype.getScriptFileNames = function () { + return this.parsedConfig.fileNames; + }; + LanguageServiceHost.prototype.getCompilationSettings = function () { + return this.parsedConfig.options; + }; + LanguageServiceHost.prototype.getDefaultLibFileName = function (opts) { + return getDefaultLibFilePath(opts); + }; + return LanguageServiceHost; +}()); + +/** + * 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. + */ +var RollingCache = (function () { + /** + * @param cacheRoot: root folder for the cache + * @param checkNewCache: whether to also look in new cache when reading from cache + */ + function RollingCache(cacheRoot, checkNewCache) { + this.cacheRoot = cacheRoot; + this.checkNewCache = checkNewCache; + this.oldCacheRoot = this.cacheRoot + "/cache"; + this.newCacheRoot = this.cacheRoot + "/cache_"; + emptyDirSync(this.newCacheRoot); + } + /** + * @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.checkNewCache && existsSync(this.newCacheRoot + "/" + name)) + return true; + return existsSync(this.oldCacheRoot + "/" + name); + }; + /** + * @returns true if old cache contains all names and nothing more + */ + RollingCache.prototype.match = function (names) { + if (!existsSync(this.oldCacheRoot)) + return names.length === 0; // empty folder matches + return isEqual(readdirSync(this.oldCacheRoot).sort(), names.sort()); + }; + /** + * @returns data for name, must exist in old cache (or either old of new cache if checkNewCache is true) + */ + RollingCache.prototype.read = function (name) { + if (this.checkNewCache && existsSync(this.newCacheRoot + "/" + name)) + return readJsonSync(this.newCacheRoot + "/" + name, "utf8"); + return readJsonSync(this.oldCacheRoot + "/" + name, "utf8"); + }; + RollingCache.prototype.write = function (name, data) { + if (data === undefined) + return; + if (this.checkNewCache) + writeJsonSync(this.newCacheRoot + "/" + name, data); + else + writeJson(this.newCacheRoot + "/" + name, data, { encoding: "utf8" }, function () { }); + }; + RollingCache.prototype.touch = function (name) { + if (this.checkNewCache) + ensureFileSync(this.newCacheRoot + "/" + name); + else + ensureFile(this.newCacheRoot + "/" + name, function () { }); + }; + /** + * 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 () { }); + }); + }; + return RollingCache; +}()); + +var Cache = (function () { + function Cache(host, cache, options, rootFilenames, context) { + var _this = this; + this.host = host; + this.options = options; + this.context = context; + this.cacheVersion = "1"; + this.ambientTypesDirty = false; + this.cacheDir = cache + "/" + sha1({ version: this.cacheVersion, rootFilenames: rootFilenames, options: this.options }); + this.dependencyTree = new Graph({ directed: true }); + this.dependencyTree.setDefaultNodeLabel(function (_node) { return { dirty: false }; }); + this.ambientTypes = filter(rootFilenames, function (file) { return endsWith(file, ".d.ts"); }) + .map(function (id) { return { id: id, snapshot: _this.host.getScriptSnapshot(id) }; }); + this.init(); + } + Cache.prototype.clean = function () { + this.context.info("cleaning cache: " + this.cacheDir); + emptyDirSync(this.cacheDir); + this.init(); + }; + Cache.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("import tree has cycles"); + each(this.dependencyTree.nodes(), function (id) { return cb(id); }); + }; + Cache.prototype.setDependency = function (importee, importer) { + // importee -> importer + this.context.debug(importee + " -> " + importer); + this.dependencyTree.setEdge(importer, importee); + }; + Cache.prototype.compileDone = function () { + var _this = this; + var typeNames = filter(this.ambientTypes, function (snaphot) { return snaphot.snapshot !== undefined; }) + .map(function (snaphot) { return _this.makeName(snaphot.id, snaphot.snapshot); }); + // types dirty if any d.ts changed, added or removed + this.ambientTypesDirty = !this.typesCache.match(typeNames); + if (this.ambientTypesDirty) + this.context.info("ambient types changed, redoing all diagnostics"); + each(typeNames, function (name) { return _this.typesCache.touch(name); }); + }; + Cache.prototype.diagnosticsDone = function () { + this.codeCache.roll(); + this.diagnosticsCache.roll(); + this.typesCache.roll(); + }; + Cache.prototype.getCompiled = function (id, snapshot, transform) { + var name = this.makeName(id, snapshot); + if (!this.codeCache.exists(name) || this.isDirty(id, snapshot, false)) { + this.context.debug("fresh transpile for: " + id); + var data_1 = transform(); + this.codeCache.write(name, data_1); + this.markAsDirty(id, snapshot); + return data_1; + } + this.context.debug("old transpile for: " + id); + var data = this.codeCache.read(name); + this.codeCache.write(name, data); + return data; + }; + Cache.prototype.getDiagnostics = function (id, snapshot, check) { + var name = this.makeName(id, snapshot); + if (!this.diagnosticsCache.exists(name) || this.isDirty(id, snapshot, true)) { + this.context.debug("fresh diagnostics for: " + id); + var data_2 = this.convert(check()); + this.diagnosticsCache.write(name, data_2); + this.markAsDirty(id, snapshot); + return data_2; + } + this.context.debug("old diagnostics for: " + id); + var data = this.diagnosticsCache.read(name); + this.diagnosticsCache.write(name, data); + return data; + }; + Cache.prototype.init = function () { + this.codeCache = new RollingCache(this.cacheDir + "/code", true); + this.typesCache = new RollingCache(this.cacheDir + "/types", false); + this.diagnosticsCache = new RollingCache(this.cacheDir + "/diagnostics", false); + }; + Cache.prototype.markAsDirty = function (id, _snapshot) { + this.context.debug("changed: " + id); + 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) { + var _this = this; + var label = this.dependencyTree.node(id); + if (!label) + return false; + if (!checkImports || label.dirty) + return label.dirty; + if (this.ambientTypesDirty) + return true; + var dependencies = alg.dijkstra(this.dependencyTree, id); + return some(dependencies, function (dependency, node) { + if (!node || dependency.distance === Infinity) + return false; + var l = _this.dependencyTree.node(node); + var dirty = l === undefined ? true : l.dirty; + if (dirty) + _this.context.debug("import changed: " + id + " -> " + node); + return dirty; + }); + }; + Cache.prototype.makeName = function (id, snapshot) { + var data = snapshot.getText(0, snapshot.getLength()); + return sha1({ data: data, id: id }); + }; + Cache.prototype.convert = function (data) { + return map(data, function (diagnostic) { + var entry = { + flatMessage: flattenDiagnosticMessageText(diagnostic.messageText, "\n"), + }; + if (diagnostic.file) { + var _a = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start), line = _a.line, character = _a.character; + entry.fileLine = diagnostic.file.fileName + " (" + (line + 1) + "," + (character + 1) + ")"; + } + return entry; + }); + }; + return Cache; +}()); + function getOptionsOverrides() { return { module: ModuleKind.ES2015, @@ -35,17 +307,15 @@ function getOptionsOverrides() { // MIT Licenced function findFile(cwd, filename) { var fp = cwd ? (cwd + "/" + filename) : filename; - if (existsSync(fp)) { + if (existsSync(fp)) return fp; - } var segs = cwd.split(sep); var len = segs.length; while (len--) { cwd = segs.slice(0, len).join("/"); fp = cwd + "/" + filename; - if (existsSync(fp)) { + if (existsSync(fp)) return fp; - } } return null; } @@ -63,42 +333,40 @@ catch (e) { } function parseTsConfig() { var fileName = findFile(process.cwd(), "tsconfig.json"); + if (!fileName) + throw new Error("couldn't find 'tsconfig.json' in " + process.cwd()); var text = sys.readFile(fileName); var result = parseConfigFileTextToJson(fileName, text); var configParseResult = parseJsonConfigFileContent(result.config, sys, dirname(fileName), getOptionsOverrides(), fileName); return configParseResult; } function printDiagnostics(context, diagnostics) { - diagnostics.forEach(function (diagnostic) { - var message = flattenDiagnosticMessageText(diagnostic.messageText, "\n"); - if (diagnostic.file) { - var _a = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start), line = _a.line, character = _a.character; - context.warn({ message: diagnostic.file.fileName + " (" + (line + 1) + "," + (character + 1) + "): " + message }); - } + each(diagnostics, function (diagnostic) { + if (diagnostic.fileLine) + context.warn(diagnostic.fileLine + ": " + yellow(diagnostic.flatMessage)); else - context.warn({ message: message }); + context.warn(yellow(diagnostic.flatMessage)); }); } function typescript(options) { options = __assign({}, options); - var filter = createFilter(options.include || ["*.ts+(|x)", "**/*.ts+(|x)"], options.exclude || ["*.d.ts", "**/*.d.ts"]); - delete options.include; - delete options.exclude; + defaults(options, { + check: true, + verbosity: VerbosityLevel.Info, + clean: false, + cacheRoot: process.cwd() + "/.rts2_cache", + include: ["*.ts+(|x)", "**/*.ts+(|x)"], + exclude: ["*.d.ts", "**/*.d.ts"], + }); + var filter$$1 = createFilter(options.include, options.exclude); var parsedConfig = parseTsConfig(); - var servicesHost = { - getScriptFileNames: function () { return parsedConfig.fileNames; }, - getScriptVersion: function (_fileName) { return "0"; }, - getScriptSnapshot: function (fileName) { - if (!existsSync(fileName)) - return undefined; - return ScriptSnapshot.fromString(sys.readFile(fileName)); - }, - getCurrentDirectory: function () { return process.cwd(); }, - getCompilationSettings: function () { return parsedConfig.options; }, - getDefaultLibFileName: function (opts) { return getDefaultLibFilePath(opts); }, - }; + var servicesHost = new LanguageServiceHost(parsedConfig); var services = createLanguageService(servicesHost, createDocumentRegistry()); + var context = new ConsoleContext(options.verbosity, "rollup-plugin-typescript2: "); + var cache = new Cache(servicesHost, options.cacheRoot, parsedConfig.options, parsedConfig.fileNames, context); + if (options.clean) + cache.clean(); return { resolveId: function (importee, importer) { if (importee === TSLIB) @@ -108,6 +376,8 @@ function typescript(options) { importer = importer.split("\\").join("/"); var result = nodeModuleNameResolver(importee, importer, parsedConfig.options, sys); if (result.resolvedModule && result.resolvedModule.resolvedFileName) { + if (filter$$1(result.resolvedModule.resolvedFileName)) + cache.setDependency(result.resolvedModule.resolvedFileName, importer); if (endsWith(result.resolvedModule.resolvedFileName, ".d.ts")) return null; return result.resolvedModule.resolvedFileName; @@ -117,24 +387,45 @@ function typescript(options) { load: function (id) { if (id === "\0" + TSLIB) return tslibSource; + return undefined; }, - transform: function (_code, id) { - if (!filter(id)) - return null; - var output = services.getEmitOutput(id); - var allDiagnostics = services - .getCompilerOptionsDiagnostics() - .concat(services.getSyntacticDiagnostics(id)) - .concat(services.getSemanticDiagnostics(id)); - printDiagnostics(this, allDiagnostics); - if (output.emitSkipped) - this.error({ message: "failed to transpile " + id }); - var code = find(output.outputFiles, function (entry) { return endsWith(entry.name, ".js"); }); - var map = find(output.outputFiles, function (entry) { return endsWith(entry.name, ".map"); }); - return { - code: code ? code.text : undefined, - map: map ? JSON.parse(map.text) : { mappings: "" }, - }; + transform: function (code, id) { + var _this = this; + if (!filter$$1(id)) + return undefined; + var snapshot = servicesHost.setSnapshot(id, code); + var result = cache.getCompiled(id, snapshot, function () { + var output = services.getEmitOutput(id); + if (output.emitSkipped) + _this.error({ message: red("failed to transpile " + id) }); + var transpiled = find(output.outputFiles, function (entry) { return endsWith(entry.name, ".js"); }); + var map$$1 = find(output.outputFiles, function (entry) { return endsWith(entry.name, ".map"); }); + return { + code: transpiled ? transpiled.text : undefined, + map: map$$1 ? JSON.parse(map$$1.text) : { mappings: "" }, + }; + }); + return result; + }, + outro: function () { + cache.compileDone(); + if (options.check) { + cache.walkTree(function (id) { + var snapshot = servicesHost.getScriptSnapshot(id); + if (!snapshot) { + context.error(red("failed lo load snapshot for " + id)); + return; + } + var diagnostics = cache.getDiagnostics(id, snapshot, function () { + return services + .getCompilerOptionsDiagnostics() + .concat(services.getSyntacticDiagnostics(id)) + .concat(services.getSemanticDiagnostics(id)); + }); + printDiagnostics(context, diagnostics); + }); + } + cache.diagnosticsDone(); }, }; } diff --git a/package.json b/package.json index b22bee2..9c87003 100644 --- a/package.json +++ b/package.json @@ -20,21 +20,29 @@ "scripts": { "prebuild": "rimraf dist/*", "build": "rollup -c", - "lint": "tslint -c ./tslint.json src/*.ts", - "postinstall": "typings install" + "lint": "tslint -c ./tslint.json src/*.ts" }, "dependencies": { + "colors": "^1.1.2", + "fs-extra": "^2.0.0", + "graphlib": "^2.1.1", "lodash": "^4.17.4", + "object-hash": "^1.1.5", "rollup-pluginutils": "^2.0.1", "tslib": "^1.5.0" }, "peerDependencies": { - "typescript": "^2.0" + "typescript": "^2.0", + "tslib": "^1.5.0" }, "devDependencies": { "@alexlur/rollup-plugin-typescript": "^0.8.1", + "@types/colors": "^1.1.1", + "@types/fs-extra": "0.0.37", + "@types/graphlib": "^2.1.3", "@types/lodash": "^4.14.52", "@types/node": "^6.0.53", + "@types/object-hash": "^0.5.28", "rimraf": "^2.5.4", "rollup": "^0.41.4", "tslint": "^4.4.2", diff --git a/rollup.config.js b/rollup.config.js index 38d926a..84f1099 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -7,11 +7,14 @@ export default { external: [ 'path', - 'fs', + 'fs-extra', 'object-assign', 'rollup-pluginutils', 'typescript', - 'lodash' + 'lodash', + 'graphlib', + 'object-hash', + 'colors/safe' ], plugins: [ diff --git a/src/cache.ts b/src/cache.ts new file mode 100644 index 0000000..5e00ba2 --- /dev/null +++ b/src/cache.ts @@ -0,0 +1,219 @@ +import { IContext } from "./context"; +import * as ts from "typescript"; +import * as graph from "graphlib"; +import * as hash from "object-hash"; +import * as _ from "lodash"; +import { RollingCache } from "./rollingcache"; +import * as fs from "fs-extra"; + +export interface ICode +{ + code: string | undefined; + map: string | undefined; +} + +interface INodeLabel +{ + dirty: boolean; +} + +export interface IDiagnostics +{ + flatMessage: string; + fileLine?: string; +} + +interface ITypeSnapshot +{ + id: string; + snapshot: ts.IScriptSnapshot | undefined; +} + +export class Cache +{ + private cacheVersion = "1"; + private dependencyTree: graph.Graph; + private ambientTypes: ITypeSnapshot[]; + private ambientTypesDirty = false; + private cacheDir: string; + private codeCache: RollingCache; + private typesCache: RollingCache; + private diagnosticsCache: RollingCache; + + constructor(private host: ts.LanguageServiceHost, cache: string, private options: ts.CompilerOptions, rootFilenames: string[], private context: IContext) + { + this.cacheDir = `${cache}/${hash.sha1({ version: this.cacheVersion, rootFilenames, options: this.options })}`; + + this.dependencyTree = new graph.Graph({ directed: true }); + this.dependencyTree.setDefaultNodeLabel((_node: string) => { return { dirty: false }; }); + + this.ambientTypes = _ + .filter(rootFilenames, (file) => _.endsWith(file, ".d.ts")) + .map((id) => { return { id, snapshot: this.host.getScriptSnapshot(id) }; }); + + this.init(); + } + + public clean() + { + this.context.info(`cleaning cache: ${this.cacheDir}`); + fs.emptyDirSync(this.cacheDir); + + this.init(); + } + + 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("import tree has cycles"); + + _.each(this.dependencyTree.nodes(), (id: string) => cb(id)); + } + + public setDependency(importee: string, importer: string): void + { + // importee -> importer + this.context.debug(`${importee} -> ${importer}`); + this.dependencyTree.setEdge(importer, importee); + } + + public compileDone(): void + { + let typeNames = _ + .filter(this.ambientTypes, (snaphot) => snaphot.snapshot !== undefined) + .map((snaphot) => this.makeName(snaphot.id, snaphot.snapshot!)); + + // types dirty if any d.ts changed, added or removed + this.ambientTypesDirty = !this.typesCache.match(typeNames); + + if (this.ambientTypesDirty) + this.context.info("ambient types changed, redoing all diagnostics"); + + _.each(typeNames, (name) => this.typesCache.touch(name)); + } + + public diagnosticsDone() + { + this.codeCache.roll(); + this.diagnosticsCache.roll(); + this.typesCache.roll(); + } + + public getCompiled(id: string, snapshot: ts.IScriptSnapshot, transform: () => ICode | undefined): ICode | undefined + { + let name = this.makeName(id, snapshot); + + if (!this.codeCache.exists(name) || this.isDirty(id, snapshot, false)) + { + this.context.debug(`fresh transpile for: ${id}`); + + let data = transform(); + this.codeCache.write(name, data); + this.markAsDirty(id, snapshot); + return data; + } + + this.context.debug(`old transpile for: ${id}`); + + let data = this.codeCache.read(name); + this.codeCache.write(name, data); + return data; + } + + public getDiagnostics(id: string, snapshot: ts.IScriptSnapshot, check: () => ts.Diagnostic[]): IDiagnostics[] + { + let name = this.makeName(id, snapshot); + + if (!this.diagnosticsCache.exists(name) || this.isDirty(id, snapshot, true)) + { + this.context.debug(`fresh diagnostics for: ${id}`); + + let data = this.convert(check()); + this.diagnosticsCache.write(name, data); + this.markAsDirty(id, snapshot); + return data; + } + + this.context.debug(`old diagnostics for: ${id}`); + + let data = this.diagnosticsCache.read(name); + this.diagnosticsCache.write(name, data); + return data; + } + + private init() + { + this.codeCache = new RollingCache(`${this.cacheDir}/code`, true); + this.typesCache = new RollingCache(`${this.cacheDir}/types`, false); + this.diagnosticsCache = new RollingCache(`${this.cacheDir}/diagnostics`, false); + } + + private markAsDirty(id: string, _snapshot: ts.IScriptSnapshot): void + { + this.context.debug(`changed: ${id}`); + this.dependencyTree.setNode(id, { dirty: true }); + } + + // returns true if node or any of its imports or any of global types changed + private isDirty(id: string, _snapshot: ts.IScriptSnapshot, checkImports: boolean): boolean + { + let label = this.dependencyTree.node(id) as INodeLabel; + + if (!label) + return false; + + if (!checkImports || label.dirty) + return label.dirty; + + if (this.ambientTypesDirty) + return true; + + let dependencies = graph.alg.dijkstra(this.dependencyTree, id); + + return _.some(dependencies, (dependency, node) => + { + if (!node || dependency.distance === Infinity) + return false; + + let l = this.dependencyTree.node(node) as INodeLabel | undefined; + let dirty = l === undefined ? true : l.dirty; + + if (dirty) + this.context.debug(`import changed: ${id} -> ${node}`); + + return dirty; + }); + } + + private makeName(id: string, snapshot: ts.IScriptSnapshot) + { + let data = snapshot.getText(0, snapshot.getLength()); + return hash.sha1({ data, id }); + } + + private convert(data: ts.Diagnostic[]): IDiagnostics[] + { + return _.map(data, (diagnostic) => + { + let entry: IDiagnostics = + { + flatMessage: ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"), + }; + + if (diagnostic.file) + { + let { line, character } = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start); + entry.fileLine = `${diagnostic.file.fileName} (${line + 1},${character + 1})`; + } + + return entry; + }); + } +} diff --git a/src/context.ts b/src/context.ts new file mode 100644 index 0000000..3ca05be --- /dev/null +++ b/src/context.ts @@ -0,0 +1,61 @@ +interface Message +{ + message: string; +} + +export interface IRollupContext +{ + warn(message: Message | string): void; + error(message: Message | string): void; +} + +export interface IContext +{ + warn(message: string): void; + error(message: string): void; + info(message: string): void; + debug(message: string): void; +} + +export enum VerbosityLevel +{ + Error = 0, + Warning, + Info, + Debug, +} + +export class ConsoleContext implements IContext +{ + constructor(private verbosity: VerbosityLevel, private prefix: string = "") + { + } + + public warn(message: string): void + { + if (this.verbosity < VerbosityLevel.Warning) + return; + console.log(`${this.prefix}${message}`); + } + + public error(message: string): void + { + if (this.verbosity < VerbosityLevel.Error) + return; + console.log(`${this.prefix}${message}`); + } + + public info(message: string): void + { + if (this.verbosity < VerbosityLevel.Info) + return; + console.log(`${this.prefix}${message}`); + } + + public debug(message: string): void + { + if (this.verbosity < VerbosityLevel.Debug) + return; + console.log(`${this.prefix}${message}`); + } +} diff --git a/src/host.ts b/src/host.ts new file mode 100644 index 0000000..492419c --- /dev/null +++ b/src/host.ts @@ -0,0 +1,59 @@ +import * as fs from "fs-extra"; +import * as ts from "typescript"; +import * as _ from "lodash"; + +export class LanguageServiceHost implements ts.LanguageServiceHost +{ + private cwd = process.cwd(); + private snapshots: { [fileName: string]: ts.IScriptSnapshot } = {}; + + constructor(private parsedConfig: ts.ParsedCommandLine) + { + } + + public setSnapshot(fileName: string, data: string): ts.IScriptSnapshot + { + let snapshot = ts.ScriptSnapshot.fromString(data); + this.snapshots[fileName] = snapshot; + return snapshot; + } + + public getScriptSnapshot(fileName: string): ts.IScriptSnapshot | undefined + { + if (_.has(this.snapshots, fileName)) + return this.snapshots[fileName]; + + if (fs.existsSync(fileName)) + { + this.snapshots[fileName] = ts.ScriptSnapshot.fromString(ts.sys.readFile(fileName)); + return this.snapshots[fileName]; + } + + return undefined; + } + + public getCurrentDirectory() + { + return this.cwd; + } + + public getScriptVersion(_fileName: string) + { + return "0"; + } + + public getScriptFileNames() + { + return this.parsedConfig.fileNames; + } + + public getCompilationSettings(): ts.CompilerOptions + { + return this.parsedConfig.options; + } + + public getDefaultLibFileName(opts: ts.CompilerOptions) + { + return ts.getDefaultLibFilePath(opts); + } +} diff --git a/src/index.ts b/src/index.ts index 253a2cd..e9bfa9c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,12 @@ +import { IContext, ConsoleContext, IRollupContext, VerbosityLevel } from "./context"; +import { LanguageServiceHost } from "./host"; +import { Cache, ICode, IDiagnostics } from "./cache"; import * as ts from "typescript"; import { createFilter } from "rollup-pluginutils"; -import * as fs from "fs"; +import * as fs from "fs-extra"; import * as path from "path"; -import { existsSync } from "fs"; import * as _ from "lodash"; +import * as colors from "colors/safe"; function getOptionsOverrides(): ts.CompilerOptions { @@ -23,10 +26,8 @@ function findFile(cwd: string, filename: string) { let fp = cwd ? (cwd + "/" + filename) : filename; - if (existsSync(fp)) - { + if (fs.existsSync(fp)) return fp; - } const segs = cwd.split(path.sep); let len = segs.length; @@ -35,10 +36,8 @@ function findFile(cwd: string, filename: string) { cwd = segs.slice(0, len).join("/"); fp = cwd + "/" + filename; - if (existsSync(fp)) - { + if (fs.existsSync(fp)) return fp; - } } return null; @@ -61,6 +60,9 @@ try function parseTsConfig() { const fileName = findFile(process.cwd(), "tsconfig.json"); + if (!fileName) + throw new Error(`couldn't find 'tsconfig.json' in ${process.cwd()}`); + const text = ts.sys.readFile(fileName); const result = ts.parseConfigFileTextToJson(fileName, text); const configParseResult = ts.parseJsonConfigFileContent(result.config, ts.sys, path.dirname(fileName), getOptionsOverrides(), fileName); @@ -68,58 +70,56 @@ function parseTsConfig() return configParseResult; } -interface Message +function printDiagnostics(context: IContext, diagnostics: IDiagnostics[]) { - message: string; -} -interface Context -{ - warn(message: Message): void; - error(message: Message): void; -} -function printDiagnostics(context: Context, diagnostics: ts.Diagnostic[]) -{ - diagnostics.forEach((diagnostic) => + _.each(diagnostics, (diagnostic) => { - let message = ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"); - if (diagnostic.file) - { - let { line, character } = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start); - context.warn({ message: `${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}` }); - } + if (diagnostic.fileLine) + context.warn(`${diagnostic.fileLine}: ${colors.yellow(diagnostic.flatMessage)}`); else - context.warn({ message }); + context.warn(colors.yellow(diagnostic.flatMessage)); }); }; -export default function typescript (options: any) +interface IOptions +{ + include: string; + exclude: string; + check: boolean; + verbosity: number; + clean: boolean; + cacheRoot: string; +} + +export default function typescript (options: IOptions) { options = { ... options }; - const filter = createFilter(options.include || [ "*.ts+(|x)", "**/*.ts+(|x)" ], options.exclude || [ "*.d.ts", "**/*.d.ts" ]); + _.defaults(options, + { + check: true, + verbosity: VerbosityLevel.Info, + clean: false, + cacheRoot: `${process.cwd()}/.rts2_cache`, + include: [ "*.ts+(|x)", "**/*.ts+(|x)" ], + exclude: [ "*.d.ts", "**/*.d.ts" ], + }); - delete options.include; - delete options.exclude; + const filter = createFilter(options.include, options.exclude); let parsedConfig = parseTsConfig(); - const servicesHost: ts.LanguageServiceHost = { - getScriptFileNames: () => parsedConfig.fileNames, - getScriptVersion: (_fileName) => "0", - getScriptSnapshot: (fileName) => - { - if (!fs.existsSync(fileName)) - return undefined; - - return ts.ScriptSnapshot.fromString(ts.sys.readFile(fileName)); - }, - getCurrentDirectory: () => process.cwd(), - getCompilationSettings: () => parsedConfig.options, - getDefaultLibFileName: (opts) => ts.getDefaultLibFilePath(opts), - }; + const servicesHost = new LanguageServiceHost(parsedConfig); const services = ts.createLanguageService(servicesHost, ts.createDocumentRegistry()); + const context = new ConsoleContext(options.verbosity, "rollup-plugin-typescript2: "); + + const cache = new Cache(servicesHost, options.cacheRoot, parsedConfig.options, parsedConfig.fileNames, context); + + if (options.clean) + cache.clean(); + return { resolveId(importee: string, importer: string) @@ -136,6 +136,9 @@ export default function typescript (options: any) if (result.resolvedModule && result.resolvedModule.resolvedFileName) { + if (filter(result.resolvedModule.resolvedFileName)) + cache.setDependency(result.resolvedModule.resolvedFileName, importer); + if (_.endsWith(result.resolvedModule.resolvedFileName, ".d.ts")) return null; @@ -145,35 +148,68 @@ export default function typescript (options: any) return null; }, - load(id: string): any + load(id: string): string | undefined { if (id === "\0" + TSLIB) return tslibSource; + + return undefined; }, - transform(this: Context, _code: string, id: string): any + transform(this: IRollupContext, code: string, id: string): ICode | undefined { - if (!filter(id)) return null; + if (!filter(id)) + return undefined; - let output = services.getEmitOutput(id); + const snapshot = servicesHost.setSnapshot(id, code); + let result = cache.getCompiled(id, snapshot, () => + { + const output = services.getEmitOutput(id); - let allDiagnostics = services - .getCompilerOptionsDiagnostics() - .concat(services.getSyntacticDiagnostics(id)) - .concat(services.getSemanticDiagnostics(id)); + if (output.emitSkipped) + this.error({ message: colors.red(`failed to transpile ${id}`)}); - printDiagnostics(this, allDiagnostics); + const transpiled = _.find(output.outputFiles, (entry: ts.OutputFile) => _.endsWith(entry.name, ".js") ); + const map = _.find(output.outputFiles, (entry: ts.OutputFile) => _.endsWith(entry.name, ".map") ); - if (output.emitSkipped) - this.error({ message: `failed to transpile ${id}`}); + return { + code: transpiled ? transpiled.text : undefined, + map: map ? JSON.parse(map.text) : { mappings: "" }, + }; + }); - const code: ts.OutputFile = _.find(output.outputFiles, (entry: ts.OutputFile) => _.endsWith(entry.name, ".js") ); - const map: ts.OutputFile = _.find(output.outputFiles, (entry: ts.OutputFile) => _.endsWith(entry.name, ".map") ); + return result; + }, - return { - code: code ? code.text : undefined, - map: map ? JSON.parse(map.text) : { mappings: "" }, - }; + outro(): void + { + cache.compileDone(); + + if (options.check) + { + cache.walkTree((id: string) => + { + const snapshot = servicesHost.getScriptSnapshot(id); + + if (!snapshot) + { + context.error(colors.red(`failed lo load snapshot for ${id}`)); + return; + } + + const diagnostics = cache.getDiagnostics(id, snapshot, () => + { + return services + .getCompilerOptionsDiagnostics() + .concat(services.getSyntacticDiagnostics(id)) + .concat(services.getSemanticDiagnostics(id)); + }); + + printDiagnostics(context, diagnostics); + }); + } + + cache.diagnosticsDone(); }, }; } diff --git a/src/rollingcache.ts b/src/rollingcache.ts new file mode 100644 index 0000000..aab4340 --- /dev/null +++ b/src/rollingcache.ts @@ -0,0 +1,87 @@ +import * as fs from "fs-extra"; +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 +{ + private oldCacheRoot: string; + private newCacheRoot: string; + + /** + * @param cacheRoot: root folder for the cache + * @param checkNewCache: whether to also look in new cache when reading from cache + */ + constructor(private cacheRoot: string, private checkNewCache: boolean) + { + this.oldCacheRoot = `${this.cacheRoot}/cache`; + this.newCacheRoot = `${this.cacheRoot}/cache_`; + + fs.emptyDirSync(this.newCacheRoot); + } + + /** + * @returns true if name exist in old cache (or either old of new cache if checkNewCache is true) + */ + public exists(name: string): boolean + { + if (this.checkNewCache && fs.existsSync(`${this.newCacheRoot}/${name}`)) + return true; + + return fs.existsSync(`${this.oldCacheRoot}/${name}`); + } + + /** + * @returns true if old cache contains all names and nothing more + */ + public match(names: string[]): boolean + { + if (!fs.existsSync(this.oldCacheRoot)) + return names.length === 0; // empty folder matches + + return _.isEqual(fs.readdirSync(this.oldCacheRoot).sort(), names.sort()); + } + + /** + * @returns data for name, must exist in old cache (or either old of new cache if checkNewCache is true) + */ + public read(name: string): DataType + { + if (this.checkNewCache && fs.existsSync(`${this.newCacheRoot}/${name}`)) + return fs.readJsonSync(`${this.newCacheRoot}/${name}`, "utf8"); + + return fs.readJsonSync(`${this.oldCacheRoot}/${name}`, "utf8"); + } + + public write(name: string, data: DataType): void + { + if (data === undefined) + return; + + if (this.checkNewCache) + 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}`, () => { ; }); + } + + /** + * clears old cache and moves new in its place + */ + public roll() + { + fs.remove(this.oldCacheRoot, () => + { + fs.move(this.newCacheRoot, this.oldCacheRoot, () => { ; }); + }); + } +} diff --git a/tsconfig.json b/tsconfig.json index 7f6146b..26ac9c0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,8 @@ "listFiles": true, "pretty": true, "moduleResolution": "node", - "noEmitOnError": true + "noEmitOnError": true, + "strictNullChecks": true }, "include": [ "typings/**/*.d.ts",