nx 22.7.7 → 22.7.9

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 (52) hide show
  1. package/dist/src/ai/set-up-ai-agents/set-up-ai-agents.js +2 -1
  2. package/dist/src/command-line/configure-ai-agents/configure-ai-agents.js +1 -1
  3. package/dist/src/command-line/init/init-v2.js +1 -1
  4. package/dist/src/command-line/migrate/migrate-ui-api.js +17 -12
  5. package/dist/src/command-line/reset/reset.js +6 -1
  6. package/dist/src/daemon/client/client.d.ts +45 -3
  7. package/dist/src/daemon/client/client.js +138 -42
  8. package/dist/src/daemon/client/daemon-socket-messenger.js +4 -0
  9. package/dist/src/daemon/message-types/daemon-message.d.ts +33 -0
  10. package/dist/src/daemon/message-types/daemon-message.js +48 -0
  11. package/dist/src/daemon/server/latest-nx.js +3 -1
  12. package/dist/src/daemon/server/server.js +22 -0
  13. package/dist/src/daemon/server/shutdown-utils.d.ts +5 -0
  14. package/dist/src/daemon/server/shutdown-utils.js +11 -4
  15. package/dist/src/daemon/socket-utils.d.ts +1 -0
  16. package/dist/src/daemon/socket-utils.js +24 -3
  17. package/dist/src/daemon/tmp-dir.d.ts +43 -3
  18. package/dist/src/daemon/tmp-dir.js +437 -15
  19. package/dist/src/devkit-internals.d.ts +1 -0
  20. package/dist/src/devkit-internals.js +4 -1
  21. package/dist/src/native/index.js +71 -28
  22. package/dist/src/native/native-file-cache-location.d.ts +17 -1
  23. package/dist/src/native/native-file-cache-location.js +86 -12
  24. package/dist/src/native/nx.wasm32-wasi.debug.wasm +0 -0
  25. package/dist/src/native/nx.wasm32-wasi.wasm +0 -0
  26. package/dist/src/project-graph/file-utils.js +5 -1
  27. package/dist/src/project-graph/plugins/isolation/isolated-plugin.d.ts +1 -0
  28. package/dist/src/project-graph/plugins/isolation/isolated-plugin.js +18 -1
  29. package/dist/src/project-graph/plugins/isolation/messaging.d.ts +16 -3
  30. package/dist/src/project-graph/plugins/isolation/messaging.js +3 -0
  31. package/dist/src/project-graph/plugins/isolation/plugin-worker.js +58 -8
  32. package/dist/src/project-graph/project-graph.js +20 -0
  33. package/dist/src/utils/command-line-utils.js +30 -20
  34. package/dist/src/utils/git-revision.d.ts +12 -0
  35. package/dist/src/utils/git-revision.js +25 -0
  36. package/dist/src/utils/git-utils.d.ts +1 -3
  37. package/dist/src/utils/git-utils.js +83 -47
  38. package/dist/src/utils/nx-tmp-dir.d.ts +18 -0
  39. package/dist/src/utils/nx-tmp-dir.js +40 -0
  40. package/dist/src/utils/owned-private-dir.d.ts +132 -0
  41. package/dist/src/utils/owned-private-dir.js +323 -0
  42. package/dist/src/utils/package-json.d.ts +3 -3
  43. package/dist/src/utils/package-json.js +30 -12
  44. package/dist/src/utils/package-manager.d.ts +13 -0
  45. package/dist/src/utils/package-manager.js +27 -0
  46. package/dist/src/utils/safe-spawn.d.ts +24 -0
  47. package/dist/src/utils/safe-spawn.js +104 -0
  48. package/dist/src/utils/shell-quoting.d.ts +15 -0
  49. package/dist/src/utils/shell-quoting.js +35 -0
  50. package/dist/src/utils/wait-for-socket-connection.d.ts +11 -0
  51. package/dist/src/utils/wait-for-socket-connection.js +14 -8
  52. package/package.json +17 -13
