nx 23.3.0-beta.1 → 23.3.0-beta.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.
Files changed (51) hide show
  1. package/dist/src/command-line/init/implementation/add-nx-to-monorepo.js +0 -3
  2. package/dist/src/command-line/init/implementation/add-nx-to-nest.js +0 -3
  3. package/dist/src/command-line/init/implementation/add-nx-to-npm-repo.js +0 -3
  4. package/dist/src/command-line/init/implementation/angular/index.js +0 -3
  5. package/dist/src/command-line/init/implementation/angular/legacy-angular-versions.js +0 -3
  6. package/dist/src/command-line/init/implementation/utils.d.ts +0 -1
  7. package/dist/src/command-line/init/implementation/utils.js +0 -8
  8. package/dist/src/command-line/init/init-v2.js +0 -3
  9. package/dist/src/command-line/nx-cloud/connect/connect-to-nx-cloud.js +0 -10
  10. package/dist/src/core/graph/main.js +1 -1
  11. package/dist/src/daemon/client/client.d.ts +2 -0
  12. package/dist/src/daemon/client/client.js +7 -0
  13. package/dist/src/daemon/message-types/get-plugin-capabilities.d.ts +5 -0
  14. package/dist/src/daemon/message-types/get-plugin-capabilities.js +11 -0
  15. package/dist/src/daemon/server/handle-get-plugin-capabilities.d.ts +2 -0
  16. package/dist/src/daemon/server/handle-get-plugin-capabilities.js +13 -0
  17. package/dist/src/daemon/server/project-graph-incremental-recomputation.js +1 -1
  18. package/dist/src/daemon/server/server.js +5 -0
  19. package/dist/src/native/index.d.ts +20 -0
  20. package/dist/src/native/native-bindings.js +1 -0
  21. package/dist/src/native/nx.wasm32-wasi.debug.wasm +0 -0
  22. package/dist/src/native/nx.wasm32-wasi.wasm +0 -0
  23. package/dist/src/project-graph/affected/locators/project-glob-changes.js +19 -26
  24. package/dist/src/project-graph/error-types.d.ts +5 -0
  25. package/dist/src/project-graph/error-types.js +44 -5
  26. package/dist/src/project-graph/file-utils.d.ts +2 -0
  27. package/dist/src/project-graph/file-utils.js +6 -1
  28. package/dist/src/project-graph/nx-deps-cache.d.ts +5 -2
  29. package/dist/src/project-graph/nx-deps-cache.js +4 -4
  30. package/dist/src/project-graph/plugins/get-plugins.d.ts +3 -1
  31. package/dist/src/project-graph/plugins/get-plugins.js +60 -82
  32. package/dist/src/project-graph/plugins/in-process-loader.d.ts +1 -1
  33. package/dist/src/project-graph/plugins/in-process-loader.js +1 -4
  34. package/dist/src/project-graph/plugins/isolation/index.d.ts +1 -1
  35. package/dist/src/project-graph/plugins/isolation/index.js +3 -1
  36. package/dist/src/project-graph/plugins/isolation/isolated-plugin.d.ts +8 -0
  37. package/dist/src/project-graph/plugins/isolation/isolated-plugin.js +40 -5
  38. package/dist/src/project-graph/plugins/isolation/load-isolated-plugin.d.ts +10 -1
  39. package/dist/src/project-graph/plugins/isolation/load-isolated-plugin.js +67 -17
  40. package/dist/src/project-graph/plugins/loaded-nx-plugin.d.ts +2 -0
  41. package/dist/src/project-graph/plugins/loaded-nx-plugin.js +9 -0
  42. package/dist/src/project-graph/plugins/nx-plugin-capabilities.d.ts +6 -0
  43. package/dist/src/project-graph/plugins/nx-plugin-capabilities.js +28 -0
  44. package/dist/src/project-graph/plugins/tasks-execution-hooks.js +12 -0
  45. package/dist/src/project-graph/project-graph.js +41 -42
  46. package/dist/src/utils/ab-testing.d.ts +1 -1
  47. package/dist/src/utils/ab-testing.js +0 -4
  48. package/dist/src/utils/handle-errors.js +5 -43
  49. package/dist/src/utils/plugins/plugin-capabilities.js +7 -2
  50. package/dist/src/utils/serialize-overrides-into-command-line.js +2 -4
  51. package/package.json +11 -12
@@ -7,6 +7,7 @@ exports.getPluginsSeparated = getPluginsSeparated;
7
7
  exports.getOnlyDefaultPlugins = getOnlyDefaultPlugins;
8
8
  exports.getPluginsIfLoadedOrLoading = getPluginsIfLoadedOrLoading;
9
9
  exports.cleanupPlugins = cleanupPlugins;
10
+ exports.capabilitiesOfConfiguredPlugins = capabilitiesOfConfiguredPlugins;
10
11
  exports.reasonToError = reasonToError;
11
12
  const node_path_1 = require("node:path");
