@nx/js 23.1.0-rc.0 → 23.1.0-rc.2

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.
@@ -11,7 +11,7 @@ const crypto_1 = require("crypto");
11
11
  const path = tslib_1.__importStar(require("path"));
12
12
  const path_1 = require("path");
13
13
  const buildable_libs_utils_1 = require("../../utils/buildable-libs-utils");
14
- const kill_tree_1 = require("./lib/kill-tree");
14
+ const native_1 = require("nx/src/native");
15
15
  const line_aware_writer_1 = require("./lib/line-aware-writer");
16
16
  const coalescing_debounce_1 = require("./lib/coalescing-debounce");
17
17
  const fileutils_1 = require("nx/src/utils/fileutils");
@@ -67,23 +67,26 @@ async function* nodeExecutor(options, context) {
67
67
  const previousTask = currentTask;
68
68
  const task = tasks.shift();
69
69
  if (previousTask && !previousTask.killed) {
70
- previousTask.killed = true;
71
- if (previousTask.childProcess?.connected) {
72
- previousTask.childProcess.disconnect();
73
- }
74
- previousTask.childProcess?.removeAllListeners();
70
+ // stop() marks the task killed, detaches listeners, and waits for the
71
+ // process tree to exit.
75
72
  await previousTask.stop('SIGTERM');
76
73
  await new Promise((resolve) => setImmediate(resolve));
77
74
  }
78
75
  currentTask = task;
79
76
  globalLineAwareWriter.setActiveProcess(task.id);
80
77
  await task.start();
78
+ // A file change may have queued another task while we waited for the
79
+ // previous process to stop. Its debounce trigger piggybacked on this
80
+ // in-flight run, so drain the queue now or it would sit unprocessed
81
+ // until the next change.
82
+ if (tasks.length > 0) {
83
+ await processQueue();
84
+ }
81
85
  };
82
86
  const debouncedProcessQueue = (0, coalescing_debounce_1.createCoalescingDebounce)(processQueue, options.debounce ?? 1_000);
