Skip to content

Commit 8981d8c

Browse files
committed
fix(@angular-devkit/build-angular): improve sharing of TypeScript compilation state between various esbuild instances during rebuilds
This commit improves the logic to block and share a TypeScript results across multiple esbuild instances (browser and server builds) which fixes an issue that previously during rebuilds in some cases did not block the build correctly.
1 parent 99d9037 commit 8981d8c

File tree

3 files changed

+153
-16
lines changed

3 files changed

+153
-16
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import type { logging } from '@angular-devkit/core';
10+
import { concatMap, count, firstValueFrom, timeout } from 'rxjs';
11+
import { buildApplication } from '../../index';
12+
import { OutputHashing } from '../../schema';
13+
import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup';
14+
15+
describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
16+
beforeEach(async () => {
17+
await harness.modifyFile('src/tsconfig.app.json', (content) => {
18+
const tsConfig = JSON.parse(content);
19+
tsConfig.files = ['main.server.ts', 'main.ts'];
20+
21+
return JSON.stringify(tsConfig);
22+
});
23+
24+
await harness.writeFiles({
25+
'src/lazy.ts': `export const foo: number = 1;`,
26+
'src/main.ts': `export async function fn () {
27+
const lazy = await import('./lazy');
28+
return lazy.foo;
29+
}`,
30+
'src/main.server.ts': `export { fn as default } from './main';`,
31+
});
32+
});
33+
34+
describe('Behavior: "Rebuild both server and browser bundles when using lazy loading"', () => {
35+
it('detect changes and errors when expected', async () => {
36+
harness.useTarget('build', {
37+
...BASE_OPTIONS,
38+
watch: true,
39+
namedChunks: true,
40+
outputHashing: OutputHashing.None,
41+
server: 'src/main.server.ts',
42+
ssr: true,
43+
});
44+
45+
const builderAbort = new AbortController();
46+
const buildCount = await firstValueFrom(
47+
harness.execute({ outputLogsOnFailure: true, signal: builderAbort.signal }).pipe(
48+
timeout(20_000),
49+
concatMap(async ({ result, logs }, index) => {
50+
switch (index) {
51+
case 0:
52+
expect(result?.success).toBeTrue();
53+
54+
// Add valid code
55+
await harness.appendToFile('src/lazy.ts', `console.log('foo');`);
56+
57+
break;
58+
case 1:
59+
expect(result?.success).toBeTrue();
60+
61+
// Update type of 'foo' to invalid (number -> string)
62+
await harness.writeFile('src/lazy.ts', `export const foo: string = 1;`);
63+
64+
break;
65+
case 2:
66+
expect(result?.success).toBeFalse();
67+
expect(logs).toContain(
68+
jasmine.objectContaining<logging.LogEntry>({
69+
message: jasmine.stringMatching(
70+
`Type 'number' is not assignable to type 'string'.`,
71+
),
72+
}),
73+
);
74+
75+
// Fix TS error
76+
await harness.writeFile('src/lazy.ts', `export const foo: string = "1";`);
77+
78+
break;
79+
case 3:
80+
expect(result?.success).toBeTrue();
81+
82+
builderAbort.abort();
83+
break;
84+
}
85+
}),
86+
count(),
87+
),
88+
);
89+
90+
expect(buildCount).toBe(4);
91+
});
92+
});
93+
});
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
export class SharedTSCompilationState {
10+
#pendingCompilation = true;
11+
#resolveCompilationReady: (() => void) | undefined;
12+
#compilationReadyPromise: Promise<void> | undefined;
13+
14+
get waitUntilReady(): Promise<void> {
15+
if (!this.#pendingCompilation) {
16+
return Promise.resolve();
17+
}
18+
19+
this.#compilationReadyPromise ??= new Promise((resolve) => {
20+
this.#resolveCompilationReady = resolve;
21+
});
22+
23+
return this.#compilationReadyPromise;
24+
}
25+
26+
markAsReady(): void {
27+
this.#resolveCompilationReady?.();
28+
this.#compilationReadyPromise = undefined;
29+
this.#pendingCompilation = false;
30+
}
31+
32+
markAsInProgress(): void {
33+
this.#pendingCompilation = true;
34+
}
35+
36+
dispose(): void {
37+
this.markAsReady();
38+
globalSharedCompilationState = undefined;
39+
}
40+
}
41+
42+
let globalSharedCompilationState: SharedTSCompilationState | undefined;
43+
44+
export function getSharedCompilationState(): SharedTSCompilationState {
45+
return (globalSharedCompilationState ??= new SharedTSCompilationState());
46+
}

