Skip to content

Commit 0042601

Browse files
committed
fixup! fix(@angular/build): merge component stylesheet metafiles in a stable order
1 parent c57b288 commit 0042601

3 files changed

Lines changed: 37 additions & 13 deletions

File tree

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

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,25 @@ import { createComponentStyleBundler, setupBundlerContexts } from './setup-bundl
4343
/** The esbuild error text prefix used to detect top-level await errors. */
4444
const TOP_LEVEL_AWAIT_ERROR_TEXT =
4545
'Top-level await is not available in the configured target environment';
46+
/**
47+
* Serializes a metafile with object keys sorted so the stats file is identical across builds.
48+
* Arrays keep their order.
49+
*/
50+
function stableStringify(value: unknown): string {
51+
return JSON.stringify(
52+
value,
53+
(_key, current: unknown) =>
54+
current && typeof current === 'object' && !Array.isArray(current)
55+
? Object.fromEntries(
56+
Object.keys(current)
57+
.sort()
58+
.map((key) => [key, (current as Record<string, unknown>)[key]]),
59+
)
60+
: current,
61+
2,
62+
);
63+
}
64+
4665
// eslint-disable-next-line max-lines-per-function
4766
export async function executeBuild(
4867
options: NormalizedApplicationBuildOptions,
@@ -394,14 +413,14 @@ export async function executeBuild(
394413
if (options.stats) {
395414
executionResult.addOutputFile(
396415
'browser-stats.json',
397-
JSON.stringify(browserMetafile, null, 2),
416+
stableStringify(browserMetafile),
398417
BuildOutputFileType.Root,
399418
);
400419

401420
if (serverEntryPoint) {
402421
executionResult.addOutputFile(
403422
'server-stats.json',
404-
JSON.stringify(serverMetafile, null, 2),
423+
stableStringify(serverMetafile),
405424
BuildOutputFileType.Root,
406425
);
407426
}

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,9 @@ 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 () => {
55+
it('attributes a shared component stylesheet output to every input on every build', async () => {
5656
// 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.
57+
// file name bundle to the same output file. The merged metafile must list both inputs.
5958
const component = (dir: string) => `
6059
import { Component } from '@angular/core';
6160
@Component({
@@ -102,7 +101,10 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
102101
expect(result?.success).toBeTrue();
103102

104103
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']);
104+
expect(Object.keys(stats.outputs['shared.css'].inputs)).toEqual([
105+
'src/app/a/shared.css',
106+
'src/app/b/shared.css',
107+
]);
106108
}
107109
});
108110

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

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

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) {
629+
for (const { outputFiles, metafile } of additionalResults.values()) {
635630
// Add any additional output files to the main output files
636631
if (outputFiles?.length) {
637632
result.outputFiles?.push(...outputFiles);
@@ -643,7 +638,15 @@ export function createCompilerPlugin(
643638
// mitigating significant performance overhead for large apps.
644639
// See: https://bugs.chromium.org/p/v8/issues/detail?id=11536
645640
Object.assign(result.metafile.inputs, metafile.inputs);
646-
Object.assign(result.metafile.outputs, metafile.outputs);
641+
for (const [outputPath, output] of Object.entries(metafile.outputs)) {
642+
// Two stylesheets can bundle to the same output file. Union the inputs into a new
643+
// object so every input is attributed and the bundler's cached metafile is untouched.
644+
const existing: Metafile['outputs'][string] | undefined =
645+
result.metafile.outputs[outputPath];
646+
result.metafile.outputs[outputPath] = existing
647+
? { ...existing, inputs: { ...existing.inputs, ...output.inputs } }
648+
: output;
649+
}
647650
}
648651
}
649652

0 commit comments

Comments
 (0)