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) Ƭ **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` Ƭ **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 #### 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. | | `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. | | `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` | - | | `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) - [Task](../../devkit/documents/Task)
- [TaskGraph](../../devkit/documents/TaskGraph) - [TaskGraph](../../devkit/documents/TaskGraph)
- [TaskHasher](../../devkit/documents/TaskHasher) - [TaskHasher](../../devkit/documents/TaskHasher)
- [TaskResult](../../devkit/documents/TaskResult)
- [Tree](../../devkit/documents/Tree) - [Tree](../../devkit/documents/Tree)
- [Workspace](../../devkit/documents/Workspace) - [Workspace](../../devkit/documents/Workspace)
@ -86,6 +87,10 @@ It only uses language primitives and immutable objects
- [NxPluginV2](../../devkit/documents/NxPluginV2) - [NxPluginV2](../../devkit/documents/NxPluginV2)
- [PackageManager](../../devkit/documents/PackageManager) - [PackageManager](../../devkit/documents/PackageManager)
- [PluginConfiguration](../../devkit/documents/PluginConfiguration) - [PluginConfiguration](../../devkit/documents/PluginConfiguration)
- [PostTasksExecution](../../devkit/documents/PostTasksExecution)
- [PostTasksExecutionContext](../../devkit/documents/PostTasksExecutionContext)
- [PreTasksExecution](../../devkit/documents/PreTasksExecution)
- [PreTasksExecutionContext](../../devkit/documents/PreTasksExecutionContext)
- [ProjectType](../../devkit/documents/ProjectType) - [ProjectType](../../devkit/documents/ProjectType)
- [ProjectsMetadata](../../devkit/documents/ProjectsMetadata) - [ProjectsMetadata](../../devkit/documents/ProjectsMetadata)
- [PromiseExecutor](../../devkit/documents/PromiseExecutor) - [PromiseExecutor](../../devkit/documents/PromiseExecutor)
@ -94,6 +99,7 @@ It only uses language primitives and immutable objects
- [StringChange](../../devkit/documents/StringChange) - [StringChange](../../devkit/documents/StringChange)
- [TargetDefaults](../../devkit/documents/TargetDefaults) - [TargetDefaults](../../devkit/documents/TargetDefaults)
- [TaskGraphExecutor](../../devkit/documents/TaskGraphExecutor) - [TaskGraphExecutor](../../devkit/documents/TaskGraphExecutor)
- [TaskResults](../../devkit/documents/TaskResults)
- [ToJSOptions](../../devkit/documents/ToJSOptions) - [ToJSOptions](../../devkit/documents/ToJSOptions)
- [WorkspaceJsonConfiguration](../../devkit/documents/WorkspaceJsonConfiguration) - [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) - [Task](../../devkit/documents/Task)
- [TaskGraph](../../devkit/documents/TaskGraph) - [TaskGraph](../../devkit/documents/TaskGraph)
- [TaskHasher](../../devkit/documents/TaskHasher) - [TaskHasher](../../devkit/documents/TaskHasher)
- [TaskResult](../../devkit/documents/TaskResult)
- [Tree](../../devkit/documents/Tree) - [Tree](../../devkit/documents/Tree)
- [Workspace](../../devkit/documents/Workspace) - [Workspace](../../devkit/documents/Workspace)
@ -86,6 +87,10 @@ It only uses language primitives and immutable objects
- [NxPluginV2](../../devkit/documents/NxPluginV2) - [NxPluginV2](../../devkit/documents/NxPluginV2)
- [PackageManager](../../devkit/documents/PackageManager) - [PackageManager](../../devkit/documents/PackageManager)
- [PluginConfiguration](../../devkit/documents/PluginConfiguration) - [PluginConfiguration](../../devkit/documents/PluginConfiguration)
- [PostTasksExecution](../../devkit/documents/PostTasksExecution)
- [PostTasksExecutionContext](../../devkit/documents/PostTasksExecutionContext)
- [PreTasksExecution](../../devkit/documents/PreTasksExecution)
- [PreTasksExecutionContext](../../devkit/documents/PreTasksExecutionContext)
- [ProjectType](../../devkit/documents/ProjectType) - [ProjectType](../../devkit/documents/ProjectType)
- [ProjectsMetadata](../../devkit/documents/ProjectsMetadata) - [ProjectsMetadata](../../devkit/documents/ProjectsMetadata)
- [PromiseExecutor](../../devkit/documents/PromiseExecutor) - [PromiseExecutor](../../devkit/documents/PromiseExecutor)
@ -94,6 +99,7 @@ It only uses language primitives and immutable objects
- [StringChange](../../devkit/documents/StringChange) - [StringChange](../../devkit/documents/StringChange)
- [TargetDefaults](../../devkit/documents/TargetDefaults) - [TargetDefaults](../../devkit/documents/TargetDefaults)
- [TaskGraphExecutor](../../devkit/documents/TaskGraphExecutor) - [TaskGraphExecutor](../../devkit/documents/TaskGraphExecutor)
- [TaskResults](../../devkit/documents/TaskResults)
- [ToJSOptions](../../devkit/documents/ToJSOptions) - [ToJSOptions](../../devkit/documents/ToJSOptions)
- [WorkspaceJsonConfiguration](../../devkit/documents/WorkspaceJsonConfiguration) - [WorkspaceJsonConfiguration](../../devkit/documents/WorkspaceJsonConfiguration)

View File

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

View File

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

View File

@ -26,6 +26,11 @@ import {
import { deepMergeJson } from './config/deep-merge-json'; import { deepMergeJson } from './config/deep-merge-json';
import { filterReleaseGroups } from './config/filter-release-groups'; import { filterReleaseGroups } from './config/filter-release-groups';
import { printConfigAndExit } from './utils/print-config'; 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 { export interface PublishProjectsResult {
[projectName: string]: { [projectName: string]: {
@ -249,6 +254,10 @@ async function runPublishOnProjects(
].join('\n')}\n` ].join('\n')}\n`
); );
} }
await runPreTasksExecution({
workspaceRoot,
nxJsonConfiguration: nxJson,
});
/** /**
* Run the relevant nx-release-publish executor on each of the selected projects. * Run the relevant nx-release-publish executor on each of the selected projects.
@ -276,6 +285,11 @@ async function runPublishOnProjects(
code: taskData.code, code: taskData.code,
}; };
} }
await runPostTasksExecution({
taskResults: commandResults,
workspaceRoot,
nxJsonConfiguration: nxJson,
});
return publishProjectsResult; return publishProjectsResult;
} }

View File

@ -78,6 +78,16 @@ import {
type HandleFlushSyncGeneratorChangesToDiskMessage, type HandleFlushSyncGeneratorChangesToDiskMessage,
} from '../message-types/flush-sync-generator-changes-to-disk'; } from '../message-types/flush-sync-generator-changes-to-disk';
import { DelayedSpinner } from '../../utils/delayed-spinner'; 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 = { const DAEMON_ENV_SETTINGS = {
NX_PROJECT_GLOB_CACHE: 'false', NX_PROJECT_GLOB_CACHE: 'false',
@ -435,6 +445,26 @@ export class DaemonClient {
return this.sendToDaemonViaQueue(message); 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> { async isServerAvailable(): Promise<boolean> {
return new Promise((resolve) => { return new Promise((resolve) => {
try { 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, isHandleFlushSyncGeneratorChangesToDiskMessage,
} from '../message-types/flush-sync-generator-changes-to-disk'; } from '../message-types/flush-sync-generator-changes-to-disk';
import { handleFlushSyncGeneratorChangesToDisk } from './handle-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 performanceObserver: PerformanceObserver | undefined;
let workspaceWatcherError: Error | undefined; let workspaceWatcherError: Error | undefined;
@ -281,6 +291,14 @@ async function handleMessage(socket, data: string) {
payload.deletedFiles 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 { } else {
await respondWithErrorAndExit( await respondWithErrorAndExit(
socket, socket,

View File

@ -57,6 +57,10 @@ export type {
CreateMetadata, CreateMetadata,
CreateMetadataContext, CreateMetadataContext,
ProjectsMetadata, ProjectsMetadata,
PreTasksExecution,
PreTasksExecutionContext,
PostTasksExecution,
PostTasksExecutionContext,
} from './project-graph/plugins'; } from './project-graph/plugins';
export { AggregateCreateNodesError } from './project-graph/error-types'; export { AggregateCreateNodesError } from './project-graph/error-types';
@ -64,10 +68,15 @@ export { AggregateCreateNodesError } from './project-graph/error-types';
export { createNodesFromFiles } from './project-graph/plugins'; export { createNodesFromFiles } from './project-graph/plugins';
/** /**
* @category Workspace * @category Tasks
*/ */
export type { Task, TaskGraph } from './config/task-graph'; export type { Task, TaskGraph } from './config/task-graph';
/**
* @category Tasks
*/
export type { TaskResult, TaskResults } from './tasks-runner/life-cycle';
/** /**
* @category Workspace * @category Workspace
*/ */

View File

@ -13,6 +13,7 @@ import {
cleanupPluginTSTranspiler, cleanupPluginTSTranspiler,
pluginTranspilerIsRegistered, pluginTranspilerIsRegistered,
} from './transpiler'; } from './transpiler';
import { isIsolationEnabled } from './isolation/enabled';
/** /**
* Stuff for specified NX Plugins. * Stuff for specified NX Plugins.
@ -97,23 +98,6 @@ export function cleanupPlugins() {
* Stuff for generic loading * 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() const loadingMethod = isIsolationEnabled()
? loadNxPluginInIsolation ? loadNxPluginInIsolation
: loadNxPlugin; : 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 type { ProjectGraph } from '../../../config/project-graph';
import { PluginConfiguration } from '../../../config/nx-json'; import type { PluginConfiguration } from '../../../config/nx-json';
import { import type {
CreateDependenciesContext, CreateDependenciesContext,
CreateMetadataContext, CreateMetadataContext,
CreateNodesContextV2, CreateNodesContextV2,
PreTasksExecutionContext,
PostTasksExecutionContext,
} from '../public-api'; } from '../public-api';
import type { LoadedNxPlugin } from '../loaded-nx-plugin'; import type { LoadedNxPlugin } from '../loaded-nx-plugin';
import { Serializable } from 'child_process'; import type { Serializable } from 'child_process';
import { Socket } from 'net'; import type { Socket } from 'net';
export interface PluginWorkerLoadMessage { export interface PluginWorkerLoadMessage {
type: 'load'; type: 'load';
@ -31,6 +33,8 @@ export interface PluginWorkerLoadResult {
hasCreateDependencies: boolean; hasCreateDependencies: boolean;
hasProcessProjectGraph: boolean; hasProcessProjectGraph: boolean;
hasCreateMetadata: boolean; hasCreateMetadata: boolean;
hasPreTasksExecution: boolean;
hasPostTasksExecution: boolean;
success: true; 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 = export type PluginWorkerMessage =
| PluginWorkerLoadMessage | PluginWorkerLoadMessage
| PluginWorkerCreateNodesMessage | PluginWorkerCreateNodesMessage
| PluginCreateDependenciesMessage | PluginCreateDependenciesMessage
| PluginCreateMetadataMessage; | PluginCreateMetadataMessage
| PluginWorkerPreTasksExecutionMessage
| PluginWorkerPostTasksExecutionMessage;
export type PluginWorkerResult = export type PluginWorkerResult =
| PluginWorkerLoadResult | PluginWorkerLoadResult
| PluginWorkerCreateNodesResult | PluginWorkerCreateNodesResult
| PluginCreateDependenciesResult | PluginCreateDependenciesResult
| PluginCreateMetadataResult; | PluginCreateMetadataResult
| PluginWorkerPreTasksExecutionMessageResult
| PluginWorkerPostTasksExecutionMessageResult;
export function isPluginWorkerMessage( export function isPluginWorkerMessage(
message: Serializable message: Serializable
@ -133,9 +186,11 @@ export function isPluginWorkerMessage(
'load', 'load',
'createNodes', 'createNodes',
'createDependencies', 'createDependencies',
'processProjectGraph',
'createMetadata', 'createMetadata',
'processProjectGraph',
'shutdown', 'shutdown',
'preTasksExecution',
'postTasksExecution',
].includes(message.type) ].includes(message.type)
); );
} }
@ -153,6 +208,8 @@ export function isPluginWorkerResult(
'createDependenciesResult', 'createDependenciesResult',
'processProjectGraphResult', 'processProjectGraphResult',
'createMetadataResult', 'createMetadataResult',
'preTasksExecutionResult',
'postTasksExecutionResult',
].includes(message.type) ].includes(message.type)
); );
} }

View File

@ -43,7 +43,7 @@ const MAX_MESSAGE_WAIT =
interface PendingPromise { interface PendingPromise {
promise: Promise<unknown>; promise: Promise<unknown>;
resolver: (result: any) => void; resolver: (result?: any) => void;
rejector: (err: any) => void; rejector: (err: any) => void;
} }
@ -217,6 +217,46 @@ function createWorkerHandler(
); );
} }
: undefined, : 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) { } else if (result.success === false) {
onloadError(result.error); onloadError(result.error);
@ -246,6 +286,22 @@ function createWorkerHandler(
rejector(result.error); 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 { consumeMessage, isPluginWorkerMessage } from './messaging';
import { createSerializableError } from '../../../utils/serializable-error'; import { createSerializableError } from '../../../utils/serializable-error';
import { consumeMessagesFromSocket } from '../../../utils/consume-messages-from-socket'; import { consumeMessagesFromSocket } from '../../../utils/consume-messages-from-socket';
import type { LoadedNxPlugin } from '../loaded-nx-plugin';
import { createServer } from 'net'; import { createServer } from 'net';
import { unlinkSync } from 'fs'; import { unlinkSync } from 'fs';
@ -12,7 +13,7 @@ if (process.env.NX_PERF_LOGGING === 'true') {
global.NX_GRAPH_CREATION = true; global.NX_GRAPH_CREATION = true;
global.NX_PLUGIN_WORKER = true; global.NX_PLUGIN_WORKER = true;
let connected = false; let connected = false;
let plugin; let plugin: LoadedNxPlugin;
const socketPath = process.argv[2]; const socketPath = process.argv[2];
@ -75,6 +76,10 @@ const server = createServer((socket) => {
!!plugin.processProjectGraph, !!plugin.processProjectGraph,
hasCreateMetadata: hasCreateMetadata:
'createMetadata' in plugin && !!plugin.createMetadata, 'createMetadata' in plugin && !!plugin.createMetadata,
hasPreTasksExecution:
'preTasksExecution' in plugin && !!plugin.preTasksExecution,
hasPostTasksExecution:
'postTasksExecution' in plugin && !!plugin.postTasksExecution,
success: true, 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 && m.default &&
('createNodes' in m.default || ('createNodes' in m.default ||
'createNodesV2' 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; return m.default;
} }

View File

@ -11,9 +11,13 @@ import type {
CreateNodesContextV2, CreateNodesContextV2,
CreateNodesResult, CreateNodesResult,
NxPluginV2, NxPluginV2,
PostTasksExecutionContext,
PreTasksExecutionContext,
ProjectsMetadata, ProjectsMetadata,
} from './public-api'; } from './public-api';
import { createNodesFromFiles } from './utils'; import { createNodesFromFiles } from './utils';
import { isIsolationEnabled } from './isolation/enabled';
import { isDaemonEnabled } from '../../daemon/client/client';
export class LoadedNxPlugin { export class LoadedNxPlugin {
readonly name: string; readonly name: string;
@ -35,6 +39,12 @@ export class LoadedNxPlugin {
graph: ProjectGraph, graph: ProjectGraph,
context: CreateMetadataContext context: CreateMetadataContext
) => Promise<ProjectsMetadata>; ) => Promise<ProjectsMetadata>;
readonly preTasksExecution?: (
context: PreTasksExecutionContext
) => Promise<NodeJS.ProcessEnv>;
readonly postTasksExecution?: (
context: PostTasksExecutionContext
) => Promise<void>;
readonly options?: unknown; readonly options?: unknown;
readonly include?: string[]; readonly include?: string[];
@ -107,10 +117,37 @@ export class LoadedNxPlugin {
this.createMetadata = async (graph, context) => this.createMetadata = async (graph, context) =>
plugin.createMetadata(graph, this.options, 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. // 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`. // For methods to interact with plugins from within Nx, see `./internal-api.ts`.
import { import type {
FileMap, FileMap,
ProjectGraph, ProjectGraph,
ProjectGraphExternalNode, ProjectGraphExternalNode,
} from '../../config/project-graph'; } 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 type { NxJsonConfiguration } from '../../config/nx-json';
import { RawProjectGraphDependency } from '../project-graph-builder'; import type { RawProjectGraphDependency } from '../project-graph-builder';
import type { TaskResults } from '../../tasks-runner/life-cycle';
/** /**
* Context for {@link CreateNodesFunction} * Context for {@link CreateNodesFunction}
@ -146,7 +147,7 @@ export type CreateMetadata<T = unknown> = (
) => ProjectsMetadata | Promise<ProjectsMetadata>; ) => 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> = { export type NxPluginV2<TOptions = unknown> = {
name: string; name: string;
@ -176,9 +177,37 @@ export type NxPluginV2<TOptions = unknown> = {
* Provides a function to create metadata for the {@link ProjectGraph} * Provides a function to create metadata for the {@link ProjectGraph}
*/ */
createMetadata?: CreateMetadata<TOptions>; 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; 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')); pluginsToLoad.push(join(__dirname, '../../adapter/angular-json'));
} }
const plugins = await getPlugins(); const plugins = await getPlugins(workspaceRoot);
const res = await retrieveProjectConfigurations( const res = await retrieveProjectConfigurations(
plugins, plugins,

View File

@ -1,6 +1,9 @@
import { TaskStatus } from './tasks-runner'; import { TaskStatus } from './tasks-runner';
import { Task } from '../config/task-graph'; import { Task } from '../config/task-graph';
/**
* The result of a completed {@link Task}
*/
export interface TaskResult { export interface TaskResult {
task: Task; task: Task;
status: TaskStatus; status: TaskStatus;
@ -8,6 +11,11 @@ export interface TaskResult {
terminalOutput?: string; 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 { export interface TaskMetadata {
groupId: number; groupId: number;
} }

View File

@ -34,7 +34,12 @@ import {
} from '../utils/sync-generators'; } from '../utils/sync-generators';
import { workspaceRoot } from '../utils/workspace-root'; import { workspaceRoot } from '../utils/workspace-root';
import { createTaskGraph } from './create-task-graph'; 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 { createRunManyDynamicOutputRenderer } from './life-cycles/dynamic-run-many-terminal-output-life-cycle';
import { createRunOneDynamicOutputRenderer } from './life-cycles/dynamic-run-one-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'; 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 chalk = require('chalk');
import type { Observable } from 'rxjs'; import type { Observable } from 'rxjs';
import { printPowerpackLicense } from '../utils/powerpack'; import { printPowerpackLicense } from '../utils/powerpack';
import {
runPostTasksExecution,
runPreTasksExecution,
} from '../project-graph/plugins/tasks-execution-hooks';
async function getTerminalOutputLifeCycle( async function getTerminalOutputLifeCycle(
initiatingProject: string, initiatingProject: string,
@ -177,23 +186,42 @@ export async function runCommand(
const status = await handleErrors( const status = await handleErrors(
process.env.NX_VERBOSE_LOGGING === 'true', process.env.NX_VERBOSE_LOGGING === 'true',
async () => { async () => {
await runPreTasksExecution({
workspaceRoot,
nxJsonConfiguration: nxJson,
});
const taskResults = await runCommandForTasks( const taskResults = await runCommandForTasks(
projectsToRun, projectsToRun,
currentProjectGraph, currentProjectGraph,
{ nxJson }, { nxJson },
nxArgs, {
...nxArgs,
skipNxCache:
nxArgs.skipNxCache ||
process.env.NX_SKIP_NX_CACHE === 'true' ||
process.env.NX_DISABLE_NX_CACHE === 'true',
},
overrides, overrides,
initiatingProject, initiatingProject,
extraTargetDependencies, extraTargetDependencies,
extraOptions extraOptions
); );
return Object.values(taskResults).some( const result = Object.values(taskResults).some(
(taskResult) => (taskResult) =>
taskResult.status === 'failure' || taskResult.status === 'skipped' taskResult.status === 'failure' || taskResult.status === 'skipped'
) )
? 1 ? 1
: 0; : 0;
await runPostTasksExecution({
taskResults,
workspaceRoot,
nxJsonConfiguration: nxJson,
});
return result;
} }
); );
@ -209,7 +237,7 @@ export async function runCommandForTasks(
initiatingProject: string | null, initiatingProject: string | null,
extraTargetDependencies: Record<string, (TargetDependencyConfig | string)[]>, extraTargetDependencies: Record<string, (TargetDependencyConfig | string)[]>,
extraOptions: { excludeTaskDependencies: boolean; loadDotEnvFiles: boolean } extraOptions: { excludeTaskDependencies: boolean; loadDotEnvFiles: boolean }
): Promise<{ [id: string]: TaskResult }> { ): Promise<TaskResults> {
const projectNames = projectsToRun.map((t) => t.name); const projectNames = projectsToRun.map((t) => t.name);
const { projectGraph, taskGraph } = await ensureWorkspaceIsInSyncAndGetGraphs( const { projectGraph, taskGraph } = await ensureWorkspaceIsInSyncAndGetGraphs(

View File

@ -230,7 +230,6 @@ export class TaskOrchestrator {
task: Task; task: Task;
status: 'local-cache' | 'local-cache-kept-existing' | 'remote-cache'; status: 'local-cache' | 'local-cache-kept-existing' | 'remote-cache';
}> { }> {
task.startTime = Date.now();
const cachedResult = await this.cache.get(task); const cachedResult = await this.cache.get(task);
if (!cachedResult || cachedResult.code !== 0) return null; if (!cachedResult || cachedResult.code !== 0) return null;
@ -241,7 +240,6 @@ export class TaskOrchestrator {
if (shouldCopyOutputsFromCache) { if (shouldCopyOutputsFromCache) {
await this.cache.copyFilesFromCache(task.hash, cachedResult, outputs); await this.cache.copyFilesFromCache(task.hash, cachedResult, outputs);
} }
task.endTime = Date.now();
const status = cachedResult.remote const status = cachedResult.remote
? 'remote-cache' ? 'remote-cache'
: shouldCopyOutputsFromCache : shouldCopyOutputsFromCache
@ -545,6 +543,10 @@ export class TaskOrchestrator {
// region Lifecycle // region Lifecycle
private async preRunSteps(tasks: Task[], metadata: TaskMetadata) { 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); await this.options.lifeCycle.startTasks(tasks, metadata);
} }
@ -558,7 +560,9 @@ export class TaskOrchestrator {
doNotSkipCache: boolean, doNotSkipCache: boolean,
{ groupId }: { groupId: number } { groupId }: { groupId: number }
) { ) {
const now = Date.now();
for (const task of tasks) { for (const task of tasks) {
task.endTime = now;
await this.recordOutputsHash(task); await this.recordOutputsHash(task);
} }