From 93a871be4c064b2cd88de91b900ce7cb9bbfc7e5 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:34:06 -0400 Subject: [PATCH] refactor(@angular/build): decouple compiler options transformation and remove worker options IPC Move the TypeScript compiler options transformation logic from compiler-plugin.ts into transformCompilerOptions in the compilation layer. This replaces numeric literal values with strongly-typed TypeScript enums and encapsulates options normalization within the compilation classes. Replace the optionsChannel, optionsSignal, and synchronous Atomics barrier between the main thread and the compilation worker thread with serializable CompilerOptionOverrides passed during initialization. Any compiler options transformation warnings are now returned directly in AngularCompilationResult.warnings rather than mutating a main-thread array. --- .../compilation/angular-compilation.ts | 4 +- .../compilation/angular-compilation_spec.ts | 180 +++++++++++++++++- .../angular/compilation/aot-compilation.ts | 14 +- .../angular/compilation/compiler-options.ts | 136 +++++++++++++ .../src/tools/angular/compilation/index.ts | 2 +- .../angular/compilation/jit-compilation.ts | 15 +- .../angular/compilation/noop-compilation.ts | 38 +++- .../compilation/parallel-compilation.ts | 26 +-- .../angular/compilation/parallel-worker.ts | 21 +- .../tools/esbuild/angular/compiler-plugin.ts | 128 ++----------- 10 files changed, 390 insertions(+), 174 deletions(-) create mode 100644 packages/angular/build/src/tools/angular/compilation/compiler-options.ts diff --git a/packages/angular/build/src/tools/angular/compilation/angular-compilation.ts b/packages/angular/build/src/tools/angular/compilation/angular-compilation.ts index 308b0d255df3..ea1fe00d04aa 100644 --- a/packages/angular/build/src/tools/angular/compilation/angular-compilation.ts +++ b/packages/angular/build/src/tools/angular/compilation/angular-compilation.ts @@ -10,6 +10,7 @@ import type * as ng from '@angular/compiler-cli'; import type { PartialMessage } from 'esbuild'; import { profileSync } from '../../esbuild/profiling'; import type { AngularHostOptions } from '../angular-host'; +import type { CompilerOptionOverrides } from './compiler-options'; export interface EmitFileResult { filename: string; @@ -37,6 +38,7 @@ export interface AngularCompilationResult { externalStylesheets?: ReadonlyMap; templateUpdates?: ReadonlyMap; componentResourcesDependencies?: ReadonlyMap; + warnings?: readonly PartialMessage[]; } export enum DiagnosticModes { @@ -82,7 +84,7 @@ export abstract class AngularCompilation { abstract initialize( tsconfig: string, hostOptions: AngularHostOptions, - compilerOptionsTransformer?: (compilerOptions: ng.CompilerOptions) => ng.CompilerOptions, + compilerOptionOverrides?: CompilerOptionOverrides, ): Promise; emitAffectedFiles(): Iterable | Promise> { diff --git a/packages/angular/build/src/tools/angular/compilation/angular-compilation_spec.ts b/packages/angular/build/src/tools/angular/compilation/angular-compilation_spec.ts index 66d21cb52f51..ad28c31bcce5 100644 --- a/packages/angular/build/src/tools/angular/compilation/angular-compilation_spec.ts +++ b/packages/angular/build/src/tools/angular/compilation/angular-compilation_spec.ts @@ -8,12 +8,13 @@ import ts from 'typescript'; import type { AngularHostOptions } from '../angular-host'; +import { transformCompilerOptions } from './compiler-options'; +import { TypeScriptCompilation } from './typescript-compilation'; import { AngularCompilation, AngularCompilationResult, DiagnosticModes, NoopCompilation, - TypeScriptCompilation, createAngularCompilation, } from './index'; @@ -62,13 +63,24 @@ describe('AngularCompilation', () => { it('initializes with empty referencedFiles and compiler options', async () => { const compilation = new NoopCompilation(); const mockHostOptions = {} as AngularHostOptions; - const result = await compilation.initialize('tsconfig.json', mockHostOptions, (opts) => ({ - ...opts, - customOption: true, - })); + const result = await compilation.initialize('tsconfig.json', mockHostOptions); expect(result.referencedFiles).toEqual([]); - expect(result.compilerOptions['customOption']).toBe(true); + expect(result.compilerOptions).toBeDefined(); + }); + + it('initializes with CompilerOptionOverrides object', async () => { + const compilation = new NoopCompilation(); + const mockHostOptions = {} as AngularHostOptions; + const result = await compilation.initialize('tsconfig.json', mockHostOptions, { + sourcemap: true, + enableHmr: true, + }); + + expect(result.referencedFiles).toEqual([]); + expect(result.compilerOptions.inlineSources).toBe(true); + expect(result.compilerOptions.inlineSourceMap).toBe(true); + expect(result.compilerOptions['_enableHmr']).toBe(true); }); it('throws when calling emitAffectedFiles', () => { @@ -184,4 +196,160 @@ describe('AngularCompilation', () => { ); }); }); + + describe('transformCompilerOptions', () => { + it('does not mutate the input compiler options object', () => { + const originalOptions: ts.CompilerOptions = { + target: ts.ScriptTarget.ES2020, + module: ts.ModuleKind.CommonJS, + }; + const originalCopy = { ...originalOptions }; + + transformCompilerOptions(ts, originalOptions); + + expect(originalOptions).toEqual(originalCopy); + }); + + it('sets target to ES2022 and useDefineForClassFields to false when target is undefined', () => { + const { compilerOptions, warnings } = transformCompilerOptions( + ts, + { module: ts.ModuleKind.ES2022 }, + undefined, + 'tsconfig.json', + ); + + expect(compilerOptions.target).toBe(ts.ScriptTarget.ES2022); + expect(compilerOptions.useDefineForClassFields).toBe(false); + expect(warnings.length).toBe(1); + expect(warnings[0].text).toContain( + "TypeScript compiler options 'target' and 'useDefineForClassFields'", + ); + expect(warnings[0].location?.file).toBe('tsconfig.json'); + }); + + it('preserves existing useDefineForClassFields if target < ES2022', () => { + const { compilerOptions, warnings } = transformCompilerOptions( + ts, + { + target: ts.ScriptTarget.ES2020, + useDefineForClassFields: true, + module: ts.ModuleKind.ES2022, + }, + undefined, + 'tsconfig.json', + ); + + expect(compilerOptions.target).toBe(ts.ScriptTarget.ES2022); + expect(compilerOptions.useDefineForClassFields).toBe(true); + expect(warnings.length).toBe(1); + }); + + it('sets compilationMode to full and warns when compilationMode is partial', () => { + const { compilerOptions, warnings } = transformCompilerOptions(ts, { + target: ts.ScriptTarget.ES2022, + module: ts.ModuleKind.ES2022, + compilationMode: 'partial', + }); + + expect(compilerOptions.compilationMode).toBe('full'); + expect(warnings.length).toBe(1); + expect(warnings[0].text).toContain('Angular partial compilation mode is not supported'); + }); + + it('configures incremental and tsBuildInfoFile when cachePath is provided', () => { + const { compilerOptions } = transformCompilerOptions( + ts, + { target: ts.ScriptTarget.ES2022 }, + { cachePath: '/tmp/cache' }, + ); + + expect(compilerOptions.incremental).toBe(true); + expect(compilerOptions.tsBuildInfoFile).toContain('.tsbuildinfo'); + }); + + it('sets incremental to false when cachePath is not provided or incremental is false', () => { + const { compilerOptions: opt1 } = transformCompilerOptions( + ts, + { target: ts.ScriptTarget.ES2022 }, + undefined, + ); + expect(opt1.incremental).toBe(false); + + const { compilerOptions: opt2 } = transformCompilerOptions( + ts, + { target: ts.ScriptTarget.ES2022, incremental: false }, + { cachePath: '/tmp/cache' }, + ); + expect(opt2.incremental).toBe(false); + }); + + it('sets module to ES2022 and warns when module < ES2015', () => { + const { compilerOptions, warnings } = transformCompilerOptions(ts, { + target: ts.ScriptTarget.ES2022, + module: ts.ModuleKind.CommonJS, + }); + + expect(compilerOptions.module).toBe(ts.ModuleKind.ES2022); + expect(warnings.length).toBe(1); + expect(warnings[0].text).toContain( + "TypeScript compiler options 'module' values 'CommonJS', 'UMD'", + ); + }); + + it('warns when isolatedModules is enabled with emitDecoratorMetadata', () => { + const { warnings } = transformCompilerOptions(ts, { + target: ts.ScriptTarget.ES2022, + module: ts.ModuleKind.ES2022, + isolatedModules: true, + emitDecoratorMetadata: true, + }); + + expect(warnings.length).toBe(1); + expect(warnings[0].text).toContain( + "TypeScript compiler option 'isolatedModules' may prevent", + ); + }); + + it('synchronizes customConditions when moduleResolution is Bundler or module is Preserve', () => { + const { compilerOptions: bundlerOptions } = transformCompilerOptions( + ts, + { target: ts.ScriptTarget.ES2022, moduleResolution: ts.ModuleResolutionKind.Bundler }, + { customConditions: ['development'] }, + ); + expect(bundlerOptions.customConditions).toEqual(['development']); + + const { compilerOptions: preserveOptions } = transformCompilerOptions( + ts, + { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.Preserve }, + { customConditions: ['development'] }, + ); + expect(preserveOptions.customConditions).toEqual(['development']); + }); + + it('applies override options correctly', () => { + const { compilerOptions } = transformCompilerOptions( + ts, + { target: ts.ScriptTarget.ES2022, isolatedModules: true }, + { + sourcemap: true, + preserveSymlinks: true, + externalRuntimeStyles: true, + enableHmr: true, + instrumentForCoverage: true, + includeTestMetadata: true, + }, + ); + + expect(compilerOptions.inlineSources).toBe(true); + expect(compilerOptions.inlineSourceMap).toBe(true); + expect(compilerOptions.preserveSymlinks).toBe(true); + expect(compilerOptions.externalRuntimeStyles).toBe(true); + expect(compilerOptions['_enableHmr']).toBe(true); + expect(compilerOptions['_useTypeScriptTranspilation']).toBe(true); + expect(compilerOptions.supportTestBed).toBe(true); + expect(compilerOptions.supportJitMode).toBe(true); + expect(compilerOptions.noEmitOnError).toBe(false); + expect(compilerOptions.composite).toBe(false); + }); + }); }); diff --git a/packages/angular/build/src/tools/angular/compilation/aot-compilation.ts b/packages/angular/build/src/tools/angular/compilation/aot-compilation.ts index 99f6da756930..ec68415e62fb 100644 --- a/packages/angular/build/src/tools/angular/compilation/aot-compilation.ts +++ b/packages/angular/build/src/tools/angular/compilation/aot-compilation.ts @@ -7,6 +7,7 @@ */ import type * as ng from '@angular/compiler-cli'; +import type { PartialMessage } from 'esbuild'; import assert from 'node:assert'; import { relative } from 'node:path'; import ts from 'typescript'; @@ -26,6 +27,7 @@ import { DiagnosticModes, EmitFileResult, } from './angular-compilation'; +import { CompilerOptionOverrides, transformCompilerOptions } from './compiler-options'; import { collectHmrCandidates } from './hmr-candidates'; import { TypeScriptCompilation } from './typescript-compilation'; import { printSourceFileWithMap } from './typescript-printer'; @@ -64,7 +66,7 @@ export class AotCompilation extends TypeScriptCompilation { async initialize( tsconfig: string, hostOptions: AngularHostOptions, - compilerOptionsTransformer?: (compilerOptions: ng.CompilerOptions) => ng.CompilerOptions, + compilerOptionOverrides?: CompilerOptionOverrides, ): Promise { // Dynamically load the Angular compiler CLI package const { NgtscProgram, OptimizeFor } = await AngularCompilation.loadCompilerCli(); @@ -75,8 +77,13 @@ export class AotCompilation extends TypeScriptCompilation { rootNames, errors: configurationDiagnostics, } = await this.loadConfiguration(tsconfig); - const compilerOptions = - compilerOptionsTransformer?.(originalCompilerOptions) ?? originalCompilerOptions; + + const { compilerOptions, warnings } = transformCompilerOptions( + ts, + originalCompilerOptions, + compilerOptionOverrides, + tsconfig, + ); const useTypeScriptTranspilation = (compilerOptions['_useTypeScriptTranspilation'] as boolean | undefined) ?? @@ -245,6 +252,7 @@ export class AotCompilation extends TypeScriptCompilation { externalStylesheets: hostOptions.externalStylesheets, templateUpdates, componentResourcesDependencies, + warnings, }; } diff --git a/packages/angular/build/src/tools/angular/compilation/compiler-options.ts b/packages/angular/build/src/tools/angular/compilation/compiler-options.ts new file mode 100644 index 000000000000..bb8c494f2213 --- /dev/null +++ b/packages/angular/build/src/tools/angular/compilation/compiler-options.ts @@ -0,0 +1,136 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import type * as ng from '@angular/compiler-cli'; +import type { PartialMessage } from 'esbuild'; +import * as path from 'node:path'; +import type ts from 'typescript'; + +export interface CompilerOptionOverrides { + sourcemap?: boolean; + preserveSymlinks?: boolean; + cachePath?: string; + externalRuntimeStyles?: boolean; + enableHmr?: boolean; + instrumentForCoverage?: boolean; + includeTestMetadata?: boolean; + customConditions?: string[]; +} + +export function transformCompilerOptions( + typeScript: typeof ts, + baseCompilerOptions: ng.CompilerOptions, + overrides?: CompilerOptionOverrides, + tsconfig?: string, +): { compilerOptions: ng.CompilerOptions; warnings: PartialMessage[] } { + const compilerOptions = { ...baseCompilerOptions }; + const warnings: PartialMessage[] = []; + + if ( + compilerOptions.target === undefined || + compilerOptions.target < typeScript.ScriptTarget.ES2022 + ) { + // If 'useDefineForClassFields' is already defined in the users project leave the value as is. + // Otherwise fallback to false due to https://github.com/microsoft/TypeScript/issues/45995 + // which breaks the deprecated `@Effects` NGRX decorator and potentially other existing code as well. + compilerOptions.target = typeScript.ScriptTarget.ES2022; + compilerOptions.useDefineForClassFields ??= false; + + warnings.push({ + text: + `TypeScript compiler options 'target' and 'useDefineForClassFields' are set to 'ES2022' and ` + + `'false' respectively by the Angular CLI.`, + location: tsconfig ? { file: tsconfig } : null, + notes: [ + { + text: + 'To control ECMA version and features use the Browserslist configuration. ' + + 'For more information, see https://angular.dev/tools/cli/build#configuring-browser-compatibility', + }, + ], + }); + } + + if (compilerOptions.compilationMode === 'partial') { + warnings.push({ + text: 'Angular partial compilation mode is not supported when building applications.', + location: null, + notes: [{ text: 'Full compilation mode will be used instead.' }], + }); + compilerOptions.compilationMode = 'full'; + } + + // Enable incremental compilation by default if caching is enabled and incremental is not explicitly disabled + if (compilerOptions.incremental !== false && overrides?.cachePath) { + compilerOptions.incremental = true; + // Set the build info file location to the configured cache directory + compilerOptions.tsBuildInfoFile = path.join(overrides.cachePath, '.tsbuildinfo'); + } else { + compilerOptions.incremental = false; + } + + if ( + compilerOptions.module === undefined || + compilerOptions.module < typeScript.ModuleKind.ES2015 + ) { + compilerOptions.module = typeScript.ModuleKind.ES2022; + warnings.push({ + text: `TypeScript compiler options 'module' values 'CommonJS', 'UMD', 'System' and 'AMD' are not supported.`, + location: null, + notes: [{ text: `The 'module' option will be set to 'ES2022' instead.` }], + }); + } + + if (compilerOptions.isolatedModules && compilerOptions.emitDecoratorMetadata) { + warnings.push({ + text: `TypeScript compiler option 'isolatedModules' may prevent the 'emitDecoratorMetadata' option from emitting all metadata.`, + location: null, + notes: [ + { + text: + `The 'emitDecoratorMetadata' option is not required by Angular` + + 'and can be removed if not explictly required by the project.', + }, + ], + }); + } + + // Synchronize custom resolve conditions. + // Set if using the supported bundler resolution mode (bundler is the default in new projects) + if ( + compilerOptions.moduleResolution === typeScript.ModuleResolutionKind.Bundler || + compilerOptions.module === typeScript.ModuleKind.Preserve + ) { + compilerOptions.customConditions = overrides?.customConditions; + } + + return { + compilerOptions: { + ...compilerOptions, + noEmitOnError: false, + composite: false, + inlineSources: !!overrides?.sourcemap, + inlineSourceMap: !!overrides?.sourcemap, + sourceMap: undefined, + mapRoot: undefined, + sourceRoot: undefined, + preserveSymlinks: overrides?.preserveSymlinks, + externalRuntimeStyles: overrides?.externalRuntimeStyles, + _enableHmr: !!overrides?.enableHmr, + // TypeScript transpilation is forced if: + // - isolatedModules is disabled (TS needs full module types to emit JS). + // - Karma code coverage is active (the coverage instrumentation transformer is Babel-based + // and cannot parse raw TypeScript code; Vitest handles coverage instrumentation downstream). + _useTypeScriptTranspilation: + !compilerOptions.isolatedModules || !!overrides?.instrumentForCoverage, + supportTestBed: !!overrides?.includeTestMetadata, + supportJitMode: !!overrides?.includeTestMetadata, + }, + warnings, + }; +} diff --git a/packages/angular/build/src/tools/angular/compilation/index.ts b/packages/angular/build/src/tools/angular/compilation/index.ts index 736adea60682..39391bcfb562 100644 --- a/packages/angular/build/src/tools/angular/compilation/index.ts +++ b/packages/angular/build/src/tools/angular/compilation/index.ts @@ -14,6 +14,6 @@ export { type EmitFileResult, type FileTransformResult, } from './angular-compilation'; +export type { CompilerOptionOverrides } from './compiler-options'; export { createAngularCompilation, type AngularCompilationMode } from './factory'; export { NoopCompilation } from './noop-compilation'; -export { TypeScriptCompilation } from './typescript-compilation'; diff --git a/packages/angular/build/src/tools/angular/compilation/jit-compilation.ts b/packages/angular/build/src/tools/angular/compilation/jit-compilation.ts index e4e371a05df8..b54f66701c6c 100644 --- a/packages/angular/build/src/tools/angular/compilation/jit-compilation.ts +++ b/packages/angular/build/src/tools/angular/compilation/jit-compilation.ts @@ -7,6 +7,7 @@ */ import type * as ng from '@angular/compiler-cli'; +import type { PartialMessage } from 'esbuild'; import assert from 'node:assert'; import ts from 'typescript'; import { profileSync } from '../../esbuild/profiling'; @@ -20,6 +21,7 @@ import { DiagnosticModes, EmitFileResult, } from './angular-compilation'; +import { CompilerOptionOverrides, transformCompilerOptions } from './compiler-options'; import { TypeScriptCompilation } from './typescript-compilation'; class JitCompilationState { @@ -42,7 +44,7 @@ export class JitCompilation extends TypeScriptCompilation { async initialize( tsconfig: string, hostOptions: AngularHostOptions, - compilerOptionsTransformer?: (compilerOptions: ng.CompilerOptions) => ng.CompilerOptions, + compilerOptionOverrides?: CompilerOptionOverrides, ): Promise { // Dynamically load the Angular compiler CLI package const { constructorParametersDownlevelTransform } = @@ -54,8 +56,13 @@ export class JitCompilation extends TypeScriptCompilation { rootNames, errors: configurationDiagnostics, } = await this.loadConfiguration(tsconfig); - const compilerOptions = - compilerOptionsTransformer?.(originalCompilerOptions) ?? originalCompilerOptions; + + const { compilerOptions, warnings } = transformCompilerOptions( + ts, + originalCompilerOptions, + compilerOptionOverrides, + tsconfig, + ); if (hostOptions.modifiedFiles) { this.invalidateFiles(hostOptions.modifiedFiles); @@ -93,7 +100,7 @@ export class JitCompilation extends TypeScriptCompilation { .getSourceFiles() .map((sourceFile) => sourceFile.fileName); - return { compilerOptions, referencedFiles }; + return { compilerOptions, referencedFiles, warnings }; } protected override *collectDiagnostics(modes: DiagnosticModes): Iterable { diff --git a/packages/angular/build/src/tools/angular/compilation/noop-compilation.ts b/packages/angular/build/src/tools/angular/compilation/noop-compilation.ts index 55c5913dbad4..7ace166d90fd 100644 --- a/packages/angular/build/src/tools/angular/compilation/noop-compilation.ts +++ b/packages/angular/build/src/tools/angular/compilation/noop-compilation.ts @@ -6,20 +6,48 @@ * found in the LICENSE file at https://angular.dev/license */ -import type * as ng from '@angular/compiler-cli'; import { AngularHostOptions } from '../angular-host'; import { AngularCompilation, AngularCompilationResult } from './angular-compilation'; +import type { CompilerOptionOverrides } from './compiler-options'; +/** + * An Angular compilation that performs no actual compilation or code emission. + * Used for secondary compilation contexts where only the resolved compiler options + * and configuration state are needed. + */ export class NoopCompilation extends AngularCompilation { async initialize( tsconfig: string, hostOptions: AngularHostOptions, - compilerOptionsTransformer?: (compilerOptions: ng.CompilerOptions) => ng.CompilerOptions, + compilerOptionOverrides?: CompilerOptionOverrides, ): Promise { - // Load the compiler configuration and transform as needed + // Load the compiler configuration const { options: originalCompilerOptions } = await this.loadConfiguration(tsconfig); - const compilerOptions = - compilerOptionsTransformer?.(originalCompilerOptions) ?? originalCompilerOptions; + // Apply relevant overrides directly without invoking `transformCompilerOptions` + // to avoid loading the `typescript` package on the main thread. + const compilerOptions = { + ...originalCompilerOptions, + noEmitOnError: false, + composite: false, + inlineSources: !!compilerOptionOverrides?.sourcemap, + inlineSourceMap: !!compilerOptionOverrides?.sourcemap, + sourceMap: undefined, + mapRoot: undefined, + sourceRoot: undefined, + preserveSymlinks: compilerOptionOverrides?.preserveSymlinks, + externalRuntimeStyles: compilerOptionOverrides?.externalRuntimeStyles, + _enableHmr: !!compilerOptionOverrides?.enableHmr, + _useTypeScriptTranspilation: + !originalCompilerOptions.isolatedModules || + !!compilerOptionOverrides?.instrumentForCoverage, + supportTestBed: !!compilerOptionOverrides?.includeTestMetadata, + supportJitMode: !!compilerOptionOverrides?.includeTestMetadata, + customConditions: + originalCompilerOptions.moduleResolution === 100 /* Bundler */ || + originalCompilerOptions.module === 200 /* Preserve */ + ? compilerOptionOverrides?.customConditions + : originalCompilerOptions.customConditions, + }; return { compilerOptions, referencedFiles: [] }; } diff --git a/packages/angular/build/src/tools/angular/compilation/parallel-compilation.ts b/packages/angular/build/src/tools/angular/compilation/parallel-compilation.ts index 7fc530161789..4df13b3a500e 100644 --- a/packages/angular/build/src/tools/angular/compilation/parallel-compilation.ts +++ b/packages/angular/build/src/tools/angular/compilation/parallel-compilation.ts @@ -6,7 +6,6 @@ * found in the LICENSE file at https://angular.dev/license */ -import type { CompilerOptions } from '@angular/compiler-cli'; import type { PartialMessage } from 'esbuild'; import { createRequire } from 'node:module'; import { MessageChannel } from 'node:worker_threads'; @@ -19,6 +18,7 @@ import { DiagnosticModes, EmitFileResult, } from './angular-compilation'; +import type { CompilerOptionOverrides } from './compiler-options'; /** * An Angular compilation which uses a Node.js Worker thread to load and execute @@ -51,7 +51,7 @@ export class ParallelCompilation extends AngularCompilation { override async initialize( tsconfig: string, hostOptions: AngularHostOptions, - compilerOptionsTransformer?: (compilerOptions: CompilerOptions) => CompilerOptions, + compilerOptionOverrides?: CompilerOptionOverrides, ): Promise { const stylesheetChannel = new MessageChannel(); // The request identifier is required because Angular can issue multiple concurrent requests @@ -84,22 +84,6 @@ export class ParallelCompilation extends AngularCompilation { } }); - // The compiler options transformation is a synchronous operation and uses shared memory combined - // with the Atomics API to block execution here until a response is received. - const optionsChannel = new MessageChannel(); - const optionsSignal = new Int32Array(new SharedArrayBuffer(4)); - optionsChannel.port1.on('message', (compilerOptions) => { - try { - const transformedOptions = compilerOptionsTransformer?.(compilerOptions) ?? compilerOptions; - optionsChannel.port1.postMessage({ transformedOptions }); - } catch (error) { - optionsChannel.port1.postMessage({ error }); - } finally { - Atomics.store(optionsSignal, 0, 1); - Atomics.notify(optionsSignal, 0); - } - }); - let success = false; try { // Execute the initialize function in the worker thread @@ -109,15 +93,14 @@ export class ParallelCompilation extends AngularCompilation { tsconfig, jit: this.jit, browserOnlyBuild: this.browserOnlyBuild, + compilerOptionOverrides, stylesheetPort: stylesheetChannel.port2, - optionsPort: optionsChannel.port2, - optionsSignal, webWorkerPort: webWorkerChannel.port2, webWorkerSignal, }, { name: 'initialize', - transferList: [stylesheetChannel.port2, optionsChannel.port2, webWorkerChannel.port2], + transferList: [stylesheetChannel.port2, webWorkerChannel.port2], }, ); success = true; @@ -125,7 +108,6 @@ export class ParallelCompilation extends AngularCompilation { return result; } finally { stylesheetChannel.port1.close(); - optionsChannel.port1.close(); if (!success) { this.#webWorkerChannel?.port1.close(); this.#webWorkerChannel = undefined; diff --git a/packages/angular/build/src/tools/angular/compilation/parallel-worker.ts b/packages/angular/build/src/tools/angular/compilation/parallel-worker.ts index b736e710fa7d..71b4d34e2756 100644 --- a/packages/angular/build/src/tools/angular/compilation/parallel-worker.ts +++ b/packages/angular/build/src/tools/angular/compilation/parallel-worker.ts @@ -18,6 +18,7 @@ import type { DiagnosticModes, } from './angular-compilation'; import { AotCompilation } from './aot-compilation'; +import type { CompilerOptionOverrides } from './compiler-options'; import { JitCompilation } from './jit-compilation'; export interface InitRequest { @@ -25,9 +26,8 @@ export interface InitRequest { browserOnlyBuild: boolean; tsconfig: string; fileReplacements?: Record; + compilerOptionOverrides?: CompilerOptionOverrides; stylesheetPort: MessagePort; - optionsPort: MessagePort; - optionsSignal: Int32Array; webWorkerPort: MessagePort; webWorkerSignal: Int32Array; } @@ -73,6 +73,7 @@ export async function initialize(request: InitRequest): Promise { - Atomics.store(request.optionsSignal, 0, 0); - request.optionsPort.postMessage(compilerOptions); - - Atomics.wait(request.optionsSignal, 0, 0); - const result = receiveMessageOnPort(request.optionsPort)?.message; - - if (result?.error) { - throw result.error; - } - - return result?.transformedOptions ?? compilerOptions; - }, + request.compilerOptionOverrides, ); success = true; @@ -130,6 +119,7 @@ export async function initialize(request: InitRequest): Promise[2] { - return (compilerOptions) => { - // target of 9 is ES2022 (using the number avoids an expensive import of typescript just for an enum) - if (compilerOptions.target === undefined || compilerOptions.target < 9 /** ES2022 */) { - // If 'useDefineForClassFields' is already defined in the users project leave the value as is. - // Otherwise fallback to false due to https://github.com/microsoft/TypeScript/issues/45995 - // which breaks the deprecated `@Effects` NGRX decorator and potentially other existing code as well. - compilerOptions.target = 9; /** ES2022 */ - compilerOptions.useDefineForClassFields ??= false; - - // Only add the warning on the initial build - setupWarnings?.push({ - text: - `TypeScript compiler options 'target' and 'useDefineForClassFields' are set to 'ES2022' and ` + - `'false' respectively by the Angular CLI.`, - location: { file: pluginOptions.tsconfig }, - notes: [ - { - text: - 'To control ECMA version and features use the Browserslist configuration. ' + - 'For more information, see https://angular.dev/tools/cli/build#configuring-browser-compatibility', - }, - ], - }); - } - - if (compilerOptions.compilationMode === 'partial') { - setupWarnings?.push({ - text: 'Angular partial compilation mode is not supported when building applications.', - location: null, - notes: [{ text: 'Full compilation mode will be used instead.' }], - }); - compilerOptions.compilationMode = 'full'; - } - - // Enable incremental compilation by default if caching is enabled and incremental is not explicitly disabled - if ( - compilerOptions.incremental !== false && - pluginOptions.sourceFileCache?.persistentCachePath - ) { - compilerOptions.incremental = true; - // Set the build info file location to the configured cache directory - compilerOptions.tsBuildInfoFile = path.join( - pluginOptions.sourceFileCache?.persistentCachePath, - '.tsbuildinfo', - ); - } else { - compilerOptions.incremental = false; - } - - if (compilerOptions.module === undefined || compilerOptions.module < 5 /** ES2015 */) { - compilerOptions.module = 7; /** ES2022 */ - setupWarnings?.push({ - text: `TypeScript compiler options 'module' values 'CommonJS', 'UMD', 'System' and 'AMD' are not supported.`, - location: null, - notes: [{ text: `The 'module' option will be set to 'ES2022' instead.` }], - }); - } - - if (compilerOptions.isolatedModules && compilerOptions.emitDecoratorMetadata) { - setupWarnings?.push({ - text: `TypeScript compiler option 'isolatedModules' may prevent the 'emitDecoratorMetadata' option from emitting all metadata.`, - location: null, - notes: [ - { - text: - `The 'emitDecoratorMetadata' option is not required by Angular` + - 'and can be removed if not explictly required by the project.', - }, - ], - }); - } - - // Synchronize custom resolve conditions. - // Set if using the supported bundler resolution mode (bundler is the default in new projects) - if ( - compilerOptions.moduleResolution === 100 /* ModuleResolutionKind.Bundler */ || - compilerOptions.module === 200 /** ModuleKind.Preserve */ - ) { - compilerOptions.customConditions = customConditions; - } - - return { - ...compilerOptions, - noEmitOnError: false, - composite: false, - inlineSources: !!pluginOptions.sourcemap, - inlineSourceMap: !!pluginOptions.sourcemap, - sourceMap: undefined, - mapRoot: undefined, - sourceRoot: undefined, - preserveSymlinks, - externalRuntimeStyles: pluginOptions.externalRuntimeStyles, - _enableHmr: !!pluginOptions.templateUpdates, - // TypeScript transpilation is forced if: - // - isolatedModules is disabled (TS needs full module types to emit JS). - // - Karma code coverage is active (the coverage instrumentation transformer is Babel-based - // and cannot parse raw TypeScript code; Vitest handles coverage instrumentation downstream). - _useTypeScriptTranspilation: - !compilerOptions.isolatedModules || !!pluginOptions.instrumentForCoverage, - supportTestBed: !!pluginOptions.includeTestMetadata, - supportJitMode: !!pluginOptions.includeTestMetadata, - }; - }; -} - function bundleWebWorker( build: PluginBuild, pluginOptions: CompilerPluginOptions,