Merge pull request #1 from ezolenko/cache

Cache
This commit is contained in:
Eugene Zolenko 2017-02-10 22:35:51 -07:00 committed by GitHub
commit 0a13321fc6
12 changed files with 1234 additions and 168 deletions

View File

@ -1,3 +1,4 @@
{
"typescript.tsdk": "./node_modules/typescript/lib"
"editor.tabSize": 4,
"editor.useTabStops": true
}

View File

@ -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+.

View File

@ -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();
},
};
}

View File

@ -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();
},
};
}

View File

@ -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",

View File

@ -7,11 +7,14 @@ export default {
external: [
'path',
'fs',
'fs-extra',
'object-assign',
'rollup-pluginutils',
'typescript',
'lodash'
'lodash',
'graphlib',
'object-hash',
'colors/safe'
],
plugins: [

219
src/cache.ts Normal file
View File

@ -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<ICode | undefined>;
private typesCache: RollingCache<string>;
private diagnosticsCache: RollingCache<IDiagnostics[]>;
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<ICode>(`${this.cacheDir}/code`, true);
this.typesCache = new RollingCache<string>(`${this.cacheDir}/types`, false);
this.diagnosticsCache = new RollingCache<IDiagnostics[]>(`${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;
});
}
}

61
src/context.ts Normal file
View File

@ -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}`);
}
}

59
src/host.ts Normal file
View File

@ -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);
}
}

View File

@ -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();
},
};
}

87
src/rollingcache.ts Normal file
View File

@ -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 <DataType>
{
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, () => { ; });
});
}
}

View File

@ -12,7 +12,8 @@
"listFiles": true,
"pretty": true,
"moduleResolution": "node",
"noEmitOnError": true
"noEmitOnError": true,
"strictNullChecks": true
},
"include": [
"typings/**/*.d.ts",