fix(core): merge default plugins through the source-map-aware merge path - #36257
Merged
Conversation
✅ Deploy Preview for nx-docs ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for nx-dev ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
Contributor
|
View your CI Pipeline Execution ↗ for commit a87458b
☁️ Nx Cloud last updated this comment at |
AgentEnder
force-pushed
the
fix/default-plugin-source-map-attribution
branch
from
July 13, 2026 13:35
7839b8e to
2e3d934
Compare
…entity - the targets.<name> source map key stays with the plugin that created the target; a later plugin that only layers fields (dependsOn, options, ...) onto it no longer becomes its owner - the key changes hands only when a merge changes the target's identity: a new/different executor or command, or an incompatible replace - target-defaults stamps are weak: they never author a target's existence, so any real plugin reclaims the node key from them, and a real plugin re-stating a value a target-defaults stamp predicted reclaims the field attribution
- track every name a project root has ever been identified by, so a ref registered after a project in the same batch renamed its referent still binds to the right root and resolves to the final name at substitution time - history is cleared when substitutions apply, so nothing leaks across graph constructions
Default plugin results (project.json, package.json, ...) were applied to the merged rootMap without source maps and their attribution grafted on afterwards with only-fill-missing semantics, so a field a default plugin overrode on an inferred target kept the inferring plugin's attribution (e.g. a project.json dependsOn shown as authored by @nx/gradle from build.gradle.kts). - default plugins now merge into the manager through the same source-map-aware merge as specified plugins and target defaults, so the merge itself decides field provenance for all three layers; the overlay and its heuristics are deleted - the intermediate staging merge only feeds target-default synthesis now: it is skipped when nx.json has no targetDefaults, writes no source maps, collects external nodes and merge errors into scratch objects (the real merge reports both), and no longer registers name-reference sentinels - filter.plugin attribution no longer needs staging source maps: a default plugin can never be named by the filter, so when the merged default layer authors a target's executor/command the target has no matchable source plugin; otherwise the specified layer's source maps name the originator
…ath [Self-Healing CI Rerun]
… clones The rootMap merge adopts input arrays/objects by reference and grows them in place (e.g. mergeMetadata), so staging the default layer for target-default synthesis was corrupting the plugin results before the real merge re-read them — any workspace with targetDefaults plus two default-layer results carrying metadata on one root got every metadata entry duplicated. Staging now merges deep clones, keeping the originals pristine for the real merge.
…get ownership Target-node ownership transferred only on top-level executor/command changes, but run-commands and run-script targets carry their runnable identity in options.command / options.commands / options.script (the same rule isCompatibleTarget applies). A plugin that supplies that identity on a target that had none now claims targets.<name>, mirroring the existing transfer for setting an executor on a bare target.
… compatibility The target-level IntegerLikeSpreadKeyError was the only spread guard gated on base compatibility; the options- and configurations-level guards depend only on the authored config. That gate let the error diverge between the target-defaults staging merge (default-only base) and the real merge (full base), so discarding staging errors could silently lose it — a specified target with a conflicting executor made the real merge incompatible and the ambiguous key landed as a literal property with no report. The ambiguity is a property of the authored config, so throw unconditionally.
…arameters The default-plugin intermediate rootMap is now a throwaway synthesis feed, but several docs still described the deleted two-phase architecture: registerNameRefs told maintainers to pass an intermediate rootMap and call again later (re-introducing the sentinel-orphaning bug the staging comment warns about), the batch-merge wrapper JSDoc mentioned merging into other rootMaps, a spec comment described default batches merging outside the manager, and the public TargetDefaultFilter.plugin JSDoc predated the narrowed matching semantics (default-layer identities resolve to no source plugin). Also drops the rootMap override parameters every caller passed identically.
…arameters [Self-Healing CI Rerun]
AgentEnder
force-pushed
the
fix/default-plugin-source-map-attribution
branch
from
July 16, 2026 00:19
3f428d4 to
7ec54db
Compare
…arameters [Self-Healing CI Rerun]
The nested IntegerLikeSpreadKeyError only fired when the merge actually
reached `mergeObjectWithSpread`. But a pre-`'...'` key the base owns takes
the `result[key] = mergeBase[key]` shortcut in `mergeTargetConfigurations`
(and the analogous `mergeConfigurations` config-name shortcut), dropping the
incoming value without inspection — so a nested `{ '...': …, '<integer>': … }`
never reached the throw. With a staging base the authored target threw
IntegerLikeSpreadKeyError; when the real base owned the key it did not, so the
staging error was discarded and the invalid object silently dropped.
Mirror the top-level fix: extract the check into `assertNoIntegerLikeSpreadKey`
and run it eagerly at both base-owns-key shortcuts before the incoming value is
dropped, so the ambiguity throws regardless of which side owns the key. Also
correct the staging comments in `project-configuration-utils.ts`: the discard
is safe because throw reachability (not just the condition) is now
base-independent, and name refs are skipped because `applySubstitutions` sweeps
only the manager's rootMap.
…elf-Healing CI Rerun]
Contributor
There was a problem hiding this comment.
Nx Cloud has identified a flaky task in your failed CI:
🔂 Since the failure was identified as flaky, we triggered a CI rerun by adding an empty commit to this branch.
🔔 Heads up, your workspace has pending recommendations ↗ to auto-apply fixes for similar failures.
🎓 Learn more about Self-Healing CI on nx.dev
FrozenPandaz
approved these changes
Jul 23, 2026
FrozenPandaz
pushed a commit
that referenced
this pull request
Jul 29, 2026
…ath (#36257) ## Current Behavior Fields that a default plugin (`project.json`, `package.json`) overrides on a target inferred by a specified plugin keep the inferring plugin's source-map attribution. Default-plugin results are applied to the merged rootMap without source maps, and their attribution is grafted on afterwards with only-fill-missing semantics — so any key the specified plugin already wrote keeps its stale entry even when the default plugin replaced the value. Real-world repro (nrwl/ocean): `nx show target :nx-api:gradle:processResources --verbose` shows the `dependsOn` entries authored in `apps/nx-api/project.json` as `(from apps/nx-api/build.gradle.kts by @nx/gradle)`. ## Expected Behavior Every field is attributed to the layer that actually authored its final value. Default plugins now merge into the manager through the same source-map-aware merge as specified plugins and synthetic target defaults, so the merge itself decides provenance for all three layers and the overlay (plus its heuristics) is deleted. Supporting semantics, each with its own commit: - **Target node ownership follows identity**: the `targets.<name>` source-map key stays with the plugin that created the target; it only changes hands when a merge changes the target's identity (new/different executor or command, or an incompatible replace). Target-defaults stamps are weak — always reclaimable, never able to steal. - **Name history**: name-reference sentinels registered after a project in the same batch renamed their referent still bind to the right root. - **Leaner staging**: the intermediate default-layer merge now exists only to feed target-defaults synthesis — it is skipped entirely when nx.json has no `targetDefaults`, writes no source maps, and collects errors/external nodes into scratch objects. `filter.plugin` attribution is derived without staging source maps: a default plugin can never be named by the filter, so a default-layer-authored identity simply resolves to no matchable source plugin. Verified with 316/316 tests across the merge-related suites (including a regression test mirroring the ocean repro) and validated against the live repro in nrwl/ocean: the `dependsOn` entries now show `(from apps/nx-api/project.json by nx/core/project-json)` while the target identity stays with `@nx/gradle`. ## Related Issue(s) Reported via Polygraph session verification of the nested-array `targetDefaults` work (#36049) in nrwl/ocean; no standalone GitHub issue. The attribution bug predates #36049 (introduced with the default-layer overlay in #34285). <!-- polygraph-session-start --> --- [View session information ↗](https://app.trypolygraph.com/orgs/6a061dcb561c062131116eca/sessions/reapply-target-defaults-d52a940d) <!-- polygraph-session-end --> Fixes NXC-4608 --------- Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com> (cherry picked from commit c16c58a)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Current Behavior
Fields that a default plugin (
project.json,package.json) overrides on a target inferred by a specified plugin keep the inferring plugin's source-map attribution. Default-plugin results are applied to the merged rootMap without source maps, and their attribution is grafted on afterwards with only-fill-missing semantics — so any key the specified plugin already wrote keeps its stale entry even when the default plugin replaced the value.Real-world repro (nrwl/ocean):
nx show target :nx-api:gradle:processResources --verboseshows thedependsOnentries authored inapps/nx-api/project.jsonas(from apps/nx-api/build.gradle.kts by @nx/gradle).Expected Behavior
Every field is attributed to the layer that actually authored its final value. Default plugins now merge into the manager through the same source-map-aware merge as specified plugins and synthetic target defaults, so the merge itself decides provenance for all three layers and the overlay (plus its heuristics) is deleted.
Supporting semantics, each with its own commit:
targets.<name>source-map key stays with the plugin that created the target; it only changes hands when a merge changes the target's identity (new/different executor or command, or an incompatible replace). Target-defaults stamps are weak — always reclaimable, never able to steal.targetDefaults, writes no source maps, and collects errors/external nodes into scratch objects.filter.pluginattribution is derived without staging source maps: a default plugin can never be named by the filter, so a default-layer-authored identity simply resolves to no matchable source plugin.Verified with 316/316 tests across the merge-related suites (including a regression test mirroring the ocean repro) and validated against the live repro in nrwl/ocean: the
dependsOnentries now show(from apps/nx-api/project.json by nx/core/project-json)while the target identity stays with@nx/gradle.Related Issue(s)
Reported via Polygraph session verification of the nested-array
targetDefaultswork (#36049) in nrwl/ocean; no standalone GitHub issue. The attribution bug predates #36049 (introduced with the default-layer overlay in #34285).View session information ↗
Fixes NXC-4608