diff --git a/packages/angular/build/src/builders/application/execute-build.ts b/packages/angular/build/src/builders/application/execute-build.ts index 8fe488a8ff76..379566e4de4d 100644 --- a/packages/angular/build/src/builders/application/execute-build.ts +++ b/packages/angular/build/src/builders/application/execute-build.ts @@ -8,7 +8,10 @@ import { BuilderContext } from '@angular-devkit/architect'; import { createAngularCompilation } from '../../tools/angular/compilation'; -import { AngularCompilationContext } from '../../tools/esbuild/angular/compilation-state'; +import { + AngularCompilationContext, + PrimaryCompilationContext, +} from '../../tools/esbuild/angular/compilation-state'; import { SourceFileCache } from '../../tools/esbuild/angular/source-file-cache'; import { generateBudgetStats } from '../../tools/esbuild/budget-stats'; import { BundleContextResult, BundlerContext } from '../../tools/esbuild/bundler-context'; @@ -125,7 +128,7 @@ export async function executeBuild( !!options.jit, !options.serverEntryPoint, ); - angularCompilationContext = new AngularCompilationContext(angularCompilation); + angularCompilationContext = new PrimaryCompilationContext(angularCompilation); bundlerContexts = setupBundlerContexts( options, target, diff --git a/packages/angular/build/src/private.ts b/packages/angular/build/src/private.ts index 9225c2f9d686..c4295aca88bb 100644 --- a/packages/angular/build/src/private.ts +++ b/packages/angular/build/src/private.ts @@ -13,7 +13,8 @@ * their existence may change in any future version. */ -import { NoopCompilation, createAngularCompilation } from './tools/angular/compilation'; +import { createAngularCompilation } from './tools/angular/compilation'; +import { SecondaryCompilationContext } from './tools/esbuild/angular/compilation-state'; import { CompilerPluginOptions, createCompilerPlugin as internalCreateCompilerPlugin, @@ -52,7 +53,7 @@ export function createCompilerPlugin( return internalCreateCompilerPlugin( pluginOptions, pluginOptions.noopTypeScriptCompilation - ? new NoopCompilation() + ? new SecondaryCompilationContext() : () => createAngularCompilation(!!pluginOptions.jit, !!pluginOptions.browserOnlyBuild), new ComponentStylesheetBundler( styleOptions, 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 ea1fe00d04aa..6bd836139c5f 100644 --- a/packages/angular/build/src/tools/angular/compilation/angular-compilation.ts +++ b/packages/angular/build/src/tools/angular/compilation/angular-compilation.ts @@ -6,9 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -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'; @@ -50,37 +48,6 @@ export enum DiagnosticModes { } export abstract class AngularCompilation { - static #angularCompilerCliModule?: typeof ng; - - static async loadCompilerCli(): Promise { - AngularCompilation.#angularCompilerCliModule ??= await import('@angular/compiler-cli'); - - return AngularCompilation.#angularCompilerCliModule; - } - - protected async loadConfiguration(tsconfig: string): Promise { - const { readConfiguration } = await AngularCompilation.loadCompilerCli(); - - return profileSync('NG_READ_CONFIG', () => - readConfiguration(tsconfig, { - // Angular specific configuration defaults and overrides to ensure a functioning compilation. - suppressOutputPathCheck: true, - outDir: undefined, - sourceMap: false, - declaration: false, - declarationMap: false, - allowEmptyCodegenFiles: false, - annotationsAs: 'decorators', - enableResourceInlining: false, - supportTestBed: false, - supportJitMode: false, - // Disable removing of comments as TS is quite aggressive with these and can - // remove important annotations, such as /* @__PURE__ */ and comments like /* vite-ignore */. - removeComments: false, - }), - ); - } - abstract initialize( tsconfig: string, hostOptions: AngularHostOptions, 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 ad28c31bcce5..a867f666c349 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 @@ -14,7 +14,6 @@ import { AngularCompilation, AngularCompilationResult, DiagnosticModes, - NoopCompilation, createAngularCompilation, } from './index'; @@ -59,44 +58,6 @@ describe('AngularCompilation', () => { expect(diagnostics).toEqual({}); }); - describe('NoopCompilation', () => { - 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); - - expect(result.referencedFiles).toEqual([]); - 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', () => { - const compilation = new NoopCompilation(); - expect(() => compilation.emitAffectedFiles()).toThrowError( - 'Not available when using noop compilation.', - ); - }); - - it('returns empty diagnostics from diagnoseFiles', async () => { - const compilation = new NoopCompilation(); - const diagnostics = await compilation.diagnoseFiles(); - expect(diagnostics).toEqual({}); - }); - }); - describe('TypeScriptCompilation', () => { class MockTypeScriptCompilation extends TypeScriptCompilation { async initialize(): Promise { @@ -167,6 +128,12 @@ describe('AngularCompilation', () => { await compilation.update?.(new Set(['/src/test.ts'])); expect(compilation.getCachedSourceFiles().has('/src/test.ts')).toBeFalse(); }); + + it('dynamically loads the @angular/compiler-cli module', async () => { + const compilerCli = await TypeScriptCompilation.loadCompilerCli(); + expect(compilerCli).toBeDefined(); + expect(typeof compilerCli.readConfiguration).toBe('function'); + }); }); describe('createAngularCompilation', () => { 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 ec68415e62fb..9fd160774cf7 100644 --- a/packages/angular/build/src/tools/angular/compilation/aot-compilation.ts +++ b/packages/angular/build/src/tools/angular/compilation/aot-compilation.ts @@ -22,10 +22,9 @@ import { replaceBootstrap } from '../transformers/jit-bootstrap-transformer'; import { lazyRoutesTransformer } from '../transformers/lazy-routes-transformer'; import { createWorkerTransformer } from '../transformers/web-worker-transformer'; import { - AngularCompilation, - AngularCompilationResult, + type AngularCompilationResult, DiagnosticModes, - EmitFileResult, + type EmitFileResult, } from './angular-compilation'; import { CompilerOptionOverrides, transformCompilerOptions } from './compiler-options'; import { collectHmrCandidates } from './hmr-candidates'; @@ -69,7 +68,7 @@ export class AotCompilation extends TypeScriptCompilation { compilerOptionOverrides?: CompilerOptionOverrides, ): Promise { // Dynamically load the Angular compiler CLI package - const { NgtscProgram, OptimizeFor } = await AngularCompilation.loadCompilerCli(); + const { NgtscProgram, OptimizeFor } = await TypeScriptCompilation.loadCompilerCli(); // Load the compiler configuration and transform as needed const { diff --git a/packages/angular/build/src/tools/angular/compilation/index.ts b/packages/angular/build/src/tools/angular/compilation/index.ts index 39391bcfb562..268abec678ca 100644 --- a/packages/angular/build/src/tools/angular/compilation/index.ts +++ b/packages/angular/build/src/tools/angular/compilation/index.ts @@ -16,4 +16,3 @@ export { } from './angular-compilation'; export type { CompilerOptionOverrides } from './compiler-options'; export { createAngularCompilation, type AngularCompilationMode } from './factory'; -export { NoopCompilation } from './noop-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 b54f66701c6c..fee636219be0 100644 --- a/packages/angular/build/src/tools/angular/compilation/jit-compilation.ts +++ b/packages/angular/build/src/tools/angular/compilation/jit-compilation.ts @@ -16,10 +16,9 @@ import { createJitResourceTransformer } from '../transformers/jit-resource-trans import { lazyRoutesTransformer } from '../transformers/lazy-routes-transformer'; import { createWorkerTransformer } from '../transformers/web-worker-transformer'; import { - AngularCompilation, - AngularCompilationResult, + type AngularCompilationResult, DiagnosticModes, - EmitFileResult, + type EmitFileResult, } from './angular-compilation'; import { CompilerOptionOverrides, transformCompilerOptions } from './compiler-options'; import { TypeScriptCompilation } from './typescript-compilation'; diff --git a/packages/angular/build/src/tools/angular/compilation/noop-compilation.ts b/packages/angular/build/src/tools/angular/compilation/noop-compilation.ts deleted file mode 100644 index 7ace166d90fd..000000000000 --- a/packages/angular/build/src/tools/angular/compilation/noop-compilation.ts +++ /dev/null @@ -1,58 +0,0 @@ -/** - * @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 { 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, - compilerOptionOverrides?: CompilerOptionOverrides, - ): Promise { - // Load the compiler configuration - const { options: originalCompilerOptions } = await this.loadConfiguration(tsconfig); - // 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: [] }; - } - - override emitAffectedFiles(): never { - throw new Error('Not available when using noop compilation.'); - } -} diff --git a/packages/angular/build/src/tools/angular/compilation/typescript-compilation.ts b/packages/angular/build/src/tools/angular/compilation/typescript-compilation.ts index 7591e2028c36..970041f916fc 100644 --- a/packages/angular/build/src/tools/angular/compilation/typescript-compilation.ts +++ b/packages/angular/build/src/tools/angular/compilation/typescript-compilation.ts @@ -6,14 +6,46 @@ * found in the LICENSE file at https://angular.dev/license */ +import type * as ng from '@angular/compiler-cli'; import type { PartialMessage } from 'esbuild'; import ts from 'typescript'; import { toPosixPath } from '../../../utils/path'; -import { profileAsync } from '../../esbuild/profiling'; +import { profileAsync, profileSync } from '../../esbuild/profiling'; import { AngularCompilation, DiagnosticModes } from './angular-compilation'; import { convertTypeScriptDiagnostic } from './diagnostics'; export abstract class TypeScriptCompilation extends AngularCompilation { + static #angularCompilerCliModule?: typeof ng; + + static async loadCompilerCli(): Promise { + TypeScriptCompilation.#angularCompilerCliModule ??= await import('@angular/compiler-cli'); + + return TypeScriptCompilation.#angularCompilerCliModule; + } + + protected async loadConfiguration(tsconfig: string): Promise { + const { readConfiguration } = await TypeScriptCompilation.loadCompilerCli(); + + return profileSync('NG_READ_CONFIG', () => + readConfiguration(tsconfig, { + // Angular specific configuration defaults and overrides to ensure a functioning compilation. + suppressOutputPathCheck: true, + outDir: undefined, + sourceMap: false, + declaration: false, + declarationMap: false, + allowEmptyCodegenFiles: false, + annotationsAs: 'decorators', + enableResourceInlining: false, + supportTestBed: false, + supportJitMode: false, + // Disable removing of comments as TS is quite aggressive with these and can + // remove important annotations, such as /* @__PURE__ */ and comments like /* vite-ignore */. + removeComments: false, + }), + ); + } + protected readonly sourceFiles = new Map(); protected invalidateFiles(files: Iterable): void { diff --git a/packages/angular/build/src/tools/esbuild/angular/compilation-state.ts b/packages/angular/build/src/tools/esbuild/angular/compilation-state.ts index 286411a06057..541f52bbca76 100644 --- a/packages/angular/build/src/tools/esbuild/angular/compilation-state.ts +++ b/packages/angular/build/src/tools/esbuild/angular/compilation-state.ts @@ -6,24 +6,46 @@ * found in the LICENSE file at https://angular.dev/license */ -import { type AngularCompilation, NoopCompilation } from '../../angular/compilation'; +import type { CompilerOptions } from '@angular/compiler-cli'; +import type { AngularCompilation } from '../../angular/compilation'; -export class AngularCompilationContext { - #compilation: AngularCompilation; +export abstract class AngularCompilationContext { + abstract readonly compilation?: AngularCompilation; + abstract isPrimary(): this is PrimaryCompilationContext; + abstract readonly waitUntilReady: Promise; + abstract getCompilerOptions(): Promise; + abstract dispose(): Promise; + + createSecondaryContext(): AngularCompilationContext { + return new SecondaryCompilationContext(this); + } +} + +export class PrimaryCompilationContext extends AngularCompilationContext { + readonly #compilation: AngularCompilation; #pendingCompilation = true; #resolveCompilationReady: ((value: boolean) => void) | undefined; #compilationReadyPromise: Promise | undefined; #hasErrors = true; + #compilerOptions: CompilerOptions | undefined; + #resolveCompilerOptions: ((options: CompilerOptions) => void) | undefined; + #compilerOptionsPromise: Promise | undefined; + constructor(compilation: AngularCompilation) { + super(); this.#compilation = compilation; } - get compilation(): AngularCompilation { + override isPrimary(): this is PrimaryCompilationContext { + return true; + } + + override get compilation(): AngularCompilation { return this.#compilation; } - get waitUntilReady(): Promise { + override get waitUntilReady(): Promise { if (!this.#pendingCompilation) { return Promise.resolve(this.#hasErrors); } @@ -35,20 +57,51 @@ export class AngularCompilationContext { return this.#compilationReadyPromise; } + override getCompilerOptions(): Promise { + if (this.#compilerOptions) { + return Promise.resolve(this.#compilerOptions); + } + + if (!this.#pendingCompilation) { + return Promise.resolve({}); + } + + this.#compilerOptionsPromise ??= new Promise((resolve) => { + this.#resolveCompilerOptions = resolve; + }); + + return this.#compilerOptionsPromise; + } + + setCompilerOptions(options: CompilerOptions): void { + this.#compilerOptions = options; + this.#resolveCompilerOptions?.(options); + this.#resolveCompilerOptions = undefined; + this.#compilerOptionsPromise = undefined; + } + markAsReady(hasErrors: boolean): void { this.#hasErrors = hasErrors; this.#resolveCompilationReady?.(hasErrors); + this.#resolveCompilationReady = undefined; this.#compilationReadyPromise = undefined; this.#pendingCompilation = false; + + if (this.#resolveCompilerOptions) { + this.#resolveCompilerOptions(this.#compilerOptions ?? {}); + this.#resolveCompilerOptions = undefined; + this.#compilerOptionsPromise = undefined; + } } markAsInProgress(): void { this.#pendingCompilation = true; + this.#compilerOptions = undefined; } #disposal: Promise | undefined; - dispose(): Promise { + override dispose(): Promise { // Reuse any in progress disposal to ensure all callers can await completion return (this.#disposal ??= this.#close()); } @@ -61,27 +114,27 @@ export class AngularCompilationContext { // Suppress closure errors to avoid unhandled rejections during teardown. } } +} - createSecondaryContext(): AngularCompilationContext { - return new SecondaryCompilationContext(this); +export class SecondaryCompilationContext extends AngularCompilationContext { + constructor(private readonly primaryContext?: AngularCompilationContext) { + super(); } -} -class SecondaryCompilationContext extends AngularCompilationContext { - constructor(private primaryContext: AngularCompilationContext) { - super(new NoopCompilation()); + override isPrimary(): this is PrimaryCompilationContext { + return false; } - override get waitUntilReady(): Promise { - return this.primaryContext.waitUntilReady; + override get compilation(): undefined { + return undefined; } - override markAsReady(hasErrors: boolean): void { - // No-op: secondary contexts do not control compilation state + override get waitUntilReady(): Promise { + return this.primaryContext?.waitUntilReady ?? Promise.resolve(false); } - override markAsInProgress(): void { - // No-op: secondary contexts do not control compilation state + override getCompilerOptions(): Promise { + return this.primaryContext?.getCompilerOptions() ?? Promise.resolve({}); } override async dispose(): Promise { diff --git a/packages/angular/build/src/tools/esbuild/angular/compilation-state_spec.ts b/packages/angular/build/src/tools/esbuild/angular/compilation-state_spec.ts new file mode 100644 index 000000000000..8007427d497c --- /dev/null +++ b/packages/angular/build/src/tools/esbuild/angular/compilation-state_spec.ts @@ -0,0 +1,178 @@ +/** + * @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 { CompilerOptions } from '@angular/compiler-cli'; +import type { AngularCompilation } from '../../angular/compilation'; +import { PrimaryCompilationContext, SecondaryCompilationContext } from './compilation-state'; + +describe('compilation-state', () => { + let mockCompilation: jasmine.SpyObj; + + beforeEach(() => { + mockCompilation = jasmine.createSpyObj('AngularCompilation', [ + 'initialize', + 'close', + ]); + }); + + describe('PrimaryCompilationContext', () => { + it('identifies as primary and provides the underlying compilation', () => { + const context = new PrimaryCompilationContext(mockCompilation); + + expect(context.isPrimary()).toBe(true); + expect(context.compilation).toBe(mockCompilation); + }); + + it('resolves getCompilerOptions after setCompilerOptions is called', async () => { + const context = new PrimaryCompilationContext(mockCompilation); + const options: CompilerOptions = { target: 9, allowJs: true }; + + context.setCompilerOptions(options); + + const resolved = await context.getCompilerOptions(); + expect(resolved).toEqual(options); + }); + + it('resolves pending getCompilerOptions when setCompilerOptions is called later', async () => { + const context = new PrimaryCompilationContext(mockCompilation); + const options: CompilerOptions = { target: 9, allowJs: true }; + + const optionsPromise = context.getCompilerOptions(); + context.setCompilerOptions(options); + + const resolved = await optionsPromise; + expect(resolved).toEqual(options); + }); + + it('manages ready state lifecycle correctly', async () => { + const context = new PrimaryCompilationContext(mockCompilation); + + // Initially in progress + const readyPromise = context.waitUntilReady; + context.markAsReady(false); + + const hasErrors = await readyPromise; + expect(hasErrors).toBe(false); + + // When already ready, immediately returns error state + expect(await context.waitUntilReady).toBe(false); + + // Mark in progress again + context.markAsInProgress(); + const secondReadyPromise = context.waitUntilReady; + context.markAsReady(true); + + expect(await secondReadyPromise).toBe(true); + }); + + it('closes compilation and marks ready with errors on dispose', async () => { + const context = new PrimaryCompilationContext(mockCompilation); + + const readyPromise = context.waitUntilReady; + await context.dispose(); + + expect(mockCompilation.close).toHaveBeenCalledTimes(1); + expect(await readyPromise).toBe(true); + }); + + it('unblocks pending getCompilerOptions on markAsReady error', async () => { + const context = new PrimaryCompilationContext(mockCompilation); + + const optionsPromise = context.getCompilerOptions(); + context.markAsReady(true); + + const options = await optionsPromise; + expect(options).toEqual({}); + }); + + it('returns empty options from getCompilerOptions when compilation is not pending and options are unset', async () => { + const context = new PrimaryCompilationContext(mockCompilation); + + context.markAsReady(false); + + const options = await context.getCompilerOptions(); + expect(options).toEqual({}); + }); + + it('clears cached compiler options on markAsInProgress to wait for fresh options on rebuild', async () => { + const context = new PrimaryCompilationContext(mockCompilation); + const initialOptions: CompilerOptions = { target: 9, allowJs: false }; + context.setCompilerOptions(initialOptions); + context.markAsReady(false); + + expect(await context.getCompilerOptions()).toEqual(initialOptions); + + // Rebuild starts + context.markAsInProgress(); + + // Pending call should wait for new options rather than returning stale ones + const nextOptionsPromise = context.getCompilerOptions(); + const updatedOptions: CompilerOptions = { target: 10, allowJs: true }; + context.setCompilerOptions(updatedOptions); + + expect(await nextOptionsPromise).toEqual(updatedOptions); + }); + + it('creates a linked SecondaryCompilationContext', () => { + const context = new PrimaryCompilationContext(mockCompilation); + const secondary = context.createSecondaryContext(); + + expect(secondary).toBeInstanceOf(SecondaryCompilationContext); + expect(secondary.isPrimary()).toBe(false); + expect(secondary.compilation).toBeUndefined(); + }); + }); + + describe('SecondaryCompilationContext', () => { + it('identifies as non-primary and has undefined compilation', () => { + const primary = new PrimaryCompilationContext(mockCompilation); + const secondary = primary.createSecondaryContext(); + + expect(secondary.isPrimary()).toBe(false); + expect(secondary.compilation).toBeUndefined(); + }); + + it('delegates waitUntilReady to primary context', async () => { + const primary = new PrimaryCompilationContext(mockCompilation); + const secondary = primary.createSecondaryContext(); + + const secondaryReadyPromise = secondary.waitUntilReady; + primary.markAsReady(false); + + expect(await secondaryReadyPromise).toBe(false); + }); + + it('delegates getCompilerOptions to primary context', async () => { + const primary = new PrimaryCompilationContext(mockCompilation); + const secondary = primary.createSecondaryContext(); + const options: CompilerOptions = { target: 9, allowJs: false }; + + primary.setCompilerOptions(options); + + expect(await secondary.getCompilerOptions()).toEqual(options); + }); + + it('does not dispose primary compilation when secondary is disposed', async () => { + const primary = new PrimaryCompilationContext(mockCompilation); + const secondary = primary.createSecondaryContext(); + + await secondary.dispose(); + + expect(mockCompilation.close).not.toHaveBeenCalled(); + }); + + it('handles standalone instantiation with sensible defaults', async () => { + const standalone = new SecondaryCompilationContext(); + + expect(standalone.isPrimary()).toBe(false); + expect(standalone.compilation).toBeUndefined(); + expect(await standalone.waitUntilReady).toBe(false); + expect(await standalone.getCompilerOptions()).toEqual({}); + }); + }); +}); diff --git a/packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts b/packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts index 05c49040b43c..5ce1130d093a 100644 --- a/packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts +++ b/packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts @@ -23,12 +23,12 @@ import * as path from 'node:path'; import { maxWorkers, useTypeChecking } from '../../../utils/environment-options'; import { calculateHash, initializeHash } from '../../../utils/hash'; import { AngularHostOptions } from '../../angular/angular-host'; -import { AngularCompilation, DiagnosticModes, NoopCompilation } from '../../angular/compilation'; +import { AngularCompilation, DiagnosticModes } from '../../angular/compilation'; import { type PersistentCacheStore, createPersistentCacheStore } from '../cache'; import { JavaScriptTransformer } from '../javascript-transformer'; import { LoadResultCache, createCachedLoad } from '../load-result-cache'; import { logCumulativeDurations, profileAsync, resetCumulativeDurations } from '../profiling'; -import { AngularCompilationContext } from './compilation-state'; +import { AngularCompilationContext, PrimaryCompilationContext } from './compilation-state'; import { ComponentStylesheetBundler } from './component-stylesheets'; import { FileReferenceTracker } from './file-reference-tracker'; import { setupJitPluginCallbacks } from './jit-plugin-callbacks'; @@ -117,12 +117,12 @@ export function createCompilerPlugin( const angularCompilationContext = compilationContextOrCompilation instanceof AngularCompilationContext ? compilationContextOrCompilation - : new AngularCompilationContext( + : new PrimaryCompilationContext( typeof compilationContextOrCompilation === 'function' ? await compilationContextOrCompilation() : compilationContextOrCompilation, ); - const compilation: AngularCompilation = angularCompilationContext.compilation; + const compilation: AngularCompilation | undefined = angularCompilationContext.compilation; // The in-memory cache of TypeScript file outputs will be used during the build in `onLoad` callbacks for TS files. // A string value indicates direct TS/NG output and a Uint8Array indicates fully transformed code. @@ -150,12 +150,23 @@ export function createCompilerPlugin( // eslint-disable-next-line max-lines-per-function build.onStart(async () => { await initializeHash(); - angularCompilationContext.markAsInProgress(); const result: OnStartResult = { warnings: setupWarnings, }; + if (!angularCompilationContext.isPrimary()) { + hasCompilationErrors = await angularCompilationContext.waitUntilReady; + const compilerOptions = await angularCompilationContext.getCompilerOptions(); + shouldTsIgnoreJs = !compilerOptions.allowJs; + useTypeScriptTranspilation = !!compilerOptions['_useTypeScriptTranspilation']; + + return result; + } + + angularCompilationContext.markAsInProgress(); + const compilation = angularCompilationContext.compilation; + // Reset debug performance tracking resetCumulativeDurations(); @@ -163,10 +174,7 @@ export function createCompilerPlugin( // Angular compiler which does not have direct knowledge of transitive resource // dependencies or web worker processing. let modifiedFiles; - if ( - !(compilation instanceof NoopCompilation) && - pluginOptions.sourceFileCache?.modifiedFiles.size - ) { + if (pluginOptions.sourceFileCache?.modifiedFiles.size) { // TODO: Differentiate between changed input files and stale output files modifiedFiles = referencedFileTracker.update(pluginOptions.sourceFileCache.modifiedFiles); pluginOptions.sourceFileCache.invalidate(modifiedFiles); @@ -320,6 +328,7 @@ export function createCompilerPlugin( if (initializationResult.warnings?.length) { setupWarnings?.push(...initializationResult.warnings); } + angularCompilationContext.setCompilerOptions(initializationResult.compilerOptions); shouldTsIgnoreJs = !initializationResult.compilerOptions.allowJs; useTypeScriptTranspilation = !!initializationResult.compilerOptions['_useTypeScriptTranspilation']; @@ -345,12 +354,7 @@ export function createCompilerPlugin( // Initialization failure prevents further compilation steps hasCompilationErrors = true; - - return result; - } - - if (compilation instanceof NoopCompilation) { - hasCompilationErrors = await angularCompilationContext.waitUntilReady; + angularCompilationContext.markAsReady(true); return result; } @@ -444,7 +448,7 @@ export function createCompilerPlugin( let contents = typeScriptFileCache.get(request); let directContents: string | undefined; - if (contents === undefined && compilation.transformFile) { + if (contents === undefined && compilation?.transformFile) { try { directContents = await readFile(request, 'utf-8'); const transformResult = await compilation.transformFile(request, directContents); @@ -607,7 +611,9 @@ export function createCompilerPlugin( build.onEnd((result) => { // Ensure other compilations are unblocked if the main compilation throws during start - angularCompilationContext.markAsReady(hasCompilationErrors); + if (angularCompilationContext.isPrimary()) { + angularCompilationContext.markAsReady(hasCompilationErrors); + } for (const { outputFiles, metafile } of additionalResults.values()) { // Add any additional output files to the main output files