Skip to content

fix(core): merge default plugins through the source-map-aware merge path - #36257

Merged
FrozenPandaz merged 13 commits into
masterfrom
fix/default-plugin-source-map-attribution
Jul 23, 2026
Merged

fix(core): merge default plugins through the source-map-aware merge path#36257
FrozenPandaz merged 13 commits into
masterfrom
fix/default-plugin-source-map-attribution

Conversation

@AgentEnder

@AgentEnder AgentEnder commented Jul 7, 2026

Copy link
Copy Markdown
Member

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).


View session information ↗

Fixes NXC-4608

@netlify

netlify Bot commented Jul 7, 2026

Copy link
Copy Markdown

Deploy Preview for nx-docs ready!

Name Link
🔨 Latest commit 91ad3c7
🔍 Latest deploy log https://app.netlify.com/projects/nx-docs/deploys/6a6141ec4be96b0008b6fe5b
😎 Deploy Preview https://deploy-preview-36257--nx-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify

netlify Bot commented Jul 7, 2026

Copy link
Copy Markdown

Deploy Preview for nx-dev ready!

Name Link
🔨 Latest commit 91ad3c7
🔍 Latest deploy log https://app.netlify.com/projects/nx-dev/deploys/6a6141ec61d7dd0008e6298c
😎 Deploy Preview https://deploy-preview-36257--nx-dev.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@nx-cloud

nx-cloud Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

View your CI Pipeline Execution ↗ for commit a87458b

Command Status Duration Result
nx affected --targets=lint,test,build,e2e,e2e-c... ✅ Succeeded 16m 51s View ↗
nx run-many -t check-imports check-lock-files c... ✅ Succeeded 2s View ↗
nx-cloud record -- pnpm nx-cloud conformance:check ✅ Succeeded 38s View ↗
nx build workspace-plugin ✅ Succeeded <1s View ↗
nx-cloud record -- nx sync:check ✅ Succeeded 13s View ↗
nx-cloud record -- nx format:check ✅ Succeeded 5s View ↗

☁️ Nx Cloud last updated this comment at 2026-07-22 22:40:13 UTC

nx-cloud[bot]

This comment was marked as outdated.

@polygraph-app
polygraph-app Bot marked this pull request as ready for review July 7, 2026 19:01
@polygraph-app
polygraph-app Bot requested a review from a team as a code owner July 7, 2026 19:01
@polygraph-app
polygraph-app Bot requested a review from leosvelperez July 7, 2026 19:01
@AgentEnder
AgentEnder force-pushed the fix/default-plugin-source-map-attribution branch from 7839b8e to 2e3d934 Compare July 13, 2026 13:35
nx-cloud[bot]

This comment was marked as outdated.

AgentEnder and others added 10 commits July 15, 2026 19:28
…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
… 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.
@AgentEnder
AgentEnder force-pushed the fix/default-plugin-source-map-attribution branch from 3f428d4 to 7ec54db Compare July 16, 2026 00:19
nx-cloud[bot]

This comment was marked as outdated.

AgentEnder and others added 2 commits July 22, 2026 17:21
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.

@nx-cloud nx-cloud Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Nx Cloud View detailed reasoning in Nx Cloud ↗

🔔 Heads up, your workspace has pending recommendations ↗ to auto-apply fixes for similar failures.


🎓 Learn more about Self-Healing CI on nx.dev

@FrozenPandaz
FrozenPandaz merged commit c16c58a into master Jul 23, 2026
19 checks passed
@FrozenPandaz
FrozenPandaz deleted the fix/default-plugin-source-map-attribution branch July 23, 2026 16:41
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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants