Skip to content

Commit 3e6324f

Browse files
clydinalan-agius4
authored andcommitted
refactor(@angular-devkit/build-angular): support an ESM-only @angular/compiler-cli package
This uses a dynamic import to load `@angular/compiler-cli` which may be ESM. CommonJS code can load ESM code via a dynamic import. Unfortunately, TypeScript will currently, unconditionally downlevel dynamic import into a require call. require calls cannot load ESM code and will result in a runtime error. To workaround this, a Function constructor is used to prevent TypeScript from changing the dynamic import. Once TypeScript provides support for keeping the dynamic import this workaround can be dropped and replaced with a standard dynamic import.
1 parent b877710 commit 3e6324f

File tree

4 files changed

+41
-16
lines changed

4 files changed

+41
-16
lines changed

packages/angular_devkit/build_angular/src/utils/i18n-options.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ export async function configureI18nBuild<T extends BrowserBuilderSchema | Server
161161
}
162162

163163
const buildOptions = { ...options };
164-
const tsConfig = readTsconfig(buildOptions.tsConfig, context.workspaceRoot);
164+
const tsConfig = await readTsconfig(buildOptions.tsConfig, context.workspaceRoot);
165165
const metadata = await context.getProjectMetadata(context.target);
166166
const i18n = createI18nOptions(metadata, buildOptions.localize);
167167

packages/angular_devkit/build_angular/src/utils/read-tsconfig.ts