@@ -0,0 +1,323 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DirectoryRefusedError = void 0;
4
+ exports.describeRefusal = describeRefusal;
5
+ exports.remedyFor = remedyFor;
6
+ exports.isSafeSharedRoot = isSafeSharedRoot;
7
+ exports.isPeerWritable = isPeerWritable;
8
+ exports.ensureSafeSharedRoot = ensureSafeSharedRoot;
9
+ exports.isOwnedRealDirectory = isOwnedRealDirectory;
10
+ exports.getUserSegment = getUserSegment;
11
+ exports.ensureOwnedPrivateDir = ensureOwnedPrivateDir;
12
+ const node_fs_1 = require("node:fs");
13
+ const node_os_1 = require("node:os");
14
+ // A string discriminant, not a boolean `ok`: this repo compiles with
15
+ // `strict: false`, where TypeScript does not narrow a union on a boolean literal.
16
+ const allow = (path) => ({ status: 'ok', path });
17
+ const deny = (refusal) => ({
18
+ status: 'refused',
19
+ refusal,
20
+ });
21
+ const notADirectory = (dir, stats) => stats.isSymbolicLink()
22
+ ? { kind: 'not-a-directory', dir, symlink: true }
23
+ : { kind: 'not-a-directory', dir };
24
+ // Four octal digits, so a sticky container reads `1777` and a plain directory
25
+ // `0755` — the notation `chmod` and `ls` use. Prefixing a literal `0` instead
26
+ // renders sticky modes as `01777`, which is not a form anyone writes.
27
+ const asMode = (mode) => (mode & 0o7777).toString(8).padStart(4, '0');
28
+ /** The user-facing sentence for a refusal. The only place wording is decided. */
29
+ function describeRefusal(r) {
30
+ switch (r.kind) {
31
+ case 'not-created':
32
+ return `${r.dir} could not be created${r.code ? ` (${r.code})` : ''}`;
33
+ case 'not-inspectable':
34
+ return `${r.dir} could not be inspected${r.code ? ` (${r.code})` : ''}`;
35
+ case 'not-a-directory':
36
+ return r.symlink
37
+ ? `${r.dir} is a symlink, not a real directory — something replaced the path Nx expected to create`
38
+ : `${r.dir} exists and is not a directory`;
39
+ case 'foreign-owner':
40
+ return `${r.dir} is owned by uid ${r.uid}, not by you`;
41
+ case 'foreign-shared-container':
42
+ return `${r.dir} belongs to another user (uid ${r.uid}) rather than to you or to root`;
43
+ case 'not-tightenable':
44
+ return `${r.dir} is reachable by other users (mode ${asMode(r.mode)}) and could not be tightened to 0700`;
45
+ case 'peer-writable-not-sticky':
46
+ return `${r.dir} is writable by other users but not sticky (mode ${asMode(r.mode)}), so a peer could replace directories inside it`;
47
+ default: {
48
+ // This repo sets `strict: false` and leaves `noImplicitReturns` unset, so
49
+ // a new DirRefusal member would otherwise compile here and render as
50
+ // `undefined` inside the aggregate message. Assignability to `never` still
51
+ // holds under these settings, so this is a real compile-time guard.
52
+ const unhandled = r;
53
+ throw new Error(`Unhandled directory refusal: ${unhandled.kind}`);
54
+ }
55
+ }
56
+ }
57
+ /** Single-quoted for a shell, so a path with a space or quote survives a paste. */
58
+ const shellQuote = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
59
+ /** What the user can do about a refusal, or `undefined` when there is nothing. */
60
+ function remedyFor(r) {
61
+ switch (r.kind) {
62
+ case 'not-a-directory':
63
+ return r.symlink
64
+ ? `${r.dir} is a symlink where Nx expects a directory. If you did not create it, treat it as hostile: remove the link itself (not what it points at) and run the command again.`
65
+ : undefined;
66
+ case 'foreign-owner':
67
+ return `${r.dir} belongs to another user on this machine, so Nx cannot keep its own directory there. Set NX_SOCKET_DIR to a short directory your user owns, or move it aside — which you can do yourself if you own the directory it sits in, and otherwise needs an administrator.`;
68
+ case 'not-tightenable':
69
+ // Both producers reach here having already established that the directory
70
+ // is ours, so `chmod` is the user's to run. Names no cause: one producer
71
+ // is a mount that discards the mode, the other is any `chmod` failure.
72
+ return `${r.dir} is reachable by other users (mode ${asMode(r.mode)}) and Nx could not restrict it. Run \`chmod 0700 ${shellQuote(r.dir)}\` and try again; if the mode does not stick, set NX_SOCKET_DIR to a short directory on a filesystem that keeps POSIX permissions.`;
73
+ case 'foreign-shared-container':
74
+ // Unreachable today: `isSafeSharedRoot` denies with this kind only when
75
+ // `stats.uid !== 0`, so a root-owned container never reaches here.
76
+ if (r.uid === 0) {
77
+ return undefined;
78
+ }
79
+ const q = shellQuote(r.dir);
80
+ return `${r.dir} belongs to another user on this machine, so Nx cannot keep a private directory beneath it. Ask an administrator to hand it to root with \`sudo chown root ${q} && sudo chmod 1777 ${q}\`; every user can then keep their own directory under it.`;
81
+ case 'not-created':
82
+ case 'not-inspectable':
83
+ case 'peer-writable-not-sticky':
84
+ return undefined;
85
+ default: {
86
+ // Matches `describeRefusal`'s arm, so a new kind cannot ship with a
87
+ // sentence and no advice — which is how `not-tightenable` went unadvised.
88
+ const unhandled = r;
89
+ return undefined;
90
+ }
91
+ }
92
+ }
93
+ /** One refusal as an `Error`, so several can travel in an `AggregateError`. */
94
+ class DirectoryRefusedError extends Error {
95
+ constructor(refusal) {
96
+ super(describeRefusal(refusal));
97
+ this.refusal = refusal;
98
+ this.name = 'DirectoryRefusedError';
99
+ }
100
+ }
101
+ exports.DirectoryRefusedError = DirectoryRefusedError;
102
+ /**
103
+ * chmod a path only if it is a real directory, never following a symlink at its
104
+ * final component — `chmodSync` follows them, retargeting the mode change. The
105
+ * directory check is on the descriptor, not the errno: a deny-list fails *open*
106
+ * on codes it does not know. `O_NONBLOCK` stops a planted FIFO blocking `openSync`.
107
+ */
108
+ function chmodRealDirectory(path, mode) {
109
+ let fd;
110
+ try {
111
+ fd = (0, node_fs_1.openSync)(path, node_fs_1.constants.O_RDONLY | node_fs_1.constants.O_NOFOLLOW | node_fs_1.constants.O_NONBLOCK);
112
+ }
113
+ catch {
114
+ return false;
115
+ }
116
+ try {
117
+ if (!(0, node_fs_1.fstatSync)(fd).isDirectory()) {
118
+ return false;
119
+ }
120
+ (0, node_fs_1.fchmodSync)(fd, mode);
121
+ return true;
122
+ }
123
+ catch {
124
+ return false;
125
+ }
126
+ finally {
127
+ (0, node_fs_1.closeSync)(fd);
128
+ }
129
+ }
130
+ /**
131
+ * Sticky. Restricts rename and unlink in a writable directory to the owner of
132
+ * each entry — plus the directory's own owner, which is why the ownership check
133
+ * below is not redundant with this one.
134
+ */
135
+ const S_ISVTX = 0o1000;
136
+ /**
137
+ * Whether a shared container is safe to keep an owner-only directory under.
138
+ *
139
+ * On POSIX, a container writable by other users must be sticky, and it must be
140
+ * owned by either root or the current user. Windows short-circuits after the
141
+ * directory test — the OS temp root is already scoped to one account, so there
142
+ * is no shared level whose ownership could matter.
143
+ *
144
+ * Sticky directories still let the directory's own owner rename entries, so a
145
+ * container owned by another unprivileged user could replace a previously
146
+ * verified private directory beneath it.
147
+ */
148
+ function isSafeSharedRoot(dir) {
149
+ try {
150
+ const stats = (0, node_fs_1.lstatSync)(dir);
151
+ if (!stats.isDirectory()) {
152
+ return deny(notADirectory(dir, stats));
153
+ }
154
+ if (process.platform === 'win32') {
155
+ // The OS temp root is already scoped to the current Windows user.
156
+ return allow(dir);
157
+ }
158
+ if (typeof process.getuid === 'function' &&
159
+ stats.uid !== process.getuid() &&
160
+ stats.uid !== 0) {
161
+ return deny({ kind: 'foreign-shared-container', dir, uid: stats.uid });
162
+ }
163
+ return !(stats.mode & 0o022) || !!(stats.mode & S_ISVTX)
164
+ ? allow(dir)
165
+ : deny({ kind: 'peer-writable-not-sticky', dir, mode: stats.mode });
166
+ }
167
+ catch (e) {
168
+ return deny({ kind: 'not-inspectable', dir, code: e?.code });
169
+ }
170
+ }
171
+ /**
172
+ * Whether other users on this machine can write into `dir`. Gates whether a
173
+ * refusal message may cite other users, so it must not over-report: `false` on
174
+ * Windows, where libuv synthesizes `st_mode` from the READONLY attribute and
175
+ * reports an ordinary per-account directory as `0666`, and `false` on a path
176
+ * that cannot be inspected.
177
+ *
178
+ * `statSync`, not `lstatSync`: the question is about the directory that will be
179
+ * used, not the link pointing at it. Link modes mislead in both directions —
180
+ * Linux creates symlinks `0777`, macOS applies the umask. Nothing here decides
181
+ * whether a path is accepted, so following the link costs nothing the guards
182
+ * do not already re-check.
183
+ */
184
+ function isPeerWritable(dir) {
185
+ if (process.platform === 'win32') {
186
+ return false;
187
+ }
188
+ try {
189
+ return !!((0, node_fs_1.statSync)(dir).mode & 0o022);
190
+ }
191
+ catch {
192
+ return false;
193
+ }
194
+ }
195
+ /**
196
+ * Create a shared container as sticky + world-writable if it does not exist,
197
+ * and report whether the resulting path is safe for the current user.
198
+ *
199
+ * **A container that already exists is never modified — only judged.** Do not
200
+ * chmod before deciding trust: `CAP_FOWNER` (root's default in Docker and most
201
+ * CI images) can chmod a directory it does not own, so a peer-owned root would
202
+ * be widened to `1777` and then refused, and an operator who deliberately
203
+ * tightened this root would have it re-widened on every `nx` process.
204
+ */
205
+ function ensureSafeSharedRoot(dir) {
206
+ if (process.platform === 'win32') {
207
+ try {
208
+ (0, node_fs_1.mkdirSync)(dir, { recursive: true });
209
+ return allow(dir);
210
+ }
211
+ catch (e) {
212
+ return deny({ kind: 'not-created', dir, code: e?.code });
213
+ }
214
+ }
215
+ try {
216
+ (0, node_fs_1.mkdirSync)(dir, { mode: 0o1777 });
217
+ // Ours, created a statement ago. Load-bearing on macOS: XNU strips S_ISVTX
218
+ // at mkdir, so the sticky bit exists solely because of this call.
219
+ chmodRealDirectory(dir, 0o1777);
220
+ }
221
+ catch (e) {
222
+ if (e?.code !== 'EEXIST') {
223
+ return deny({ kind: 'not-created', dir, code: e?.code });
224
+ }
225
+ }
226
+ // One verdict for both branches: the chmod above can fail and leave a
227
+ // peer-writable non-sticky container, which trusting the creation would brand
228
+ // safe.
229
+ const verdict = isSafeSharedRoot(dir);
230
+ return verdict.status === 'ok'
231
+ ? allow(dir)
232
+ : deny(verdict.refusal);
233
+ }
234
+ /**
235
+ * Whether `dir` is an existing real directory owned by us. Unlike
236
+ * `ensureOwnedPrivateDir` it creates nothing and repairs nothing — for callers
237
+ * that only want to know whether a path is safe to act on, such as deleting.
238
+ */
239
+ function isOwnedRealDirectory(dir) {
240
+ try {
241
+ const stats = (0, node_fs_1.lstatSync)(dir);
242
+ if (!stats.isDirectory()) {
243
+ return null;
244
+ }
245
+ return typeof process.getuid !== 'function' ||
246
+ stats.uid === process.getuid()
247
+ ? dir
248
+ : null;
249
+ }
250
+ catch {
251
+ return null;
252
+ }
253
+ }
254
+ /** The path segment separating one user's Nx runtime state from another's. */
255
+ function getUserSegment() {
256
+ try {
257
+ if (typeof process.getuid === 'function') {
258
+ return String(process.getuid());
259
+ }
260
+ }
261
+ catch { }
262
+ try {
263
+ const { username } = (0, node_os_1.userInfo)();
264
+ if (username) {
265
+ return username;
266
+ }
267
+ }
268
+ catch { }
269
+ return 'unknown';
270
+ }
271
+ /**
272
+ * Ensure `dir` exists, is a real directory owned by us, and carries no group or
273
+ * other bits at all — read and search alone reach a socket inside it, so 0755 is
274
+ * re-locked rather than accepted. A `refused` status carries which check said
275
+ * no — usually `foreign-owner` for a directory another user planted, but also
276
+ * `not-created`/`not-inspectable` for a filesystem error, `not-a-directory`, or
277
+ * `not-tightenable` when the re-lock did not land.
278
+ *
279
+ * Node builtins only: reached from the native binding loader.
280
+ */
281
+ function ensureOwnedPrivateDir(dir) {
282
+ try {
283
+ (0, node_fs_1.mkdirSync)(dir, { mode: 0o700 });
284
+ }
285
+ catch (e) {
286
+ if (e?.code !== 'EEXIST') {
287
+ return deny({ kind: 'not-created', dir, code: e?.code });
288
+ }
289
+ }
290
+ // One verdict for both branches, as in `ensureSafeSharedRoot`: mounts that
291
+ // ignore the mode argument can land a directory Nx asked for at 0700 on 0777,
292
+ // so the creation path is judged like any other.
293
+ try {
294
+ const stats = (0, node_fs_1.lstatSync)(dir);
295
+ // Before the Windows short-circuit: "is a real directory" holds on every
296
+ // platform.
297
+ if (!stats.isDirectory()) {
298
+ return deny(notADirectory(dir, stats));
299
+ }
300
+ if (typeof process.getuid !== 'function') {
301
+ // Windows: the roots there are per-user OS temp dirs, not a shared /tmp.
302
+ return allow(dir);
303
+ }
304
+ if (stats.uid !== process.getuid()) {
305
+ return deny({ kind: 'foreign-owner', dir, uid: stats.uid });
306
+ }
307
+ if (stats.mode & 0o077) {
308
+ if (!chmodRealDirectory(dir, 0o700)) {
309
+ return deny({ kind: 'not-tightenable', dir, mode: stats.mode });
310
+ }
311
+ // Read the mode back rather than trusting the chmod's return: mounts that
312
+ // ignore modes report success and change nothing.
313
+ const after = (0, node_fs_1.lstatSync)(dir);
314
+ if (after.mode & 0o077) {
315
+ return deny({ kind: 'not-tightenable', dir, mode: after.mode });
316
+ }
317
+ }
318
+ return allow(dir);
319
+ }
320
+ catch (e) {
321
+ return deny({ kind: 'not-inspectable', dir, code: e?.code });
322
+ }
323
+ }
@@ -1,7 +1,7 @@
1
1
  import { NxJsonConfiguration } from '../config/nx-json';
