Skip to content

fix(@angular/build): merge component stylesheet metafiles in a stable order - #34210

Open
sdjayna wants to merge 3 commits into
angular:mainfrom
sdjayna:fix-build-metafile-merge-order
Open

sdjayna wants to merge 3 commits into
angular:mainfrom
sdjayna:fix-build-metafile-merge-order

Conversation

@sdjayna

@sdjayna sdjayna commented Sep 30, 2026 •

Copy link
Copy Markdown

Build output is non-deterministic: two builds of identical sources can write different stats files. Merging the component stylesheet metafiles in a stable order fixes it.

PR Checklist

PR Type

  • Bugfix

What is the current behavior?

Issue Number: #34209

outputs[<file>].inputs in the stats file changes between builds of the same sources. The cause is one loop in build.onEnd: the compiler plugin merges each component stylesheet's metafile into the main metafile in the order the stylesheets finished bundling, and when two stylesheets produce the same output file, the last one merged owns the entry. Every emitted file is byte-identical; only the metadata moves.

What is the new behavior?

The entries are sorted by key before merging, so the merged metafile is the same on every build.

The new spec builds two components in different directories with identical stylesheets, builds twice, and asserts the shared output is attributed to the same input both times.

Does this PR introduce a breaking change?

  • No

Other information

Measured on 22.1.9 with the reproduction at https://github.com/sdjayna/angular-metafile-merge-order-repro (60 components, 12 identical-stylesheet pairs, 4 identical-image pairs): without the change 5 of 5 build pairs differed, in 7 to 10 of 54 outputs entries; with it, 0 of 3 differed and the stats file was byte-identical. The same loop is on main today. The sort runs once per build over one entry per stylesheet, so its cost does not register against the bundling.

… 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 angular#34209

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request ensures deterministic metafile generation by sorting additional compilation results by key order before merging, preventing non-deterministic output attribution caused by completion-order variations. A test has been added to verify this behavior. The feedback suggests that if the merging logic is eventually updated to merge duplicate output inputs rather than overwriting them, the test assertion should be updated to expect both input files.

Comment thread packages/angular/build/src/builders/application/tests/options/stats-json_spec.ts Outdated
@alan-agius4

Copy link
Copy Markdown
Collaborator

Thanks for investigating this! However, there are a couple of concerns with this approach:

  1. Stats accuracy: outputs[file].inputs in an esbuild metafile is intended to list all input source files that contributed to that output file. By sorting and continuing to shallowly overwrite with Object.assign(result.metafile.outputs, metafile.outputs), we deterministically drop inputs (as seen in the test where src/app/a/shared.css is completely missing from outputs['shared.css'].inputs). When an output already exists in result.metafile.outputs, we should merge existingOutput.inputs rather than replacing the entire output entry.
  2. Performance: [...additionalResults].sort(...) creates and sorts an array of [key, value] tuples across all component stylesheets on every build and rebuild in the critical path, even when statsJson is disabled. In large apps with thousands of components, this introduces unnecessary allocations and overhead. If deterministic key ordering for stats.json is required, that can be done when generating/serializing the stats JSON (where options.stats is enabled) rather than in the compilation plugin's onEnd callback.

@sdjayna

sdjayna commented Sep 30, 2026

Copy link
Copy Markdown
Author

Both points taken, and this replaces what I said to the bot above. Pushed as two fixups.

  • The merge now unions inputs when an output already exists, and builds a new object for the merged entry. The new object matters: the stylesheet bundler caches its result across rebuilds and the clone shares each output's inputs, so assigning in place carried one stylesheet's input into the other's entry on later rebuilds. The sort is gone.
  • The stats files are serialised with sorted keys, behind options.stats. onEnd is otherwise as it was.
  • Three specs: both inputs listed on every build; both stats files sorted; and a five-build watch test that changes one of two identical stylesheets, restores it, and changes it again, the ordering that exposes an in-place merge.

Each spec fails against the code it guards, and the application target passes in full. One detail for the record: unminified CSS carries a source-path comment, so identical stylesheets share a hashed name only when styles are minified, which is why this stayed hidden in development builds.

@alan-agius4 alan-agius4 added the action: review The PR is still awaiting reviews from at least one requested reviewer label Oct 1, 2026
@alan-agius4
alan-agius4 self-requested a review October 1, 2026 15:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

action: review The PR is still awaiting reviews from at least one requested reviewer area: @angular/build

Projects

None yet

Development

Successfully merging this pull request may close these issues.

stats.json is non-deterministic when two component stylesheets produce the same output file

2 participants