nx 23.1.0-beta.2 → 23.1.0-beta.4

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 (28) hide show
  1. package/dist/src/command-line/migrate/migrate.d.ts +2 -0
  2. package/dist/src/command-line/migrate/migrate.js +32 -6
  3. package/dist/src/daemon/server/project-graph-incremental-recomputation.js +30 -11
  4. package/dist/src/native/index.d.ts +48 -1
  5. package/dist/src/native/native-bindings.js +1 -0
  6. package/dist/src/native/nx.wasm32-wasi.debug.wasm +0 -0
  7. package/dist/src/native/nx.wasm32-wasi.wasm +0 -0
  8. package/dist/src/plugins/js/utils/register.d.ts +31 -10
  9. package/dist/src/plugins/js/utils/register.js +87 -18
  10. package/dist/src/tasks-runner/default-tasks-runner.js +6 -3
  11. package/dist/src/tasks-runner/init-tasks-runner.js +1 -1
  12. package/dist/src/tasks-runner/life-cycle.d.ts +10 -5
  13. package/dist/src/tasks-runner/life-cycle.js +4 -4
  14. package/dist/src/tasks-runner/life-cycles/performance-analysis.d.ts +156 -0
  15. package/dist/src/tasks-runner/life-cycles/performance-analysis.js +486 -0
  16. package/dist/src/tasks-runner/life-cycles/performance-life-cycle.d.ts +59 -0
  17. package/dist/src/tasks-runner/life-cycles/performance-life-cycle.js +152 -0
  18. package/dist/src/tasks-runner/life-cycles/performance-report.d.ts +62 -0
  19. package/dist/src/tasks-runner/life-cycles/performance-report.js +318 -0
  20. package/dist/src/tasks-runner/run-command.d.ts +1 -1
  21. package/dist/src/tasks-runner/run-command.js +12 -2
  22. package/dist/src/utils/min-release-age/behavior/pnpm.d.ts +6 -5
  23. package/dist/src/utils/min-release-age/behavior/pnpm.js +87 -339
  24. package/dist/src/utils/package-manager.d.ts +6 -3
  25. package/dist/src/utils/package-manager.js +11 -7
  26. package/dist/src/utils/terminal-link.d.ts +10 -6
  27. package/dist/src/utils/terminal-link.js +9 -11
  28. package/package.json +11 -11
@@ -85,6 +85,7 @@ export declare class Migrator {
85
85
  }>;
86
86
  private createMigrateJson;
87
87
  private buildPackageJsonUpdates;
88
+ private resolveVersionForCascade;
88
89
  private populatePackageJsonUpdatesAndGetPackagesToCheck;
89
90
  private getPackageJsonUpdatesFromMigrationConfig;
