Skip to content

Commit 58acf2f

Browse files
committed
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 616a9f7 commit 58acf2f

4 files changed

Lines changed: 70 additions & 37 deletions

File tree

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

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,6 @@ export class AngularCompilerPlugin implements Tapable {
100100
private _donePromise: Promise<void> | null;
101101
private _compiler: any = null;
102102
private _compilation: any = null;
103-
private _failedCompilation = false;
104103

105104
// TypeChecker process.
106105
private _forkTypeChecker = true;
@@ -114,7 +113,6 @@ export class AngularCompilerPlugin implements Tapable {
114113

115114
get options() { return this._options; }
116115
get done() { return this._donePromise; }
117-
get failedCompilation() { return this._failedCompilation; }
118116
get entryModule() {
119117
const splitted = this._entryModule.split('#');
120118
const path = splitted[0];
@@ -311,13 +309,19 @@ export class AngularCompilerPlugin implements Tapable {
311309
this._updateForkedTypeChecker(changedTsFiles);
312310
}
313311

314-
if (this._JitMode) {
312+
// We want to allow emitting with errors on the first run so that imports can be added
313+
// to the webpack dependency tree and rebuilds triggered by file edits.
314+
const compilerOptions = {
315+
...this._angularCompilerOptions,
316+
noEmitOnError: !this._firstRun
317+
};
315318

319+
if (this._JitMode) {
316320
// Create the TypeScript program.
317321
time('AngularCompilerPlugin._createOrUpdateProgram.ts.createProgram');
318322
this._program = ts.createProgram(
319323
this._tsFilenames,
320-
this._angularCompilerOptions,
324+
compilerOptions,
321325
this._angularCompilerHost,
322326
this._program as ts.Program
323327
);
@@ -329,7 +333,7 @@ export class AngularCompilerPlugin implements Tapable {
329333
// Create the Angular program.
330334
this._program = createProgram({
331335
rootNames: this._tsFilenames,
332-
options: this._angularCompilerOptions,
336+
options: compilerOptions,
333337
host: this._angularCompilerHost,
334338
oldProgram: this._program as Program
335339
});
@@ -527,7 +531,6 @@ export class AngularCompilerPlugin implements Tapable {
527531
compiler.plugin('done', () => {
528532
this._donePromise = null;
529533
this._compilation = null;
530-
this._failedCompilation = false;
531534
});
532535

533536
// TODO: consider if it's better to remove this plugin and instead make it wait on the
@@ -604,7 +607,6 @@ export class AngularCompilerPlugin implements Tapable {
604607
timeEnd('AngularCompilerPlugin._make');
605608
cb();
606609
}, (err: any) => {
607-
this._failedCompilation = true;
608610
compilation.errors.push(err.stack);
609611
timeEnd('AngularCompilerPlugin._make');
610612
cb();
@@ -726,8 +728,6 @@ export class AngularCompilerPlugin implements Tapable {
726728
// Reset changed files on successful compilation.
727729
if (emitResult && !emitResult.emitSkipped && this._compilation.errors.length === 0) {
728730
this._compilerHost.resetChangedFileTracker();
729-
} else {
730-
this._failedCompilation = true;
731731
}
732732
}
733733
timeEnd('AngularCompilerPlugin._update');
@@ -789,7 +789,9 @@ export class AngularCompilerPlugin implements Tapable {
789789
'AngularCompilerPlugin._emit.ts'));
790790
}
791791

792-
if (!hasErrors(allDiagnostics)) {
792+
// Always emit on the first run, so that imports are processed by webpack and the user
793+
// can trigger a rebuild by editing any file.
794+
if (!hasErrors(allDiagnostics) || this._firstRun) {
793795
sourceFiles.forEach((sf) => {
794796
const timeLabel = `AngularCompilerPlugin._emit.ts+${sf.fileName}+.emit`;
795797
time(timeLabel);
@@ -820,7 +822,9 @@ export class AngularCompilerPlugin implements Tapable {
820822
'AngularCompilerPlugin._emit.ng'));
821823
}
822824

823-
if (!hasErrors(allDiagnostics)) {
825+
// Always emit on the first run, so that imports are processed by webpack and the user
826+
// can trigger a rebuild by editing any file.
827+
if (!hasErrors(allDiagnostics) || this._firstRun) {
824828
time('AngularCompilerPlugin._emit.ng.emit');
825829
const extractI18n = !!this._angularCompilerOptions.i18nOutFile;
826830
const emitFlags = extractI18n ? EmitFlags.I18nBundle : EmitFlags.Default;

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

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -544,19 +544,14 @@ export function ngcLoader(this: LoaderContext & { _compilation: any }, source: s
544544
.then(() => {
545545
timeEnd(timeLabel + '.ngcLoader.AngularCompilerPlugin');
546546
const result = plugin.getFile(sourceFileName);
547-
if (plugin.failedCompilation) {
548-
// Return an empty string if there is no result to prevent extra loader errors.
549-
// Plugin errors were already pushed to the compilation errors.
550-
timeEnd(timeLabel);
551-
cb(null, result.outputText || '', result.sourceMap);
552-
} else {
553-
timeEnd(timeLabel);
547+
timeEnd(timeLabel);
548+
if (result.outputText === undefined) {
549+
throw new Error('TypeScript compilation failed.');
550+
}
554551
cb(null, result.outputText, result.sourceMap);
555-
}
556-
})
557-
.catch(err => {
558-
timeEnd(timeLabel + '.ngcLoader.AngularCompilerPlugin');
559-
cb(err);
552+
})
553+
.catch(err => {
554+
cb(err);
560555
});
561556
} else if (plugin instanceof AotPlugin) {
562557
time(timeLabel + '.ngcLoader.AotPlugin');
@@ -645,15 +640,12 @@ export function ngcLoader(this: LoaderContext & { _compilation: any }, source: s
645640
timeEnd(timeLabel + '.ngcLoader.AotPlugin.transpile');
646641

647642
timeEnd(timeLabel + '.ngcLoader.AotPlugin');
648-
if (plugin.failedCompilation && plugin.compilerOptions.noEmitOnError) {
649-
// Return an empty string to prevent extra loader errors (missing imports etc).
650-
// Plugin errors were already pushed to the compilation errors.
651-
timeEnd(timeLabel);
652-
cb(null, '');
653-
} else {
654-
timeEnd(timeLabel);
655-
cb(null, result.outputText, result.sourceMap);
643+
timeEnd(timeLabel);
644+
645+
if (result.outputText === undefined) {
646+
throw new Error('TypeScript compilation failed.');
656647
}
648+
cb(null, result.outputText, result.sourceMap);
657649
})
658650
.catch(err => cb(err));
659651
}

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

Lines changed: 0 additions & 6 deletions
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];
@@ -426,7 +424,6 @@ export class AotPlugin implements Tapable {
426424
compiler.plugin('done', () => {
427425
this._donePromise = null;
428426
this._compilation = null;
429-
this._failedCompilation = false;
430427
});
431428

432429
compiler.plugin('after-resolvers', (compiler: any) => {
@@ -638,14 +635,11 @@ export class AotPlugin implements Tapable {
638635
.then(() => {
639636
if (this._compilation.errors == 0) {
640637
this._compilerHost.resetChangedFileTracker();
641-
} else {
642-
this._failedCompilation = true;
643638
}
644639

645640
timeEnd('AotPlugin._make');
646641
cb();
647642
}, (err: any) => {
648-
this._failedCompilation = true;
649643
compilation.errors.push(err.stack);
650644
timeEnd('AotPlugin._make');
651645
cb();
Lines changed: 43 additions & 0 deletions
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)