feat(core): add pre and post run apis (#29636)

<!-- Please make sure you have read the submission guidelines before
posting an PR -->
<!--
https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr
-->

<!-- Please make sure that your commit message follows our format -->
<!-- Example: `fix(nx): must begin with lowercase` -->

<!-- If this is a particularly complex change or feature addition, you
can request a dedicated Nx release for this pull request branch. Mention
someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they
will confirm if the PR warrants its own release for testing purposes,
and generate it for you if appropriate. -->

## Current Behavior
<!-- This is the behavior we have today -->

There is no specific API for running things before and after tasks run.

## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->

This PR adds an API akin to npm's `preinstall` and `postinstall`.

Plugins can now specify `preTasksExecution` and `postTasksExecution`
functions which run before and after Nx runs tasks respectively.

```ts
import type { PreTasksExecutionContext, PostTasksExecutionContext } from '@nx/devkit';

interface PluginOptions {
  field: any;
}

export function preTasksExecution(options: PluginOptions, context: PreTasksExecutionContext) {
  console.log('prerun')
}

export function postTasksExecution(options: PluginOptions, context: PostTasksExecutionContext) {
  console.log('postrun', context.taskResults)
}
```

## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->

Fixes #
This commit is contained in:
Jason Jean 2025-01-27 12:09:43 -05:00 committed by GitHub
parent f02a88a72c
commit 4a9508b368
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 679 additions and 50 deletions

View File

@ -2,4 +2,4 @@
Ƭ **NxPlugin**: [`NxPluginV2`](../../devkit/documents/NxPluginV2)
A plugin for Nx
A plugin which enhances the behavior of Nx

View File

@ -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 |

View File

@ -0,0 +1,24 @@
# Type alias: PostTasksExecution\<TOptions\>
Ƭ **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`\>

View File

@ -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` |

View File

@ -0,0 +1,24 @@
# Type alias: PreTasksExecution\<TOptions\>
Ƭ **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`\>

View File

@ -0,0 +1,10 @@
# Type alias: PreTasksExecutionContext
Ƭ **PreTasksExecutionContext**: `Object`
#### Type declaration
| Name | Type |
| :-------------------- | :------------------------------------------------------------------ |
| `nxJsonConfiguration` | [`NxJsonConfiguration`](../../devkit/documents/NxJsonConfiguration) |
| `workspaceRoot` | `string` |

View File

@ -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)

View File

@ -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`

View File

@ -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

View File

@ -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)

View File

