nx 23.1.1 → 23.1.3

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 (128) hide show
  1. package/dist/release/changelog-renderer/index.js +4 -5
  2. package/dist/release/index.d.ts +1 -1
  3. package/dist/src/command-line/add/add.js +5 -0
  4. package/dist/src/command-line/affected/affected.js +1 -0
  5. package/dist/src/command-line/configure-ai-agents/configure-ai-agents.js +2 -2
  6. package/dist/src/command-line/import/import.js +1 -2
  7. package/dist/src/command-line/init/init-v2.js +4 -5
  8. package/dist/src/command-line/migrate/migrate.d.ts +4 -0
  9. package/dist/src/command-line/migrate/migrate.js +89 -26
  10. package/dist/src/command-line/release/changelog.js +5 -0
  11. package/dist/src/command-line/release/index.d.ts +1 -1
  12. package/dist/src/command-line/release/utils/git.js +1 -1
  13. package/dist/src/command-line/release/utils/release-graph.d.ts +17 -0
  14. package/dist/src/command-line/release/utils/release-graph.js +71 -19
  15. package/dist/src/command-line/release/utils/remote-release-clients/github.js +6 -0
  16. package/dist/src/command-line/release/version/release-group-processor.d.ts +24 -0
  17. package/dist/src/command-line/release/version/release-group-processor.js +80 -6
  18. package/dist/src/command-line/release/version/version-actions.d.ts +27 -2
  19. package/dist/src/command-line/release/version/version-actions.js +9 -1
  20. package/dist/src/command-line/release/version.js +9 -5
  21. package/dist/src/command-line/reset/reset.js +6 -1
  22. package/dist/src/command-line/run/run-one.js +1 -0
  23. package/dist/src/command-line/run-many/run-many.js +1 -0
  24. package/dist/src/command-line/watch/watch.js +13 -0
  25. package/dist/src/core/graph/main.js +1 -1
  26. package/dist/src/core/graph/styles.js +1 -1
  27. package/dist/src/daemon/client/client.d.ts +52 -1
  28. package/dist/src/daemon/client/client.js +160 -36
  29. package/dist/src/daemon/client/daemon-environment.d.ts +17 -0
  30. package/dist/src/daemon/client/daemon-environment.js +110 -2
  31. package/dist/src/daemon/client/daemon-socket-messenger.js +4 -0
  32. package/dist/src/daemon/message-types/daemon-message.d.ts +33 -0
  33. package/dist/src/daemon/message-types/daemon-message.js +48 -0
  34. package/dist/src/daemon/server/file-watching/file-watcher-sockets.d.ts +5 -0
  35. package/dist/src/daemon/server/file-watching/file-watcher-sockets.js +16 -0
  36. package/dist/src/daemon/server/latest-nx.js +5 -2
  37. package/dist/src/daemon/server/server.js +24 -0
  38. package/dist/src/daemon/server/shutdown-utils.d.ts +5 -0
  39. package/dist/src/daemon/server/shutdown-utils.js +11 -4
  40. package/dist/src/daemon/socket-utils.d.ts +1 -0
  41. package/dist/src/daemon/socket-utils.js +24 -3
  42. package/dist/src/daemon/tmp-dir.d.ts +43 -3
  43. package/dist/src/daemon/tmp-dir.js +437 -15
  44. package/dist/src/devkit-internals.d.ts +94 -4
  45. package/dist/src/devkit-internals.js +194 -5
  46. package/dist/src/generators/internal-utils/format-changed-files-with-prettier-if-available.d.ts +5 -0
  47. package/dist/src/generators/internal-utils/format-changed-files-with-prettier-if-available.js +9 -2
  48. package/dist/src/generators/tree.d.ts +4 -2
  49. package/dist/src/migrations/update-23-2-0/set-cache-on-executor-target-defaults.d.ts +12 -0
  50. package/dist/src/migrations/update-23-2-0/set-cache-on-executor-target-defaults.js +250 -0
  51. package/dist/src/migrations/update-23-2-0/set-cache-on-executor-target-defaults.md +50 -0
  52. package/dist/src/native/index.d.ts +10 -0
  53. package/dist/src/native/index.js +71 -28
  54. package/dist/src/native/native-bindings.js +1 -0
  55. package/dist/src/native/native-file-cache-location.d.ts +17 -1
  56. package/dist/src/native/native-file-cache-location.js +86 -12
  57. package/dist/src/native/nx.wasm32-wasi.debug.wasm +0 -0
  58. package/dist/src/native/nx.wasm32-wasi.wasm +0 -0
  59. package/dist/src/nx-cloud/utilities/get-cloud-options.js +5 -2
  60. package/dist/src/nx-cloud/utilities/onboarding.js +5 -2
  61. package/dist/src/plugins/js/lock-file/bun-parser.js +43 -10
  62. package/dist/src/plugins/js/lock-file/pnpm-parser.js +60 -0
  63. package/dist/src/plugins/js/lock-file/project-graph-pruning.js +14 -8
  64. package/dist/src/plugins/js/project-graph/affected/npm-packages.js +51 -20
  65. package/dist/src/plugins/js/project-graph/build-dependencies/target-project-locator.js +2 -0
  66. package/dist/src/project-graph/affected/affected-project-graph-models.d.ts +1 -1
  67. package/dist/src/project-graph/affected/affected-project-graph.d.ts +1 -1
  68. package/dist/src/project-graph/affected/affected-project-graph.js +2 -2
  69. package/dist/src/project-graph/affected/locators/project-glob-changes.js +5 -2
  70. package/dist/src/project-graph/error-types.d.ts +12 -1
  71. package/dist/src/project-graph/error-types.js +27 -2
  72. package/dist/src/project-graph/plugins/isolation/isolated-plugin.d.ts +1 -0
  73. package/dist/src/project-graph/plugins/isolation/isolated-plugin.js +18 -1
  74. package/dist/src/project-graph/plugins/isolation/messaging.d.ts +16 -3
  75. package/dist/src/project-graph/plugins/isolation/messaging.js +3 -0
  76. package/dist/src/project-graph/plugins/isolation/plugin-worker.js +58 -8
  77. package/dist/src/project-graph/project-graph.js +20 -0
  78. package/dist/src/project-graph/utils/project-configuration/project-nodes-manager.js +5 -1
  79. package/dist/src/project-graph/utils/project-configuration/target-normalization.js +133 -3
  80. package/dist/src/tasks-runner/task-env.js +5 -2
  81. package/dist/src/tasks-runner/utils.js +2 -6
  82. package/dist/src/utils/assert-workspace-validity.js +7 -2
  83. package/dist/src/utils/fileutils.d.ts +5 -0
  84. package/dist/src/utils/fileutils.js +7 -2
  85. package/dist/src/utils/git-worktrees.d.ts +30 -0
  86. package/dist/src/utils/git-worktrees.js +212 -0
  87. package/dist/src/utils/ignore.d.ts +128 -1
  88. package/dist/src/utils/ignore.js +182 -12
  89. package/dist/src/utils/long-running-target.d.ts +10 -0
  90. package/dist/src/utils/long-running-target.js +19 -0
  91. package/dist/src/utils/min-release-age/behavior/bun.js +7 -11
  92. package/dist/src/utils/min-release-age/behavior/npm.js +5 -2
  93. package/dist/src/utils/min-release-age/packument.js +1 -1
  94. package/dist/src/utils/nx-tmp-dir.d.ts +18 -0
  95. package/dist/src/utils/nx-tmp-dir.js +40 -0
  96. package/dist/src/utils/owned-private-dir.d.ts +132 -0
  97. package/dist/src/utils/owned-private-dir.js +323 -0
  98. package/dist/src/utils/package-manager-config/bunfig.d.ts +14 -0
  99. package/dist/src/utils/package-manager-config/bunfig.js +49 -0
  100. package/dist/src/utils/package-manager-config/npmrc.d.ts +23 -0
  101. package/dist/src/utils/package-manager-config/npmrc.js +124 -0
  102. package/dist/src/utils/package-manager-config/pnpm-config.d.ts +17 -0
  103. package/dist/src/utils/package-manager-config/pnpm-config.js +60 -0
  104. package/dist/src/utils/package-manager.d.ts +21 -2
  105. package/dist/src/utils/package-manager.js +244 -47
  106. package/dist/src/utils/provenance.js +6 -14
  107. package/dist/src/utils/registry-config/bun.d.ts +2 -0
  108. package/dist/src/utils/registry-config/bun.js +257 -0
  109. package/dist/src/utils/registry-config/index.d.ts +13 -0
  110. package/dist/src/utils/registry-config/index.js +102 -0
  111. package/dist/src/utils/registry-config/pnpm.d.ts +2 -0
  112. package/dist/src/utils/registry-config/pnpm.js +1551 -0
  113. package/dist/src/utils/registry-config/utils.d.ts +195 -0
  114. package/dist/src/utils/registry-config/utils.js +496 -0
  115. package/dist/src/utils/registry-config/yarn-berry.d.ts +2 -0
  116. package/dist/src/utils/registry-config/yarn-berry.js +612 -0
  117. package/dist/src/utils/registry-config/yarn-classic.d.ts +2 -0
  118. package/dist/src/utils/registry-config/yarn-classic.js +1001 -0
  119. package/dist/src/utils/safe-spawn.d.ts +24 -0
  120. package/dist/src/utils/safe-spawn.js +104 -0
  121. package/dist/src/utils/shell-quoting.d.ts +15 -0
  122. package/dist/src/utils/shell-quoting.js +35 -0
  123. package/dist/src/utils/wait-for-socket-connection.d.ts +11 -0
  124. package/dist/src/utils/wait-for-socket-connection.js +14 -8
  125. package/migrations.json +6 -0
  126. package/package.json +12 -12
  127. package/dist/src/utils/min-release-age/npmrc.d.ts +0 -15
  128. package/dist/src/utils/min-release-age/npmrc.js +0 -45
