Skip to content

Latest commit

 

History

History
365 lines (286 loc) · 8.18 KB

cristal-plugin.md

File metadata and controls

365 lines (286 loc) · 8.18 KB

Nx Inferred Tasks

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.

0. The Skeleton

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.

1. The logic

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

2. Export

Export the plugin.ts from the tools/workspace/src/index.ts

index.ts
// src/tools/workspace/src/index.ts

export * from './plugins/plugin';

3. Enable on nx.json

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!

4. Adjust the movies project

Now let's make it finally happen. We just have to move some files around.

4.1 Move the Dockerfile

Move (or better copy) the existing files from tools/deploy/ to the apps/movies/ folder.

In the end you need to have:

Dockerfile
nginx.config

Inside the apps/movies folder.

4.2 Remove the deploy target

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"
      }
    }
  }
}

5. Run nx show project movies --web

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

6. Execute!

run your inferred task !!!!