@@ -53,6 +53,16 @@ export interface AngularWebpackPluginOptions {
5353 inlineStyleFileExtension ?: string ;
5454}
5555
56+ /**
57+ * The Angular compilation state that is maintained across each Webpack compilation.
58+ */
59+ interface AngularCompilationState {
60+ ngccProcessor ?: NgccProcessor ;
61+ resourceLoader ?: WebpackResourceLoader ;
62+ previousUnused ?: Set < string > ;
63+ pathsPlugin : TypeScriptPathsPlugin ;
64+ }
65+
5666function initializeNgccProcessor (
5767 compiler : Compiler ,
5868 tsconfig : string ,
@@ -138,9 +148,8 @@ export class AngularWebpackPlugin {
138148 return this . pluginOptions ;
139149 }
140150
141- // eslint-disable-next-line max-lines-per-function
142151 apply ( compiler : Compiler ) : void {
143- const { NormalModuleReplacementPlugin, util } = compiler . webpack ;
152+ const { NormalModuleReplacementPlugin, WebpackError , util } = compiler . webpack ;
144153 this . webpackCreateHash = util . createHash ;
145154
146155 // Setup file replacements with webpack
@@ -175,171 +184,185 @@ export class AngularWebpackPlugin {
175184 // Load the compiler-cli if not already available
176185 compiler . hooks . beforeCompile . tapPromise ( PLUGIN_NAME , ( ) => this . initializeCompilerCli ( ) ) ;
177186
178- let ngccProcessor : NgccProcessor | undefined ;
179- let resourceLoader : WebpackResourceLoader | undefined ;
180- let previousUnused : Set < string > | undefined ;
187+ const compilationState : AngularCompilationState = { pathsPlugin } ;
181188 compiler . hooks . thisCompilation . tap ( PLUGIN_NAME , ( compilation ) => {
182- // Register plugin to ensure deterministic emit order in multi-plugin usage
183- const emitRegistration = this . registerWithCompilation ( compilation ) ;
184- this . watchMode = compiler . watchMode ;
185-
186- // Initialize webpack cache
187- if ( ! this . webpackCache && compilation . options . cache ) {
188- this . webpackCache = compilation . getCache ( PLUGIN_NAME ) ;
189- }
190-
191- // Initialize the resource loader if not already setup
192- if ( ! resourceLoader ) {
193- resourceLoader = new WebpackResourceLoader ( this . watchMode ) ;
189+ try {
190+ this . setupCompilation ( compilation , compilationState ) ;
191+ } catch ( error ) {
192+ compilation . errors . push (
193+ new WebpackError (
194+ `Failed to initialize Angular compilation - ${
195+ error instanceof Error ? error . message : error
196+ } `,
197+ ) ,
198+ ) ;
194199 }
200+ } ) ;
201+ }
195202
196- // Initialize and process eager ngcc if not already setup
197- if ( ! ngccProcessor ) {
198- const { processor, errors, warnings } = initializeNgccProcessor (
199- compiler ,
200- this . pluginOptions . tsconfig ,
201- this . compilerNgccModule ,
202- ) ;
203+ private setupCompilation ( compilation : Compilation , state : AngularCompilationState ) : void {
204+ const compiler = compilation . compiler ;
203205
204- processor . process ( ) ;
205- warnings . forEach ( ( warning ) => addWarning ( compilation , warning ) ) ;
206- errors . forEach ( ( error ) => addError ( compilation , error ) ) ;
206+ // Register plugin to ensure deterministic emit order in multi-plugin usage
207+ const emitRegistration = this . registerWithCompilation ( compilation ) ;
208+ this . watchMode = compiler . watchMode ;
207209
208- ngccProcessor = processor ;
209- }
210+ // Initialize webpack cache
211+ if ( ! this . webpackCache && compilation . options . cache ) {
212+ this . webpackCache = compilation . getCache ( PLUGIN_NAME ) ;
213+ }
210214
211- // Setup and read TypeScript and Angular compiler configuration
212- const { compilerOptions, rootNames, errors } = this . loadConfiguration ( ) ;
215+ // Initialize the resource loader if not already setup
216+ if ( ! state . resourceLoader ) {
217+ state . resourceLoader = new WebpackResourceLoader ( this . watchMode ) ;
218+ }
213219
214- // Create diagnostics reporter and report configuration file errors
215- const diagnosticsReporter = createDiagnosticsReporter ( compilation , ( diagnostic ) =>
216- this . compilerCli . formatDiagnostics ( [ diagnostic ] ) ,
220+ // Initialize and process eager ngcc if not already setup
221+ if ( ! state . ngccProcessor ) {
222+ const { processor, errors, warnings } = initializeNgccProcessor (
223+ compiler ,
224+ this . pluginOptions . tsconfig ,
225+ this . compilerNgccModule ,
217226 ) ;
218- diagnosticsReporter ( errors ) ;
219227
220- // Update TypeScript path mapping plugin with new configuration
221- pathsPlugin . update ( compilerOptions ) ;
228+ processor . process ( ) ;
229+ warnings . forEach ( ( warning ) => addWarning ( compilation , warning ) ) ;
230+ errors . forEach ( ( error ) => addError ( compilation , error ) ) ;
222231
223- // Create a Webpack-based TypeScript compiler host
224- const system = createWebpackSystem (
225- // Webpack lacks an InputFileSytem type definition with sync functions
226- compiler . inputFileSystem as InputFileSystemSync ,
227- normalizePath ( compiler . context ) ,
228- ) ;
229- const host = ts . createIncrementalCompilerHost ( compilerOptions , system ) ;
230-
231- // Setup source file caching and reuse cache from previous compilation if present
232- let cache = this . sourceFileCache ;
233- let changedFiles ;
234- if ( cache ) {
235- changedFiles = new Set < string > ( ) ;
236- for ( const changedFile of [ ...compiler . modifiedFiles , ...compiler . removedFiles ] ) {
237- const normalizedChangedFile = normalizePath ( changedFile ) ;
238- // Invalidate file dependencies
239- this . fileDependencies . delete ( normalizedChangedFile ) ;
240- // Invalidate existing cache
241- cache . invalidate ( normalizedChangedFile ) ;
242-
243- changedFiles . add ( normalizedChangedFile ) ;
244- }
245- } else {
246- // Initialize a new cache
247- cache = new SourceFileCache ( ) ;
248- // Only store cache if in watch mode
249- if ( this . watchMode ) {
250- this . sourceFileCache = cache ;
251- }
232+ state . ngccProcessor = processor ;
233+ }
234+
235+ // Setup and read TypeScript and Angular compiler configuration
236+ const { compilerOptions, rootNames, errors } = this . loadConfiguration ( ) ;
237+
238+ // Create diagnostics reporter and report configuration file errors
239+ const diagnosticsReporter = createDiagnosticsReporter ( compilation , ( diagnostic ) =>
240+ this . compilerCli . formatDiagnostics ( [ diagnostic ] ) ,
241+ ) ;
242+ diagnosticsReporter ( errors ) ;
243+
244+ // Update TypeScript path mapping plugin with new configuration
245+ state . pathsPlugin . update ( compilerOptions ) ;
246+
247+ // Create a Webpack-based TypeScript compiler host
248+ const system = createWebpackSystem (
249+ // Webpack lacks an InputFileSytem type definition with sync functions
250+ compiler . inputFileSystem as InputFileSystemSync ,
251+ normalizePath ( compiler . context ) ,
252+ ) ;
253+ const host = ts . createIncrementalCompilerHost ( compilerOptions , system ) ;
254+
255+ // Setup source file caching and reuse cache from previous compilation if present
256+ let cache = this . sourceFileCache ;
257+ let changedFiles ;
258+ if ( cache ) {
259+ changedFiles = new Set < string > ( ) ;
260+ for ( const changedFile of [ ...compiler . modifiedFiles , ...compiler . removedFiles ] ) {
261+ const normalizedChangedFile = normalizePath ( changedFile ) ;
262+ // Invalidate file dependencies
263+ this . fileDependencies . delete ( normalizedChangedFile ) ;
264+ // Invalidate existing cache
265+ cache . invalidate ( normalizedChangedFile ) ;
266+
267+ changedFiles . add ( normalizedChangedFile ) ;
252268 }
253- augmentHostWithCaching ( host , cache ) ;
269+ } else {
270+ // Initialize a new cache
271+ cache = new SourceFileCache ( ) ;
272+ // Only store cache if in watch mode
273+ if ( this . watchMode ) {
274+ this . sourceFileCache = cache ;
275+ }
276+ }
277+ augmentHostWithCaching ( host , cache ) ;
254278
255- const moduleResolutionCache = ts . createModuleResolutionCache (
256- host . getCurrentDirectory ( ) ,
257- host . getCanonicalFileName . bind ( host ) ,
258- compilerOptions ,
259- ) ;
279+ const moduleResolutionCache = ts . createModuleResolutionCache (
280+ host . getCurrentDirectory ( ) ,
281+ host . getCanonicalFileName . bind ( host ) ,
282+ compilerOptions ,
283+ ) ;
260284
261- // Setup source file dependency collection
262- augmentHostWithDependencyCollection ( host , this . fileDependencies , moduleResolutionCache ) ;
263-
264- // Setup on demand ngcc
265- augmentHostWithNgcc ( host , ngccProcessor , moduleResolutionCache ) ;
266-
267- // Setup resource loading
268- resourceLoader . update ( compilation , changedFiles ) ;
269- augmentHostWithResources ( host , resourceLoader , {
270- directTemplateLoading : this . pluginOptions . directTemplateLoading ,
271- inlineStyleFileExtension : this . pluginOptions . inlineStyleFileExtension ,
272- } ) ;
273-
274- // Setup source file adjustment options
275- augmentHostWithReplacements ( host , this . pluginOptions . fileReplacements , moduleResolutionCache ) ;
276- augmentHostWithSubstitutions ( host , this . pluginOptions . substitutions ) ;
277-
278- // Create the file emitter used by the webpack loader
279- const { fileEmitter, builder, internalFiles } = this . pluginOptions . jitMode
280- ? this . updateJitProgram ( compilerOptions , rootNames , host , diagnosticsReporter )
281- : this . updateAotProgram (
282- compilerOptions ,
283- rootNames ,
284- host ,
285- diagnosticsReporter ,
286- resourceLoader ,
287- ) ;
285+ // Setup source file dependency collection
286+ augmentHostWithDependencyCollection ( host , this . fileDependencies , moduleResolutionCache ) ;
288287
289- // Set of files used during the unused TypeScript file analysis
290- const currentUnused = new Set < string > ( ) ;
288+ // Setup on demand ngcc
289+ augmentHostWithNgcc ( host , state . ngccProcessor , moduleResolutionCache ) ;
291290
292- for ( const sourceFile of builder . getSourceFiles ( ) ) {
293- if ( internalFiles ?. has ( sourceFile ) ) {
294- continue ;
295- }
291+ // Setup resource loading
292+ state . resourceLoader . update ( compilation , changedFiles ) ;
293+ augmentHostWithResources ( host , state . resourceLoader , {
294+ directTemplateLoading : this . pluginOptions . directTemplateLoading ,
295+ inlineStyleFileExtension : this . pluginOptions . inlineStyleFileExtension ,
296+ } ) ;
296297
297- // Ensure all program files are considered part of the compilation and will be watched.
298- // Webpack does not normalize paths. Therefore, we need to normalize the path with FS seperators.
299- compilation . fileDependencies . add ( externalizePath ( sourceFile . fileName ) ) ;
298+ // Setup source file adjustment options
299+ augmentHostWithReplacements ( host , this . pluginOptions . fileReplacements , moduleResolutionCache ) ;
300+ augmentHostWithSubstitutions ( host , this . pluginOptions . substitutions ) ;
301+
302+ // Create the file emitter used by the webpack loader
303+ const { fileEmitter, builder, internalFiles } = this . pluginOptions . jitMode
304+ ? this . updateJitProgram ( compilerOptions , rootNames , host , diagnosticsReporter )
305+ : this . updateAotProgram (
306+ compilerOptions ,
307+ rootNames ,
308+ host ,
309+ diagnosticsReporter ,
310+ state . resourceLoader ,
311+ ) ;
300312
301- // Add all non-declaration files to the initial set of unused files. The set will be
302- // analyzed and pruned after all Webpack modules are finished building.
303- if ( ! sourceFile . isDeclarationFile ) {
304- currentUnused . add ( normalizePath ( sourceFile . fileName ) ) ;
305- }
313+ // Set of files used during the unused TypeScript file analysis
314+ const currentUnused = new Set < string > ( ) ;
315+
316+ for ( const sourceFile of builder . getSourceFiles ( ) ) {
317+ if ( internalFiles ?. has ( sourceFile ) ) {
318+ continue ;
306319 }
307320
308- compilation . hooks . finishModules . tapPromise ( PLUGIN_NAME , async ( modules ) => {
309- // Rebuild any remaining AOT required modules
310- await this . rebuildRequiredFiles ( modules , compilation , fileEmitter ) ;
321+ // Ensure all program files are considered part of the compilation and will be watched.
322+ // Webpack does not normalize paths. Therefore, we need to normalize the path with FS seperators.
323+ compilation . fileDependencies . add ( externalizePath ( sourceFile . fileName ) ) ;
311324
312- // Clear out the Webpack compilation to avoid an extra retaining reference
313- resourceLoader ?. clearParentCompilation ( ) ;
325+ // Add all non-declaration files to the initial set of unused files. The set will be
326+ // analyzed and pruned after all Webpack modules are finished building.
327+ if ( ! sourceFile . isDeclarationFile ) {
328+ currentUnused . add ( normalizePath ( sourceFile . fileName ) ) ;
329+ }
330+ }
314331
315- // Analyze program for unused files
316- if ( compilation . errors . length > 0 ) {
317- return ;
318- }
332+ compilation . hooks . finishModules . tapPromise ( PLUGIN_NAME , async ( modules ) => {
333+ // Rebuild any remaining AOT required modules
334+ await this . rebuildRequiredFiles ( modules , compilation , fileEmitter ) ;
319335
320- for ( const webpackModule of modules ) {
321- const resource = ( webpackModule as NormalModule ) . resource ;
322- if ( resource ) {
323- this . markResourceUsed ( normalizePath ( resource ) , currentUnused ) ;
324- }
325- }
336+ // Clear out the Webpack compilation to avoid an extra retaining reference
337+ state . resourceLoader ?. clearParentCompilation ( ) ;
326338
327- for ( const unused of currentUnused ) {
328- if ( previousUnused && previousUnused . has ( unused ) ) {
329- continue ;
330- }
331- addWarning (
332- compilation ,
333- ` ${ unused } is part of the TypeScript compilation but it's unused.\n` +
334- `Add only entry points to the 'files' or 'include' properties in your tsconfig.` ,
335- ) ;
339+ // Analyze program for unused files
340+ if ( compilation . errors . length > 0 ) {
341+ return ;
342+ }
343+
344+ for ( const webpackModule of modules ) {
345+ const resource = ( webpackModule as NormalModule ) . resource ;
346+ if ( resource ) {
347+ this . markResourceUsed ( normalizePath ( resource ) , currentUnused ) ;
336348 }
337- previousUnused = currentUnused ;
338- } ) ;
349+ }
339350
340- // Store file emitter for loader usage
341- emitRegistration . update ( fileEmitter ) ;
351+ for ( const unused of currentUnused ) {
352+ if ( state . previousUnused ?. has ( unused ) ) {
353+ continue ;
354+ }
355+ addWarning (
356+ compilation ,
357+ `${ unused } is part of the TypeScript compilation but it's unused.\n` +
358+ `Add only entry points to the 'files' or 'include' properties in your tsconfig.` ,
359+ ) ;
360+ }
361+ state . previousUnused = currentUnused ;
342362 } ) ;
363+
364+ // Store file emitter for loader usage
365+ emitRegistration . update ( fileEmitter ) ;
343366 }
344367
345368 private registerWithCompilation ( compilation : Compilation ) {
0 commit comments