Enforce Organizational Best Practices with a Local Plugin

Every repository has a unique set of conventions and best practices that developers need to learn in order to write code that integrates well with the rest of the code base. It is important to document those best practices, but developers don't always read the documentation and even if they have read the documentation, they don't consistently follow the documentation every time they perform a task. Nx allows you to encode these best practices in code generators that have been tailored to your specific repository.

In this tutorial, we will create a generator that helps enforce the follow best practices:

  • Every project in this repository should use Vitest for unit tests.
  • Every project in this repository should be tagged with a scope:* tag that is chosen from the list of available scopes.
  • Projects should be placed in folders that match the scope that they are assigned.
  • Vitest should clear mocks before running tests.

Get Started

Let's first create a new workspace with the create-nx-workspace command:

npx create-nx-workspace myorg --preset=react-monorepo --ci=github

Then we , install the @nx/plugin package and generate a plugin:

npx nx add @nx/plugin

npx nx g @nx/plugin:plugin tools/recommended

This will create a recommended project that contains all your plugin code.

Create a Customized Library Generator

To create a new generator run:

npx nx generate @nx/plugin:generator tools/recommended/src/generators/library

The new generator is located in tools/recommended/src/generators/library. The generator.ts file contains the code that runs the generator. We can delete the files directory since we won't be using it and update the generator.ts file with the following code:

tools/recommended/src/generators/library/generator.ts
1import { Tree } from '@nx/devkit'; 2import { Linter } from '@nx/eslint'; 3import { libraryGenerator as reactLibraryGenerator } from '@nx/react'; 4import { LibraryGeneratorSchema } from './schema'; 5 6export async function libraryGenerator( 7 tree: Tree, 8 options: LibraryGeneratorSchema 9) { 10 const callbackAfterFilesUpdated = await reactLibraryGenerator(tree, { 11 ...options, 12 linter: Linter.EsLint, 13 style: 'css', 14 unitTestRunner: 'vitest', 15 }); 16 17 return callbackAfterFilesUpdated; 18} 19 20export default libraryGenerator; 21

Notice how this generator is calling the @nx/react plugin's library generator with a predetermined list of options. This helps developers to always create projects with the recommended settings.

We're returning the callbackAfterFilesUpdated function because the @nx/react:library generator sometimes needs to install packages from NPM after the file system has been updated by the generator. You can provide your own callback function instead, if you have tasks that rely on actual files being present.

To try out the generator in dry-run mode, use the following command:

npx nx g @myorg/recommended:library test-library --dry-run

Remove the --dry-run flag to actually create a new project.

Add Generator Options

The schema.d.ts file contains all the options that the generator supports. By default, it includes only a name option. Let's add a directory option to pass on to the @nx/react generator.

tools/recommended/src/generators/library/schema.d.ts
1export interface LibraryGeneratorSchema { 2 name: string; 3 directory?: string; 4} 5
More details

The schema.d.ts file is used for type checking inside the implementation file. It should match the properties in schema.json.

The schema files not only provide structure to the CLI, but also allow Nx Console to show an accurate UI for the generator.

Nx Console UI for the library generator

Notice how we made the description argument optional in both the JSON and type files. If we call the generator without passing a directory, the project will be created in a directory with same name as the project. We can test the changes to the generator with the following command:

npx nx g @myorg/recommended:library test-library --directory=nested/directory/test-library --dry-run

Choose a Scope

It can be helpful to tag a library with a scope that matches the application it should be associated with. With these tags in place, you can set up rules for how projects can depend on each other. For our repository, let's say the scopes can be store, api or shared and the default directory structure should match the chosen scope. We can update the generator to encourage developers to maintain this structure.

tools/recommended/src/generators/library/schema.d.ts
1export interface LibraryGeneratorSchema { 2 name: string; 3 scope: string; 4 directory?: string; 5} 6

We can check that the scope logic is being applied correctly by running the generator again and specifying a scope.

npx nx g @myorg/recommended:library test-library --scope=shared --dry-run

This should create the test-library in the shared folder.

Configure Tasks

You can also use your Nx plugin to configure how your tasks are run. Usually, organization focused plugins configure tasks by modifying the configuration files for each project. If you have developed your own tooling scripts for your organization, you may want to create an executor or infer tasks, but that process is covered in more detail in the tooling plugin tutorial.

Let's update our library generator to set the clearMocks property to true in the vitest configuration. First we'll run the reactLibraryGenerator and then we'll modify the created files.

tools/recommended/src/generators/library/generator.ts
1import { formatFiles, Tree, runTasksInSerial } from '@nx/devkit'; 2import { Linter } from '@nx/eslint'; 3import { libraryGenerator as reactLibraryGenerator } from '@nx/react'; 4import { LibraryGeneratorSchema } from './schema'; 5 6export async function libraryGenerator( 7 tree: Tree, 8 options: LibraryGeneratorSchema 9) { 10 const directory = options.directory || `${options.scope}/${options.name}`; 11 12 const tasks = []; 13 tasks.push( 14 await reactLibraryGenerator(tree, { 15 ...options, 16 tags: `scope:${options.scope}`, 17 directory, 18 linter: Linter.EsLint, 19 style: 'css', 20 unitTestRunner: 'vitest', 21 }) 22 ); 23 24 updateViteConfiguration(tree, directory); 25 await formatFiles(tree); 26 27 return runTasksInSerial(...tasks); 28} 29 30function updateViteConfiguration(tree, directory) { 31 // Read the vite configuration file 32 let viteConfiguration = 33 tree.read(`${directory}/vite.config.ts`)?.toString() || ''; 34 35 // Modify the configuration 36 // This is done with a naive search and replace, but could be done in a more robust way using AST nodes. 37 viteConfiguration = viteConfiguration.replace( 38 `globals: true,`, 39 `globals: true,\n clearMocks:true,` 40 ); 41 42 // Write the modified configuration back to the file 43 tree.write(`${directory}/vite.config.ts`, viteConfiguration); 44} 45 46export default libraryGenerator; 47

We updated the generator to use some new helper functions from the Nx devkit. Here are a few functions you may find useful. See the full API reference for all the options.

Now let's check to make sure that the clearMocks property is set correctly by the generator. First, we'll commit our changes so far. Then, we'll run the generator without the --dry-run flag so we can inspect the file contents.

git add .

git commit -am "library generator"

npx nx g @myorg/recommended:library store-test --scope=store

Next Steps

Now that we have a working library generator, here are some more topics you may want to investigate.

Encourage Adoption

Once you have a set of generators in place in your organization's plugin, the rest of the work is all communication. Let your developers know that the plugin is available and encourage them to use it. These are the most important points to communicate to your developers:

  • Whenever there are multiple plugins that provide a generator with the same name, use the recommended version
  • If there are repetitive or error prone processes that they identify, ask the plugin team to write a generator for that process

Now you can go through all the README files in the repository and replace any multiple step instructions with a single line calling a generator.