Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 66 additions & 52 deletions packages/angular/build/src/builders/application/execute-build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import { BuilderContext } from '@angular-devkit/architect';
import { createAngularCompilation } from '../../tools/angular/compilation';
import { AngularCompilationContext } 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';
Expand Down Expand Up @@ -71,61 +72,74 @@ export async function executeBuild(
let codeBundleCache;
let bundlingResult: BundleContextResult;
let templateUpdates: Map<string, string> | undefined;
if (rebuildState) {
bundlerContexts = rebuildState.rebuildContexts;
componentStyleBundler = rebuildState.componentStyleBundler;
codeBundleCache = rebuildState.codeBundleCache;
templateUpdates = rebuildState.templateUpdates;
// Reset template updates for new rebuild
templateUpdates?.clear();

const allFileChanges = rebuildState.fileChanges.all;

// Bundle all contexts that do not require TypeScript changed file checks.
// These will automatically use cached results based on the changed files.
bundlingResult = await BundlerContext.bundleAll(bundlerContexts.otherContexts, allFileChanges);

// Check the TypeScript code bundling cache for changes. If invalid, force a rebundle of
// all TypeScript related contexts.
const forceTypeScriptRebuild = codeBundleCache?.invalidate(allFileChanges);
const typescriptResults: BundleContextResult[] = [];
for (const typescriptContext of bundlerContexts.typescriptContexts) {
typescriptContext.invalidate(allFileChanges);
const result = await typescriptContext.bundle(forceTypeScriptRebuild);
typescriptResults.push(result);
}
bundlingResult = BundlerContext.mergeResults([bundlingResult, ...typescriptResults]);
} else {
const target = transformSupportedBrowsersToTargets(browsers);
codeBundleCache = new SourceFileCache(cacheOptions.enabled ? cacheOptions.path : undefined);
componentStyleBundler = createComponentStyleBundler(options, target);
if (options.templateUpdates) {
templateUpdates = new Map<string, string>();
}
bundlerContexts = setupBundlerContexts(
options,
target,
codeBundleCache,
componentStyleBundler,
// Create new reusable compilation for the appropriate mode based on the `jit` plugin option
await createAngularCompilation(!!options.jit, !options.serverEntryPoint),
templateUpdates,
);
let angularCompilationContext: AngularCompilationContext | undefined;
try {
if (rebuildState) {
bundlerContexts = rebuildState.rebuildContexts;
componentStyleBundler = rebuildState.componentStyleBundler;
codeBundleCache = rebuildState.codeBundleCache;
templateUpdates = rebuildState.templateUpdates;
// Reset template updates for new rebuild
templateUpdates?.clear();

const allFileChanges = rebuildState.fileChanges.all;

// Bundle all contexts that do not require TypeScript changed file checks.
// These will automatically use cached results based on the changed files.
bundlingResult = await BundlerContext.bundleAll(
bundlerContexts.otherContexts,
allFileChanges,
);

// Bundle everything on initial build
bundlingResult = await BundlerContext.bundleAll([
...bundlerContexts.typescriptContexts,
...bundlerContexts.otherContexts,
]);
}
// Check the TypeScript code bundling cache for changes. If invalid, force a rebundle of
// all TypeScript related contexts.
const forceTypeScriptRebuild = codeBundleCache?.invalidate(allFileChanges);
const typescriptResults: BundleContextResult[] = [];
for (const typescriptContext of bundlerContexts.typescriptContexts) {
typescriptContext.invalidate(allFileChanges);
const result = await typescriptContext.bundle(forceTypeScriptRebuild);
typescriptResults.push(result);
}
bundlingResult = BundlerContext.mergeResults([bundlingResult, ...typescriptResults]);
} else {
const target = transformSupportedBrowsersToTargets(browsers);
codeBundleCache = new SourceFileCache(cacheOptions.enabled ? cacheOptions.path : undefined);
componentStyleBundler = createComponentStyleBundler(options, target);
if (options.templateUpdates) {
templateUpdates = new Map<string, string>();
}
const angularCompilation = await createAngularCompilation(
!!options.jit,
!options.serverEntryPoint,
);
angularCompilationContext = new AngularCompilationContext(angularCompilation);
bundlerContexts = setupBundlerContexts(
options,
target,
codeBundleCache,
componentStyleBundler,
angularCompilationContext,
templateUpdates,
);

// Update any external component styles if enabled and rebuilding.
// TODO: Only attempt rebundling of invalidated styles once incremental build results are supported.
if (rebuildState && options.externalRuntimeStyles) {
componentStyleBundler.invalidate(rebuildState.fileChanges.all);
// Bundle everything on initial build
bundlingResult = await BundlerContext.bundleAll([
...bundlerContexts.typescriptContexts,
...bundlerContexts.otherContexts,
]);
}

// Update any external component styles if enabled and rebuilding.
// TODO: Only attempt rebundling of invalidated styles once incremental build results are supported.
if (rebuildState && options.externalRuntimeStyles) {
componentStyleBundler.invalidate(rebuildState.fileChanges.all);

const componentResults = await componentStyleBundler.bundleAllFiles(true, true);
bundlingResult = BundlerContext.mergeResults([bundlingResult, ...componentResults]);
const componentResults = await componentStyleBundler.bundleAllFiles(true, true);
bundlingResult = BundlerContext.mergeResults([bundlingResult, ...componentResults]);
}
} catch (error) {
await angularCompilationContext?.dispose();
throw error;
}

const executionResult = new ExecutionResult(
Expand Down
27 changes: 20 additions & 7 deletions packages/angular/build/src/builders/application/setup-bundling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
* found in the LICENSE file at https://angular.dev/license
*/

import { AngularCompilation } from '../../tools/angular/compilation';
import type { AngularCompilationContext } from '../../tools/esbuild/angular/compilation-state';
import { ComponentStylesheetBundler } from '../../tools/esbuild/angular/component-stylesheets';
import { SourceFileCache } from '../../tools/esbuild/angular/source-file-cache';
import type { SourceFileCache } from '../../tools/esbuild/angular/source-file-cache';
import {
createBrowserCodeBundleOptions,
createBrowserPolyfillBundleOptions,
Expand All @@ -20,7 +20,7 @@ import { BundlerContext } from '../../tools/esbuild/bundler-context';
import { createGlobalScriptsBundleOptions } from '../../tools/esbuild/global-scripts';
import { createGlobalStylesBundleOptions } from '../../tools/esbuild/global-styles';
import { getSupportedNodeTargets } from '../../tools/esbuild/utils';
import { NormalizedApplicationBuildOptions } from './options';
import type { NormalizedApplicationBuildOptions } from './options';

/**
* Generates one or more BundlerContext instances based on the builder provided
Expand All @@ -35,7 +35,7 @@ export function setupBundlerContexts(
target: string[],
codeBundleCache: SourceFileCache,
stylesheetBundler: ComponentStylesheetBundler,
angularCompilation: AngularCompilation,
angularCompilationContext: AngularCompilationContext,
templateUpdates: Map<string, string> | undefined,
): {
typescriptContexts: BundlerContext[];
Expand Down Expand Up @@ -63,7 +63,7 @@ export function setupBundlerContexts(
target,
codeBundleCache,
stylesheetBundler,
angularCompilation,
angularCompilationContext,
templateUpdates,
),
),
Expand All @@ -75,6 +75,7 @@ export function setupBundlerContexts(
target,
codeBundleCache,
stylesheetBundler,
angularCompilationContext.createSecondaryContext(),
);
if (browserPolyfillBundleOptions) {
const browserPolyfillContext = new BundlerContext(
Expand Down Expand Up @@ -117,7 +118,13 @@ export function setupBundlerContexts(
new BundlerContext(
workspaceRoot,
watch,
createServerMainCodeBundleOptions(options, nodeTargets, codeBundleCache, stylesheetBundler),
createServerMainCodeBundleOptions(
options,
nodeTargets,
codeBundleCache,
stylesheetBundler,
angularCompilationContext.createSecondaryContext(),
),
),
);

Expand All @@ -127,7 +134,13 @@ export function setupBundlerContexts(
new BundlerContext(
workspaceRoot,
watch,
createSsrEntryCodeBundleOptions(options, nodeTargets, codeBundleCache, stylesheetBundler),
createSsrEntryCodeBundleOptions(
options,
nodeTargets,
codeBundleCache,
stylesheetBundler,
angularCompilationContext.createSecondaryContext(),
),
),
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,23 @@
* found in the LICENSE file at https://angular.dev/license
*/

export class SharedTSCompilationState {
import { type AngularCompilation, NoopCompilation } from '../../angular/compilation';

export class AngularCompilationContext {
#compilation: AngularCompilation;
#pendingCompilation = true;
#resolveCompilationReady: ((value: boolean) => void) | undefined;
#compilationReadyPromise: Promise<boolean> | undefined;
#hasErrors = true;

constructor(compilation: AngularCompilation) {
this.#compilation = compilation;
}

get compilation(): AngularCompilation {
return this.#compilation;
}

get waitUntilReady(): Promise<boolean> {
if (!this.#pendingCompilation) {
return Promise.resolve(this.#hasErrors);
Expand All @@ -35,14 +46,44 @@ export class SharedTSCompilationState {
this.#pendingCompilation = true;
}

dispose(): void {
#disposed = false;

async dispose(): Promise<void> {
if (this.#disposed) {
return;
}
this.#disposed = true;
this.markAsReady(true);
globalSharedCompilationState = undefined;
try {
await this.#compilation.close?.();
} catch {
// Suppress closure errors to avoid unhandled rejections during teardown.
}
}
Comment thread
clydin marked this conversation as resolved.

createSecondaryContext(): AngularCompilationContext {
return new SecondaryCompilationContext(this);
}
}

let globalSharedCompilationState: SharedTSCompilationState | undefined;
class SecondaryCompilationContext extends AngularCompilationContext {
constructor(private primaryContext: AngularCompilationContext) {
super(new NoopCompilation());
}

export function getSharedCompilationState(): SharedTSCompilationState {
return (globalSharedCompilationState ??= new SharedTSCompilationState());
override get waitUntilReady(): Promise<boolean> {
return this.primaryContext.waitUntilReady;
}

override markAsReady(hasErrors: boolean): void {
// No-op: secondary contexts do not control compilation state
}

override markAsInProgress(): void {
// No-op: secondary contexts do not control compilation state
}

override async dispose(): Promise<void> {
// No-op for secondary context to avoid disposing the primary compilation worker
}
}
36 changes: 19 additions & 17 deletions packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { AngularCompilation, DiagnosticModes, NoopCompilation } from '../../angu
import { JavaScriptTransformer } from '../javascript-transformer';
import { LoadResultCache, createCachedLoad } from '../load-result-cache';
import { logCumulativeDurations, profileAsync, resetCumulativeDurations } from '../profiling';
import { SharedTSCompilationState, getSharedCompilationState } from './compilation-state';
import { AngularCompilationContext } from './compilation-state';
import { ComponentStylesheetBundler } from './component-stylesheets';
import { FileReferenceTracker } from './file-reference-tracker';
import { setupJitPluginCallbacks } from './jit-plugin-callbacks';
Expand Down Expand Up @@ -61,7 +61,10 @@ export interface CompilerPluginOptions {
// eslint-disable-next-line max-lines-per-function
export function createCompilerPlugin(
pluginOptions: CompilerPluginOptions,
compilationOrFactory: AngularCompilation | (() => Promise<AngularCompilation>),
compilationContextOrCompilation:
| AngularCompilationContext
| AngularCompilation
| (() => Promise<AngularCompilation>),
stylesheetBundler: ComponentStylesheetBundler,
): Plugin {
return {
Expand Down Expand Up @@ -111,10 +114,15 @@ export function createCompilerPlugin(

// The factory is only relevant for compatibility purposes with the private API.
// TODO: Update private API in the next major to allow compilation function factory removal here.
const compilation =
typeof compilationOrFactory === 'function'
? await compilationOrFactory()
: compilationOrFactory;
const angularCompilationContext =
compilationContextOrCompilation instanceof AngularCompilationContext
? compilationContextOrCompilation
: new AngularCompilationContext(
typeof compilationContextOrCompilation === 'function'
? await compilationContextOrCompilation()
: compilationContextOrCompilation,
);
const compilation: AngularCompilation = 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.
Expand All @@ -136,17 +144,12 @@ export function createCompilerPlugin(
// Determines if transpilation should be handle by TypeScript or esbuild
let useTypeScriptTranspilation = true;

let sharedTSCompilationState: SharedTSCompilationState | undefined;

// To fully invalidate files, track resource referenced files and their referencing source
const referencedFileTracker = new FileReferenceTracker();

// eslint-disable-next-line max-lines-per-function
build.onStart(async () => {
sharedTSCompilationState = getSharedCompilationState();
if (!(compilation instanceof NoopCompilation)) {
sharedTSCompilationState.markAsInProgress();
}
angularCompilationContext.markAsInProgress();

const result: OnStartResult = {
warnings: setupWarnings,
Expand Down Expand Up @@ -348,7 +351,7 @@ export function createCompilerPlugin(
}

if (compilation instanceof NoopCompilation) {
hasCompilationErrors = await sharedTSCompilationState.waitUntilReady;
hasCompilationErrors = await angularCompilationContext.waitUntilReady;

return result;
}
Expand Down Expand Up @@ -417,7 +420,7 @@ export function createCompilerPlugin(
// Reset the setup warnings so that they are only shown during the first build.
setupWarnings = undefined;

sharedTSCompilationState.markAsReady(hasCompilationErrors);
angularCompilationContext.markAsReady(hasCompilationErrors);

return result;
});
Expand Down Expand Up @@ -576,7 +579,7 @@ export function createCompilerPlugin(

build.onEnd((result) => {
// Ensure other compilations are unblocked if the main compilation throws during start
sharedTSCompilationState?.markAsReady(hasCompilationErrors);
angularCompilationContext.markAsReady(hasCompilationErrors);

for (const { outputFiles, metafile } of additionalResults.values()) {
// Add any additional output files to the main output files
Expand All @@ -598,8 +601,7 @@ export function createCompilerPlugin(
});

build.onDispose(() => {
sharedTSCompilationState?.dispose();
void compilation.close?.();
void angularCompilationContext.dispose();
void javascriptTransformer.close();
void cacheStore?.close();
});
Expand Down
Loading
Loading