Skip to content

Commit 0a0c5e7

Browse files
committed
perf(@angular/build): optimize incremental watch rebuilds in AOT compilation
- Pre-populate `.d.ts` shape signatures on the previous `BuilderProgram` for modified files prior to diffing when type-checking or TypeScript transpilation is active, preventing false-positive `.d.ts` cascades on incremental watch edits. - Synchronize `typeScriptProgram` with `angularCompiler.getCurrentProgram()` after `collectDiagnostics()` and clear uncommitted `changedFilesSet` and `oldSignatures` entries so subsequent incremental builds preserve `.d.ts` signatures and do not treat `.ngtypecheck.ts` shims as newly added files. - Detect affected dependent `.ngtypecheck.ts` shims in `isolatedModules` / `verbatimModuleSyntax` mode via TypeScript's `state.hasCalledUpdateShapeSignature`, ensuring cross-component template diagnostics are re-evaluated when a dependency's `.d.ts` shape changes without cascading on private edits. - Avoid `filter().flatMap()` allocations and skip `getResourceDependencies()` for declaration files when collecting `referencedFiles`.
1 parent dcf906a commit 0a0c5e7

2 files changed

Lines changed: 376 additions & 82 deletions

File tree

‎packages/angular/build/src/builders/application/tests/behavior/rebuild-errors_spec.ts‎

Lines changed: 111 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,6 @@ import {
1515
expectNoLog,
1616
} from '../setup';
1717

18-
/**
19-
* Maximum time in milliseconds for single build/rebuild
20-
* This accounts for CI variability.
21-
*/
22-
export const BUILD_TIMEOUT = 30_000;
23-
2418
describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
2519
describe('Behavior: "Rebuild Error Detection"', () => {
2620
it('detects template errors with no AOT codegen or TS emit differences', async () => {
@@ -128,6 +122,116 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
128122
);
129123
});
130124

125+
it('detects template errors across components on watch rebuild when isolatedModules is enabled', async () => {
126+
harness.useTarget('build', {
127+
...BASE_OPTIONS,
128+
watch: true,
129+
});
130+
131+
await harness.modifyFile('tsconfig.json', (content) => {
132+
const tsconfig = JSON.parse(content);
133+
tsconfig.compilerOptions = {
134+
...tsconfig.compilerOptions,
135+
isolatedModules: true,
136+
};
137+
138+
return JSON.stringify(tsconfig);
139+
});
140+
141+
const goodChildComponentContents = `
142+
import { Component, Input } from '@angular/core';
143+
@Component({
144+
selector: 'child',
145+
standalone: true,
146+
template: '<p>{{ value }}</p>',
147+
})
148+
export class ChildComponent {
149+
@Input() value!: number;
150+
}
151+
`;
152+
153+
const typeErrorText = `Type 'number' is not assignable to type 'string'.`;
154+
155+
await harness.writeFiles({
156+
'src/app/child.component.ts': goodChildComponentContents,
157+
'src/app/app.module.ts': `
158+
import { NgModule } from '@angular/core';
159+
import { BrowserModule } from '@angular/platform-browser';
160+
import { AppComponent } from './app.component';
161+
@NgModule({
162+
imports: [
163+
BrowserModule,
164+
AppComponent,
165+
],
166+
providers: [],
167+
bootstrap: [AppComponent]
168+
})
169+
export class AppModule { }
170+
`,
171+
'src/app/app.component.ts': `
172+
import { Component } from '@angular/core';
173+
import { ChildComponent } from './child.component';
174+
@Component({
175+
selector: 'app-root',
176+
standalone: true,
177+
imports: [ChildComponent],
178+
template: '<child [value]="123" />',
179+
})
180+
export class AppComponent {}
181+
`,
182+
});
183+
184+
await harness.executeWithCases(
185+
[
186+
async ({ result }) => {
187+
expect(result?.success).toBeTrue();
188+
189+
// Update child component to change input type from number to string
190+
await harness.writeFile(
191+
'src/app/child.component.ts',
192+
`
193+
import { Component, Input } from '@angular/core';
194+
@Component({
195+
selector: 'child',
196+
standalone: true,
197+
template: '<p>{{ value }}</p>',
198+
})
199+
export class ChildComponent {
200+
@Input() value!: string;
201+
}
202+
`,
203+
);
204+
},
205+
async ({ result, logs }) => {
206+
expect(result?.success).toBeFalse();
207+
expectLog(logs, typeErrorText);
208+
209+
// Make an unrelated change to verify error persists
210+
await harness.modifyFile('src/main.ts', (content) => content + '\n');
211+
},
212+
async ({ result, logs }) => {
213+
expect(result?.success).toBeFalse();
214+
expectLog(logs, typeErrorText);
215+
216+
// Revert back to number
217+
await harness.writeFile('src/app/child.component.ts', goodChildComponentContents);
218+
},
219+
async ({ result, logs }) => {
220+
expect(result?.success).toBeTrue();
221+
expectNoLog(logs, typeErrorText);
222+
223+
// Make an unrelated change to verify error cache cleared
224+
await harness.modifyFile('src/main.ts', (content) => content + '\n');
225+
},
226+
({ result, logs }) => {
227+
expect(result?.success).toBeTrue();
228+
expectNoLog(logs, typeErrorText);
229+
},
230+
],
231+
{ outputLogsOnFailure: false },
232+
);
233+
});
234+
131235
it('detects cumulative block syntax errors', async () => {
132236
harness.useTarget('build', {
133237
...BASE_OPTIONS,
@@ -234,7 +338,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
234338
await harness.appendToFile('src/app/app.component.html', '<div>Guten Tag</div>');
235339
},
236340
({ logs }) => {
237-
expectNoLog(logs, 'invalid-css-content');
341+
expectNoLog(logs, 'Unexpected character "EOF"');
238342

239343
harness.expectFile('dist/browser/main.js').content.toContain('Hello, world!');
240344
harness.expectFile('dist/browser/main.js').content.toContain('Guten Tag');

0 commit comments

Comments
 (0)