diff --git a/packages/ngtools/webpack/src/ivy/loader.ts b/packages/ngtools/webpack/src/ivy/loader.ts index 5b4a7a513f04..a648613f7fe5 100644 --- a/packages/ngtools/webpack/src/ivy/loader.ts +++ b/packages/ngtools/webpack/src/ivy/loader.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ import * as path from 'path'; -import { AngularPluginSymbol, FileEmitter } from './symbol'; +import { AngularPluginSymbol, FileEmitterCollection } from './symbol'; export function angularWebpackLoader( this: import('webpack').loader.LoaderContext, @@ -20,8 +20,8 @@ export function angularWebpackLoader( throw new Error('Invalid webpack version'); } - const emitFile = this._compilation[AngularPluginSymbol] as FileEmitter; - if (typeof emitFile !== 'function') { + const fileEmitter = this._compilation[AngularPluginSymbol] as FileEmitterCollection; + if (typeof fileEmitter !== 'object') { if (this.resourcePath.endsWith('.js')) { // Passthrough for JS files when no plugin is used this.callback(undefined, content, map); @@ -34,7 +34,7 @@ export function angularWebpackLoader( return; } - emitFile(this.resourcePath) + fileEmitter.emit(this.resourcePath) .then((result) => { if (!result) { if (this.resourcePath.endsWith('.js')) { diff --git a/packages/ngtools/webpack/src/ivy/plugin.ts b/packages/ngtools/webpack/src/ivy/plugin.ts index 83d02beed92f..affdc28e8b6a 100644 --- a/packages/ngtools/webpack/src/ivy/plugin.ts +++ b/packages/ngtools/webpack/src/ivy/plugin.ts @@ -27,7 +27,7 @@ import { augmentProgramWithVersioning, } from './host'; import { externalizePath, normalizePath } from './paths'; -import { AngularPluginSymbol, EmitFileResult, FileEmitter } from './symbol'; +import { AngularPluginSymbol, EmitFileResult, FileEmitter, FileEmitterCollection } from './symbol'; import { InputFileSystemSync, createWebpackSystem } from './system'; import { createAotTransformers, createJitTransformers, mergeTransformers } from './transformation'; @@ -54,7 +54,7 @@ interface WebpackCompilation extends compilation.Compilation { // tslint:disable-next-line: no-any compilationDependencies: { add(item: string): any }; rebuildModule(module: compilation.Module, callback: () => void): void; - [AngularPluginSymbol]: FileEmitter; + [AngularPluginSymbol]: FileEmitterCollection; } function initializeNgccProcessor( @@ -156,6 +156,12 @@ export class AngularWebpackPlugin { compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (thisCompilation) => { const compilation = thisCompilation as WebpackCompilation; + // Register plugin to ensure deterministic emit order in multi-plugin usage + if (!compilation[AngularPluginSymbol]) { + compilation[AngularPluginSymbol] = new FileEmitterCollection(); + } + const emitRegistration = compilation[AngularPluginSymbol].register(); + // Store watch mode; assume true if not present (webpack < 4.23.0) this.watchMode = compiler.watchMode ?? true; @@ -294,7 +300,7 @@ export class AngularWebpackPlugin { }); // Store file emitter for loader usage - compilation[AngularPluginSymbol] = fileEmitter; + emitRegistration.update(fileEmitter); }); } @@ -538,7 +544,7 @@ export class AngularWebpackPlugin { // tslint:disable-next-line: no-any (angularProgram as any).reuseTsProgram = // tslint:disable-next-line: no-any - angularCompiler?.getNextProgram() || (angularCompiler as any)?.getCurrentProgram(); + angularCompiler.getNextProgram?.() || (angularCompiler as any).getCurrentProgram?.(); return this.createFileEmitter( builder, diff --git a/packages/ngtools/webpack/src/ivy/symbol.ts b/packages/ngtools/webpack/src/ivy/symbol.ts index 2b1f171f410e..451946ff52ca 100644 --- a/packages/ngtools/webpack/src/ivy/symbol.ts +++ b/packages/ngtools/webpack/src/ivy/symbol.ts @@ -15,3 +15,43 @@ export interface EmitFileResult { } export type FileEmitter = (file: string) => Promise; + +export class FileEmitterRegistration { + #fileEmitter?: FileEmitter; + + update(emitter: FileEmitter): void { + this.#fileEmitter = emitter; + } + + emit(file: string): Promise { + if (!this.#fileEmitter) { + throw new Error('Emit attempted before Angular Webpack plugin initialization.'); + } + + return this.#fileEmitter(file); + } +} + +export class FileEmitterCollection { + #registrations: FileEmitterRegistration[] = []; + + register(): FileEmitterRegistration { + const registration = new FileEmitterRegistration(); + this.#registrations.push(registration); + + return registration; + } + + async emit(file: string): Promise { + if (this.#registrations.length === 1) { + return this.#registrations[0].emit(file); + } + + for (const registration of this.#registrations) { + const result = await registration.emit(file); + if (result) { + return result; + } + } + } +}