12
13
  const angular_json_1 = require("../../adapter/angular-json");
@@ -15,6 +16,9 @@ const workspace_root_1 = require("../../utils/workspace-root");
15
16
  const in_process_loader_1 = require("./in-process-loader");
16
17
  const isolation_1 = require("./isolation");
17
18
  const resolve_plugin_1 = require("./resolve-plugin");
19
+ const is_on_daemon_1 = require("../../daemon/is-on-daemon");
20
+ const client_1 = require("../../daemon/client/client");
21
+ const nx_plugin_capabilities_1 = require("./nx-plugin-capabilities");
18
22
  const enabled_1 = require("./isolation/enabled");
19
23
  const isolated_plugin_1 = require("./isolation/isolated-plugin");
20
24
  const sandbox_socket_hint_1 = require("../../daemon/sandbox-socket-hint");
@@ -26,10 +30,15 @@ const transpiler_1 = require("./transpiler");
26
30
  * Stuff for specified NX Plugins.
27
31
  */
28
32
  let currentPluginsConfigurationHash;
29
- let loadedPlugins;
30
33
  let cachedSeparatedPlugins;
31
34
  let pendingPluginsPromise;
32
- let cleanupSpecifiedPlugins;
35
+ /** Workers aren't disposed here; the next load's `wantPlugins` sweeps the ones it no longer names. */
36
+ function forgetSpecifiedPlugins() {
37
+ if ((0, transpiler_1.pluginTranspilerIsRegistered)()) {
38
+ (0, transpiler_1.cleanupPluginTSTranspiler)();
39
+ }
40
+ pendingPluginsPromise = undefined;
41
+ }
33
42
  // In-flight separated-plugins load, tagged with its hash. Two roles: a
34
43
  // concurrent caller for the same set shares this load instead of racing a
35
44
  // second one, and it gates the cache commit — a load writes the cache only if
