Skip to content

Commit 1d00784

Browse files
committed
build(broccoli): refactor typescript plugin to be incremental via DiffingBroccoliPlugin
1 parent 9d1df21 commit 1d00784

File tree

6 files changed

+214
-80
lines changed

6 files changed

+214
-80
lines changed

gulpfile.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,10 @@ gulp.task('build/format.dart', rundartpackage(gulp, gulpPlugins, {
146146

147147
function doCheckFormat() {
148148
return gulp.src(['Brocfile*.js', 'modules/**/*.ts', 'tools/**/*.ts', '!**/typings/**/*.d.ts',
149-
'!tools/broccoli/tree-differ.ts']) // See https://github.com/angular/clang-format/issues/4
149+
// skipped due to https://github.com/angular/clang-format/issues/4
150+
'!tools/broccoli/tree-differ.ts',
151+
// skipped due to https://github.com/angular/gulp-clang-format/issues/3
152+
'!tools/broccoli/broccoli-typescript.ts' ])
150153
.pipe(format.checkFormat('file'));
151154
}
152155

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
/// <reference path="../typings/node/node.d.ts" />
2+
/// <reference path="../../node_modules/typescript/bin/typescript.d.ts" />
3+
4+
import fs = require('fs');
5+
import fse = require('fs-extra');
6+
import path = require('path');
7+
import ts = require('typescript');
8+
import {wrapDiffingPlugin, DiffingBroccoliPlugin, DiffResult} from './diffing-broccoli-plugin';
9+
10+
11+
type FileRegistry = ts.Map<{version: number}>;
12+
13+
const FS_OPTS = {encoding: 'utf-8'};
14+
15+
16+
/**
17+
* Broccoli plugin that implements incremental Typescript compiler.
18+
*
19+
* It instantiates a typescript compiler instance that keeps all the state about the project and
20+
* can reemit only the files that actually changed.
21+
*
22+
* Limitations: only files that map directly to the changed source file via naming conventions are
23+
* reemited. This primarily affects code that uses `const enum`s, because changing the enum value
24+
* requires global emit, which can affect many files.
25+
*/
26+
class DiffingTSCompiler implements DiffingBroccoliPlugin {
27+
private tsOpts: ts.CompilerOptions;
28+
private fileRegistry: FileRegistry = Object.create(null);
29+
private rootFilePaths: string[];
30+
private tsServiceHost: ts.LanguageServiceHost;
31+
private tsService: ts.LanguageService;
32+
33+
34+
constructor(public inputPath: string, public cachePath: string, public options) {
35+
this.tsOpts = Object.create(options);
36+
this.tsOpts.outDir = this.cachePath;
37+
this.tsOpts.target = (<any>ts).ScriptTarget[options.target];
38+
this.rootFilePaths = options.rootFilePaths ? options.rootFilePaths.splice(0) : [];
39+
this.tsServiceHost = new CustomLanguageServiceHost(this.tsOpts, this.rootFilePaths,
40+
this.fileRegistry, this.inputPath);
41+
this.tsService = ts.createLanguageService(this.tsServiceHost, ts.createDocumentRegistry())
42+
}
43+
44+
45+
rebuild(treeDiff: DiffResult) {
46+
let pathsToEmit = [];
47+
let pathsWithErrors = [];
48+
49+
treeDiff.changedPaths.filter((changedPath) =>
50+
changedPath.match(/\.ts/) && !changedPath.match(/\.d\.ts/))
51+
.forEach((tsFilePath) => {
52+
if (!this.fileRegistry[tsFilePath]) {
53+
this.fileRegistry[tsFilePath] = {version: 0};
54+
this.rootFilePaths.push(tsFilePath);
55+
} else {
56+
this.fileRegistry[tsFilePath].version++;
57+
}
58+
59+
pathsToEmit.push(tsFilePath);
60+
});
61+
62+
treeDiff.removedPaths.filter((changedPath) =>
63+
changedPath.match(/\.ts/) && !changedPath.match(/\.d\.ts/))
64+
.forEach((tsFilePath) => {
65+
console.log('removing outputs for', tsFilePath);
66+
67+
this.rootFilePaths.splice(this.rootFilePaths.indexOf(tsFilePath), 1);
68+
this.fileRegistry[tsFilePath] = null;
69+
70+
let jsFilePath = tsFilePath.replace(/\.ts$/, '.js');
71+
let mapFilePath = tsFilePath.replace(/.ts$/, '.js.map');
72+
let dtsFilePath = tsFilePath.replace(/\.ts$/, '.d.ts');
73+
74+
fs.unlinkSync(path.join(this.cachePath, jsFilePath));
75+
fs.unlinkSync(path.join(this.cachePath, mapFilePath));
76+
fs.unlinkSync(path.join(this.cachePath, dtsFilePath));
77+
});
78+
79+
pathsToEmit.forEach((tsFilePath) => {
80+
let output = this.tsService.getEmitOutput(tsFilePath);
81+
82+
if (output.emitSkipped) {
83+
let errorFound = this.logError(tsFilePath);
84+
if (errorFound) {
85+
pathsWithErrors.push(tsFilePath);
86+
}
87+
} else {
88+
output.outputFiles.forEach(o => {
89+
let destDirPath = path.dirname(o.name);
90+
fse.mkdirsSync(destDirPath);
91+
fs.writeFileSync(o.name, o.text, FS_OPTS);
92+
});
93+
}
94+
});
95+
96+
if (pathsWithErrors.length) {
97+
throw new Error('Typescript found errors listed above...');
98+
}
99+
}
100+
101+
102+
private logError(tsFilePath) {
103+
let allDiagnostics = this.tsService.getCompilerOptionsDiagnostics()
104+
.concat(this.tsService.getSyntacticDiagnostics(tsFilePath))
105+
.concat(this.tsService.getSemanticDiagnostics(tsFilePath));
106+
107+
allDiagnostics.forEach(diagnostic => {
108+
let message = ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
109+
if (diagnostic.file) {
110+
let{line, character} = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
111+
console.log(` Error ${diagnostic.file.fileName} (${line + 1},${character + 1}): ` +
112+
`${message}`);
113+
} else {
114+
console.log(` Error: ${message}`);
115+
}
116+
});
117+
118+
return !!allDiagnostics.length;
119+
}
120+
}
121+
122+
123+
class CustomLanguageServiceHost implements ts.LanguageServiceHost {
124+
private currentDirectory: string;
125+
private defaultLibFilePath: string;
126+
127+
128+
constructor(private compilerOptions: ts.CompilerOptions, private fileNames: string[],
129+
private fileRegistry: FileRegistry, private treeInputPath: string) {
130+
this.currentDirectory = process.cwd();
131+
this.defaultLibFilePath = ts.getDefaultLibFilePath(compilerOptions);
132+
}
133+
134+
135+
getScriptFileNames(): string[] { return this.fileNames; }
136+
137+
138+
getScriptVersion(fileName: string): string {
139+
return this.fileRegistry[fileName] && this.fileRegistry[fileName].version.toString();
140+
}
141+
142+
143+
getScriptSnapshot(tsFilePath: string): ts.IScriptSnapshot {
144+
// TODO: this method is called a lot, add cache
145+
146+
let absoluteTsFilePath = (tsFilePath == this.defaultLibFilePath) ?
147+
tsFilePath :
148+
path.join(this.treeInputPath, tsFilePath);
149+
150+
if (!fs.existsSync(absoluteTsFilePath)) {
151+
// TypeScript seems to request lots of bogus paths during import path lookup and resolution,
152+
// so we we just return undefined when the path is not correct.
153+
return undefined;
154+
}
155+
return ts.ScriptSnapshot.fromString(fs.readFileSync(absoluteTsFilePath, FS_OPTS));
156+
}
157+
158+
159+
getCurrentDirectory(): string { return this.currentDirectory; }
160+
161+
162+
getCompilationSettings(): ts.CompilerOptions { return this.compilerOptions; }
163+
164+
165+
getDefaultLibFileName(options: ts.CompilerOptions): string {
166+
// ignore options argument, options should not change during the lifetime of the plugin
167+
return this.defaultLibFilePath;
168+
}
169+
}
170+
171+
172+
export default wrapDiffingPlugin(DiffingTSCompiler);

tools/broccoli/diffing-broccoli-plugin.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -62,19 +62,24 @@ class DiffingPluginWrapper implements BroccoliTree {
6262

6363

6464
rebuild() {
65-
let firstRun = !this.initialized;
66-
this.init();
65+
try {
66+
let firstRun = !this.initialized;
67+
this.init();
6768

68-
let diffResult = this.treeDiffer.diffTree();
69-
diffResult.log(!firstRun);
69+
let diffResult = this.treeDiffer.diffTree();
70+
diffResult.log(!firstRun);
7071

71-
var rebuildPromise = this.wrappedPlugin.rebuild(diffResult);
72+
var rebuildPromise = this.wrappedPlugin.rebuild(diffResult);
7273

73-
if (rebuildPromise) {
74-
return (<Promise<any>>rebuildPromise).then(this.relinkOutputAndCachePaths.bind(this));
75-
}
74+
if (rebuildPromise) {
75+
return (<Promise<any>>rebuildPromise).then(this.relinkOutputAndCachePaths.bind(this));
76+
}
7677

77-
this.relinkOutputAndCachePaths();
78+
this.relinkOutputAndCachePaths();
79+
} catch (e) {
80+
e.message = `[${this.description}]: ${e.message}`;
81+
throw e;
82+
}
7883
}
7984

8085

tools/broccoli/trees/browser_tree.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ var path = require('path');
88
var replace = require('broccoli-replace');
99
var stew = require('broccoli-stew');
1010
var ts2dart = require('../broccoli-ts2dart');
11-
var TypescriptCompiler = require('../typescript');
1211

12+
import compileWithTypescript from '../broccoli-typescript';
1313
import destCopy from '../broccoli-dest-copy';
1414
import {default as transpileWithTraceur, TRACEUR_RUNTIME_PATH} from '../traceur/index';
1515

@@ -42,20 +42,26 @@ module.exports = function makeBrowserTree(options, destinationPath) {
4242
// Use TypeScript to transpile the *.ts files to ES6
4343
// We don't care about errors: we let the TypeScript compilation to ES5
4444
// in node_tree.ts do the type-checking.
45-
var typescriptTree = new TypescriptCompiler(modulesTree, {
46-
target: 'ES6',
47-
sourceMap: true,
48-
mapRoot: '', /* force sourcemaps to use relative path */
45+
var typescriptTree = compileWithTypescript(modulesTree, {
4946
allowNonTsExtensions: false,
50-
typescript: require('typescript'),
51-
noEmitOnError: false,
47+
declaration: true,
5248
emitDecoratorMetadata: true,
53-
outDir: 'angular2'
49+
mapRoot: '', // force sourcemaps to use relative path
50+
noEmitOnError: false, // temporarily ignore errors, we type-check only via cjs build
51+
rootDir: '.',
52+
sourceMap: true,
53+
sourceRoot: '.',
54+
target: 'ES6'
5455
});
5556
typescriptTree = stew.rename(typescriptTree, '.js', '.es6');
5657

5758
var es6Tree = mergeTrees([traceurTree, typescriptTree]);
5859

60+
// TODO(iminar): tree differ seems to have issues with trees created by mergeTrees, investigate!
61+
// ENOENT error is thrown while doing fs.readdirSync on inputRoot
62+
// in the meantime, we just do noop mv to create a new tree
63+
es6Tree = stew.mv(es6Tree, '');
64+
5965
// Call Traceur again to lower the ES6 build tree to ES5
6066
var es5Tree = transpileWithTraceur(es6Tree, {
6167
destExtension: '.js',

tools/broccoli/trees/node_tree.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ var replace = require('broccoli-replace');
99
var stew = require('broccoli-stew');
1010
var ts2dart = require('../broccoli-ts2dart');
1111
import transpileWithTraceur from '../traceur/index';
12-
var TypescriptCompiler = require('../typescript');
12+
import compileWithTypescript from '../broccoli-typescript';
1313

1414
var projectRootDir = path.normalize(path.join(__dirname, '..', '..', '..', '..'));
1515

@@ -98,17 +98,20 @@ module.exports = function makeNodeTree(destinationPath) {
9898
packageJsons, {files: ["**/**"], context: {'packageJson': COMMON_PACKAGE_JSON}});
9999

100100

101-
var typescriptTree = new TypescriptCompiler(modulesTree, {
102-
target: 'ES5',
103-
sourceMap: true,
101+
var typescriptTree = compileWithTypescript(modulesTree, {
102+
allowNonTsExtensions: false,
103+
declaration: true,
104104
mapRoot: '', /* force sourcemaps to use relative path */
105105
module: 'commonjs',
106-
allowNonTsExtensions: false,
107-
typescript: require('typescript'),
108106
noEmitOnError: true,
109-
outDir: 'angular2'
107+
rootDir: '.',
108+
rootFilePaths: ['angular2/traceur-runtime.d.ts'],
109+
sourceMap: true,
110+
sourceRoot: '.',
111+
target: 'ES5'
110112
});
111113

114+
112115
nodeTree = mergeTrees([nodeTree, typescriptTree, docs, packageJsons]);
113116

114117
// TODO(iminar): tree differ seems to have issues with trees created by mergeTrees, investigate!

tools/broccoli/typescript/index.ts

Lines changed: 0 additions & 55 deletions
This file was deleted.

0 commit comments

Comments
 (0)