66 * found in the LICENSE file at https://angular.io/license
77 */
88
9- import {
10- CompilerHost ,
11- CompilerOptions ,
12- NgtscProgram ,
13- readConfiguration ,
14- } from '@angular/compiler-cli' ;
9+ import type { CompilerHost , CompilerOptions , NgtscProgram } from '@angular/compiler-cli' ;
10+ import { strict as assert } from 'assert' ;
1511import { createHash } from 'crypto' ;
1612import * as ts from 'typescript' ;
1713import type { Compilation , Compiler , Module , NormalModule } from 'webpack' ;
@@ -61,6 +57,7 @@ export interface AngularWebpackPluginOptions {
6157function initializeNgccProcessor (
6258 compiler : Compiler ,
6359 tsconfig : string ,
60+ compilerNgccModule : typeof import ( '@angular/compiler-cli/ngcc' ) | undefined ,
6461) : { processor : NgccProcessor ; errors : string [ ] ; warnings : string [ ] } {
6562 const { inputFileSystem, options : webpackOptions } = compiler ;
6663 const mainFields = webpackOptions . resolve ?. mainFields ?. flat ( ) ?? [ ] ;
@@ -73,7 +70,14 @@ function initializeNgccProcessor(
7370 extensions : [ '.json' ] ,
7471 useSyncFileSystemCalls : true ,
7572 } ) ;
73+
74+ // The compilerNgccModule field is guaranteed to be defined during a compilation
75+ // due to the `beforeCompile` hook. Usage of this property accessor prior to the
76+ // hook execution is an implementation error.
77+ assert . ok ( compilerNgccModule , `'@angular/compiler-cli/ngcc' used prior to Webpack compilation.` ) ;
78+
7679 const processor = new NgccProcessor (
80+ compilerNgccModule ,
7781 mainFields ,
7882 warnings ,
7983 errors ,
@@ -95,6 +99,8 @@ const compilationFileEmitters = new WeakMap<Compilation, FileEmitterCollection>(
9599
96100export class AngularWebpackPlugin {
97101 private readonly pluginOptions : AngularWebpackPluginOptions ;
102+ private compilerCliModule ?: typeof import ( '@angular/compiler-cli' ) ;
103+ private compilerNgccModule ?: typeof import ( '@angular/compiler-cli/ngcc' ) ;
98104 private watchMode ?: boolean ;
99105 private ngtscNextProgram ?: NgtscProgram ;
100106 private builder ?: ts . EmitAndSemanticDiagnosticsBuilderProgram ;
@@ -117,6 +123,15 @@ export class AngularWebpackPlugin {
117123 } ;
118124 }
119125
126+ private get compilerCli ( ) : typeof import ( '@angular/compiler-cli' ) {
127+ // The compilerCliModule field is guaranteed to be defined during a compilation
128+ // due to the `beforeCompile` hook. Usage of this property accessor prior to the
129+ // hook execution is an implementation error.
130+ assert . ok ( this . compilerCliModule , `'@angular/compiler-cli' used prior to Webpack compilation.` ) ;
131+
132+ return this . compilerCliModule ;
133+ }
134+
120135 get options ( ) : AngularWebpackPluginOptions {
121136 return this . pluginOptions ;
122137 }
@@ -153,6 +168,9 @@ export class AngularWebpackPlugin {
153168 } ) ;
154169 } ) ;
155170
171+ // Load the compiler-cli if not already available
172+ compiler . hooks . beforeCompile . tapPromise ( PLUGIN_NAME , ( ) => this . initializeCompilerCli ( ) ) ;
173+
156174 let ngccProcessor : NgccProcessor | undefined ;
157175 let resourceLoader : WebpackResourceLoader | undefined ;
158176 let previousUnused : Set < string > | undefined ;
@@ -172,6 +190,7 @@ export class AngularWebpackPlugin {
172190 const { processor, errors, warnings } = initializeNgccProcessor (
173191 compiler ,
174192 this . pluginOptions . tsconfig ,
193+ this . compilerNgccModule ,
175194 ) ;
176195
177196 processor . process ( ) ;
@@ -185,7 +204,9 @@ export class AngularWebpackPlugin {
185204 const { compilerOptions, rootNames, errors } = this . loadConfiguration ( ) ;
186205
187206 // Create diagnostics reporter and report configuration file errors
188- const diagnosticsReporter = createDiagnosticsReporter ( compilation ) ;
207+ const diagnosticsReporter = createDiagnosticsReporter ( compilation , ( diagnostic ) =>
208+ this . compilerCli . formatDiagnostics ( [ diagnostic ] ) ,
209+ ) ;
189210 diagnosticsReporter ( errors ) ;
190211
191212 // Update TypeScript path mapping plugin with new configuration
@@ -398,7 +419,10 @@ export class AngularWebpackPlugin {
398419 options : compilerOptions ,
399420 rootNames,
400421 errors,
401- } = readConfiguration ( this . pluginOptions . tsconfig , this . pluginOptions . compilerOptions ) ;
422+ } = this . compilerCli . readConfiguration (
423+ this . pluginOptions . tsconfig ,
424+ this . pluginOptions . compilerOptions ,
425+ ) ;
402426 compilerOptions . enableIvy = true ;
403427 compilerOptions . noEmitOnError = false ;
404428 compilerOptions . suppressOutputPathCheck = true ;
@@ -422,7 +446,7 @@ export class AngularWebpackPlugin {
422446 resourceLoader : WebpackResourceLoader ,
423447 ) {
424448 // Create the Angular specific program that contains the Angular compiler
425- const angularProgram = new NgtscProgram (
449+ const angularProgram = new this . compilerCli . NgtscProgram (
426450 rootNames ,
427451 compilerOptions ,
428452 host ,
@@ -561,8 +585,15 @@ export class AngularWebpackPlugin {
561585 }
562586 }
563587
588+ // Temporary workaround during transition to ESM-only @angular/compiler-cli
589+ // TODO_ESM: This workaround should be removed prior to the final release of v13
590+ // and replaced with only `this.compilerCli.OptimizeFor`.
591+ const OptimizeFor =
592+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
593+ ( this . compilerCli as any ) . OptimizeFor ??
594+ require ( '@angular/compiler-cli/src/ngtsc/typecheck/api' ) . OptimizeFor ;
595+
564596 // Collect new Angular diagnostics for files affected by changes
565- const { OptimizeFor } = require ( '@angular/compiler-cli/src/ngtsc/typecheck/api' ) ;
566597 const optimizeDiagnosticsFor =
567598 affectedFiles . size <= DIAGNOSTICS_AFFECTED_THRESHOLD
568599 ? OptimizeFor . SingleFile
@@ -628,7 +659,7 @@ export class AngularWebpackPlugin {
628659 ] ;
629660 diagnosticsReporter ( diagnostics ) ;
630661
631- const transformers = createJitTransformers ( builder , this . pluginOptions ) ;
662+ const transformers = createJitTransformers ( builder , this . compilerCli , this . pluginOptions ) ;
632663
633664 return {
634665 fileEmitter : this . createFileEmitter ( builder , transformers , ( ) => [ ] ) ,
@@ -687,4 +718,37 @@ export class AngularWebpackPlugin {
687718 return { content, map, dependencies, hash } ;
688719 } ;
689720 }
721+
722+ private async initializeCompilerCli ( ) : Promise < void > {
723+ if ( this . compilerCliModule ) {
724+ return ;
725+ }
726+
727+ // This uses a dynamic import to load `@angular/compiler-cli` which may be ESM.
728+ // CommonJS code can load ESM code via a dynamic import. Unfortunately, TypeScript
729+ // will currently, unconditionally downlevel dynamic import into a require call.
730+ // require calls cannot load ESM code and will result in a runtime error. To workaround
731+ // this, a Function constructor is used to prevent TypeScript from changing the dynamic import.
732+ // Once TypeScript provides support for keeping the dynamic import this workaround can
733+ // be dropped.
734+ const compilerCliModule = await new Function ( `return import('@angular/compiler-cli');` ) ( ) ;
735+ let compilerNgccModule ;
736+ try {
737+ compilerNgccModule = await new Function ( `return import('@angular/compiler-cli/ngcc');` ) ( ) ;
738+ } catch {
739+ // If the `exports` field entry is not present then try the file directly.
740+ // TODO_ESM: This try/catch can be removed once the `exports` field is present in `@angular/compiler-cli`
741+ compilerNgccModule = await new Function (
742+ `return import('@angular/compiler-cli/ngcc/index.js');` ,
743+ ) ( ) ;
744+ }
745+ // If it is not ESM then the functions needed will be stored in the `default` property.
746+ // TODO_ESM: This conditional can be removed when `@angular/compiler-cli` is ESM only.
747+ this . compilerCliModule = compilerCliModule . readConfiguration
748+ ? compilerCliModule
749+ : compilerCliModule . default ;
750+ this . compilerNgccModule = compilerNgccModule . process
751+ ? compilerNgccModule
752+ : compilerNgccModule . default ;
753+ }
690754}
0 commit comments