Nx plugin development - Build your own Vite plugin
Tags: nx, typescript, monorepo, vite
Published: 2022-12-02 16:57:25

## Overview

An Nx plugin is an npm package to extend Nx itself. The most significant parts of that are generators and executors. Generators are the way to create files from templates, and executors are runners for something like the build process to package your application. It’s easy to get started.

With the upcoming Vite integration, let us build our own simple Vite package (for educational purposes).

## Getting started

Let's start a new Nx workspace for our plugin development. Nx comes with two scripts to easily scaffold a whole workspace. These are `create-nx-workspace` and `create-nx-plugin`. The first one creates a new Nx workspace, so you probably used it already if you have ever used Nx. The second script creates an empty workspace with a new Nx plugin. Let's get started:

```bash
npx create-nx-plugin awesome-plugins --pluginName vite
```

It will create a new Nx workspace with the new plugin. The file structure looks like this:

![Nx plugin workspace](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xeusc0n7xkovi45faxtf.png)


A Nx plugin is just a regular npm package with some special marker files referred in the package.json. Here is our package.json:
```json
{
	"name": "@awesome-plugins/vite",
	"version": "0.0.1",
	"main": "src/index.js",
	"generators": "./generators.json",
	"executors": "./executors.json"
}
```

It's generated into our Vite project and will be part of the package. It is just a regular package.json, except for the additional fields `generators` and `executors`. A generator is how Nx generates files, and an executor executes tasks like build or test. With these config files referred to in the package.json, Nx can find out what our plugin can do and what Nx can run with it.

## Generators

Let's start with generators. Generators are the way to generate files in Nx. They are easy to use because they are plain Typescript with helper functions from the Nx devkit. The generator from `@nrwl/nx-plugin` (loop closed, you created an Nx plugin with an Nx plugin) created a generator called `vite` for us. 

```ts

...

export default async function (tree: Tree, options: ViteGeneratorSchema) {
  const normalizedOptions = normalizeOptions(tree, options);
  addProjectConfiguration(tree, normalizedOptions.projectName, {
    root: normalizedOptions.projectRoot,
    projectType: 'library',
    sourceRoot: `${normalizedOptions.projectRoot}/src`,
    targets: {
      build: {
        executor: '@awesome-plugins/vite:build',
      },
    },
    tags: normalizedOptions.parsedTags,
  });
  addFiles(tree, normalizedOptions);
  await formatFiles(tree);
}
```

The shown function is the primary function of our generator. Generators are functions with a given signature. The first parameter is `tree` from type `Tree`. The `Tree` object is an abstraction of the filesystem and has methods to read, write and delete files. The second parameter here is specific to the generator. Besides the `generator.ts` the nx-plugin package created two files, the `schema.d.ts` and the `schema.json`. The `schema.d.ts` looks like this:

```ts
export interface ViteGeneratorSchema {
	name: string;
	tags?: string;
	directory?: string;
}
```

It's the Typescript inteface matching our generator parameters. A generator can have any number of  parameters. If you call the generator like this:

```
nx generate @awesome-plugins/vite:vite --name=my-lib
```

The Nx cli will call your generator function and creates the object with type `ViteGeneratorSchema` filled with all your parameters. The second file is the `schema.json` and looks like this:

```json
{
	"$schema": "http://json-schema.org/schema",
	"cli": "nx",
	"$id": "Vite",
	"title": "",
	"type": "object",
	"properties": {
		"name": {
			"type": "string",
			"description": "",
			"$default": {
				"$source": "argv",
				"index": 0
			},
			"x-prompt": "What name would you like to use?"
		},
		"tags": {
			"type": "string",
			"description": "Add tags to the project (used for linting)",
			"alias": "t"
		},
		"directory": {
			"type": "string",
			"description": "A directory where the project is placed"
		}
	},
	"required": ["name"]
}
```

The `schema.json` is the cli configuration for your interface. You can set descriptions, default values, aliases, and so on for all parameters. Furthermore, it's possible to configure which parameters are required and which are optional. The Nx cli will use this file to show the descriptions to the user of our plugin and show errors, if required and the `$default` configuration for the name field allows to leave the `--name` parameter and just use the first parameter for it, like this:

```
nx generate @awesome-plugins/vite:vite my-lib
```

Now we know how to push parameters into our function, let's see what we can do with them. The `vite` generator does two main things, add some files and add a project configuration for the project generated by your generator. Let's take a look at the files first. In your generator there's a function called named `addFiles()`. This function is defined in the same file and looks like this:

```ts
function addFiles(tree: Tree, options: NormalizedSchema) {
	const templateOptions = {
		...options,
		...names(options.name),
		offsetFromRoot: offsetFromRoot(options.projectRoot),
		template: '',
	};
	
	generateFiles(
		tree,
		path.join(__dirname, 'files'),
		options.projectRoot,
		templateOptions
	);
}
```

