Skip to content

Commit abde078

Browse files
committed
refactor(@angular/build): improve typescript bundling context rebuild checking
The TypeScript-based bundling contexts have now been separated from the other bundling contexts that may be present in an application build. The later of which include global styles, scripts, and non-TypeScript polyfills. This allows for the TypeScript bundling contexts to perform additional checks during a rebuild to determine if they actually need to be rebundled. The bundling context caching for the TypeScript contexts has not yet been enabled but these changes prepare for the switch to allow conditional rebundling on file changes.
1 parent 69768bd commit abde078

File tree

6 files changed

+92
-46
lines changed

6 files changed

+92
-46
lines changed

packages/angular/build/src/builders/application/execute-build.ts

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -74,37 +74,48 @@ export async function executeBuild(
7474
let bundlerContexts;
7575
let componentStyleBundler;
7676
let codeBundleCache;
77+
let bundlingResult: BundleContextResult;
7778
if (rebuildState) {
7879
bundlerContexts = rebuildState.rebuildContexts;
7980
componentStyleBundler = rebuildState.componentStyleBundler;
8081
codeBundleCache = rebuildState.codeBundleCache;
82+
const allFileChanges = rebuildState.fileChanges.all;
83+
84+
// Bundle all contexts that do not require TypeScript changed file checks.
85+
// These will automatically use cached results based on the changed files.
86+
bundlingResult = await BundlerContext.bundleAll(bundlerContexts.otherContexts, allFileChanges);
87+
88+
// Check the TypeScript code bundling cache for changes. If invalid, force a rebundle of
89+
// all TypeScript related contexts.
90+
// TODO: Enable cached bundling for the typescript contexts
91+
const forceTypeScriptRebuild = codeBundleCache?.invalidate(allFileChanges);
92+
const typescriptResults: BundleContextResult[] = [];
93+
for (const typescriptContext of bundlerContexts.typescriptContexts) {
94+
typescriptContext.invalidate(allFileChanges);
95+
const result = await typescriptContext.bundle(forceTypeScriptRebuild);
96+
typescriptResults.push(result);
97+
}
98+
bundlingResult = BundlerContext.mergeResults([bundlingResult, ...typescriptResults]);
8199
} else {
82100
const target = transformSupportedBrowsersToTargets(browsers);
83101
codeBundleCache = new SourceFileCache(cacheOptions.enabled ? cacheOptions.path : undefined);
84102
componentStyleBundler = createComponentStyleBundler(options, target);
85103
bundlerContexts = setupBundlerContexts(options, target, codeBundleCache, componentStyleBundler);
86-
}
87104

88-
let bundlingResult = await BundlerContext.bundleAll(
89-
bundlerContexts,
90-
rebuildState?.fileChanges.all,
91-
);
105+
// Bundle everything on initial build
106+
bundlingResult = await BundlerContext.bundleAll([
107+
...bundlerContexts.typescriptContexts,
108+
...bundlerContexts.otherContexts,
109+
]);
110+
}
92111

112+
// Update any external component styles if enabled and rebuilding.
113+
// TODO: Only attempt rebundling of invalidated styles once incremental build results are supported.
93114
if (rebuildState && options.externalRuntimeStyles) {
94-
const invalidatedStylesheetEntries = componentStyleBundler.invalidate(
95-
rebuildState.fileChanges.all,
96-
);
97-
98-
if (invalidatedStylesheetEntries?.length) {
99-
const componentResults: BundleContextResult[] = [];
100-
for (const stylesheetFile of invalidatedStylesheetEntries) {
101-
// externalId is already linked in the bundler context so only enabling is required here
102-
const result = await componentStyleBundler.bundleFile(stylesheetFile, true, true);
103-
componentResults.push(result);
104-
}
115+
componentStyleBundler.invalidate(rebuildState.fileChanges.all);
105116

106-
bundlingResult = BundlerContext.mergeResults([bundlingResult, ...componentResults]);
107-
}
117+
const componentResults = await componentStyleBundler.bundleAllFiles(true, true);
118+
bundlingResult = BundlerContext.mergeResults([bundlingResult, ...componentResults]);
108119
}
109120