+18-6
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,28 @@ import * as path from 'path';
1616
* @param workspaceRoot - workspaceRoot root location when provided
1717
* it will resolve 'tsconfigPath' from this path.
1818
*/
19-
export function readTsconfig(tsconfigPath: string, workspaceRoot?: string): ParsedConfiguration {
19+
export async function readTsconfig(
20+
tsconfigPath: string,
21+
workspaceRoot?: string,
22+
): Promise<ParsedConfiguration> {
2023
const tsConfigFullPath = workspaceRoot ? path.resolve(workspaceRoot, tsconfigPath) : tsconfigPath;
2124

22-
// We use 'ng' instead of 'ts' here because 'ts' is not aware of 'angularCompilerOptions'
23-
// and will not merged them if they are at un upper level tsconfig file when using `extends`.
24-
const ng: typeof import('@angular/compiler-cli') = require('@angular/compiler-cli');
25+
// This uses a dynamic import to load `@angular/compiler-cli` which may be ESM.
26+
// CommonJS code can load ESM code via a dynamic import. Unfortunately, TypeScript
27+
// will currently, unconditionally downlevel dynamic import into a require call.
28+
// require calls cannot load ESM code and will result in a runtime error. To workaround
29+
// this, a Function constructor is used to prevent TypeScript from changing the dynamic import.
30+
// Once TypeScript provides support for keeping the dynamic import this workaround can
31+
// be dropped.
32+
const compilerCliModule = await new Function(`return import('@angular/compiler-cli');`)();
33+
// If it is not ESM then the functions needed will be stored in the `default` property.
34+
const { formatDiagnostics, readConfiguration } = (
35+
compilerCliModule.readConfiguration ? compilerCliModule : compilerCliModule.default
36+
) as typeof import('@angular/compiler-cli');
2537

26-
const configResult = ng.readConfiguration(tsConfigFullPath);
38+
const configResult = readConfiguration(tsConfigFullPath);
2739
if (configResult.errors && configResult.errors.length) {
28-
throw new Error(ng.formatDiagnostics(configResult.errors));
40+
throw new Error(formatDiagnostics(configResult.errors));
2941
}
3042

3143
return configResult;

packages/angular_devkit/build_angular/src/utils/webpack-browser-config.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export async function generateWebpackConfig(
4040
}
4141

4242
const tsConfigPath = path.resolve(workspaceRoot, options.tsConfig);
43-
const tsConfig = readTsconfig(tsConfigPath);
43+
const tsConfig = await readTsconfig(tsConfigPath);
4444

4545
const ts = await import('typescript');
4646
const scriptTarget = tsConfig.options.target || ts.ScriptTarget.ES5;

packages/angular_devkit/build_angular/src/webpack/configs/common.ts

+21-8
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,6 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {
10-
GLOBAL_DEFS_FOR_TERSER,
11-
GLOBAL_DEFS_FOR_TERSER_WITH_AOT,
12-
VERSION as NG_VERSION,
13-
} from '@angular/compiler-cli';
149
import CopyWebpackPlugin, { ObjectPattern } from 'copy-webpack-plugin';
1510
import { createHash } from 'crypto';
1611
import { createWriteStream, existsSync, promises as fsPromises } from 'fs';
@@ -42,7 +37,7 @@ import { JavaScriptOptimizerPlugin } from '../plugins/javascript-optimizer-plugi
4237
import { getOutputHashFormat, getWatchOptions, normalizeExtraEntryPoints } from '../utils/helpers';
4338

4439
// eslint-disable-next-line max-lines-per-function
45-
export function getCommonConfig(wco: WebpackConfigOptions): Configuration {
40+
export async function getCommonConfig(wco: WebpackConfigOptions): Promise<Configuration> {
4641
const { root, projectRoot, buildOptions, tsConfig } = wco;
4742
const {
4843
platform = 'browser',
@@ -54,6 +49,23 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration {
5449
const extraRules: RuleSetRule[] = [];
5550
const entryPoints: { [key: string]: [string, ...string[]] } = {};
5651

52+
// This uses a dynamic import to load `@angular/compiler-cli` which may be ESM.
53+
// CommonJS code can load ESM code via a dynamic import. Unfortunately, TypeScript
54+
// will currently, unconditionally downlevel dynamic import into a require call.
55+
// require calls cannot load ESM code and will result in a runtime error. To workaround
56+
// this, a Function constructor is used to prevent TypeScript from changing the dynamic import.
57+
// Once TypeScript provides support for keeping the dynamic import this workaround can
58+
// be dropped.
59+
const compilerCliModule = await new Function(`return import('@angular/compiler-cli');`)();
60+
// If it is not ESM then the values needed will be stored in the `default` property.
61+
const {
62+
GLOBAL_DEFS_FOR_TERSER,
63+
GLOBAL_DEFS_FOR_TERSER_WITH_AOT,
64+
VERSION: NG_VERSION,
65+
} = (
66+
compilerCliModule.GLOBAL_DEFS_FOR_TERSER ? compilerCliModule : compilerCliModule.default
67+
) as typeof import('@angular/compiler-cli');
68+
5769
// determine hashing format
5870
const hashFormat = getOutputHashFormat(buildOptions.outputHashing || 'none');
5971
const buildBrowserFeatures = new BuildBrowserFeatures(projectRoot);
@@ -393,7 +405,7 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration {
393405
infrastructureLogging: {
394406
level: buildOptions.verbose ? 'verbose' : 'error',
395407
},
396-
cache: getCacheSettings(wco, buildBrowserFeatures.supportedBrowsers),
408+
cache: getCacheSettings(wco, buildBrowserFeatures.supportedBrowsers, NG_VERSION.full),
397409
optimization: {
398410
minimizer: extraMinimizers,
399411
moduleIds: 'deterministic',
@@ -417,6 +429,7 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration {
417429
function getCacheSettings(
418430
wco: WebpackConfigOptions,
419431
supportedBrowsers: string[],
432+
angularVersion: string,
420433
): WebpackOptionsNormalized['cache'] {
421434
if (persistentBuildCacheEnabled) {
422435
const packageVersion = require('../../../package.json').version;
@@ -429,7 +442,7 @@ function getCacheSettings(
429442
// dynamic and shared among different build types: test, build and serve.
430443
// None of which are "named".
431444
name: createHash('sha1')
432-
.update(NG_VERSION.full)
445+
.update(angularVersion)
433446
.update(packageVersion)
434447
.update(wco.projectRoot)
435448
.update(JSON.stringify(wco.tsConfig))

0 commit comments

Comments
 (0)