83
87
  const addToQueue = async (childProcess, buildResult) => {
84
88
  for (const task of tasks) {
85
89
  if (!task.killed) {
86
- task.killed = true;
87
90
  await task.stop('SIGTERM');
88
91
  }
89
92
  }
@@ -156,7 +159,7 @@ async function* nodeExecutor(options, context) {
156
159
  },
157
160
  stop: async (signal = 'SIGTERM') => {
158
161
  task.killed = true;
159
- if (task.childProcess) {
162
+ if (task.childProcess?.pid) {
160
163
  if (task.childProcess.stdout) {
161
164
  task.childProcess.stdout.pause();
162
165
  }
@@ -167,7 +170,12 @@ async function* nodeExecutor(options, context) {
167
170
  task.childProcess.disconnect();
168
171
  }
169
172
  task.childProcess.removeAllListeners();
170
- await (0, kill_tree_1.killTree)(task.childProcess.pid, signal);
173
+ // Wait for the process tree to fully exit so the port is released
174
+ // before a watch-mode restart boots the next server (EADDRINUSE).
175
+ // Windows cannot deliver graceful signals through this API, so
176
+ // skip the grace period and force-kill immediately (matching the
177
+ // previous taskkill /F behavior).
178
+ await (0, native_1.killProcessTreeGraceful)(task.childProcess.pid, signal, process.platform === 'win32' ? 0 : undefined);
171
179
  }
172
180
  if (task.id === globalLineAwareWriter.currentProcessId) {
173
181
  globalLineAwareWriter.setActiveProcess(null);
@@ -12,15 +12,25 @@ import { type Tree } from '@nx/devkit';
12
12
  * compilation and emit layout are unchanged under 6.0. The value is computed by
13
13
  * the compiler itself: a throwaway program built with `configFilePath` cleared
14
14
  * takes the file-derived branch of `getCommonSourceDirectory`, so it matches
15
- * `tsc` exactly, including project-reference redirects. Configs already at the
16
- * 6.0 default are left untouched: their inferred directory equals the tsconfig
17
- * directory, or they are composite (which defaulted there before 6.0 too).
15
+ * `tsc` exactly, including project-reference redirects. The pin is written even
16
+ * when the inferred directory already equals the tsconfig directory: inference
17
+ * is per-program, and tools like ts-jest's `isolatedModules` compile single
18
+ * files against the same config, collapsing the common directory below the
19
+ * config dir and failing with TS5011.
20
+ *
21
+ * Composite configs are pinned too, to the tsconfig's own directory. Under `tsc`
22
+ * a composite `rootDir` already defaults there (for any file subset), so `"."`
23
+ * is a no-op — but ts-jest strips `composite` for its per-file transpile, and
24
+ * TypeScript 6 only exempts genuinely-composite programs from the containment
25
+ * check (`!options.composite`), so a composite spec config compiled by ts-jest
26
+ * hits TS5011 all the same. The explicit `"."` survives the strip. Pinning the
27
+ * own directory (not a deeper file-derived value) keeps a real composite build's
28
+ * emit layout unchanged.
18
29
  *
19
30
  * Each config is written on its own; nothing is written to a shared `extends`
20
- * base, so a config never inherits a `rootDir` computed for a sibling. The one
21
- * case that needs care is a config that both needs a `rootDir` and is itself an
22
- * `extends` base: its own-directory children would inherit the new value and
23
- * shift their emit layout, so each such child is pinned to its own directory to
24
- * block it. Runs only on TypeScript 6 workspaces (gated by `requires`).
31
+ * base, so a config never inherits a `rootDir` computed for a sibling and
32
+ * because every config that emits is given its own explicit `rootDir`, none
33
+ * inherits a value pinned on a base. Runs only on TypeScript 6 workspaces
34
+ * (gated by `requires`).
25
35
  */
26
36
  export default function (tree: Tree): Promise<void>;
@@ -22,16 +22,26 @@ let tsModule;
22
22
  * compilation and emit layout are unchanged under 6.0. The value is computed by
23
23
  * the compiler itself: a throwaway program built with `configFilePath` cleared
24
24
  * takes the file-derived branch of `getCommonSourceDirectory`, so it matches
25
- * `tsc` exactly, including project-reference redirects. Configs already at the
26
- * 6.0 default are left untouched: their inferred directory equals the tsconfig
27
- * directory, or they are composite (which defaulted there before 6.0 too).
25
+ * `tsc` exactly, including project-reference redirects. The pin is written even
26
+ * when the inferred directory already equals the tsconfig directory: inference
27
+ * is per-program, and tools like ts-jest's `isolatedModules` compile single
28
+ * files against the same config, collapsing the common directory below the
29
+ * config dir and failing with TS5011.
30
+ *
31
+ * Composite configs are pinned too, to the tsconfig's own directory. Under `tsc`
32
+ * a composite `rootDir` already defaults there (for any file subset), so `"."`
33
+ * is a no-op — but ts-jest strips `composite` for its per-file transpile, and
34
+ * TypeScript 6 only exempts genuinely-composite programs from the containment
35
+ * check (`!options.composite`), so a composite spec config compiled by ts-jest
36
+ * hits TS5011 all the same. The explicit `"."` survives the strip. Pinning the
37
+ * own directory (not a deeper file-derived value) keeps a real composite build's
38
+ * emit layout unchanged.
28
39
  *
29
40
  * Each config is written on its own; nothing is written to a shared `extends`
30
- * base, so a config never inherits a `rootDir` computed for a sibling. The one
31
- * case that needs care is a config that both needs a `rootDir` and is itself an
32
- * `extends` base: its own-directory children would inherit the new value and
33
- * shift their emit layout, so each such child is pinned to its own directory to
34
- * block it. Runs only on TypeScript 6 workspaces (gated by `requires`).
41
+ * base, so a config never inherits a `rootDir` computed for a sibling and
42
+ * because every config that emits is given its own explicit `rootDir`, none
43
+ * inherits a value pinned on a base. Runs only on TypeScript 6 workspaces
44
+ * (gated by `requires`).
35
45
  */
36
46
  async function default_1(tree) {
37
47
  tsModule ??= (0, ensure_typescript_1.ensureTypescript)();
@@ -50,7 +60,10 @@ async function default_1(tree) {
50
60
  analysis: analyze(ts, absPath, parseConfigHost),
51
61
  };
52
62
  });
53
- // Phase 2: pin each config that needs a `rootDir` on its own.
63
+ // Phase 2: pin each config that needs a `rootDir` on its own. Every config
64
+ // that emits is given its own explicit value here — including composites,
65
+ // pinned to their own directory — so none is left to inherit a `rootDir` this
66
+ // migration pins on a base.
54
67
  let changed = 0;
55
68
  for (const c of candidates) {
56
69
  if (c.analysis.kind === 'write') {
@@ -60,36 +73,6 @@ async function default_1(tree) {
60
73
  }
61
74
  }
62
75
  }
63
- // Phase 3: shield own-dir configs that now inherit a pinned `rootDir` from a
64
- // base. Re-parse each from the tree (so Phase 2's writes are visible); when
65
- // TypeScript reports an inherited `rootDir` where there was none, pin the
66
- // config to its own directory so its emit layout does not shift. Repeat until
67
- // stable, since shielding one config can turn it into a base for another. The
68
- // tree-backed host lets TypeScript resolve `extends`; the `readDirectory`
69
- // no-op skips the unused file scan.
70
- const readFile = (filePath) => tree.read(filePath, 'utf-8') ?? undefined;
71
- const treeParseHost = {
72
- ...ts.sys,
73
- readFile,
74
- readDirectory: () => [],
75
- };
76
- const shielded = new Set();
77
- let added = true;
78
- while (added) {
79
- added = false;
80
- for (const c of candidates) {
81
- if (c.analysis.kind !== 'own-dir' || shielded.has(c.relPath)) {
82
- continue;
83
- }
84
- if (inheritsRootDir(ts, treeParseHost, readFile, c.relPath)) {
85
- if (setRootDir(tree, c.relPath, '.')) {
86
- changed++;
87
- }
88
- shielded.add(c.relPath);
89
- added = true;
90
- }
91
- }
92
- }
93
76
  if (changed > 0) {
94
77
  await (0, devkit_1.formatFiles)(tree);
95
78
  }
@@ -106,34 +89,26 @@ function analyze(ts, absPath, parseConfigHost) {
106
89
  if (!setsEmitGate(options) || fileNames.length === 0) {
107
90
  return { kind: 'inert' };
108
91
  }
109
- // Composite already defaults `rootDir` to the tsconfig's own directory in both
110
- // 5.x and 6.0, so the 6.0 change leaves it alone. Treat it as own-dir: never
111
- // pin a file-derived value, and shield it from a `rootDir` this migration pins
112
- // on an `extends` base.
92
+ // Composite: pin the tsconfig's own directory, not a file-derived value.
93
+ // `rootDir` already defaults there under `tsc`, so `"."` preserves a real
94
+ // composite build's emit layout but ts-jest strips `composite` for its
95
+ // per-file transpile, and TS6 only skips the containment check for genuinely
96
+ // composite programs, so an unpinned composite spec config still fails with
97
+ // TS5011 under ts-jest. The explicit pin survives the strip.
113
98
  if (options.composite) {
114
- return { kind: 'own-dir' };
99
+ return { kind: 'write', dirAbs: toPosix((0, node_path_1.dirname)(absPath)) };
115
100
  }
116
101
  const commonDir = computeCommonSourceDirectory(ts, fileNames, options, projectReferences);
117
102
  if (!commonDir) {
118
103
  return { kind: 'inert' };
119
104
  }
120
- if (equalPaths(ts, commonDir, (0, node_path_1.dirname)(absPath))) {
121
- return { kind: 'own-dir' };
122
- }
105
+ // Written even when `commonDir` is the tsconfig's own directory (the 6.0
106
+ // default): a tool compiling a subset of the config's files — ts-jest with
107
+ // `isolatedModules` builds a program per test file — re-infers the common
108
+ // directory from that subset alone, and without an explicit `rootDir` TS6
109
+ // fails with TS5011.
123
110
  return { kind: 'write', dirAbs: toPosix(commonDir).replace(/\/+$/, '') };
124
111
  }
125
- // True when TypeScript resolves an inherited `rootDir` for the config. An own-dir
126
- // config sets none of its own, so any `rootDir` reported here comes from a base
127
- // this migration just pinned, and inheriting it would shift the config's emit
128
- // layout.
129
- function inheritsRootDir(ts, parseHost, readFile, relPath) {
130
- const config = ts.readConfigFile(relPath, readFile).config;
131
- if (!config) {
132
- return false;
133
- }
134
- const { options } = ts.parseJsonConfigFileContent(config, parseHost, (0, node_path_1.dirname)(relPath));
135
- return options.rootDir != null;
136
- }
137
112
  // Any of these makes TypeScript run the source-file containment / common-source
138
113
  // computation that emits TS5011 / TS6059 when `rootDir` is absent.
139
114
  function setsEmitGate(o) {
@@ -203,13 +178,6 @@ function toRelativeRootDir(tsconfigDir, commonDir) {
203
178
  const rel = node_path_1.posix.relative(toPosix(tsconfigDir), toPosix(commonDir));
204
179
  return rel === '' ? '.' : rel;
205
180
  }
206
- function equalPaths(ts, a, b) {
207
- const normalize = (p) => {
208
- const stripped = toPosix(p).replace(/\/+$/, '');
209
- return ts.sys.useCaseSensitiveFileNames ? stripped : stripped.toLowerCase();
210
- };
211
- return normalize(a) === normalize(b);
212
- }
213
181
  function toPosix(p) {
214
182
  return p.split('\\').join('/');
215
183
  }
@@ -2,9 +2,11 @@
2
2
 
3
3
  Before TypeScript 6, a tsconfig that did not set `rootDir` had it inferred as the common directory of the program's non-declaration input files. TypeScript 6 changed that default to the tsconfig's own directory. A program whose files resolve outside that directory (most commonly a spec or e2e tsconfig that imports another project's source through a `paths` alias) now hard-fails with `TS5011` or `TS6059` because a file falls outside the assumed root.
4
4
 
5
- For every project `tsconfig*.json` that does not already set `rootDir` (directly or through `extends`), this migration pins `rootDir` to exactly the directory TypeScript 5 would have inferred, so both compilation and emit layout stay the same under TypeScript 6. The value is computed by the TypeScript compiler itself, so it matches `tsc` exactly, including project-reference redirects. Configs that cannot hit the error are skipped: ones without an output option (`outDir`, `outFile`, `sourceRoot`, `mapRoot`, or `declaration` + `declarationDir`), ones with no input files, composite projects (whose `rootDir` already defaulted to the tsconfig directory before TypeScript 6), and ones whose inferred directory already equals the tsconfig directory. The migration only runs when the workspace is on TypeScript 6.
5
+ For every project `tsconfig*.json` that does not already set `rootDir` (directly or through `extends`), this migration pins `rootDir` to exactly the directory TypeScript 5 would have inferred, so both compilation and emit layout stay the same under TypeScript 6. The value is computed by the TypeScript compiler itself, so it matches `tsc` exactly, including project-reference redirects. The pin is written even when the inferred directory already equals the tsconfig directory: inference is per-program, and a tool that compiles a subset of the config's files — ts-jest with `isolatedModules` builds a program per test file — re-infers a deeper common directory from that subset and fails with `TS5011`. Configs that cannot hit the error are skipped: ones without an output option (`outDir`, `outFile`, `sourceRoot`, `mapRoot`, or `declaration` + `declarationDir`), and ones with no input files. The migration only runs when the workspace is on TypeScript 6.
6
6
 
7
- Each config is updated on its own; the migration never writes `rootDir` to a shared `extends` base, so a config never inherits a value computed for a sibling. The one case that needs care is a config that both needs a `rootDir` and is itself an `extends` base: its self-contained children would inherit the new value and emit to the wrong place, so each such child is pinned to its own directory to keep its layout. This includes a composite child, whose own-directory default would otherwise be overridden by the inherited value.
7
+ Composite projects are pinned to their own directory (`"."`). Under `tsc` a composite `rootDir` already defaults there, so the pin is a no-op for a real composite build but ts-jest strips `composite` for its per-file transpile, and TypeScript 6 only exempts genuinely-composite programs from the containment check, so a composite spec config compiled by ts-jest still fails with `TS5011` without an explicit `rootDir`. Pinning the own directory (rather than a deeper file-derived value) keeps the composite build's emit layout unchanged while fixing the ts-jest case.
8
+
9
+ Each config is updated on its own; the migration never writes `rootDir` to a shared `extends` base, so a config never inherits a value computed for a sibling. Because every config that emits is given its own explicit `rootDir`, none is left to inherit a value pinned on a base.
8
10
 
9
11
  #### Sample Code Changes
10
12
 
package/migrations.json CHANGED
@@ -39,8 +39,8 @@
39
39
  "documentation": "./dist/src/migrations/update-23-1-0/add-ignore-deprecations-for-ts6.md"
40
40
  },
41
41
  "23-1-0-set-tsconfig-root-dir-for-ts6": {
42
- "version": "23.1.0-beta.8",
43
- "description": "Sets an explicit `rootDir` on project `tsconfig*.json` files that lack one, pinned to the source directory TypeScript 5 inferred implicitly, so programs whose input files span more than one directory (for example a spec tsconfig importing another project's source through a `paths` alias) keep compiling under TypeScript 6, which otherwise hard-fails with TS5011 or TS6059. The value is computed by the compiler, so emit layout is unchanged. Each config is written on its own; the migration never writes to a shared `extends` base, so a config never inherits a value computed for a sibling. Composite projects are not given an inferred `rootDir`, since it already defaulted to the tsconfig directory before TypeScript 6; a composite that extends a base receiving a pinned `rootDir` is itself pinned to its own directory to preserve that default.",
42
+ "version": "23.1.0-rc.2",
43
+ "description": "Sets an explicit `rootDir` on project `tsconfig*.json` files that lack one, pinned to the source directory TypeScript 5 inferred implicitly, so programs keep compiling and emitting the same layout under TypeScript 6, which otherwise hard-fails with TS5011 or TS6059 (for example a spec tsconfig importing another project's source through a `paths` alias). The pin is written even when the inferred directory already equals the tsconfig directory, because tools like ts-jest with `isolatedModules` compile a program per file and re-infer a deeper directory from that subset. The value is computed by the compiler, so emit layout is unchanged. Each config is written on its own; the migration never writes to a shared `extends` base, so a config never inherits a value computed for a sibling. Composite projects are pinned to their own directory: `rootDir` already defaults there under `tsc` so it is a no-op for a real composite build, but ts-jest strips `composite` for its per-file transpile and the explicit value is needed to avoid TS5011 there.",
44
44
  "requires": {
45
45
  "typescript": ">=6.0.0"
46
46
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nx/js",
3
- "version": "23.1.0-rc.0",
3
+ "version": "23.1.0-rc.2",
4
4
  "private": false,
5
5
  "type": "commonjs",
6
6
  "files": [
@@ -144,11 +144,11 @@
144
144
  "source-map-support": "0.5.19",
145
145
  "tinyglobby": "^0.2.12",
146
146
  "tslib": "^2.3.0",
147
- "@nx/workspace": "23.1.0-rc.0",
148
- "@nx/devkit": "23.1.0-rc.0"
147
+ "@nx/devkit": "23.1.0-rc.2",
148
+ "@nx/workspace": "23.1.0-rc.2"
149
149
  },
150
150
  "devDependencies": {
151
- "nx": "23.1.0-rc.0"
151
+ "nx": "23.1.0-rc.2"
152
152
  },
153
153
  "peerDependencies": {
154
154
  "@swc/cli": ">=0.6.0 <0.9.0",
@@ -1 +0,0 @@
1
- export declare function killTree(pid: number, signal: NodeJS.Signals): Promise<void>;
@@ -1,113 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.killTree = killTree;
4
- // Adapted from https://raw.githubusercontent.com/pkrumins/node-tree-kill/deee138/index.js
5
- const child_process_1 = require("child_process");
6
- async function killTree(pid, signal) {
7
- const tree = {};
8
- const pidsToProcess = {};
9
- tree[pid] = [];
10
- pidsToProcess[pid] = 1;
11
- return new Promise((resolve, reject) => {
12
- const callback = (error) => {
13
- if (error) {
14
- reject(error);
15
- }
16
- else {
17
- resolve();
18
- }
19
- };
20
- switch (process.platform) {
21
- case 'win32':
22
- (0, child_process_1.exec)('taskkill /pid ' + pid + ' /T /F', {
23
- windowsHide: true,
24
- }, (error) => {
25
- // Ignore Fatal errors (128) because it might be due to the process already being killed.
26
- // On Linux/Mac we can check ESRCH (no such process), but on Windows we can't.
27
- callback(error?.code !== 128 ? error : null);
28
- });
29
- break;
30
- case 'darwin':
31
- buildProcessTree(pid, tree, pidsToProcess, function (parentPid) {
32
- return (0, child_process_1.spawn)('pgrep', ['-P', parentPid], {
33
- windowsHide: true,
34
- });
35
- }, function () {
36
- killAll(tree, signal, callback);
37
- });
38
- break;
39
- default: // Linux
40
- buildProcessTree(pid, tree, pidsToProcess, function (parentPid) {
41
- return (0, child_process_1.spawn)('ps', ['-o', 'pid', '--no-headers', '--ppid', parentPid], {
42
- windowsHide: true,
43
- });
44
- }, function () {
45
- killAll(tree, signal, callback);
46
- });
47
- break;
48
- }
49
- });
50
- }
51
- function killAll(tree, signal, callback) {
52
- const killed = {};
53
- try {
54
- Object.keys(tree).forEach(function (pid) {
55
- tree[pid].forEach(function (pidpid) {
56
- if (!killed[pidpid]) {
57
- killPid(pidpid, signal);
58
- killed[pidpid] = 1;
59
- }
60
- });
61
- if (!killed[pid]) {
62
- killPid(pid, signal);
63
- killed[pid] = 1;
64
- }
65
- });
66
- }
67
- catch (err) {
68
- if (callback) {
69
- return callback(err);
70
- }
71
- else {
72
- throw err;
73
- }
74
- }
75
- if (callback) {
76
- return callback();
77
- }
78
- }
79
- function killPid(pid, signal) {
80
- try {
81
- process.kill(parseInt(pid, 10), signal);
82
- }
83
- catch (err) {
84
- if (err.code !== 'ESRCH')
85
- throw err;
86
- }
87
- }
88
- function buildProcessTree(parentPid, tree, pidsToProcess, spawnChildProcessesList, cb) {
89
- const ps = spawnChildProcessesList(parentPid);
90
- let allData = '';
91
- ps.stdout.on('data', (data) => {
92
- data = data.toString('ascii');
93
- allData += data;
94
- });
95
- const onClose = function (code) {
96
- delete pidsToProcess[parentPid];
97
- if (code != 0) {
98
- // no more parent processes
99
- if (Object.keys(pidsToProcess).length == 0) {
100
- cb();
101
- }
102
- return;
103
- }
104
- allData.match(/\d+/g).forEach((_pid) => {
105
- const pid = parseInt(_pid, 10);
106
- tree[parentPid].push(pid);
107
- tree[pid] = [];
108
- pidsToProcess[pid] = 1;
109
- buildProcessTree(pid, tree, pidsToProcess, spawnChildProcessesList, cb);
110
- });
111
- };
112
- ps.on('close', onClose);
113
- }