Skip to content

Commit e04a0d6

Browse files
filipesilvaBrocco
authored andcommitted
fix(@ngtools/webpack): always emit on first build
We want to allow emitting with errors on the first run so that imports can be added to the webpack dependency tree and rebuilds triggered by file edits. This will not cause webpack to finish the compilation successfully on error, it will just allow it to follow imports. Fix #7890
1 parent 7d0ccb3 commit e04a0d6

File tree

4 files changed

+67
-33
lines changed

4 files changed

+67
-33
lines changed

packages/@ngtools/webpack/src/angular_compiler_plugin.ts

+15-11
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,6 @@ export class AngularCompilerPlugin implements Tapable {
101101
private _donePromise: Promise<void> | null;
102102
private _compiler: any = null;
103103
private _compilation: any = null;
104-
private _failedCompilation = false;
105104

106105
// TypeChecker process.
107106
private _forkTypeChecker = true;
@@ -115,7 +114,6 @@ export class AngularCompilerPlugin implements Tapable {
115114

116115
get options() { return this._options; }
117116
get done() { return this._donePromise; }
118-
get failedCompilation() { return this._failedCompilation; }
119117
get entryModule() {
120118
const splitted = this._entryModule.split('#');
121119
const path = splitted[0];
@@ -327,13 +325,19 @@ export class AngularCompilerPlugin implements Tapable {
327325
this._updateForkedTypeChecker(changedTsFiles);
328326
}
329327

330-
if (this._JitMode) {
328+
// We want to allow emitting with errors on the first run so that imports can be added
329+
// to the webpack dependency tree and rebuilds triggered by file edits.
330+
const compilerOptions = {
331+
...this._angularCompilerOptions,
332+
noEmitOnError: !this._firstRun
333+
};
331334

335+
if (this._JitMode) {
332336
// Create the TypeScript program.
333337
time('AngularCompilerPlugin._createOrUpdateProgram.ts.createProgram');
334338
this._program = ts.createProgram(
335339
this._tsFilenames,
336-
this._angularCompilerOptions,
340+
compilerOptions,
337341
this._angularCompilerHost,
338342
this._program as ts.Program
339343
);
@@ -345,7 +349,7 @@ export class AngularCompilerPlugin implements Tapable {
345349
// Create the Angular program.
346350
this._program = createProgram({
347351
rootNames: this._tsFilenames,
348-
options: this._angularCompilerOptions,
352+
options: compilerOptions,
349353
host: this._angularCompilerHost,
350354
oldProgram: this._program as Program
351355
});
@@ -543,7 +547,6 @@ export class AngularCompilerPlugin implements Tapable {
543547
compiler.plugin('done', () => {
544548
this._donePromise = null;
545549
this._compilation = null;
546-
this._failedCompilation = false;
547550
});
548551

549552
// TODO: consider if it's better to remove this plugin and instead make it wait on the
@@ -618,7 +621,6 @@ export class AngularCompilerPlugin implements Tapable {
618621
timeEnd('AngularCompilerPlugin._make');
619622
cb();
620623
}, (err: any) => {
621-
this._failedCompilation = true;
622624
compilation.errors.push(err.stack);
623625
timeEnd('AngularCompilerPlugin._make');
624626
cb();
@@ -740,8 +742,6 @@ export class AngularCompilerPlugin implements Tapable {
740742
// Reset changed files on successful compilation.
741743
if (emitResult && !emitResult.emitSkipped && this._compilation.errors.length === 0) {
742744
this._compilerHost.resetChangedFileTracker();
743-
} else {
744-
this._failedCompilation = true;
745745
}
746746
}
747747
timeEnd('AngularCompilerPlugin._update');
@@ -803,7 +803,9 @@ export class AngularCompilerPlugin implements Tapable {
803803
'AngularCompilerPlugin._emit.ts'));
804804
}
805805

806-
if (!hasErrors(allDiagnostics)) {
806+
// Always emit on the first run, so that imports are processed by webpack and the user
807+
// can trigger a rebuild by editing any file.
808+
if (!hasErrors(allDiagnostics) || this._firstRun) {
807809
sourceFiles.forEach((sf) => {
808810
const timeLabel = `AngularCompilerPlugin._emit.ts+${sf.fileName}+.emit`;
809811
time(timeLabel);
@@ -834,7 +836,9 @@ export class AngularCompilerPlugin implements Tapable {
834836
'AngularCompilerPlugin._emit.ng'));
835837
}
836838

837-
if (!hasErrors(allDiagnostics)) {
839+
// Always emit on the first run, so that imports are processed by webpack and the user
840+
// can trigger a rebuild by editing any file.
841+
if (!hasErrors(allDiagnostics) || this._firstRun) {
838842
time('AngularCompilerPlugin._emit.ng.emit');
839843
const extractI18n = !!this._angularCompilerOptions.i18nOutFile;
840844
const emitFlags = extractI18n ? EmitFlags.I18nBundle : EmitFlags.Default;

packages/@ngtools/webpack/src/loader.ts

+9-16
Original file line numberDiff line numberDiff line change
@@ -557,15 +557,11 @@ export function ngcLoader(this: LoaderContext & { _compilation: any }, source: s
557557
result.sourceMap = JSON.stringify(sourceMap);
558558
}
559559

560-
if (plugin.failedCompilation) {
561-
// Return an empty string if there is no result to prevent extra loader errors.
562-
// Plugin errors were already pushed to the compilation errors.
563-
timeEnd(timeLabel);
564-
cb(null, result.outputText || '', result.sourceMap);
565-
} else {
566-
timeEnd(timeLabel);
567-
cb(null, result.outputText, result.sourceMap);
560+
timeEnd(timeLabel);
561+
if (result.outputText === undefined) {
562+
throw new Error('TypeScript compilation failed.');
568563
}
564+
cb(null, result.outputText, result.sourceMap);
569565
})
570566
.catch(err => {
571567
timeEnd(timeLabel + '.ngcLoader.AngularCompilerPlugin');
@@ -658,15 +654,12 @@ export function ngcLoader(this: LoaderContext & { _compilation: any }, source: s
658654
timeEnd(timeLabel + '.ngcLoader.AotPlugin.transpile');
659655

660656
timeEnd(timeLabel + '.ngcLoader.AotPlugin');
661-
if (plugin.failedCompilation && plugin.compilerOptions.noEmitOnError) {
662-
// Return an empty string to prevent extra loader errors (missing imports etc).
663-
// Plugin errors were already pushed to the compilation errors.
664-
timeEnd(timeLabel);
665-
cb(null, '');
666-
} else {
667-
timeEnd(timeLabel);
668-
cb(null, result.outputText, result.sourceMap);
657+
timeEnd(timeLabel);
658+
659+
if (result.outputText === undefined) {
660+
throw new Error('TypeScript compilation failed.');
669661
}
662+
cb(null, result.outputText, result.sourceMap);
670663
})
671664
.catch(err => cb(err));
672665
}

packages/@ngtools/webpack/src/plugin.ts

-6
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@ export class AotPlugin implements Tapable {
6262
private _donePromise: Promise<void> | null;
6363
private _compiler: any = null;
6464
private _compilation: any = null;
65-
private _failedCompilation = false;
6665

6766
private _typeCheck = true;
6867
private _skipCodeGeneration = false;
@@ -90,7 +89,6 @@ export class AotPlugin implements Tapable {
9089
get compilerHost() { return this._compilerHost; }
9190
get compilerOptions() { return this._compilerOptions; }
9291
get done() { return this._donePromise; }
93-
get failedCompilation() { return this._failedCompilation; }
9492
get entryModule() {
9593
const splitted = this._entryModule.split('#');
9694
const path = splitted[0];
@@ -428,7 +426,6 @@ export class AotPlugin implements Tapable {
428426
compiler.plugin('done', () => {
429427
this._donePromise = null;
430428
this._compilation = null;
431-
this._failedCompilation = false;
432429
});
433430

434431
compiler.plugin('after-resolvers', (compiler: any) => {
@@ -640,14 +637,11 @@ export class AotPlugin implements Tapable {
640637
.then(() => {
641638
if (this._compilation.errors == 0) {
642639
this._compilerHost.resetChangedFileTracker();
643-
} else {
644-
this._failedCompilation = true;
645640
}
646641

647642
timeEnd('AotPlugin._make');
648643
cb();
649644
}, (err: any) => {
650-
this._failedCompilation = true;
651645
compilation.errors.push(err.stack);
652646
timeEnd('AotPlugin._make');
653647
cb();
+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import {
2+
killAllProcesses,
3+
waitForAnyProcessOutputToMatch,
4+
execAndWaitForOutputToMatch,
5+
} from '../../utils/process';
6+
import {replaceInFile, appendToFile} from '../../utils/fs';
7+
import {getGlobalVariable} from '../../utils/env';
8+
9+
10+
const failedRe = /webpack: Failed to compile/;
11+
const successRe = /webpack: Compiled successfully/;
12+
const errorRe = /ERROR in/;
13+
14+
export default function() {
15+
if (process.platform.startsWith('win')) {
16+
return Promise.resolve();
17+
}
18+
// Skip this in ejected tests.
19+
if (getGlobalVariable('argv').eject) {
20+
return Promise.resolve();
21+
}
22+
23+
return Promise.resolve()
24+
// Add an error to a non-main file.
25+
.then(() => appendToFile('src/app/app.component.ts', ']]]]]'))
26+
// Should have an error.
27+
.then(() => execAndWaitForOutputToMatch('ng', ['serve'], failedRe))
28+
// Fix the error, should trigger a rebuild.
29+
.then(() => Promise.all([
30+
waitForAnyProcessOutputToMatch(successRe, 20000),
31+
replaceInFile('src/app/app.component.ts', ']]]]]', '')
32+
]))
33+
.then((results) => {
34+
const stderr = results[0].stderr;
35+
if (errorRe.test(stderr)) {
36+
throw new Error('Expected no error but an error was shown.');
37+
}
38+
})
39+
.then(() => killAllProcesses(), (err: any) => {
40+
killAllProcesses();
41+
throw err;
42+
});
43+
}

0 commit comments

Comments
 (0)