packages/angular_devkit/build_angular/src/tools/esbuild/angular/compiler-plugin.ts

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
import { BundleStylesheetOptions } from '../stylesheets/bundle-options';
3232
import { AngularHostOptions } from './angular-host';
3333
import { AngularCompilation, AotCompilation, JitCompilation, NoopCompilation } from './compilation';
34+
import { SharedTSCompilationState, getSharedCompilationState } from './compilation-state';
3435
import { ComponentStylesheetBundler } from './component-stylesheets';
3536
import { setupJitPluginCallbacks } from './jit-plugin-callbacks';
3637
import { SourceFileCache } from './source-file-cache';
@@ -48,29 +49,18 @@ export interface CompilerPluginOptions {
4849
loadResultCache?: LoadResultCache;
4950
}
5051

51-
// TODO: find a better way to unblock TS compilation of server bundles.
52-
let TS_COMPILATION_READY: Promise<void> | undefined;
53-
5452
// eslint-disable-next-line max-lines-per-function
5553
export function createCompilerPlugin(
5654
pluginOptions: CompilerPluginOptions,
5755
styleOptions: BundleStylesheetOptions & { inlineStyleLanguage: string },
5856
): Plugin {
59-
let resolveCompilationReady: (() => void) | undefined;
60-
61-
if (!pluginOptions.noopTypeScriptCompilation) {
62-
TS_COMPILATION_READY = new Promise<void>((resolve) => {
63-
resolveCompilationReady = resolve;
64-
});
65-
}
66-
6757
return {
6858
name: 'angular-compiler',
6959
// eslint-disable-next-line max-lines-per-function
7060
async setup(build: PluginBuild): Promise<void> {
7161
let setupWarnings: PartialMessage[] | undefined = [];
72-
7362
const preserveSymlinks = build.initialOptions.preserveSymlinks;
63+
7464
let tsconfigPath = pluginOptions.tsconfig;
7565
if (!preserveSymlinks) {
7666
// Use the real path of the tsconfig if not preserving symlinks.
@@ -112,8 +102,14 @@ export function createCompilerPlugin(
112102
styleOptions,
113103
pluginOptions.loadResultCache,
114104
);
105+
let sharedTSCompilationState: SharedTSCompilationState | undefined;
115106

116107
build.onStart(async () => {
108+
sharedTSCompilationState = getSharedCompilationState();
109+
if (!(compilation instanceof NoopCompilation)) {
110+
sharedTSCompilationState.markAsInProgress();
111+
}
112+
117113
const result: OnStartResult = {
118114
warnings: setupWarnings,
119115
};
@@ -259,7 +255,7 @@ export function createCompilerPlugin(
259255
shouldTsIgnoreJs = !allowJs;
260256

261257
if (compilation instanceof NoopCompilation) {
262-
await TS_COMPILATION_READY;
258+
await sharedTSCompilationState.waitUntilReady;
263259

264260
return result;
265261
}
@@ -287,8 +283,7 @@ export function createCompilerPlugin(
287283
// Reset the setup warnings so that they are only shown during the first build.
288284
setupWarnings = undefined;
289285

290-
// TODO: find a better way to unblock TS compilation of server bundles.
291-
resolveCompilationReady?.();
286+
sharedTSCompilationState.markAsReady();
292287

293288
return result;
294289
});
@@ -388,7 +383,10 @@ export function createCompilerPlugin(
388383
logCumulativeDurations();
389384
});
390385

391-
build.onDispose(() => void stylesheetBundler.dispose());
386+
build.onDispose(() => {
387+
sharedTSCompilationState?.dispose();
388+
void stylesheetBundler.dispose();
389+
});
392390
},
393391
};
394392
}

0 commit comments

Comments
 (0)