77 */
88import { CompilerHost , CompilerOptions , readConfiguration } from '@angular/compiler-cli' ;
99import { NgtscProgram } from '@angular/compiler-cli/src/ngtsc/program' ;
10+ import { createHash } from 'crypto' ;
1011import * as path from 'path' ;
1112import * as ts from 'typescript' ;
1213import {
@@ -31,7 +32,7 @@ import {
3132 augmentProgramWithVersioning ,
3233} from './host' ;
3334import { externalizePath , normalizePath } from './paths' ;
34- import { AngularPluginSymbol , FileEmitter } from './symbol' ;
35+ import { AngularPluginSymbol , EmitFileResult , FileEmitter } from './symbol' ;
3536import { createWebpackSystem } from './system' ;
3637import { createAotTransformers , createJitTransformers , mergeTransformers } from './transformation' ;
3738
@@ -76,6 +77,10 @@ function initializeNgccProcessor(
7677 return { processor, errors, warnings } ;
7778}
7879
80+ function hashContent ( content : string ) : Uint8Array {
81+ return createHash ( 'md5' ) . update ( content ) . digest ( ) ;
82+ }
83+
7984const PLUGIN_NAME = 'angular-compiler' ;
8085
8186export class AngularWebpackPlugin {
@@ -87,6 +92,8 @@ export class AngularWebpackPlugin {
8792 private buildTimestamp ! : number ;
8893 private readonly lazyRouteMap : Record < string , string > = { } ;
8994 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 } > ( ) ;
9097
9198 constructor ( options : Partial < AngularPluginOptions > = { } ) {
9299 this . pluginOptions = {
@@ -181,10 +188,7 @@ export class AngularWebpackPlugin {
181188 pathsPlugin . update ( compilerOptions ) ;
182189
183190 // 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 ) ) ;
188192 const host = ts . createIncrementalCompilerHost ( compilerOptions , system ) ;
189193
190194 // Setup source file caching and reuse cache from previous compilation if present
@@ -251,22 +255,7 @@ export class AngularWebpackPlugin {
251255
252256 compilation . hooks . finishModules . tapPromise ( PLUGIN_NAME , async ( modules ) => {
253257 // 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 ) ;
270259
271260 // Analyze program for unused files
272261 if ( compilation . errors . length > 0 ) {
@@ -304,6 +293,47 @@ export class AngularWebpackPlugin {
304293 } ) ;
305294 }
306295
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+
307337 private loadConfiguration ( compilation : WebpackCompilation ) {
308338 const { options : compilerOptions , rootNames, errors } = readConfiguration (
309339 this . pluginOptions . tsconfig ,
@@ -432,10 +462,14 @@ export class AngularWebpackPlugin {
432462 if ( angularCompiler . getDiagnosticsForFile ) {
433463 // @angular /compiler-cli 11.1+
434464 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+ ) ;
436468 } else {
437469 // @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 [ ] ;
439473 diagnosticsReporter ( getDiagnostics . call ( angularCompiler , sourceFile ) ) ;
440474 }
441475 }
@@ -550,13 +584,17 @@ export class AngularWebpackPlugin {
550584 onAfterEmit ?: ( sourceFile : ts . SourceFile ) => void ,
551585 ) : FileEmitter {
552586 return async ( file : string ) => {
587+ if ( this . requiredFilesToEmitCache . has ( file ) ) {
588+ return this . requiredFilesToEmitCache . get ( file ) ;
589+ }
590+
553591 const sourceFile = program . getSourceFile ( file ) ;
554592 if ( ! sourceFile ) {
555593 return undefined ;
556594 }
557595
558- let content : string | undefined = undefined ;
559- let map : string | undefined = undefined ;
596+ let content : string | undefined ;
597+ let map : string | undefined ;
560598 program . emit (
561599 sourceFile ,
562600 ( filename , data ) => {
@@ -573,12 +611,19 @@ export class AngularWebpackPlugin {
573611
574612 onAfterEmit ?.( sourceFile ) ;
575613
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+
576621 const dependencies = [
577622 ...program . getAllDependencies ( sourceFile ) ,
578623 ...getExtraDependencies ( sourceFile ) ,
579624 ] . map ( externalizePath ) ;
580625
581- return { content, map, dependencies } ;
626+ return { content, map, dependencies, hash } ;
582627 } ;
583628 }
584629}
0 commit comments