@ -14,6 +14,7 @@ import {
describe('Jest', () => {
beforeAll(() => {
newProject({ name: uniq('proj-jest'), packages: ['@nx/js', '@nx/node'] });
process.env.NX_E2E_VERBOSE_LOGGING = 'true';
});
afterAll(() => cleanupProject());

View File

@ -29,7 +29,7 @@ describe('Convert Nx Executor', () => {
const registry = new schema.CoreSchemaRegistry();
registry.addPostTransform(schema.transforms.addUndefinedDefaults);
const testArchitectHost = new TestingArchitectHost();
const testArchitectHost = new TestingArchitectHost(fs.tempDir, fs.tempDir);
testArchitectHost.workspaceRoot = fs.tempDir;
const architect = new Architect(testArchitectHost, registry);

View File

@ -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;
}

View File

@ -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<NodeJS.ProcessEnv[]> {
const message: HandlePreTasksExecutionMessage = {
type: PRE_TASKS_EXECUTION,
context,
};
return this.sendToDaemonViaQueue(message);
}
async runPostTasksExecution(
context: PostTasksExecutionContext
): Promise<void> {
const message: HandlePostTasksExecutionMessage = {
type: POST_TASKS_EXECUTION,
context,
};
return this.sendToDaemonViaQueue(message);
}
async isServerAvailable(): Promise<boolean> {
return new Promise((resolve) => {
try {

View File

@ -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
);
}

View File

@ -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.`,
};
}
}

View File

@ -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,

View File

@ -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
*/

View File

@ -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;

View File

@ -0,0 +1,18 @@
import { IS_WASM } from '../../../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;
}

View File

@ -1,13 +1,15 @@
import { ProjectGraph } from '../../../config/project-graph';
import { PluginConfiguration } from '../../../config/nx-json';
import {
import type { ProjectGraph } from '../../../config/project-graph';
import type { PluginConfiguration } from '../../../config/nx-json';
import type {
CreateDependenciesContext,
CreateMetadataContext,
CreateNodesContextV2,
PreTasksExecutionContext,
PostTasksExecutionContext,
} from '../public-api';
import type { LoadedNxPlugin } from '../loaded-nx-plugin';
import { Serializable } from 'child_process';
import { Socket } from 'net';
import type { Serializable } from 'child_process';
import type { Socket } from 'net';
export interface PluginWorkerLoadMessage {
type: 'load';
@ -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)
);
}

View File

@ -43,7 +43,7 @@ const MAX_MESSAGE_WAIT =
interface PendingPromise {
promise: Promise<unknown>;
resolver: (result: any) => void;
resolver: (result?: any) => void;
rejector: (err: any) => void;
}
@ -217,6 +217,46 @@ function createWorkerHandler(
);
}
: undefined,
preTasksExecution: result.hasPreTasksExecution
? (context) => {
const tx =
pluginName + worker.pid + ':preTasksExecution:' + 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 + ':postTasksExecution:' + 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 +286,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);
}
},
});
};
}

View File

@ -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,
},
};
}
},
});
})
);

View File

@ -18,7 +18,10 @@ async function importPluginModule(pluginPath: string): Promise<NxPlugin> {
m.default &&
('createNodes' in m.default ||
'createNodesV2' in m.default ||
'createDependencies' in m.default)
'createDependencies' in m.default ||
'createMetadata' in m.default ||
'preTasksExecution' in m.default ||
'postTasksExecution' in m.default)
) {
return m.default;
}

View File

@ -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<ProjectsMetadata>;
readonly preTasksExecution?: (
context: PreTasksExecutionContext
) => Promise<NodeJS.ProcessEnv>;
readonly postTasksExecution?: (
context: PostTasksExecutionContext
) => Promise<void>;
readonly options?: unknown;
readonly include?: string[];
@ -107,10 +117,37 @@ export class LoadedNxPlugin {
this.createMetadata = async (graph, context) =>
plugin.createMetadata(graph, this.options, context);
}
if (plugin.preTasksExecution) {
this.preTasksExecution = async (context: PreTasksExecutionContext) => {
const updates = {};
let revokeFn: () => void;
if (isIsolationEnabled() || isDaemonEnabled()) {
const { proxy, revoke } = Proxy.revocable<NodeJS.ProcessEnv>(
process.env,
{
set: (target, key: string, value) => {
target[key] = value;
updates[key] = value;
return true;
},
}
);
process.env = proxy;
revokeFn = revoke;
}
await plugin.preTasksExecution(this.options, context);
if (revokeFn) {
revokeFn();
}
return updates;
};
if (plugin.postTasksExecution) {
this.postTasksExecution = async (context: PostTasksExecutionContext) =>
plugin.postTasksExecution(this.options, context);
}
}
}
}
export type CreateNodesResultWithContext = CreateNodesResult & {
file: string;
pluginName: string;
};

View File

@ -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<T = unknown> = (
) => ProjectsMetadata | Promise<ProjectsMetadata>;
/**
* A plugin for Nx which creates nodes and dependencies for the {@link ProjectGraph}
* A plugin which enhances the behavior of Nx
*/
export type NxPluginV2<TOptions = unknown> = {
name: string;
@ -176,9 +177,37 @@ export type NxPluginV2<TOptions = unknown> = {
* Provides a function to create metadata for the {@link ProjectGraph}
*/
createMetadata?: CreateMetadata<TOptions>;
/**
* Provides a function to run before the Nx runs tasks
*/
preTasksExecution?: PreTasksExecution<TOptions>;
/**
* Provides a function to run after the Nx runs tasks
*/
postTasksExecution?: PostTasksExecution<TOptions>;
};
export type PreTasksExecutionContext = {
readonly workspaceRoot: string;
readonly nxJsonConfiguration: NxJsonConfiguration;
};
export type PostTasksExecutionContext = {
readonly workspaceRoot: string;
readonly nxJsonConfiguration: NxJsonConfiguration;
readonly taskResults: TaskResults;
};
export type PreTasksExecution<TOptions = unknown> = (
options: TOptions | undefined,
context: PreTasksExecutionContext
) => void | Promise<void>;
export type PostTasksExecution<TOptions = unknown> = (
options: TOptions | undefined,
context: PostTasksExecutionContext
) => void | Promise<void>;
/**
* A plugin for Nx
* A plugin which enhances the behavior of Nx
*/
export type NxPlugin = NxPluginV2;

View File

@ -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(pluginContext.workspaceRoot);
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);
}
}

View File

@ -97,7 +97,7 @@ export async function retrieveProjectConfigurationsWithAngularProjects(
pluginsToLoad.push(join(__dirname, '../../adapter/angular-json'));
}
const plugins = await getPlugins();
const plugins = await getPlugins(workspaceRoot);
const res = await retrieveProjectConfigurations(
plugins,

View File

@ -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<string, TaskResult>;
export interface TaskMetadata {
groupId: number;
}

View File

@ -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,42 @@ 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:
nxArgs.skipNxCache ||
process.env.NX_SKIP_NX_CACHE === 'true' ||
process.env.NX_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 +237,7 @@ export async function runCommandForTasks(
initiatingProject: string | null,
extraTargetDependencies: Record<string, (TargetDependencyConfig | string)[]>,
extraOptions: { excludeTaskDependencies: boolean; loadDotEnvFiles: boolean }
): Promise<{ [id: string]: TaskResult }> {
): Promise<TaskResults> {
const projectNames = projectsToRun.map((t) => t.name);
const { projectGraph, taskGraph } = await ensureWorkspaceIsInSyncAndGetGraphs(

View File

@ -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);
}