110121
if (options.optimizationOptions.scripts && shouldOptimizeChunks) {

packages/angular/build/src/builders/application/setup-bundling.ts

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@ export function setupBundlerContexts(
3434
target: string[],
3535
codeBundleCache: SourceFileCache,
3636
stylesheetBundler: ComponentStylesheetBundler,
37-
): BundlerContext[] {
37+
): {
38+
typescriptContexts: BundlerContext[];
39+
otherContexts: BundlerContext[];
40+
} {
3841
const {
3942
outputMode,
4043
serverEntryPoint,
@@ -44,10 +47,11 @@ export function setupBundlerContexts(
4447
workspaceRoot,
4548
watch = false,
4649
} = options;
47-
const bundlerContexts = [];
50+
const typescriptContexts = [];
51+
const otherContexts = [];
4852

4953
// Browser application code
50-
bundlerContexts.push(
54+
typescriptContexts.push(
5155
new BundlerContext(
5256
workspaceRoot,
5357
watch,
@@ -63,17 +67,24 @@ export function setupBundlerContexts(
6367
stylesheetBundler,
6468
);
6569
if (browserPolyfillBundleOptions) {
66-
bundlerContexts.push(new BundlerContext(workspaceRoot, watch, browserPolyfillBundleOptions));
70+
const browserPolyfillContext = new BundlerContext(
71+
workspaceRoot,
72+
watch,
73+
browserPolyfillBundleOptions,
74+
);
75+
if (typeof browserPolyfillBundleOptions === 'function') {
76+
otherContexts.push(browserPolyfillContext);
77+
} else {
78+
typescriptContexts.push(browserPolyfillContext);
79+
}
6780
}
6881

6982
// Global Stylesheets
7083
if (options.globalStyles.length > 0) {
7184
for (const initial of [true, false]) {
7285
const bundleOptions = createGlobalStylesBundleOptions(options, target, initial);
7386
if (bundleOptions) {
74-
bundlerContexts.push(
75-
new BundlerContext(workspaceRoot, watch, bundleOptions, () => initial),
76-
);
87+
otherContexts.push(new BundlerContext(workspaceRoot, watch, bundleOptions, () => initial));
7788
}
7889
}
7990
}
@@ -83,9 +94,7 @@ export function setupBundlerContexts(
8394
for (const initial of [true, false]) {
8495
const bundleOptions = createGlobalScriptsBundleOptions(options, target, initial);
8596
if (bundleOptions) {
86-
bundlerContexts.push(
87-
new BundlerContext(workspaceRoot, watch, bundleOptions, () => initial),
88-
);
97+
otherContexts.push(new BundlerContext(workspaceRoot, watch, bundleOptions, () => initial));
8998
}
9099
}
91100
}
@@ -94,7 +103,7 @@ export function setupBundlerContexts(
94103
if (serverEntryPoint && (outputMode || prerenderOptions || appShellOptions || ssrOptions)) {
95104
const nodeTargets = [...target, ...getSupportedNodeTargets()];
96105

97-
bundlerContexts.push(
106+
typescriptContexts.push(
98107
new BundlerContext(
99108
workspaceRoot,
100109
watch,
@@ -104,7 +113,7 @@ export function setupBundlerContexts(
104113

105114
if (outputMode && ssrOptions?.entry) {
106115
// New behavior introduced: 'server.ts' is now bundled separately from 'main.server.ts'.
107-
bundlerContexts.push(
116+
typescriptContexts.push(
108117
new BundlerContext(
109118
workspaceRoot,
110119
watch,
@@ -121,11 +130,11 @@ export function setupBundlerContexts(
121130
);
122131

123132
if (serverPolyfillBundleOptions) {
124-
bundlerContexts.push(new BundlerContext(workspaceRoot, watch, serverPolyfillBundleOptions));
133+
otherContexts.push(new BundlerContext(workspaceRoot, watch, serverPolyfillBundleOptions));
125134
}
126135
}
127136

128-
return bundlerContexts;
137+
return { typescriptContexts, otherContexts };
129138
}
130139

131140
export function createComponentStyleBundler(

packages/angular/build/src/tools/esbuild/angular/component-stylesheets.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,14 @@ export class ComponentStylesheetBundler {
8686
);
8787
}
8888

89+
bundleAllFiles(external: boolean, direct: boolean) {
90+
return Promise.all(
91+
Array.from(this.#fileContexts.entries()).map(([entry]) =>
92+
this.bundleFile(entry, external, direct),
93+
),
94+
);
95+
}
96+
8997
async bundleInline(
9098
data: string,
9199
filename: string,

packages/angular/build/src/tools/esbuild/angular/source-file-cache.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,21 +25,29 @@ export class SourceFileCache extends Map<string, ts.SourceFile> {
2525
super();
2626
}
2727

28-
invalidate(files: Iterable<string>): void {
28+
invalidate(files: Iterable<string>): boolean {
2929
if (files !== this.modifiedFiles) {
3030
this.modifiedFiles.clear();
3131
}
32+
33+
const extraWatchFiles = new Set(this.referencedFiles?.map(path.normalize));
34+
35+
let invalid = false;
3236
for (let file of files) {
3337
file = path.normalize(file);
34-
this.loadResultCache.invalidate(file);
38+
invalid = this.loadResultCache.invalidate(file) || invalid;
3539

3640
// Normalize separators to allow matching TypeScript Host paths
3741
if (USING_WINDOWS) {
3842
file = file.replace(WINDOWS_SEP_REGEXP, path.posix.sep);
3943
}
4044

41-
this.delete(file);
45+
invalid = this.delete(file) || invalid;
4246
this.modifiedFiles.add(file);
47+
48+
invalid = extraWatchFiles.has(file) || invalid;
4349
}
50+
51+
return invalid;
4452
}
4553
}

packages/angular/build/src/tools/esbuild/application-code-bundle.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { BundlerOptionsFactory } from './bundler-context';
2424
import { createCompilerPluginOptions } from './compiler-plugin-options';
2525
import { createExternalPackagesPlugin } from './external-packages-plugin';
2626
import { createAngularLocaleDataPlugin } from './i18n-locale-plugin';
27+
import type { LoadResultCache } from './load-result-cache';
2728
import { createLoaderImportAttributePlugin } from './loader-import-attribute-plugin';
2829
import { createRxjsEsmResolutionPlugin } from './rxjs-esm-resolution-plugin';
2930
import { createServerBundleMetadata } from './server-bundle-metadata-plugin';
@@ -106,7 +107,7 @@ export function createBrowserPolyfillBundleOptions(
106107
options,
107108
namespace,
108109
true,
109-
sourceFileCache,
110+
sourceFileCache.loadResultCache,
110111
);
111112
if (!polyfillBundleOptions) {
112113
return;
@@ -184,7 +185,7 @@ export function createServerPolyfillBundleOptions(
184185
},
185186
namespace,
186187
false,
187-
sourceFileCache,
188+
sourceFileCache?.loadResultCache,
188189
);
189190

190191
if (!polyfillBundleOptions) {
@@ -602,7 +603,7 @@ function getEsBuildCommonPolyfillsOptions(
602603
options: NormalizedApplicationBuildOptions,
603604
namespace: string,
604605
tryToResolvePolyfillsAsRelative: boolean,
605-
sourceFileCache: SourceFileCache | undefined,
606+
loadResultCache: LoadResultCache | undefined,
606607
): BuildOptions | undefined {
607608
const { jit, workspaceRoot, i18nOptions } = options;
608609
const buildOptions: BuildOptions = {
@@ -647,7 +648,7 @@ function getEsBuildCommonPolyfillsOptions(
647648
buildOptions.plugins?.push(
648649
createVirtualModulePlugin({
649650
namespace,
650-
cache: sourceFileCache?.loadResultCache,
651+
cache: loadResultCache,
651652
loadContent: async (_, build) => {
652653
let polyfillPaths = polyfills;
653654
let warnings: PartialMessage[] | undefined;

packages/angular/build/src/tools/esbuild/bundler-execution-result.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ export interface BuildOutputAsset {
2020
}
2121

2222
export interface RebuildState {
23-
rebuildContexts: BundlerContext[];
23+
rebuildContexts: {
24+
typescriptContexts: BundlerContext[];
25+
otherContexts: BundlerContext[];
26+
};
2427
componentStyleBundler: ComponentStylesheetBundler;
2528
codeBundleCache?: SourceFileCache;
2629
fileChanges: ChangedFiles;
@@ -51,7 +54,10 @@ export class ExecutionResult {
5154
htmlBaseHref?: string;
5255

5356
constructor(
54-
private rebuildContexts: BundlerContext[],
57+
private rebuildContexts: {
58+
typescriptContexts: BundlerContext[];
59+
otherContexts: BundlerContext[];
60+
},
5561
private componentStyleBundler: ComponentStylesheetBundler,
5662
private codeBundleCache?: SourceFileCache,
5763
) {}
@@ -141,7 +147,9 @@ export class ExecutionResult {
141147

142148
get watchFiles() {
143149
// Bundler contexts internally normalize file dependencies
144-
const files = this.rebuildContexts.flatMap((context) => [...context.watchFiles]);
150+
const files = this.rebuildContexts.typescriptContexts
151+
.flatMap((context) => [...context.watchFiles])
152+
.concat(this.rebuildContexts.otherContexts.flatMap((context) => [...context.watchFiles]));
145153
if (this.codeBundleCache?.referencedFiles) {
146154
// These files originate from TS/NG and can have POSIX path separators even on Windows.
147155
// To ensure path comparisons are valid, all these paths must be normalized.
@@ -156,8 +164,6 @@ export class ExecutionResult {
156164
}
157165

158166
createRebuildState(fileChanges: ChangedFiles): RebuildState {
159-
this.codeBundleCache?.invalidate([...fileChanges.modified, ...fileChanges.removed]);
160-
161167
return {
162168
rebuildContexts: this.rebuildContexts,
163169
codeBundleCache: this.codeBundleCache,
@@ -180,7 +186,10 @@ export class ExecutionResult {
180186
}
181187

182188
async dispose(): Promise<void> {
183-
await Promise.allSettled(this.rebuildContexts.map((context) => context.dispose()));
184-
await this.componentStyleBundler.dispose();
189+
await Promise.allSettled([
190+
...this.rebuildContexts.typescriptContexts.map((context) => context.dispose()),
191+
...this.rebuildContexts.otherContexts.map((context) => context.dispose()),
192+
this.componentStyleBundler.dispose(),
193+
]);
185194
}
186195
}

0 commit comments

Comments
 (0)