nx 23.2.0 → 23.3.0-beta.0

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 (30) hide show
  1. package/dist/release/changelog-renderer/index.d.ts +4 -0
  2. package/dist/release/changelog-renderer/index.js +17 -1
  3. package/dist/src/command-line/migrate/prompt-files.js +2 -9
  4. package/dist/src/command-line/release/changelog.js +3 -1
  5. package/dist/src/command-line/reset/reset.js +1 -3
  6. package/dist/src/daemon/client/daemon-environment.d.ts +10 -0
  7. package/dist/src/daemon/client/daemon-environment.js +44 -0
  8. package/dist/src/daemon/server/handle-client-env.js +24 -3
  9. package/dist/src/native/nx.wasm32-wasi.debug.wasm +0 -0
  10. package/dist/src/native/nx.wasm32-wasi.wasm +0 -0
  11. package/dist/src/nx-cloud/update-manager.js +4 -3
  12. package/dist/src/plugins/js/lock-file/pruned-output.d.ts +10 -9
  13. package/dist/src/plugins/js/lock-file/pruned-output.js +37 -13
  14. package/dist/src/project-graph/plugins/isolation/isolated-plugin.d.ts +16 -0
  15. package/dist/src/project-graph/plugins/isolation/isolated-plugin.js +59 -22
  16. package/dist/src/tasks-runner/forked-process-task-runner.d.ts +3 -0
  17. package/dist/src/tasks-runner/forked-process-task-runner.js +15 -6
  18. package/dist/src/tasks-runner/init-tasks-runner.js +32 -2
  19. package/dist/src/tasks-runner/life-cycles/task-history-life-cycle.js +16 -11
  20. package/dist/src/tasks-runner/running-tasks/batch-process.js +3 -0
  21. package/dist/src/tasks-runner/running-tasks/shared-running-task.d.ts +1 -0
  22. package/dist/src/tasks-runner/running-tasks/shared-running-task.js +12 -1
  23. package/dist/src/tasks-runner/task-orchestrator.d.ts +3 -0
  24. package/dist/src/tasks-runner/task-orchestrator.js +31 -3
  25. package/dist/src/utils/min-release-age/behavior/pnpm.js +17 -9
  26. package/dist/src/utils/package-json.js +13 -2
  27. package/dist/src/utils/params.js +1 -1
  28. package/dist/src/utils/path.d.ts +6 -0
  29. package/dist/src/utils/path.js +15 -0
  30. package/package.json +11 -11
@@ -106,5 +106,9 @@ export default class DefaultChangelogRenderer {
106
106
  protected groupChangesByScope(changes: ChangelogChange[]): Record<string, ChangelogChange[]>;
107
107
  protected extractBreakingChangeExplanation(message: string): string | null;
108
108
  private isBreakingChangeBoundary;
109
+ protected isAiAgentAuthor(author: {
110
+ name: string;
111
+ email: string;
112
+ }): boolean;
109
113
  protected formatName(name?: string): string;
110
114
  }
@@ -3,6 +3,18 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const semver_1 = require("semver");
4
4
  // Stripped from breaking change explanations (e.g. bot/session markers).
5
5
  const HtmlCommentRegex = /<!--[\s\S]*?-->/g;
