Skip to content

Commit 93a871b

Browse files
committed
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.
1 parent bb72145 commit 93a871b

10 files changed

Lines changed: 390 additions & 174 deletions

File tree

packages/angular/build/src/tools/angular/compilation/angular-compilation.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type * as ng from '@angular/compiler-cli';
1010
import type { PartialMessage } from 'esbuild';
1111
import { profileSync } from '../../esbuild/profiling';
1212
import type { AngularHostOptions } from '../angular-host';
13+
import type { CompilerOptionOverrides } from './compiler-options';
1314

1415
export interface EmitFileResult {
1516
filename: string;
@@ -37,6 +38,7 @@ export interface AngularCompilationResult {
3738
externalStylesheets?: ReadonlyMap<string, string>;
3839
templateUpdates?: ReadonlyMap<string, string>;
3940
componentResourcesDependencies?: ReadonlyMap<string, readonly string[]>;
41+
warnings?: readonly PartialMessage[];
4042
}
4143

4244
export enum DiagnosticModes {
@@ -82,7 +84,7 @@ export abstract class AngularCompilation {
8284
abstract initialize(
8385
tsconfig: string,
8486
hostOptions: AngularHostOptions,
85-
compilerOptionsTransformer?: (compilerOptions: ng.CompilerOptions) => ng.CompilerOptions,
87+
compilerOptionOverrides?: CompilerOptionOverrides,
8688
): Promise<AngularCompilationResult>;
8789

8890
emitAffectedFiles(): Iterable<EmitFileResult> | Promise<Iterable<EmitFileResult>> {

packages/angular/build/src/tools/angular/compilation/angular-compilation_spec.ts

Lines changed: 174 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@
88

99
import ts from 'typescript';
1010
import type { AngularHostOptions } from '../angular-host';
11+
import { transformCompilerOptions } from './compiler-options';
12+
import { TypeScriptCompilation } from './typescript-compilation';
1113
import {
1214
AngularCompilation,
1315
AngularCompilationResult,
1416
DiagnosticModes,
1517
NoopCompilation,
16-
TypeScriptCompilation,
1718
createAngularCompilation,
1819
} from './index';
1920

@@ -62,13 +63,24 @@ describe('AngularCompilation', () => {
6263
it('initializes with empty referencedFiles and compiler options', async () => {
6364
const compilation = new NoopCompilation();
6465
const mockHostOptions = {} as AngularHostOptions;
65-
const result = await compilation.initialize('tsconfig.json', mockHostOptions, (opts) => ({
66-
...opts,
67-
customOption: true,
68-
}));
66+
const result = await compilation.initialize('tsconfig.json', mockHostOptions);
6967

7068
expect(result.referencedFiles).toEqual([]);
71-
expect(result.compilerOptions['customOption']).toBe(true);
69+
expect(result.compilerOptions).toBeDefined();
70+
});
71+
72+
it('initializes with CompilerOptionOverrides object', async () => {
73+
const compilation = new NoopCompilation();
74+
const mockHostOptions = {} as AngularHostOptions;
75+
const result = await compilation.initialize('tsconfig.json', mockHostOptions, {
76+
sourcemap: true,
77+
enableHmr: true,
78+
});
79+
80+
expect(result.referencedFiles).toEqual([]);
81+
expect(result.compilerOptions.inlineSources).toBe(true);
82+
expect(result.compilerOptions.inlineSourceMap).toBe(true);
83+
expect(result.compilerOptions['_enableHmr']).toBe(true);
7284
});
7385

7486
it('throws when calling emitAffectedFiles', () => {
@@ -184,4 +196,160 @@ describe('AngularCompilation', () => {
184196
);
185197
});
186198
});
199+
200+
describe('transformCompilerOptions', () => {
201+
it('does not mutate the input compiler options object', () => {
202+
const originalOptions: ts.CompilerOptions = {
203+
target: ts.ScriptTarget.ES2020,
204+
module: ts.ModuleKind.CommonJS,
205+
};
206+
const originalCopy = { ...originalOptions };
207+
208+
transformCompilerOptions(ts, originalOptions);
209+
210+
expect(originalOptions).toEqual(originalCopy);
211+
});
212+
213+
it('sets target to ES2022 and useDefineForClassFields to false when target is undefined', () => {
214+
const { compilerOptions, warnings } = transformCompilerOptions(
215+
ts,
216+
{ module: ts.ModuleKind.ES2022 },
217+
undefined,
218+
'tsconfig.json',
219+
);
220+
221+
expect(compilerOptions.target).toBe(ts.ScriptTarget.ES2022);
222+
expect(compilerOptions.useDefineForClassFields).toBe(false);
223+
expect(warnings.length).toBe(1);
224+
expect(warnings[0].text).toContain(
225+
"TypeScript compiler options 'target' and 'useDefineForClassFields'",
226+
);
227+
expect(warnings[0].location?.file).toBe('tsconfig.json');
228+
});
229+
230+
it('preserves existing useDefineForClassFields if target < ES2022', () => {
231+
const { compilerOptions, warnings } = transformCompilerOptions(
232+
ts,
233+
{
234+
target: ts.ScriptTarget.ES2020,
235+
useDefineForClassFields: true,
236+
module: ts.ModuleKind.ES2022,
237+
},
238+
undefined,
239+
'tsconfig.json',
240+
);
241+
242+
expect(compilerOptions.target).toBe(ts.ScriptTarget.ES2022);
243+
expect(compilerOptions.useDefineForClassFields).toBe(true);
244+
expect(warnings.length).toBe(1);
245+
});
246+
247+
it('sets compilationMode to full and warns when compilationMode is partial', () => {
248+
const { compilerOptions, warnings } = transformCompilerOptions(ts, {
249+
target: ts.ScriptTarget.ES2022,
250+
module: ts.ModuleKind.ES2022,
251+
compilationMode: 'partial',
252+
});
253+
254+
expect(compilerOptions.compilationMode).toBe('full');
255+
expect(warnings.length).toBe(1);
256+
expect(warnings[0].text).toContain('Angular partial compilation mode is not supported');
257+
});
258+
259+
it('configures incremental and tsBuildInfoFile when cachePath is provided', () => {
260+
const { compilerOptions } = transformCompilerOptions(
261+
ts,
262+
{ target: ts.ScriptTarget.ES2022 },
263+
{ cachePath: '/tmp/cache' },
264+
);
265+
266+
expect(compilerOptions.incremental).toBe(true);
267+
expect(compilerOptions.tsBuildInfoFile).toContain('.tsbuildinfo');
268+
});
269+
270+
it('sets incremental to false when cachePath is not provided or incremental is false', () => {
271+
const { compilerOptions: opt1 } = transformCompilerOptions(
272+
ts,
273+
{ target: ts.ScriptTarget.ES2022 },
274+
undefined,
275+
);
276+
expect(opt1.incremental).toBe(false);
277+
278+
const { compilerOptions: opt2 } = transformCompilerOptions(
279+
ts,
280+
{ target: ts.ScriptTarget.ES2022, incremental: false },
281+
{ cachePath: '/tmp/cache' },
282+
);
283+
expect(opt2.incremental).toBe(false);
284+
});
285+
286+
it('sets module to ES2022 and warns when module < ES2015', () => {
287+
const { compilerOptions, warnings } = transformCompilerOptions(ts, {
288+
target: ts.ScriptTarget.ES2022,
289+
module: ts.ModuleKind.CommonJS,
290+
});
291+
292+
expect(compilerOptions.module).toBe(ts.ModuleKind.ES2022);
293+
expect(warnings.length).toBe(1);
294+
expect(warnings[0].text).toContain(
295+
"TypeScript compiler options 'module' values 'CommonJS', 'UMD'",
296+
);
297+
});
298+
299+
it('warns when isolatedModules is enabled with emitDecoratorMetadata', () => {
300+
const { warnings } = transformCompilerOptions(ts, {
301+
target: ts.ScriptTarget.ES2022,
302+
module: ts.ModuleKind.ES2022,
303+
isolatedModules: true,
304+
emitDecoratorMetadata: true,
305+
});
306+
307+
expect(warnings.length).toBe(1);
308+
expect(warnings[0].text).toContain(
309+
"TypeScript compiler option 'isolatedModules' may prevent",
310+
);
311+
});
312+
313+
it('synchronizes customConditions when moduleResolution is Bundler or module is Preserve', () => {
314+
const { compilerOptions: bundlerOptions } = transformCompilerOptions(
315+
ts,
316+
{ target: ts.ScriptTarget.ES2022, moduleResolution: ts.ModuleResolutionKind.Bundler },
317+
{ customConditions: ['development'] },
318+
);
319+
expect(bundlerOptions.customConditions).toEqual(['development']);
320+
321+
const { compilerOptions: preserveOptions } = transformCompilerOptions(
322+
ts,
323+
{ target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.Preserve },
324+
{ customConditions: ['development'] },
325+
);
326+
expect(preserveOptions.customConditions).toEqual(['development']);
327+
});
328+
329+
it('applies override options correctly', () => {
330+
const { compilerOptions } = transformCompilerOptions(
331+
ts,
332+
{ target: ts.ScriptTarget.ES2022, isolatedModules: true },
333+
{
334+
sourcemap: true,
335+
preserveSymlinks: true,
336+
externalRuntimeStyles: true,
337+
enableHmr: true,
338+
instrumentForCoverage: true,
339+
includeTestMetadata: true,
340+
},
341+
);
342+
343+
expect(compilerOptions.inlineSources).toBe(true);
344+
expect(compilerOptions.inlineSourceMap).toBe(true);
345+
expect(compilerOptions.preserveSymlinks).toBe(true);
346+
expect(compilerOptions.externalRuntimeStyles).toBe(true);
347+
expect(compilerOptions['_enableHmr']).toBe(true);
348+
expect(compilerOptions['_useTypeScriptTranspilation']).toBe(true);
349+
expect(compilerOptions.supportTestBed).toBe(true);
350+
expect(compilerOptions.supportJitMode).toBe(true);
351+
expect(compilerOptions.noEmitOnError).toBe(false);
352+
expect(compilerOptions.composite).toBe(false);
353+
});
354+
});
187355
});

packages/angular/build/src/tools/angular/compilation/aot-compilation.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88

99
import type * as ng from '@angular/compiler-cli';
10+
import type { PartialMessage } from 'esbuild';
1011
import assert from 'node:assert';
1112
import { relative } from 'node:path';
1213
import ts from 'typescript';
@@ -26,6 +27,7 @@ import {
2627
DiagnosticModes,
2728
EmitFileResult,
2829
} from './angular-compilation';
30+
import { CompilerOptionOverrides, transformCompilerOptions } from './compiler-options';
2931
import { collectHmrCandidates } from './hmr-candidates';
3032
import { TypeScriptCompilation } from './typescript-compilation';
3133
import { printSourceFileWithMap } from './typescript-printer';
@@ -64,7 +66,7 @@ export class AotCompilation extends TypeScriptCompilation {
6466
async initialize(
6567
tsconfig: string,
6668
hostOptions: AngularHostOptions,
67-
compilerOptionsTransformer?: (compilerOptions: ng.CompilerOptions) => ng.CompilerOptions,
69+
compilerOptionOverrides?: CompilerOptionOverrides,
6870
): Promise<AngularCompilationResult> {
6971
// Dynamically load the Angular compiler CLI package
7072
const { NgtscProgram, OptimizeFor } = await AngularCompilation.loadCompilerCli();
@@ -75,8 +77,13 @@ export class AotCompilation extends TypeScriptCompilation {
7577
rootNames,
7678
errors: configurationDiagnostics,
7779
} = await this.loadConfiguration(tsconfig);
78-
const compilerOptions =
79-
compilerOptionsTransformer?.(originalCompilerOptions) ?? originalCompilerOptions;
80+
81+
const { compilerOptions, warnings } = transformCompilerOptions(
82+
ts,
83+
originalCompilerOptions,
84+
compilerOptionOverrides,
85+
tsconfig,
86+
);
8087

8188
const useTypeScriptTranspilation =
8289
(compilerOptions['_useTypeScriptTranspilation'] as boolean | undefined) ??
@@ -245,6 +252,7 @@ export class AotCompilation extends TypeScriptCompilation {
245252
externalStylesheets: hostOptions.externalStylesheets,
246253
templateUpdates,
247254
componentResourcesDependencies,
255+
warnings,
248256
};
249257
}
250258

0 commit comments

Comments
 (0)