@nx/dotnet 23.2.0-beta.11 → 23.2.0-beta.12

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.
@@ -1,4 +1,10 @@
1
1
  import { ProjectConfiguration } from '@nx/devkit';
2
+ export declare const ANALYZER_CANCELLED_MESSAGE = "Analyzer run was cancelled";
3
+ /**
4
+ * Cancel any in-flight analyzer process. Safe to call even if nothing is running.
5
+ */
6
+ export declare function cancelPendingAnalysis(): void;
7
+ export declare function getAnalysisTimeoutMs(): number;
2
8
  export interface AnalysisSuccessResult {
3
9
  nodesByFile: Record<string, ProjectConfiguration>;
4
10
  referencesByRoot: Record<string, {
@@ -1 +1 @@
1
- {"version":3,"file":"analyzer-client.d.ts","sourceRoot":"","sources":["../../src/analyzer/analyzer-client.ts"],"names":[],"mappings":"AAIA,OAAO,EAAyB,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAQzE,MAAM,WAAW,qBAAqB;IAEpC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAC;IAElD,gBAAgB,EAAE,MAAM,CACtB,MAAM,EACN;QAAE,IAAI,EAAE,MAAM,EAAE,CAAC;QAAC,gBAAgB,EAAE,MAAM,CAAA;KAAE,CAC7C,CAAC;CACH;AACD,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,KAAK,CAAC;CACd;AACD,MAAM,MAAM,cAAc,GAAG,qBAAqB,GAAG,mBAAmB,CAAC;AAEzE;;;GAGG;AACH,MAAM,WAAW,qBAAqB;IACpC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AA4ID;;;;GAIG;AACH,wBAAsB,eAAe,CACnC,KAAK,EAAE,MAAM,EAAE,EACf,OAAO,CAAC,EAAE,qBAAqB,GAC9B,OAAO,CAAC,cAAc,CAAC,CA2DzB;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,IAAI,cAAc,CASzD;AAED;;GAEG;AACH,wBAAgB,UAAU,IAAI,IAAI,CAEjC;AAED,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,cAAc,GACrB,MAAM,IAAI,mBAAmB,CAE/B;AAED,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,cAAc,GACrB,MAAM,IAAI,qBAAqB,CAEjC"}
1
+ {"version":3,"file":"analyzer-client.d.ts","sourceRoot":"","sources":["../../src/analyzer/analyzer-client.ts"],"names":[],"mappings":"AAEA,OAAO,EAIL,oBAAoB,EACrB,MAAM,YAAY,CAAC;AAmBpB,eAAO,MAAM,0BAA0B,+BAA+B,CAAC;AAIvE;;GAEG;AACH,wBAAgB,qBAAqB,IAAI,IAAI,CAK5C;AAED,wBAAgB,oBAAoB,IAAI,MAAM,CAS7C;AAED,MAAM,WAAW,qBAAqB;IAEpC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAC;IAElD,gBAAgB,EAAE,MAAM,CACtB,MAAM,EACN;QAAE,IAAI,EAAE,MAAM,EAAE,CAAC;QAAC,gBAAgB,EAAE,MAAM,CAAA;KAAE,CAC7C,CAAC;CACH;AACD,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,KAAK,CAAC;CACd;AACD,MAAM,MAAM,cAAc,GAAG,qBAAqB,GAAG,mBAAmB,CAAC;AAEzE;;;GAGG;AACH,MAAM,WAAW,qBAAqB;IACpC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AA4MD;;;;GAIG;AACH,wBAAsB,eAAe,CACnC,KAAK,EAAE,MAAM,EAAE,EACf,OAAO,CAAC,EAAE,qBAAqB,GAC9B,OAAO,CAAC,cAAc,CAAC,CA+DzB;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,IAAI,cAAc,CASzD;AAED;;GAEG;AACH,wBAAgB,UAAU,IAAI,IAAI,CAEjC;AAED,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,cAAc,GACrB,MAAM,IAAI,mBAAmB,CAE/B;AAED,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,cAAc,GACrB,MAAM,IAAI,qBAAqB,CAEjC"}
@@ -1,15 +1,44 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ANALYZER_CANCELLED_MESSAGE = void 0;
4
+ exports.cancelPendingAnalysis = cancelPendingAnalysis;
5
+ exports.getAnalysisTimeoutMs = getAnalysisTimeoutMs;
3
6
  exports.analyzeProjects = analyzeProjects;
4
7
  exports.readCachedAnalysisResult = readCachedAnalysisResult;
5
8
  exports.clearCache = clearCache;
6
9
  exports.isAnalysisErrorResult = isAnalysisErrorResult;
7
10
  exports.isAnalysisSuccessResult = isAnalysisSuccessResult;
8
- const node_child_process_1 = require("node:child_process");
9
11
  const node_fs_1 = require("node:fs");
10
12
  const node_path_1 = require("node:path");
11
13
  const devkit_1 = require("@nx/devkit");
12
14
  const internal_1 = require("@nx/devkit/internal");
15
+ const DEFAULT_ANALYSIS_TIMEOUT_SECONDS = (0, internal_1.isCI)() ? 600 : 120;
16
+ // setTimeout silently clamps a delay past the 32-bit signed max to 1ms, which
17
+ // would abort immediately — the opposite of what a large value asks for.
18
+ const MAX_TIMEOUT_MS = 2 ** 31 - 1;
19
+ // Thrown when a newer analysis supersedes an in-flight one. Callers swallow it
20
+ // rather than surfacing an internal string to the user.
21
+ exports.ANALYZER_CANCELLED_MESSAGE = 'Analyzer run was cancelled';
22
+ let currentAbortController;
23
+ /**
24
+ * Cancel any in-flight analyzer process. Safe to call even if nothing is running.
25
+ */
26
+ function cancelPendingAnalysis() {
27
+ if (currentAbortController) {
28
+ currentAbortController.abort('cancelled');
29
+ currentAbortController = undefined;
30
+ }
31
+ }
32
+ function getAnalysisTimeoutMs() {
33
+ const envTimeout = process.env.NX_DOTNET_PROJECT_GRAPH_TIMEOUT;
34
+ if (envTimeout) {
35
+ const parsed = Number(envTimeout);
36
+ if (!Number.isNaN(parsed) && parsed > 0) {
37
+ return Math.min(parsed * 1000, MAX_TIMEOUT_MS);
38
+ }
39
+ }
40
+ return DEFAULT_ANALYSIS_TIMEOUT_SECONDS * 1000;
41
+ }
13
42
  let cache = null;
14
43
  /**
15
44
  * Get the path to the msbuild-analyzer executable
@@ -41,7 +70,7 @@ async function calculateProjectFilesHash(files) {
41
70
  * Run the msbuild-analyzer and return the results.
42
71
  * Uses stdin for large file lists to avoid ARG_MAX issues.
43
72
  */
44
- function runAnalyzer(files, options) {
73
+ async function runAnalyzer(files, options) {
45
74
  if (files.length === 0) {
46
75
  return { nodesByFile: {}, referencesByRoot: {} };
47
76
  }
@@ -72,44 +101,106 @@ function runAnalyzer(files, options) {
72
101
  // }
73
102
  // }
74
103
  // }
104
+ // Nothing here may contain a double quote: on Windows `safeSpawn` routes a bare binary
105
+ // name through cmd.exe, whose quoting cannot express one, so it refuses the argument.
106
+ // That is why the options JSON goes over stdin instead (see below).
107
+ const args = [analyzerPath, devkit_1.workspaceRoot];
108
+ // Cancel any in-flight analyzer from a previous call, then create a fresh controller.
109
+ cancelPendingAnalysis();
110
+ const controller = new AbortController();
111
+ currentAbortController = controller;
112
+ const signal = controller.signal;
113
+ const timeoutMs = getAnalysisTimeoutMs();
114
+ const timeoutSeconds = timeoutMs / 1000;
115
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
116
+ let stdout = '';
117
+ let stderr = '';
75
118
  try {
76
- let output;
77
- // Prepare CLI arguments
78
- const args = [analyzerPath, devkit_1.workspaceRoot];
79
- // Add plugin options as JSON string if provided
80
- if (options) {
81
- args.push(JSON.stringify(options));
82
- }
83
- // Use stdin mode for large file lists to avoid ARG_MAX issues. The analyzer
84
- // partitions paths by filename, so we just stream everything in one block.
85
- const input = files.join('\n');
86
- const result = (0, node_child_process_1.spawnSync)('dotnet', args, {
87
- input,
88
- encoding: 'utf-8',
89
- maxBuffer: 10 * 1024 * 1024, // 10MB buffer
90
- stdio: ['pipe', 'pipe', 'pipe'], // stdin, stdout, stderr
91
- windowsHide: true,
92
- env,
119
+ await new Promise((resolve, reject) => {
120
+ const child = (0, internal_1.safeSpawn)('dotnet', args, {
121
+ stdio: ['pipe', 'pipe', 'pipe'],
122
+ windowsHide: true,
123
+ env,
124
+ });
125
+ // A plugin worker torn down by `nx reset` would otherwise orphan the analyzer.
126
+ (0, internal_1.killChildOnHostExit)(child);
127
+ // On abort, kill the process tree and settle immediately a wedged
128
+ // process that outlives the kill signal would otherwise keep this
129
+ // promise pending and the abort error would never surface.
130
+ const onAbort = () => {
131
+ if (child.pid) {
132
+ (0, internal_1.killProcessTreeGraceful)(child.pid).catch(() => { });
133
+ }
134
+ reject(new Error('Analyzer aborted'));
135
+ };
136
+ signal.addEventListener('abort', onAbort, { once: true });
137
+ child.stdout?.setEncoding('utf-8');
138
+ child.stderr?.setEncoding('utf-8');
139
+ child.stdout?.on('data', (data) => {
140
+ stdout += data;
141
+ });
142
+ child.stderr?.on('data', (data) => {
143
+ stderr += data;
144
+ });
145
+ child.on('error', (error) => {
146
+ signal.removeEventListener('abort', onAbort);
147
+ reject(error);
148
+ });
149
+ child.on('close', (code) => {
150
+ signal.removeEventListener('abort', onAbort);
151
+ if (code === 0) {
152
+ resolve();
153
+ }
154
+ else {
155
+ reject(new Error(`Analyzer exited with code ${code}: ${stderr}`));
156
+ }
157
+ });
158
+ // Stream the options JSON, then the file list, over stdin. The first line is
159
+ // always written, empty when there are no options, so the analyzer never has to
160
+ // infer whether one was sent. The analyzer partitions paths by filename, so we
161
+ // just send everything in one block.
162
+ // EPIPE if the analyzer dies early. Surfaced via `close` only when the
163
+ // child also exits non-zero; a stdin error with a clean exit means the
164
+ // analyzer read a truncated file list.
165
+ child.stdin?.on('error', () => { });
166
+ child.stdin?.end([options ? JSON.stringify(options) : '', ...files].join('\n'));
93
167
  });
94
- if (result.error) {
95
- throw result.error;
168
+ }
169
+ catch (error) {
170
+ if (signal.reason === 'cancelled') {
171
+ throw new Error(exports.ANALYZER_CANCELLED_MESSAGE);
172
+ }
173
+ if (signal.aborted) {
174
+ throw new Error(`msbuild-analyzer timed out after ${timeoutSeconds} ${timeoutSeconds === 1 ? 'second' : 'seconds'}.\n` +
175
+ ` 1. Set the environment variable NX_DOTNET_PROJECT_GRAPH_TIMEOUT to a higher value (in seconds) to increase the timeout.\n` +
176
+ ` 2. If the issue persists, set NX_DOTNET_DISABLE=true to disable the .NET plugin entirely.`);
96
177
  }
97
- if (result.status !== 0) {
98
- throw new Error(`Analyzer exited with code ${result.status}: ${result.stderr}`);
178
+ const err = error;
179
+ const stderrAlreadyInMessage = err.message.includes(stderr);
180
+ if (stderr && !stderrAlreadyInMessage) {
181
+ devkit_1.logger.error(`msbuild-analyzer error: ${stderr}`);
99
182
  }
100
- // Output stderr (includes performance logs when NX_PERF_LOGGING=true)
101
- if (result.stderr) {
102
- console.error(result.stderr);
183
+ throw new Error(`Failed to run msbuild-analyzer: ${err.message}${stderr && !stderrAlreadyInMessage ? `\n${stderr}` : ''}`);
184
+ }
185
+ finally {
186
+ clearTimeout(timer);
187
+ // Drop the settled controller so a later cancelPendingAnalysis() cannot
188
+ // abort a run that already finished.
189
+ if (currentAbortController === controller) {
190
+ currentAbortController = undefined;
103
191
  }
104
- output = result.stdout;
105
- return JSON.parse(output);
192
+ }
193
+ // Output stderr (includes performance logs when NX_PERF_LOGGING=true)
194
+ if (stderr) {
195
+ console.error(stderr);
196
+ }
197
+ try {
198
+ // parseJson over JSON.parse: on malformed output it reports line:column with
199
+ // a code frame, where JSON.parse gives only a character offset.
200
+ return (0, devkit_1.parseJson)(stdout);
106
201
  }
107
202
  catch (error) {
108
- const err = error;
109
- if (err.stderr) {
110
- devkit_1.logger.error(`msbuild-analyzer error: ${err.stderr}`);
111
- }
112
- throw new Error(`Failed to run msbuild-analyzer: ${err.message}${err.stderr ? `\n${err.stderr}` : ''}`);
203
+ throw new Error(`Failed to parse msbuild-analyzer output: ${error.message}`);
113
204
  }
114
205
  }
115
206
  /**
@@ -141,7 +232,7 @@ async function analyzeProjects(files, options) {
141
232
  }
142
233
  // Run the analyzer
143
234
  try {
144
- const result = runAnalyzer(files, options);
235
+ const result = await runAnalyzer(files, options);
145
236
  // Update local cache
146
237
  cache = {
147
238
  hash: filesHash,
@@ -162,10 +253,14 @@ async function analyzeProjects(files, options) {
162
253
  const errorResult = {
163
254
  error: err,
164
255
  };
165
- cache = {
166
- hash: filesHash,
167
- result: errorResult,
168
- };
256
+ // A superseded run is not a result: caching it would hand the sentinel to
257
+ // createDependencies via readCachedAnalysisResult().
258
+ if (err.message !== exports.ANALYZER_CANCELLED_MESSAGE) {
259
+ cache = {
260
+ hash: filesHash,
261
+ result: errorResult,
262
+ };
263
+ }
169
264
  return errorResult;
170
265
  }
171
266
  }
Binary file
Binary file
@@ -1 +1 @@
1
- {"version":3,"file":"create-nodes.d.ts","sourceRoot":"","sources":["../../src/plugins/create-nodes.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,WAAW,EAGX,mBAAmB,EACpB,MAAM,YAAY,CAAC;AAOpB,MAAM,MAAM,2BAA2B,GAAG,OAAO,CAAC,mBAAmB,CAAC,GAAG;IACvE;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,KAAK,CAAC,EAAE,2BAA2B,GAAG,KAAK,CAAC;IAC5C;;;OAGG;IACH,IAAI,CAAC,EAAE,2BAA2B,GAAG,KAAK,CAAC;IAC3C;;;OAGG;IACH,KAAK,CAAC,EAAE,2BAA2B,GAAG,KAAK,CAAC;IAC5C;;;OAGG;IACH,OAAO,CAAC,EAAE,2BAA2B,GAAG,KAAK,CAAC;IAC9C;;;OAGG;IACH,OAAO,CAAC,EAAE,2BAA2B,GAAG,KAAK,CAAC;IAC9C;;;OAGG;IACH,IAAI,CAAC,EAAE,2BAA2B,GAAG,KAAK,CAAC;IAC3C;;;OAGG;IACH,KAAK,CAAC,EAAE,2BAA2B,GAAG,KAAK,CAAC;IAC5C;;;OAGG;IACH,GAAG,CAAC,EAAE,2BAA2B,GAAG,KAAK,CAAC;CAC3C;AAqFD,eAAO,MAAM,WAAW,EAAE,WAAW,CAAC,mBAAmB,CA6ExD,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,aAAa,kCAAc,CAAC"}
1
+ {"version":3,"file":"create-nodes.d.ts","sourceRoot":"","sources":["../../src/plugins/create-nodes.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,WAAW,EAGX,mBAAmB,EACpB,MAAM,YAAY,CAAC;AAQpB,MAAM,MAAM,2BAA2B,GAAG,OAAO,CAAC,mBAAmB,CAAC,GAAG;IACvE;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,KAAK,CAAC,EAAE,2BAA2B,GAAG,KAAK,CAAC;IAC5C;;;OAGG;IACH,IAAI,CAAC,EAAE,2BAA2B,GAAG,KAAK,CAAC;IAC3C;;;OAGG;IACH,KAAK,CAAC,EAAE,2BAA2B,GAAG,KAAK,CAAC;IAC5C;;;OAGG;IACH,OAAO,CAAC,EAAE,2BAA2B,GAAG,KAAK,CAAC;IAC9C;;;OAGG;IACH,OAAO,CAAC,EAAE,2BAA2B,GAAG,KAAK,CAAC;IAC9C;;;OAGG;IACH,IAAI,CAAC,EAAE,2BAA2B,GAAG,KAAK,CAAC;IAC3C;;;OAGG;IACH,KAAK,CAAC,EAAE,2BAA2B,GAAG,KAAK,CAAC;IAC5C;;;OAGG;IACH,GAAG,CAAC,EAAE,2BAA2B,GAAG,KAAK,CAAC;CAC3C;AAqFD,eAAO,MAAM,WAAW,EAAE,WAAW,CAAC,mBAAmB,CAqFxD,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,aAAa,kCAAc,CAAC"}
@@ -92,6 +92,14 @@ exports.createNodes = [
92
92
  };
93
93
  const result = await (0, analyzer_client_1.analyzeProjects)([...configFilePaths], analyzerOptions);
94
94
  if ((0, analyzer_client_1.isAnalysisErrorResult)(result)) {
95
+ if (result.error.message === analyzer_client_1.ANALYZER_CANCELLED_MESSAGE) {
96
+ // Superseded by a newer analysis — silently return empty rather than
97
+ // failing the user's command with an internal sentinel.
98
+ // Safe only because the daemon drops this compute at its
99
+ // `stalePostCreateNodes` guard before createDependencies runs; that one
100
+ // would throw on the cache this run deliberately did not write.
101
+ return [];
102
+ }
95
103
  throw result.error;
96
104
  }
97
105
  const { nodesByFile } = result;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nx/dotnet",
3
- "version": "23.2.0-beta.11",
3
+ "version": "23.2.0-beta.12",
4
4
  "private": false,
5
5
  "description": "The Nx Plugin for .NET containing graph support for working with .NET projects in an Nx workspace.",
6
6
  "keywords": [
@@ -49,7 +49,7 @@
49
49
  "dependencies": {
50
50
  "tslib": "^2.3.0",
51
51
  "ignore": "^7.0.5",
52
- "@nx/devkit": "23.2.0-beta.11"
52
+ "@nx/devkit": "23.2.0-beta.12"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@types/jest": "30.0.0",
@@ -58,7 +58,7 @@
58
58
  "memfs": "^4.9.2",
59
59
  "ts-jest": "^29.4.7",
60
60
  "typescript": "~6.0.3",
61
- "nx": "23.2.0-beta.11"
61
+ "nx": "23.2.0-beta.12"
62
62
  },
63
63
  "publishConfig": {
64
64
  "access": "public"