nx 23.2.0-beta.2 → 23.2.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dist/bin/nx.js +1 -0
  2. package/dist/src/analytics/analytics.js +9 -7
  3. package/dist/src/command-line/graph/graph.d.ts +1 -0
  4. package/dist/src/command-line/graph/graph.js +15 -6
  5. package/dist/src/command-line/migrate/migrate.js +16 -1
  6. package/dist/src/command-line/nx-cloud/connect/connect-to-nx-cloud.js +1 -1
  7. package/dist/src/command-line/release/utils/remote-release-clients/github.js +1 -1
  8. package/dist/src/command-line/release/utils/remote-release-clients/gitlab.js +1 -1
  9. package/dist/src/config/nx-json.d.ts +4 -2
  10. package/dist/src/core/graph/main.js +1 -1
  11. package/dist/src/native/index.d.ts +2 -2
  12. package/dist/src/native/nx.wasm32-wasi.debug.wasm +0 -0
  13. package/dist/src/native/nx.wasm32-wasi.wasm +0 -0
  14. package/dist/src/plugins/js/lock-file/pnpm-parser.js +4 -2
  15. package/dist/src/plugins/js/project-graph/build-dependencies/target-project-locator.js +12 -4
  16. package/dist/src/project-graph/utils/project-configuration/name-substitution-manager.d.ts +1 -0
  17. package/dist/src/project-graph/utils/project-configuration/name-substitution-manager.js +16 -2
  18. package/dist/src/project-graph/utils/project-configuration/project-nodes-manager.d.ts +5 -8
  19. package/dist/src/project-graph/utils/project-configuration/project-nodes-manager.js +11 -14
  20. package/dist/src/project-graph/utils/project-configuration/source-maps.d.ts +12 -5
  21. package/dist/src/project-graph/utils/project-configuration/source-maps.js +21 -7
  22. package/dist/src/project-graph/utils/project-configuration/target-defaults.d.ts +1 -1
  23. package/dist/src/project-graph/utils/project-configuration/target-defaults.js +26 -27
  24. package/dist/src/project-graph/utils/project-configuration/target-merging.js +50 -8
  25. package/dist/src/project-graph/utils/project-configuration/target-normalization.js +16 -3
  26. package/dist/src/project-graph/utils/project-configuration/utils.d.ts +15 -0
  27. package/dist/src/project-graph/utils/project-configuration/utils.js +42 -6
  28. package/dist/src/project-graph/utils/project-configuration-utils.d.ts +15 -12
  29. package/dist/src/project-graph/utils/project-configuration-utils.js +79 -99
  30. package/dist/src/tasks-runner/task-env.d.ts +9 -0
  31. package/dist/src/tasks-runner/task-env.js +26 -2
  32. package/dist/src/tasks-runner/task-orchestrator.js +2 -6
  33. package/dist/src/utils/analytics-prompt.d.ts +0 -6
  34. package/dist/src/utils/analytics-prompt.js +0 -51
  35. package/dist/src/utils/catalog/bun-manager-utils.d.ts +8 -0
  36. package/dist/src/utils/catalog/bun-manager-utils.js +174 -0
  37. package/dist/src/utils/catalog/bun-manager.d.ts +28 -0
  38. package/dist/src/utils/catalog/bun-manager.js +130 -0
  39. package/dist/src/utils/catalog/index.d.ts +1 -1
  40. package/dist/src/utils/catalog/index.js +1 -1
  41. package/dist/src/utils/catalog/manager-factory.js +3 -0
  42. package/dist/src/utils/catalog/manager-utils.d.ts +8 -1
  43. package/dist/src/utils/catalog/manager-utils.js +9 -4
  44. package/dist/src/utils/catalog/manager.d.ts +13 -1
  45. package/dist/src/utils/catalog/manager.js +30 -0
  46. package/dist/src/utils/catalog/pnpm-manager.d.ts +2 -1
  47. package/dist/src/utils/catalog/pnpm-manager.js +3 -0
  48. package/dist/src/utils/catalog/types.d.ts +4 -0
  49. package/dist/src/utils/catalog/yarn-manager.d.ts +6 -2
  50. package/dist/src/utils/catalog/yarn-manager.js +23 -32
  51. package/dist/src/utils/git-utils.d.ts +15 -0
  52. package/dist/src/utils/git-utils.js +64 -6
  53. package/dist/src/utils/package-json.js +11 -7
  54. package/dist/src/utils/package-manager.d.ts +3 -1
  55. package/dist/src/utils/package-manager.js +22 -2
  56. package/dist/src/utils/workspace-id.d.ts +20 -0
  57. package/dist/src/utils/workspace-id.js +54 -0
  58. package/package.json +22 -17