@@ -5,10 +5,13 @@ exports.normalizeTarget = normalizeTarget;
5
5
  exports.validateAndNormalizeProjectRootMap = validateAndNormalizeProjectRootMap;
6
6
  const executor_utils_1 = require("../../../command-line/run/executor-utils");
7
7
  const fileutils_1 = require("../../../utils/fileutils");
8
+ const long_running_target_1 = require("../../../utils/long-running-target");
9
+ const output_1 = require("../../../utils/output");
8
10
  const to_project_name_1 = require("../../../config/to-project-name");
9
11
  const error_types_1 = require("../../error-types");
10
12
  const target_merging_1 = require("./target-merging");
11
13
  const node_fs_1 = require("node:fs");
14
+ const git_worktrees_1 = require("../../../utils/git-worktrees");
12
15
  const path_1 = require("path");
13
16
  function validateProject(project,
14
17
  // name -> project
@@ -65,15 +68,137 @@ function normalizeTarget(target, project, workspaceRoot, projectsMap, errorMsgKe
65
68
  }
66
69
  return target;
67
70
  }
71
+ // TODO(v24): remove the legacy target-name cache fallback and its warning.
72
+ // Removal needs a second mechanism for plugin-inferred targets, which the
73
+ // accompanying migration cannot reach.
74
+ /**
75
+ * Whether `target` is cacheable only by way of the legacy name-based fallback:
76
+ * the exact target-name key of `targetDefaults` declares `cache: true`, but an
77
+ * executor key won target-default resolution instead, so the merged target never
78
+ * received it.
79
+ *
80
+ * A `true` result means the user's `cache: true` silently lost, so this doubles
81
+ * as the condition for warning them that the name key is being shadowed.
82
+ */
83
+ function isLegacyCachedTarget(targetName, targetDefaults, target) {
84
+ // Resolution already decided `cache`, so the name key isn't shadowed.
85
+ if (target.cache !== undefined) {
86
+ return false;
87
+ }
88
+ if (isLongRunningTarget(targetName, target)) {
89
+ return false;
90
+ }
91
+ // Restricted to shadowing, which is narrower than what pre-23 restored: that
92
+ // derivation matched on target name alone, so a name key dropped as
93
+ // incompatible (its entry declared a foreign executor) was cacheable too.
94
+ // Restoring that as well would mean writing `cache` with no key to name in
95
+ // the warning, and no migration able to retire it. Deliberately not covered.
96
+ if (!findShadowingTargetDefaultKey(targetDefaults, target)) {
97
+ return false;
98
+ }
99
+ return declaresCacheTrue(targetDefaults?.[targetName]);
100
+ }
101
+ /**
102
+ * Whether the name key declares `cache: true` on an entry that always applies.
103
+ *
104
+ * Filters are deliberately not evaluated. They cannot express a pre-23 config
105
+ * (the filtered array shape postdates the behavior being restored), and a
106
+ * filtered entry declaring `cache` may or may not apply to this project — so
107
+ * rather than guess, a filtered `cache` declares the value unknowable and
108
+ * nothing is restored. Among unfiltered entries the last wins, matching the
109
+ * in-key merge order.
110
+ */
111
+ function declaresCacheTrue(value) {
112
+ if (!value) {
113
+ return false;
114
+ }
115
+ const entries = Array.isArray(value) ? value : [value];
116
+ let declared;
117
+ for (const entry of entries) {
118
+ // `nx.json` is hand-edited; a null or scalar entry would throw here.
119
+ if (!entry || typeof entry !== 'object')
120
+ continue;
121
+ if (entry.cache === undefined)
122
+ continue;
123
+ if (entry.filter)
124
+ return false;
125
+ declared = entry.cache;
126
+ }
127
+ return declared === true;
128
+ }
129
+ /**
130
+ * The normalization-time half of the pre-23 `longRunningTask` guard, which kept
131
+ * `cacheableOperations` from ever making these cacheable. Its remaining clause
132
+ * — `task.overrides['watch']` — is a runtime invocation override with no target
133
+ * equivalent, so it has no counterpart here.
134
+ */
135
+ function isLongRunningTarget(targetName, target) {
136
+ return !!target.continuous || (0, long_running_target_1.isLongRunningTargetName)(targetName);
137
+ }
138
+ /**
139
+ * The `targetDefaults` key that beat the target-name key for `target`. Only an
140
+ * executor key can: key precedence puts the exact target name ahead of every
141
+ * glob, so nothing else outranks it. Undefined when the name key lost for
142
+ * another reason (e.g. its entry declared a foreign executor and was dropped as
143
+ * incompatible) — see {@link isLegacyCachedTarget} for why that case is left
144
+ * alone even though pre-23 restored it.
145
+ *
146
+ * `hasOwnProperty` rather than a lookup: an executor named `__proto__` resolves
147
+ * through the prototype chain to a truthy object, which would report a key the
148
+ * user never wrote.
149
+ */
150
+ function findShadowingTargetDefaultKey(targetDefaults, target) {
151
+ return target.executor &&
152
+ targetDefaults &&
153
+ Object.prototype.hasOwnProperty.call(targetDefaults, target.executor)
154
+ ? target.executor
155
+ : undefined;
156
+ }
157
+ /**
158
+ * Emits a single grouped warning for every (shadowing key, target-name key)
159
+ * pair that relied on the deprecated fallback. Grouping matters because the
160
+ * same pair recurs in every affected project — a per-target warning would
161
+ * print hundreds of identical lines in a large workspace.
162
+ */
163
+ function warnAboutLegacyCachedTargets(legacyCacheReads) {
164
+ if (legacyCacheReads.size === 0) {
165
+ return;
166
+ }
167
+ const bodyLines = [];
168
+ for (const [shadowingKey, targetKeys] of legacyCacheReads) {
169
+ for (const targetKey of targetKeys) {
170
+ bodyLines.push(` - "${shadowingKey}" does not set "cache", so it was read from "${targetKey}"`);
171
+ }
172
+ }
173
+ bodyLines.push('', 'An executor key applies to every target that resolves through it, so exclude any continuous target before setting "cache" on one — a target that is both cacheable and continuous is rejected.', 'Target defaults resolve to a single key rather than merging, so an executor key hides the target name key entirely.', 'Set "cache" on the executor key to keep these targets cacheable — reading it from the target name key is deprecated and will be removed in Nx 24.');
174
+ output_1.output.warn({
175
+ title: 'Some targets are only cacheable through a deprecated fallback.',
176
+ bodyLines,
177
+ });
178
+ }
68
179
  function normalizeTargets(project, sourceMaps, nxJsonConfiguration, workspaceRoot,
69
180
  /**
70
181
  * Project configurations keyed by project name
71
182
  */
72
- projects) {
183
+ projects,
184
+ /**
185
+ * Shadowing `targetDefaults` key -> target name keys its `cache` was read
186
+ * from. Accumulated across projects so the deprecation warns once per pair.
187
+ */
188
+ legacyCacheReads) {
73
189
  const targetErrorMessage = [];
74
190
  for (const targetName in project.targets) {
75
191
  project.targets[targetName] = normalizeTarget(project.targets[targetName], project, workspaceRoot, projects, [project.root, targetName].join(':'));
76
192
  const target = project.targets[targetName];
193
+ const targetDefaults = nxJsonConfiguration.targetDefaults;
194
+ if (isLegacyCachedTarget(targetName, targetDefaults, target)) {
195
+ target.cache = true;
196
+ // Always defined: `isLegacyCachedTarget` returns false without it.
197
+ const shadowingKey = findShadowingTargetDefaultKey(targetDefaults, target);
198
+ const targetKeys = legacyCacheReads.get(shadowingKey) ?? new Set();
199
+ targetKeys.add(targetName);
200
+ legacyCacheReads.set(shadowingKey, targetKeys);
201
+ }
77
202
  if (
78
203
  // If the target has no executor or command, it doesn't do anything
79
204
  !target.executor &&
@@ -106,6 +231,7 @@ function validateAndNormalizeProjectRootMap(workspaceRoot, projectRootMap, nxJso
106
231
  const conflicts = new Map();
107
232
  const projectRootsWithNoName = [];
108
233
  const validityErrors = [];
234
+ const legacyCacheReads = new Map();
109
235
  for (const root in projectRootMap) {
110
236
  const project = projectRootMap[root];
111
237
  // We're setting `// targets` as a comment `targets` is empty due to Project Crystal.
@@ -155,7 +281,7 @@ function validateAndNormalizeProjectRootMap(workspaceRoot, projectRootMap, nxJso
155
281
  for (const root in projectRootMap) {
156
282
  const project = projectRootMap[root];
157
283
  try {
158
- normalizeTargets(project, sourceMaps, nxJsonConfiguration, workspaceRoot, projects);
284
+ normalizeTargets(project, sourceMaps, nxJsonConfiguration, workspaceRoot, projects, legacyCacheReads);
159
285
  }
160
286
  catch (e) {
161
287
  if (e instanceof error_types_1.WorkspaceValidityError) {
@@ -166,9 +292,13 @@ function validateAndNormalizeProjectRootMap(workspaceRoot, projectRootMap, nxJso
166
292
  }
167
293
  }
168
294
  }
295
+ warnAboutLegacyCachedTargets(legacyCacheReads);
169
296
  const errors = [];
170
297
  if (conflicts.size > 0) {
171
- errors.push(new error_types_1.MultipleProjectsWithSameNameError(conflicts, projects));
298
+ // Only on the way to throwing, so a workspace without duplicates never
299
+ // pays for reading git's worktree registry.
300
+ const worktreeAdvice = (0, git_worktrees_1.analyzeWorktreeConflicts)(workspaceRoot, conflicts);
301
+ errors.push(new error_types_1.MultipleProjectsWithSameNameError(conflicts, projects, worktreeAdvice ?? undefined));
172
302
  }
173
303
  if (projectRootsWithNoName.length > 0) {
174
304
  errors.push(new error_types_1.ProjectsWithNoNameError(projectRootsWithNoName, projects));
@@ -166,10 +166,13 @@ function getOwnerTargetForTask(task, graph) {
166
166
  for (const targets of Object.values(project.data.metadata.targetGroups)) {
167
167
  if (targets.includes(task.target.target)) {
168
168
  for (const target of targets) {
169
- if (project.data.targets[target].metadata?.nonAtomizedTarget) {
169
+ // a target group can name a target the project does not define —
170
+ // inference plugins build the group and the targets separately, so
171
+ // the two can disagree. skip the phantom rather than dereferencing it.
172
+ if (project.data.targets[target]?.metadata?.nonAtomizedTarget) {
170
173
  return [
171
174
  target,
172
- project.data.targets[target].metadata?.nonAtomizedTarget,
175
+ project.data.targets[target]?.metadata?.nonAtomizedTarget,
173
176
  ];
174
177
  }
175
178
  }
@@ -36,6 +36,7 @@ const fileutils_1 = require("../utils/fileutils");
36
36
  const find_matching_projects_1 = require("../utils/find-matching-projects");
37
37
  const legacy_depends_on_warning_1 = require("./legacy-depends-on-warning");
38
38
  const globs_1 = require("../utils/globs");
39
+ const long_running_target_1 = require("../utils/long-running-target");
39
40
  const path_1 = require("../utils/path");
40
41
  const serialize_overrides_into_command_line_1 = require("../utils/serialize-overrides-into-command-line");
41
42
  const split_target_1 = require("../utils/split-target");
@@ -429,14 +430,9 @@ function isCacheableTask(task) {
429
430
  return task.cache;
430
431
  }
431
432
  function longRunningTask(task) {
432
- const t = task.target.target;
433
433
  return (task.continuous ||
434
434
  (!!task.overrides['watch'] && task.overrides['watch'] !== 'false') ||
435
- t.endsWith(':watch') ||
436
- t.endsWith('-watch') ||
437
- t === 'serve' ||
438
- t === 'dev' ||
439
- t === 'start');
435
+ (0, long_running_target_1.isLongRunningTargetName)(task.target.target));
440
436
  }
441
437
  // TODO: vsavkin remove when nx-cloud doesn't depend on it
442
438
  function unparse(options) {
@@ -3,7 +3,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.assertWorkspaceValidity = assertWorkspaceValidity;
4
4
  const find_matching_projects_1 = require("./find-matching-projects");
5
5
  const output_1 = require("./output");
6
- const devkit_internals_1 = require("../devkit-internals");
6
+ // Import from the defining module, not the devkit-internals barrel: this file
7
+ // is on the project-graph construction path, and the barrel now eagerly
8
+ // re-exports the project-graph modules, which would create a require cycle back
9
+ // through build-project-graph. (Release modules cross the barrel as erased
10
+ // types only — see the note in packages/devkit/internal.ts.)
11
+ const error_types_1 = require("../project-graph/error-types");
7
12
  function assertWorkspaceValidity(projects, nxJson) {
8
13
  const projectNames = Object.keys(projects);
9
14
  const projectGraphNodes = projectNames.reduce((graph, project) => {
@@ -65,7 +70,7 @@ function assertWorkspaceValidity(projects, nxJson) {
65
70
  })
66
71
  .join('\n\n');
67
72
  }
68
- throw new devkit_internals_1.WorkspaceValidityError(message);
73
+ throw new error_types_1.WorkspaceValidityError(message);
69
74
  }
70
75
  function detectAndSetInvalidProjectGlobValues(map, sourceName, desiredImplicitDeps, projectConfigurations, projects) {
71
76
  const invalidProjectsOrGlobs = desiredImplicitDeps.filter((implicit) => {
@@ -27,6 +27,11 @@ interface YamlReadOptions {
27
27
  * Compatibility with JSON.parse behaviour. If true, then duplicate keys in a mapping will override values rather than throwing an error.
28
28
  */
29
29
  json?: boolean;
30
+ /**
31
+ * Resolves only the tags every YAML parser must support, so every scalar stays
32
+ * a string instead of `true` or `12`.
33
+ */
34
+ failsafe?: boolean;
30
35
  }
31
36
  /**
32
37
  * Reads a YAML file and returns the object the YAML content represents.
@@ -41,8 +41,13 @@ function readJsonFile(path, options) {
41
41
  */
42
42
  function readYamlFile(path, options) {
43
43
  const content = (0, node_fs_1.readFileSync)(path, 'utf-8');
44
- const { load } = require('@zkochan/js-yaml');
45
- return load(content, { ...options, filename: path });
44
+ const { load, FAILSAFE_SCHEMA } = require('@zkochan/js-yaml');
45
+ const { failsafe, ...loadOptions } = options ?? {};
46
+ return load(content, {
47
+ ...loadOptions,
48
+ ...(failsafe ? { schema: FAILSAFE_SCHEMA } : {}),
49
+ filename: path,
50
+ });
46
51
  }
47
52
  /**
48
53
  * Serializes the given data to JSON and writes it to a file.
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Roots of the git linked worktrees that live inside `workspaceRoot`,
3
+ * relative to it and separator-normalized.
4
+ *
5
+ * Reads git's own registry rather than probing the workspace, so it costs one
6
+ * `readdir` plus a small file per worktree. Worktrees outside the workspace
7
+ * are dropped - nothing walks them. Submodules use the same gitfile mechanism
8
+ * but register under `<git-dir>/modules`, so they never appear here.
9
+ */
10
+ export declare function nestedWorktreeRoots(workspaceRoot: string): string[];
11
+ /**
12
+ * Whether `path` sits inside `root`, comparing whole path segments so that
13
+ * `wt-other` is not read as living inside `wt`.
14
+ */
15
+ export declare function isInside(path: string, root: string): boolean;
16
+ /** Advice for duplicate project names that come from nested git worktrees. */
17
+ export interface WorktreeConflictAdvice {
18
+ /** Paths to add to `.gitignore`. */
19
+ ignoreTargets: string[];
20
+ /**
21
+ * Whether ignoring them settles every duplicate. When false the caller still
22
+ * owes the reader the ordinary advice for the ones left over.
23
+ */
24
+ explainsAllConflicts: boolean;
25
+ }
26
+ /**
27
+ * What to tell someone whose duplicate project names come from git worktrees
28
+ * nested in the workspace, or null when none of them do.
29
+ */
30
+ export declare function analyzeWorktreeConflicts(workspaceRoot: string, conflicts: Map<string, string[]>): WorktreeConflictAdvice | null;
@@ -0,0 +1,212 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.nestedWorktreeRoots = nestedWorktreeRoots;
4
+ exports.isInside = isInside;
5
+ exports.analyzeWorktreeConflicts = analyzeWorktreeConflicts;
6
+ const node_fs_1 = require("node:fs");
7
+ const node_path_1 = require("node:path");
8
+ const GITDIR_PREFIX = 'gitdir:';
9
+ function readRecordedPath(file, base) {
10
+ let contents;
11
+ try {
12
+ contents = (0, node_fs_1.readFileSync)(file, 'utf-8').trim();
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ const raw = contents.startsWith(GITDIR_PREFIX)
18
+ ? contents.slice(GITDIR_PREFIX.length).trim()
19
+ : contents;
20
+ return raw ? (0, node_path_1.resolve)(base, raw) : null;
21
+ }
22
+ function isDirectory(path) {
23
+ try {
24
+ return (0, node_fs_1.statSync)(path).isDirectory();
25
+ }
26
+ catch {
27
+ return false;
28
+ }
29
+ }
30
+ /**
31
+ * The nearest directory at or above `from` holding a `.git`, or null when
32
+ * there is no repository above it.
33
+ *
34
+ * The workspace root and the repository root are often the same directory, but
35
+ * a workspace nested in a larger repo is ordinary - and its worktrees are
36
+ * registered against the repository, not against the workspace.
37
+ */
38
+ function findGitRoot(from) {
39
+ let current = (0, node_path_1.resolve)(from);
40
+ while (!(0, node_fs_1.existsSync)((0, node_path_1.join)(current, '.git'))) {
41
+ const parent = (0, node_path_1.dirname)(current);
42
+ if (parent === current) {
43
+ return null;
44
+ }
45
+ current = parent;
46
+ }
47
+ return current;
48
+ }
49
+ /**
50
+ * `<git-dir>/worktrees`, where git registers every linked worktree of the
51
+ * repository `workspaceRoot` belongs to - found by walking up, since the
52
+ * workspace need not be the repository root. Null when there is no `.git`, or
53
+ * when it is a gitfile that names nothing. The registry itself is not checked -
54
+ * reading it is what tells us whether anything is registered.
55
+ */
56
+ function worktreeRegistry(workspaceRoot) {
57
+ const gitRoot = findGitRoot(workspaceRoot);
58
+ if (!gitRoot) {
59
+ return null;
60
+ }
61
+ const dotGit = (0, node_path_1.join)(gitRoot, '.git');
62
+ let gitDir;
63
+ try {
64
+ gitDir = (0, node_fs_1.statSync)(dotGit).isDirectory()
65
+ ? dotGit
66
+ : readRecordedPath(dotGit, gitRoot);
67
+ }
68
+ catch {
69
+ return null;
70
+ }
71
+ if (!gitDir) {
72
+ return null;
73
+ }
74
+ // Running from inside a linked worktree lands on
75
+ // `<main>/.git/worktrees/<name>`, whose `commondir` names the real git dir.
76
+ // Ignored unless it names a directory that exists. That is a sanity check
77
+ // on a path out of a file we did not write, not a bound on where it may
78
+ // point - it can still name any directory on the machine.
79
+ const commonDir = readRecordedPath((0, node_path_1.join)(gitDir, 'commondir'), gitDir);
80
+ return (0, node_path_1.join)(commonDir && isDirectory(commonDir) ? commonDir : gitDir, 'worktrees');
81
+ }
82
+ /**
83
+ * Roots of the git linked worktrees that live inside `workspaceRoot`,
84
+ * relative to it and separator-normalized.
85
+ *
86
+ * Reads git's own registry rather than probing the workspace, so it costs one
87
+ * `readdir` plus a small file per worktree. Worktrees outside the workspace
88
+ * are dropped - nothing walks them. Submodules use the same gitfile mechanism
89
+ * but register under `<git-dir>/modules`, so they never appear here.
90
+ */
91
+ function nestedWorktreeRoots(workspaceRoot) {
92
+ const registry = worktreeRegistry(workspaceRoot);
93
+ if (!registry) {
94
+ return [];
95
+ }
96
+ let entries;
97
+ try {
98
+ entries = (0, node_fs_1.readdirSync)(registry);
99
+ }
100
+ catch {
101
+ return [];
102
+ }
103
+ const roots = [];
104
+ for (const entry of entries) {
105
+ // Points at the worktree's own `.git` gitfile, so its parent is the root.
106
+ const gitfile = readRecordedPath((0, node_path_1.join)(registry, entry, 'gitdir'), (0, node_path_1.join)(registry, entry));
107
+ if (!gitfile || !(0, node_fs_1.existsSync)(gitfile)) {
108
+ continue;
109
+ }
110
+ const root = (0, node_path_1.relative)(workspaceRoot, (0, node_path_1.dirname)(gitfile));
111
+ // Neither the workspace itself nor anything outside it is a nested
112
+ // worktree Nx would walk into. Compared by whole segments, because `..` is
113
+ // a traversal and `..hidden` is an ordinary directory name; and by
114
+ // absoluteness, because `relative` across Windows drives returns its
115
+ // second argument, which is outside by definition and carries no `..`.
116
+ if (!root || (0, node_path_1.isAbsolute)(root) || root.split(node_path_1.sep)[0] === '..') {
117
+ continue;
118
+ }
119
+ roots.push(root.split(node_path_1.sep).join('/'));
120
+ }
121
+ return roots;
122
+ }
123
+ /**
124
+ * Whether `path` sits inside `root`, comparing whole path segments so that
125
+ * `wt-other` is not read as living inside `wt`.
126
+ */
127
+ function isInside(path, root) {
128
+ return path === root || path.startsWith(`${root}/`);
129
+ }
130
+ /**
131
+ * What to tell someone whose duplicate project names come from git worktrees
132
+ * nested in the workspace, or null when none of them do.
133
+ */
134
+ function analyzeWorktreeConflicts(workspaceRoot, conflicts) {
135
+ const worktrees = nestedWorktreeRoots(workspaceRoot);
136
+ if (!worktrees.length) {
137
+ return null;
138
+ }
139
+ const offending = [];
140
+ let explainsAllConflicts = true;
141
+ for (const roots of conflicts.values()) {
142
+ const fromWorktrees = worktrees.filter((worktree) => roots.some((root) => isInside(root, worktree)));
143
+ // What would still be defined twice once the worktrees are out of the way.
144
+ const remaining = roots.filter((root) => !worktrees.some((worktree) => isInside(root, worktree)));
145
+ // Worth naming whenever a worktree is involved, even if ignoring it
146
+ // doesn't settle the whole conflict.
147
+ for (const worktree of fromWorktrees) {
148
+ if (!offending.includes(worktree)) {
149
+ offending.push(worktree);
150
+ }
151
+ }
152
+ if (!fromWorktrees.length || remaining.length > 1) {
153
+ explainsAllConflicts = false;
154
+ }
155
+ }
156
+ if (!offending.length) {
157
+ return null;
158
+ }
159
+ return {
160
+ ignoreTargets: ignoreTargetsFor(workspaceRoot, offending, worktrees),
161
+ explainsAllConflicts,
162
+ };
163
+ }
164
+ /**
165
+ * The worktree roots themselves, or the one directory holding them.
166
+ *
167
+ * Collapsing to the directory is only worth anything when it saves lines, and
168
+ * only safe when it holds nothing else. With a single worktree it saves
169
+ * nothing and risks everything: a lone worktree in `apps/` would have us name
170
+ * `apps/`, which holds only that worktree today and is where the reader will
171
+ * put projects tomorrow.
172
+ */
173
+ function ignoreTargetsFor(workspaceRoot, offending, worktrees) {
174
+ if (offending.length < 2) {
175
+ return offending.map(anchored);
176
+ }
177
+ const parent = commonParent(offending);
178
+ return (parent && holdsOnlyWorktrees(workspaceRoot, parent, worktrees)
179
+ ? [parent]
180
+ : offending).map(anchored);
181
+ }
182
+ /**
183
+ * A gitignore pattern rooted at the workspace rather than matched anywhere.
184
+ *
185
+ * Without the leading slash a single-segment path like `wt1` is a name, not a
186
+ * location - it would ignore every `wt1` at any depth. Applied to all of them
187
+ * so the emitted line reads the same way wherever it came from.
188
+ */
189
+ function anchored(root) {
190
+ return `/${root}`;
191
+ }
192
+ /**
193
+ * The directory every one of `roots` sits directly inside, or null when they
194
+ * don't share one - including when it would be the workspace root, which is
195
+ * never something to ignore.
196
+ */
197
+ function commonParent(roots) {
198
+ const parents = new Set(roots.map((root) => root.split('/').slice(0, -1).join('/')));
199
+ const [parent] = parents;
200
+ return parents.size === 1 && parent ? parent : null;
201
+ }
202
+ function holdsOnlyWorktrees(workspaceRoot, directory, worktrees) {
203
+ let entries;
204
+ try {
205
+ entries = (0, node_fs_1.readdirSync)((0, node_path_1.join)(workspaceRoot, directory));
206
+ }
207
+ catch {
208
+ return false;
209
+ }
210
+ return (entries.length > 0 &&
211
+ entries.every((entry) => worktrees.includes(`${directory}/${entry}`)));
212
+ }
@@ -1,7 +1,134 @@
1
1
  import ignore = require('ignore');
2
2
  import { Tree } from '../generators/tree';
3
3
  export declare function getIgnoreObject(root?: string): ReturnType<typeof ignore>;
4
- export declare function getIgnoreObjectForTree(tree: Tree): ignore.Ignore;
4
+ /** One directory's ignore files, and the directory its patterns are rooted at. */
5
+ export type ScopedIgnoreMatcher = {
6
+ /** Workspace-relative POSIX directory, `''` for the workspace root. */
7
+ dir: string;
8
+ /** How they relate is decided at build time - see `merge`. */
9
+ matchers: ReturnType<typeof ignore>[];
10
+ };
11
+ /**
12
+ * Resolves the ignore files that apply to a directory: its own and every one
13
+ * above it, up to the workspace root.
14
+ *
15
+ * Ignore files cascade - a `.gitignore` covers its own directory and below, and
16
+ * its patterns are relative to *itself*, not to the workspace root. Reading only
17
+ * the root file, which is what `getIgnoreObject` does, silently misses every
18
+ * nested one.
19
+ *
20
+ * A directory's answer is its own files plus its parent's, so every directory on
21
+ * the way up is memoized rather than only the one asked for: sibling leaves
22
+ * share the whole trunk, and a later walk stops at the first directory already
23
+ * known.
24
+ *
25
+ * `read` decides where the files come from - `tree.read` for a generator, disk
26
+ * for a caller with no tree - and returns an empty string or null when there is
27
+ * no such file. Paths handed to it are workspace-relative POSIX.
28
+ *
29
+ * `merge` decides how the files *within one directory* relate, and the two
30
+ * consumers genuinely need different rules:
31
+ *
32
+ * - `false` is prettier's: one matcher per file, any of them excluding wins, and
33
+ * a negation counts only if none excluded. `createIsIgnoredFunction` builds an
34
+ * ignorer per `--ignore-path` and ORs them, so a `!x` in `.prettierignore`
35
+ * cannot re-include an `x` that `.gitignore` excluded.
36
+ * - `true` is git's and the native walker's: all files in one matcher, so
37
+ * `.nxignore`'s `!x` removes `.gitignore`'s exclusion of `x` outright. It has
38
+ * to be a merge rather than a precedence check between separate matchers,
39
+ * because a lone `!x` in its own matcher reports an opinion on `x/` but *none*
40
+ * on `x/a.ts` (measured), so the exclusion would still reach the children.
41
+ * The merge only removes the exclusion within that one directory - a negation
42
+ * in a nested file still loses to an ancestor's exclusion.
43
+ *
44
+ * When `merge` is true, `filenames` order matters: they go into one matcher in
45
+ * order and the last matching pattern decides, so list them lowest-authority
46
+ * first.
47
+ */
48
+ export declare function createIgnoreChainResolver(read: (path: string) => string | null | undefined, filenames: string[], merge: boolean): (dir: string) => ScopedIgnoreMatcher[];
49
+ /**
50
+ * True when the file is ignored, resolving the chain nearest file first.
51
+ *
52
+ * Each matcher is tested against the path relative to its own directory, which
53
+ * is what makes a nested pattern like `/build` mean that directory's `build`
54
+ * rather than the workspace's.
55
+ *
56
+ * Nearest directory with an *opinion* wins, not the first match: a nested
57
+ * `!keep.log` must override the root's `*.log`, which is git's rule for files.
58
+ * A nested negation of a *directory* does not reach its children - see the
59
+ * `merge` note on `createIgnoreChainResolver`.
60
+ *
61
+ * How the files of one directory relate is decided when the chain is built - see
62
+ * that same note. Here they are simply the entry's matchers: any one excluding
63
+ * wins, and a negation counts only if none excluded.
64
+ *
65
+ * Two preconditions, both satisfied by a pruning walk and neither enforced:
66
+ *
67
+ * - `filePath` is workspace-relative POSIX and must sit under every `dir` in the
68
+ * chain, which holds when the chain came from that file's own directory.
69
+ * - No ancestor directory of `filePath` may itself be ignored. git refuses to
70
+ * re-include a file inside an excluded directory, and this does not implement
71
+ * that rule: asked directly about `dist/keep.ts` with a root `dist/` and a
72
+ * nested `dist/.gitignore` holding `!keep.ts`, it answers "not ignored" where
73
+ * git says ignored (measured). `visitNotIgnoredFiles` never asks, because it
74
+ * prunes `dist/` before descending - which is what makes its answers match
75
+ * git, and why that pruning is load-bearing for correctness rather than speed.
76
+ */
77
+ export declare function isIgnoredByChain(chain: ScopedIgnoreMatcher[], filePath: string): boolean;
78
+ /**
79
+ * Directories that should never be walked, whatever the workspace's own ignore
80
+ * files say - `node_modules`, `.git`, the nx caches.
81
+ *
82
+ * The list comes from the native walker rather than a second copy here, so a
83
+ * filesystem walk and a tree walk cannot drift apart.
84
+ *
85
+ * Checked ahead of the cascading chain rather than folded into it: these are not
86
+ * re-includable, and as ordinary patterns a nested negation could resurrect
87
+ * `node_modules`.
88
+ */
89
+ export declare function isAlwaysIgnored(path: string): boolean;
90
+ /**
91
+ * The chain bound to a tree, as predicates over workspace-relative POSIX paths.
92
+ *
93
+ * Files and directories are asked separately because the answers differ: a
94
+ * pattern is only directory-only if it ends in a slash, and `ignore` will not
95
+ * match `dist/` against the path `dist`. Callers must not have to know that, so
96
+ * the slash is appended inside and never leaves this module.
97
+ */
98
+ export type TreeIgnoreChecker = {
99
+ isIgnoredFile: (path: string) => boolean;
100
+ isIgnoredDirectory: (path: string) => boolean;
101
+ };
102
+ /**
103
+ * What git ignores, which is also what the native walker ignores.
104
+ *
105
+ * `.nxignore` outranks `.gitignore` - `walker.rs` registers it with
106
+ * `add_custom_ignore_filename` - which a merge with `.nxignore` last reproduces.
107
+ * git itself does not read it.
108
+ *
109
+ * Reads from the tree rather than disk because a generator can create or amend
110
+ * an ignore file in the same run, which would leave the on-disk copy stale.
111
+ */
112
+ export declare function createGitIgnoreChecker(tree: Tree): TreeIgnoreChecker;
113
+ /**
114
+ * What prettier ignores: the workspace root only, and one ignorer per
115
+ * `--ignore-path` ORed rather than merged (both measured), so a `!` in
116
+ * `.prettierignore` cannot re-include what `.gitignore` excluded. That is the
117
+ * CLI `nx format:check` shells out to.
118
+ *
119
+ * Not an exact match for that command: `isAlwaysIgnored` is wider than
120
+ * prettier's built-ins, and `format.ts` filters its own patterns through
121
+ * `.nxignore`, which this does not read.
122
+ *
123
+ * Reads from the tree rather than disk, as above.
124
+ */
125
+ export declare function createPrettierIgnoreChecker(tree: Tree): TreeIgnoreChecker;
126
+ /**
127
+ * `path.dirname` for the workspace-relative POSIX paths the chain is keyed by,
128
+ * except that the workspace root is `''` rather than `.` - that is the key
129
+ * `createIgnoreChainResolver` terminates on.
130
+ */
131
+ export declare function posixDirname(relativePath: string): string;
5
132
  /**
6
133
  * Adds an entry to a .gitignore file if it's not already covered by existing patterns.
7
134
  * Creates the file if it doesn't exist.