Skip to content

Commit c57b288

Browse files
committed
fix(@angular/build): merge component stylesheet metafiles in a stable order
The compiler plugin merges each component stylesheet and web worker metafile into the main esbuild metafile by iterating `additionalResults` in insertion order. The map is populated as each bundle finishes, so the order varies from build to build. When two entries produce the same output file, for example two components whose stylesheets share a file name and content, or two identical images referenced from different stylesheets, the last merged entry wins and `outputs[<file>].inputs` in the stats file flips between builds of identical sources while every emitted file stays byte-identical. Sort the entries by key before merging so the result is the same on every build. Any consumer of the metafile from `onEnd` sees the same attribution. Fixes #34209
1 parent 73f6bbb commit c57b288

2 files changed

Lines changed: 60 additions & 1 deletion

File tree

‎packages/angular/build/src/builders/application/tests/options/stats-json_spec.ts‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,60 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
5252
harness.expectFile('dist/server-stats.json').toNotExist();
5353
});
5454

55+
it('attributes a shared component stylesheet output to the same input on every build', async () => {
56+
// Two components in different directories with byte-identical stylesheets of the same
57+
// file name bundle to the same output file. The merged metafile must attribute that output
58+
// to the same input regardless of which stylesheet bundle finishes first.
59+
const component = (dir: string) => `
60+
import { Component } from '@angular/core';
61+
@Component({
62+
selector: 'app-shared-${dir}',
63+
template: '<p>${dir}</p>',
64+
styleUrl: './shared.css',
65+
})
66+
export class Shared${dir.toUpperCase()}Component {}
67+
`;
68+
await harness.writeFiles({
69+
'src/app/a/shared.component.ts': component('a'),
70+
'src/app/a/shared.css': 'p { color: red; }',
71+
'src/app/b/shared.component.ts': component('b'),
72+
'src/app/b/shared.css': 'p { color: red; }',
73+
});
74+
await harness.writeFile(
75+
'src/app/app.module.ts',
76+
`
77+
import { NgModule } from '@angular/core';
78+
import { BrowserModule } from '@angular/platform-browser';
79+
import { AppComponent } from './app.component';
80+
import { SharedAComponent } from './a/shared.component';
81+
import { SharedBComponent } from './b/shared.component';
82+
@NgModule({
83+
declarations: [AppComponent],
84+
imports: [BrowserModule, SharedAComponent, SharedBComponent],
85+
bootstrap: [AppComponent],
86+
})
87+
export class AppModule {}
88+
`,
89+
);
90+
await harness.writeFile(
91+
'src/app/app.component.html',
92+
'<app-shared-a></app-shared-a><app-shared-b></app-shared-b>',
93+
);
94+
95+
harness.useTarget('build', {
96+
...BASE_OPTIONS,
97+
statsJson: true,
98+
});
99+
100+
for (let i = 0; i < 2; i++) {
101+
const { result } = await harness.executeOnce();
102+
expect(result?.success).toBeTrue();
103+
104+
const stats = JSON.parse(harness.readFile('dist/browser-stats.json'));
105+
expect(Object.keys(stats.outputs['shared.css'].inputs)).toEqual(['src/app/b/shared.css']);
106+
}
107+
});
108+
55109
describe('server build', () => {
56110
beforeEach(async () => {
57111
await harness.modifyFile('src/tsconfig.app.json', (content) => {

‎packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -626,7 +626,12 @@ export function createCompilerPlugin(
626626
angularCompilationContext.markAsReady(hasCompilationErrors);
627627
}
628628

629-
for (const { outputFiles, metafile } of additionalResults.values()) {
629+
// Merge in key order: the map fills in completion order, and when two entries share an
630+
// output file the last one merged wins, so the metafile would otherwise vary between builds.
631+
const sortedResults = [...additionalResults].sort(([a], [b]) =>
632+
a < b ? -1 : a > b ? 1 : 0,
633+
);
634+
for (const [, { outputFiles, metafile }] of sortedResults) {
630635
// Add any additional output files to the main output files
631636
if (outputFiles?.length) {
632637
result.outputFiles?.push(...outputFiles);

0 commit comments

Comments
 (0)