diff --git a/lib/Host.js b/lib/Host.js index 9b670fb..42f8b14 100644 --- a/lib/Host.js +++ b/lib/Host.js @@ -3,14 +3,17 @@ var commondir = require('commondir'); var events = require('events'); var fs = require('fs'); +var realpath = require('fs.realpath') var log = require('util').debuglog(require('../package').name); +var trace = require('util').debuglog(require('../package').name + '-trace'); var os = require('os'); var path = require('path'); var util = require('util'); module.exports = function (ts) { - function Host(currentDirectory, languageVersion) { - this.currentDirectory = currentDirectory; + function Host(currentDirectory, outputDirectory, languageVersion) { + this.currentDirectory = this.getCanonicalFileName(path.resolve(currentDirectory)); + this.outputDirectory = this.getCanonicalFileName(path.resolve(outputDirectory)); this.languageVersion = languageVersion; this.files = {}; this.previousFiles = {}; @@ -31,13 +34,21 @@ module.exports = function (ts) { log('Resetting (version %d)', this.version); }; - Host.prototype._normalizedRelative = function(filename) { - return ts.normalizePath(path.relative(this.currentDirectory, path.resolve(filename))); - }; - Host.prototype._addFile = function (filename, root) { - var normalized = this._normalizedRelative(filename); - log('Parsing %s (norm: %s)', filename, normalized); + + // Ensure that the relative, non-canonical file name is what's passed + // to 'createSourceFile', as that's the name that will be used in error + // messages, etc. + + var relative = ts.normalizeSlashes(path.relative( + this.currentDirectory, + path.resolve( + this.currentDirectory, + filename + ) + )); + var canonical = this._canonical(filename); + trace('Parsing %s', canonical); var text; try { @@ -47,52 +58,44 @@ module.exports = function (ts) { } var file; - var current = this.files[normalized]; - var previous = this.previousFiles[normalized]; + var current = this.files[canonical]; + var previous = this.previousFiles[canonical]; var version; if (current && current.contents === text) { file = current.ts; version = current.version; - log('Reused current file %s (version %d)', normalized, version); + trace('Reused current file %s (version %d)', canonical, version); } else if (previous && previous.contents === text) { file = previous.ts; version = previous.version; - log('Reused previous file %s (version %d)', normalized, version); + trace('Reused previous file %s (version %d)', canonical, version); } else { - file = ts.createSourceFile(filename, text, this.languageVersion, true); + file = ts.createSourceFile(relative, text, this.languageVersion, true); version = this.version; - log('New version of source file %s (version %d)', normalized, version); + trace('New version of source file %s (version %d)', canonical, version); } - this.files[normalized] = { - filename: filename, + this.files[canonical] = { + filename: relative, contents: text, ts: file, root: root, version: version }; - - this._emitFile(normalized); + this.emit('file', canonical, relative); return file; }; - Host.prototype._emitFile = function (normalized) { - var idPath = './' + normalized; - var fullPath = path.resolve(idPath); - this.emit('file', fullPath, idPath); - } - Host.prototype.getSourceFile = function (filename) { - var normalized = this._normalizedRelative(filename); - - if (this.files[normalized]) - return this.files[normalized].ts; - - if (normalized === '__lib.d.ts') + if (filename === '__lib.d.ts') { return this.libDefault; - + } + var canonical = this._canonical(filename); + if (this.files[canonical]) { + return this.files[canonical].ts; + } return this._addFile(filename, false); }; @@ -103,9 +106,19 @@ module.exports = function (ts) { }; Host.prototype.writeFile = function (filename, data) { - var normalized = this._normalizedRelative(filename); - log('Cache write %s (norm: %s)', filename, normalized); - this.output[normalized] = data; + + var outputCanonical = this._canonical(filename); + log('Cache write %s', outputCanonical); + this.output[outputCanonical] = data; + + var sourceCanonical = this._inferSourceCanonical(outputCanonical); + var sourceFollowed = this._follow(path.dirname(sourceCanonical)) + '/' + path.basename(sourceCanonical); + + if (sourceFollowed !== sourceCanonical) { + outputCanonical = this._inferOutputCanonical(sourceFollowed); + log('Cache write (followed) %s', outputCanonical); + this.output[outputCanonical] = data; + } }; Host.prototype.getCurrentDirectory = function () { @@ -113,7 +126,7 @@ module.exports = function (ts) { }; Host.prototype.getCanonicalFileName = function (filename) { - return this._normalizedRelative(filename); + return ts.normalizeSlashes(ts.sys.useCaseSensitiveFileNames ? filename : filename.toLowerCase()); }; Host.prototype.useCaseSensitiveFileNames = function () { @@ -130,8 +143,7 @@ module.exports = function (ts) { }; Host.prototype.readFile = function (filename) { - var normalized = this._normalizedRelative(filename); - return ts.sys.readFile(normalized); + return ts.sys.readFile(filename); }; Host.prototype._rootDir = function () { @@ -140,10 +152,91 @@ module.exports = function (ts) { if (!Object.hasOwnProperty.call(this.files, filename)) continue; if (/\.d\.ts$/.test(filename)) continue; - dirs.push(path.dirname(filename)); + dirs.push(this.getCanonicalFileName(path.dirname(filename))); } var result = commondir(this.currentDirectory, dirs); - return result; + return this.getCanonicalFileName(result); + }; + + Host.prototype._rootFilenames = function () { + + var rootFilenames = []; + + for (var filename in this.files) { + if (!Object.hasOwnProperty.call(this.files, filename)) continue; + if (!this.files[filename].root) continue; + rootFilenames.push(filename); + } + return rootFilenames; + } + + Host.prototype._output = function (filename) { + + var outputCanonical = this._inferOutputCanonical(filename); + log('Cache read %s', outputCanonical); + + var output = this.output[outputCanonical]; + if (!output) { + log('Cache miss on %s', outputCanonical); + } + return output; + } + + Host.prototype._canonical = function (filename) { + return this.getCanonicalFileName(path.resolve( + this.currentDirectory, + filename + )); + } + + Host.prototype._inferOutputCanonical = function (filename) { + + var sourceCanonical = this._canonical(filename); + var outputRelative = path.relative( + this._rootDir(), + sourceCanonical + ); + var outputCanonical = this.getCanonicalFileName(path.resolve( + this.outputDirectory, + outputRelative + )); + return outputCanonical; + } + + Host.prototype._inferSourceCanonical = function (filename) { + + var outputCanonical = this._canonical(filename); + var outputRelative = path.relative( + this.outputDirectory, + outputCanonical + ); + var sourceCanonical = this.getCanonicalFileName(path.resolve( + this._rootDir(), + outputRelative + )); + return sourceCanonical; + } + + Host.prototype._follow = function (filename) { + + filename = this._canonical(filename); + var basename; + var parts = []; + + do { + var stats = fs.lstatSync(filename); + if (stats.isSymbolicLink()) { + filename = realpath.realpathSync(filename); + } else { + basename = path.basename(filename); + if (basename) { + parts.unshift(basename); + filename = path.dirname(filename); + } + } + } while (basename); + + return ts.normalizeSlashes(filename + parts.join('/')); }; return Host; diff --git a/lib/Tsifier.js b/lib/Tsifier.js index 87835dd..32d9596 100644 --- a/lib/Tsifier.js +++ b/lib/Tsifier.js @@ -5,6 +5,7 @@ var events = require('events'); var extend = require('util')._extend; var fs = require('fs'); var log = require('util').debuglog(require('../package').name); +var trace = require('util').debuglog(require('../package').name + '-trace'); var path = require('path'); var through = require('through2'); var time = require('./time'); @@ -38,10 +39,6 @@ module.exports = function (ts) { return file.replace(/\.\w+$/i, extension); } - function getRelativeFilename(file) { - return './' + ts.normalizeSlashes(path.relative(currentDirectory, file)); - } - function fileExists(file) { try { var stats = fs.lstatSync(file); @@ -128,7 +125,11 @@ module.exports = function (ts) { self.opts = parsedOptions.options; self.files = parsedOptions.fileNames; self.bopts = bopts; - self.host = new Host(currentDirectory, this.opts.target); + self.host = new Host( + currentDirectory, + this.opts.outDir, + this.opts.target + ); self.host.on('file', function (file, id) { self.emit('file', file, id); @@ -140,15 +141,15 @@ module.exports = function (ts) { Tsifier.prototype.reset = function () { var self = this; self.host._reset(); - self.addAll(self.files.map(getRelativeFilename)); + self.addFiles(self.files); }; Tsifier.prototype.generateCache = function (files) { - this.addAll(files.map(getRelativeFilename)); + this.addFiles(files); this.compile(); }; - Tsifier.prototype.addAll = function (files) { + Tsifier.prototype.addFiles = function (files) { var self = this; files.forEach(function (file) { self.host._addFile(file, true); @@ -157,12 +158,7 @@ module.exports = function (ts) { Tsifier.prototype.compile = function () { var self = this; - var rootFilenames = []; - for (var filename in self.host.files) { - if (!Object.hasOwnProperty.call(self.host.files, filename)) continue; - if (!self.host.files[filename].root) continue; - rootFilenames.push(filename); - } + var rootFilenames = self.host._rootFilenames(); log('Compiling files:'); rootFilenames.forEach(function (file) { log(' %s', file); }); @@ -252,7 +248,7 @@ module.exports = function (ts) { Tsifier.prototype.transform = function (file) { var self = this; - log('Transforming %s', file); + trace('Transforming %s', file); if (isTypescriptDeclaration(file)) { return through(transform); @@ -271,7 +267,7 @@ module.exports = function (ts) { if (self.host.error) return; - var compiled = self.getCompiledFile(getRelativeFilename(file)); + var compiled = self.getCompiledFile(file); if (compiled) { this.push(compiled); } @@ -282,22 +278,12 @@ module.exports = function (ts) { Tsifier.prototype.getCompiledFile = function (inputFile, alreadyMissedCache) { var self = this; - var normalized = ts.normalizePath(inputFile); - var rootDir = self.host._rootDir(); - var outputExtension = (self.opts.jsx === ts.JsxEmit.Preserve && isTsx(inputFile)) ? '.jsx' : '.js'; - var outputFile = '__tsify__/' + ts.normalizeSlashes(path.relative( - rootDir, - path.resolve(replaceFileExtension(normalized, outputExtension)) - )); - var output = self.host.output[outputFile]; - - log('Cache read %s (norm: %s)', outputFile, normalized); + var output = self.host._output(replaceFileExtension(inputFile, outputExtension)); if (!output) { if (alreadyMissedCache) return; - log('Cache miss on %s (norm: %s)', outputFile, normalized); self.generateCache([inputFile]); if (self.host.error) return; @@ -305,17 +291,17 @@ module.exports = function (ts) { } if (self.opts.inlineSourceMap) { - output = self.setFullSourcePathInSourcemap(output, normalized); + output = self.setSourcePathInSourcemap(output, inputFile); } - return output; }; - Tsifier.prototype.setFullSourcePathInSourcemap = function (output, normalized) { + Tsifier.prototype.setSourcePathInSourcemap = function (output, inputFile) { var self = this; - if (self.bopts.basedir) { - normalized = ts.normalizeSlashes(path.relative(self.bopts.basedir, normalized)); - } + var normalized = ts.normalizePath(path.relative( + self.bopts.basedir || currentDirectory, + inputFile + )); var sourcemap = convert.fromComment(output); sourcemap.setProperty('sources', [normalized]); diff --git a/package.json b/package.json index 359bd5b..32851f1 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "eslint": "^2.7.0", "event-stream": "^3.3.1", "fs-extra": "^0.28.0", + "fs.realpath": "^1.0.0", "node-foo": "^0.2.3", "semver": "^5.1.0", "source-map": "^0.5.3",