90
91
  /**
@@ -172,6 +173,7 @@ export declare function createFetcher(pmc: PackageManagerCommands): ((pkg: strin
172
173
  stats?: MigrateFetchStats;
173
174
  };
174
175
  export { filterDowngradedUpdates };
176
+ export declare function generateMigrationsJsonAndUpdatePackageJson(root: string, opts: GenerateMigrations, fetch?: MigratorOptions['fetch']): Promise<void>;
175
177
  /**
176
178
  * Detects npm peer-dependency resolution failures. Keyed on the `ERESOLVE`
177
179
  * error code, which npm consistently emits for this class of failure across
@@ -6,6 +6,7 @@ exports.resolveCanonicalNxPackage = resolveCanonicalNxPackage;
6
6
  exports.resolveInclude = resolveInclude;
7
7
  exports.parseMigrationsOptions = parseMigrationsOptions;
8
8
  exports.createFetcher = createFetcher;
9
+ exports.generateMigrationsJsonAndUpdatePackageJson = generateMigrationsJsonAndUpdatePackageJson;
9
10
  exports.isNpmPeerDepsError = isNpmPeerDepsError;
10
11
  exports.resolveAgenticRunId = resolveAgenticRunId;
11
12
  exports.formatSkippedPromptsNextStep = formatSkippedPromptsNextStep;
@@ -191,16 +192,37 @@ class Migrator {
191
192
  !this.areIncompatiblePackagesPresent(packageUpdate.incompatibleWith) &&
192
193
  (!this.interactive ||
193
194
  (await this.runPackageJsonUpdatesConfirmationPrompt(packageUpdate, packageUpdateKey, packageToCheck.package)))) {
194
- Object.entries(packageUpdate.packages).forEach(([name, update]) => {
195
+ const updateEntries = Object.entries(packageUpdate.packages);
196
+ // Validate all up front so invalid metadata fails fast, before any
197
+ // resolution does I/O.
198
+ for (const [name, update] of updateEntries) {
195
199
  this.validatePackageUpdateVersion(packageToCheck.package, name, update);
196
- filteredUpdates[name] = update;
197
- this.packageUpdates[name] = update;
198
- });
200
+ }
201
+ // Resolve serially: resolution can prompt (pnpm strict cooldown) and
202
+ // append to minimumReleaseAgeExclude, so a serial loop avoids
203
+ // overlapping prompts and keeps packageUpdates ordering stable.
204
+ for (const [name, update] of updateEntries) {
205
+ const resolvedUpdate = {
206
+ ...update,
207
+ version: await this.resolveVersionForCascade(name, update.version),
208
+ };
209
+ filteredUpdates[name] = resolvedUpdate;
210
+ this.packageUpdates[name] = resolvedUpdate;
211
+ }
199
212
  }
200
213
  }
201
214
  await Promise.all(Object.entries(filteredUpdates).map(([name, update]) => this.buildPackageJsonUpdates(name, update)));
202
215
  }
203
216
  }
217
+ async resolveVersionForCascade(packageName, version) {
218
+ // Already a fully-qualified semver (incl. prereleases) - nothing to resolve.
219
+ if ((0, semver_1.valid)(version)) {
220
+ return version;
221
+ }
222
+ // Otherwise resolve the spec (range/tag) through the min-release-age policy,
223
+ // which also honors any configured minimumReleaseAgeExclude entries.
224
+ return (0, resolve_package_version_1.resolvePackageVersionRespectingMinReleaseAge)(packageName, version);
225
+ }
204
226
  async populatePackageJsonUpdatesAndGetPackagesToCheck(targetPackage, target) {
205
227
  let targetVersion = target.version;
206
228
  if (this.to[targetPackage]) {
@@ -1347,6 +1369,7 @@ function readNxVersion(packageJson, root) {
1347
1369
  (0, package_json_1.getDependencyVersionFromPackageJson)('@nx/workspace', root, packageJson) ??
1348
1370
  (0, package_json_1.getDependencyVersionFromPackageJson)('@nrwl/workspace', root, packageJson));
1349
1371
  }
1372
+ // Exported for testing the optional-include orchestration seam (see NXC-4590).
1350
1373
  async function generateMigrationsJsonAndUpdatePackageJson(root, opts, fetch) {
1351
1374
  const pmc = (0, package_manager_1.getPackageManagerCommand)();
1352
1375
  let phase = 'fetch_migrations';
@@ -1417,7 +1440,11 @@ async function generateMigrationsJsonAndUpdatePackageJson(root, opts, fetch) {
1417
1440
  const writableUpdates = (0, update_filters_1.filterDowngradedUpdates)(packageUpdates, (0, catalog_1.resolveCatalogSpecifiers)(originalPackageJson), installedPackageVersions);
1418
1441
  const wrotePackageJson = await updatePackageJson(root, writableUpdates);
1419
1442
  const wroteNxJsonInstallation = await updateInstallationDetails(root, writableUpdates);
1420
- const promptMigrationFiles = (0, prompt_files_1.writePromptMigrationFiles)(root, migrations, promptContents ?? {}, packageUpdates[walkedTargetPackage].version);
1443
+ // Under `--include=optional` the target's own entry is filtered out of
1444
+ // `packageUpdates` (it's a required package), so resolve the version
1445
+ // defensively. Also reused by the completion analytics below.
1446
+ const resolvedTargetVersion = packageUpdates[walkedTargetPackage]?.version ?? opts.targetVersion;
1447
+ const promptMigrationFiles = (0, prompt_files_1.writePromptMigrationFiles)(root, migrations, promptContents ?? {}, resolvedTargetVersion);
1421
1448
  if (migrations.length > 0) {
1422
1449
  await createMigrationsFile(root, [
1423
1450
  ...addSplitConfigurationMigrationIfAvailable(from, writableUpdates),
@@ -1429,7 +1456,6 @@ async function generateMigrationsJsonAndUpdatePackageJson(root, opts, fetch) {
1429
1456
  : include === 'optional'
1430
1457
  ? `- Processed optional dependency updates only (skipped required package updates).`
1431
1458
  : null;
1432
- const resolvedTargetVersion = packageUpdates[walkedTargetPackage]?.version ?? opts.targetVersion;
1433
1459
  // The param expressions below evaluate before the report function is
1434
1460
  // entered; `safeReport` keeps them inside the analytics boundary so a
1435
1461
  // param-building throw can't surface here and convert an already
@@ -178,9 +178,26 @@ async function getCachedSerializedProjectGraphPromise(socket) {
178
178
  }
179
179
  function scheduleProjectGraphRecomputation(createdFiles, updatedFiles, deletedFiles) {
180
180
  ++fileChangeCounter;
181
- for (let f of [...createdFiles, ...updatedFiles]) {
181
+ // Hash the changed files up front and drop no-op rewrites before they can
182
+ // trigger an expensive recompute. Restoring a cached task output, a
183
+ // `git checkout` back to the same content, or a formatter that changes
184
+ // nothing all rewrite a file (new inode) the watcher reports as changed
185
+ // even though the bytes are identical. updateFilesInContext updates the
186
+ // workspace context and returns only the files whose content actually
187
+ // changed. Hashing here — once per watcher batch — rather than inside the
188
+ // recompute keeps it off the stale-retry path, which would otherwise see
189
+ // "no change" after the first pass already updated the context hashes.
190
+ perf_hooks_1.performance.mark('hash-watched-changes-start');
191
+ const changedFileHashes = createdFiles.length > 0 ||
192
+ updatedFiles.length > 0 ||
193
+ deletedFiles.length > 0
194
+ ? ((0, workspace_context_1.updateFilesInContext)(workspace_root_1.workspaceRoot, [...createdFiles, ...updatedFiles], deletedFiles) ?? {})
195
+ : {};
196
+ perf_hooks_1.performance.mark('hash-watched-changes-end');
197
+ perf_hooks_1.performance.measure('hash changed files from watcher', 'hash-watched-changes-start', 'hash-watched-changes-end');
198
+ for (const [f, hash] of Object.entries(changedFileHashes)) {
182
199
  collectedDeletedFiles.delete(f);
183
- collectedUpdatedFiles.set(f, fileChangeCounter);
200
+ collectedUpdatedFiles.set(f, { version: fileChangeCounter, hash });
184
201
  }
185
202
  for (let f of deletedFiles) {
186
203
  collectedUpdatedFiles.delete(f);
@@ -188,9 +205,7 @@ function scheduleProjectGraphRecomputation(createdFiles, updatedFiles, deletedFi
188
205
  }
189
206
  // The native watcher already coalesces a burst of events into one batch,
190
207
  // so socket + listener notifications dispatch immediately.
191
- if (createdFiles.length > 0 ||
192
- updatedFiles.length > 0 ||
193
- deletedFiles.length > 0) {
208
+ if (Object.keys(changedFileHashes).length > 0 || deletedFiles.length > 0) {
194
209
  (0, file_change_events_1.notifyFileChangeListeners)({ createdFiles, updatedFiles, deletedFiles });
195
210
  (0, file_watcher_sockets_1.notifyFileWatcherSockets)(createdFiles, updatedFiles, deletedFiles);
196
211
  // Bump generation synchronously so any in-flight compute fails its
@@ -284,14 +299,18 @@ async function processFilesAndCreateAndSerializeProjectGraph(separatedPlugins) {
284
299
  return cachedSerializedProjectGraphPromise;
285
300
  };
286
301
  try {
287
- perf_hooks_1.performance.mark('hash-watched-changes-start');
288
302
  const updatedFilesSnapshot = new Map(collectedUpdatedFiles);
289
303
  const deletedFilesSnapshot = new Map(collectedDeletedFiles);
290
304
  const updatedFiles = [...updatedFilesSnapshot.keys()];
291
305
  const deletedFiles = [...deletedFilesSnapshot.keys()];
292
- let updatedFileHashes = (0, workspace_context_1.updateFilesInContext)(workspace_root_1.workspaceRoot, updatedFiles, deletedFiles);
293
- perf_hooks_1.performance.mark('hash-watched-changes-end');
294
- perf_hooks_1.performance.measure('hash changed files from watcher', 'hash-watched-changes-start', 'hash-watched-changes-end');
306
+ // Hashes were already computed (and the workspace context updated) in
307
+ // scheduleProjectGraphRecomputation, which also dropped no-op rewrites.
308
+ // Reuse them so the context isn't re-hashed on every (possibly stale)
309
+ // recompute attempt.
310
+ const updatedFileHashes = {};
311
+ for (const [f, { hash }] of updatedFilesSnapshot) {
312
+ updatedFileHashes[f] = hash;
313
+ }
295
314
  logger_1.serverLogger.requestLog(`Updated workspace context based on watched changes, recomputing project graph...`);
296
315
  logger_1.serverLogger.requestLog(updatedFiles);
297
316
  logger_1.serverLogger.requestLog(deletedFiles);
@@ -333,8 +352,8 @@ async function processFilesAndCreateAndSerializeProjectGraph(separatedPlugins) {
333
352
  // from the daemon's view (project graph misses recently added files).
334
353
  // Match version-stamps so a file modified mid-flight (higher version)
335
354
  // stays in the queue for reprocessing.
336
- for (const [f, version] of updatedFilesSnapshot) {
337
- if (collectedUpdatedFiles.get(f) === version) {
355
+ for (const [f, { version }] of updatedFilesSnapshot) {
356
+ if (collectedUpdatedFiles.get(f)?.version === version) {
338
357
  collectedUpdatedFiles.delete(f);
339
358
  }
340
359
  }
@@ -34,7 +34,7 @@ export declare class AppLifeCycle {
34
34
  startTasks(tasks: Array<Task>, metadata: object): void
35
35
  printTaskTerminalOutput(task: Task, status: string, output: string): void
36
36
  endTasks(taskResults: Array<TaskResult>, metadata: object): void
37
- endCommand(): void
37
+ endCommand(summary?: PerformanceSummaryPayload | undefined | null): void
38
38
  __init(doneCallback: (() => unknown)): void
39
39
  registerRunningTask(taskId: string, parserAndWriter: ExternalObject<[ParserArc, WriterArc]>): void
40
40
  registerRunningTaskWithEmptyParser(taskId: string): void
@@ -275,6 +275,15 @@ export interface CachedResult {
275
275
  size?: number
276
276
  }
277
277
 
278
+ /**
279
+ * Cache hits vs total; present only when there was a cache outcome. A bypassed
280
+ * cache is signalled separately by `cache_skipped`.
281
+ */
282
+ export interface CacheStat {
283
+ hits: number
284
+ total: number
285
+ }
286
+
278
287
  export declare function canInstallNxConsole(): Promise<boolean>
