An Angular CLI app built with ng build -c staging ships unhashed file names and skips budgets; why, and how should angular.json be fixed?
answer
- default applies only when none named
- options plus the named ones
- left to right, last wins
- whole keys, not deep merge
basics
~20 sAn Angular CLI configuration named with -c replaces defaultConfiguration instead of stacking on it, so staging gets only the base options and its own overrides. Repeat production's settings in staging, or build with -c production,staging.
solid answer
~40 sThe CLI resolves a target's options as the base `options` object, then each **named** configuration spread over it in order. `defaultConfiguration` is used only when no configuration is named, so `ng build -c staging` never applies `production`. The generated `production` configuration is where `budgets` and `outputHashing: "all"` live, so a `staging` configuration that holds only `fileReplacements` builds with the builder default `outputHashing: "none"` and no budgets, while `optimization` stays on only because the builder defaults it to `true`. Two fixes work: copy the production settings into `staging`, or list both, `ng build -c production,staging`, which applies them left to right with the last value winning. The merge is shallow: a key set in `staging` replaces the whole value from `production`, so two `fileReplacements` arrays do not combine; the later one wins outright.
code
bash · 8 lines# options + staging only: no production budgets, outputHashing falls back to "none"
ng build -c staging
# options + production + staging, applied left to right, last value wins per key
ng build -c production,staging
# the same layering through the long form
ng run my-app:build:production,staginggo deeper
Recall that -c names the configuration to use and that several names can be given separated by commas.
Explain that defaultConfiguration applies only when none is named and that configurations spread over options left to right, last value winning.
Diagnose environment-only regressions by reconstructing effective options, and choose between self-contained configurations, layering and moving shared settings into options.
Set a workspace convention for where shared build settings live so new deployable configurations cannot silently lose hashing or budgets.
## The symptom After a team adds a `staging` configuration for its own environment file, the staging deployment starts serving stale JavaScript after each release, because files are called `main.js` instead of `main-<hash>.js`, and a bundle that would break the production budget passes staging without a warning. Production builds are fine. ## How the CLI resolves options For `ng build`, `ng serve` or `ng run`, Architect computes the options handed to the builder like this: 1. Take the target's **`options`** object. 2. Decide the configuration list: the value of `--configuration` if given, otherwise the target's **`defaultConfiguration`**. 3. Split that value on commas and, for each name **in order**, spread the configuration's object over the current options. 4. Anything still unset falls back to the builder schema's default. Two consequences drive the bug: - **A named configuration replaces the default.** Step 2 uses `defaultConfiguration` only when nothing is named. `-c staging` therefore means `options + staging`, never `options + production + staging`. - **The merge is shallow.** Each configuration's top-level keys overwrite the previous value entirely. Arrays such as `budgets` or `fileReplacements`, and objects such as `optimization`, are replaced, not merged. ## Why staging lost hashing and budgets | Option | Where production gets it | What staging got | |---|---|---| | `outputHashing` | `production` configuration: `"all"` | builder default: `"none"` | | `budgets` | `production` configuration | builder default: none | | `optimization` | builder default: `true` | builder default: `true` | | `sourceMap` | builder default: `false` | builder default: `false` | | `fileReplacements` | none | `staging` configuration | Optimization looked right only by accident of the builder's defaults, which is why the problem went unnoticed. ## Fix A: make staging self-contained Repeat what production sets: ```json "staging": { "budgets": [ { "type": "initial", "maximumWarning": "500kB", "maximumError": "1MB" } ], "outputHashing": "all", "fileReplacements": [ { "replace": "src/environments/environment.ts", "with": "src/environments/environment.staging.ts" } ] } ``` The configuration is explicit and reads on its own, at the cost of duplicated values that can drift apart. ## Fix B: layer configurations Keep `staging` minimal and build with: ```bash ng build -c production,staging ``` Production's settings apply first and staging's overrides second. Watch the shallow merge: if `production` later gains its own `fileReplacements`, staging's array **replaces** it entirely, so a replacement production relied on silently disappears from staging builds. For the same reason, a staging configuration cannot add a single budget to production's list; it must restate the whole `budgets` array. ## Fix C: change what the default is for Some teams make `options` hold everything that is common to deployable builds (hashing, budgets) and keep configurations for the genuine differences. Then `development` explicitly switches those settings off, and any new deployable configuration inherits them automatically. This is the most robust against the next configuration someone adds. ## Why the CLI resolves options this way Configurations are designed as **independent, named sets of overrides** for one target, not as an inheritance chain. That keeps each run predictable: the options a build receives are exactly the base options plus the configurations you named, in the order you named them, plus any command-line flags. There is no hidden parent. The flip side is that nothing is inherited implicitly, so a configuration that should behave like production must either say so itself or be layered after `production` explicitly. `defaultConfiguration` is a convenience for the command you type most, not a base class. ## Checking a fix - Build with the exact command CI uses and inspect the output file names for hashes. - Temporarily lower a budget to confirm the staging build reports it. - Remember that `ng serve -c staging` resolves through the serve target's `buildTarget`, so the same rules apply to the build configuration it names. ## The general rule A configuration is a **patch over `options`**, not over another configuration. If two configurations must share settings, either put those settings in `options` or name both configurations on the command line, in order.
- If production and staging both define fileReplacements, what does ng build -c production,staging use?Only staging's array. Configurations are spread over the options one after another, and a top-level key from a later configuration replaces the earlier value entirely, so arrays are not concatenated. If staging needs production's replacements too, it must list them itself.
- Why did the staging bundle still come out minified?Because the application builder's schema defaults `optimization` to `true`. Production never set it, so staging, which also never set it, fell back to the same default. Only settings written into the production configuration, such as budgets and output hashing, were lost.
saying these in an interview costs you the question
- A named configuration is applied on top of defaultConfiguration.
- ng build -c production,staging deep-merges the fileReplacements arrays of both.
- The order of configurations after -c does not affect the result.
- Missing hashing in staging must be a builder bug, since production works.
- Budgets apply to every build once they are defined anywhere in angular.json.