@@ -173,9 +173,8 @@ export function createCompilerPlugin(
173173
174174 // This uses a wrapped dynamic import to load `@angular/compiler-cli` which is ESM.
175175 // 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' ) ;
179178
180179 // Temporary deep import for transformer support
181180 const {
@@ -185,7 +184,7 @@ export function createCompilerPlugin(
185184
186185 // Setup defines based on the values provided by the Angular compiler-cli
187186 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 ) ) {
189188 if ( key in build . initialOptions . define ) {
190189 // Skip keys that have been manually provided
191190 continue ;
@@ -202,7 +201,7 @@ export function createCompilerPlugin(
202201 rootNames,
203202 errors : configurationDiagnostics ,
204203 } = profileSync ( 'NG_READ_CONFIG' , ( ) =>
205- compilerCli . readConfiguration ( pluginOptions . tsconfig , {
204+ readConfiguration ( pluginOptions . tsconfig , {
206205 noEmitOnError : false ,
207206 suppressOutputPathCheck : true ,
208207 outDir : undefined ,
@@ -249,6 +248,7 @@ export function createCompilerPlugin(
249248 let previousBuilder : ts . EmitAndSemanticDiagnosticsBuilderProgram | undefined ;
250249 let previousAngularProgram : NgtscProgram | undefined ;
251250 const babelDataCache = new Map < string , string > ( ) ;
251+ const diagnosticCache = new WeakMap < ts . SourceFile , ts . Diagnostic [ ] > ( ) ;
252252
253253 build . onStart ( async ( ) => {
254254 const result : OnStartResult = {
@@ -339,12 +339,10 @@ export function createCompilerPlugin(
339339 // Create the Angular specific program that contains the Angular compiler
340340 const angularProgram = profileSync (
341341 'NG_CREATE_PROGRAM' ,
342- ( ) =>
343- new compilerCli . NgtscProgram ( rootNames , compilerOptions , host , previousAngularProgram ) ,
342+ ( ) => new NgtscProgram ( rootNames , compilerOptions , host , previousAngularProgram ) ,
344343 ) ;
345344 previousAngularProgram = angularProgram ;
346345 const angularCompiler = angularProgram . compiler ;
347- const { ignoreForDiagnostics } = angularCompiler ;
348346 const typeScriptProgram = angularProgram . getTsProgram ( ) ;
349347 augmentProgramWithVersioning ( typeScriptProgram ) ;
350348
@@ -366,12 +364,16 @@ export function createCompilerPlugin(
366364 yield * builder . getGlobalDiagnostics ( ) ;
367365
368366 // 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 ;
370370 for ( const sourceFile of builder . getSourceFiles ( ) ) {
371- if ( ignoreForDiagnostics . has ( sourceFile ) ) {
371+ if ( angularCompiler . ignoreForDiagnostics . has ( sourceFile ) ) {
372372 continue ;
373373 }
374374
375+ // TypeScript will use cached diagnostics for files that have not been
376+ // changed or affected for this build when using incremental building.
375377 yield * profileSync (
376378 'NG_DIAGNOSTICS_SYNTACTIC' ,
377379 ( ) => builder . getSyntacticDiagnostics ( sourceFile ) ,
@@ -383,12 +385,22 @@ export function createCompilerPlugin(
383385 true ,
384386 ) ;
385387
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+ }
392404 }
393405 }
394406
@@ -408,7 +420,7 @@ export function createCompilerPlugin(
408420 mergeTransformers ( angularCompiler . prepareEmit ( ) . transformers , {
409421 before : [ replaceBootstrap ( ( ) => builder . getProgram ( ) . getTypeChecker ( ) ) ] ,
410422 } ) ,
411- ( sourceFile ) => angularCompiler . incrementalDriver . recordSuccessfulEmit ( sourceFile ) ,
423+ ( sourceFile ) => angularCompiler . incrementalCompilation . recordSuccessfulEmit ( sourceFile ) ,
412424 ) ;
413425
414426 return result ;
@@ -590,3 +602,52 @@ async function transformWithBabel(
590602
591603 return result ?. code ?? data ;
592604}
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