Create own default plugin to NX workspace
Tags: nx, monorepo
Published: 2022-05-27 12:40:09

I love working with Nrwl [Nx](https://nx.dev/). It has increased my productivity and "forced" me to write better code and better architecture to my projects. NX is also great because there are lot of plugins available for Nx to generate libraries and apps for different frameworks. These plugins are available from [Nrwl](https://nrwl.io) and also from [community](https://nx.dev/community).

Sometimes there are some small differencies how you might want to generate your libraries. Perhaps you have some specific options you always select or there are some extra files you want to include. For this purpose there are custom generators. You can create your own custom generators which may compose and extend other existing generators. This makes it possible to generate your libraries as you like so that you don't have to manually modify those every time.

## Own Plugin

You can create own plugin inside your Nx workspace.

- First add nx-plugin package: `npm install @nrwl/nx-plugin -D`
- Then generate new plugin: `npx nx generate @nrwl/nx-plugin:plugin workspace-extensions`

Now you will have new `workspace-extensions` -library inside your libs.

## Set your plugin as default collection

To be able to run generators from your own plugin by default you can set it as default collection to your `nx.json` -configuration:

```json
...
"cli": {
	"defaultCollection": "@santicon/workspace-extensions"
},
...
```

Now you can create plugin with `npx nx g my-lib` instead of specifying the plugin as `npx nx g @santicon/workspace-extensions:lib my-lib`.

## Create new generator to your plugin

You can add new generators to your plugin using Nx. Im going to create generator for generating react libraries with my custom defaults. First I generate my generator to my plugin:

```sh
npx nx generate @nrwl/nx-plugin:generator react-library --project=workspace-extensions
```

Then I will modify the generator to run default react library generator from `@nrwl/react`.

First I just copy properties from original react library generator [schema.json](https://github.com/nrwl/nx/blob/master/packages/react/src/generators/library/schema.json) and typescript definitions from [schema.d.ts](https://github.com/nrwl/nx/blob/master/packages/react/src/generators/library/schema.d.ts). I could keep all the options and only change default values or I can add my own options.

This time I changed `css` to be `none` (because I will use tailwindcss) and builder to `swc`. I use linting rules to enforce module boundaried using tags. Therefore I added my own required inputs for `scope`, `type` and `domain`.

### Schema

```json
// schema.json
{
  "$schema": "http://json-schema.org/schema",
  "cli": "nx",
  "$id": "ReactLibrary",
  "title": "",
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "description": "Library name",
      "$default": {
        "$source": "argv",
        "index": 0
      },
      "x-prompt": "What name would you like to use for the library?",
      "pattern": "^[a-zA-Z].*$"
    },
    "directory": {
      "type": "string",
      "description": "A directory where the lib is placed.",
      "alias": "dir"
    },
    "domain": {
      "description": "Domain where this library belongs.",
      "type": "string",
      "default": "none",
      "alias": "dom",
      "x-prompt": {
        "message": "Which domain this library belongs?",
        "type": "list",
        "items": [
          {
            "value": "web",
            "label": "Web"
          },
          {
            "value": "common",
            "label": "Common"
          }
        ]
      }
    },
    "scope": {
      "type": "string",
      "description": "A scope for the lib.",
      "alias": "sc"
    },
    "type": {
      "description": "Library type",
      "type": "string",
      "alias": "t",
      "x-prompt": {
        "message": "Select library type?",
        "type": "list",
        "items": [
          {
            "value": "data",
            "label": "Data"
          },
          {
            "value": "model",
            "label": "Model"
          },
          {
            "value": "util",
            "label": "Util"
          },
          {
            "value": "feature",
            "label": "Feature"
          },
          {
            "value": "ui",
            "label": "Ui"
          }
        ]
      }
    },
    "style": {
      "description": "The file extension to be used for style files.",
      "type": "string",
      "default": "none",
      "alias": "s",
      "x-prompt": {
        "message": "Which stylesheet format would you like to use?",
        "type": "list",
        "items": [
          { "value": "css", "label": "CSS" },
          {
            "value": "scss",
            "label": "SASS(.scss)       [ http://sass-lang.com          ]"
          },
          {
            "value": "styl",
            "label": "Stylus(.styl)     [ http://stylus-lang.com        ]"
          },
          {
            "value": "less",
            "label": "LESS              [ http://lesscss.org            ]"
          },
          {
            "value": "styled-components",
            "label": "styled-components [ https://styled-components.com ]"
          },
          {
            "value": "@emotion/styled",
            "label": "emotion           [ https://emotion.sh            ]"
          },
          {
            "value": "styled-jsx",
            "label": "styled-jsx        [ https://www.npmjs.com/package/styled-jsx ]"
          },
          {
            "value": "none",
            "label": "None"
          }
        ]
      }
    },
    "linter": {
      "description": "The tool to use for running lint checks.",
      "type": "string",
      "enum": ["eslint", "tslint"],
      "default": "eslint"
    },
    "unitTestRunner": {
      "type": "string",
      "enum": ["jest", "none"],
      "description": "Test runner to use for unit tests.",
      "default": "jest"
    },
    "skipFormat": {
      "description": "Skip formatting files.",
      "type": "boolean",
      "default": false
    },
    "skipTsConfig": {
      "type": "boolean",
      "default": false,
      "description": "Do not update `tsconfig.json` for development experience."
    },
    "pascalCaseFiles": {
      "type": "boolean",
      "description": "Use pascal case component file name (e.g. `App.tsx`).",
      "alias": "P",
      "default": false
    },
    "routing": {
      "type": "boolean",
      "description": "Generate library with routes."
    },
    "appProject": {
      "type": "string",
      "description": "The application project to add the library route to.",
      "alias": "a"
    },
    "publishable": {
      "type": "boolean",
      "description": "Create a publishable library."
    },
    "buildable": {
      "type": "boolean",
      "default": false,
      "description": "Generate a buildable library."
    },
    "importPath": {
      "type": "string",
      "description": "The library name used to import it, like `@myorg/my-awesome-lib`."
    },
    "component": {
      "type": "boolean",
      "description": "Generate a default component.",
      "default": true
    },
    "js": {
      "type": "boolean",
      "description": "Generate JavaScript files rather than TypeScript files.",
      "default": false
    },
    "globalCss": {
      "type": "boolean",
      "description": "When `true`, the stylesheet is generated using global CSS instead of CSS modules (e.g. file is `*.css` rather than `*.module.css`).",
      "default": false
    },
    "strict": {
      "type": "boolean",
      "description": "Whether to enable tsconfig strict mode or not.",
      "default": true
    },
    "setParserOptionsProject": {
      "type": "boolean",
      "description": "Whether or not to configure the ESLint `parserOptions.project` option. We do not do this by default for lint performance reasons.",
      "default": false
    },
    "standaloneConfig": {
      "description": "Split the project configuration into `<projectRoot>/project.json` rather than including it inside `workspace.json`.",
      "type": "boolean"
    },
    "compiler": {
      "type": "string",
      "enum": ["babel", "swc"],
      "default": "swc",
      "description": "Which compiler to use."
    }
  },
  "required": ["name", "type", "scope", "domain"]
}
```

### Type definitions for Schema

```typescript
// schema.d.ts
import { SupportedStyles } from '@nrwl/react';
export interface ReactLibraryGeneratorSchema {
  name: string;
  directory?: string;
  style: SupportedStyles;
  skipTsConfig: boolean;
  skipFormat: boolean;
  pascalCaseFiles?: boolean;
  routing?: boolean;
  appProject?: string;
  unitTestRunner: 'jest' | 'none';
  linter: Linter;
  component?: boolean;
  publishable?: boolean;
  buildable?: boolean;
  importPath?: string;
  js?: boolean;
  globalCss?: boolean;
  strict?: boolean;
  setParserOptionsProject?: boolean;
  standaloneConfig?: boolean;
  compiler?: 'babel' | 'swc';
  domain: 'web' | 'common';
  type: 'data' | 'model' | 'util' | 'feature' | 'ui';
  scope: string;
}
```

### Generator

Actual generator will be very simple. It will run the original generator imported from `@nrwl/react`. It will setup tags for type, scope and domain from options. Set the framework tag to be `react`. Then it will add files from plugin templates.

```typescript
// generator.ts
import {
  generateFiles,
  getWorkspaceLayout,
  joinPathFragments,
  names,
  Tree,
} from '@nrwl/devkit';
import { libraryGenerator } from '@nrwl/react';
import { ReactLibraryGeneratorSchema } from './schema';

export default async function (
  tree: Tree,
  options: ReactLibraryGeneratorSchema,
) {
  const { libsDir } = getWorkspaceLayout(tree);
  const name = names(options.name).fileName;
  const projectDirectory = options.directory
    ? `${names(options.directory).fileName}/${name}`
    : name;
  const projectRoot = joinPathFragments(libsDir, projectDirectory);
  const tags = `project:${options.domain},scope:${options.scope},type:${options.type},framework:react`;

  // run the original generator
  await libraryGenerator(tree, { ...options, tags });

  // Add own custom files
  generateFiles(
    tree, // the virtual file system
    joinPathFragments(__dirname, './files'), // path to the file templates)
    projectRoot, // destination path of the files
    { ...options, name: names(options.name).className, tmpl: '' }, // config object to replace variable in file templates
  );
}
```

### Template

README.md__tmpl__:

```markdown
# <%= name %>

This is React library for <%= domain %>

type: <%= type %>
scope: <%= scope %>

```

**Note!** When you use VS code and NX Console you have to reload window before changes to your generator will be visible to NX Console.

## Testing

What is great with NX Plugin is that it comes with unit and e2e tests. I see e2e tests to be specially helpfull when writing my generators because those create new tmp workspace under `path_to_your_workspace/tmp/nx-e2e`. It's easy to check that generated files are correct withour polluting your own repo with test libraries. It's also easy to add unit tests for generators. Here is small example how to test that generator is adding files correctly.

```typescript
// generator.spec.ts
// ...
  it('should create readme', async () => {
    await generator(appTree, options);
    const readme = appTree.read('libs/test/README.md');
    expect(readme.toString()).toMatchInlineSnapshot(`
      "# Test

      This is React library for web

      type: data
      scope: shared
      "
    `);
  });
```

## Next steps

Now I have simple plugin that I can extend to create more complex generators. Remember that we can always run generators from other plugins and only add what is missing. Best place to get inspiration what is possible is to look at existing plugins from [NX repo](https://github.com/nrwl/nx). There is also good documentation in [NX Docs](https://nx.dev/packages/nx-plugin#generating-a-plugin).