2
2
  import { ProjectConfiguration, ProjectMetadata, TargetConfiguration } from '../config/workspace-json-project-json';
3
3
  import type { Tree } from '../generators/tree';
4
- import { PackageManagerCommands } from './package-manager';
4
+ import { PackageManager, PackageManagerCommands } from './package-manager';
5
5
  export interface NxProjectPackageJsonConfiguration extends Partial<ProjectConfiguration> {
6
6
  includedScripts?: string[];
7
7
  }
@@ -124,11 +124,11 @@ export declare function readModulePackageJson(moduleSpecifier: string, requirePa
124
124
  packageJson: PackageJson;
125
125
  path: string;
126
126
  };
127
- export declare function installPackageToTmp(pkg: string, requiredVersion: string): {
127
+ export declare function installPackageToTmp(pkg: string, requiredVersion: string, packageManager: PackageManager): {
128
128
  tempDir: string;
129
129
  cleanup: () => void;
130
130
  };
131
- export declare function installPackageToTmpAsync(pkg: string, requiredVersion: string): Promise<{
131
+ export declare function installPackageToTmpAsync(pkg: string, requiredVersion: string, packageManager: PackageManager): Promise<{
132
132
  tempDir: string;
133
133
  cleanup: () => void;
134
134
  }>;
@@ -204,22 +204,40 @@ function readModulePackageJson(moduleSpecifier, requirePaths = (0, installation_
204
204
  * Prepares all necessary information for installing a package to a temporary directory.
205
205
  * This is used by both sync and async installation functions.
206
206
  */
207
- function preparePackageInstallation(pkg, requiredVersion) {
207
+ function preparePackageInstallation(pkg, requiredVersion, packageManager) {
208
208
  const { dir: tempDir, cleanup } = (0, package_manager_1.createTempNpmDirectory)?.() ?? {
209
209
  dir: (0, tmp_1.dirSync)().name,
210
210
  cleanup: () => { },
211
211
  };
212
212
  console.log(`Fetching ${pkg}...`);
213
- const packageManager = (0, package_manager_1.detectPackageManager)();
214
213
  const isVerbose = process.env.NX_VERBOSE_LOGGING === 'true';
215
214
  generatePackageManagerFiles(tempDir, packageManager);
216
- const preInstallCommand = (0, package_manager_1.getPackageManagerCommand)(packageManager).preInstall;
215
+ // For pnpm, `addDev` is `pnpm add -Dw` when the workspace has a
216
+ // pnpm-workspace.yaml. `createTempNpmDirectory` copies a sanitized copy of
217
+ // it into the temp dir, so the `-w` here resolves to the temp dir.
217
218
  const pmCommands = (0, package_manager_1.getPackageManagerCommand)(packageManager);
218
- let addCommand = pmCommands.addDev;
219
- if (packageManager === 'pnpm') {
220
- addCommand = 'pnpm add -D'; // we need to ensure that we are not using workspace command
221
- }
222
- const installCommand = `${addCommand} ${pkg}@${requiredVersion} ${pmCommands.ignoreScriptsFlag ?? ''}`;
219
+ const preInstallCommand = pmCommands.preInstall;
220
+ // Keep peer dependencies out of the temp install. `ensurePackage` puts the
221
+ // workspace's `node_modules` on `NODE_PATH`, so a loaded package resolves its
222
+ // peers from the workspace instead of pulling its own (possibly incompatible)
223
+ // copies into the temp dir.
224
+ //
225
+ // npm needs `--legacy-peer-deps` rather than `--omit=peer`: npm marks a package
226
+ // as a peer if anything in the tree peer-depends on it, so `--omit=peer` also
227
+ // prunes packages that are real dependencies. Bun's `--omit=peer` does not.
228
+ const skipPeerDependenciesFlags = {
229
+ npm: '--legacy-peer-deps',
230
+ bun: '--omit=peer',
231
+ pnpm: '--config.auto-install-peers=false',
232
+ };
233
+ const installCommand = [
234
+ pmCommands.addDev,
235
+ `${pkg}@${requiredVersion}`,
236
+ skipPeerDependenciesFlags[packageManager],
237
+ pmCommands.ignoreScriptsFlag,
238
+ ]
239
+ .filter(Boolean)
240
+ .join(' ');
223
241
  const execOptions = {
224
242
  cwd: tempDir,
225
243
  stdio: isVerbose ? 'inherit' : 'ignore',
@@ -239,8 +257,8 @@ function preparePackageInstallation(pkg, requiredVersion) {
239
257
  execOptions,
240
258
  };
241
259
  }
242
- function installPackageToTmp(pkg, requiredVersion) {
243
- const { tempDir, cleanup, preInstallCommand, installCommand, execOptions } = preparePackageInstallation(pkg, requiredVersion);
260
+ function installPackageToTmp(pkg, requiredVersion, packageManager) {
261
+ const { tempDir, cleanup, preInstallCommand, installCommand, execOptions } = preparePackageInstallation(pkg, requiredVersion, packageManager);
244
262
  if (preInstallCommand) {
245
263
  // ensure package.json and repo in tmp folder is set to a proper package manager state
246
264
  (0, child_process_1.execSync)(preInstallCommand, execOptions);
@@ -251,8 +269,8 @@ function installPackageToTmp(pkg, requiredVersion) {
251
269
  cleanup,
252
270
  };
253
271
  }
254
- async function installPackageToTmpAsync(pkg, requiredVersion) {
255
- const { tempDir, cleanup, preInstallCommand, installCommand, execOptions } = preparePackageInstallation(pkg, requiredVersion);
272
+ async function installPackageToTmpAsync(pkg, requiredVersion, packageManager) {
273
+ const { tempDir, cleanup, preInstallCommand, installCommand, execOptions } = preparePackageInstallation(pkg, requiredVersion, packageManager);
256
274
  try {
257
275
  if (preInstallCommand) {
258
276
  // ensure package.json and repo in tmp folder is set to a proper package manager state
@@ -75,6 +75,19 @@ export declare function modifyYarnRcYmlToFitNewDirectory(contents: string): stri
75
75
  * @returns Updated string contents of the yarnrc.yml file
76
76
  */
77
77
  export declare function modifyYarnRcToFitNewDirectory(contents: string): string;
78
+ /**
79
+ * We copy pnpm-workspace.yaml to the temporary directory so the workspace's
80
+ * registry, auth and release-age settings still apply, and so `pnpm add -w`
81
+ * recognizes the directory as a workspace root. `packages` (member globs) and
82
+ * `patchedDependencies` (relative patch paths) only resolve in the real
83
+ * workspace, so they are dropped.
84
+ *
85
+ * Exported for testing - not meant to be used outside of this file.
86
+ *
87
+ * @param contents The string contents of the pnpm-workspace.yaml file
88
+ * @returns Updated string contents of the pnpm-workspace.yaml file
89
+ */
90
+ export declare function modifyPnpmWorkspaceYamlToFitNewDirectory(contents: string): string;
78
91
  export declare function copyPackageManagerConfigurationFiles(root: string, destination: string): void;
79
92
  /**
80
93
  * Creates a temporary directory where you can run package manager commands safely.
@@ -8,6 +8,7 @@ exports.parseVersionFromPackageManagerField = parseVersionFromPackageManagerFiel
8
8
  exports.findFileInPackageJsonDirectory = findFileInPackageJsonDirectory;
9
9
  exports.modifyYarnRcYmlToFitNewDirectory = modifyYarnRcYmlToFitNewDirectory;
10
10
  exports.modifyYarnRcToFitNewDirectory = modifyYarnRcToFitNewDirectory;
11
+ exports.modifyPnpmWorkspaceYamlToFitNewDirectory = modifyPnpmWorkspaceYamlToFitNewDirectory;
11
12
  exports.copyPackageManagerConfigurationFiles = copyPackageManagerConfigurationFiles;
12
13
  exports.createTempNpmDirectory = createTempNpmDirectory;
13
14
  exports.resolvePackageVersionUsingRegistry = resolvePackageVersionUsingRegistry;
@@ -322,12 +323,33 @@ function modifyYarnRcToFitNewDirectory(contents) {
322
323
  }
323
324
  return lines.join('\n');
324
325
  }
326
+ /**
327
+ * We copy pnpm-workspace.yaml to the temporary directory so the workspace's
328
+ * registry, auth and release-age settings still apply, and so `pnpm add -w`
329
+ * recognizes the directory as a workspace root. `packages` (member globs) and
330
+ * `patchedDependencies` (relative patch paths) only resolve in the real
331
+ * workspace, so they are dropped.
332
+ *
333
+ * Exported for testing - not meant to be used outside of this file.
334
+ *
335
+ * @param contents The string contents of the pnpm-workspace.yaml file
336
+ * @returns Updated string contents of the pnpm-workspace.yaml file
337
+ */
338
+ function modifyPnpmWorkspaceYamlToFitNewDirectory(contents) {
339
+ const doc = (0, yaml_1.parseDocument)(contents);
340
+ if (doc.contents) {
341
+ doc.delete('packages');
342
+ doc.delete('patchedDependencies');
343
+ }
344
+ return doc.toString();
345
+ }
325
346
  function copyPackageManagerConfigurationFiles(root, destination) {
326
347
  for (const packageManagerConfigFile of [
327
348
  '.npmrc',
328
349
  '.yarnrc',
329
350
  '.yarnrc.yml',
330
351
  'bunfig.toml',
352
+ 'pnpm-workspace.yaml',
331
353
  ]) {
332
354
  // f is an absolute path, including the {workspaceRoot}.
333
355
  const f = findFileInPackageJsonDirectory(packageManagerConfigFile, root);
@@ -355,6 +377,11 @@ function copyPackageManagerConfigurationFiles(root, destination) {
355
377
  (0, fs_1.copyFileSync)(f, destinationPath);
356
378
  break;
357
379
  }
380
+ case 'pnpm-workspace.yaml': {
381
+ const updated = modifyPnpmWorkspaceYamlToFitNewDirectory((0, fileutils_1.readFileIfExisting)(f));
382
+ (0, fs_1.writeFileSync)(destinationPath, updated);
383
+ break;
384
+ }
358
385
  }
359
386
  }
360
387
  }
@@ -0,0 +1,24 @@
1
+ import { ChildProcess, ExecFileSyncOptions, SpawnOptions } from 'child_process';
2
+ /**
3
+ * Spawn a binary that may be a Windows `.cmd`/`.bat` shim, without letting its
4
+ * arguments become shell syntax.
5
+ *
6
+ * Off Windows, and for a `.exe`, no shell is involved at all. Where one is —
7
+ * a shim or a bare name — the binary and every argument are quoted for cmd.exe.
8
+ *
9
+ * Not a complete guarantee: cmd.exe expands `%VAR%` in the binary or any
10
+ * argument whether or not it is quoted, so a caller with untrusted arguments
11
+ * inherits that gap (NXC-4798). See the note on `LINE_BREAK` above.
12
+ *
13
+ * @throws on Windows when the binary or an argument contains a line break, or a
14
+ * literal `"` (via `quoteShellArg`). Those are the only refusals — the `%`
15
+ * expansion above is not one of them.
16
+ */
17
+ export declare function safeSpawn(binary: string, args: readonly string[], options?: Omit<SpawnOptions, 'shell'>): ChildProcess;
18
+ /**
19
+ * Synchronous {@link safeSpawn}, returning the child's stdout.
20
+ *
21
+ * Carries the same Windows caveats: `%VAR%` is expanded quoted or not
22
+ * (NXC-4798), and a line break or literal `"` throws.
23
+ */
24
+ export declare function safeExecFileSync(binary: string, args: readonly string[], options?: Omit<ExecFileSyncOptions, 'encoding' | 'shell'>): string;
@@ -0,0 +1,104 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.safeSpawn = safeSpawn;
4
+ exports.safeExecFileSync = safeExecFileSync;
5
+ const child_process_1 = require("child_process");
6
+ const path_1 = require("path");
7
+ const shell_quoting_1 = require("./shell-quoting");
8
+ // Node refuses to launch a Windows `.cmd`/`.bat` shim without a shell
9
+ // (CVE-2024-27980). A bare name takes the shell for the same reason, one step
10
+ // removed: libuv walks PATH and PATHEXT itself, so no shell is needed to *find*
11
+ // the binary — but the walk may land on a `.cmd`/`.bat`, and this cannot know
12
+ // which in advance, so it is deliberately conservative. Everything else,
13
+ // including a `.exe`, spawns directly, so the shell and the quoting below reach
14
+ // only the invocations that cannot do without them.
15
+ //
16
+ // Deliberately a function: a module-level const is fixed at import time, which
17
+ // would make the Windows branch untestable.
18
+ function needsShell(binary) {
19
+ if (process.platform !== 'win32') {
20
+ return false;
21
+ }
22
+ // win32.extname, not the platform default: this branch only runs on Windows,
23
+ // where `path` is win32 anyway, and pinning it lets a test model the real
24
+ // parse rather than posix's (which reads `C:\ws\my.app\gradlew` as `.app\gradlew`).
25
+ const ext = path_1.win32.extname(binary).toLowerCase();
26
+ return ext === '' || ext === '.cmd' || ext === '.bat';
27
+ }
28
+ // A line break ends the command line whatever the quoting, so nothing can carry
29
+ // one.
30
+ //
31
+ // `%` is deliberately NOT refused, and this is a known gap rather than a safe
32
+ // case. cmd.exe expands `%VAR%` whether or not the value is quoted — see
33
+ // `quoteShellArg`'s own docstring in ./shell-quoting, which says so and pins it
34
+ // with a test — so quoting contains nothing here. It applies to the binary path
35
+ // as much as to the arguments, and an expansion yielding a `"` defeats the
36
+ // literal-quote refusal below, because that inspects the pre-expansion bytes.
37
+ // `bin/nx.ts` loads the workspace's own `.env` before the graph is built, so a
38
+ // repo can set the variable too.
39
+ //
40
+ // Refusing `%` is what an earlier revision did, and it made any workspace under
41
+ // a `%` path — a legal Windows directory name — fail graph construction. The gap
42
+ // is bounded by the argument that bounds this whole class: a repo that can reach
43
+ // here already has `mvnw` / `pom.xml` executing on its behalf. Closing it
44
+ // properly is tracked as NXC-4798.
45
+ const LINE_BREAK = /[\r\n]/;
46
+ /**
47
+ * Spawn a binary that may be a Windows `.cmd`/`.bat` shim, without letting its
48
+ * arguments become shell syntax.
49
+ *
50
+ * Off Windows, and for a `.exe`, no shell is involved at all. Where one is —
51
+ * a shim or a bare name — the binary and every argument are quoted for cmd.exe.
52
+ *
53
+ * Not a complete guarantee: cmd.exe expands `%VAR%` in the binary or any
54
+ * argument whether or not it is quoted, so a caller with untrusted arguments
55
+ * inherits that gap (NXC-4798). See the note on `LINE_BREAK` above.
56
+ *
57
+ * @throws on Windows when the binary or an argument contains a line break, or a
58
+ * literal `"` (via `quoteShellArg`). Those are the only refusals — the `%`
59
+ * expansion above is not one of them.
60
+ */
61
+ function safeSpawn(binary, args, options = {}) {
62
+ const shell = needsShell(binary);
63
+ return (0, child_process_1.spawn)(quoteBinary(binary, shell), quoteArgs(args, shell), {
64
+ ...options,
65
+ windowsHide: true,
66
+ shell,
67
+ });
68
+ }
69
+ /**
70
+ * Synchronous {@link safeSpawn}, returning the child's stdout.
71
+ *
72
+ * Carries the same Windows caveats: `%VAR%` is expanded quoted or not
73
+ * (NXC-4798), and a line break or literal `"` throws.
74
+ */
75
+ function safeExecFileSync(binary, args, options = {}) {
76
+ const shell = needsShell(binary);
77
+ return (0, child_process_1.execFileSync)(quoteBinary(binary, shell), quoteArgs(args, shell), {
78
+ stdio: 'pipe',
79
+ ...options,
80
+ windowsHide: true,
81
+ shell,
82
+ encoding: 'utf-8',
83
+ });
84
+ }
85
+ // The binary needs quoting too: Node joins it into the same command line, so a
86
+ // path holding a space or an `&` would split there.
87
+ function quoteBinary(binary, shell) {
88
+ if (!shell) {
89
+ return binary;
90
+ }
91
+ return quoteForCmd(binary, `the path ${JSON.stringify(binary)}`);
92
+ }
93
+ function quoteForCmd(value, described, remedy = '') {
94
+ if (LINE_BREAK.test(value)) {
95
+ throw new Error(`Cannot pass ${described} to cmd.exe: a line break inside it would end the command line before cmd.exe reached the rest of it.${remedy}`);
96
+ }
97
+ return (0, shell_quoting_1.quoteShellArg)(value);
98
+ }
99
+ function quoteArgs(args, shell) {
100
+ if (!shell) {
101
+ return [...args];
102
+ }
103
+ return args.map((arg) => quoteForCmd(arg, `the argument ${JSON.stringify(arg)}`, ' Remove it from your Nx configuration and try again.'));
104
+ }
@@ -8,3 +8,18 @@ export declare function needsShellQuoting(str: string): boolean;
8
8
  * Check if a string is already wrapped in matching quotes (single or double).
9
9
  */
10
10
  export declare function isAlreadyQuoted(str: string): boolean;
11
+ /**
12
+ * Quote a string so it survives being interpolated into a shell command line
13
+ * as a single argument.
14
+ *
15
+ * On Windows the safety boundary is one unbroken double-quoted run: it keeps
16
+ * `^`, `&`, `|`, `<` and `>` literal through cmd.exe's parse and a `.cmd`
17
+ * shim's re-parse of `%*`, but not `%`, which cmd.exe expands inside double
18
+ * quotes too.
19
+ *
20
+ * @throws on Windows when the argument contains a double quote, which ends that
21
+ * run, since cmd.exe recognizes no backslash escape. Carrying one means
22
+ * caret-escaping every metacharacter instead, doubled for a `.cmd` shim, which
23
+ * this path does not implement.
24
+ */
25
+ export declare function quoteShellArg(arg: string): string;