/// import typescript = require('typescript-api'); import gutil = require('gulp-util'); import path = require('path'); import stream = require('stream'); import fs = require('fs'); // Only used for readonly access import sourcemapApply = require('vinyl-sourcemaps-apply'); var defaultLibSnapshot = typescript.ScriptSnapshot.fromString( fs.readFileSync(path.join(__dirname, '../lib.d.ts')).toString('utf8') ); export interface Map { [key: string]: T; } export interface FileData { file?: gutil.File; content: string; scriptSnapshot: typescript.IScriptSnapshot; byteOrderMark: typescript.ByteOrderMark; referencedFiles: string[]; addedToCompiler: boolean; } export class Project implements typescript.IReferenceResolverHost { /** * Files from the previous compilation. * Used to find the differences with the previous compilation, to make the new compilation faster. */ previousFiles: Map = {}; /** * The files in the current compilation. * This Map only contains the files in the project, not external files. Those are in Project#additionalFiles. * The file property of the FileData objects in this Map are set. */ currentFiles: Map = {}; /** * External files of the current compilation. * When a file is imported by or referenced from another file, and the file is not one of the input files, it * is added to this Map. The file property of the FileData objects in this Map are not set. */ additionalFiles: Map = {}; /** * A complete list of all the files in the current compilation. * The list can contain duplicates, but that doesn't matter for the code below. */ references: string[] = []; /** * Whether there was a 'no-default-lib' tag found in one of the files in the current compilation. */ private hasNoDefaultLibTag: boolean = false; /** * Whether the default lib.d.ts was added to the compiler. Used by Project#setDefaultLib. */ private defaultLibInCompiler: boolean = false; /** * Whether there should not be loaded external files to the project. * Example: * In the lib directory you have .ts files. * In the definitions directory you have the .d.ts files. * If you turn this option on, you should add in your gulp file the definitions directory as an input source. * Advantage: * - Faster builds * Disadvantage: * - If you forget some directory, your compile will fail. */ private noExternalResolve: boolean; /** * Sort output based on tags. * tsc does this when you pass the --out parameter. */ private sortOutput: boolean; compiler: typescript.TypeScriptCompiler; /** * The version number of the compilation. * This number is increased for every compilation in the same gulp session. * Used for incremental builds. */ version: number = 0; constructor(settings: typescript.ImmutableCompilationSettings, noExternalResolve: boolean, sortOutput: boolean) { this.compiler = new typescript.TypeScriptCompiler(new typescript.NullLogger(), settings); if (!settings.noLib()) { this.setDefaultLib(true); } this.noExternalResolve = noExternalResolve; this.sortOutput = sortOutput; } /** * Adds or removes lib.d.ts */ setDefaultLib(active: boolean) { if (active != this.defaultLibInCompiler) { if (active) { // Add defaultLib this.compiler.addFile('lib.d.ts', defaultLibSnapshot, typescript.ByteOrderMark.Utf8, this.version, false, []); } else { // Remove lib.d.ts this.compiler.removeFile('lib.d.ts'); } this.defaultLibInCompiler = active; } } /** * Resets the compiler. * The compiler needs to be reset for incremental builds. */ reset() { this.previousFiles = this.currentFiles; this.currentFiles = {}; this.references = []; for (var filename in this.additionalFiles) { if (!Object.prototype.hasOwnProperty.call(this.additionalFiles, filename)) { continue; } this.compiler.removeFile(filename); } this.additionalFiles = {}; this.version++; this.hasNoDefaultLibTag = false; } /** * Adds a file to the project. */ addFile(file: gutil.File) { this.currentFiles[this.normalizePath(file.path)] = this.getFileDataFromGulpFile(file); } private getOriginalName(filename: string): string { return filename.replace(/(\.d\.ts|\.js|\.js.map)$/, '.ts') } private getError(info: typescript.Diagnostic) { var filename = this.getOriginalName(info.fileName()) var file = this.currentFiles[filename]; if (file) { filename = path.relative(file.file.cwd, info.fileName()); } else { filename = info.fileName(); } var err = new Error(); err.name = 'TypeScript error'; err.message = gutil.colors.red(filename + '(' + info.line() + ',' + info.character() + '): ') + info.message(); return err; } /** * Compiles the input files */ compile(jsStream: stream.Readable, declStream: stream.Readable, errorCallback: (err: Error) => void) { // Delete files that are in previousFiles, but not in currentFiles for (var filename in this.previousFiles) { if (!Object.prototype.hasOwnProperty.call(this.previousFiles, filename)) { continue; } if (!this.currentFiles[filename]) { this.compiler.removeFile(filename); } } // Add / update files in currentFiles for (var filename in this.currentFiles) { if (!Object.prototype.hasOwnProperty.call(this.currentFiles, filename)) { continue; } var fileData = this.currentFiles[filename]; fileData.addedToCompiler = true; if (this.previousFiles[filename]) { // Update var range = this.getTextChangeRange(this.previousFiles[filename].content, fileData.content); if (range != typescript.TextChangeRange.unchanged) { this.compiler.updateFile(filename, fileData.scriptSnapshot, this.version, false, range); } } else { // Add this.compiler.addFile(filename, fileData.scriptSnapshot, fileData.byteOrderMark, this.version, false, /*referenceStrings*/ []); } } // Look for external files (imports and references to files outside the input files) if (!this.noExternalResolve || this.sortOutput) { for (var filename in this.currentFiles) { if (!Object.prototype.hasOwnProperty.call(this.currentFiles, filename)) { continue; } if (this.references.indexOf(filename) != -1 && !this.sortOutput) { continue; } var references: typescript.ReferenceResolutionResult = typescript.ReferenceResolver.resolve([filename], this, false); var referenceStrings: string[] = references.resolvedFiles.map((ref) => this.normalizePath(ref.path)); this.currentFiles[filename].referencedFiles = referenceStrings; this.references = this.references.concat(referenceStrings); this.hasNoDefaultLibTag = this.hasNoDefaultLibTag || references.seenNoDefaultLibTag; } } if (!this.noExternalResolve) { this.setDefaultLib(!this.hasNoDefaultLibTag); this.handleReferences(this.references); } var results = this.compiler.compile(path => typescript.IO.resolvePath(path)); var outputJS: gutil.File[] = []; var sourcemaps: { [ filename: string ]: string } = {}; while (results.moveNext()) { var res = results.current(); res.diagnostics.forEach(item => { errorCallback(this.getError(item)); }); res.outputFiles.forEach(outputFile => { var originalName = this.getOriginalName(outputFile.name); var original: FileData = this.currentFiles[originalName]; if (!original) return; if (outputFile.fileType === typescript.OutputFileType.SourceMap) { sourcemaps[originalName] = outputFile.text; } switch (outputFile.fileType) { case typescript.OutputFileType.JavaScript: var file = new gutil.File({ path: outputFile.name, contents: new Buffer(this.removeSourceMapComment(outputFile.text)), cwd: original.file.cwd, base: original.file.base }); if (original.file.sourceMap) file.sourceMap = original.file.sourceMap; outputJS.push(file); break; case typescript.OutputFileType.Declaration: var file = new gutil.File({ path: outputFile.name, contents: new Buffer(outputFile.text), cwd: original.file.cwd, base: original.file.base }); declStream.push(file); break; } }); } var emit = (originalName: string, file: gutil.File) => { var map = sourcemaps[originalName]; if (map) sourcemapApply(file, map); jsStream.push(file); }; if (this.sortOutput) { var done: { [ filename: string] : boolean } = {}; var sortedEmit = (originalName: string, file: gutil.File) => { if (done[originalName]) return; done[originalName] = true; var inputFile = this.currentFiles[originalName]; for (var j = 0; j < outputJS.length; ++j) { var other = outputJS[j]; var otherName = this.getOriginalName(other.path); if (inputFile.referencedFiles.indexOf(otherName) !== -1) { sortedEmit(otherName, other); } } emit(originalName, file); }; for (var i = 0; i < outputJS.length; ++i) { var file = outputJS[i]; var originalName = this.getOriginalName(file.path); sortedEmit(originalName, file); } } else { for (var i = 0; i < outputJS.length; ++i) { var file = outputJS[i]; var originalName = this.getOriginalName(file.path); emit(originalName, file); } } } private handleReferences(references: string[]) { references.forEach((filename) => { filename = this.normalizePath(filename); if (!(this.currentFiles[filename] || this.additionalFiles[filename].addedToCompiler)) { var data = this.additionalFiles[filename]; this.compiler.addFile(filename, data.scriptSnapshot, data.byteOrderMark, this.version, false, []); data.addedToCompiler = true; } }); } private getFileDataFromGulpFile(file: gutil.File): FileData { var str = file.contents.toString('utf8'); var data = this.getFileData(this.normalizePath(file.path), str); data.file = file; return data; } private getFileData(filename: string, content: string): FileData { return { content: content, scriptSnapshot: typescript.ScriptSnapshot.fromString(content), byteOrderMark: typescript.ByteOrderMark.Utf8, referencedFiles: [], addedToCompiler: false } } private getTextChangeRange(oldStr: string, newStr: string) { var begin = 0; var end = 0; var max = newStr.length > oldStr.length ? newStr : oldStr; var min = newStr.length > oldStr.length ? oldStr : newStr; if (min == max) return typescript.TextChangeRange.unchanged; for (var i = 0; i < min.length; ++i) { if (min.charAt(i) == max.charAt(i)) { begin = i + 1; } else { break; } } for (var i = 0; i + begin < min.length; ++i) { if (min.charAt(min.length - 1 - i) == max.charAt(max.length - 1 - i)) { end = i + 1; } else { break; } } return new typescript.TextChangeRange(new typescript.TextSpan(begin, oldStr.length - begin - end), newStr.length - begin - end); } private removeSourceMapComment(content: string): string { // By default the TypeScript automaticly inserts a source map comment. // This should be removed because gulp-sourcemaps takes care of that. // The comment is always on the last line, so it's easy to remove it // (But the last line also ends with a \n, so we need to look for the \n before the other) var index = content.lastIndexOf('\n', content.length - 2); return content.substring(0, index) + '\n'; } normalizePath(path: string) { path = this.resolvePath(path); // Switch to forward slashes path = typescript.switchToForwardSlashes(path); return path; } // IReferenceResolverHost getScriptSnapshot(filename: string): typescript.IScriptSnapshot { filename = this.normalizePath(filename); if (this.currentFiles[filename]) { return this.currentFiles[filename].scriptSnapshot; } else if (this.additionalFiles[filename]) { return this.additionalFiles[filename].scriptSnapshot; } else if (!this.noExternalResolve) { var data: string = fs.readFileSync(filename).toString('utf8'); this.additionalFiles[filename] = this.getFileData(filename, data); return this.additionalFiles[filename].scriptSnapshot; } } resolveRelativePath(path: string, directory: string): string { var unQuotedPath = typescript.stripStartAndEndQuotes(path); var normalizedPath: string; if (typescript.isRooted(unQuotedPath) || !directory) { normalizedPath = unQuotedPath; } else { normalizedPath = typescript.IOUtils.combine(directory, unQuotedPath); } // get the absolute path normalizedPath = this.resolvePath(normalizedPath); // Switch to forward slashes normalizedPath = typescript.switchToForwardSlashes(normalizedPath); return normalizedPath; } fileExists(path: string): boolean { if (this.currentFiles[path] || this.additionalFiles[path]) { return true; } else if (!this.noExternalResolve) { return typescript.IO.fileExists(path); } else { return false; } } getParentDirectory(path: string): string { return typescript.IO.dirName(path); } directoryExists(path: string): boolean { var newPath = path; if (newPath.substr(newPath.length - 1) != '/') { newPath += '/'; } for (var filename in this.currentFiles) { if (!Object.prototype.hasOwnProperty.call(this.currentFiles, filename)) { continue; } if (filename.length > newPath.length) { if (filename.substring(0, newPath.length) == newPath) { return true; } } } for (var filename in this.additionalFiles) { if (!Object.prototype.hasOwnProperty.call(this.additionalFiles, filename)) { continue; } if (filename.length > newPath.length) { if (filename.substring(0, newPath.length) == newPath) { return true; } } } if (this.noExternalResolve) { return false; } else { return typescript.IO.directoryExists(path); } } resolvePath(path: string): string { return typescript.IO.resolvePath(path); } }