7
7
*/
8
8
import { CompilerHost , CompilerOptions , readConfiguration } from '@angular/compiler-cli' ;
9
9
import { NgtscProgram } from '@angular/compiler-cli/src/ngtsc/program' ;
10
+ import { createHash } from 'crypto' ;
10
11
import * as path from 'path' ;
11
12
import * as ts from 'typescript' ;
12
13
import {
@@ -31,7 +32,7 @@ import {
31
32
augmentProgramWithVersioning ,
32
33
} from './host' ;
33
34
import { externalizePath , normalizePath } from './paths' ;
34
- import { AngularPluginSymbol , FileEmitter } from './symbol' ;
35
+ import { AngularPluginSymbol , EmitFileResult , FileEmitter } from './symbol' ;
35
36
import { createWebpackSystem } from './system' ;
36
37
import { createAotTransformers , createJitTransformers , mergeTransformers } from './transformation' ;
37
38
@@ -76,6 +77,10 @@ function initializeNgccProcessor(
76
77
return { processor, errors, warnings } ;
77
78
}
78
79
80
+ function hashContent ( content : string ) : Uint8Array {
81
+ return createHash ( 'md5' ) . update ( content ) . digest ( ) ;
82
+ }
83
+
79
84
const PLUGIN_NAME = 'angular-compiler' ;
80
85
81
86
export class AngularWebpackPlugin {
@@ -87,6 +92,8 @@ export class AngularWebpackPlugin {
87
92
private buildTimestamp ! : number ;
88
93
private readonly lazyRouteMap : Record < string , string > = { } ;
89
94
private readonly requiredFilesToEmit = new Set < string > ( ) ;
95
+ private readonly requiredFilesToEmitCache = new Map < string , EmitFileResult | undefined > ( ) ;
96
+ private readonly fileEmitHistory = new Map < string , { length : number ; hash : Uint8Array } > ( ) ;
90
97
91
98
constructor ( options : Partial < AngularPluginOptions > = { } ) {
92
99
this . pluginOptions = {
@@ -181,10 +188,7 @@ export class AngularWebpackPlugin {
181
188
pathsPlugin . update ( compilerOptions ) ;
182
189
183
190
// Create a Webpack-based TypeScript compiler host
184
- const system = createWebpackSystem (
185
- compiler . inputFileSystem ,
186
- normalizePath ( compiler . context ) ,
187
- ) ;
191
+ const system = createWebpackSystem ( compiler . inputFileSystem , normalizePath ( compiler . context ) ) ;
188
192
const host = ts . createIncrementalCompilerHost ( compilerOptions , system ) ;
189
193
190
194
// Setup source file caching and reuse cache from previous compilation if present
@@ -251,22 +255,7 @@ export class AngularWebpackPlugin {
251
255
252
256
compilation . hooks . finishModules . tapPromise ( PLUGIN_NAME , async ( modules ) => {
253
257
// Rebuild any remaining AOT required modules
254
- const rebuild = ( filename : string ) => new Promise < void > ( ( resolve ) => {
255
- const module = modules . find (
256
- ( { resource } : compilation . Module & { resource ?: string } ) =>
257
- resource && normalizePath ( resource ) === filename ,
258
- ) ;
259
- if ( ! module ) {
260
- resolve ( ) ;
261
- } else {
262
- compilation . rebuildModule ( module , resolve ) ;
263
- }
264
- } ) ;
265
-
266
- for ( const requiredFile of this . requiredFilesToEmit ) {
267
- await rebuild ( requiredFile ) ;
268
- }
269
- this . requiredFilesToEmit . clear ( ) ;
258
+ await this . rebuildRequiredFiles ( modules , compilation , fileEmitter ) ;
270
259
271
260
// Analyze program for unused files
272
261
if ( compilation . errors . length > 0 ) {
@@ -304,6 +293,47 @@ export class AngularWebpackPlugin {
304
293
} ) ;
305
294
}
306
295
296
+ private async rebuildRequiredFiles (
297
+ modules : compilation . Module [ ] ,
298
+ compilation : WebpackCompilation ,
299
+ fileEmitter : FileEmitter ,
300
+ ) {
301
+ const rebuild = ( filename : string ) =>
302
+ new Promise < void > ( ( resolve ) => {
303
+ const module = modules . find (
304
+ ( { resource } : compilation . Module & { resource ?: string } ) =>
305
+ resource && normalizePath ( resource ) === filename ,
306
+ ) ;
307
+ if ( ! module ) {
308
+ resolve ( ) ;
309
+ } else {
310
+ compilation . rebuildModule ( module , resolve ) ;
311
+ }
312
+ } ) ;
313
+
314
+ for ( const requiredFile of this . requiredFilesToEmit ) {
315
+ const history = this . fileEmitHistory . get ( requiredFile ) ;
316
+ if ( history ) {
317
+ const emitResult = await fileEmitter ( requiredFile ) ;
318
+ if (
319
+ emitResult ?. content === undefined ||
320
+ history . length !== emitResult . content . length ||
321
+ emitResult . hash === undefined ||
322
+ Buffer . compare ( history . hash , emitResult . hash ) !== 0
323
+ ) {
324
+ // New emit result is different so rebuild using new emit result
325
+ this . requiredFilesToEmitCache . set ( requiredFile , emitResult ) ;
326
+ await rebuild ( requiredFile ) ;
327
+ }
328
+ } else {
329
+ // No emit history so rebuild
330
+ await rebuild ( requiredFile ) ;
331
+ }
332
+ }
333
+ this . requiredFilesToEmit . clear ( ) ;
334
+ this . requiredFilesToEmitCache . clear ( ) ;
335
+ }
336
+
307
337
private loadConfiguration ( compilation : WebpackCompilation ) {
308
338
const { options : compilerOptions , rootNames, errors } = readConfiguration (
309
339
this . pluginOptions . tsconfig ,
@@ -432,10 +462,14 @@ export class AngularWebpackPlugin {
432
462
if ( angularCompiler . getDiagnosticsForFile ) {
433
463
// @angular /compiler-cli 11.1+
434
464
const { OptimizeFor } = require ( '@angular/compiler-cli/src/ngtsc/typecheck/api' ) ;
435
- diagnosticsReporter ( angularCompiler . getDiagnosticsForFile ( sourceFile , OptimizeFor . WholeProgram ) ) ;
465
+ diagnosticsReporter (
466
+ angularCompiler . getDiagnosticsForFile ( sourceFile , OptimizeFor . WholeProgram ) ,
467
+ ) ;
436
468
} else {
437
469
// @angular /compiler-cli 11.0+
438
- const getDiagnostics = angularCompiler . getDiagnostics as ( sourceFile : ts . SourceFile ) => ts . Diagnostic [ ] ;
470
+ const getDiagnostics = angularCompiler . getDiagnostics as (
471
+ sourceFile : ts . SourceFile ,
472
+ ) => ts . Diagnostic [ ] ;
439
473
diagnosticsReporter ( getDiagnostics . call ( angularCompiler , sourceFile ) ) ;
440
474
}
441
475
}
@@ -550,13 +584,17 @@ export class AngularWebpackPlugin {
550
584
onAfterEmit ?: ( sourceFile : ts . SourceFile ) => void ,
551
585
) : FileEmitter {
552
586
return async ( file : string ) => {
587
+ if ( this . requiredFilesToEmitCache . has ( file ) ) {
588
+ return this . requiredFilesToEmitCache . get ( file ) ;
589
+ }
590
+
553
591
const sourceFile = program . getSourceFile ( file ) ;
554
592
if ( ! sourceFile ) {
555
593
return undefined ;
556
594
}
557
595
558
- let content : string | undefined = undefined ;
559
- let map : string | undefined = undefined ;
596
+ let content : string | undefined ;
597
+ let map : string | undefined ;
560
598
program . emit (
561
599
sourceFile ,
562
600
( filename , data ) => {
@@ -573,12 +611,19 @@ export class AngularWebpackPlugin {
573
611
574
612
onAfterEmit ?.( sourceFile ) ;
575
613
614
+ let hash ;
615
+ if ( content !== undefined && this . watchMode ) {
616
+ // Capture emit history info for Angular rebuild analysis
617
+ hash = hashContent ( content ) ;
618
+ this . fileEmitHistory . set ( file , { length : content . length , hash } ) ;
619
+ }
620
+
576
621
const dependencies = [
577
622
...program . getAllDependencies ( sourceFile ) ,
578
623
...getExtraDependencies ( sourceFile ) ,
579
624
] . map ( externalizePath ) ;
580
625
581
- return { content, map, dependencies } ;
626
+ return { content, map, dependencies, hash } ;
582
627
} ;
583
628
}
584
629
}
0 commit comments