@@ -74,11 +83,10 @@ const loadingMethod = async (plugin, root, index) => {
74
83
  if (!(0, enabled_1.isIsolationEnabled)() || isolationRefusedInThisProcess) {
75
84
  return (0, in_process_loader_1.loadNxPlugin)(plugin, root, index);
76
85
  }
77
- const [isolatedPlugin, cleanup] = await (0, isolation_1.loadIsolatedNxPlugin)(plugin, root, index);
78
86
  // Awaited here rather than handed on, because the worker failure surfaces on
79
87
  // this promise and the fallback has to happen before the caller sees it.
80
88
  try {
81
- return [Promise.resolve(await isolatedPlugin), cleanup];
89
+ return await (0, isolation_1.loadIsolatedNxPlugin)(plugin, root, index);
82
90
  }
83
91
  catch (e) {
84
92
  // Proof, kept separate from policy. The errno the worker saw is what makes
@@ -92,7 +100,6 @@ const loadingMethod = async (plugin, root, index) => {
92
100
  !((provenRefusal && (0, native_1.isAiAgent)()) || (0, is_sandbox_1.isSandbox)())) {
93
101
  throw e;
94
102
  }
95
- cleanup();
96
103
  // Read and set in one synchronous step. Concurrently loaded plugins each
97
104
  // arrive here with their own failure, so testing the latch after setting it
98
105
  // is what keeps the advice to one copy.
@@ -152,13 +159,9 @@ async function getPluginsSeparated(nxJson, root = workspace_root_1.workspaceRoot
152
159
  if (pendingSeparatedPlugins?.hash === pluginsConfigurationHash) {
153
160
  return pendingSeparatedPlugins.promise;
154
161
  }
155
- // Plugins config changed (e.g. `nx add @nx/maven` updated nx.json). The
156
- // cached SeparatedPlugins is invalidated by the early-return above, but
157
- // pendingPluginsPromise — the in-flight load — would otherwise be reused
158
- // by the `??=` below and serve the previous plugin set forever. Tear
159
- // down the old workers and force a fresh load.
160
- cleanupSpecifiedPlugins?.();
161
- pendingPluginsPromise = undefined;
162
+ // Plugins config changed (e.g. `nx add @nx/maven`): drop the in-flight load,
163
+ // or the `??=` below would serve the previous plugin set forever.
164
+ forgetSpecifiedPlugins();
162
165
  const loadPromise = (async () => {
163
166
  const results = await Promise.allSettled([
164
167
  getOnlyDefaultPlugins(root),
@@ -188,7 +191,6 @@ async function getPluginsSeparated(nxJson, root = workspace_root_1.workspaceRoot
188
191
  if (pendingSeparatedPlugins?.promise === loadPromise) {
189
192
  cachedSeparatedPlugins = separatedPlugins;
190
193
  currentPluginsConfigurationHash = pluginsConfigurationHash;
191
- loadedPlugins = specifiedPlugins.concat(defaultPlugins);
192
194
  }
193
195
  return separatedPlugins;
194
196
  })();
@@ -212,7 +214,6 @@ async function getPluginsSeparated(nxJson, root = workspace_root_1.workspaceRoot
212
214
  */
213
215
  let loadedDefaultPlugins;
214
216
  let loadedDefaultPluginsHash;
215
- let cleanupDefaultPlugins;
216
217
  let pendingDefaultPluginPromise;
217
218
  async function getOnlyDefaultPlugins(root = workspace_root_1.workspaceRoot) {
218
219
  const hash = root;
@@ -220,19 +221,15 @@ async function getOnlyDefaultPlugins(root = workspace_root_1.workspaceRoot) {
220
221
  if (loadedDefaultPlugins && hash === loadedDefaultPluginsHash) {
221
222
  return loadedDefaultPlugins;
222
223
  }
223
- // Cleanup current plugins before loading new ones
224
- if (cleanupDefaultPlugins) {
225
- cleanupDefaultPlugins();
224
+ const loadPromise = (pendingDefaultPluginPromise ??=
225
+ loadDefaultNxPlugins(workspace_root_1.workspaceRoot));
226
+ const result = await loadPromise;
227
+ // Commit only while this is still the registered load, so a set released while
228
+ // it was loading is not handed back out as the current one.
229
+ if (pendingDefaultPluginPromise === loadPromise) {
230
+ loadedDefaultPlugins = result;
231
+ loadedDefaultPluginsHash = hash;
226
232
  }
227
- pendingDefaultPluginPromise ??= loadDefaultNxPlugins(workspace_root_1.workspaceRoot);
228
- const [result, cleanupFn] = await pendingDefaultPluginPromise;
229
- cleanupDefaultPlugins = () => {
230
- loadedDefaultPlugins = undefined;
231
- pendingDefaultPluginPromise = undefined;
232
- cleanupFn();
233
- };
234
- loadedDefaultPlugins = result;
235
- loadedDefaultPluginsHash = hash;
236
233
  return result;
237
234
  }
238
235
  /**
@@ -252,9 +249,9 @@ function getPluginsIfLoadedOrLoading() {
252
249
  return Promise.resolve(separated).then(({ specifiedPlugins, defaultPlugins }) => specifiedPlugins.concat(defaultPlugins));
253
250
  }
254
251
  function cleanupPlugins() {
255
- cleanupSpecifiedPlugins?.();
256
- cleanupDefaultPlugins?.();
257
- pendingPluginsPromise = undefined;
252
+ (0, isolation_1.disposeIsolatedPlugins)();
253
+ forgetSpecifiedPlugins();
254
+ loadedDefaultPlugins = undefined;
258
255
  pendingDefaultPluginPromise = undefined;
259
256
  cachedSeparatedPlugins = undefined;
260
257
  // Drop the in-flight load too: clearing the marker flips its commit gate to
@@ -264,19 +261,38 @@ function cleanupPlugins() {
264
261
  /**
265
262
  * Stuff for generic loading
266
263
  */
264
+ function pluginLabel(plugin) {
265
+ return typeof plugin === 'string' ? plugin : plugin.plugin;
266
+ }
267
+ async function loadPlugins(loader, pluginConfigurations, root, assignIndexes) {
268
+ const loads = pluginConfigurations.map((plugin, index) => ({
269
+ plugin,
270
+ index: assignIndexes ? index : undefined,
271
+ }));
272
+ (0, isolation_1.wantPlugins)(loader, loads, root);
273
+ return Promise.allSettled(loads.map(async ({ plugin, index }) => {
274
+ const label = pluginLabel(plugin);
275
+ performance.mark(`Load Nx Plugin: ${label} - start`);
276
+ const res = await (0, exports.loadingMethod)(plugin, root, index);
277
+ performance.mark(`Load Nx Plugin: ${label} - end`);
278
+ performance.measure(`Load Nx Plugin: ${label}`, `Load Nx Plugin: ${label} - start`, `Load Nx Plugin: ${label} - end`);
279
+ return res;
280
+ }));
281
+ }
282
+ async function capabilitiesOfConfiguredPlugins(nxJson, root = workspace_root_1.workspaceRoot) {
283
+ if (!(0, is_on_daemon_1.isOnDaemon)() && (0, client_1.isDaemonEnabled)()) {
284
+ return client_1.daemonClient.getPluginCapabilities();
285
+ }
286
+ const recorded = (0, nx_plugin_capabilities_1.capabilitiesOfNxPluginsReadFromCache)();
287
+ if (recorded) {
288
+ return recorded;
289
+ }
290
+ return (await getPlugins(nxJson, root)).map((plugin) => plugin.capabilities());
291
+ }
267
292
  async function loadDefaultNxPlugins(root = workspace_root_1.workspaceRoot) {
268
293
  performance.mark('loadDefaultNxPlugins:start');
269
294
  const plugins = getDefaultPlugins(root);
270
- const cleanupFunctions = [];
271
- const results = await Promise.allSettled(plugins.map(async (plugin) => {
272
- performance.mark(`Load Nx Plugin: ${plugin} - start`);
273
- const [loadedPluginPromise, cleanup] = await (0, exports.loadingMethod)(plugin, root);
274
- cleanupFunctions.push(cleanup);
275
- const res = await loadedPluginPromise;
276
- performance.mark(`Load Nx Plugin: ${plugin} - end`);
277
- performance.measure(`Load Nx Plugin: ${plugin}`, `Load Nx Plugin: ${plugin} - start`, `Load Nx Plugin: ${plugin} - end`);
278
- return res;
279
- }));
295
+ const results = await loadPlugins('default', plugins, root, false);
280
296
  const defaultPluginResults = [];
281
297
  const errors = [];
282
298
  for (let i = 0; i < results.length; i++) {
@@ -292,52 +308,25 @@ async function loadDefaultNxPlugins(root = workspace_root_1.workspaceRoot) {
292
308
  }
293
309
  }
294
310
  if (errors.length > 0) {
295
- for (const fn of cleanupFunctions) {
296
- fn();
297
- }
311
+ // Cleared so the next call retries instead of re-awaiting this rejection.
312
+ pendingDefaultPluginPromise = undefined;
298
313
  const errorMessage = errors
299
314
  .map((e) => ` - ${e.pluginName}: ${e.error.message}`)
300
315
  .join('\n');
301
316
  throw new AggregateError(errors.map((e) => e.error), `Failed to load ${errors.length} default Nx plugin(s):\n${errorMessage}`);
302
317
  }
303
- const ret = [
304
- defaultPluginResults,
305
- () => {
306
- for (const fn of cleanupFunctions) {
307
- fn();
308
- }
309
- if ((0, transpiler_1.pluginTranspilerIsRegistered)()) {
310
- (0, transpiler_1.cleanupPluginTSTranspiler)();
311
- }
312
- },
313
- ];
314
318
  performance.mark('loadDefaultNxPlugins:end');
315
319
  performance.measure('loadDefaultNxPlugins', 'loadDefaultNxPlugins:start', 'loadDefaultNxPlugins:end');
316
- return ret;
320
+ return defaultPluginResults;
317
321
  }
318
322
  async function loadSpecifiedNxPlugins(pluginsConfigurations, root = workspace_root_1.workspaceRoot) {
319
- // Returning existing plugins is handled by getPlugins,
320
- // so, if we are here and there are existing plugins, they are stale
321
- if (cleanupSpecifiedPlugins) {
322
- cleanupSpecifiedPlugins();
323
- }
324
323
  performance.mark('loadSpecifiedNxPlugins:start');
325
324
  pluginsConfigurations ??= [];
326
325
  // Drop the cached workspace-layout snapshot local-plugin resolution uses:
327
326
  // in a long-lived daemon it can predate a newly added local plugin and
328
327
  // resolve it to the workspace root. Runs only when the plugin set changed.
329
328
  (0, resolve_plugin_1.resetResolvePluginCache)();
330
- const cleanupFunctions = [];
331
- const results = await Promise.allSettled(pluginsConfigurations.map(async (plugin, index) => {
332
- const pluginPath = typeof plugin === 'string' ? plugin : plugin.plugin;
333
- performance.mark(`Load Nx Plugin: ${pluginPath} - start`);
334
- const [loadedPluginPromise, cleanup] = await (0, exports.loadingMethod)(plugin, root, index);
335
- cleanupFunctions.push(cleanup);
336
- const res = await loadedPluginPromise;
337
- performance.mark(`Load Nx Plugin: ${pluginPath} - end`);
338
- performance.measure(`Load Nx Plugin: ${pluginPath}`, `Load Nx Plugin: ${pluginPath} - start`, `Load Nx Plugin: ${pluginPath} - end`);
339
- return res;
340
- }));
329
+ const results = await loadPlugins('specified', pluginsConfigurations, root, true);
341
330
  performance.mark('loadSpecifiedNxPlugins:end');
342
331
  performance.measure('loadSpecifiedNxPlugins', 'loadSpecifiedNxPlugins:start', 'loadSpecifiedNxPlugins:end');
343
332
  const plugins = [];
@@ -357,23 +346,12 @@ async function loadSpecifiedNxPlugins(pluginsConfigurations, root = workspace_ro
357
346
  }
358
347
  }
359
348
  if (errors.length > 0) {
360
- for (const fn of cleanupFunctions) {
361
- fn();
362
- }
349
+ // Wants are not retracted: a newer load may own the declaration by now.
363
350
  const errorMessage = errors
364
351
  .map((e) => ` - ${e.pluginName}: ${e.error.message}`)
365
352
  .join('\n');
366
353
  throw new AggregateError(errors.map((e) => e.error), `Failed to load ${errors.length} Nx plugin(s):\n${errorMessage}`);
367
354
  }
368
- cleanupSpecifiedPlugins = () => {
369
- for (const fn of cleanupFunctions) {
370
- fn();
371
- }
372
- if ((0, transpiler_1.pluginTranspilerIsRegistered)()) {
373
- (0, transpiler_1.cleanupPluginTSTranspiler)();
374
- }
375
- pendingPluginsPromise = undefined;
376
- };
377
355
  return plugins;
378
356
  }
379
357
  function reasonToError(reason) {
@@ -6,5 +6,5 @@ export declare function readPluginPackageJson(pluginName: string, projects: Reco
6
6
  path: string;
7
7
  json: PackageJson;
8
8
  };
9
- export declare function loadNxPlugin(plugin: PluginConfiguration, root: string, index?: number): readonly [Promise<LoadedNxPlugin>, () => void];
9
+ export declare function loadNxPlugin(plugin: PluginConfiguration, root: string, index?: number): Promise<LoadedNxPlugin>;
10
10
  export declare function loadNxPluginAsync(pluginConfiguration: PluginConfiguration, paths: string[], root: string, index?: number): Promise<LoadedNxPlugin>;
@@ -38,10 +38,7 @@ function readPluginPackageJson(pluginName, projects, paths = (0, installation_di
38
38
  }
39
39
  }
40
40
  function loadNxPlugin(plugin, root, index) {
41
- return [
42
- loadNxPluginAsync(plugin, (0, installation_directory_1.getNxRequirePaths)(root), root, index),
43
- () => { },
44
- ];
41
+ return loadNxPluginAsync(plugin, (0, installation_directory_1.getNxRequirePaths)(root), root, index);
45
42
  }
46
43
  async function loadNxPluginAsync(pluginConfiguration, paths, root, index) {
47
44
  const moduleName = typeof pluginConfiguration === 'string'
@@ -1 +1 @@
1
- export { loadIsolatedNxPlugin } from './load-isolated-plugin';
1
+ export { disposeIsolatedPlugins, loadIsolatedNxPlugin, wantPlugins, } from './load-isolated-plugin';
@@ -1,5 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.loadIsolatedNxPlugin = void 0;
3
+ exports.wantPlugins = exports.loadIsolatedNxPlugin = exports.disposeIsolatedPlugins = void 0;
4
4
  var load_isolated_plugin_1 = require("./load-isolated-plugin");
5
+ Object.defineProperty(exports, "disposeIsolatedPlugins", { enumerable: true, get: function () { return load_isolated_plugin_1.disposeIsolatedPlugins; } });
5
6
  Object.defineProperty(exports, "loadIsolatedNxPlugin", { enumerable: true, get: function () { return load_isolated_plugin_1.loadIsolatedNxPlugin; } });
7
+ Object.defineProperty(exports, "wantPlugins", { enumerable: true, get: function () { return load_isolated_plugin_1.wantPlugins; } });
@@ -4,6 +4,7 @@ import type { PluginConfiguration } from '../../../config/nx-json';
4
4
  import type { ProjectGraph } from '../../../config/project-graph';
5
5
  import type { RawProjectGraphDependency } from '../../project-graph-builder';
6
6
  import { LoadedNxPlugin } from '../loaded-nx-plugin';
7
+ import type { NxPluginCapabilities } from '../nx-plugin-capabilities';
7
8
  import type { CreateDependenciesContext, CreateMetadataContext, CreateNodesContext, CreateNodesResult, PostTasksExecutionContext, PreTasksExecutionContext, ProjectsMetadata } from '../public-api';
8
9
  import type { PluginWorkerLoadResult } from './messaging';
9
10
  import { Hook, Phase } from './plugin-lifecycle-manager';
@@ -26,6 +27,7 @@ export declare class IsolatedPlugin implements LoadedNxPlugin {
26
27
  private worker;
27
28
  private socket;
28
29
  private _alive;
30
+ private _released;
29
31
  private _connectPromise;
30
32
  private txId;
31
33
  private pendingCount;
@@ -41,6 +43,7 @@ export declare class IsolatedPlugin implements LoadedNxPlugin {
41
43
  */
42
44
  static load(plugin: PluginConfiguration, root: string, index?: number): Promise<IsolatedPlugin>;
43
45
  private constructor();
46
+ capabilities(): NxPluginCapabilities;
44
47
  private spawnAndConnect;
45
48
  /**
46
49
  * Drops the worker from service without judging why. The next hook call
@@ -66,6 +69,11 @@ export declare class IsolatedPlugin implements LoadedNxPlugin {
66
69
  private shutdownIfInactive;
67
70
  setWorkerEnv(env: Record<string, string>): Promise<void>;
68
71
  notifyPhaseAborted(phase: Phase, lastCompletedHook: Hook): void;
72
+ /**
73
+ * Like `shutdown`, but for good: a later hook call still answers, then shuts
74
+ * the worker back down instead of leaving a respawned one running.
75
+ */
76
+ dispose(): void;
69
77
  shutdown(): void;
70
78
  private registerProcessMetrics;
71
79
  }
@@ -44,9 +44,16 @@ class IsolatedPlugin {
44
44
  const moduleName = typeof plugin === 'string' ? plugin : plugin.plugin;
45
45
  const { name, pluginPath, shouldRegisterTSTranspiler } = await (0, resolve_plugin_1.resolveNxPlugin)(moduleName, root, (0, installation_directory_1.getNxRequirePaths)(root));
46
46
  const instance = new IsolatedPlugin(plugin, root, name, pluginPath, shouldRegisterTSTranspiler, index);
47
- const loadResult = await instance.spawnAndConnect();
48
- instance.setupHooks(loadResult);
49
- return instance;
47
+ try {
48
+ const loadResult = await instance.spawnAndConnect();
49
+ instance.setupHooks(loadResult);
50
+ return instance;
51
+ }
52
+ catch (e) {
53
+ // A timed-out worker is still running, and the caller is about to drop this instance.
54
+ instance.shutdown();
55
+ throw e;
56
+ }
50
57
  }
51
58
  constructor(plugin, root, name, pluginPath, shouldRegisterTSTranspiler, index) {
52
59
  this.index = index;
@@ -54,6 +61,7 @@ class IsolatedPlugin {
54
61
  this.worker = null;
55
62
  this.socket = null;
56
63
  this._alive = false;
64
+ this._released = false;
57
65
  this._connectPromise = null;
58
66
  this.txId = 0;
59
67
  this.pendingCount = 0;
@@ -91,6 +99,15 @@ class IsolatedPlugin {
91
99
  this.pluginPath = pluginPath;
92
100
  this.shouldRegisterTSTranspiler = shouldRegisterTSTranspiler;
93
101
  }
102
+ capabilities() {
103
+ return {
104
+ createNodesPattern: this.createNodes?.[0],
105
+ hasCreateDependencies: !!this.createDependencies,
106
+ hasCreateMetadata: !!this.createMetadata,
107
+ hasPreTasksExecution: !!this.preTasksExecution,
108
+ hasPostTasksExecution: !!this.postTasksExecution,
109
+ };
110
+ }
94
111
  async spawnAndConnect() {
95
112
  const { worker, socket } = await startPluginWorker(this.name);
96
113
  this.worker = worker;
@@ -215,7 +232,15 @@ class IsolatedPlugin {
215
232
  const shutdown = (hookName) => this.shutdownIfInactive(hookName);
216
233
  const wrap = (hook, hookFn) => this.lifecycle.wrapHook(hook, async (...args) => {
217
234
  await this.ensureAlive();
218
- return hookFn(...args);
235
+ try {
236
+ return await hookFn(...args);
237
+ }
238
+ finally {
239
+ // A released plugin still answers, then shuts its worker straight back down.
240
+ if (this._released) {
241
+ shutdown(hook);
242
+ }
243
+ }
219
244
  }, () => shutdown(hook));
220
245
  if (loadResult.createNodesPattern) {
221
246
  this.createNodes = [
@@ -339,8 +364,18 @@ class IsolatedPlugin {
339
364
  this.shutdownIfInactive(lastCompletedHook);
340
365
  }
341
366
  }
367
+ /**
368
+ * Like `shutdown`, but for good: a later hook call still answers, then shuts
369
+ * the worker back down instead of leaving a respawned one running.
370
+ */
371
+ dispose() {
372
+ this._released = true;
373
+ this.shutdown();
374
+ }
342
375
  shutdown() {
343
- if (!this._alive)
376
+ // Not `_alive`: that is only set once the worker answers a load, so gating
377
+ // on it would leave a worker that never got that far running.
378
+ if (!this.worker && !this.socket)
344
379
  return;
345
380
  this._alive = false;
346
381
  this._connectPromise = null;
@@ -1,3 +1,12 @@
1
1
  import { PluginConfiguration } from '../../../config/nx-json';
2
2
  import type { LoadedNxPlugin } from '../loaded-nx-plugin';
3
- export declare function loadIsolatedNxPlugin(plugin: PluginConfiguration, root: string, index?: number): Promise<[Promise<LoadedNxPlugin>, () => void]>;
3
+ /**
4
+ * Declares a loader's plugins and disposes any no loader wants. Call before loading.
5
+ * Anything that makes a running worker unfit for reuse must be in its cache key.
6
+ */
7
+ export declare function wantPlugins(loader: string, plugins: Array<{
8
+ plugin: PluginConfiguration;
9
+ index?: number;
10
+ }>, root: string): void;
11
+ export declare function disposeIsolatedPlugins(): void;
12
+ export declare function loadIsolatedNxPlugin(plugin: PluginConfiguration, root: string, index?: number): Promise<LoadedNxPlugin>;
@@ -1,24 +1,74 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.wantPlugins = wantPlugins;
4
+ exports.disposeIsolatedPlugins = disposeIsolatedPlugins;
3
5
  exports.loadIsolatedNxPlugin = loadIsolatedNxPlugin;
4
6
  const isolated_plugin_1 = require("./isolated-plugin");
5
- const isolatedPluginCache = (global['isolatedPluginCache'] ??= new Map());
6
- async function loadIsolatedNxPlugin(plugin, root, index) {
7
- const cacheKey = JSON.stringify({ plugin, root });
8
- if (isolatedPluginCache.has(cacheKey)) {
9
- return [isolatedPluginCache.get(cacheKey), () => { }];
10
- }
11
- const pluginPromise = isolated_plugin_1.IsolatedPlugin.load(plugin, root, index).catch((err) => {
12
- // Remove failed entries from cache so subsequent calls can retry
13
- isolatedPluginCache.delete(cacheKey);
7
+ const loadedPlugins = (global['nxLoadedPlugins'] ??= new Map());
8
+ /**
9
+ * Keys each loader last asked for; a sweep keeps their union. Global like
10
+ * `loadedPlugins`, or one copy of Nx would sweep another's workers.
11
+ */
12
+ const wantedBy = (global['nxWantedPlugins'] ??=
13
+ new Map());
14
+ /**
15
+ * Declares a loader's plugins and disposes any no loader wants. Call before loading.
16
+ * Anything that makes a running worker unfit for reuse must be in its cache key.
17
+ */
18
+ function wantPlugins(loader, plugins, root) {
19
+ wantedBy.set(loader, new Set(plugins.map(({ plugin, index }) => getCacheKey(plugin, root, index))));
20
+ sweep();
21
+ }
22
+ function disposeIsolatedPlugins() {
23
+ wantedBy.clear();
24
+ sweep();
25
+ }
26
+ function loadIsolatedNxPlugin(plugin, root, index) {
27
+ const cacheKey = getCacheKey(plugin, root, index);
28
+ return (loadedPlugins.get(cacheKey) ??
29
+ register(cacheKey, isolated_plugin_1.IsolatedPlugin.load(plugin, root, index)));
30
+ }
31
+ function register(cacheKey, loading) {
32
+ const entry = loading.then((plugin) => {
33
+ // Swept while loading: nothing names it any more.
34
+ if (!isWanted(cacheKey)) {
35
+ plugin.dispose();
36
+ }
37
+ return plugin;
38
+ }, (err) => {
39
+ forget(cacheKey, entry);
14
40
  throw err;
15
41
  });
16
- isolatedPluginCache.set(cacheKey, pluginPromise);
17
- const cleanup = async () => {
18
- const instancePromise = isolatedPluginCache.get(cacheKey);
19
- isolatedPluginCache.delete(cacheKey);
20
- const instance = await instancePromise;
21
- instance?.shutdown();
22
- };
23
- return [pluginPromise, cleanup];
42
+ if (isWanted(cacheKey)) {
43
+ loadedPlugins.set(cacheKey, entry);
44
+ }
45
+ return entry;
46
+ }
47
+ function sweep() {
48
+ for (const [cacheKey, entry] of [...loadedPlugins]) {
49
+ if (isWanted(cacheKey)) {
50
+ continue;
51
+ }
52
+ loadedPlugins.delete(cacheKey);
53
+ entry.then((plugin) => plugin.dispose(),
54
+ // No worker to dispose; the caller reports the rejection.
55
+ () => { });
56
+ }
57
+ }
58
+ function isWanted(cacheKey) {
59
+ for (const keys of wantedBy.values()) {
60
+ if (keys.has(cacheKey)) {
61
+ return true;
62
+ }
63
+ }
64
+ return false;
65
+ }
66
+ /** Includes the index: a plugin carries its nx.json position, which exclusions and errors point at. */
67
+ function getCacheKey(plugin, root, index) {
68
+ return JSON.stringify({ plugin, root, index });
69
+ }
70
+ function forget(cacheKey, entry) {
71
+ if (loadedPlugins.get(cacheKey) === entry) {
72
+ loadedPlugins.delete(cacheKey);
73
+ }
24
74
  }
@@ -3,6 +3,7 @@ import { type PluginConfiguration } from '../../config/nx-json';
3
3
  import type { RawProjectGraphDependency } from '../project-graph-builder';
4
4
  import type { CreateDependenciesContext, CreateMetadataContext, CreateNodesContext, CreateNodesResult, NxPlugin, PreTasksExecutionContext, ProjectsMetadata } from './public-api';
5
5
  import { type MaybeStubbedPostTasksExecutionContext } from './task-results-stub';
6
+ import type { NxPluginCapabilities } from './nx-plugin-capabilities';
6
7
  /**
7
8
  * NOTE: Avoid using `import type` with this class. It causes issues with
8
9
  * jest's module resolution when running tests in projects that import
@@ -39,4 +40,5 @@ export declare class LoadedNxPlugin {
39
40
  */
40
41
  setWorkerEnv?(env: Record<string, string>): Promise<void>;
41
42
  constructor(plugin: NxPlugin, pluginDefinition: PluginConfiguration, index?: number);
43
+ capabilities(): NxPluginCapabilities;
42
44
  }
@@ -104,5 +104,14 @@ class LoadedNxPlugin {
104
104
  this.postTasksExecution = async (context) => plugin.postTasksExecution(this.options, await (0, task_results_stub_1.rehydrateTerminalOutputs)(context));
105
105
  }
106
106
  }
107
+ capabilities() {
108
+ return {
109
+ createNodesPattern: this.createNodes?.[0],
110
+ hasCreateDependencies: !!this.createDependencies,
111
+ hasCreateMetadata: !!this.createMetadata,
112
+ hasPreTasksExecution: !!this.preTasksExecution,
113
+ hasPostTasksExecution: !!this.postTasksExecution,
114
+ };
115
+ }
107
116
  }
108
117
  exports.LoadedNxPlugin = LoadedNxPlugin;
@@ -0,0 +1,6 @@
1
+ import { type CachedPluginCapabilities, NxPluginCapabilities as NxPluginCapabilitiesStore } from '../../native';
2
+ export type NxPluginCapabilities = CachedPluginCapabilities;
3
+ export declare function getNxPluginCapabilitiesStore(): NxPluginCapabilitiesStore | null;
4
+ export declare function noteGraphReadFromCache(computedAt: number | undefined): void;
5
+ /** Null means "load them", never "no plugin registers anything". */
6
+ export declare function capabilitiesOfNxPluginsReadFromCache(): NxPluginCapabilities[] | null;
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getNxPluginCapabilitiesStore = getNxPluginCapabilitiesStore;
4
+ exports.noteGraphReadFromCache = noteGraphReadFromCache;
5
+ exports.capabilitiesOfNxPluginsReadFromCache = capabilitiesOfNxPluginsReadFromCache;
6
+ const native_1 = require("../../native");
7
+ const db_connection_1 = require("../../utils/db-connection");
8
+ let store;
9
+ function getNxPluginCapabilitiesStore() {
10
+ // The database is not part of the WASM build.
11
+ if (native_1.IS_WASM) {
12
+ return null;
13
+ }
14
+ // Checkout-local: the rows describe this checkout's graph.
15
+ store ??= new native_1.NxPluginCapabilities((0, db_connection_1.getLocalDbConnection)());
16
+ return store;
17
+ }
18
+ let computedAtOfGraphReadFromCache;
19
+ function noteGraphReadFromCache(computedAt) {
20
+ computedAtOfGraphReadFromCache = computedAt;
21
+ }
22
+ /** Null means "load them", never "no plugin registers anything". */
23
+ function capabilitiesOfNxPluginsReadFromCache() {
24
+ if (computedAtOfGraphReadFromCache === undefined) {
25
+ return null;
26
+ }
27
+ return (getNxPluginCapabilitiesStore()?.get(computedAtOfGraphReadFromCache) ?? null);
28
+ }
@@ -8,8 +8,17 @@ const is_on_daemon_1 = require("../../daemon/is-on-daemon");
8
8
  const client_1 = require("../../daemon/client/client");
9
9
  const workspace_root_1 = require("../../utils/workspace-root");
10
10
  const task_results_stub_1 = require("./task-results-stub");
11
+ const nx_plugin_capabilities_1 = require("./nx-plugin-capabilities");
12
+ /** True only when the cached graph's recorded rows show no plugin has `hook`; unknown is false. */
13
+ function knownThatNoPluginRegisters(hook) {
14
+ const recorded = (0, nx_plugin_capabilities_1.capabilitiesOfNxPluginsReadFromCache)();
15
+ return !!recorded && !recorded.some((capabilities) => capabilities[hook]);
16
+ }
11
17
  async function runPreTasksExecution(pluginContext) {
12
18
  if ((0, is_on_daemon_1.isOnDaemon)() || !(0, client_1.isDaemonEnabled)()) {
19
+ if (knownThatNoPluginRegisters('hasPreTasksExecution')) {
20
+ return [];
21
+ }
13
22
  performance.mark(`preTasksExecution:start`);
14
23
  const plugins = await (0, get_plugins_1.getPlugins)((0, nx_json_1.readNxJson)(pluginContext.workspaceRoot), pluginContext.workspaceRoot);
15
24
  const envs = await Promise.all(plugins
@@ -45,6 +54,9 @@ function applyProcessEnvs(envs) {
45
54
  }
46
55
  async function runPostTasksExecution(context) {
47
56
  if ((0, is_on_daemon_1.isOnDaemon)() || !(0, client_1.isDaemonEnabled)()) {
57
+ if (knownThatNoPluginRegisters('hasPostTasksExecution')) {
58
+ return;
59
+ }
48
60
  performance.mark(`postTasksExecution:start`);
49
61
  const plugins = await (0, get_plugins_1.getPlugins)((0, nx_json_1.readNxJson)(workspace_root_1.workspaceRoot));
50
62
  await Promise.all(plugins