@@ -492,14 +492,14 @@ export interface HashInputs {
492
492
  * so the caller can set it as an env var for child processes.
493
493
  * Used by CLI and daemon.
494
494
  */
495
- export declare function initializeTelemetry(connection: ExternalObject<NxDbConnection>, workspaceId: string, userId: string, nxVersion: string, packageManagerName: string, packageManagerVersion: string | undefined | null, nodeVersion: string, osArch: string, osPlatform: string, osRelease: string, isCi: boolean, isNxCloud: boolean): string
495
+ export declare function initializeTelemetry(connection: ExternalObject<NxDbConnection>, workspaceId: string, userId: string | undefined | null, nxVersion: string, packageManagerName: string, packageManagerVersion: string | undefined | null, nodeVersion: string, osArch: string, osPlatform: string, osRelease: string, isCi: boolean, isNxCloud: boolean): string
496
496
 
497
497
  /**
498
498
  * Initialize telemetry with a pre-fetched session ID.
499
499
  * No DB connection — used by plugin workers that inherit the
500
500
  * session ID from their parent process via env var.
501
501
  */
502
- export declare function initializeTelemetryWithSessionId(sessionId: string, workspaceId: string, userId: string, nxVersion: string, packageManagerName: string, packageManagerVersion: string | undefined | null, nodeVersion: string, osArch: string, osPlatform: string, osRelease: string, isCi: boolean, isNxCloud: boolean): void
502
+ export declare function initializeTelemetryWithSessionId(sessionId: string, workspaceId: string, userId: string | undefined | null, nxVersion: string, packageManagerName: string, packageManagerVersion: string | undefined | null, nodeVersion: string, osArch: string, osPlatform: string, osRelease: string, isCi: boolean, isNxCloud: boolean): void
503
503
 
504
504
  export interface InputsInput {
505
505
  input: string
Binary file
@@ -183,7 +183,8 @@ function getNodes(data, isV5) {
183
183
  }
184
184
  }
185
185
  const packageNames = new Set();
186
- for (const [key, snapshot] of Object.entries(data.packages)) {
186
+ // pnpm omits the packages block for workspace-only lockfiles (no external deps)
187
+ for (const [key, snapshot] of Object.entries(data.packages ?? {})) {
187
188
  let packageNameObj;
188
189
  const originalPackageName = extractNameFromKey(key, isV5);
189
190
  if (!originalPackageName) {
@@ -364,7 +365,8 @@ function getHoistedVersion(packageName, isV5, hoistedKeysByPackage) {
364
365
  }
365
366
  function getDependencies(data, keyMap, isV5, ctx) {
366
367
  const results = [];
367
- Object.keys(data.packages).forEach((key) => {
368
+ // pnpm omits the packages block for workspace-only lockfiles (no external deps)
369
+ Object.keys(data.packages ?? {}).forEach((key) => {
368
370
  const snapshot = data.packages[key];
369
371
  const nodes = keyMap.get(key);
370
372
  nodes.forEach((node) => {
@@ -161,11 +161,19 @@ class TargetProjectLocator {
161
161
  return externalNodeName;
162
162
  }
163
163
  const version = (0, semver_1.clean)(externalPackageJson.version);
164
- let matchingExternalNode = this.npmProjects[`npm:${externalPackageJson.name}@${version}`];
165
- if (!matchingExternalNode) {
166
- // check if it's a package alias, where the resolved package key is used as the version
164
+ const isAliasImport = packageName !== externalPackageJson.name;
165
+ let matchingExternalNode = null;
166
+ if (isAliasImport) {
167
+ // Prefer the alias node when both the alias import and the resolved package
168
+ // exist in the graph, otherwise generated package.json files lose the alias key.
167
169
  const aliasNpmProjectKey = `npm:${packageName}@npm:${externalPackageJson.name}@${version}`;
168
- matchingExternalNode = this.npmProjects[aliasNpmProjectKey];
170
+ matchingExternalNode =
171
+ this.npmProjects[aliasNpmProjectKey] ??
172
+ this.npmProjects[`npm:${packageName}`];
173
+ }
174
+ if (!matchingExternalNode) {
175
+ matchingExternalNode =
176
+ this.npmProjects[`npm:${externalPackageJson.name}@${version}`];
169
177
  }
170
178
  if (!matchingExternalNode) {
171
179
  // Fallback to package name as key. This can happen if the version in project graph is not the same as in the resolved package.json.
@@ -34,6 +34,7 @@ export declare function isUsageRef(value: unknown): value is UsageRef;
34
34
  export declare class ProjectNameInNodePropsManager {
35
35
  private getNameMap;
36
36
  private pendingByName;
37
+ private nameHistory;
37
38
  constructor(getNameMap?: () => Record<string, ProjectConfiguration>);
38
39
  registerNameRefs(pluginResultProjects?: Record<string, Omit<ProjectConfiguration, 'root'> & Partial<ProjectConfiguration>>): void;
39
40
  private processInputs;
@@ -51,6 +51,9 @@ function isUsageRef(value) {
51
51
  class ProjectNameInNodePropsManager {
52
52
  constructor(getNameMap) {
53
53
  this.pendingByName = new Map();
54
+ // name → root for every name a project has ever been identified by, so refs
55
+ // to a since-renamed name still bind to the right root (see createRef).
56
+ this.nameHistory = new Map();
54
57
  this.getNameMap = getNameMap ?? (() => ({}));
55
58
  }
56
59
  // Replaces each project-name ref in `inputs`/`dependsOn` with a sentinel.
@@ -148,9 +151,14 @@ class ProjectNameInNodePropsManager {
148
151
  projects[j] = this.createRef(name);
149
152
  }
150
153
  }
151
- // Builds a sentinel and registers it.
154
+ // Builds a sentinel and registers it. When `referencedName` isn't in the
155
+ // current name map, fall back to the rename history: a batch may reference a
156
+ // project by a name that a project earlier in the same batch already renamed
157
+ // (refs register after the whole batch merges), and the old name still
158
+ // identifies the same root.
152
159
  createRef(referencedName, targetPart) {
153
- const referencedRoot = this.getNameMap()[referencedName]?.root;
160
+ const referencedRoot = this.getNameMap()[referencedName]?.root ??
161
+ this.nameHistory.get(referencedName);
154
162
  const ref = referencedRoot !== undefined
155
163
  ? new RootRef(referencedRoot, targetPart)
156
164
  : new UsageRef(referencedName, targetPart);
@@ -168,6 +176,11 @@ class ProjectNameInNodePropsManager {
168
176
  // RootRef by prototype swap. Sentinel identity across spread copies means
169
177
  // one promotion updates every array the sentinel reached.
170
178
  identifyProjectWithRoot(root, name) {
179
+ // Every name a root has ever gone by, including pre-rename names. A later
180
+ // ref to a stale name resolves to the root it identified at the time.
181
+ // (If two roots use the same name over a graph construction, the later
182
+ // one wins — matching how the live name map would have resolved it.)
183
+ this.nameHistory.set(name, root);
171
184
  const pending = this.pendingByName.get(name);
172
185
  if (!pending)
173
186
  return;
@@ -207,6 +220,7 @@ class ProjectNameInNodePropsManager {
207
220
  }
208
221
  }
209
222
  this.pendingByName.clear();
223
+ this.nameHistory.clear();
210
224
  }
211
225
  substituteInArray(entries, nameByRoot) {
212
226
  for (let i = 0; i < entries.length; i++) {
@@ -28,15 +28,12 @@ export declare class ProjectNodesManager {
28
28
  mergeProjectNode(project: ProjectConfiguration, configurationSourceMaps?: ConfigurationSourceMaps, sourceInformation?: SourceInformation): void;
29
29
  /**
30
30
  * Inserts project-name sentinels into `inputs` and `dependsOn` on the
31
- * merged objects from `mergedRootMap` (defaulting to this manager's
32
- * rootMap). Walking the merged entries matters because a spread-produced
33
- * array is a fresh instance.
34
- *
35
- * Pass a different `mergedRootMap` for the default-plugin intermediate
36
- * pass, then call again with `this.rootMap` after it's applied so
37
- * name refs introduced by that merge are sentinelized as well.
31
+ * merged objects in this manager's rootMap. Walking the merged entries —
32
+ * not the plugin results — matters because a spread-produced array is a
33
+ * fresh instance, and because sentinels written into plugin-result arrays
34
+ * would corrupt them for any later merge that re-reads the results.
38
35
  */
39
- registerNameRefs(pluginResultProjects?: Record<string, Omit<ProjectConfiguration, 'root'> & Partial<ProjectConfiguration>>, mergedRootMap?: Record<string, ProjectConfiguration>): void;
36
+ registerNameRefs(pluginResultProjects?: Record<string, Omit<ProjectConfiguration, 'root'> & Partial<ProjectConfiguration>>): void;
40
37
  /**
41
38
  * Applies all pending name substitutions. Call once after all plugin
42
39
  * results have been merged.
@@ -129,10 +129,10 @@ deferSpreadsWithoutBase = true) {
129
129
  // if augmenting an existing target.
130
130
  const target = project.targets?.[targetName];
131
131
  if (sourceMap) {
132
- // A target default stamping fields onto an existing target must not
133
- // steal provenance for the target's existence; real plugins still win
134
- // last. Field-level attribution is handled inside
135
- // `mergeTargetConfigurations`.
132
+ // Claims the node key for a newly-created target (or reclaims it from
133
+ // a weak target-defaults stamp). Whether an *existing* target changes
134
+ // owners is decided inside `mergeTargetConfigurations`, which knows
135
+ // whether this merge changed the target's identity.
136
136
  (0, source_maps_1.recordTargetIdentitySourceMapInfo)(sourceMap, (0, source_maps_1.targetSourceMapKey)(targetName), sourceInformation);
137
137
  }
138
138
  const normalizedTarget = skipTargetNormalization
@@ -257,21 +257,18 @@ class ProjectNodesManager {
257
257
  }
258
258
  /**
259
259
  * Inserts project-name sentinels into `inputs` and `dependsOn` on the
260
- * merged objects from `mergedRootMap` (defaulting to this manager's
261
- * rootMap). Walking the merged entries matters because a spread-produced
262
- * array is a fresh instance.
263
- *
264
- * Pass a different `mergedRootMap` for the default-plugin intermediate
265
- * pass, then call again with `this.rootMap` after it's applied so
266
- * name refs introduced by that merge are sentinelized as well.
260
+ * merged objects in this manager's rootMap. Walking the merged entries —
261
+ * not the plugin results — matters because a spread-produced array is a
262
+ * fresh instance, and because sentinels written into plugin-result arrays
263
+ * would corrupt them for any later merge that re-reads the results.
267
264
  */
268
- registerNameRefs(pluginResultProjects, mergedRootMap = this.rootMap) {
265
+ registerNameRefs(pluginResultProjects) {
269
266
  if (!pluginResultProjects)
270
267
  return;
271
268
  const scoped = {};
272
269
  for (const root in pluginResultProjects) {
273
- if (mergedRootMap[root]) {
274
- scoped[root] = mergedRootMap[root];
270
+ if (this.rootMap[root]) {
271
+ scoped[root] = this.rootMap[root];
275
272
  }
276
273
  }
277
274
  this.nameSubstitutionManager.registerNameRefs(scoped);
@@ -9,12 +9,19 @@ export type SourceInformation = [file: string | null, plugin: string];
9
9
  */
10
10
  export declare const TARGET_DEFAULTS_PLUGIN_NAME = "nx/target-defaults";
11
11
  /**
12
- * Write the source for a target's "identity" key (the target node itself, or
13
- * its executor/command). Real plugins win last; a target-defaults stamp only
14
- * claims the key when no real plugin already recorded it — target defaults
15
- * never bring a target into existence, they only layer fields onto one.
12
+ * Write the source for the target node key (`targets.<name>`). Ownership of a
13
+ * target follows its identity, not the last writer:
14
+ *
15
+ * - An unowned key goes to whoever writes it first (the creator).
16
+ * - A target-defaults stamp is weak — it never authors a target's existence,
17
+ * so any real plugin reclaims the key from it, and it can never take the
18
+ * key from a real plugin.
19
+ * - Between real plugins, the key only changes hands when the merge changed
20
+ * the target's identity (executor/command) — a plugin that merely layers
21
+ * fields (dependsOn, options, …) onto an existing target does not become
22
+ * its owner.
16
23
  */
17
- export declare function recordTargetIdentitySourceMapInfo(sourceMap: Record<string, SourceInformation>, key: string, sourceInfo: SourceInformation): void;
24
+ export declare function recordTargetIdentitySourceMapInfo(sourceMap: Record<string, SourceInformation>, key: string, sourceInfo: SourceInformation, identityChanged?: boolean): void;
18
25
  /** Source map per project root. */
19
26
  export type ConfigurationSourceMaps = Record<string, Record<string, SourceInformation>>;
20
27
  export declare function forEachSourceMapKeyForArray(prefixKey: string, array: unknown[], callback: (key: string, index: number) => void, startIndex?: number): void;
@@ -21,14 +21,28 @@ exports.targetConfigurationsSourceMapKey = targetConfigurationsSourceMapKey;
21
21
  */
22
22
  exports.TARGET_DEFAULTS_PLUGIN_NAME = 'nx/target-defaults';
23
23
  /**
24
- * Write the source for a target's "identity" key (the target node itself, or
25
- * its executor/command). Real plugins win last; a target-defaults stamp only
26
- * claims the key when no real plugin already recorded it — target defaults
27
- * never bring a target into existence, they only layer fields onto one.
24
+ * Write the source for the target node key (`targets.<name>`). Ownership of a
25
+ * target follows its identity, not the last writer:
26
+ *
27
+ * - An unowned key goes to whoever writes it first (the creator).
28
+ * - A target-defaults stamp is weak — it never authors a target's existence,
29
+ * so any real plugin reclaims the key from it, and it can never take the
30
+ * key from a real plugin.
31
+ * - Between real plugins, the key only changes hands when the merge changed
32
+ * the target's identity (executor/command) — a plugin that merely layers
33
+ * fields (dependsOn, options, …) onto an existing target does not become
34
+ * its owner.
28
35
  */
29
- function recordTargetIdentitySourceMapInfo(sourceMap, key, sourceInfo) {
30
- if (sourceInfo[1] !== exports.TARGET_DEFAULTS_PLUGIN_NAME ||
31
- sourceMap[key] === undefined) {
36
+ function recordTargetIdentitySourceMapInfo(sourceMap, key, sourceInfo, identityChanged = false) {
37
+ const existing = sourceMap[key];
38
+ if (existing === undefined) {
39
+ sourceMap[key] = sourceInfo;
40
+ return;
41
+ }
42
+ if (sourceInfo[1] === exports.TARGET_DEFAULTS_PLUGIN_NAME) {
43
+ return;
44
+ }
45
+ if (existing[1] === exports.TARGET_DEFAULTS_PLUGIN_NAME || identityChanged) {
32
46
  sourceMap[key] = sourceInfo;
33
47
  }
34
48
  }
@@ -19,7 +19,7 @@ type CreateNodesResultEntry = readonly [
19
19
  * fly. That's all the matcher needs; the full target merge happens
20
20
  * downstream in the real merge.
21
21
  */
22
- export declare function createTargetDefaultsResults(specifiedPluginRootMap: Record<string, ProjectConfiguration>, defaultPluginRootMap: Record<string, ProjectConfiguration>, nxJsonConfiguration: NxJsonConfiguration, specifiedSourceMaps?: ConfigurationSourceMaps, defaultSourceMaps?: ConfigurationSourceMaps): CreateNodesResultEntry[];
22
+ export declare function createTargetDefaultsResults(specifiedPluginRootMap: Record<string, ProjectConfiguration>, defaultPluginRootMap: Record<string, ProjectConfiguration>, nxJsonConfiguration: NxJsonConfiguration, specifiedSourceMaps?: ConfigurationSourceMaps): CreateNodesResultEntry[];
23
23
  /**
24
24
  * Public reader that resolves the target defaults applying to a given
25
25
  * target. Accepts the nested map shape (a value is either a plain config
@@ -21,7 +21,7 @@ const utils_1 = require("./utils");
21
21
  * fly. That's all the matcher needs; the full target merge happens
22
22
  * downstream in the real merge.
23
23
  */
24
- function createTargetDefaultsResults(specifiedPluginRootMap, defaultPluginRootMap, nxJsonConfiguration, specifiedSourceMaps, defaultSourceMaps) {
24
+ function createTargetDefaultsResults(specifiedPluginRootMap, defaultPluginRootMap, nxJsonConfiguration, specifiedSourceMaps) {
25
25
  const targetDefaultsConfig = nxJsonConfiguration.targetDefaults;
26
26
  if (!targetDefaultsConfig) {
27
27
  return [];
@@ -65,7 +65,7 @@ function createTargetDefaultsResults(specifiedPluginRootMap, defaultPluginRootMa
65
65
  if (!effective)
66
66
  continue;
67
67
  const sourcePlugin = needsSourcePlugin
68
- ? resolveSourcePlugin(root, targetName, specifiedSourceMaps, defaultSourceMaps)
68
+ ? resolveSourcePlugin(root, targetName, defaultTargets[targetName], specifiedSourceMaps)
69
69
  : undefined;
70
70
  const syntheticTargets = buildSyntheticTargetsForRoot(targetName, root, effective, targetDefaults, projectName, projectNode, sourcePlugin);
71
71
  for (const { key, index, target } of syntheticTargets) {
@@ -372,34 +372,33 @@ function normalizeTargetDefaultValue(value) {
372
372
  // A bare config object is functionally a one-element array.
373
373
  return [value ?? {}];
374
374
  }
375
- function resolveSourcePlugin(root, targetName, specifiedSourceMaps, defaultSourceMaps) {
376
- // Default-plugin attribution overrides specified-plugin attribution in the
377
- // merge, so check it first. The executor/command keys carry the plugin that
378
- // *created* the target, which is what `filter.plugin` ("targets originated by
379
- // X") means. The top-level `targets.<name>` node key is deliberately NOT used
380
- // as a fallback: it tracks the last writer, so a later plugin augmenting the
381
- // target would mis-attribute it. The trade-off is that a target with neither
382
- // an executor nor a command (a rare, non-runnable shape) resolves to no
383
- // source plugin and won't match a `filter.plugin` default — accepted, since
384
- // every runnable target carries one of these keys.
385
- const executorKey = `${(0, source_maps_1.targetSourceMapKey)(targetName)}.executor`;
386
- const commandKey = `${(0, source_maps_1.targetSourceMapKey)(targetName)}.command`;
387
- const candidates = [
388
- pluginFromSourceMap(defaultSourceMaps, root, executorKey),
389
- pluginFromSourceMap(defaultSourceMaps, root, commandKey),
390
- pluginFromSourceMap(specifiedSourceMaps, root, executorKey),
391
- pluginFromSourceMap(specifiedSourceMaps, root, commandKey),
392
- ];
393
- for (const candidate of candidates) {
394
- if (candidate && candidate !== source_maps_1.TARGET_DEFAULTS_PLUGIN_NAME)
395
- return candidate;
375
+ function resolveSourcePlugin(root, targetName, defaultTarget, specifiedSourceMaps) {
376
+ // `filter.plugin` ("targets originated by X") can only name a plugin from
377
+ // nx.json's `plugins` — the specified set. When the default layer authors
378
+ // the target's identity (its merged config carries an executor or command,
379
+ // which the merge lets win), the originator is a default plugin and the
380
+ // target has no matchable source plugin; no source maps are needed to see
381
+ // that. Otherwise the specified layer's executor/command attribution names
382
+ // the originator. The top-level `targets.<name>` node key is deliberately
383
+ // NOT used as a fallback: it tracks ownership, so a later plugin augmenting
384
+ // the target would mis-attribute it. The trade-off is that a target with
385
+ // neither an executor nor a command (a rare, non-runnable shape) resolves to
386
+ // no source plugin and won't match a `filter.plugin` default — accepted,
387
+ // since every runnable target carries one of these keys.
388
+ if (defaultTarget &&
389
+ (defaultTarget.executor !== undefined ||
390
+ defaultTarget.command !== undefined)) {
391
+ return undefined;
392
+ }
393
+ const sourceMap = specifiedSourceMaps?.[root];
394
+ for (const identityKey of ['executor', 'command']) {
395
+ const plugin = sourceMap?.[`${(0, source_maps_1.targetSourceMapKey)(targetName)}.${identityKey}`]?.[1];
396
+ if (plugin && plugin !== source_maps_1.TARGET_DEFAULTS_PLUGIN_NAME) {
397
+ return plugin;
398
+ }
396
399
  }
397
400
  return undefined;
398
401
  }
399
- function pluginFromSourceMap(maps, root, key) {
400
- const entry = maps?.[root]?.[key];
401
- return entry?.[1];
402
- }
403
402
  // Builds a name → MatcherProjectNode view for `findMatchingProjects` to
404
403
  // consult, across both layered rootMaps. Tags are unioned across layers. A
405
404
  // full `mergeProjectConfigurationIntoRootMap` would be overkill — the matcher
@@ -170,6 +170,11 @@ function mergeConfigurations(newConfigurations, baseConfigurations, projectConfi
170
170
  // Before '...': base wins for shared names. Keep base's source-map
171
171
  // entries when it owns the config.
172
172
  if (baseHasConfig) {
173
+ // Base wins, so the incoming config is dropped without reaching
174
+ // `mergeConfigurationValue`. Validate its nested spread here so an
175
+ // integer-like-key ambiguity throws regardless of which side owns
176
+ // the config name (mirrors the base-independent target-level check).
177
+ (0, utils_1.assertNoIntegerLikeSpreadKey)(newConfigurations?.[configName], configIdentifier ? `Object at "${configIdentifier}"` : 'Object');
173
178
  mergedConfigurations[configName] = baseConfigurations[configName];
174
179
  }
175
180
  else {
@@ -282,8 +287,11 @@ function mergeTargetConfigurations(target, baseTarget, projectConfigSourceMap, s
282
287
  ? new Set(targetKeys.slice(0, spreadPosInTarget))
283
288
  : new Set();
284
289
  // Integer-like keys get hoisted to targetKeys[0], making their position
285
- // relative to '...' unrecoverable.
286
- if (hasSpread &&
290
+ // relative to '...' unrecoverable. This is a property of the authored
291
+ // config, not of the base, so it throws regardless of compatibility —
292
+ // which also keeps the error identical between the target-defaults staging
293
+ // merge and the real merge, whose bases differ.
294
+ if (spreadPosInTarget >= 0 &&
287
295
  targetKeys[0] &&
288
296
  utils_1.INTEGER_LIKE_KEY_PATTERN.test(targetKeys[0])) {
289
297
  throw new utils_1.IntegerLikeSpreadKeyError(targetKeys[0], targetIdentifier ? `Target at "${targetIdentifier}"` : 'Target');
@@ -304,6 +312,13 @@ function mergeTargetConfigurations(target, baseTarget, projectConfigSourceMap, s
304
312
  return;
305
313
  if (hasSpread && keysBeforeSpread.has(key)) {
306
314
  // Before '...': base wins; fall through to target only if base lacks it.
315
+ // When base wins, the incoming `target[key]` is dropped without ever
316
+ // reaching `getMergeValueResult`, so validate its nested spread here —
317
+ // the integer-like-key ambiguity is a property of the authored value,
318
+ // not of which side owns the key, and must throw either way.
319
+ (0, utils_1.assertNoIntegerLikeSpreadKey)(target[key], projectConfigSourceMap
320
+ ? `Object at "${targetIdentifier}.${key}"`
321
+ : 'Object');
307
322
  result[key] =
308
323
  key in mergeBase
309
324
  ? mergeBase[key]
@@ -381,13 +396,20 @@ function mergeTargetConfigurations(target, baseTarget, projectConfigSourceMap, s
381
396
  processKey(key);
382
397
  }
383
398
  }
384
- // Update source map once after loop. Real plugins win last, but a target
385
- // default — which only stamps fields onto an existing target and never
386
- // authors its existence — must not steal the node key from the plugin that
387
- // introduced the target. An incompatible replace clears these keys above, so
388
- // a replacing real plugin still re-owns the node.
399
+ // Update the node key once after the loop. Ownership follows identity: this
400
+ // merge claims `targets.<name>` only when it changed the target's identity
401
+ // (a new/different executor or command, or an incompatible replace — whose
402
+ // key purge above empties the slot anyway). A plugin that only layers fields
403
+ // onto an existing target leaves the node with its creator; weak
404
+ // target-defaults stamps are always reclaimable.
389
405
  if (projectConfigSourceMap) {
390
- (0, source_maps_1.recordTargetIdentitySourceMapInfo)(projectConfigSourceMap, targetIdentifier, sourceInformation);
406
+ const identityChanged = !isCompatible ||
407
+ (target.executor !== undefined &&
408
+ target.executor !== baseTarget?.executor) ||
409
+ (target.command !== undefined &&
410
+ target.command !== baseTarget?.command) ||
411
+ suppliesNewOptionsIdentity(baseTarget, target);
412
+ (0, source_maps_1.recordTargetIdentitySourceMapInfo)(projectConfigSourceMap, targetIdentifier, sourceInformation, identityChanged);
391
413
  }
392
414
  // merge options if there are any
393
415
  // if the targets aren't compatible, we simply discard the old options during the merge
@@ -443,6 +465,26 @@ function isCompatibleTarget(a, b) {
443
465
  }
444
466
  return true;
445
467
  }
468
+ /**
469
+ * Run-commands and run-script targets carry their runnable identity in
470
+ * `options` (see {@link isCompatibleTarget}). A layer that supplies that
471
+ * identity where the base had none changed what the target runs — the same
472
+ * identity change as setting an executor on a target that had none.
473
+ */
474
+ function suppliesNewOptionsIdentity(baseTarget, target) {
475
+ const executor = target.executor ?? baseTarget?.executor;
476
+ if (executor === 'nx:run-commands') {
477
+ const baseCommand = baseTarget?.options?.command ??
478
+ baseTarget?.options?.commands?.join(' && ');
479
+ const targetCommand = target.options?.command ?? target.options?.commands?.join(' && ');
480
+ return !!targetCommand && targetCommand !== baseCommand;
481
+ }
482
+ if (executor === 'nx:run-script') {
483
+ const targetScript = target.options?.script;
484
+ return !!targetScript && targetScript !== baseTarget?.options?.script;
485
+ }
486
+ return false;
487
+ }
446
488
  function resolveNxTokensInOptions(object, project, key) {
447
489
  const result = Array.isArray(object) ? [...object] : { ...object };
448
490
  for (let [opt, value] of Object.entries(object ?? {})) {
@@ -115,9 +115,22 @@ function validateAndNormalizeProjectRootMap(workspaceRoot, projectRootMap, nxJso
115
115
  // We initially did this in the project.json plugin, but
116
116
  // that resulted in project.json files without names causing
117
117
  // the resulting project to change names from earlier plugins...
118
- if (!project.name &&
119
- (0, node_fs_1.existsSync)((0, path_1.join)(workspaceRoot, project.root, 'project.json'))) {
120
- project.name = (0, to_project_name_1.toProjectName)((0, path_1.join)(root, 'project.json'));
118
+ if (!project.name) {
119
+ const projectJsonPath = (0, path_1.join)(workspaceRoot, project.root, 'project.json');
120
+ if ((0, node_fs_1.existsSync)(projectJsonPath)) {
121
+ // The project.json plugin may not have run (e.g. when a single
122
+ // plugin is run in isolation via `addPlugin` from a generator), so
123
+ // prefer the name declared in project.json before deriving one from
124
+ // the directory name.
125
+ let nameFromProjectJson;
126
+ try {
127
+ nameFromProjectJson =
128
+ (0, fileutils_1.readJsonFile)(projectJsonPath).name;
129
+ }
130
+ catch { }
131
+ project.name =
132
+ nameFromProjectJson ?? (0, to_project_name_1.toProjectName)((0, path_1.join)(root, 'project.json'));
133
+ }
121
134
  }
122
135
  try {
123
136
  validateProject(project, projects);
@@ -8,6 +8,21 @@ export declare const INTEGER_LIKE_KEY_PATTERN: RegExp;
8
8
  export declare class IntegerLikeSpreadKeyError extends Error {
9
9
  constructor(key: string, context: string);
10
10
  }
11
+ /**
12
+ * Throws `IntegerLikeSpreadKeyError` when `value` is a `'...'` spread object
13
+ * whose enumeration hoists an integer-like key ahead of the spread, making its
14
+ * authored position ambiguous.
15
+ *
16
+ * The ambiguity is a property of the authored value alone, not of any merge
17
+ * base. `mergeObjectWithSpread` runs this the moment it merges such a value —
18
+ * but a merge layer that lets the base win a key drops the incoming value
19
+ * without merging it, so the check must also be run eagerly at those
20
+ * base-owns-key shortcuts. Otherwise the error would surface or vanish
21
+ * depending on which side owns the key (e.g. it fires in the target-defaults
22
+ * staging merge but not the real merge), which is exactly the divergence that
23
+ * makes discarding staging errors unsafe.
24
+ */
25
+ export declare function assertNoIntegerLikeSpreadKey(value: unknown, errorContext: string): void;
11
26
  type SourceMapContext = {
12
27
  sourceMap: Record<string, SourceInformation>;
13
28
  key: string;
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.IntegerLikeSpreadKeyError = exports.INTEGER_LIKE_KEY_PATTERN = exports.NX_SPREAD_TOKEN = void 0;
4
4
  exports.uniqueKeysInObjects = uniqueKeysInObjects;
5
+ exports.assertNoIntegerLikeSpreadKey = assertNoIntegerLikeSpreadKey;
5
6
  exports.getMergeValueResult = getMergeValueResult;
6
7
  const source_maps_1 = require("./source-maps");
7
8
  exports.NX_SPREAD_TOKEN = '...';
@@ -30,6 +31,29 @@ class IntegerLikeSpreadKeyError extends Error {
30
31
  }
31
32
  }
32
33
  exports.IntegerLikeSpreadKeyError = IntegerLikeSpreadKeyError;
34
+ /**
35
+ * Throws `IntegerLikeSpreadKeyError` when `value` is a `'...'` spread object
36
+ * whose enumeration hoists an integer-like key ahead of the spread, making its
37
+ * authored position ambiguous.
38
+ *
39
+ * The ambiguity is a property of the authored value alone, not of any merge
40
+ * base. `mergeObjectWithSpread` runs this the moment it merges such a value —
41
+ * but a merge layer that lets the base win a key drops the incoming value
42
+ * without merging it, so the check must also be run eagerly at those
43
+ * base-owns-key shortcuts. Otherwise the error would surface or vanish
44
+ * depending on which side owns the key (e.g. it fires in the target-defaults
45
+ * staging merge but not the real merge), which is exactly the divergence that
46
+ * makes discarding staging errors unsafe.
47
+ */
48
+ function assertNoIntegerLikeSpreadKey(value, errorContext) {
49
+ if (!isObject(value) || value[exports.NX_SPREAD_TOKEN] !== true) {
50
+ return;
51
+ }
52
+ const keys = Object.keys(value);
53
+ if (keys[0] && exports.INTEGER_LIKE_KEY_PATTERN.test(keys[0])) {
54
+ throw new IntegerLikeSpreadKeyError(keys[0], errorContext);
55
+ }
56
+ }
33
57
  /**
34
58
  * `"..."` in `newValue` (as an array element or a key set to `true`)
35
59
  * expands the base at that position; otherwise `newValue` replaces
@@ -52,11 +76,25 @@ function getMergeValueResult(baseValue, newValue, sourceMapContext, deferSpreads
52
76
  // plugin already set, purely as a merge guard — must not steal provenance
53
77
  // from the layer that introduced the value. A genuinely new value (no base,
54
78
  // or a different one) still attributes to the new layer.
55
- if (!isUnchangedPrimitive(newValue, baseValue)) {
79
+ //
80
+ // The exception cuts the other way too: when the *existing* attribution is a
81
+ // target-defaults stamp (TD merges before the plugin whose value it
82
+ // predicted, so its stamp lands first), a real plugin re-stating that value
83
+ // is the genuine author and reclaims the key.
84
+ if (!isUnchangedPrimitive(newValue, baseValue) ||
85
+ isReclaimableTargetDefaultsStamp(sourceMapContext)) {
56
86
  writeTopLevelSourceMap(sourceMapContext);
57
87
  }
58
88
  return newValue;
59
89
  }
90
+ // Whether the key's current attribution is a weak target-defaults stamp that
91
+ // the (non-target-defaults) writer should reclaim despite the value being
92
+ // unchanged.
93
+ function isReclaimableTargetDefaultsStamp(ctx) {
94
+ return (!!ctx &&
95
+ ctx.sourceMap[ctx.key]?.[1] === source_maps_1.TARGET_DEFAULTS_PLUGIN_NAME &&
96
+ ctx.sourceInformation[1] !== source_maps_1.TARGET_DEFAULTS_PLUGIN_NAME);
97
+ }
60
98
  // Whether `newValue` leaves a defined primitive base untouched. Objects fall
61
99
  // through (always re-attributed) to preserve the existing replace semantics —
62
100
  // the regression this guards is scalar executor/command re-stamping.
@@ -120,12 +158,10 @@ function mergeObjectWithSpread(baseValue, newValue, sourceMapContext, deferSprea
120
158
  const errorContext = sourceMapContext?.key
121
159
  ? `Object at "${sourceMapContext.key}"`
122
160
  : 'Object';
161
+ // Integer-like keys are hoisted to the front of enumeration, so one
162
+ // alongside `'...'` makes its authored position ambiguous.
163
+ assertNoIntegerLikeSpreadKey(newValue, errorContext);
123
164
  const newKeys = Object.keys(newValue);
124
- // Integer-like keys are hoisted to the front of enumeration, so if one
125
- // exists alongside `'...'` it must be newKeys[0].
126
- if (newKeys[0] && exports.INTEGER_LIKE_KEY_PATTERN.test(newKeys[0])) {
127
- throw new IntegerLikeSpreadKeyError(newKeys[0], errorContext);
128
- }
129
165
  // Base per-key sources captured lazily — only for shared keys the new
130
166
  // object overwrites before `'...'`, since writing their new source
131
167
  // clobbers the base entry the spread will need to restore.
@@ -60,19 +60,22 @@ export type MergeError = AggregateCreateNodesError | MergeNodesError | ProjectsW
60
60
  /**
61
61
  * Merges create nodes results into a single rootMap.
62
62
  *
63
- * Specified plugin results are merged once into the manager. Default
64
- * plugin results are first staged into an intermediate rootMap (with
65
- * `'...'` spreads deferred) so that synthesis can read each layer's
66
- * contribution without re-running the merge. The synthetic result from
67
- * `createTargetDefaultsResults` is then merged into the manager, and
68
- * the staged intermediate is replayed on top — that replay is where
69
- * deferred spreads expand against the final (specified + synth) base.
63
+ * Every layer merges into the manager through the same source-map-aware
64
+ * merge, in precedence order:
70
65
  *
71
- * Synthesis itself doesn't materialize a second rootMap. Per
72
- * (root, target) it does an on-the-fly merge of the two layered
73
- * contributions to learn the eventual executor/command, then matches
74
- * defaults against that merged shape. This keeps specified-plugin
75
- * merge work to a single pass.
66
+ * specified plugins → synthetic target defaults → default plugins
67
+ *
68
+ * so field-level provenance is decided by the merge itself for all three
69
+ * layers — whichever layer a field's final value came from owns its
70
+ * attribution, and `'...'` spreads in default-plugin configs resolve against
71
+ * the accumulated specified + target-defaults base.
72
+ *
73
+ * Target-default synthesis needs the *merged* shape of the default layer
74
+ * (to predict each target's eventual executor/command) before that layer
75
+ * merges into the manager. To get it, default results are first staged into
76
+ * a throwaway intermediate rootMap with unresolvable `'...'` spreads
77
+ * deferred. The staging output feeds only `createTargetDefaultsResults`; the
78
+ * default plugins then merge into the manager from their original results.
76
79
  */
77
80
  export declare function mergeCreateNodesResults(specifiedResults: CreateNodesResultEntry[][], defaultResults: CreateNodesResultEntry[][], nxJsonConfiguration: NxJsonConfiguration, workspaceRoot: string, errors: MergeError[]): {
78
81
  projectRootMap: Record<string, ProjectConfiguration>;