6
+ // Keyed on email: agent display names track model versions (e.g. "Claude Opus
7
+ // 5 (1M context)"), and a human contributor can share a name with an agent.
8
+ const AiAgentAuthorEmails = new Set([
9
+ 'noreply@anthropic.com',
10
+ 'claude@anthropic.com',
11
+ 'amp@ampcode.com',
12
+ 'cursoragent@cursor.com',
13
+ 'openhands@all-hands.dev',
14
+ 'opendevin@all-hands.dev',
15
+ ]);
16
+ // The Copilot coding agent's trailer is not always [bot]-suffixed.
17
+ const CopilotAuthorEmailRegex = /^(?:\d+\+)?copilot@users\.noreply\.github\.com$/;
6
18
  class DefaultChangelogRenderer {
7
19
  /**
8
20
  * A ChangelogRenderer class takes in the determined changes and other relevant metadata
@@ -212,7 +224,7 @@ class DefaultChangelogRenderer {
212
224
  }
213
225
  for (const author of change.authors) {
214
226
  const name = this.formatName(author.name);
215
- if (!name || name.includes('[bot]')) {
227
+ if (!name || name.includes('[bot]') || this.isAiAgentAuthor(author)) {
216
228
  continue;
217
229
  }
218
230
  if (!_authors.has(name)) {
@@ -371,6 +383,10 @@ class DefaultChangelogRenderer {
371
383
  // Git metadata delimiter following the body
372
384
  trimmed === '"');
373
385
  }
386
+ isAiAgentAuthor(author) {
387
+ const email = (author.email || '').trim().toLowerCase();
388
+ return (AiAgentAuthorEmails.has(email) || CopilotAuthorEmailRegex.test(email));
389
+ }
374
390
  formatName(name = '') {
375
391
  return name
376
392
  .split(' ')
@@ -44,15 +44,8 @@ async function resolvePromptFiles(packageName, packageVersion, migrations, migra
44
44
  ? resolvedPromptFiles
45
45
  : undefined;
46
46
  }
47
- function isPromptPathWithinMigrationsDir(migrationsDir, promptRelPath) {
48
- const rel = (0, path_1.relative)(migrationsDir, (0, path_1.join)(migrationsDir, promptRelPath));
49
- return !((0, path_1.isAbsolute)(promptRelPath) ||
50
- rel === '..' ||
51
- rel.startsWith(`..${path_1.sep}`) ||
52
- rel.startsWith(`..${path_1.posix.sep}`));
53
- }
54
47
  function assertPromptPathWithinMigrationsDir(migrationsDir, promptRelPath, packageName, packageVersion) {
55
- if (!isPromptPathWithinMigrationsDir(migrationsDir, promptRelPath)) {
48
+ if (!(0, path_2.isContainedRelativePath)(promptRelPath)) {
56
49
  throw new Error(`Invalid prompt path "${promptRelPath}" in package "${packageName}@${packageVersion}": prompt paths must be relative and resolve within the package's migrations directory.`);
57
50
  }
58
51
  }
@@ -77,7 +70,7 @@ exports.PromptResolutionError = PromptResolutionError;
77
70
  * directory and point at an existing file.
78
71
  */
79
72
  function resolvePrompt(promptPath, migrationsDir) {
80
- if (!isPromptPathWithinMigrationsDir(migrationsDir, promptPath)) {
73
+ if (!(0, path_2.isContainedRelativePath)(promptPath)) {
81
74
  throw new PromptResolutionError(promptPath, migrationsDir);
82
75
  }
83
76
  const resolvedPath = (0, path_1.join)(migrationsDir, promptPath);
@@ -380,7 +380,9 @@ function createAPI(overrideReleaseConfig, ignoreNxJsonConfig) {
380
380
  }
381
381
  else {
382
382
  const groupCacheKey = `${releaseGroup.name}:fixed`;
383
- const fromSHA = await getCachedFromSHA(groupCacheKey, releaseGroup.releaseTag.pattern, {}, workspacePreid ?? projectsPreid?.[Object.keys(projectsPreid)[0]], releaseGroup.releaseTag.checkAllBranchesWhen, releaseGroup.releaseTag.requireSemver, releaseGroup.releaseTag.strictPreid);
383
+ const fromSHA = await getCachedFromSHA(groupCacheKey, releaseGroup.releaseTag.pattern, {
384
+ releaseGroupName: releaseGroup.name,
385
+ }, workspacePreid ?? projectsPreid?.[Object.keys(projectsPreid)[0]], releaseGroup.releaseTag.checkAllBranchesWhen, releaseGroup.releaseTag.requireSemver, releaseGroup.releaseTag.strictPreid);
384
386
  if (!fromSHA) {
385
387
  throw new Error(`Unable to determine the previous git tag. If this is the first release of your release group, use the --first-release option or set the "release.changelog.automaticFromRef" config property in nx.json to generate a changelog from the first commit. Otherwise, be sure to configure the "release.releaseTag.pattern" property in nx.json to match the structure of your repository's git tags.`);
386
388
  }
@@ -2,7 +2,6 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.resetHandler = resetHandler;
4
4
  const node_fs_1 = require("node:fs");
5
- const node_path_1 = require("node:path");
6
5
  const client_1 = require("../../daemon/client/client");
7
6
  const tmp_dir_1 = require("../../daemon/tmp-dir");
8
7
  const cache_directory_1 = require("../../utils/cache-directory");
@@ -125,8 +124,7 @@ async function resetCloudClient() {
125
124
  }
126
125
  function removeInstalledNxCloudClient() {
127
126
  return incrementalBackoff(INCREMENTAL_BACKOFF_FIRST_DELAY, INCREMENTAL_BACKOFF_MAX_DURATION, () => {
128
- const cloudClientDir = (0, update_manager_1.getBundleInstallDefaultLocation)();
129
- (0, node_fs_1.rmSync)((0, node_path_1.join)(cloudClientDir, 'cloud'), { recursive: true, force: true });
127
+ (0, node_fs_1.rmSync)((0, update_manager_1.getBundleInstallDefaultLocation)(), { recursive: true, force: true });
130
128
  });
131
129
  }
132
130
  function cleanupCacheEntries() {
@@ -62,3 +62,13 @@ export declare function getAppliedDaemonClientEnv(): {
62
62
  * previous client is stale all the same.
63
63
  */
64
64
  export declare function applyDaemonEnvFromClient(newEnv: NodeJS.ProcessEnv): string[];
65
+ /**
66
+ * Yarn Berry creates a fresh BERRY_BIN_FOLDER for every invocation and puts it
67
+ * first on PATH. The wrappers inside are runtime state the daemon and workers
68
+ * must receive, but the folder's random name is not project-graph identity:
69
+ * two invocations whose environments differ only by it would compute the same
70
+ * graph. Strips the folder from PATH and pins the var to a sentinel so such
71
+ * environments compare equal. Returns a copy; never mutates runtime env.
72
+ */
73
+ export declare function normalizeDaemonEnvironmentForGraph(env: NodeJS.ProcessEnv, platform?: NodeJS.Platform): NodeJS.ProcessEnv;
74
+ export declare function getChangedEnvKeys(before: NodeJS.ProcessEnv, after: NodeJS.ProcessEnv): string[];
@@ -6,6 +6,8 @@ exports.getDaemonClientEnvGeneration = getDaemonClientEnvGeneration;
6
6
  exports.getDaemonSpawnEnv = getDaemonSpawnEnv;
7
7
  exports.getAppliedDaemonClientEnv = getAppliedDaemonClientEnv;
8
8
  exports.applyDaemonEnvFromClient = applyDaemonEnvFromClient;
9
+ exports.normalizeDaemonEnvironmentForGraph = normalizeDaemonEnvironmentForGraph;
10
+ exports.getChangedEnvKeys = getChangedEnvKeys;
9
11
  const file_hasher_1 = require("../../hasher/file-hasher");
10
12
  const DAEMON_ENV_REQUIRED_SETTINGS = {
11
13
  NX_PROJECT_GLOB_CACHE: 'false',
@@ -376,3 +378,45 @@ function applyDaemonEnvFromClient(newEnv) {
376
378
  appliedClientEnv = { ...newEnv };
377
379
  return [...changedKeys];
378
380
  }
381
+ const BERRY_BIN_FOLDER_GRAPH_IDENTITY = '<YARN_BERRY_BIN_FOLDER>';
382
+ /**
383
+ * Yarn Berry creates a fresh BERRY_BIN_FOLDER for every invocation and puts it
384
+ * first on PATH. The wrappers inside are runtime state the daemon and workers
385
+ * must receive, but the folder's random name is not project-graph identity:
386
+ * two invocations whose environments differ only by it would compute the same
387
+ * graph. Strips the folder from PATH and pins the var to a sentinel so such
388
+ * environments compare equal. Returns a copy; never mutates runtime env.
389
+ */
390
+ function normalizeDaemonEnvironmentForGraph(env, platform = process.platform) {
391
+ const normalized = { ...env };
392
+ const berryBinFolder = normalized.BERRY_BIN_FOLDER;
393
+ if (!berryBinFolder) {
394
+ return normalized;
395
+ }
396
+ const pathKey = Object.keys(normalized).find((key) => key.toUpperCase() === 'PATH');
397
+ if (pathKey && normalized[pathKey]) {
398
+ const pathDelimiter = platform === 'win32' ? ';' : ':';
399
+ const canonicalize = (value) => {
400
+ const withPlatformSeparators = platform === 'win32' ? value.replaceAll('/', '\\') : value;
401
+ return platform === 'win32'
402
+ ? withPlatformSeparators.toLowerCase()
403
+ : withPlatformSeparators;
404
+ };
405
+ const canonicalBerryBinFolder = canonicalize(berryBinFolder);
406
+ normalized[pathKey] = normalized[pathKey]
407
+ .split(pathDelimiter)
408
+ .filter((entry) => canonicalize(entry) !== canonicalBerryBinFolder)
409
+ .join(pathDelimiter);
410
+ }
411
+ normalized.BERRY_BIN_FOLDER = BERRY_BIN_FOLDER_GRAPH_IDENTITY;
412
+ return normalized;
413
+ }
414
+ function getChangedEnvKeys(before, after) {
415
+ const changedKeys = [];
416
+ for (const key of new Set([...Object.keys(before), ...Object.keys(after)])) {
417
+ if (before[key] !== after[key]) {
418
+ changedKeys.push(key);
419
+ }
420
+ }
421
+ return changedKeys;
422
+ }
@@ -29,12 +29,30 @@ async function handleClientEnv(env) {
29
29
  while (inFlightApply) {
30
30
  await inFlightApply;
31
31
  }
32
+ const graphEnvBefore = (0, daemon_environment_1.normalizeDaemonEnvironmentForGraph)(process.env);
33
+ const previousClientEnv = (0, daemon_environment_1.getAppliedDaemonClientEnv)()?.env;
32
34
  const changedEnvKeys = (0, daemon_environment_1.applyDaemonEnvFromClient)(env);
33
35
  if (changedEnvKeys.length === 0) {
34
36
  return;
35
37
  }
36
- logger_1.serverLogger.log(`Graph recompute necessary due to env variable refresh. Changed keys: ${changedEnvKeys.join(', ')}`);
37
- const apply = applyEnvChange(env);
38
+ // Runtime changes always reach the workers, but only changes that survive
39
+ // graph normalization (e.g. Yarn Berry's per-invocation BERRY_BIN_FOLDER
40
+ // does not) invalidate the computed graph.
41
+ const graphChangedKeys = new Set([
42
+ ...(0, daemon_environment_1.getChangedEnvKeys)(graphEnvBefore, (0, daemon_environment_1.normalizeDaemonEnvironmentForGraph)(process.env)),
43
+ ...(previousClientEnv
44
+ ? (0, daemon_environment_1.getChangedEnvKeys)((0, daemon_environment_1.normalizeDaemonEnvironmentForGraph)(previousClientEnv), (0, daemon_environment_1.normalizeDaemonEnvironmentForGraph)(env))
45
+ : []),
46
+ ]);
47
+ if (graphChangedKeys.size > 0) {
48
+ logger_1.serverLogger.log(`Graph recompute necessary due to env variable refresh. Changed keys: ${[
49
+ ...graphChangedKeys,
50
+ ].join(', ')}`);
51
+ }
52
+ else {
53
+ logger_1.serverLogger.log(`Env variable refresh changed only runtime values (${changedEnvKeys.join(', ')}); graph identity is unchanged, keeping the cached graph.`);
54
+ }
55
+ const apply = applyEnvChange(env, graphChangedKeys.size > 0);
38
56
  inFlightApply = apply;
39
57
  try {
40
58
  await apply;
@@ -45,8 +63,11 @@ async function handleClientEnv(env) {
45
63
  }
46
64
  }
47
65
  }
48
- async function applyEnvChange(env) {
66
+ async function applyEnvChange(env, invalidateGraph) {
49
67
  await forwardEnvToPluginWorkers(env);
68
+ if (!invalidateGraph) {
69
+ return;
70
+ }
50
71
  // Discarding the cached graph makes the next request recompute under the
51
72
  // new env, and chains any in-flight compute (started under the old env) to
52
73
  // that successor.
Binary file
@@ -12,6 +12,7 @@ const axios_1 = require("./utilities/axios");
12
12
  const debug_logger_1 = require("./debug-logger");
13
13
  const tar = tslib_1.__importStar(require("tar-stream"));
14
14
  const cache_directory_1 = require("../utils/cache-directory");
15
+ const is_ci_1 = require("../utils/is-ci");
15
16
  const crypto_1 = require("crypto");
16
17
  const workspace_root_1 = require("../utils/workspace-root");
17
18
  class NxCloudEnterpriseOutdatedError extends Error {
@@ -106,9 +107,9 @@ function getBundleInstallDefaultLocation() {
106
107
  if ((0, fs_1.existsSync)(legacyPath)) {
107
108
  return legacyPath;
108
109
  }
109
- else {
110
- return (0, path_1.join)(cache_directory_1.cacheDir, 'cloud');
111
- }
110
+ // The bundle `require`s a bare `nx`, so it must sit where that resolves: on CI
111
+ // the shared per-user root is outside the checkout (NXC-4944).
112
+ return (0, path_1.join)((0, is_ci_1.isCI)() ? (0, cache_directory_1.cacheDirectoryForWorkspace)(workspace_root_1.workspaceRoot) : cache_directory_1.cacheDir, 'cloud');
112
113
  }
113
114
  const runnerBundleInstallDirectory = getBundleInstallDefaultLocation();
114
115
  function getLatestInstalledRunnerBundle() {
@@ -42,15 +42,16 @@ type PrunedPnpmConfig = {
42
42
  * file is inert, verified installable with identical module resolution on
43
43
  * pnpm 9, 10 and 11.
44
44
  *
45
- * pnpm 11+ reads build approvals (`allowBuilds`), `supportedArchitectures` and
46
- * `patchedDependencies` only from pnpm-workspace.yaml, so those are carried
47
- * from the workspace root; pass `prunedLockfileContent` to scope the approvals
48
- * to the packages the pruned lockfile keeps (an approval for an absent package
49
- * is inert either way, this only keeps the emitted file accurate). On
50
- * pnpm <=10, which reads them from the emitted package.json instead (see
51
- * `getPrunedPnpmPackageJsonBuildSettings`), or when the workspace declares
52
- * none, the file holds only `packages: []`. Resolution-time config stays out:
53
- * the pruned lockfile bakes it in (`stripPrunedLockfilePnpmConfig`).
45
+ * On pnpm 11+, this file carries declared `minimumReleaseAge`,
46
+ * `minimumReleaseAgeExclude`, `minimumReleaseAgeStrict`, build approvals,
47
+ * `supportedArchitectures`, and applicable `patchedDependencies` from the
48
+ * workspace root. Approvals and patches are scoped to the pruned lockfile.
49
+ *
50
+ * On pnpm <=10, only build approvals, `supportedArchitectures`, and
51
+ * `patchedDependencies` are emitted in `package.json`.
52
+ *
53
+ * Settings encoded in the pruned resolution, such as `overrides` and
54
+ * `packageExtensions`, are not copied (`stripPrunedLockfilePnpmConfig`).
54
55
  *
55
56
  * The major is the build machine's pnpm, which is all that is knowable at
56
57
  * build time: an output built on pnpm <=10 but deployed on pnpm 11+ will not
@@ -107,15 +107,16 @@ function dropInheritedPnpmPatchedDependencies(packageJson) {
107
107
  * file is inert, verified installable with identical module resolution on
108
108
  * pnpm 9, 10 and 11.
109
109
  *
110
- * pnpm 11+ reads build approvals (`allowBuilds`), `supportedArchitectures` and
111
- * `patchedDependencies` only from pnpm-workspace.yaml, so those are carried
112
- * from the workspace root; pass `prunedLockfileContent` to scope the approvals
113
- * to the packages the pruned lockfile keeps (an approval for an absent package
114
- * is inert either way, this only keeps the emitted file accurate). On
115
- * pnpm <=10, which reads them from the emitted package.json instead (see
116
- * `getPrunedPnpmPackageJsonBuildSettings`), or when the workspace declares
117
- * none, the file holds only `packages: []`. Resolution-time config stays out:
118
- * the pruned lockfile bakes it in (`stripPrunedLockfilePnpmConfig`).
110
+ * On pnpm 11+, this file carries declared `minimumReleaseAge`,
111
+ * `minimumReleaseAgeExclude`, `minimumReleaseAgeStrict`, build approvals,
112
+ * `supportedArchitectures`, and applicable `patchedDependencies` from the
113
+ * workspace root. Approvals and patches are scoped to the pruned lockfile.
114
+ *
115
+ * On pnpm <=10, only build approvals, `supportedArchitectures`, and
116
+ * `patchedDependencies` are emitted in `package.json`.
117
+ *
118
+ * Settings encoded in the pruned resolution, such as `overrides` and
119
+ * `packageExtensions`, are not copied (`stripPrunedLockfilePnpmConfig`).
119
120
  *
120
121
  * The major is the build machine's pnpm, which is all that is knowable at
121
122
  * build time: an output built on pnpm <=10 but deployed on pnpm 11+ will not
@@ -129,6 +130,29 @@ function getPrunedPnpmInstallSettingsYaml(workspaceRootPath = workspace_root_1.w
129
130
  // list is accepted by pnpm 9-11 without pulling any importer into the install.
130
131
  return dump({ packages: [], ...settings });
131
132
  }
133
+ /**
134
+ * pnpm-workspace.yaml fields carried verbatim from the workspace root to the
135
+ * pruned output, with no interpretation beyond "declared or not" (unlike
136
+ * `allowBuilds`, filtered to the pruned lockfile, and `patchedDependencies`,
137
+ * resolved against it):
138
+ * - `supportedArchitectures`.
139
+ * - `minimumReleaseAge`/`minimumReleaseAgeExclude`: silently dropping these
140
+ * falls back to pnpm's own default cutoff instead of the workspace's,
141
+ * which can reject lockfile entries the workspace itself allows (or vice
142
+ * versa).
143
+ * - `minimumReleaseAgeStrict`: starting with pnpm 11.0.0, a declared
144
+ * `minimumReleaseAge` enables strict mode unless `minimumReleaseAgeStrict:
145
+ * false` is also declared. Dropping an explicit `false` here would
146
+ * silently re-enable strict mode in the pruned output and make its
147
+ * install reject an immature exact dependency the source workspace
148
+ * accepted.
149
+ */
150
+ const STRAIGHT_COPY_WORKSPACE_SETTINGS = [
151
+ 'supportedArchitectures',
152
+ 'minimumReleaseAge',
153
+ 'minimumReleaseAgeExclude',
154
+ 'minimumReleaseAgeStrict',
155
+ ];
132
156
  /**
133
157
  * The settings `getPrunedPnpmInstallSettingsYaml` emits, before serialization,
134
158
  * so a caller can tell which of them the workspace actually declares. Null when
@@ -155,7 +179,7 @@ function getPrunedPnpmWorkspaceSettings(workspaceRootPath, prunedLockfileContent
155
179
  }
156
180
  catch {
157
181
  // Unreadable or malformed pnpm-workspace.yaml: skip rather than guess.
158
- logger_1.logger.warn('Could not read the workspace root pnpm-workspace.yaml; the pruned output will not declare pnpm install settings (build-script approvals, supportedArchitectures, patchedDependencies).');
182
+ logger_1.logger.warn('Could not read the workspace root pnpm-workspace.yaml; the pruned output will not declare pnpm install settings (build-script approvals, supportedArchitectures, minimumReleaseAge, patchedDependencies).');
159
183
  return null;
160
184
  }
161
185
  const settings = {};
@@ -167,8 +191,8 @@ function getPrunedPnpmWorkspaceSettings(workspaceRootPath, prunedLockfileContent
167
191
  settings.allowBuilds = allowBuilds;
168
192
  }
169
193
  }
170
- if (rootSettings.supportedArchitectures) {
171
- settings.supportedArchitectures = rootSettings.supportedArchitectures;
194
+ for (const field of STRAIGHT_COPY_WORKSPACE_SETTINGS) {
195
+ assignDefined(settings, rootSettings, field);
172
196
  }
173
197
  const patchedDependencies = precomputed?.patchedDependencies ??
174
198
  (prunedLockfileContent
@@ -216,7 +240,7 @@ function getPnpmMajor(workspaceRootPath) {
216
240
  function getPnpmMajorOrWarn(workspaceRootPath) {
217
241
  const pnpmMajor = getPnpmMajor(workspaceRootPath);
218
242
  if (pnpmMajor === null) {
219
- logger_1.logger.warn('Could not determine the pnpm version. The pruned output will not carry pnpm build-script approvals, supportedArchitectures, or patchedDependencies declarations; patch files still ship.');
243
+ logger_1.logger.warn('Could not determine the pnpm version. The pruned output will not carry pnpm build-script approvals, supportedArchitectures, minimumReleaseAge, or patchedDependencies declarations; patch files still ship.');
220
244
  }
221
245
  return pnpmMajor;
222
246
  }
@@ -1,3 +1,5 @@
1
+ import { ChildProcess } from 'child_process';
2
+ import { Socket } from 'net';
1
3
  import type { PluginConfiguration } from '../../../config/nx-json';
2
4
  import type { ProjectGraph } from '../../../config/project-graph';
3
5
  import type { RawProjectGraphDependency } from '../../project-graph-builder';
@@ -40,6 +42,13 @@ export declare class IsolatedPlugin implements LoadedNxPlugin {
40
42
  static load(plugin: PluginConfiguration, root: string, index?: number): Promise<IsolatedPlugin>;
41
43
  private constructor();
42
44
  private spawnAndConnect;
45
+ /**
46
+ * Drops the worker from service without judging why. The next hook call
47
+ * respawns it through `ensureAlive`.
48
+ */
49
+ private markUnusable;
50
+ private failPendingRequests;
51
+ private handleFramingFailure;
43
52
  /**
44
53
  * Ensures the worker is alive, restarting it if necessary.
45
54
  * Called before each hook execution to handle plugins that were
@@ -61,6 +70,7 @@ export declare class IsolatedPlugin implements LoadedNxPlugin {
61
70
  private registerProcessMetrics;
62
71
  }
63
72
  export declare function getPluginWorkerSocketId(): string;
73
+ export declare function connectToWorker(worker: ChildProcess, ipcPath: string, name: string): Promise<Socket>;
64
74
  /**
65
75
  * Marks a failure to *start or reach* a worker, as distinct from a plugin that
66
76
  * loaded and then threw. Only the former can be retried in-process: swallowing
@@ -75,5 +85,11 @@ export declare const PLUGIN_WORKER_STARTUP_FAILURE: unique symbol;
75
85
  * also degrading for an OOM kill or a broken install.
76
86
  */
77
87
  export declare const PLUGIN_WORKER_SOCKET_REFUSED: unique symbol;
88
+ /**
89
+ * Describes how a worker died for an error message. A `null` code with a signal
90
+ * is an outside kill rather than anything the worker chose, and SIGKILL is what
91
+ * an out-of-memory kill looks like, so both are called out by name.
92
+ */
93
+ export declare function describeWorkerExit(code: number | null, signal: NodeJS.Signals | null): string;
78
94
  export declare function isPluginWorkerStartupFailure(error: unknown): boolean;
79
95
  export declare function isPluginWorkerSocketRefusal(error: unknown): boolean;
@@ -2,6 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PLUGIN_WORKER_SOCKET_REFUSED = exports.PLUGIN_WORKER_STARTUP_FAILURE = exports.IsolatedPlugin = void 0;
4
4
  exports.getPluginWorkerSocketId = getPluginWorkerSocketId;
5
+ exports.connectToWorker = connectToWorker;
6
+ exports.describeWorkerExit = describeWorkerExit;
5
7
  exports.isPluginWorkerStartupFailure = isPluginWorkerStartupFailure;
6
8
  exports.isPluginWorkerSocketRefusal = isPluginWorkerSocketRefusal;
7
9
  const child_process_1 = require("child_process");
@@ -93,32 +95,51 @@ class IsolatedPlugin {
93
95
  this.worker = worker;
94
96
  this.socket = socket;
95
97
  this.registerProcessMetrics();
96
- this.exitHandler = () => {
97
- this._alive = false;
98
- this._connectPromise = null;
99
- if (this.worker?.stdout) {
100
- this.worker.stdout.unpipe(process.stdout);
101
- }
102
- if (this.worker?.stderr) {
103
- this.worker.stderr.unpipe(process.stderr);
104
- }
105
- // Reject all pending requests
106
- const error = new Error(`Plugin worker "${this.name}" exited unexpectedly.`);
107
- for (const { onError } of this.responseHandlers.values()) {
108
- onError(error);
109
- }
110
- this.responseHandlers.clear();
98
+ this.exitHandler = (code, signal) => {
99
+ this.markUnusable();
100
+ this.failPendingRequests(new Error(`Plugin worker "${this.name}" exited unexpectedly ${describeWorkerExit(code, signal)}.`));
111
101
  };
112
102
  worker.on('exit', this.exitHandler);
113
103
  socket.on('data', (0, consume_messages_from_socket_1.consumeMessagesFromSocket)(this.handleSocketData, (err) => {
114
- // Nothing else settles the pending hook promises on a framing
115
- // failure, so surface it the same way a dead worker would.
116
- this.exitHandler?.();
117
- socket.destroy();
118
- console.error(err.message);
104
+ // The worker may still be running here; it is the stream that failed, so
105
+ // reporting this as an exit would send whoever reads it hunting for a
106
+ // dead process.
107
+ this.handleFramingFailure(socket, err);
119
108
  }));
120
109
  return this.sendLoadMessage();
121
110
  }
111
+ /**
112
+ * Drops the worker from service without judging why. The next hook call
113
+ * respawns it through `ensureAlive`.
114
+ */
115
+ markUnusable() {
116
+ this._alive = false;
117
+ this._connectPromise = null;
118
+ if (this.worker?.stdout) {
119
+ this.worker.stdout.unpipe(process.stdout);
120
+ }
121
+ if (this.worker?.stderr) {
122
+ this.worker.stderr.unpipe(process.stderr);
123
+ }
124
+ }
125
+ failPendingRequests(error) {
126
+ for (const { onError } of this.responseHandlers.values()) {
127
+ onError(error);
128
+ }
129
+ this.responseHandlers.clear();
130
+ }
131
+ handleFramingFailure(socket, error) {
132
+ const framingError = new Error(`Plugin worker "${this.name}" sent a message the host could not read, ` +
133
+ `so its connection was dropped. ${error.message}`);
134
+ this.markUnusable();
135
+ socket.destroy();
136
+ if (this.responseHandlers.size === 0) {
137
+ console.error(framingError.message);
138
+ }
139
+ else {
140
+ this.failPendingRequests(framingError);
141
+ }
142
+ }
122
143
  /**
123
144
  * Ensures the worker is alive, restarting it if necessary.
124
145
  * Called before each hook execution to handle plugins that were
@@ -430,7 +451,7 @@ async function connectToWorker(worker, ipcPath, name) {
430
451
  let earlyExitError = null;
431
452
  // If the worker exits before we connect, abort polling immediately
432
453
  // rather than burning through attempts against a dead socket.
433
- worker.once('exit', (code) => {
454
+ worker.once('exit', (code, signal) => {
434
455
  if (!abortController.signal.aborted) {
435
456
  // The worker sets this code only after an EPERM/EACCES on its own bind,
436
457
  // so it is proof of a refusal rather than an inference from the
@@ -438,7 +459,7 @@ async function connectToWorker(worker, ipcPath, name) {
438
459
  // sets no variable `isSandbox()` reads.
439
460
  const refused = code === socket_refused_exit_code_1.SOCKET_REFUSED_EXIT_CODE;
440
461
  earlyExitError = markWorkerStartupFailure(new Error([
441
- `Plugin worker for "${name}" exited with code ${code} before the connection was established.`,
462
+ `Plugin worker for "${name}" exited ${describeWorkerExit(code, signal)} before the connection was established.`,
442
463
  // The worker's own stderr may be lost with the process, so the
443
464
  // cause and the fix are repeated here.
444
465
  ...(refused || (0, is_sandbox_1.isSandbox)()
@@ -476,6 +497,22 @@ exports.PLUGIN_WORKER_STARTUP_FAILURE = Symbol.for('nx.pluginWorkerStartupFailur
476
497
  * also degrading for an OOM kill or a broken install.
477
498
  */
478
499
  exports.PLUGIN_WORKER_SOCKET_REFUSED = Symbol.for('nx.pluginWorkerSocketRefused');
500
+ /**
501
+ * Describes how a worker died for an error message. A `null` code with a signal
502
+ * is an outside kill rather than anything the worker chose, and SIGKILL is what
503
+ * an out-of-memory kill looks like, so both are called out by name.
504
+ */
505
+ function describeWorkerExit(code, signal) {
506
+ if (signal) {
507
+ return signal === 'SIGKILL'
508
+ ? `(killed by ${signal}, commonly an out-of-memory kill)`
509
+ : `(killed by ${signal})`;
510
+ }
511
+ if (code !== null) {
512
+ return `(exit code ${code})`;
513
+ }
514
+ return '(no exit code or signal reported)';
515
+ }
479
516
  function markWorkerStartupFailure(error, refused = false) {
480
517
  error[exports.PLUGIN_WORKER_STARTUP_FAILURE] = true;
481
518
  if (refused) {
@@ -11,6 +11,8 @@ export declare class ForkedProcessTaskRunner {
11
11
  cliPath: string;
12
12
  private readonly verbose;
13
13
  private processes;
14
+ private processMessageHandler;
15
+ private processExitHandler;
14
16
  private finishedProcesses;
15
17
  private pseudoTerminals;
16
18
  constructor(options: DefaultTasksRunnerOptions, tuiEnabled: boolean);
@@ -39,4 +41,5 @@ export declare class ForkedProcessTaskRunner {
39
41
  private writeTerminalOutput;
40
42
  cleanup(signal?: NodeJS.Signals): Promise<void>;
41
43
  private setupProcessEventListeners;
44
+ removeProcessEventListeners(): void;
42
45
  }
@@ -269,7 +269,7 @@ class ForkedProcessTaskRunner {
269
269
  this.cleanUpBatchProcesses();
270
270
  }
271
271
  setupProcessEventListeners() {
272
- const messageHandler = (message) => {
272
+ this.processMessageHandler = (message) => {
273
273
  this.pseudoTerminals.forEach((p) => {
274
274
  p.sendMessageToChildren(message);
275
275
  });
@@ -279,21 +279,30 @@ class ForkedProcessTaskRunner {
279
279
  }
280
280
  });
281
281
  };
282
+ this.processExitHandler = () => {
283
+ this.cleanup();
284
+ process.off('message', this.processMessageHandler);
285
+ };
282
286
  // When the nx process gets a message, it will be sent into the task's process
283
- process.on('message', messageHandler);
287
+ process.on('message', this.processMessageHandler);
284
288
  // Terminate any task processes on exit (sync, last resort).
285
289
  // cleanup() is async but the initial signal dispatch is synchronous
286
290
  // (killProcessTreeGraceful snapshots and signals before the async
287
291
  // grace period). The grace period won't complete here, but each
288
292
  // child also has its own sync exit handler as a final fallback.
289
- process.once('exit', () => {
290
- this.cleanup();
291
- process.off('message', messageHandler);
292
- });
293
+ process.once('exit', this.processExitHandler);
293
294
  // No SIGINT/SIGTERM/SIGHUP handlers here. The orchestrator's
294
295
  // setupSignalHandlers() owns signal dispatch and calls FPTR.cleanup()
295
296
  // in the direct path; in the forked path the process is detached
296
297
  // and never receives these signals from the OS.
297
298
  }
299
+ // Long-lived processes (Nx Cloud agents) create a runner per invocation;
300
+ // without this each instance stays reachable from `process` forever.
301
+ // Call only after cleanup(): the exit handler is the last-resort child
302
+ // kill, so it must not be removed while children may still be alive.
303
+ removeProcessEventListeners() {
304
+ process.off('message', this.processMessageHandler);
305
+ process.off('exit', this.processExitHandler);
306
+ }
298
307
  }
299
308
  exports.ForkedProcessTaskRunner = ForkedProcessTaskRunner;
@@ -9,6 +9,7 @@ const life_cycle_1 = require("./life-cycle");
9
9
  const task_orchestrator_1 = require("./task-orchestrator");
10
10
  const create_task_hasher_1 = require("../hasher/create-task-hasher");
11
11
  const client_1 = require("../daemon/client/client");
12
+ const shared_running_task_1 = require("./running-tasks/shared-running-task");
12
13
  const task_results_life_cycle_1 = require("./life-cycles/task-results-life-cycle");
13
14
  async function createOrchestrator(tasks, projectGraph, fullTaskGraph, nxJson, lifeCycle) {
14
15
  (0, dotenv_1.loadRootEnvFiles)();
@@ -47,6 +48,11 @@ async function createOrchestrator(tasks, projectGraph, fullTaskGraph, nxJson, li
47
48
  orchestrator.processAllScheduledTasks();
48
49
  return orchestrator;
49
50
  }
51
+ // Nothing awaits the dispose chains below, so an unhandled rejection would take
52
+ // down a long-lived agent process.
53
+ function logDisposeFailure(e) {
54
+ console.error('Failed to dispose the task orchestrator:', e);
55
+ }
50
56
  async function runDiscreteTasks(tasks, projectGraph, fullTaskGraph, nxJson, lifeCycle) {
51
57
  const orchestrator = await createOrchestrator(tasks, projectGraph, fullTaskGraph, nxJson, lifeCycle);
52
58
  let groupId = 0;
@@ -75,12 +81,36 @@ async function runDiscreteTasks(tasks, projectGraph, fullTaskGraph, nxJson, life
75
81
  return [hit];
76
82
  return [await orchestrator.runTaskDirectly(true, task, groupId++)];
77
83
  });
78
- return [...batchResults, ...taskResults];
84
+ const results = [...batchResults, ...taskResults];
85
+ // Callers like Nx Cloud agents create an orchestrator per invocation in a
86
+ // long-lived process; release its process-level listeners once all tasks
87
+ // settle, otherwise every invocation's orchestrator stays reachable forever.
88
+ // Not awaited, so callers keep consuming handles as they settle; the forked
89
+ // runner's exit handler still reaps children until dispose() runs.
90
+ Promise.allSettled(results)
91
+ .then(() => orchestrator.dispose())
92
+ .catch(logDisposeFailure);
93
+ return results;
79
94
  }
80
95
  async function runContinuousTasks(tasks, projectGraph, fullTaskGraph, nxJson, lifeCycle) {
81
96
  const orchestrator = await createOrchestrator(tasks, projectGraph, fullTaskGraph, nxJson, lifeCycle);
82
- return tasks.reduce((current, task, index) => {
97
+ const runningTasks = tasks.reduce((current, task, index) => {
83
98
  current[task.id] = orchestrator.startContinuousTask(task, index);
84
99
  return current;
85
100
  }, {});
101
+ // Unlike runDiscreteTasks, this must resolve at task start: callers keep
102
+ // the RunningTask handles to kill later, so disposal has to be deferred
103
+ // until every task actually exits.
104
+ Promise.allSettled(Object.entries(runningTasks).map(async ([taskId, promise]) => {
105
+ const runningTask = await promise;
106
+ // A shared task is owned by another nx process; this orchestrator has
107
+ // no child to protect for it, so disposal does not wait on it.
108
+ if (runningTask instanceof shared_running_task_1.SharedRunningTask) {
109
+ return;
110
+ }
111
+ await orchestrator.waitForContinuousTaskExit(taskId);
112
+ }))
113
+ .then(() => orchestrator.dispose())
114
+ .catch(logDisposeFailure);
115
+ return runningTasks;
86
116
  }
@@ -22,6 +22,8 @@ function getTasksHistoryLifeCycle() {
22
22
  class TaskHistoryLifeCycle {
23
23
  constructor() {
24
24
  this.startTimings = {};
25
+ // Holds no terminalOutput: this singleton outlives every run in the
26
+ // process, and agents complete thousands of tasks in one process.
25
27
  this.pendingResults = new Map();
26
28
  this.taskRuns = new Map();
27
29
  this.taskHistory = (0, task_history_1.getTaskHistory)();
@@ -36,25 +38,28 @@ class TaskHistoryLifeCycle {
36
38
  }
37
39
  }
38
40
  async endTasks(taskResults) {
39
- for (const taskResult of taskResults) {
40
- this.pendingResults.set(taskResult.task.id, taskResult);
41
+ for (const { task, code, status } of taskResults) {
42
+ this.pendingResults.set(task.id, { task, code, status });
41
43
  }
42
44
  }
43
45
  async endCommand() {
46
+ const pendingResults = Array.from(this.pendingResults.values());
47
+ this.pendingResults.clear();
44
48
  if (!this.taskHistory) {
45
49
  return;
46
50
  }
47
51
  // Build TaskRun objects now — task.hash is guaranteed to be set by this point
48
- for (const [, taskResult] of this.pendingResults) {
49
- this.taskRuns.set(taskResult.task.hash, {
50
- hash: taskResult.task.hash,
51
- target: taskResult.task.target,
52
- code: taskResult.code,
53
- status: taskResult.status,
54
- start: taskResult.task.startTime ?? this.startTimings[taskResult.task.id],
55
- end: taskResult.task.endTime ?? Date.now(),
56
- cacheable: taskResult.task.cache === true,
52
+ for (const { task, code, status } of pendingResults) {
53
+ this.taskRuns.set(task.hash, {
54
+ hash: task.hash,
55
+ target: task.target,
56
+ code,
57
+ status,
58
+ start: task.startTime ?? this.startTimings[task.id],
59
+ end: task.endTime ?? Date.now(),
60
+ cacheable: task.cache === true,
57
61
  });
62
+ delete this.startTimings[task.id];
58
63
  }
59
64
  const runs = [];
60
65
  // Only check for flaky tasks among cacheable tasks
@@ -233,6 +233,9 @@ class BatchProcess {
233
233
  this.capturedOutputFd = undefined;
234
234
  }
235
235
  }
236
+ // Resolves on the CompleteBatchExecution message, not process exit: the
237
+ // child can outlive its results (open handles keep it alive), so callers
238
+ // must not treat a settled batch as an exited process.
236
239
  async getResults() {
237
240
  return Promise.race([
238
241
  new Promise((_, rej) => {
@@ -3,6 +3,7 @@ import { RunningTasksService } from '../../native';
3
3
  export declare class SharedRunningTask implements RunningTask {
4
4
  private runningTasksService;
5
5
  private exitCallbacks;
6
+ private detached;
6
7
  constructor(runningTasksService: RunningTasksService, taskId: string);
7
8
  getResults(): Promise<{
8
9
  code: number;
@@ -5,7 +5,11 @@ class SharedRunningTask {
5
5
  constructor(runningTasksService, taskId) {
6
6
  this.runningTasksService = runningTasksService;
7
7
  this.exitCallbacks = [];
8
+ this.detached = false;
8
9
  this.waitForTaskToFinish(taskId).then(() => {
10
+ if (this.detached) {
11
+ return;
12
+ }
9
13
  // notify exit callbacks
10
14
  this.exitCallbacks.forEach((cb) => cb(0));
11
15
  });
@@ -13,8 +17,14 @@ class SharedRunningTask {
13
17
  async getResults() {
14
18
  throw new Error('Results cannot be retrieved from a shared task');
15
19
  }
20
+ // Detaches from the shared task; the owning process keeps running it.
16
21
  kill() {
22
+ if (this.detached) {
23
+ return;
24
+ }
25
+ this.detached = true;
17
26
  this.exitCallbacks.forEach((cb) => cb(0));
27
+ this.exitCallbacks = [];
18
28
  }
19
29
  onExit(cb) {
20
30
  this.exitCallbacks.push(cb);
@@ -24,7 +34,8 @@ class SharedRunningTask {
24
34
  // wait for the running task to finish
25
35
  do {
26
36
  await new Promise((resolve) => setTimeout(resolve, 100));
27
- } while (this.runningTasksService.getRunningTasks([taskId]).length);
37
+ } while (!this.detached &&
38
+ this.runningTasksService.getRunningTasks([taskId]).length);
28
39
  }
29
40
  }
30
41
  exports.SharedRunningTask = SharedRunningTask;
@@ -56,6 +56,7 @@ export declare class TaskOrchestrator {
56
56
  private discreteTaskExitHandled;
57
57
  private continuousTaskExitHandled;
58
58
  private cleanupPromise;
59
+ private signalHandlers;
59
60
  constructor(hasher: TaskHasher, initiatingProject: string | undefined, initiatingTasks: Task[], projectGraph: ProjectGraph, taskGraph: TaskGraph, nxJson: NxJsonConfiguration, options: NxArgs & DefaultTasksRunnerOptions, bail: boolean, daemon: DaemonClient, outputStyle: string, fullTaskGraph?: TaskGraph);
60
61
  init(): Promise<void>;
61
62
  run(): Promise<{
@@ -251,6 +252,8 @@ export declare class TaskOrchestrator {
251
252
  private cleanup;
252
253
  private performCleanup;
253
254
  private setupSignalHandlers;
255
+ waitForContinuousTaskExit(taskId: string): Promise<void>;
256
+ dispose(): Promise<void>;
254
257
  private cleanUpUnneededContinuousTasks;
255
258
  }
256
259
  export declare function getThreadPoolSize(options: NxArgs & DefaultTasksRunnerOptions, taskGraph: TaskGraph): {
@@ -105,6 +105,7 @@ class TaskOrchestrator {
105
105
  this.discreteTaskExitHandled = new Map();
106
106
  this.continuousTaskExitHandled = new Map();
107
107
  this.cleanupPromise = null;
108
+ this.signalHandlers = [];
108
109
  }
109
110
  async init() {
110
111
  this.setupSignalHandlers();
@@ -159,6 +160,7 @@ class TaskOrchestrator {
159
160
  this.cache.removeOldCacheRecords();
160
161
  }
161
162
  await this.cleanup();
163
+ await this.dispose();
162
164
  // Public API (defaultTasksRunner) returns a plain object keyed by
163
165
  // task id. Internal state is a Map for faster lookup.
164
166
  return Object.fromEntries(this.completedTasks);
@@ -1474,9 +1476,35 @@ class TaskOrchestrator {
1474
1476
  }
1475
1477
  });
1476
1478
  };
1477
- process.on('SIGINT', () => handleSignal('SIGINT'));
1478
- process.on('SIGTERM', () => handleSignal('SIGTERM'));
1479
- process.on('SIGHUP', () => handleSignal('SIGHUP'));
1479
+ for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
1480
+ const handler = () => handleSignal(signal);
1481
+ this.signalHandlers.push([signal, handler]);
1482
+ process.on(signal, handler);
1483
+ }
1484
+ }
1485
+ // Registered at child creation, so unlike subscribing to RunningTask.onExit
1486
+ // after the fact it cannot miss an exit that already happened.
1487
+ waitForContinuousTaskExit(taskId) {
1488
+ return this.continuousTaskExitHandled.get(taskId) ?? Promise.resolve();
1489
+ }
1490
+ // Releases the process-level listeners registered by setupSignalHandlers.
1491
+ // Each closes over `this`, so a long-lived caller (an Nx Cloud agent creates
1492
+ // an orchestrator per invocation) leaks whole orchestrators until they run.
1493
+ async dispose() {
1494
+ // The forked runner's exit handler is the last-resort kill for child
1495
+ // processes, and a batch child can outlive its results message. Reap
1496
+ // children first so removing the handler cannot orphan a live one.
1497
+ try {
1498
+ await this.forkedProcessTaskRunner.cleanup();
1499
+ }
1500
+ catch (e) {
1501
+ console.error('Failed to clean up child processes on dispose:', e);
1502
+ }
1503
+ for (const [signal, handler] of this.signalHandlers) {
1504
+ process.off(signal, handler);
1505
+ }
1506
+ this.signalHandlers = [];
1507
+ this.forkedProcessTaskRunner.removeProcessEventListeners();
1480
1508
  }
1481
1509
  cleanUpUnneededContinuousTasks() {
1482
1510
  const incompleteTasks = this.tasksSchedule.getIncompleteTasks();
@@ -9,6 +9,14 @@ const errors_1 = require("../errors");
9
9
  const pick_1 = require("../pick");
10
10
  // Ordered newest-bound-first so the first matching row applies.
11
11
  const PNPM_BEHAVIOR_ROWS = [
12
+ {
13
+ minVersion: '12.0.0',
14
+ major: 12,
15
+ strictMode: 'default-loose',
16
+ strictAutoOnWhenExplicit: true,
17
+ excludeGrammar: 'v2-globs-unions',
18
+ writesExcludes: true,
19
+ },
12
20
  {
13
21
  minVersion: '11.1.3',
14
22
  major: 11,
@@ -134,7 +142,7 @@ function toBoolean(value) {
134
142
  async function readPnpmPolicy(root, pmVersion) {
135
143
  // A pnpm major newer than the table knows can behave differently; defer to a
136
144
  // real install rather than guessing.
137
- if ((0, semver_1.gte)(pmVersion, '12.0.0')) {
145
+ if ((0, semver_1.gte)(pmVersion, '13.0.0')) {
138
146
  return {
139
147
  outcome: 'ambiguous',
140
148
  reason: `pnpm ${pmVersion} is newer than the known cooldown behavior table.`,
@@ -155,15 +163,15 @@ async function readPnpmPolicy(root, pmVersion) {
155
163
  }
156
164
  const { windowMinutes: explicitWindow, excludes, strictExplicit, ignoreMissingTimeExplicit, } = parseCooldownConfig(config);
157
165
  // An explicitly configured window (from whatever surface pnpm honors) wins;
158
- // otherwise v11 falls back to its built-in 1440-minute (1 day) default, while
159
- // v10 has no default (no cooldown).
166
+ // otherwise v11 and v12 fall back to the built-in 1440-minute (1 day) default,
167
+ // while v10 has no default (no cooldown).
160
168
  let windowMinutes;
161
169
  let windowExplicit;
162
170
  if (explicitWindow !== undefined) {
163
171
  windowMinutes = explicitWindow;
164
172
  windowExplicit = true;
165
173
  }
166
- else if (row.major === 11) {
174
+ else if (row.major >= 11) {
167
175
  windowMinutes = 1440;
168
176
  windowExplicit = false;
169
177
  }
@@ -178,14 +186,14 @@ async function readPnpmPolicy(root, pmVersion) {
178
186
  const cutoffMs = Date.now() - windowMs;
179
187
  const sourceDescription = `pnpm minimumReleaseAge (${windowMinutes} min)`;
180
188
  const strict = resolveStrict(row, strictExplicit, windowExplicit);
181
- // v10 always errors on missing time; v11 defaults to skip unless a surface
182
- // explicitly disabled it.
183
- const ignoreMissingTime = row.major === 11 ? (ignoreMissingTimeExplicit ?? true) : false;
189
+ // v10 always errors on missing time; v11 and above default to skip unless a
190
+ // surface explicitly disabled it.
191
+ const ignoreMissingTime = row.major >= 11 ? (ignoreMissingTimeExplicit ?? true) : false;
184
192
  const behavior = {
185
193
  packageManager: 'pnpm',
186
194
  strict,
187
- // Loose fallback only exists on v11 when strict is off.
188
- looseFallback: row.major === 11 && !strict,
195
+ // Loose fallback only exists from v11 onwards when strict is off.
196
+ looseFallback: row.major >= 11 && !strict,
189
197
  writesExcludes: row.writesExcludes,
190
198
  missingTimeMap: row.major === 10 ? 'error' : ignoreMissingTime ? 'skip' : 'error',
191
199
  };
@@ -23,6 +23,7 @@ const target_merging_1 = require("../project-graph/utils/project-configuration/t
23
23
  const catalog_1 = require("./catalog");
24
24
  const fileutils_1 = require("./fileutils");
25
25
  const has_nx_js_plugin_1 = require("./has-nx-js-plugin");
26
+ const path_2 = require("./path");
26
27
  const installation_directory_1 = require("./installation-directory");
27
28
  const package_manager_1 = require("./package-manager");
28
29
  const workspace_root_1 = require("./workspace-root");
@@ -35,15 +36,25 @@ function normalizePackageGroup(packageGroup) {
35
36
  }));
36
37
  }
37
38
  function readNxMigrateConfig(json) {
39
+ // Registry fetching uses this value to build a temporary extraction path.
40
+ // Reject unsupported absolute paths and escaping parent traversal before extraction.
41
+ const assertContained = (migrations) => {
42
+ if (!(0, path_2.isContainedRelativePath)(migrations)) {
43
+ throw new Error(`Invalid migrations path "${migrations}" in package "${json.name ?? 'unknown'}@${json.version ?? 'unknown'}": migration paths must not be absolute or escape their base directory through parent traversal.`);
44
+ }
45
+ return migrations;
46
+ };
38
47
  const parseNxMigrationsConfig = (fromJson) => {
39
48
  if (!fromJson) {
40
49
  return {};
41
50
  }
42
51
  if (typeof fromJson === 'string') {
43
- return { migrations: fromJson, packageGroup: [] };
52
+ return { migrations: assertContained(fromJson), packageGroup: [] };
44
53
  }
45
54
  return {
46
- ...(fromJson.migrations ? { migrations: fromJson.migrations } : {}),
55
+ ...(fromJson.migrations
56
+ ? { migrations: assertContained(fromJson.migrations) }
57
+ : {}),
47
58
  ...(fromJson.packageGroup
48
59
  ? { packageGroup: normalizePackageGroup(fromJson.packageGroup) }
49
60
  : {}),
@@ -443,7 +443,7 @@ function applyVerbosity(options, schema, isVerbose) {
443
443
  }
444
444
  function combineOptionsForExecutor(commandLineOpts, config, target, schema, defaultProjectName, relativeCwd, isVerbose = false) {
445
445
  const r = convertAliases(coerceTypesInOptions(convertToCamelCase(commandLineOpts, schema), schema), schema, false);
446
- let combined = target.options || {};
446
+ let combined = { ...target.options };
447
447
  if (config && target.configurations && target.configurations[config]) {
448
448
  Object.assign(combined, target.configurations[config]);
449
449
  }
@@ -8,6 +8,12 @@ export declare function normalizePath(osSpecificPath: string): string;
8
8
  * This should not be used to read files on disk because of the removal of Windows drive letters.
9
9
  */
10
10
  export declare function joinPathFragments(...fragments: string[]): string;
11
+ /**
12
+ * True when `relativePath` stays inside the directory it is resolved against.
13
+ * Absolute paths are rejected because `path.join` silently treats them as
14
+ * relative, so they would otherwise slip past a `..` check.
15
+ */
16
+ export declare function isContainedRelativePath(relativePath: string): boolean;
11
17
  /**
12
18
  * When running a script with the package manager (e.g. `npm run`), the package manager will
13
19
  * traverse the directory tree upwards until it finds a `package.json` and will set `process.cwd()`
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.normalizePath = normalizePath;
4
4
  exports.joinPathFragments = joinPathFragments;
5
+ exports.isContainedRelativePath = isContainedRelativePath;
5
6
  exports.getCwd = getCwd;
6
7
  const tslib_1 = require("tslib");
7
8
  const path = tslib_1.__importStar(require("path"));
@@ -23,6 +24,20 @@ function normalizePath(osSpecificPath) {
23
24
  function joinPathFragments(...fragments) {
24
25
  return normalizePath(path.join(...fragments));
25
26
  }
27
+ /**
28
+ * True when `relativePath` stays inside the directory it is resolved against.
29
+ * Absolute paths are rejected because `path.join` silently treats them as
30
+ * relative, so they would otherwise slip past a `..` check.
31
+ */
32
+ function isContainedRelativePath(relativePath) {
33
+ if (path.isAbsolute(relativePath)) {
34
+ return false;
35
+ }
36
+ const normalized = path.normalize(relativePath);
37
+ return !(normalized === '..' ||
38
+ normalized.startsWith(`..${path.sep}`) ||
39
+ normalized.startsWith(`..${path.posix.sep}`));
40
+ }
26
41
  /**
27
42
  * When running a script with the package manager (e.g. `npm run`), the package manager will
28
43
  * traverse the directory tree upwards until it finds a `package.json` and will set `process.cwd()`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nx",
3
- "version": "23.2.0",
3
+ "version": "23.3.0-beta.0",
4
4
  "private": false,
5
5
  "type": "commonjs",
6
6
  "description": "The core Nx plugin contains the core functionality of Nx like the project graph, nx commands and task orchestration.",
@@ -185,16 +185,16 @@
185
185
  }
186
186
  },
187
187
  "optionalDependencies": {
188
- "@nx/nx-darwin-arm64": "23.2.0",
189
- "@nx/nx-darwin-x64": "23.2.0",
190
- "@nx/nx-freebsd-x64": "23.2.0",
191
- "@nx/nx-linux-arm-gnueabihf": "23.2.0",
192
- "@nx/nx-linux-arm64-gnu": "23.2.0",
193
- "@nx/nx-linux-arm64-musl": "23.2.0",
194
- "@nx/nx-linux-x64-gnu": "23.2.0",
195
- "@nx/nx-linux-x64-musl": "23.2.0",
196
- "@nx/nx-win32-arm64-msvc": "23.2.0",
197
- "@nx/nx-win32-x64-msvc": "23.2.0"
188
+ "@nx/nx-darwin-arm64": "23.3.0-beta.0",
189
+ "@nx/nx-darwin-x64": "23.3.0-beta.0",
190
+ "@nx/nx-freebsd-x64": "23.3.0-beta.0",
191
+ "@nx/nx-linux-arm-gnueabihf": "23.3.0-beta.0",
192
+ "@nx/nx-linux-arm64-gnu": "23.3.0-beta.0",
193
+ "@nx/nx-linux-arm64-musl": "23.3.0-beta.0",
194
+ "@nx/nx-linux-x64-gnu": "23.3.0-beta.0",
195
+ "@nx/nx-linux-x64-musl": "23.3.0-beta.0",
196
+ "@nx/nx-win32-arm64-msvc": "23.3.0-beta.0",
197
+ "@nx/nx-win32-x64-msvc": "23.3.0-beta.0"
198
198
  },
199
199
  "nx-migrations": {
200
200
  "migrations": "./migrations.json",