Skip to content

Commit bee06ce

Browse files
authored
Merge pull request #188 from typed-ember/fail-the-build-but-on-purpose
Fail the build when `noEmitOnError` is set and there's a type error
2 parents 8ac2547 + bad675a commit bee06ce

File tree

4 files changed

+68
-13
lines changed

4 files changed

+68
-13
lines changed

README.md

+23-5
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ Support for (mostly in the form of documentation!) for the changes in Ember 3.1
7676

7777
### `tsconfig.json`
7878

79-
We generate a good default [`tsconfig.json`][blueprint], which will usually make everything _Just Work_. In general, you may customize your TypeScript build process as usual using the `tsconfig.json` file.
79+
We generate a good default [`tsconfig.json`][blueprint], which will usually make everything _Just Work™_. In general, you may customize your TypeScript build process as usual using the `tsconfig.json` file.
8080

8181
However, there are a few things worth noting if you're already familiar with TypeScript and looking to make further or more advanced customizations (i.e. _most_ users can just ignore this section!):
8282

@@ -116,13 +116,31 @@ In addition to the points made below, you may find the [Typing Your Ember][typin
116116

117117
### Incremental adoption
118118

119-
If you are porting an existing app to TypeScript, you can install this addon and migrate your files incrementally by changing their extensions from `.js` to `.ts`. As TypeScript starts to find errors (and it usually does!), make sure to celebrate your (small) wins with your team, especially if some people are not convinced yet. We would also love to hear your stories!
119+
If you are porting an existing app to TypeScript, you can install this addon and migrate your files incrementally by changing their extensions from `.js` to `.ts`. As TypeScript starts to find errors (and it usually does!), make sure to celebrate your wins – even if they're small! – with your team, especially if some people are not convinced yet. We would also love to hear your stories!
120120

121121
Some specific tips for success on the technical front:
122122

123-
* Start with the _least_ strict "strictness" settings (which is what the default compiler options are set to) to begin, and only tighten them down iteratively.
124-
* Make liberal use of `any` for things you don't have types for as you go, and come back and fill them in later.
125-
* A good approach is to start at your "leaf" files (the ones that don't import anything else from your app, only Ember types) and then work your way back inward toward the most core types that are used everywhere.
123+
* Use the *strictest* strictness settings that our typings allow. While it may be tempting to start with the *loosest* strictness settings and then to tighten them down as you go, this will actually mean that "getting your app type-checking" will become a repeated process – getting it type-checking with every new strictness setting you enable! – rather than something you do just once. The only strictness setting you should turn *off* is `strictFunctionTypes`, which our current type definitions do not support. The recommended *strictness* settings in your `"compilerOptions"` hash:
124+
125+
```
126+
"noImplicitAny": true,
127+
"noImplicitThis": true,
128+
"alwaysStrict": true,
129+
"strictNullChecks": true,
130+
"strictPropertyInitialization": true,
131+
"noFallthroughCasesInSwitch": true,
132+
"noUnusedLocals": true,
133+
"noUnusedParameters": true,
134+
"noImplicitReturns": true,
135+
```
136+
137+
* Make liberal use of `any` for things you don't have types for as you go, and come back and fill them in later. This will let you do the strictest strictness settings but with an escape hatch that lets you say "We will come back to this when we have more idea how to handle it."
138+
139+
* A good approach is to start at your "leaf" files (the ones that don't import anything else from your app, only Ember types) and then work your way back inward toward the most core types that are used everywhere. Often the highest-value modules are your Ember Data models and any core services that are used everywhere else in the app – and those are also the ones that tend to have the most cascading effects (having to update *tons* of other places in your app) when you type them later in the process.
140+
141+
* Set `"noEmitOnError": true` in the `"compilerOptions"` hash in your `tsconfig.json` – it will help a lot if you can be sure that for the parts of your app you *have* added types to are still correct. And you'll get nice feedback *immediately* when you have type errors that way!
142+
143+
![type errors in your build!](https://user-images.githubusercontent.com/108688/38774630-7d9224d4-403b-11e8-8dbc-87dad977a4c4.gif "example of a build error during live reload")
126144
127145
You may find the blog series ["Typing Your Ember"][typing-your-ember] helpful as it walks you through not only how to install but how to most effectively use TypeScript in an Ember app today, and gives some important info on the background and roadmap for the project.
128146

appveyor.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ install:
2121
- yarn
2222

2323
cache:
24-
# - "%LOCALAPPDATA%\\Yarn"
24+
- "%LOCALAPPDATA%\\Yarn"
2525

2626
# Post-install test scripts.
2727
test_script:

lib/incremental-typescript-compiler.js

+30-4
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,11 @@ module.exports = class IncrementalTypescriptCompiler {
3434

3535
this._buildDeferred = RSVP.defer();
3636
this._isSynced = false;
37+
this._pendingErrors = [];
3738
this._triggerDir = `${this.outDir()}/.rebuild`;
3839
this._pendingAutoresolve = null;
3940
this._didAutoresolve = false;
41+
this._watchProgram = null;
4042
}
4143

4244
treeForHost() {
@@ -108,7 +110,7 @@ module.exports = class IncrementalTypescriptCompiler {
108110
let project = this.project;
109111
let outDir = this.outDir();
110112

111-
compile(project, { outDir, watch: true }, {
113+
this._watchProgram = compile(project, { outDir, watch: true }, {
112114
reportWatchStatus: (diagnostic) => {
113115
let text = diagnostic.messageText;
114116

@@ -135,11 +137,17 @@ module.exports = class IncrementalTypescriptCompiler {
135137

136138
reportDiagnostic: (diagnostic) => {
137139
if (diagnostic.category !== 2) {
138-
this.project.ui.write(ts.formatDiagnostic(diagnostic, {
140+
let message = ts.formatDiagnostic(diagnostic, {
139141
getCanonicalFileName: path => path,
140142
getCurrentDirectory: ts.sys.getCurrentDirectory,
141143
getNewLine: () => ts.sys.newLine,
142-
}));
144+
});
145+
146+
if (this._shouldFailOnTypeError()) {
147+
this.didError(message);
148+
} else {
149+
this.project.ui.write(message);
150+
}
143151
}
144152
}
145153
});
@@ -163,9 +171,27 @@ module.exports = class IncrementalTypescriptCompiler {
163171
}
164172
}
165173

174+
didError(message) {
175+
this._pendingErrors.push(message);
176+
}
177+
166178
didSync() {
167179
this._isSynced = true;
168-
this._buildDeferred.resolve();
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+
188+
getProgram() {
189+
return this._watchProgram.getProgram();
190+
}
191+
192+
_shouldFailOnTypeError() {
193+
let options = this.getProgram().getCompilerOptions();
194+
return !!options.noEmitOnError;
169195
}
170196

171197
_mirageDirectory() {

lib/utilities/compile.js

+14-3
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,24 @@ module.exports = function compile(project, tsOptions, callbacks) {
2323
fullOptions,
2424
buildWatchHooks(ts.sys),
2525
createProgram,
26-
callbacks.reportDiagnostic,
27-
callbacks.reportWatchStatus
26+
diagnosticCallback(callbacks.reportDiagnostic),
27+
diagnosticCallback(callbacks.reportWatchStatus)
2828
);
2929

3030
return ts.createWatchProgram(host);
3131
};
3232

33+
function diagnosticCallback(callback) {
34+
if (callback) {
35+
// The initial callbacks may be synchronously invoked during instantiation of the
36+
// WatchProgram, which is annoying if those callbacks want to _reference_ it, so
37+
// we always force invocation to be asynchronous for consistency.
38+
return (diagnostic) => {
39+
process.nextTick(() => callback(diagnostic));
40+
};
41+
}
42+
}
43+
3344
function buildWatchHooks(sys) {
3445
let watchedFiles = new Map();
3546

@@ -48,7 +59,7 @@ function buildWatchHooks(sys) {
4859
if (!fs.existsSync(dir)) return;
4960

5061
let ignored = /\/(\..*?|dist|node_modules|tmp)\//;
51-
let watcher = chokidar.watch(dir, { ignored });
62+
let watcher = chokidar.watch(dir, { ignored, ignoreInitial: true });
5263

5364
watcher.on('all', (type, path) => {
5465
path = path.replace(/\\/g, '/'); // Normalize Windows

0 commit comments

Comments
 (0)