From 46e9d0e8a646805ba9e48aac1bc95761f2668571 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Tue, 6 Apr 2021 14:01:18 -0400 Subject: [PATCH] feat(@ngtools/webpack): support multiple plugin instances per compilation This change allows multiple instances of the `AngularWebpackPlugin` to be present in a Webpack configuration. Each plugin instance should reference a different TypeScript configuration file (`tsconfig.json`) and the TypeScript configuration files should be setup to not include source files present in the other TypeScript configuration files. If files are included in more than one TypeScript configuration, the first plugin present in the Webpack configuration that can emit the file will be used. Closes: #5072 --- packages/ngtools/webpack/src/ivy/loader.ts | 8 ++--- packages/ngtools/webpack/src/ivy/plugin.ts | 14 +++++--- packages/ngtools/webpack/src/ivy/symbol.ts | 40 ++++++++++++++++++++++ 3 files changed, 54 insertions(+), 8 deletions(-) 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; + } + } + } +}