Skip to content

Commit f645cfa

Browse files
authored
Merge pull request #193 from typed-ember/extract-compiler-state
Pull out incremental-compiler state management
2 parents c7d4f92 + 30646b1 commit f645cfa

File tree

6 files changed

+160
-137
lines changed

6 files changed

+160
-137
lines changed

lib/commands/precompile.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ const execa = require('execa');
66
const fs = require('fs-extra');
77
const path = require('path');
88
const walkSync = require('walk-sync');
9-
const mkdirp = require('mkdirp');
109
const Command = require('ember-cli/lib/models/command'); // eslint-disable-line node/no-unpublished-require
1110

1211
const PRECOMPILE_MANIFEST = 'tmp/.ts-precompile-manifest';
@@ -37,7 +36,7 @@ module.exports = Command.extend({
3736
];
3837

3938
// Ensure the output directory is created even if no files are generated
40-
mkdirp.sync(outDir);
39+
fs.mkdirsSync(outDir);
4140

4241
return execa('tsc', flags).then(() => {
4342
let output = [];
@@ -54,7 +53,7 @@ module.exports = Command.extend({
5453
}
5554
}
5655

57-
mkdirp.sync(path.dirname(manifestPath));
56+
fs.mkdirsSync(path.dirname(manifestPath));
5857
fs.writeFileSync(manifestPath, JSON.stringify(output.reverse()));
5958
fs.remove(outDir);
6059
});
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
'use strict';
2+
3+
const debug = require('debug')('ember-cli-typescript:compiler-state');
4+
const RSVP = require('rsvp');
5+
6+
/*
7+
* This class captures the state of the Broccoli build and the TypeScript
8+
* compilation process in terms of our concurrent juggling of the two.
9+
* It maintains a `buildDeferred` object for the most recently started
10+
* TS compilation, and it accepts notifications about changes in the state
11+
* of the world framed as {broccoli,tsc}Did{Start,End} method calls.
12+
*/
13+
module.exports = class CompilerState {
14+
constructor() {
15+
this._buildingBroccoliNodes = 0;
16+
this.tscDidStart();
17+
}
18+
19+
broccoliDidStart() {
20+
this._buildingBroccoliNodes++;
21+
this._emitTransition('broccoliDidStart');
22+
}
23+
24+
broccoliDidEnd() {
25+
this._buildingBroccoliNodes--;
26+
this._emitTransition('broccoliDidEnd');
27+
}
28+
29+
tscDidStart() {
30+
if (this.buildDeferred) {
31+
this.buildDeferred.resolve();
32+
}
33+
34+
this.buildDeferred = RSVP.defer();
35+
this._tscBuilding = true;
36+
this._pendingErrors = [];
37+
this._emitTransition('tscDidStart');
38+
}
39+
40+
tscDidEnd() {
41+
if (this._pendingErrors.length) {
42+
this.buildDeferred.reject(new Error(this._pendingErrors.join('\n')));
43+
} else {
44+
this.buildDeferred.resolve();
45+
}
46+
47+
this._tscBuilding = false;
48+
this._emitTransition('tscDidEnd');
49+
}
50+
51+
didError(error) {
52+
this._pendingErrors.push(error);
53+
this._emitTransition('didError');
54+
}
55+
56+
_emitTransition(event) {
57+
debug(
58+
`%s | broccoli: %s active nodes | tsc: %s (%s errors)`,
59+
event,
60+
this._buildingBroccoliNodes,
61+
this._tscBuilding ? 'building' : 'idle',
62+
this._pendingErrors.length
63+
);
64+
}
65+
};
Lines changed: 25 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,17 @@
11
'use strict';
22

3-
const tmpdir = require('./utilities/tmpdir');
4-
const mkdirp = require('mkdirp');
3+
const tmpdir = require('../utilities/tmpdir');
54
const Funnel = require('broccoli-funnel');
65
const MergeTrees = require('broccoli-merge-trees');
7-
const symlinkOrCopy = require('symlink-or-copy');
8-
const Plugin = require('broccoli-plugin');
9-
const RSVP = require('rsvp');
106
const path = require('path');
11-
const fs = require('fs');
7+
const fs = require('fs-extra');
128
const resolve = require('resolve');
13-
const compile = require('./utilities/compile');
9+
const compile = require('../utilities/compile');
1410
const ts = require('typescript');
11+
const TypescriptOutput = require('./typescript-output-plugin');
12+
const CompilerState = require('./compiler-state');
1513

16-
const debugCompiler = require('debug')('ember-cli-typescript:compiler');
17-
const debugAutoresolve = require('debug')('ember-cli-typescript:autoresolve');
14+
const debugTsc = require('debug')('ember-cli-typescript:tsc');
1815

1916
module.exports = class IncrementalTypescriptCompiler {
2017
constructor(app, project) {
@@ -29,21 +26,12 @@ module.exports = class IncrementalTypescriptCompiler {
2926
this.app = app;
3027
this.project = project;
3128
this.addons = this._discoverAddons(project, []);
32-
this.maxBuildCount = 1;
33-
this.autoresolveThreshold = 250;
34-
35-
this._buildDeferred = RSVP.defer();
36-
this._isSynced = false;
37-
this._pendingErrors = [];
38-
this._triggerDir = `${this.outDir()}/.rebuild`;
39-
this._pendingAutoresolve = null;
40-
this._didAutoresolve = false;
29+
this.state = new CompilerState();
30+
4131
this._watchProgram = null;
4232
}
4333

4434
treeForHost() {
45-
let triggerTree = new Funnel(this._triggerDir, { destDir: 'app' });
46-
4735
let appTree = new TypescriptOutput(this, {
4836
[`${this._relativeAppRoot()}/app`]: 'app',
4937
});
@@ -53,7 +41,7 @@ module.exports = class IncrementalTypescriptCompiler {
5341
[mirage]: 'app/mirage',
5442
});
5543

56-
let tree = new MergeTrees([triggerTree, mirageTree, appTree].filter(Boolean), { overwrite: true });
44+
let tree = new MergeTrees([mirageTree, appTree].filter(Boolean), { overwrite: true });
5745
return new Funnel(tree, { srcDir: 'app' });
5846
}
5947

@@ -85,14 +73,14 @@ module.exports = class IncrementalTypescriptCompiler {
8573
}
8674

8775
buildPromise() {
88-
return this._buildDeferred.promise;
76+
return this.state.buildDeferred.promise;
8977
}
9078

9179
outDir() {
9280
if (!this._outDir) {
9381
let outDir = path.join(tmpdir(), `e-c-ts-${process.pid}`);
9482
this._outDir = outDir;
95-
mkdirp.sync(outDir);
83+
fs.mkdirsSync(outDir);
9684
}
9785

9886
return this._outDir;
@@ -104,47 +92,26 @@ module.exports = class IncrementalTypescriptCompiler {
10492
return;
10593
}
10694

107-
mkdirp.sync(this._triggerDir);
108-
this._touchRebuildTrigger();
109-
11095
let project = this.project;
11196
let outDir = this.outDir();
11297

11398
this._watchProgram = compile(project, { outDir, watch: true }, {
99+
watchedFileChanged: () => this.state.tscDidStart(),
100+
114101
reportWatchStatus: (diagnostic) => {
115102
let text = diagnostic.messageText;
116-
117-
if (text.indexOf('Starting incremental compilation') !== -1) {
118-
debugCompiler('tsc detected a file change');
119-
this.willRebuild();
120-
clearTimeout(this._pendingAutoresolve);
121-
}
103+
debugTsc(text);
122104

123105
if (text.indexOf('Compilation complete') !== -1) {
124-
debugCompiler('rebuild completed');
125-
126-
this.didSync();
127-
128-
if (this._didAutoresolve) {
129-
this._touchRebuildTrigger();
130-
this.maxBuildCount++;
131-
}
132-
133-
clearTimeout(this._pendingAutoresolve);
134-
this._didAutoresolve = false;
106+
this.state.tscDidEnd();
135107
}
136108
},
137109

138110
reportDiagnostic: (diagnostic) => {
139111
if (diagnostic.category !== 2) {
140-
let message = ts.formatDiagnostic(diagnostic, {
141-
getCanonicalFileName: path => path,
142-
getCurrentDirectory: ts.sys.getCurrentDirectory,
143-
getNewLine: () => ts.sys.newLine,
144-
});
145-
112+
let message = this._formatDiagnosticMessage(diagnostic);
146113
if (this._shouldFailOnTypeError()) {
147-
this.didError(message);
114+
this.state.didError(message);
148115
} else {
149116
this.project.ui.write(message);
150117
}
@@ -153,42 +120,18 @@ module.exports = class IncrementalTypescriptCompiler {
153120
});
154121
}
155122

156-
willRebuild() {
157-
if (this._isSynced) {
158-
this._isSynced = false;
159-
this._buildDeferred = RSVP.defer();
160-
161-
// Schedule a timer to automatically resolve if tsc doesn't pick up any file changes in a
162-
// short period. This may happen if a non-TS file changed, or if the tsc watcher is
163-
// drastically behind watchman. If the latter happens, we'll explicitly touch a file in the
164-
// broccoli output in order to ensure the changes are picked up.
165-
this._pendingAutoresolve = setTimeout(() => {
166-
debugAutoresolve('no tsc rebuild; autoresolving...');
167-
168-
this.didSync();
169-
this._didAutoresolve = true;
170-
}, this.autoresolveThreshold);
171-
}
172-
}
173-
174-
didError(message) {
175-
this._pendingErrors.push(message);
176-
}
177-
178-
didSync() {
179-
this._isSynced = true;
180-
if (this._pendingErrors.length) {
181-
this._buildDeferred.reject(new Error(this._pendingErrors.join('\n')));
182-
this._pendingErrors = [];
183-
} else {
184-
this._buildDeferred.resolve();
185-
}
186-
}
187-
188123
getProgram() {
189124
return this._watchProgram.getProgram();
190125
}
191126

127+
_formatDiagnosticMessage(diagnostic) {
128+
return ts.formatDiagnostic(diagnostic, {
129+
getCanonicalFileName: path => path,
130+
getCurrentDirectory: ts.sys.getCurrentDirectory,
131+
getNewLine: () => ts.sys.newLine,
132+
});
133+
}
134+
192135
_shouldFailOnTypeError() {
193136
let options = this.getProgram().getCompilerOptions();
194137
return !!options.noEmitOnError;
@@ -219,11 +162,6 @@ module.exports = class IncrementalTypescriptCompiler {
219162
}
220163
}
221164

222-
_touchRebuildTrigger() {
223-
debugAutoresolve('touching rebuild trigger.');
224-
fs.writeFileSync(`${this._triggerDir}/tsc-delayed-rebuild`, '', 'utf-8');
225-
}
226-
227165
_discoverAddons(node, addons) {
228166
for (let addon of node.addons) {
229167
let devDeps = addon.pkg.devDependencies || {};
@@ -261,43 +199,3 @@ module.exports = class IncrementalTypescriptCompiler {
261199
}
262200
};
263201

264-
class TypescriptOutput extends Plugin {
265-
constructor(compiler, paths) {
266-
super([]);
267-
this.compiler = compiler;
268-
this.paths = paths;
269-
this.buildCount = 0;
270-
}
271-
272-
build() {
273-
this.buildCount++;
274-
275-
// We use this to keep track of the build state between the various
276-
// Broccoli trees and tsc; when either tsc or broccoli notices a file
277-
// change, we immediately invalidate the previous build output.
278-
if (this.buildCount > this.compiler.maxBuildCount) {
279-
debugCompiler('broccoli detected a file change');
280-
this.compiler.maxBuildCount = this.buildCount;
281-
this.compiler.willRebuild();
282-
}
283-
284-
debugCompiler('waiting for tsc output', this.paths);
285-
return this.compiler.buildPromise().then(() => {
286-
debugCompiler('tsc build complete', this.paths);
287-
for (let relativeSrc of Object.keys(this.paths)) {
288-
let src = `${this.compiler.outDir()}/${relativeSrc}`;
289-
let dest = `${this.outputPath}/${this.paths[relativeSrc]}`;
290-
if (fs.existsSync(src)) {
291-
let dir = path.dirname(dest);
292-
if (dir !== '.') {
293-
mkdirp.sync(dir);
294-
}
295-
296-
symlinkOrCopy.sync(src, dest);
297-
} else {
298-
mkdirp.sync(dest);
299-
}
300-
}
301-
});
302-
}
303-
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
'use strict';
2+
3+
const Plugin = require('broccoli-plugin');
4+
const fs = require('fs-extra');
5+
const path = require('path');
6+
const symlinkOrCopy = require('symlink-or-copy');
7+
8+
/*
9+
* We don't insert the output of the TypeScript compiler directly into
10+
* the broccoli pipeline, as that would cause double rebuilds (once for
11+
* the change to the original .ts file, then another for the change to
12+
* the resulting .js file). Instead, we use this Broccoli plugin to
13+
* essentially provide a lightweight 'view' into the compiler output,
14+
* blocking the rebuild triggered by the original .ts file until the
15+
* corresponding .js file is ready.
16+
*/
17+
module.exports = class TypescriptOutput extends Plugin {
18+
constructor(compiler, paths) {
19+
super([]);
20+
this.compiler = compiler;
21+
this.paths = paths;
22+
}
23+
24+
build() {
25+
this.compiler.state.broccoliDidStart();
26+
27+
return this.compiler
28+
.buildPromise()
29+
.then(() => {
30+
for (let relativeSrc of Object.keys(this.paths)) {
31+
let src = `${this.compiler.outDir()}/${relativeSrc}`;
32+
let dest = `${this.outputPath}/${this.paths[relativeSrc]}`;
33+
if (fs.existsSync(src)) {
34+
let dir = path.dirname(dest);
35+
if (dir !== '.') {
36+
fs.mkdirsSync(dir);
37+
}
38+
39+
symlinkOrCopy.sync(src, dest);
40+
} else {
41+
fs.mkdirsSync(dest);
42+
}
43+
}
44+
})
45+
.finally(() => this.compiler.state.broccoliDidEnd());
46+
}
47+
};

0 commit comments

Comments
 (0)