From 1d0078415fdd5c5e634b99dc31f0ac71bc954cb4 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Mon, 4 May 2015 08:27:14 -0700 Subject: [PATCH] build(broccoli): refactor typescript plugin to be incremental via DiffingBroccoliPlugin --- gulpfile.js | 5 +- tools/broccoli/broccoli-typescript.ts | 172 ++++++++++++++++++++++ tools/broccoli/diffing-broccoli-plugin.ts | 23 +-- tools/broccoli/trees/browser_tree.ts | 22 ++- tools/broccoli/trees/node_tree.ts | 17 ++- tools/broccoli/typescript/index.ts | 55 ------- 6 files changed, 214 insertions(+), 80 deletions(-) create mode 100644 tools/broccoli/broccoli-typescript.ts delete mode 100644 tools/broccoli/typescript/index.ts diff --git a/gulpfile.js b/gulpfile.js index d660a9a572..74f4e6edfa 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -146,7 +146,10 @@ gulp.task('build/format.dart', rundartpackage(gulp, gulpPlugins, { function doCheckFormat() { return gulp.src(['Brocfile*.js', 'modules/**/*.ts', 'tools/**/*.ts', '!**/typings/**/*.d.ts', - '!tools/broccoli/tree-differ.ts']) // See https://github.com/angular/clang-format/issues/4 + // skipped due to https://github.com/angular/clang-format/issues/4 + '!tools/broccoli/tree-differ.ts', + // skipped due to https://github.com/angular/gulp-clang-format/issues/3 + '!tools/broccoli/broccoli-typescript.ts' ]) .pipe(format.checkFormat('file')); } diff --git a/tools/broccoli/broccoli-typescript.ts b/tools/broccoli/broccoli-typescript.ts new file mode 100644 index 0000000000..86e131662b --- /dev/null +++ b/tools/broccoli/broccoli-typescript.ts @@ -0,0 +1,172 @@ +/// +/// + +import fs = require('fs'); +import fse = require('fs-extra'); +import path = require('path'); +import ts = require('typescript'); +import {wrapDiffingPlugin, DiffingBroccoliPlugin, DiffResult} from './diffing-broccoli-plugin'; + + +type FileRegistry = ts.Map<{version: number}>; + +const FS_OPTS = {encoding: 'utf-8'}; + + +/** + * Broccoli plugin that implements incremental Typescript compiler. + * + * It instantiates a typescript compiler instance that keeps all the state about the project and + * can reemit only the files that actually changed. + * + * Limitations: only files that map directly to the changed source file via naming conventions are + * reemited. This primarily affects code that uses `const enum`s, because changing the enum value + * requires global emit, which can affect many files. + */ +class DiffingTSCompiler implements DiffingBroccoliPlugin { + private tsOpts: ts.CompilerOptions; + private fileRegistry: FileRegistry = Object.create(null); + private rootFilePaths: string[]; + private tsServiceHost: ts.LanguageServiceHost; + private tsService: ts.LanguageService; + + + constructor(public inputPath: string, public cachePath: string, public options) { + this.tsOpts = Object.create(options); + this.tsOpts.outDir = this.cachePath; + this.tsOpts.target = (ts).ScriptTarget[options.target]; + this.rootFilePaths = options.rootFilePaths ? options.rootFilePaths.splice(0) : []; + this.tsServiceHost = new CustomLanguageServiceHost(this.tsOpts, this.rootFilePaths, + this.fileRegistry, this.inputPath); + this.tsService = ts.createLanguageService(this.tsServiceHost, ts.createDocumentRegistry()) + } + + + rebuild(treeDiff: DiffResult) { + let pathsToEmit = []; + let pathsWithErrors = []; + + treeDiff.changedPaths.filter((changedPath) => + changedPath.match(/\.ts/) && !changedPath.match(/\.d\.ts/)) + .forEach((tsFilePath) => { + if (!this.fileRegistry[tsFilePath]) { + this.fileRegistry[tsFilePath] = {version: 0}; + this.rootFilePaths.push(tsFilePath); + } else { + this.fileRegistry[tsFilePath].version++; + } + + pathsToEmit.push(tsFilePath); + }); + + treeDiff.removedPaths.filter((changedPath) => + changedPath.match(/\.ts/) && !changedPath.match(/\.d\.ts/)) + .forEach((tsFilePath) => { + console.log('removing outputs for', tsFilePath); + + this.rootFilePaths.splice(this.rootFilePaths.indexOf(tsFilePath), 1); + this.fileRegistry[tsFilePath] = null; + + let jsFilePath = tsFilePath.replace(/\.ts$/, '.js'); + let mapFilePath = tsFilePath.replace(/.ts$/, '.js.map'); + let dtsFilePath = tsFilePath.replace(/\.ts$/, '.d.ts'); + + fs.unlinkSync(path.join(this.cachePath, jsFilePath)); + fs.unlinkSync(path.join(this.cachePath, mapFilePath)); + fs.unlinkSync(path.join(this.cachePath, dtsFilePath)); + }); + + pathsToEmit.forEach((tsFilePath) => { + let output = this.tsService.getEmitOutput(tsFilePath); + + if (output.emitSkipped) { + let errorFound = this.logError(tsFilePath); + if (errorFound) { + pathsWithErrors.push(tsFilePath); + } + } else { + output.outputFiles.forEach(o => { + let destDirPath = path.dirname(o.name); + fse.mkdirsSync(destDirPath); + fs.writeFileSync(o.name, o.text, FS_OPTS); + }); + } + }); + + if (pathsWithErrors.length) { + throw new Error('Typescript found errors listed above...'); + } + } + + + private logError(tsFilePath) { + let allDiagnostics = this.tsService.getCompilerOptionsDiagnostics() + .concat(this.tsService.getSyntacticDiagnostics(tsFilePath)) + .concat(this.tsService.getSemanticDiagnostics(tsFilePath)); + + allDiagnostics.forEach(diagnostic => { + let message = ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"); + if (diagnostic.file) { + let{line, character} = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start); + console.log(` Error ${diagnostic.file.fileName} (${line + 1},${character + 1}): ` + + `${message}`); + } else { + console.log(` Error: ${message}`); + } + }); + + return !!allDiagnostics.length; + } +} + + +class CustomLanguageServiceHost implements ts.LanguageServiceHost { + private currentDirectory: string; + private defaultLibFilePath: string; + + + constructor(private compilerOptions: ts.CompilerOptions, private fileNames: string[], + private fileRegistry: FileRegistry, private treeInputPath: string) { + this.currentDirectory = process.cwd(); + this.defaultLibFilePath = ts.getDefaultLibFilePath(compilerOptions); + } + + + getScriptFileNames(): string[] { return this.fileNames; } + + + getScriptVersion(fileName: string): string { + return this.fileRegistry[fileName] && this.fileRegistry[fileName].version.toString(); + } + + + getScriptSnapshot(tsFilePath: string): ts.IScriptSnapshot { + // TODO: this method is called a lot, add cache + + let absoluteTsFilePath = (tsFilePath == this.defaultLibFilePath) ? + tsFilePath : + path.join(this.treeInputPath, tsFilePath); + + if (!fs.existsSync(absoluteTsFilePath)) { + // TypeScript seems to request lots of bogus paths during import path lookup and resolution, + // so we we just return undefined when the path is not correct. + return undefined; + } + return ts.ScriptSnapshot.fromString(fs.readFileSync(absoluteTsFilePath, FS_OPTS)); + } + + + getCurrentDirectory(): string { return this.currentDirectory; } + + + getCompilationSettings(): ts.CompilerOptions { return this.compilerOptions; } + + + getDefaultLibFileName(options: ts.CompilerOptions): string { + // ignore options argument, options should not change during the lifetime of the plugin + return this.defaultLibFilePath; + } +} + + +export default wrapDiffingPlugin(DiffingTSCompiler); diff --git a/tools/broccoli/diffing-broccoli-plugin.ts b/tools/broccoli/diffing-broccoli-plugin.ts index 952bbb8245..4d91cb85ac 100644 --- a/tools/broccoli/diffing-broccoli-plugin.ts +++ b/tools/broccoli/diffing-broccoli-plugin.ts @@ -62,19 +62,24 @@ class DiffingPluginWrapper implements BroccoliTree { rebuild() { - let firstRun = !this.initialized; - this.init(); + try { + let firstRun = !this.initialized; + this.init(); - let diffResult = this.treeDiffer.diffTree(); - diffResult.log(!firstRun); + let diffResult = this.treeDiffer.diffTree(); + diffResult.log(!firstRun); - var rebuildPromise = this.wrappedPlugin.rebuild(diffResult); + var rebuildPromise = this.wrappedPlugin.rebuild(diffResult); - if (rebuildPromise) { - return (>rebuildPromise).then(this.relinkOutputAndCachePaths.bind(this)); + if (rebuildPromise) { + return (>rebuildPromise).then(this.relinkOutputAndCachePaths.bind(this)); + } + + this.relinkOutputAndCachePaths(); + } catch (e) { + e.message = `[${this.description}]: ${e.message}`; + throw e; } - - this.relinkOutputAndCachePaths(); } diff --git a/tools/broccoli/trees/browser_tree.ts b/tools/broccoli/trees/browser_tree.ts index 5c675db629..7fd04d925d 100644 --- a/tools/broccoli/trees/browser_tree.ts +++ b/tools/broccoli/trees/browser_tree.ts @@ -8,8 +8,8 @@ var path = require('path'); var replace = require('broccoli-replace'); var stew = require('broccoli-stew'); var ts2dart = require('../broccoli-ts2dart'); -var TypescriptCompiler = require('../typescript'); +import compileWithTypescript from '../broccoli-typescript'; import destCopy from '../broccoli-dest-copy'; import {default as transpileWithTraceur, TRACEUR_RUNTIME_PATH} from '../traceur/index'; @@ -42,20 +42,26 @@ module.exports = function makeBrowserTree(options, destinationPath) { // Use TypeScript to transpile the *.ts files to ES6 // We don't care about errors: we let the TypeScript compilation to ES5 // in node_tree.ts do the type-checking. - var typescriptTree = new TypescriptCompiler(modulesTree, { - target: 'ES6', - sourceMap: true, - mapRoot: '', /* force sourcemaps to use relative path */ + var typescriptTree = compileWithTypescript(modulesTree, { allowNonTsExtensions: false, - typescript: require('typescript'), - noEmitOnError: false, + declaration: true, emitDecoratorMetadata: true, - outDir: 'angular2' + mapRoot: '', // force sourcemaps to use relative path + noEmitOnError: false, // temporarily ignore errors, we type-check only via cjs build + rootDir: '.', + sourceMap: true, + sourceRoot: '.', + target: 'ES6' }); typescriptTree = stew.rename(typescriptTree, '.js', '.es6'); var es6Tree = mergeTrees([traceurTree, typescriptTree]); + // TODO(iminar): tree differ seems to have issues with trees created by mergeTrees, investigate! + // ENOENT error is thrown while doing fs.readdirSync on inputRoot + // in the meantime, we just do noop mv to create a new tree + es6Tree = stew.mv(es6Tree, ''); + // Call Traceur again to lower the ES6 build tree to ES5 var es5Tree = transpileWithTraceur(es6Tree, { destExtension: '.js', diff --git a/tools/broccoli/trees/node_tree.ts b/tools/broccoli/trees/node_tree.ts index d244231dff..3c368a62f9 100644 --- a/tools/broccoli/trees/node_tree.ts +++ b/tools/broccoli/trees/node_tree.ts @@ -9,7 +9,7 @@ var replace = require('broccoli-replace'); var stew = require('broccoli-stew'); var ts2dart = require('../broccoli-ts2dart'); import transpileWithTraceur from '../traceur/index'; -var TypescriptCompiler = require('../typescript'); +import compileWithTypescript from '../broccoli-typescript'; var projectRootDir = path.normalize(path.join(__dirname, '..', '..', '..', '..')); @@ -98,17 +98,20 @@ module.exports = function makeNodeTree(destinationPath) { packageJsons, {files: ["**/**"], context: {'packageJson': COMMON_PACKAGE_JSON}}); - var typescriptTree = new TypescriptCompiler(modulesTree, { - target: 'ES5', - sourceMap: true, + var typescriptTree = compileWithTypescript(modulesTree, { + allowNonTsExtensions: false, + declaration: true, mapRoot: '', /* force sourcemaps to use relative path */ module: 'commonjs', - allowNonTsExtensions: false, - typescript: require('typescript'), noEmitOnError: true, - outDir: 'angular2' + rootDir: '.', + rootFilePaths: ['angular2/traceur-runtime.d.ts'], + sourceMap: true, + sourceRoot: '.', + target: 'ES5' }); + nodeTree = mergeTrees([nodeTree, typescriptTree, docs, packageJsons]); // TODO(iminar): tree differ seems to have issues with trees created by mergeTrees, investigate! diff --git a/tools/broccoli/typescript/index.ts b/tools/broccoli/typescript/index.ts deleted file mode 100644 index fb2579f09f..0000000000 --- a/tools/broccoli/typescript/index.ts +++ /dev/null @@ -1,55 +0,0 @@ -/// -/// -/// - -import fs = require('fs'); -import path = require('path'); -import ts = require('typescript'); -var walkSync = require('walk-sync'); -import Writer = require('broccoli-writer'); -var xtend = require('xtend'); - -class TSCompiler extends Writer { - constructor(private inputTree, private options: ts.CompilerOptions = {}) { super(); } - - write(readTree, destDir) { - var options: ts.CompilerOptions = xtend({outDir: destDir}, this.options); - if (this.options.outDir) { - options.outDir = path.resolve(destDir, options.outDir); - } - if (options.out) { - options.out = path.resolve(destDir, options.out); - } - options.target = (ts).ScriptTarget[options.target]; - return readTree(this.inputTree) - .then(srcDir => { - var files = walkSync(srcDir) - .filter(filepath => path.extname(filepath).toLowerCase() === '.ts') - .map(filepath => path.resolve(srcDir, filepath)); - - if (files.length > 0) { - var program = ts.createProgram(files, options); - var emitResult = program.emit(); - - var allDiagnostics = ts.getPreEmitDiagnostics(program).concat(emitResult.diagnostics); - - var errMsg = ''; - allDiagnostics.forEach(diagnostic => { - var message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'); - if (!diagnostic.file) { - errMsg += `\n${message}`; - return; - } - var {line, character} = - diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start); - errMsg += `\n${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`; - }); - - if (emitResult.emitSkipped) { - throw new Error(errMsg); - } - } - }); - } -} -module.exports = TSCompiler;