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,