diff --git a/docs/generated/devkit/NxPlugin.md b/docs/generated/devkit/NxPlugin.md index cbddea1d0ba9d..e344bb6ca5f04 100644 --- a/docs/generated/devkit/NxPlugin.md +++ b/docs/generated/devkit/NxPlugin.md @@ -2,4 +2,4 @@ Ƭ **NxPlugin**: [`NxPluginV2`](../../devkit/documents/NxPluginV2) -A plugin for Nx +A plugin which enhances the behavior of Nx diff --git a/docs/generated/devkit/NxPluginV2.md b/docs/generated/devkit/NxPluginV2.md index 45c17b48915f4..44052212378f3 100644 --- a/docs/generated/devkit/NxPluginV2.md +++ b/docs/generated/devkit/NxPluginV2.md @@ -2,7 +2,7 @@ Ƭ **NxPluginV2**\<`TOptions`\>: `Object` -A plugin for Nx which creates nodes and dependencies for the [ProjectGraph](../../devkit/documents/ProjectGraph) +A plugin which enhances the behavior of Nx #### Type parameters @@ -19,3 +19,5 @@ A plugin for Nx which creates nodes and dependencies for the [ProjectGraph](../. | `createNodes?` | [`CreateNodes`](../../devkit/documents/CreateNodes)\<`TOptions`\> | Provides a file pattern and function that retrieves configuration info from those files. e.g. { '**/\*.csproj': buildProjectsFromCsProjFile } **`Deprecated`\*\* Use createNodesV2 instead. In Nx 21 support for calling createNodes with a single file for the first argument will be removed. | | `createNodesV2?` | [`CreateNodesV2`](../../devkit/documents/CreateNodesV2)\<`TOptions`\> | Provides a file pattern and function that retrieves configuration info from those files. e.g. { '\*_/_.csproj': buildProjectsFromCsProjFiles } In Nx 21 createNodes will be replaced with this property. In Nx 22, this property will be removed. | | `name` | `string` | - | +| `postTasksExecution?` | [`PostTasksExecution`](../../devkit/documents/PostTasksExecution)\<`TOptions`\> | Provides a function to run after the Nx runs tasks | +| `preTasksExecution?` | [`PreTasksExecution`](../../devkit/documents/PreTasksExecution)\<`TOptions`\> | Provides a function to run before the Nx runs tasks | diff --git a/docs/generated/devkit/PostTasksExecution.md b/docs/generated/devkit/PostTasksExecution.md new file mode 100644 index 0000000000000..6845262489781 --- /dev/null +++ b/docs/generated/devkit/PostTasksExecution.md @@ -0,0 +1,24 @@ +# Type alias: PostTasksExecution\ + +Ƭ **PostTasksExecution**\<`TOptions`\>: (`options`: `TOptions` \| `undefined`, `context`: [`PostTasksExecutionContext`](../../devkit/documents/PostTasksExecutionContext)) => `void` \| `Promise`\<`void`\> + +#### Type parameters + +| Name | Type | +| :--------- | :-------- | +| `TOptions` | `unknown` | + +#### Type declaration + +▸ (`options`, `context`): `void` \| `Promise`\<`void`\> + +##### Parameters + +| Name | Type | +| :-------- | :------------------------------------------------------------------------------ | +| `options` | `TOptions` \| `undefined` | +| `context` | [`PostTasksExecutionContext`](../../devkit/documents/PostTasksExecutionContext) | + +##### Returns + +`void` \| `Promise`\<`void`\> diff --git a/docs/generated/devkit/PostTasksExecutionContext.md b/docs/generated/devkit/PostTasksExecutionContext.md new file mode 100644 index 0000000000000..503281696d5c9 --- /dev/null +++ b/docs/generated/devkit/PostTasksExecutionContext.md @@ -0,0 +1,11 @@ +# Type alias: PostTasksExecutionContext + +Ƭ **PostTasksExecutionContext**: `Object` + +#### Type declaration + +| Name | Type | +| :-------------------- | :------------------------------------------------------------------ | +| `nxJsonConfiguration` | [`NxJsonConfiguration`](../../devkit/documents/NxJsonConfiguration) | +| `taskResults` | [`TaskResults`](../../devkit/documents/TaskResults) | +| `workspaceRoot` | `string` | diff --git a/docs/generated/devkit/PreTasksExecution.md b/docs/generated/devkit/PreTasksExecution.md new file mode 100644 index 0000000000000..68726afe7e23b --- /dev/null +++ b/docs/generated/devkit/PreTasksExecution.md @@ -0,0 +1,24 @@ +# Type alias: PreTasksExecution\ + +Ƭ **PreTasksExecution**\<`TOptions`\>: (`options`: `TOptions` \| `undefined`, `context`: [`PreTasksExecutionContext`](../../devkit/documents/PreTasksExecutionContext)) => `void` \| `Promise`\<`void`\> + +#### Type parameters + +| Name | Type | +| :--------- | :-------- | +| `TOptions` | `unknown` | + +#### Type declaration + +▸ (`options`, `context`): `void` \| `Promise`\<`void`\> + +##### Parameters + +| Name | Type | +| :-------- | :---------------------------------------------------------------------------- | +| `options` | `TOptions` \| `undefined` | +| `context` | [`PreTasksExecutionContext`](../../devkit/documents/PreTasksExecutionContext) | + +##### Returns + +`void` \| `Promise`\<`void`\> diff --git a/docs/generated/devkit/PreTasksExecutionContext.md b/docs/generated/devkit/PreTasksExecutionContext.md new file mode 100644 index 0000000000000..41814a0b244ab --- /dev/null +++ b/docs/generated/devkit/PreTasksExecutionContext.md @@ -0,0 +1,10 @@ +# Type alias: PreTasksExecutionContext + +Ƭ **PreTasksExecutionContext**: `Object` + +#### Type declaration + +| Name | Type | +| :-------------------- | :------------------------------------------------------------------ | +| `nxJsonConfiguration` | [`NxJsonConfiguration`](../../devkit/documents/NxJsonConfiguration) | +| `workspaceRoot` | `string` | diff --git a/docs/generated/devkit/README.md b/docs/generated/devkit/README.md index f0c7e6bd6a250..04a24c5a48d4b 100644 --- a/docs/generated/devkit/README.md +++ b/docs/generated/devkit/README.md @@ -59,6 +59,7 @@ It only uses language primitives and immutable objects - [Task](../../devkit/documents/Task) - [TaskGraph](../../devkit/documents/TaskGraph) - [TaskHasher](../../devkit/documents/TaskHasher) +- [TaskResult](../../devkit/documents/TaskResult) - [Tree](../../devkit/documents/Tree) - [Workspace](../../devkit/documents/Workspace) @@ -86,6 +87,10 @@ It only uses language primitives and immutable objects - [NxPluginV2](../../devkit/documents/NxPluginV2) - [PackageManager](../../devkit/documents/PackageManager) - [PluginConfiguration](../../devkit/documents/PluginConfiguration) +- [PostTasksExecution](../../devkit/documents/PostTasksExecution) +- [PostTasksExecutionContext](../../devkit/documents/PostTasksExecutionContext) +- [PreTasksExecution](../../devkit/documents/PreTasksExecution) +- [PreTasksExecutionContext](../../devkit/documents/PreTasksExecutionContext) - [ProjectType](../../devkit/documents/ProjectType) - [ProjectsMetadata](../../devkit/documents/ProjectsMetadata) - [PromiseExecutor](../../devkit/documents/PromiseExecutor) @@ -94,6 +99,7 @@ It only uses language primitives and immutable objects - [StringChange](../../devkit/documents/StringChange) - [TargetDefaults](../../devkit/documents/TargetDefaults) - [TaskGraphExecutor](../../devkit/documents/TaskGraphExecutor) +- [TaskResults](../../devkit/documents/TaskResults) - [ToJSOptions](../../devkit/documents/ToJSOptions) - [WorkspaceJsonConfiguration](../../devkit/documents/WorkspaceJsonConfiguration) diff --git a/docs/generated/devkit/TaskResult.md b/docs/generated/devkit/TaskResult.md new file mode 100644 index 0000000000000..0e05e4e8c039e --- /dev/null +++ b/docs/generated/devkit/TaskResult.md @@ -0,0 +1,36 @@ +# Interface: TaskResult + +The result of a completed [Task](../../devkit/documents/Task) + +## Table of contents + +### Properties + +- [code](../../devkit/documents/TaskResult#code): number +- [status](../../devkit/documents/TaskResult#status): TaskStatus +- [task](../../devkit/documents/TaskResult#task): Task +- [terminalOutput](../../devkit/documents/TaskResult#terminaloutput): string + +## Properties + +### code + +• **code**: `number` + +--- + +### status + +• **status**: `TaskStatus` + +--- + +### task + +• **task**: [`Task`](../../devkit/documents/Task) + +--- + +### terminalOutput + +• `Optional` **terminalOutput**: `string` diff --git a/docs/generated/devkit/TaskResults.md b/docs/generated/devkit/TaskResults.md new file mode 100644 index 0000000000000..21c6fb2268d35 --- /dev/null +++ b/docs/generated/devkit/TaskResults.md @@ -0,0 +1,5 @@ +# Type alias: TaskResults + +Ƭ **TaskResults**: `Record`\<`string`, [`TaskResult`](../../devkit/documents/TaskResult)\> + +A map of [TaskResult](../../devkit/documents/TaskResult) keyed by the ID of the completed [Task](../../devkit/documents/Task)s diff --git a/docs/generated/packages/devkit/documents/nx_devkit.md b/docs/generated/packages/devkit/documents/nx_devkit.md index f0c7e6bd6a250..04a24c5a48d4b 100644 --- a/docs/generated/packages/devkit/documents/nx_devkit.md +++ b/docs/generated/packages/devkit/documents/nx_devkit.md @@ -59,6 +59,7 @@ It only uses language primitives and immutable objects - [Task](../../devkit/documents/Task) - [TaskGraph](../../devkit/documents/TaskGraph) - [TaskHasher](../../devkit/documents/TaskHasher) +- [TaskResult](../../devkit/documents/TaskResult) - [Tree](../../devkit/documents/Tree) - [Workspace](../../devkit/documents/Workspace) @@ -86,6 +87,10 @@ It only uses language primitives and immutable objects - [NxPluginV2](../../devkit/documents/NxPluginV2) - [PackageManager](../../devkit/documents/PackageManager) - [PluginConfiguration](../../devkit/documents/PluginConfiguration) +- [PostTasksExecution](../../devkit/documents/PostTasksExecution) +- [PostTasksExecutionContext](../../devkit/documents/PostTasksExecutionContext) +- [PreTasksExecution](../../devkit/documents/PreTasksExecution) +- [PreTasksExecutionContext](../../devkit/documents/PreTasksExecutionContext) - [ProjectType](../../devkit/documents/ProjectType) - [ProjectsMetadata](../../devkit/documents/ProjectsMetadata) - [PromiseExecutor](../../devkit/documents/PromiseExecutor) @@ -94,6 +99,7 @@ It only uses language primitives and immutable objects - [StringChange](../../devkit/documents/StringChange) - [TargetDefaults](../../devkit/documents/TargetDefaults) - [TaskGraphExecutor](../../devkit/documents/TaskGraphExecutor) +- [TaskResults](../../devkit/documents/TaskResults) - [ToJSOptions](../../devkit/documents/ToJSOptions) - [WorkspaceJsonConfiguration](../../devkit/documents/WorkspaceJsonConfiguration) diff --git a/packages/nx/src/command-line/release/publish.ts b/packages/nx/src/command-line/release/publish.ts index 2ad2b875d55d0..b30a176bfad4d 100644 --- a/packages/nx/src/command-line/release/publish.ts +++ b/packages/nx/src/command-line/release/publish.ts @@ -26,6 +26,11 @@ import { import { deepMergeJson } from './config/deep-merge-json'; import { filterReleaseGroups } from './config/filter-release-groups'; import { printConfigAndExit } from './utils/print-config'; +import { workspaceRoot } from '../../utils/workspace-root'; +import { + runPostTasksExecution, + runPreTasksExecution, +} from '../../project-graph/plugins/tasks-execution-hooks'; export interface PublishProjectsResult { [projectName: string]: { @@ -249,6 +254,10 @@ async function runPublishOnProjects( ].join('\n')}\n` ); } + await runPreTasksExecution({ + workspaceRoot, + nxJsonConfiguration: nxJson, + }); /** * Run the relevant nx-release-publish executor on each of the selected projects. @@ -276,6 +285,11 @@ async function runPublishOnProjects( code: taskData.code, }; } + await runPostTasksExecution({ + taskResults: commandResults, + workspaceRoot, + nxJsonConfiguration: nxJson, + }); return publishProjectsResult; } diff --git a/packages/nx/src/daemon/client/client.ts b/packages/nx/src/daemon/client/client.ts index c8581bd5b7f2e..d76206d99fa49 100644 --- a/packages/nx/src/daemon/client/client.ts +++ b/packages/nx/src/daemon/client/client.ts @@ -78,6 +78,16 @@ import { type HandleFlushSyncGeneratorChangesToDiskMessage, } from '../message-types/flush-sync-generator-changes-to-disk'; import { DelayedSpinner } from '../../utils/delayed-spinner'; +import { + PostTasksExecutionContext, + PreTasksExecutionContext, +} from '../../project-graph/plugins/public-api'; +import { + HandlePostTasksExecutionMessage, + HandlePreTasksExecutionMessage, + POST_TASKS_EXECUTION, + PRE_TASKS_EXECUTION, +} from '../message-types/run-tasks-execution-hooks'; const DAEMON_ENV_SETTINGS = { NX_PROJECT_GLOB_CACHE: 'false', @@ -435,6 +445,26 @@ export class DaemonClient { return this.sendToDaemonViaQueue(message); } + async runPreTasksExecution( + context: PreTasksExecutionContext + ): Promise { + const message: HandlePreTasksExecutionMessage = { + type: PRE_TASKS_EXECUTION, + context, + }; + return this.sendToDaemonViaQueue(message); + } + + async runPostTasksExecution( + context: PostTasksExecutionContext + ): Promise { + const message: HandlePostTasksExecutionMessage = { + type: POST_TASKS_EXECUTION, + context, + }; + return this.sendToDaemonViaQueue(message); + } + async isServerAvailable(): Promise { return new Promise((resolve) => { try { diff --git a/packages/nx/src/daemon/message-types/run-tasks-execution-hooks.ts b/packages/nx/src/daemon/message-types/run-tasks-execution-hooks.ts new file mode 100644 index 0000000000000..e647c96f41180 --- /dev/null +++ b/packages/nx/src/daemon/message-types/run-tasks-execution-hooks.ts @@ -0,0 +1,38 @@ +import type { + PostTasksExecutionContext, + PreTasksExecutionContext, +} from '../../project-graph/plugins'; + +export const PRE_TASKS_EXECUTION = 'PRE_TASKS_EXECUTION' as const; +export const POST_TASKS_EXECUTION = 'POST_TASKS_EXECUTION' as const; + +export type HandlePreTasksExecutionMessage = { + type: typeof PRE_TASKS_EXECUTION; + context: PreTasksExecutionContext; +}; +export type HandlePostTasksExecutionMessage = { + type: typeof POST_TASKS_EXECUTION; + context: PostTasksExecutionContext; +}; + +export function isHandlePreTasksExecutionMessage( + message: unknown +): message is HandlePreTasksExecutionMessage { + return ( + typeof message === 'object' && + message !== null && + 'type' in message && + message['type'] === PRE_TASKS_EXECUTION + ); +} + +export function isHandlePostTasksExecutionMessage( + message: unknown +): message is HandlePostTasksExecutionMessage { + return ( + typeof message === 'object' && + message !== null && + 'type' in message && + message['type'] === POST_TASKS_EXECUTION + ); +} diff --git a/packages/nx/src/daemon/server/handle-tasks-execution-hooks.ts b/packages/nx/src/daemon/server/handle-tasks-execution-hooks.ts new file mode 100644 index 0000000000000..e6adfaa584194 --- /dev/null +++ b/packages/nx/src/daemon/server/handle-tasks-execution-hooks.ts @@ -0,0 +1,41 @@ +import type { + PostTasksExecutionContext, + PreTasksExecutionContext, +} from '../../project-graph/plugins/public-api'; +import { + runPostTasksExecution, + runPreTasksExecution, +} from '../../project-graph/plugins/tasks-execution-hooks'; + +export async function handleRunPreTasksExecution( + context: PreTasksExecutionContext +) { + try { + const envs = await runPreTasksExecution(context); + return { + response: JSON.stringify(envs), + description: 'handleRunPreTasksExecution', + }; + } catch (e) { + return { + error: e, + description: `Error when running preTasksExecution.`, + }; + } +} +export async function handleRunPostTasksExecution( + context: PostTasksExecutionContext +) { + try { + await runPostTasksExecution(context); + return { + response: 'true', + description: 'handleRunPostTasksExecution', + }; + } catch (e) { + return { + error: e, + description: `Error when running postTasksExecution.`, + }; + } +} diff --git a/packages/nx/src/daemon/server/server.ts b/packages/nx/src/daemon/server/server.ts index e418169cced04..e8bbf1ae9b4a4 100644 --- a/packages/nx/src/daemon/server/server.ts +++ b/packages/nx/src/daemon/server/server.ts @@ -110,6 +110,16 @@ import { isHandleFlushSyncGeneratorChangesToDiskMessage, } from '../message-types/flush-sync-generator-changes-to-disk'; import { handleFlushSyncGeneratorChangesToDisk } from './handle-flush-sync-generator-changes-to-disk'; +import { + isHandlePostTasksExecutionMessage, + isHandlePreTasksExecutionMessage, + POST_TASKS_EXECUTION, + PRE_TASKS_EXECUTION, +} from '../message-types/run-tasks-execution-hooks'; +import { + handleRunPostTasksExecution, + handleRunPreTasksExecution, +} from './handle-tasks-execution-hooks'; let performanceObserver: PerformanceObserver | undefined; let workspaceWatcherError: Error | undefined; @@ -281,6 +291,14 @@ async function handleMessage(socket, data: string) { payload.deletedFiles ) ); + } else if (isHandlePreTasksExecutionMessage(payload)) { + await handleResult(socket, PRE_TASKS_EXECUTION, () => + handleRunPreTasksExecution(payload.context) + ); + } else if (isHandlePostTasksExecutionMessage(payload)) { + await handleResult(socket, POST_TASKS_EXECUTION, () => + handleRunPostTasksExecution(payload.context) + ); } else { await respondWithErrorAndExit( socket, diff --git a/packages/nx/src/devkit-exports.ts b/packages/nx/src/devkit-exports.ts index 12a4cf607c983..e284088821770 100644 --- a/packages/nx/src/devkit-exports.ts +++ b/packages/nx/src/devkit-exports.ts @@ -57,6 +57,10 @@ export type { CreateMetadata, CreateMetadataContext, ProjectsMetadata, + PreTasksExecution, + PreTasksExecutionContext, + PostTasksExecution, + PostTasksExecutionContext, } from './project-graph/plugins'; export { AggregateCreateNodesError } from './project-graph/error-types'; @@ -64,10 +68,15 @@ export { AggregateCreateNodesError } from './project-graph/error-types'; export { createNodesFromFiles } from './project-graph/plugins'; /** - * @category Workspace + * @category Tasks */ export type { Task, TaskGraph } from './config/task-graph'; +/** + * @category Tasks + */ +export type { TaskResult, TaskResults } from './tasks-runner/life-cycle'; + /** * @category Workspace */ diff --git a/packages/nx/src/project-graph/plugins/get-plugins.ts b/packages/nx/src/project-graph/plugins/get-plugins.ts index ac4063041ceab..9dfd9143f0e1f 100644 --- a/packages/nx/src/project-graph/plugins/get-plugins.ts +++ b/packages/nx/src/project-graph/plugins/get-plugins.ts @@ -13,6 +13,7 @@ import { cleanupPluginTSTranspiler, pluginTranspilerIsRegistered, } from './transpiler'; +import { isIsolationEnabled } from './isolation/enabled'; /** * Stuff for specified NX Plugins. @@ -97,23 +98,6 @@ export function cleanupPlugins() { * Stuff for generic loading */ -function isIsolationEnabled() { - // Explicitly enabled, regardless of further conditions - if (process.env.NX_ISOLATE_PLUGINS === 'true') { - return true; - } - if ( - // Explicitly disabled - process.env.NX_ISOLATE_PLUGINS === 'false' || - // Isolation is disabled on WASM builds currently. - IS_WASM - ) { - return false; - } - // Default value - return true; -} - const loadingMethod = isIsolationEnabled() ? loadNxPluginInIsolation : loadNxPlugin; diff --git a/packages/nx/src/project-graph/plugins/isolation/enabled.ts b/packages/nx/src/project-graph/plugins/isolation/enabled.ts new file mode 100644 index 0000000000000..471d01ed28133 --- /dev/null +++ b/packages/nx/src/project-graph/plugins/isolation/enabled.ts @@ -0,0 +1,18 @@ +import { IS_WASM } from 'nx/src/native'; + +export function isIsolationEnabled() { + // Explicitly enabled, regardless of further conditions + if (process.env.NX_ISOLATE_PLUGINS === 'true') { + return true; + } + if ( + // Explicitly disabled + process.env.NX_ISOLATE_PLUGINS === 'false' || + // Isolation is disabled on WASM builds currently. + IS_WASM + ) { + return false; + } + // Default value + return true; +} diff --git a/packages/nx/src/project-graph/plugins/isolation/messaging.ts b/packages/nx/src/project-graph/plugins/isolation/messaging.ts index 20c3cd18d109b..e693a42ece1f4 100644 --- a/packages/nx/src/project-graph/plugins/isolation/messaging.ts +++ b/packages/nx/src/project-graph/plugins/isolation/messaging.ts @@ -4,6 +4,8 @@ import { CreateDependenciesContext, CreateMetadataContext, CreateNodesContextV2, + PreTasksExecutionContext, + PostTasksExecutionContext, } from '../public-api'; import type { LoadedNxPlugin } from '../loaded-nx-plugin'; import { Serializable } from 'child_process'; @@ -31,6 +33,8 @@ export interface PluginWorkerLoadResult { hasCreateDependencies: boolean; hasProcessProjectGraph: boolean; hasCreateMetadata: boolean; + hasPreTasksExecution: boolean; + hasPostTasksExecution: boolean; success: true; } | { @@ -110,17 +114,66 @@ export interface PluginCreateMetadataResult { }; } +export interface PluginWorkerPreTasksExecutionMessage { + type: 'preTasksExecution'; + payload: { + tx: string; + context: PreTasksExecutionContext; + }; +} + +export interface PluginWorkerPreTasksExecutionMessageResult { + type: 'preTasksExecutionResult'; + payload: + | { + tx: string; + success: true; + mutations: NodeJS.ProcessEnv; + } + | { + success: false; + error: Error; + tx: string; + }; +} + +export interface PluginWorkerPostTasksExecutionMessage { + type: 'postTasksExecution'; + payload: { + tx: string; + context: PostTasksExecutionContext; + }; +} + +export interface PluginWorkerPostTasksExecutionMessageResult { + type: 'postTasksExecutionResult'; + payload: + | { + tx: string; + success: true; + } + | { + success: false; + error: Error; + tx: string; + }; +} + export type PluginWorkerMessage = | PluginWorkerLoadMessage | PluginWorkerCreateNodesMessage | PluginCreateDependenciesMessage - | PluginCreateMetadataMessage; + | PluginCreateMetadataMessage + | PluginWorkerPreTasksExecutionMessage + | PluginWorkerPostTasksExecutionMessage; export type PluginWorkerResult = | PluginWorkerLoadResult | PluginWorkerCreateNodesResult | PluginCreateDependenciesResult - | PluginCreateMetadataResult; + | PluginCreateMetadataResult + | PluginWorkerPreTasksExecutionMessageResult + | PluginWorkerPostTasksExecutionMessageResult; export function isPluginWorkerMessage( message: Serializable @@ -133,9 +186,11 @@ export function isPluginWorkerMessage( 'load', 'createNodes', 'createDependencies', - 'processProjectGraph', 'createMetadata', + 'processProjectGraph', 'shutdown', + 'preTasksExecution', + 'postTasksExecution', ].includes(message.type) ); } @@ -153,6 +208,8 @@ export function isPluginWorkerResult( 'createDependenciesResult', 'processProjectGraphResult', 'createMetadataResult', + 'preTasksExecutionResult', + 'postTasksExecutionResult', ].includes(message.type) ); } diff --git a/packages/nx/src/project-graph/plugins/isolation/plugin-pool.ts b/packages/nx/src/project-graph/plugins/isolation/plugin-pool.ts index 9d7bc29280a2d..364c4f169a811 100644 --- a/packages/nx/src/project-graph/plugins/isolation/plugin-pool.ts +++ b/packages/nx/src/project-graph/plugins/isolation/plugin-pool.ts @@ -43,7 +43,7 @@ const MAX_MESSAGE_WAIT = interface PendingPromise { promise: Promise; - resolver: (result: any) => void; + resolver: (result?: any) => void; rejector: (err: any) => void; } @@ -217,6 +217,44 @@ function createWorkerHandler( ); } : undefined, + preTasksExecution: result.hasPreTasksExecution + ? (context) => { + const tx = pluginName + worker.pid + ':preRun:' + txId++; + return registerPendingPromise( + tx, + pending, + () => { + sendMessageOverSocket(socket, { + type: 'preTasksExecution', + payload: { tx, context }, + }); + }, + { + plugin: pluginName, + operation: 'preTasksExecution', + } + ); + } + : undefined, + postTasksExecution: result.hasPostTasksExecution + ? (context) => { + const tx = pluginName + worker.pid + ':postRun:' + txId++; + return registerPendingPromise( + tx, + pending, + () => { + sendMessageOverSocket(socket, { + type: 'postTasksExecution', + payload: { tx, context }, + }); + }, + { + plugin: pluginName, + operation: 'postTasksExecution', + } + ); + } + : undefined, }); } else if (result.success === false) { onloadError(result.error); @@ -246,6 +284,22 @@ function createWorkerHandler( rejector(result.error); } }, + preTasksExecutionResult: ({ tx, ...result }) => { + const { resolver, rejector } = pending.get(tx); + if (result.success) { + resolver(result.mutations); + } else if (result.success === false) { + rejector(result.error); + } + }, + postTasksExecutionResult: ({ tx, ...result }) => { + const { resolver, rejector } = pending.get(tx); + if (result.success) { + resolver(); + } else if (result.success === false) { + rejector(result.error); + } + }, }); }; } diff --git a/packages/nx/src/project-graph/plugins/isolation/plugin-worker.ts b/packages/nx/src/project-graph/plugins/isolation/plugin-worker.ts index 52a44f51d60ea..f5e32dd07ea53 100644 --- a/packages/nx/src/project-graph/plugins/isolation/plugin-worker.ts +++ b/packages/nx/src/project-graph/plugins/isolation/plugin-worker.ts @@ -1,6 +1,7 @@ import { consumeMessage, isPluginWorkerMessage } from './messaging'; import { createSerializableError } from '../../../utils/serializable-error'; import { consumeMessagesFromSocket } from '../../../utils/consume-messages-from-socket'; +import type { LoadedNxPlugin } from '../loaded-nx-plugin'; import { createServer } from 'net'; import { unlinkSync } from 'fs'; @@ -12,7 +13,7 @@ if (process.env.NX_PERF_LOGGING === 'true') { global.NX_GRAPH_CREATION = true; global.NX_PLUGIN_WORKER = true; let connected = false; -let plugin; +let plugin: LoadedNxPlugin; const socketPath = process.argv[2]; @@ -75,6 +76,10 @@ const server = createServer((socket) => { !!plugin.processProjectGraph, hasCreateMetadata: 'createMetadata' in plugin && !!plugin.createMetadata, + hasPreTasksExecution: + 'preTasksExecution' in plugin && !!plugin.preTasksExecution, + hasPostTasksExecution: + 'postTasksExecution' in plugin && !!plugin.postTasksExecution, success: true, }, }; @@ -142,6 +147,42 @@ const server = createServer((socket) => { }; } }, + preTasksExecution: async ({ tx, context }) => { + try { + const mutations = await plugin.preTasksExecution?.(context); + return { + type: 'preTasksExecutionResult', + payload: { success: true, tx, mutations }, + }; + } catch (e) { + return { + type: 'preTasksExecutionResult', + payload: { + success: false, + error: createSerializableError(e), + tx, + }, + }; + } + }, + postTasksExecution: async ({ tx, context }) => { + try { + await plugin.postTasksExecution?.(context); + return { + type: 'postTasksExecutionResult', + payload: { success: true, tx }, + }; + } catch (e) { + return { + type: 'postTasksExecutionResult', + payload: { + success: false, + error: createSerializableError(e), + tx, + }, + }; + } + }, }); }) ); diff --git a/packages/nx/src/project-graph/plugins/load-resolved-plugin.ts b/packages/nx/src/project-graph/plugins/load-resolved-plugin.ts index bfae38b7bcc81..5797ca7533839 100644 --- a/packages/nx/src/project-graph/plugins/load-resolved-plugin.ts +++ b/packages/nx/src/project-graph/plugins/load-resolved-plugin.ts @@ -18,7 +18,10 @@ async function importPluginModule(pluginPath: string): Promise { m.default && ('createNodes' in m.default || 'createNodesV2' in m.default || - 'createDependencies' in m.default) + 'createDependencies' in m.default || + 'createMetadata' in m.default || + 'preRun' in m.default || + 'postRun' in m.default) ) { return m.default; } diff --git a/packages/nx/src/project-graph/plugins/loaded-nx-plugin.ts b/packages/nx/src/project-graph/plugins/loaded-nx-plugin.ts index d35ca9c456bd1..13cad09ab5937 100644 --- a/packages/nx/src/project-graph/plugins/loaded-nx-plugin.ts +++ b/packages/nx/src/project-graph/plugins/loaded-nx-plugin.ts @@ -11,9 +11,13 @@ import type { CreateNodesContextV2, CreateNodesResult, NxPluginV2, + PostTasksExecutionContext, + PreTasksExecutionContext, ProjectsMetadata, } from './public-api'; import { createNodesFromFiles } from './utils'; +import { isIsolationEnabled } from './isolation/enabled'; +import { isDaemonEnabled } from '../../daemon/client/client'; export class LoadedNxPlugin { readonly name: string; @@ -35,6 +39,12 @@ export class LoadedNxPlugin { graph: ProjectGraph, context: CreateMetadataContext ) => Promise; + readonly preTasksExecution?: ( + context: PreTasksExecutionContext + ) => Promise; + readonly postTasksExecution?: ( + context: PostTasksExecutionContext + ) => Promise; readonly options?: unknown; readonly include?: string[]; @@ -107,10 +117,42 @@ export class LoadedNxPlugin { this.createMetadata = async (graph, context) => plugin.createMetadata(graph, this.options, context); } + + if (plugin.preTasksExecution) { + this.preTasksExecution = async (context: PreTasksExecutionContext) => { + const updates = {}; + const originalEnv = process.env; + let revokeFn: () => void; + if (isIsolationEnabled() || isDaemonEnabled()) { + const { proxy, revoke } = Proxy.revocable( + process.env, + { + set: (target, key: string, value) => { + target[key] = value; + updates[key] = value; + return true; + }, + } + ); + process.env = proxy; + revokeFn = revoke; + } + plugin.preTasksExecution(this.options, context); + + if (revokeFn) { + revokeFn(); + } + process.env = originalEnv; + for (const key in updates) { + process.env[key] = updates[key]; + } + return updates; + }; + + if (plugin.postTasksExecution) { + this.postTasksExecution = async (context: PostTasksExecutionContext) => + plugin.postTasksExecution(this.options, context); + } + } } } - -export type CreateNodesResultWithContext = CreateNodesResult & { - file: string; - pluginName: string; -}; diff --git a/packages/nx/src/project-graph/plugins/public-api.ts b/packages/nx/src/project-graph/plugins/public-api.ts index 7049a98f7e8a6..50815d8afbab2 100644 --- a/packages/nx/src/project-graph/plugins/public-api.ts +++ b/packages/nx/src/project-graph/plugins/public-api.ts @@ -1,16 +1,17 @@ // This file represents the public API for plugins which live in nx.json's plugins array. // For methods to interact with plugins from within Nx, see `./internal-api.ts`. -import { +import type { FileMap, ProjectGraph, ProjectGraphExternalNode, } from '../../config/project-graph'; -import { ProjectConfiguration } from '../../config/workspace-json-project-json'; +import type { ProjectConfiguration } from '../../config/workspace-json-project-json'; -import { NxJsonConfiguration } from '../../config/nx-json'; -import { RawProjectGraphDependency } from '../project-graph-builder'; +import type { NxJsonConfiguration } from '../../config/nx-json'; +import type { RawProjectGraphDependency } from '../project-graph-builder'; +import type { TaskResults } from '../../tasks-runner/life-cycle'; /** * Context for {@link CreateNodesFunction} @@ -146,7 +147,7 @@ export type CreateMetadata = ( ) => ProjectsMetadata | Promise; /** - * A plugin for Nx which creates nodes and dependencies for the {@link ProjectGraph} + * A plugin which enhances the behavior of Nx */ export type NxPluginV2 = { name: string; @@ -176,9 +177,37 @@ export type NxPluginV2 = { * Provides a function to create metadata for the {@link ProjectGraph} */ createMetadata?: CreateMetadata; + + /** + * Provides a function to run before the Nx runs tasks + */ + preTasksExecution?: PreTasksExecution; + + /** + * Provides a function to run after the Nx runs tasks + */ + postTasksExecution?: PostTasksExecution; +}; + +export type PreTasksExecutionContext = { + readonly workspaceRoot: string; + readonly nxJsonConfiguration: NxJsonConfiguration; +}; +export type PostTasksExecutionContext = { + readonly workspaceRoot: string; + readonly nxJsonConfiguration: NxJsonConfiguration; + readonly taskResults: TaskResults; }; +export type PreTasksExecution = ( + options: TOptions | undefined, + context: PreTasksExecutionContext +) => void | Promise; +export type PostTasksExecution = ( + options: TOptions | undefined, + context: PostTasksExecutionContext +) => void | Promise; /** - * A plugin for Nx + * A plugin which enhances the behavior of Nx */ export type NxPlugin = NxPluginV2; diff --git a/packages/nx/src/project-graph/plugins/tasks-execution-hooks.ts b/packages/nx/src/project-graph/plugins/tasks-execution-hooks.ts new file mode 100644 index 0000000000000..41add340a4c32 --- /dev/null +++ b/packages/nx/src/project-graph/plugins/tasks-execution-hooks.ts @@ -0,0 +1,89 @@ +import type { + PostTasksExecutionContext, + PreTasksExecutionContext, +} from './public-api'; +import { getPlugins } from './get-plugins'; +import { isOnDaemon } from '../../daemon/is-on-daemon'; +import { daemonClient, isDaemonEnabled } from '../../daemon/client/client'; + +export async function runPreTasksExecution( + pluginContext: PreTasksExecutionContext +) { + if (isOnDaemon() || !isDaemonEnabled()) { + performance.mark(`preTasksExecution:start`); + const plugins = await getPlugins(); + const envs = await Promise.all( + plugins + .filter((p) => p.preTasksExecution) + .map(async (plugin) => { + performance.mark(`${plugin.name}:preTasksExecution:start`); + try { + return await plugin.preTasksExecution(pluginContext); + } finally { + performance.mark(`${plugin.name}:preTasksExecution:end`); + performance.measure( + `${plugin.name}:preTasksExecution`, + `${plugin.name}:preTasksExecution:start`, + `${plugin.name}:preTasksExecution:end` + ); + } + }) + ); + + if (!isDaemonEnabled()) { + applyProcessEnvs(envs); + } + performance.mark(`preTasksExecution:end`); + performance.measure( + `preTasksExecution`, + `preTasksExecution:start`, + `preTasksExecution:end` + ); + return envs; + } else { + const envs = await daemonClient.runPreTasksExecution(pluginContext); + applyProcessEnvs(envs); + } +} + +function applyProcessEnvs(envs: NodeJS.ProcessEnv[]) { + for (const env of envs) { + for (const key in env) { + process.env[key] = env[key]; + } + } +} + +export async function runPostTasksExecution( + context: PostTasksExecutionContext +) { + if (isOnDaemon() || !isDaemonEnabled()) { + performance.mark(`postTasksExecution:start`); + const plugins = await getPlugins(); + await Promise.all( + plugins + .filter((p) => p.postTasksExecution) + .map(async (plugin) => { + performance.mark(`${plugin.name}:postTasksExecution:start`); + try { + await plugin.postTasksExecution(context); + } finally { + performance.mark(`${plugin.name}:postTasksExecution:end`); + performance.measure( + `${plugin.name}:postTasksExecution`, + `${plugin.name}:postTasksExecution:start`, + `${plugin.name}:postTasksExecution:end` + ); + } + }) + ); + performance.mark(`postTasksExecution:end`); + performance.measure( + `postTasksExecution`, + `postTasksExecution:start`, + `postTasksExecution:end` + ); + } else { + await daemonClient.runPostTasksExecution(context); + } +} diff --git a/packages/nx/src/tasks-runner/life-cycle.ts b/packages/nx/src/tasks-runner/life-cycle.ts index 2bce065d3e93f..e7c870483acc6 100644 --- a/packages/nx/src/tasks-runner/life-cycle.ts +++ b/packages/nx/src/tasks-runner/life-cycle.ts @@ -1,6 +1,9 @@ import { TaskStatus } from './tasks-runner'; import { Task } from '../config/task-graph'; +/** + * The result of a completed {@link Task} + */ export interface TaskResult { task: Task; status: TaskStatus; @@ -8,6 +11,11 @@ export interface TaskResult { terminalOutput?: string; } +/** + * A map of {@link TaskResult} keyed by the ID of the completed {@link Task}s + */ +export type TaskResults = Record; + export interface TaskMetadata { groupId: number; } diff --git a/packages/nx/src/tasks-runner/run-command.ts b/packages/nx/src/tasks-runner/run-command.ts index cae85b256e199..b97e4136aed8a 100644 --- a/packages/nx/src/tasks-runner/run-command.ts +++ b/packages/nx/src/tasks-runner/run-command.ts @@ -34,7 +34,12 @@ import { } from '../utils/sync-generators'; import { workspaceRoot } from '../utils/workspace-root'; import { createTaskGraph } from './create-task-graph'; -import { CompositeLifeCycle, LifeCycle, TaskResult } from './life-cycle'; +import { + CompositeLifeCycle, + LifeCycle, + TaskResult, + TaskResults, +} from './life-cycle'; import { createRunManyDynamicOutputRenderer } from './life-cycles/dynamic-run-many-terminal-output-life-cycle'; import { createRunOneDynamicOutputRenderer } from './life-cycles/dynamic-run-one-terminal-output-life-cycle'; import { StaticRunManyTerminalOutputLifeCycle } from './life-cycles/static-run-many-terminal-output-life-cycle'; @@ -55,6 +60,10 @@ import { shouldStreamOutput } from './utils'; import chalk = require('chalk'); import type { Observable } from 'rxjs'; import { printPowerpackLicense } from '../utils/powerpack'; +import { + runPostTasksExecution, + runPreTasksExecution, +} from '../project-graph/plugins/tasks-execution-hooks'; async function getTerminalOutputLifeCycle( initiatingProject: string, @@ -177,23 +186,41 @@ export async function runCommand( const status = await handleErrors( process.env.NX_VERBOSE_LOGGING === 'true', async () => { + await runPreTasksExecution({ + workspaceRoot, + nxJsonConfiguration: nxJson, + }); + const taskResults = await runCommandForTasks( projectsToRun, currentProjectGraph, { nxJson }, - nxArgs, + { + ...nxArgs, + skipNxCache: + process.env.NX_SKIP_NX_CACHE === 'true' || + process.env.DISABLE_NX_CACHE === 'true', + }, overrides, initiatingProject, extraTargetDependencies, extraOptions ); - return Object.values(taskResults).some( + const result = Object.values(taskResults).some( (taskResult) => taskResult.status === 'failure' || taskResult.status === 'skipped' ) ? 1 : 0; + + await runPostTasksExecution({ + taskResults, + workspaceRoot, + nxJsonConfiguration: nxJson, + }); + + return result; } ); @@ -209,7 +236,7 @@ export async function runCommandForTasks( initiatingProject: string | null, extraTargetDependencies: Record, extraOptions: { excludeTaskDependencies: boolean; loadDotEnvFiles: boolean } -): Promise<{ [id: string]: TaskResult }> { +): Promise { const projectNames = projectsToRun.map((t) => t.name); const { projectGraph, taskGraph } = await ensureWorkspaceIsInSyncAndGetGraphs( diff --git a/packages/nx/src/tasks-runner/task-orchestrator.ts b/packages/nx/src/tasks-runner/task-orchestrator.ts index 76b1114590ab0..565b90ec3b7aa 100644 --- a/packages/nx/src/tasks-runner/task-orchestrator.ts +++ b/packages/nx/src/tasks-runner/task-orchestrator.ts @@ -230,7 +230,6 @@ export class TaskOrchestrator { task: Task; status: 'local-cache' | 'local-cache-kept-existing' | 'remote-cache'; }> { - task.startTime = Date.now(); const cachedResult = await this.cache.get(task); if (!cachedResult || cachedResult.code !== 0) return null; @@ -241,7 +240,6 @@ export class TaskOrchestrator { if (shouldCopyOutputsFromCache) { await this.cache.copyFilesFromCache(task.hash, cachedResult, outputs); } - task.endTime = Date.now(); const status = cachedResult.remote ? 'remote-cache' : shouldCopyOutputsFromCache @@ -545,6 +543,10 @@ export class TaskOrchestrator { // region Lifecycle private async preRunSteps(tasks: Task[], metadata: TaskMetadata) { + const now = Date.now(); + for (const task of tasks) { + task.startTime = now; + } await this.options.lifeCycle.startTasks(tasks, metadata); } @@ -558,7 +560,9 @@ export class TaskOrchestrator { doNotSkipCache: boolean, { groupId }: { groupId: number } ) { + const now = Date.now(); for (const task of tasks) { + task.endTime = now; await this.recordOutputsHash(task); }