279
288
 
280
289
  export declare function canInstallNxConsoleForEditor(editor: SupportedEditor): Promise<boolean>
@@ -375,6 +384,13 @@ export declare function findImports(projectFileMap: Record<string, Array<string>
375
384
  */
376
385
  export declare function flushTelemetry(): void
377
386
 
387
+ /**
388
+ * The single duration formatter — used by the task list, terminal report, and TUI
389
+ * popup. Exposed to JS as `formatDuration` so all three share one implementation.
390
+ * 0 (or sub-millisecond) → "<1ms", then "470ms", "13.4s", "1m 30s".
391
+ */
392
+ export declare function formatDuration(ms: number): string
393
+
378
394
  export declare function getBinaryTarget(): string
379
395
 
380
396
  export declare function getDefaultMaxCacheSize(cachePath: string): number
@@ -524,6 +540,15 @@ export declare function killProcessTree(rootPid: number, signal?: string | numbe
524
540
  */
525
541
  export declare function killProcessTreeGraceful(rootPid: number, signal?: string | number | undefined | null, gracePeriodMs?: number | undefined | null): Promise<void>
526
542
 
543
+ /**
544
+ * A docs link rendered as an OSC 8 hyperlink. Both fields come from TS so the
545
+ * popup never hardcodes a URL.
546
+ */
547
+ export interface Link {
548
+ text: string
549
+ href: string
550
+ }
551
+
527
552
  export declare function logDebug(message: string): void
528
553
 
529
554
  /** Combined metadata for groups and processes */
@@ -564,6 +589,28 @@ export interface NxWorkspaceFilesExternals {
564
589
 
565
590
  export declare function parseTaskStatus(stringStatus: string): TaskStatus
566
591
 
592
+ /**
593
+ * Structured run report shown in the exit-countdown popup. The TUI builds the
594
+ * visual from these numbers rather than receiving a pre-formatted string.
595
+ */
596
+ export interface PerformanceSummaryPayload {
597
+ runDurationMs: number
598
+ criticalPathMs: number
599
+ criticalPathTaskCount: number
600
+ recoverableMs: number
601
+ cache?: CacheStat
602
+ cacheSkipped: boolean
603
+ /** Already in display order; a multi-line entry embeds a task list. */
604
+ recommendations: Array<string>
605
+ /** The docs footer link, rendered as a bullet and hyperlinked. */
606
+ footer: Link
607
+ /**
608
+ * Phrases already in `recommendations` to hyperlink in place (e.g. the
609
+ * remote-cache CTA); empty when none apply.
610
+ */
611
+ links: Array<Link>
612
+ }
613
+
567
614
  /** Process metadata (static, doesn't change during process lifetime) */
568
615
  export interface ProcessMetadata {
569
616
  ppid: number
@@ -605,6 +605,7 @@ module.exports.EventType = nativeBinding.EventType
605
605
  module.exports.expandOutputs = nativeBinding.expandOutputs
606
606
  module.exports.findImports = nativeBinding.findImports
607
607
  module.exports.flushTelemetry = nativeBinding.flushTelemetry
608
+ module.exports.formatDuration = nativeBinding.formatDuration
608
609
  module.exports.getBinaryTarget = nativeBinding.getBinaryTarget
609
610
  module.exports.getDefaultMaxCacheSize = nativeBinding.getDefaultMaxCacheSize
610
611
  module.exports.getEventDimensions = nativeBinding.getEventDimensions
Binary file
@@ -35,22 +35,43 @@ export declare function forceRegisterEsmLoader(): void;
35
35
  * the importing module is itself TypeScript, and the specifier is relative, so
36
36
  * it never hijacks resolution that would otherwise succeed.
37
37
  *
38
+ * Used only as the fallback for runtimes without `module.registerHooks()`
39
+ * (Node < 22.15 / < 23.5); newer runtimes register the in-thread synchronous
40
+ * twin `nodeNextEsmResolveHook` instead. Keep the two in sync.
41
+ *
38
42
  * Exported so the hook can be exercised directly in unit tests.
39
43
  */
40
44
  export declare const NODENEXT_ESM_RESOLVER_SOURCE = "\nconst EXT_FALLBACK = { '.js': ['.ts', '.tsx'], '.mjs': ['.mts'], '.cjs': ['.cts'] };\nconst TS_PARENT_RE = /\\.(?:ts|tsx|mts|cts)(?:$|\\?)/;\nexport async function resolve(specifier, context, nextResolve) {\n try {\n return await nextResolve(specifier, context);\n } catch (err) {\n if (err?.code !== 'ERR_MODULE_NOT_FOUND') throw err;\n const parent = context.parentURL;\n if (!parent || !TS_PARENT_RE.test(parent)) throw err;\n if (!(specifier.startsWith('./') || specifier.startsWith('../') || specifier.startsWith('file:'))) throw err;\n const m = specifier.match(/(\\.(?:js|mjs|cjs))($|\\?)/);\n if (!m) throw err;\n const fallbacks = EXT_FALLBACK[m[1]];\n if (!fallbacks) throw err;\n const base = specifier.slice(0, m.index);\n const suffix = specifier.slice(m.index + m[1].length);\n for (const ext of fallbacks) {\n try { return await nextResolve(base + ext + suffix, context); } catch {}\n }\n throw err;\n }\n}\n";
41
45
  /**
42
- * Register a self-contained ESM resolution hook (via `Module.register`) that
43
- * rewrites TypeScript NodeNext-style `.js`/`.mjs`/`.cjs` relative specifiers to
44
- * their `.ts`/`.mts`/`.cts` sources. This is the ESM counterpart to
45
- * `ensureCjsResolverPatched`: Node's native type stripping loads the `.ts`
46
- * file, but neither native strip nor Node's ESM resolver rewrites the
47
- * extension, so `import './foo.js'` from a `.ts` source where only `foo.ts`
48
- * exists fails with ERR_MODULE_NOT_FOUND without it.
46
+ * Synchronous in-thread twin of `NODENEXT_ESM_RESOLVER_SOURCE` for
47
+ * `module.registerHooks()`: `nextResolve` throws synchronously rather than
48
+ * rejecting, so this uses try/catch instead of `await`. Keep the two in sync.
49
+ * Exported for unit tests.
50
+ */
51
+ export declare function nodeNextEsmResolveHook(specifier: string, context: {
52
+ parentURL?: string;
53
+ }, nextResolve: (specifier: string, context?: unknown) => {
54
+ url: string;
55
+ }): {
56
+ url: string;
57
+ };
58
+ /**
59
+ * Register an ESM resolution hook that rewrites TypeScript NodeNext-style
60
+ * `.js`/`.mjs`/`.cjs` relative specifiers to their `.ts`/`.mts`/`.cts` sources.
61
+ * This is the ESM counterpart to `ensureCjsResolverPatched`: Node's native type
62
+ * stripping loads the `.ts` file, but neither native strip nor Node's ESM
63
+ * resolver rewrites the extension, so `import './foo.js'` from a `.ts` source
64
+ * where only `foo.ts` exists fails with ERR_MODULE_NOT_FOUND without it.
49
65
  *
50
- * The hook is inlined as a `data:` module (see `NODENEXT_ESM_RESOLVER_SOURCE`)
51
- * and relies only on Node's default resolver, so it needs no ts-node/swc-node.
66
+ * Prefers `module.registerHooks()` (Node 22.15+ / 23.5+), passing the in-thread
67
+ * `nodeNextEsmResolveHook`. Falls back to `module.register()` with the inlined
68
+ * `data:` module (`NODENEXT_ESM_RESOLVER_SOURCE`) on older runtimes that lack
69
+ * `registerHooks` - `module.register()` emits a runtime DeprecationWarning
70
+ * (DEP0205) on Node 25.9+/26+, so it is only used when nothing better exists.
71
+ * Either way the hook relies only on Node's default resolver - no
72
+ * ts-node/swc-node.
52
73
  *
53
- * Idempotent and best-effort: a no-op when `Module.register` is unavailable,
74
+ * Idempotent and best-effort: a no-op when no registration API is available,
54
75
  * when a TypeScript transpiler is already preloaded (see
55
76
  * `isTsTranspilerPreloaded`), or if registration fails.
56
77
  */
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.NODENEXT_ESM_RESOLVER_SOURCE = void 0;
4
4
  exports.forceRegisterEsmLoader = forceRegisterEsmLoader;
5
+ exports.nodeNextEsmResolveHook = nodeNextEsmResolveHook;
5
6
  exports.ensureNodeNextEsmResolverRegistered = ensureNodeNextEsmResolverRegistered;
6
7
  exports.ensureCjsResolverPatched = ensureCjsResolverPatched;
7
8
  exports.isNativeStripPreferred = isNativeStripPreferred;
@@ -100,6 +101,10 @@ function ensureEsmLoaderRegistered(opts) {
100
101
  * the importing module is itself TypeScript, and the specifier is relative, so
101
102
  * it never hijacks resolution that would otherwise succeed.
102
103
  *
104
+ * Used only as the fallback for runtimes without `module.registerHooks()`
105
+ * (Node < 22.15 / < 23.5); newer runtimes register the in-thread synchronous
106
+ * twin `nodeNextEsmResolveHook` instead. Keep the two in sync.
107
+ *
103
108
  * Exported so the hook can be exercised directly in unit tests.
104
109
  */
105
110
  exports.NODENEXT_ESM_RESOLVER_SOURCE = `
@@ -126,20 +131,70 @@ export async function resolve(specifier, context, nextResolve) {
126
131
  }
127
132
  }
128
133
  `;
134
+ const NODENEXT_EXT_FALLBACK = {
135
+ '.js': ['.ts', '.tsx'],
136
+ '.mjs': ['.mts'],
137
+ '.cjs': ['.cts'],
138
+ };
139
+ const NODENEXT_TS_PARENT_RE = /\.(?:ts|tsx|mts|cts)(?:$|\?)/;
140
+ /**
141
+ * Synchronous in-thread twin of `NODENEXT_ESM_RESOLVER_SOURCE` for
142
+ * `module.registerHooks()`: `nextResolve` throws synchronously rather than
143
+ * rejecting, so this uses try/catch instead of `await`. Keep the two in sync.
144
+ * Exported for unit tests.
145
+ */
146
+ function nodeNextEsmResolveHook(specifier, context, nextResolve) {
147
+ try {
148
+ return nextResolve(specifier, context);
149
+ }
150
+ catch (err) {
151
+ if (err?.code !== 'ERR_MODULE_NOT_FOUND')
152
+ throw err;
153
+ const parent = context.parentURL;
154
+ if (!parent || !NODENEXT_TS_PARENT_RE.test(parent))
155
+ throw err;
156
+ if (!(specifier.startsWith('./') ||
157
+ specifier.startsWith('../') ||
158
+ specifier.startsWith('file:'))) {
159
+ throw err;
160
+ }
161
+ const m = specifier.match(/(\.(?:js|mjs|cjs))($|\?)/);
162
+ if (!m)
163
+ throw err;
164
+ const fallbacks = NODENEXT_EXT_FALLBACK[m[1]];
165
+ if (!fallbacks)
166
+ throw err;
167
+ const base = specifier.slice(0, m.index);
168
+ const suffix = specifier.slice(m.index + m[1].length);
169
+ for (const ext of fallbacks) {
170
+ try {
171
+ return nextResolve(base + ext + suffix, context);
172
+ }
173
+ catch {
174
+ // try the next fallback
175
+ }
176
+ }
177
+ throw err;
178
+ }
179
+ }
129
180
  let nodeNextEsmResolverRegistered = false;
130
181
  /**
131
- * Register a self-contained ESM resolution hook (via `Module.register`) that
132
- * rewrites TypeScript NodeNext-style `.js`/`.mjs`/`.cjs` relative specifiers to
133
- * their `.ts`/`.mts`/`.cts` sources. This is the ESM counterpart to
134
- * `ensureCjsResolverPatched`: Node's native type stripping loads the `.ts`
135
- * file, but neither native strip nor Node's ESM resolver rewrites the
136
- * extension, so `import './foo.js'` from a `.ts` source where only `foo.ts`
137
- * exists fails with ERR_MODULE_NOT_FOUND without it.
182
+ * Register an ESM resolution hook that rewrites TypeScript NodeNext-style
183
+ * `.js`/`.mjs`/`.cjs` relative specifiers to their `.ts`/`.mts`/`.cts` sources.
184
+ * This is the ESM counterpart to `ensureCjsResolverPatched`: Node's native type
185
+ * stripping loads the `.ts` file, but neither native strip nor Node's ESM
186
+ * resolver rewrites the extension, so `import './foo.js'` from a `.ts` source
187
+ * where only `foo.ts` exists fails with ERR_MODULE_NOT_FOUND without it.
138
188
  *
139
- * The hook is inlined as a `data:` module (see `NODENEXT_ESM_RESOLVER_SOURCE`)
140
- * and relies only on Node's default resolver, so it needs no ts-node/swc-node.
189
+ * Prefers `module.registerHooks()` (Node 22.15+ / 23.5+), passing the in-thread
190
+ * `nodeNextEsmResolveHook`. Falls back to `module.register()` with the inlined
191
+ * `data:` module (`NODENEXT_ESM_RESOLVER_SOURCE`) on older runtimes that lack
192
+ * `registerHooks` - `module.register()` emits a runtime DeprecationWarning
193
+ * (DEP0205) on Node 25.9+/26+, so it is only used when nothing better exists.
194
+ * Either way the hook relies only on Node's default resolver - no
195
+ * ts-node/swc-node.
141
196
  *
142
- * Idempotent and best-effort: a no-op when `Module.register` is unavailable,
197
+ * Idempotent and best-effort: a no-op when no registration API is available,
143
198
  * when a TypeScript transpiler is already preloaded (see
144
199
  * `isTsTranspilerPreloaded`), or if registration fails.
145
200
  */
@@ -148,16 +203,16 @@ function ensureNodeNextEsmResolverRegistered() {
148
203
  return;
149
204
  nodeNextEsmResolverRegistered = true;
150
205
  const module = require('node:module');
151
- if (typeof module.register !== 'function')
152
- return;
153
206
  // Skip when a transpiler was preloaded via `--require`/`--import` (e.g.
154
207
  // `--require ts-node/register`, which Nx uses only when it runs from `.ts`
155
- // source). `module.register()` spins up a loader-hook worker thread on which
156
- // Node re-runs those preloads, resolved relative to the *current* working
157
- // directory - and Nx plugin workers `chdir()` into the analyzed workspace
158
- // first. If that workspace can't resolve the preloaded module, the loader
159
- // worker throws and can leave module resolution in a bad state, so we must
160
- // avoid the call entirely; catching it is not a clean recovery.
208
+ // source). The `module.register()` fallback below spins up a loader-hook
209
+ // worker thread on which Node re-runs those preloads, resolved relative to
210
+ // the *current* working directory - and Nx plugin workers `chdir()` into the
211
+ // analyzed workspace first. If that workspace can't resolve the preloaded
212
+ // module, the loader worker throws and can leave module resolution in a bad
213
+ // state, so we must avoid the call entirely; catching it is not a clean
214
+ // recovery. `module.registerHooks()` runs in-thread with no such worker, but
215
+ // we keep the skip uniform so resolver coverage doesn't vary by Node version.
161
216
  //
162
217
  // Consequence: in that from-`.ts`-source invocation a `type: module` plugin
163
218
  // using NodeNext `.js` specifiers won't get this resolver (a preloaded
@@ -165,6 +220,20 @@ function ensureNodeNextEsmResolverRegistered() {
165
220
  // unaffected - its workers run compiled `.js` with no preload.
166
221
  if (isTsTranspilerPreloaded())
167
222
  return;
223
+ // Synchronous in-thread hooks (Node 22.15+ / 23.5+). Preferred because
224
+ // `module.register()` is deprecated (DEP0205) from Node 25.9+/26+.
225
+ const registerHooks = module.registerHooks;
226
+ if (typeof registerHooks === 'function') {
227
+ try {
228
+ registerHooks.call(module, { resolve: nodeNextEsmResolveHook });
229
+ }
230
+ catch {
231
+ // Best-effort: leave Node's native handling in place rather than failing.
232
+ }
233
+ return;
234
+ }
235
+ if (typeof module.register !== 'function')
236
+ return;
168
237
  try {
169
238
  module.register('data:text/javascript,' + encodeURIComponent(exports.NODENEXT_ESM_RESOLVER_SOURCE));
170
239
  }
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.defaultTasksRunner = exports.RemoteCacheV2 = void 0;
4
4
  const task_orchestrator_1 = require("./task-orchestrator");
5
+ const performance_life_cycle_1 = require("./life-cycles/performance-life-cycle");
5
6
  const cache_directory_1 = require("../utils/cache-directory");
6
7
  const promises_1 = require("fs/promises");
7
8
  const path_1 = require("path");
@@ -54,13 +55,15 @@ class RemoteCacheV2 {
54
55
  }
55
56
  exports.RemoteCacheV2 = RemoteCacheV2;
56
57
  const defaultTasksRunner = async (tasks, options, context) => {
57
- const { total: threadCount } = (0, task_orchestrator_1.getThreadPoolSize)(options, context.taskGraph);
58
- await options.lifeCycle.startCommand(threadCount);
58
+ const { total: threadCount, discrete } = (0, task_orchestrator_1.getThreadPoolSize)(options, context.taskGraph);
59
+ // Total thread count drives the TUI; the discrete `--parallel` feeds the perf report.
60
+ await options.lifeCycle.startCommand(threadCount, discrete);
59
61
  try {
60
62
  return await runAllTasks(options, context);
61
63
  }
62
64
  finally {
63
- await options.lifeCycle.endCommand();
65
+ // Hand the TUI exit popup the perf report (multi-task TUI runs); other modes flush it.
66
+ await options.lifeCycle.endCommand((0, performance_life_cycle_1.getPerformanceReport)(tasks.length));
64
67
  }
65
68
  };
66
69
  exports.defaultTasksRunner = defaultTasksRunner;
@@ -15,7 +15,7 @@ async function createOrchestrator(tasks, projectGraph, fullTaskGraph, nxJson, li
15
15
  const invokeRunnerTerminalLifecycle = new invoke_runner_terminal_output_life_cycle_1.InvokeRunnerTerminalOutputLifeCycle(tasks);
16
16
  const taskResultsLifecycle = new task_results_life_cycle_1.TaskResultsLifeCycle();
17
17
  const compositedLifeCycle = new life_cycle_1.CompositeLifeCycle([
18
- ...(0, run_command_1.constructLifeCycles)(invokeRunnerTerminalLifecycle),
18
+ ...(0, run_command_1.constructLifeCycles)(invokeRunnerTerminalLifecycle, fullTaskGraph, nxJson),
19
19
  taskResultsLifecycle,
20
20
  lifeCycle,
21
21
  ]);
@@ -1,5 +1,5 @@
1
1
  import { Task } from '../config/task-graph';
2
- import { BatchInfo, BatchStatus, ExternalObject, TaskResult, TaskStatus as NativeTaskStatus } from '../native';
2
+ import { BatchInfo, BatchStatus, ExternalObject, PerformanceSummaryPayload, TaskResult, TaskStatus as NativeTaskStatus } from '../native';
3
3
  import { TaskStatus } from './tasks-runner';
4
4
  /**
5
5
  * The result of a completed {@link Task}.
@@ -21,8 +21,13 @@ export interface TaskMetadata {
21
21
  groupId: number;
22
22
  }
23
23
  export interface LifeCycle {
24
- startCommand?(parallel?: number): void | Promise<void>;
25
- endCommand?(): void | Promise<void>;
24
+ /**
25
+ * @param threadCount total thread-pool size (drives the TUI display)
26
+ * @param parallel resolved `--parallel` (discrete slots), for the performance report
27
+ */
28
+ startCommand?(threadCount?: number, parallel?: number): void | Promise<void>;
29
+ /** @param summary performance report payload for the TUI exit popup (TUI runs only) */
30
+ endCommand?(summary?: PerformanceSummaryPayload): void | Promise<void>;
26
31
  scheduleTask?(task: Task): void | Promise<void>;
27
32
  /**
28
33
  * @deprecated use startTasks
@@ -53,8 +58,8 @@ export interface LifeCycle {
53
58
  export declare class CompositeLifeCycle implements LifeCycle {
54
59
  private readonly lifeCycles;
55
60
  constructor(lifeCycles: LifeCycle[]);
56
- startCommand(parallel?: number): Promise<void>;
57
- endCommand(): Promise<void>;
61
+ startCommand(threadCount?: number, parallel?: number): Promise<void>;
62
+ endCommand(summary?: PerformanceSummaryPayload): Promise<void>;
58
63
  scheduleTask(task: Task): Promise<void>;
59
64
  startTask(task: Task): void;
60
65
  endTask(task: Task, code: number): void;
@@ -5,17 +5,17 @@ class CompositeLifeCycle {
5
5
  constructor(lifeCycles) {
6
6
  this.lifeCycles = lifeCycles;
7
7
  }
8
- async startCommand(parallel) {
8
+ async startCommand(threadCount, parallel) {
9
9
  for (let l of this.lifeCycles) {
10
10
  if (l.startCommand) {
11
- await l.startCommand(parallel);
11
+ await l.startCommand(threadCount, parallel);
12
12
  }
13
13
  }
14
14
  }
15
- async endCommand() {
15
+ async endCommand(summary) {
16
16
  for (let l of this.lifeCycles) {
17
17
  if (l.endCommand) {
18
- await l.endCommand();
18
+ await l.endCommand(summary);
19
19
  }
20
20
  }
21
21
  }