Learn how to create a crystal plugin with Nx.
This document guides you through creating a custom Nx crystal plugin to add a deploy target to your
applications based on the existence of a Dockerfile.
We already have a custom executor to execute the deploy target. We have to manually configure it per project.
Of course, it would be desirable to have the project infer the deploy target whenever there is an existing Dockerfile.
Create a new file plugin.ts under tools/workspace/src/executors/plugins/.
Put the following contents as your skeleton there:
Plugins.ts skeleton
import {
CreateNodesV2,
CreateNodesResult,
CreateDependencies,
createNodesFromFiles,
CreateNodesContext,
joinPathFragments,
readJsonFile,
ProjectConfiguration,
logger,
} from '@nx/devkit';
export interface DeployPluginOptions {
buildTargetName: string;
deployTargetName: string;
organizationName: string;
repositoryName: string;
}
function normalizeOptions(
options: Partial<DeployPluginOptions> = {}
): DeployPluginOptions {
return {
deployTargetName: options.deployTargetName ?? 'deploy',
buildTargetName: options.buildTargetName ?? 'build',
organizationName: options.organizationName ?? 'push-based',
repositoryName: options.repositoryName ?? 'react-movies-app',
};
}
export const createNodesV2: CreateNodesV2<Partial<DeployPluginOptions>> = [
'**/Dockerfile',
async (dockerFiles, options, context) => {
try {
return await createNodesFromFiles(
(dockerFilePath, options, context) => {
const projectRoot = dirname(dockerFilePath);
const opts = normalizeOptions(options ?? {});
const projectPath = joinPathFragments(
context.workspaceRoot,
projectRoot,
'project.json'
);
const {
buildTargetName,
deployTargetName,
organizationName,
repositoryName,
} = opts;
const projectConfiguration = readJsonFile(
projectPath
) as ProjectConfiguration;
const projectName =
projectConfiguration.name ?? basename(projectRoot);
const targets: Record<string, TargetConfiguration> = {};
// 👇️👇️👇️👇️👇️👇️👇️
// your code goes here
return {
projects: {
[projectRoot]: {
root: projectRoot,
projectType: 'application',
targets,
},
},
};
},
dockerFiles,
options,
context
);
} catch (e) {
logger.error(e);
}
},
];Take a while to consume this code and understand what happens.
What you want to do now is to create the deploy target and add it as a new key into the targets: Record<string, TargetConfiguration>.
Take a look at the existing target in the apps/movies/project.json
You can take this as a blueprint:
{
"deploy": {
"executor": "@react-monorepo/workspace-tools:deploy",
"inputs": [
"dockerFiles",
{}
],
"options": {
"dockerFile": "tools/deploy/frontend.Dockerfile",
"tag": "ghcr.io/push-based/react-movies-app/react-movies-app:dev"
}
}
}So you need to put a new object into targets[deployTargetName] = {} which resembles the new target
you want to have.
targets
targets[deployTargetName] = {
executor: '@react-monorepo/workspace-tools:deploy',
options: {
dockerFilePath,
tag: `ghcr.io/${organizationName}/${repositoryName}/${projectName}:dev`,
},
cache: true,
dependsOn: [buildTargetName],
inputs: [
dockerFilePath,
{
dependentTasksOutputFiles: '**/dist/**/*',
transitive: true,
},
],
};Full Solution
// tools/workspace/src/plugins/plugin.ts
import {
CreateNodesV2,
CreateNodesResult,
CreateDependencies,
createNodesFromFiles,
CreateNodesContext,
joinPathFragments,
readJsonFile,
ProjectConfiguration,
logger,
} from '@nx/devkit';
export interface DeployPluginOptions {
buildTargetName: string;
deployTargetName: string;
organizationName: string;
repositoryName: string;
}
function normalizeOptions(
options: Partial<DeployPluginOptions> = {}
): DeployPluginOptions {
return {
deployTargetName: options.deployTargetName ?? 'deploy',
buildTargetName: options.buildTargetName ?? 'build',
organizationName: options.organizationName ?? 'push-based',
repositoryName: options.repositoryName ?? 'react-movies-app',
};
}
export const createNodesV2: CreateNodesV2<Partial<DeployPluginOptions>> = [
'**/Dockerfile',
async (dockerFiles, options, context) => {
try {
return await createNodesFromFiles(
(dockerFilePath, options, context) => {
const projectRoot = dirname(dockerFilePath);
const opts = normalizeOptions(options ?? {});
const projectPath = joinPathFragments(
context.workspaceRoot,
projectRoot,
'project.json'
);
const {
buildTargetName,
deployTargetName,
organizationName,
repositoryName,
} = opts;
const projectConfiguration = readJsonFile(
projectPath
) as ProjectConfiguration;
const projectName =
projectConfiguration.name ?? basename(projectRoot);
const targets: Record<string, TargetConfiguration> = {};
// 👇️👇️👇️👇️👇️👇️👇️
// your code goes here
targets[deployTargetName] = {
executor: '@react-monorepo/workspace-tools:deploy',
options: {
dockerFilePath,
tag: `ghcr.io/${organizationName}/${repositoryName}/${projectName}:dev`,
},
cache: true,
dependsOn: [buildTargetName],
inputs: [
dockerFilePath,
{
dependentTasksOutputFiles: '**/dist/**/*',
transitive: true,
},
],
};
return {
projects: {
[projectRoot]: {
root: projectRoot,
projectType: 'application',
targets,
},
},
};
},
dockerFiles,
options,
context
);
} catch (e) {
logger.error(e);
}
},
];Export the plugin.ts from the tools/workspace/src/index.ts
index.ts
// src/tools/workspace/src/index.ts
export * from './plugins/plugin';
The last step we need to perform is to enable it in the nx.json:
Open the plugins section of it and add a new entry to enable your plugin:
{
"plugins": {
"plugin": "@react-monorepo/workspace-tools",
"options": {
"buildTargetName": "build",
"deployTargetName": "deploy"
}
}
}Well done!
Now let's make it finally happen. We just have to move some files around.
Move (or better copy) the existing files from tools/deploy/ to the apps/movies/ folder.
In the end you need to have:
Dockerfile
nginx.configInside the apps/movies folder.
Remove the section that says targets in your apps/movies/project.json file.
Remove targets
{
"/// targets": {
"deploy": {
"executor": "@react-monorepo/workspace-tools:deploy",
"options": {
"dockerFile": "tools/deploy/frontend.Dockerfile",
"tag": "ghcr.io/push-based/react-movies-app/react-movies-app:dev"
}
}
}
}
GREAT JOB!!
Run the show project command to see if your plugin is working!
You should see a deploy target being added to your project.
npx nx show project movies --webrun your inferred task !!!!