Create own default plugin to NX workspace

diginikkari

Timo Santi

Posted on May 27, 2022

Create own default plugin to NX workspace

I love working with Nrwl Nx. 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 and also from 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:

...
"cli": {
    "defaultCollection": "@santicon/workspace-extensions"
},
...
Enter fullscreen mode Exit fullscreen mode

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:

npx nx generate @nrwl/nx-plugin:generator react-library --project=workspace-extensions
Enter fullscreen mode Exit fullscreen mode

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 and typescript definitions from 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

// 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"]
}
Enter fullscreen mode Exit fullscreen mode

Type definitions for Schema

// 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;
}
Enter fullscreen mode Exit fullscreen mode

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.

// 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
  );
}
Enter fullscreen mode Exit fullscreen mode

Template

README.md_tmpl_:

# <%= name %>

This is React library for <%= domain %>

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

Enter fullscreen mode Exit fullscreen mode

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.

// 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
      "
    `);
  });
Enter fullscreen mode Exit fullscreen mode

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. There is also good documentation in NX Docs.

πŸ’– πŸ’ͺ πŸ™… 🚩
diginikkari
Timo Santi

Posted on May 27, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related