@@ -173,9 +173,8 @@ export function createCompilerPlugin(
173
173
174
174
// This uses a wrapped dynamic import to load `@angular/compiler-cli` which is ESM.
175
175
// Once TypeScript provides support for retaining dynamic imports this workaround can be dropped.
176
- const compilerCli = await loadEsmModule < typeof import ( '@angular/compiler-cli' ) > (
177
- '@angular/compiler-cli' ,
178
- ) ;
176
+ const { GLOBAL_DEFS_FOR_TERSER_WITH_AOT , NgtscProgram, OptimizeFor, readConfiguration } =
177
+ await loadEsmModule < typeof import ( '@angular/compiler-cli' ) > ( '@angular/compiler-cli' ) ;
179
178
180
179
// Temporary deep import for transformer support
181
180
const {
@@ -185,7 +184,7 @@ export function createCompilerPlugin(
185
184
186
185
// Setup defines based on the values provided by the Angular compiler-cli
187
186
build . initialOptions . define ??= { } ;
188
- for ( const [ key , value ] of Object . entries ( compilerCli . GLOBAL_DEFS_FOR_TERSER_WITH_AOT ) ) {
187
+ for ( const [ key , value ] of Object . entries ( GLOBAL_DEFS_FOR_TERSER_WITH_AOT ) ) {
189
188
if ( key in build . initialOptions . define ) {
190
189
// Skip keys that have been manually provided
191
190
continue ;
@@ -202,7 +201,7 @@ export function createCompilerPlugin(
202
201
rootNames,
203
202
errors : configurationDiagnostics ,
204
203
} = profileSync ( 'NG_READ_CONFIG' , ( ) =>
205
- compilerCli . readConfiguration ( pluginOptions . tsconfig , {
204
+ readConfiguration ( pluginOptions . tsconfig , {
206
205
noEmitOnError : false ,
207
206
suppressOutputPathCheck : true ,
208
207
outDir : undefined ,
@@ -249,6 +248,7 @@ export function createCompilerPlugin(
249
248
let previousBuilder : ts . EmitAndSemanticDiagnosticsBuilderProgram | undefined ;
250
249
let previousAngularProgram : NgtscProgram | undefined ;
251
250
const babelDataCache = new Map < string , string > ( ) ;
251
+ const diagnosticCache = new WeakMap < ts . SourceFile , ts . Diagnostic [ ] > ( ) ;
252
252
253
253
build . onStart ( async ( ) => {
254
254
const result : OnStartResult = {
@@ -339,12 +339,10 @@ export function createCompilerPlugin(
339
339
// Create the Angular specific program that contains the Angular compiler
340
340
const angularProgram = profileSync (
341
341
'NG_CREATE_PROGRAM' ,
342
- ( ) =>
343
- new compilerCli . NgtscProgram ( rootNames , compilerOptions , host , previousAngularProgram ) ,
342
+ ( ) => new NgtscProgram ( rootNames , compilerOptions , host , previousAngularProgram ) ,
344
343
) ;
345
344
previousAngularProgram = angularProgram ;
346
345
const angularCompiler = angularProgram . compiler ;
347
- const { ignoreForDiagnostics } = angularCompiler ;
348
346
const typeScriptProgram = angularProgram . getTsProgram ( ) ;
349
347
augmentProgramWithVersioning ( typeScriptProgram ) ;
350
348
@@ -366,12 +364,16 @@ export function createCompilerPlugin(
366
364
yield * builder . getGlobalDiagnostics ( ) ;
367
365
368
366
// Collect source file specific diagnostics
369
- const OptimizeFor = compilerCli . OptimizeFor ;
367
+ const affectedFiles = findAffectedFiles ( builder , angularCompiler ) ;
368
+ const optimizeFor =
369
+ affectedFiles . size > 1 ? OptimizeFor . WholeProgram : OptimizeFor . SingleFile ;
370
370
for ( const sourceFile of builder . getSourceFiles ( ) ) {
371
- if ( ignoreForDiagnostics . has ( sourceFile ) ) {
371
+ if ( angularCompiler . ignoreForDiagnostics . has ( sourceFile ) ) {
372
372
continue ;
373
373
}
374
374
375
+ // TypeScript will use cached diagnostics for files that have not been
376
+ // changed or affected for this build when using incremental building.
375
377
yield * profileSync (
376
378
'NG_DIAGNOSTICS_SYNTACTIC' ,
377
379
( ) => builder . getSyntacticDiagnostics ( sourceFile ) ,
@@ -383,12 +385,22 @@ export function createCompilerPlugin(
383
385
true ,
384
386
) ;
385
387
386
- const angularDiagnostics = profileSync (
387
- 'NG_DIAGNOSTICS_TEMPLATE' ,
388
- ( ) => angularCompiler . getDiagnosticsForFile ( sourceFile , OptimizeFor . WholeProgram ) ,
389
- true ,
390
- ) ;
391
- yield * angularDiagnostics ;
388
+ // Only request Angular template diagnostics for affected files to avoid
389
+ // overhead of template diagnostics for unchanged files.
390
+ if ( affectedFiles . has ( sourceFile ) ) {
391
+ const angularDiagnostics = profileSync (
392
+ 'NG_DIAGNOSTICS_TEMPLATE' ,
393
+ ( ) => angularCompiler . getDiagnosticsForFile ( sourceFile , optimizeFor ) ,
394
+ true ,
395
+ ) ;
396
+ diagnosticCache . set ( sourceFile , angularDiagnostics ) ;
397
+ yield * angularDiagnostics ;
398
+ } else {
399
+ const angularDiagnostics = diagnosticCache . get ( sourceFile ) ;
400
+ if ( angularDiagnostics ) {
401
+ yield * angularDiagnostics ;
402
+ }
403
+ }
392
404
}
393
405
}
394
406
@@ -408,7 +420,7 @@ export function createCompilerPlugin(
408
420
mergeTransformers ( angularCompiler . prepareEmit ( ) . transformers , {
409
421
before : [ replaceBootstrap ( ( ) => builder . getProgram ( ) . getTypeChecker ( ) ) ] ,
410
422
} ) ,
411
- ( sourceFile ) => angularCompiler . incrementalDriver . recordSuccessfulEmit ( sourceFile ) ,
423
+ ( sourceFile ) => angularCompiler . incrementalCompilation . recordSuccessfulEmit ( sourceFile ) ,
412
424
) ;
413
425
414
426
return result ;
@@ -590,3 +602,52 @@ async function transformWithBabel(
590
602
591
603
return result ?. code ?? data ;
592
604
}
605
+
606
+ function findAffectedFiles (
607
+ builder : ts . EmitAndSemanticDiagnosticsBuilderProgram ,
608
+ { ignoreForDiagnostics, ignoreForEmit, incrementalCompilation } : NgtscProgram [ 'compiler' ] ,
609
+ ) : Set < ts . SourceFile > {
610
+ const affectedFiles = new Set < ts . SourceFile > ( ) ;
611
+
612
+ // eslint-disable-next-line no-constant-condition
613
+ while ( true ) {
614
+ const result = builder . getSemanticDiagnosticsOfNextAffectedFile ( undefined , ( sourceFile ) => {
615
+ // If the affected file is a TTC shim, add the shim's original source file.
616
+ // This ensures that changes that affect TTC are typechecked even when the changes
617
+ // are otherwise unrelated from a TS perspective and do not result in Ivy codegen changes.
618
+ // For example, changing @Input property types of a directive used in another component's
619
+ // template.
620
+ // A TTC shim is a file that has been ignored for diagnostics and has a filename ending in `.ngtypecheck.ts`.
621
+ if ( ignoreForDiagnostics . has ( sourceFile ) && sourceFile . fileName . endsWith ( '.ngtypecheck.ts' ) ) {
622
+ // This file name conversion relies on internal compiler logic and should be converted
623
+ // to an official method when available. 15 is length of `.ngtypecheck.ts`
624
+ const originalFilename = sourceFile . fileName . slice ( 0 , - 15 ) + '.ts' ;
625
+ const originalSourceFile = builder . getSourceFile ( originalFilename ) ;
626
+ if ( originalSourceFile ) {
627
+ affectedFiles . add ( originalSourceFile ) ;
628
+ }
629
+
630
+ return true ;
631
+ }
632
+
633
+ return false ;
634
+ } ) ;
635
+
636
+ if ( ! result ) {
637
+ break ;
638
+ }
639
+
640
+ affectedFiles . add ( result . affected as ts . SourceFile ) ;
641
+ }
642
+
643
+ // A file is also affected if the Angular compiler requires it to be emitted
644
+ for ( const sourceFile of builder . getSourceFiles ( ) ) {
645
+ if ( ignoreForEmit . has ( sourceFile ) || incrementalCompilation . safeToSkipEmit ( sourceFile ) ) {
646
+ continue ;
647
+ }
648
+
649
+ affectedFiles . add ( sourceFile ) ;
650
+ }
651
+
652
+ return affectedFiles ;
653
+ }
0 commit comments