The function `generateFiles` comes from the `@nrwl/devkit` package. The Nx devkit brings some useful functions you usually need to create plugins. (The @nrwl packages like `@nrwl/angular` or `@nrwl/react` also use them, so they're battle-tested by Nx.)

The `generateFiles` function takes the tree object into which the files should be generated. The other three parameters are the folder where the templates for the files are taken from, here the `files` directory in the same directory as the generator. The `files` directory contains a `src` dir with a file `index.ts__template__`.

![Generated files](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ap92n67ebukh2ch5783v.png)

The notation `__template__` is the syntax for variables in filenames. Variables for templates are taken from the `templateOptions` object. It is just a plain Typescript object and every matching field from the object replaces the variable. The `__template__` will be replaced with an empty string. Let's rename the file. We change the filename from `index.ts__template__` to `__name__.ts__template__`. The `name` parameter used to set the name of your generated project now also is used to define the filename of the first file. After that, we create a new file `index.html__template__` directly into the files directory and add the following content:

```
<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="UTF-8" />
		<title><%= name %></title>
	</head>
	<body>
		<div id="app"></div>
		<script type="module" src="/src/<%= name %>.ts"></script>
	</body>
</html>
```

The variable syntax for templates is `<%= name %>`. Again, they will be replaced while generating the files from the `templateOptions` object. After that, our generator directory looks like this:
![generator Vite files](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d4duepmg6mwpuwbu9m9v.png)



Our generator is now prepared to create the files we want. Let's look at the configuration of our newly generated project. Our generator contains the following snippet:

```ts
addProjectConfiguration(tree, normalizedOptions.projectName, {
	root: normalizedOptions.projectRoot,
	projectType: 'library',
	sourceRoot: `${normalizedOptions.projectRoot}/src`,
	targets: {
		build: {
			executor: '@awesome-plugins/vite:build',
		},
	},
	tags: normalizedOptions.parsedTags,
});
```

The function `addProjectConfiguration` will exactly do what it is named. For our Nx workspace, it will create the `project.json` file for us. The created file will contain the following:

```json
{
  "$schema": "../../node_modules/nx/schemas/project-schema.json",
  "name": "mygeneratedlib",
  "projectType": "library",
  "sourceRoot": "libs/mygeneratedlib/src",
  "targets": {
    "build": {
      "executor": "@awesome-plugins/vite:build"
    }
  },
  "tags": []
}
```

This is the configuration for our generated project. The whole project after you use your new plugin will be like this:
![Generated project](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tnwln6y4pcljpuhsv9q7.png)

The configuration contains essential pieces of information like the `sourceRoot` and `projectType`, together with the targets. Possible targets are something like `build` and `test`, but they can be whatever you need. To be able to call Vite, we need to be sure it is added into the package.json. To do this, add the following into the generator, right after the `addFiles` call:

```ts
...
addDependenciesToPackageJson(tree, {}, {
	vite: '^3.2.4'
});
...
```

Again, this is a function from the nx devkit that helps add dependencies and install them. It will read the package.json from the Tree and add dependencies and devDependencies.

Now we have our dependency installed, let's implement our own `build` target for Vite. 

## Executors
As already said, executors are the Nx way to do something with your project. Parameters are identical to the generators configured. There is a `schema.json` file and a `schema.d.ts` file for them. The created Vite project already contains a `build` executor, which logs something and returns `success:true`.

```ts
export default async function runExecutor(options: BuildExecutorSchema) {
	console.log('Executor ran for Build', options);
	return {
		success: true,
	};
}
```

If you now used the generator to create `mygeneratedlib`, you can use:
```
nx build mygeneratedlib
```

The cli will log the message and exit. That's it. But we want to create our great Vite package, so we need to adjust things a little. First, we add the second parameter `context: ExecutorContext` to our function signature. Now we can look into the ExecutorContext with useful pieces of information about the workspace. 
From the context, we can take the projectDir path we need for the Vite build process. Vite itself exposes a build function to start the build process, and we need to add the `root` where the process should be started. After that we choose an proper outDir to match the Nx file structure.

```ts
export default async function runExecutor(options: BuildExecutorSchema, context: ExecutorContext) {
	console.log('Executor ran for Build', options);
	
	const projectDir = context.workspace.projects[context.projectName].root;
	const distDir = joinPathFragments(`dist/${projectDir}`);
	
	await build({
		root: `${context.root}/${projectDir}`,
		build: {
			outDir: relative(projectDir, distDir)
		}
	});
	
	return {
		success: true,
	};
}
```

This is all we need to add a working Vite build process. Vite is just an example, but if you can run something from typescript, you can run it from an nx executor. Start to integrate whatever you want into Nx.

A demo workspace can be found [here](https://github.com/DominikPieper/awesome-plugins-vite-example).

## Testing
In the second part we will talk about how to test your plugin features.