77 */
88
99import type {
10+ BuildFailure ,
1011 Metafile ,
1112 OnStartResult ,
1213 OutputFile ,
@@ -33,6 +34,7 @@ import { AngularHostOptions } from './angular-host';
3334import { AngularCompilation , AotCompilation , JitCompilation , NoopCompilation } from './compilation' ;
3435import { SharedTSCompilationState , getSharedCompilationState } from './compilation-state' ;
3536import { ComponentStylesheetBundler } from './component-stylesheets' ;
37+ import { FileReferenceTracker } from './file-reference-tracker' ;
3638import { setupJitPluginCallbacks } from './jit-plugin-callbacks' ;
3739import { SourceFileCache } from './source-file-cache' ;
3840
@@ -84,9 +86,11 @@ export function createCompilerPlugin(
8486 pluginOptions . sourceFileCache ?. typeScriptFileCache ??
8587 new Map < string , string | Uint8Array > ( ) ;
8688
87- // The stylesheet resources from component stylesheets that will be added to the build results output files
88- let additionalOutputFiles : OutputFile [ ] = [ ] ;
89- let additionalMetafiles : Metafile [ ] ;
89+ // The resources from component stylesheets and web workers that will be added to the build results output files
90+ const additionalResults = new Map <
91+ string ,
92+ { outputFiles ?: OutputFile [ ] ; metafile ?: Metafile ; errors ?: PartialMessage [ ] }
93+ > ( ) ;
9094
9195 // Create new reusable compilation for the appropriate mode based on the `jit` plugin option
9296 const compilation : AngularCompilation = pluginOptions . noopTypeScriptCompilation
@@ -106,6 +110,10 @@ export function createCompilerPlugin(
106110 ) ;
107111 let sharedTSCompilationState : SharedTSCompilationState | undefined ;
108112
113+ // To fully invalidate files, track resource referenced files and their referencing source
114+ const referencedFileTracker = new FileReferenceTracker ( ) ;
115+
116+ // eslint-disable-next-line max-lines-per-function
109117 build . onStart ( async ( ) => {
110118 sharedTSCompilationState = getSharedCompilationState ( ) ;
111119 if ( ! ( compilation instanceof NoopCompilation ) ) {
@@ -119,14 +127,24 @@ export function createCompilerPlugin(
119127 // Reset debug performance tracking
120128 resetCumulativeDurations ( ) ;
121129
122- // Reset additional output files
123- additionalOutputFiles = [ ] ;
124- additionalMetafiles = [ ] ;
130+ // Update the reference tracker and generate a full set of modified files for the
131+ // Angular compiler which does not have direct knowledge of transitive resource
132+ // dependencies or web worker processing.
133+ let modifiedFiles ;
134+ if (
135+ pluginOptions . sourceFileCache ?. modifiedFiles . size &&
136+ referencedFileTracker &&
137+ ! pluginOptions . noopTypeScriptCompilation
138+ ) {
139+ // TODO: Differentiate between changed input files and stale output files
140+ modifiedFiles = referencedFileTracker . update ( pluginOptions . sourceFileCache . modifiedFiles ) ;
141+ pluginOptions . sourceFileCache . invalidate ( modifiedFiles ) ;
142+ }
125143
126144 // Create Angular compiler host options
127145 const hostOptions : AngularHostOptions = {
128146 fileReplacements : pluginOptions . fileReplacements ,
129- modifiedFiles : pluginOptions . sourceFileCache ?. modifiedFiles ,
147+ modifiedFiles,
130148 sourceFileCache : pluginOptions . sourceFileCache ,
131149 async transformStylesheet ( data , containingFile , stylesheetFile ) {
132150 let stylesheetResult ;
@@ -142,14 +160,22 @@ export function createCompilerPlugin(
142160 ) ;
143161 }
144162
145- const { contents, resourceFiles, errors, warnings } = stylesheetResult ;
163+ const { contents, resourceFiles, referencedFiles , errors, warnings } = stylesheetResult ;
146164 if ( errors ) {
147165 ( result . errors ??= [ ] ) . push ( ...errors ) ;
148166 }
149167 ( result . warnings ??= [ ] ) . push ( ...warnings ) ;
150- additionalOutputFiles . push ( ...resourceFiles ) ;
151- if ( stylesheetResult . metafile ) {
152- additionalMetafiles . push ( stylesheetResult . metafile ) ;
168+ additionalResults . set ( stylesheetFile ?? containingFile , {
169+ outputFiles : resourceFiles ,
170+ metafile : stylesheetResult . metafile ,
171+ } ) ;
172+
173+ if ( referencedFiles ) {
174+ referencedFileTracker . add ( containingFile , referencedFiles ) ;
175+ if ( stylesheetFile ) {
176+ // Angular AOT compiler needs modified direct resource files to correctly invalidate its analysis
177+ referencedFileTracker . add ( stylesheetFile , referencedFiles ) ;
178+ }
153179 }
154180
155181 return contents ;
@@ -159,37 +185,38 @@ export function createCompilerPlugin(
159185 // The synchronous API must be used due to the TypeScript compilation currently being
160186 // fully synchronous and this process callback being called from within a TypeScript
161187 // transformer.
162- const workerResult = build . esbuild . buildSync ( {
163- platform : 'browser' ,
164- write : false ,
165- bundle : true ,
166- metafile : true ,
167- format : 'esm' ,
168- mainFields : [ 'es2020' , 'es2015' , 'browser' , 'module' , 'main' ] ,
169- sourcemap : pluginOptions . sourcemap ,
170- entryNames : 'worker-[hash]' ,
171- entryPoints : [ fullWorkerPath ] ,
172- absWorkingDir : build . initialOptions . absWorkingDir ,
173- outdir : build . initialOptions . outdir ,
174- minifyIdentifiers : build . initialOptions . minifyIdentifiers ,
175- minifySyntax : build . initialOptions . minifySyntax ,
176- minifyWhitespace : build . initialOptions . minifyWhitespace ,
177- target : build . initialOptions . target ,
178- } ) ;
188+ const workerResult = bundleWebWorker ( build , pluginOptions , fullWorkerPath ) ;
179189
180190 ( result . warnings ??= [ ] ) . push ( ...workerResult . warnings ) ;
181- additionalOutputFiles . push ( ...workerResult . outputFiles ) ;
182- if ( workerResult . metafile ) {
183- additionalMetafiles . push ( workerResult . metafile ) ;
184- }
185-
186191 if ( workerResult . errors . length > 0 ) {
187192 ( result . errors ??= [ ] ) . push ( ...workerResult . errors ) ;
193+ // Track worker file errors to allow rebuilds on changes
194+ referencedFileTracker . add (
195+ containingFile ,
196+ workerResult . errors
197+ . map ( ( error ) => error . location ?. file )
198+ . filter ( ( file ) : file is string => ! ! file )
199+ . map ( ( file ) => path . join ( build . initialOptions . absWorkingDir ?? '' , file ) ) ,
200+ ) ;
201+ additionalResults . set ( fullWorkerPath , { errors : result . errors } ) ;
188202
189203 // Return the original path if the build failed
190204 return workerFile ;
191205 }
192206
207+ assert ( 'outputFiles' in workerResult , 'Invalid web worker bundle result.' ) ;
208+ additionalResults . set ( fullWorkerPath , {
209+ outputFiles : workerResult . outputFiles ,
210+ metafile : workerResult . metafile ,
211+ } ) ;
212+
213+ referencedFileTracker . add (
214+ containingFile ,
215+ Object . keys ( workerResult . metafile . inputs ) . map ( ( input ) =>
216+ path . join ( build . initialOptions . absWorkingDir ?? '' , input ) ,
217+ ) ,
218+ ) ;
219+
193220 // Return bundled worker file entry name to be used in the built output
194221 const workerCodeFile = workerResult . outputFiles . find ( ( file ) =>
195222 file . path . endsWith ( '.js' ) ,
@@ -277,9 +304,20 @@ export function createCompilerPlugin(
277304 }
278305 } ) ;
279306
307+ // Add errors from failed additional results.
308+ // This must be done after emit to capture latest web worker results.
309+ for ( const { errors } of additionalResults . values ( ) ) {
310+ if ( errors ) {
311+ ( result . errors ??= [ ] ) . push ( ...errors ) ;
312+ }
313+ }
314+
280315 // Store referenced files for updated file watching if enabled
281316 if ( pluginOptions . sourceFileCache ) {
282- pluginOptions . sourceFileCache . referencedFiles = referencedFiles ;
317+ pluginOptions . sourceFileCache . referencedFiles = [
318+ ...referencedFiles ,
319+ ...referencedFileTracker . referencedFiles ,
320+ ] ;
283321 }
284322
285323 // Reset the setup warnings so that they are only shown during the first build.
@@ -363,20 +401,20 @@ export function createCompilerPlugin(
363401 setupJitPluginCallbacks (
364402 build ,
365403 stylesheetBundler ,
366- additionalOutputFiles ,
404+ additionalResults ,
367405 styleOptions . inlineStyleLanguage ,
368406 ) ;
369407 }
370408
371409 build . onEnd ( ( result ) => {
372- // Add any additional output files to the main output files
373- if ( additionalOutputFiles . length ) {
374- result . outputFiles ?. push ( ...additionalOutputFiles ) ;
375- }
410+ for ( const { outputFiles, metafile } of additionalResults . values ( ) ) {
411+ // Add any additional output files to the main output files
412+ if ( outputFiles ?. length ) {
413+ result . outputFiles ?. push ( ...outputFiles ) ;
414+ }
376415
377- // Combine additional metafiles with main metafile
378- if ( result . metafile && additionalMetafiles . length ) {
379- for ( const metafile of additionalMetafiles ) {
416+ // Combine additional metafiles with main metafile
417+ if ( result . metafile && metafile ) {
380418 result . metafile . inputs = { ...result . metafile . inputs , ...metafile . inputs } ;
381419 result . metafile . outputs = { ...result . metafile . outputs , ...metafile . outputs } ;
382420 }
@@ -393,6 +431,38 @@ export function createCompilerPlugin(
393431 } ;
394432}
395433
434+ function bundleWebWorker (
435+ build : PluginBuild ,
436+ pluginOptions : CompilerPluginOptions ,
437+ workerFile : string ,
438+ ) {
439+ try {
440+ return build . esbuild . buildSync ( {
441+ platform : 'browser' ,
442+ write : false ,
443+ bundle : true ,
444+ metafile : true ,
445+ format : 'esm' ,
446+ mainFields : [ 'es2020' , 'es2015' , 'browser' , 'module' , 'main' ] ,
447+ logLevel : 'silent' ,
448+ sourcemap : pluginOptions . sourcemap ,
449+ entryNames : 'worker-[hash]' ,
450+ entryPoints : [ workerFile ] ,
451+ absWorkingDir : build . initialOptions . absWorkingDir ,
452+ outdir : build . initialOptions . outdir ,
453+ minifyIdentifiers : build . initialOptions . minifyIdentifiers ,
454+ minifySyntax : build . initialOptions . minifySyntax ,
455+ minifyWhitespace : build . initialOptions . minifyWhitespace ,
456+ target : build . initialOptions . target ,
457+ } ) ;
458+ } catch ( error ) {
459+ if ( error && typeof error === 'object' && 'errors' in error && 'warnings' in error ) {
460+ return error as BuildFailure ;
461+ }
462+ throw error ;
463+ }
464+ }
465+
396466function createMissingFileError ( request : string , original : string , root : string ) : PartialMessage {
397467 const error = {
398468 text : `File '${ path . relative ( root , request ) } ' is missing from the TypeScript compilation.` ,
0 commit comments