Author nestjs library using Angular Schematics

Reading Time: 7 minutes

Loading

Introduction

When I create nestjs project to do proof of concept, I usually spend the first hour to install dependencies and add rules to eslintrc.js before writing any meaningful code. When I researched code generation in nestjs, I found out that angular schematics can author nestjs library to upgrade configurations and add new files. Therefore, I decide to design a library that updates prettier configuration and generates eslint prettier rule using schematics.

In this blog post, I author a nestjs library, test the library on nestjs application locally and publish it to npmjs.com.

let's go

Install essential dependencies to author nestjs library

npm install -g @angular-devkit/schematics-clinpm install --save-dev --save-exact cpx@1.5.0 lodash@4.17.21 rimraf@3.0.2 typescript@4.7.2
npm install --save-exact @angular-devkit/core@13.3.0 @angular-devkit/schematics@13.3.0 @schematics/angular@13.3.0

Generate new schematics project to author nestjs library

schematics blank --name=nest-prettier

My habit is to rename src to schematics

nest-prettier
├── package.json
├── package-lock.json
├── README.md
├── schematics
│   ├── collection.json
│   └── nest-prettier
│       ├── index_spec.ts
│       └── index.ts
└── tsconfig.json

Update collection.json

As the name implies, collection.json is a collection of schematics in the library. In this library, I have one schematic that is nest-add that I can install via

nest add nest-prettier

to apply changes within the the latest version of nestjs application

{
  "$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json",
  "schematics": {
    "nest-add": {
      "description": "Run prettier as an eslint rule.",
      "factory": "./nest-prettier/index#nestPrettier",
      "schema": "./nest-prettier/schema.json"
    }
  }
}

Factory method is nestPrettier in index.ts and schema.json contains a list of questions that prompts users to answer. When users answer them, properties are updated in the schematic context

// schema.ts 

export interface Schema {
  printWidth: number
  tabWidth: number
  useTabs: boolean
  semi: boolean
  singleQuote: boolean
  trailComma: string
  bracketSpacing: boolean
  eslintFileFormat: string
  quoteProps: string
  arrowParens: string
  requirePragma: boolean
  insertPragma: boolean
}

Define the build process of the schematics

When authoring nestjs library, we intend to build and publish the library to npmjs.com for other developers to use. First, we configure tsconfig.json and add new npm scripts in package.json.

// tsconfig.json
{
  "compilerOptions": {
    ...
    "rootDir": "schematics",
    "outDir": "dist",
    ...
  },
  "include": ["schematics/**/*"],
  "exclude": ["schematics/*/files/**/*"]
}

Second, We change “rootDir” from “src” to “schematics” and add "outDir": "dist" to build the schematic in dist/ directory. We modify include array to include files in schematics/ and excludes templates in files/ from compilation.

Then, we tell schematics where to find the collection.json in package.json.

"schematics": "./dist/collection.json",

nest-prettier is a dev dependency in nestjs application

"nest-add": {
  "save": "devDependencies"
},

Next, add prebuild, copy:collection, copy:schematics, postbuild and manual-test scripts in package.json

"scripts": {
    "prebuild": "rimraf dist",
    "build": "tsc -p tsconfig.json",
    "copy:collection": "cpx schematics/collection.json dist && cpx 'schematics/**/schema.json' dist",
    "copy:schematics": "cpx 'schematics/**/files/**' dist",
    "postbuild": "npm run copy:collection && npm run copy:schematics",
    "manual-test": "npm run build && schematics .:nest-add"
}

It is important to copy collection.json, schematic.json and templates in post build stage.

Finally, manual-test script permits us to test the schematic in debug mode without generating new output file.

Create factory method of the schematic

// nest-prettier/index.ts

export function nestPrettier(options: Schema): Rule {
  return (tree: Tree, context: SchematicContext) => {
    context.addTask(new NodePackageInstallTask())

    addDependencies(tree, context, [prettier, eslintConfigPrettier, eslintPluginPrettier])
    return chain([addPrettierConfig(options), addEsnlintPrettier(options)])
  }
}

Let’s outline the main flow of the schematic

  • Add NodePackageInstallTask to install the library that is mandatory
  • Update package.json of nestjs application to install prettier, eslint-config-prettier and eslint-plugin-prettier dev dendencies
  • Create a single rule that updates .prettierrc and generates the eslint prettier rule from eslintrc-prettier.template template

Implement new schematic rules

In this simple nestjs library, there are 2 rules:

addPrettierConfig – this rule overwrites .prettierrc with user inputs

addEslintPrettier – this rule generates the eslint prettier rule in eslintrc-prettier.template that we have to manually copy to .eslintrc.js. It is because schematics does not have API that can update javaScript file easily.

Overwrite Prettier configuration file

export function addPrettierConfig(options: Schema): Rule {
  return (tree: Tree, context: SchematicContext) => {
    const prettierOptions: Record<string, string | number | boolean> = {
      singleQuote: options.singleQuote,
      trailingComma: options.trailComma,
      tabWidth: options.tabWidth,
      semi: options.semi,
      printWidth: options.printWidth,
      bracketSpacing: options.bracketSpacing,
      useTabs: options.useTabs,
      arrowParens: options.arrowParens,
      quoteProps: options.quoteProps,
      requirePragma: options.requirePragma,
      insertPragma: options.insertPragma,
    }

    const configFileName = '.prettierrc'
    if (tree.exists(configFileName)) {
      tree.overwrite(configFileName, JSON.stringify(prettierOptions, null, 2))
      context.logger.info(`${configFileName} is overwritten`)
    } else {
      tree.create(configFileName, JSON.stringify(prettierOptions, null, 2))
      context.logger.info(`Created ${configFileName}`)
    }
    return tree
  }
}

The code loads the options from Schema interface and invokes Tree API to overwrite .prettierrc with the new format options.

Copy eslint prettier rule to eslint configuration file configuration file

First, we create a template, eslint-prettier.template in nest-prettier/files directory.

extends: ['plugin:prettier/recommended']
rules: {
    'prettier/prettier': ['error', {
        'singleQuote': <%= singleQuote %>, 
        'trailingComma': '<%= trailComma %>', 
        'tabWidth': <%= tabWidth %>, 
        'semi': <%= semi %>, 
        'printWidth': <%= printWidth %>, 
        'bracketSpacing': <%= bracketSpacing %>, 
        'useTabs': <%= useTabs %>,
        'arrowParens': '<%= arrowParens %>',
        'quoteProps': '<%= quoteProps %>',
        'requirePragma': <%= requirePragma %>,
        'insertPragma': <%= insertPragma %>, 
    }]
}

<% singleQuote %> is a parameterized variable that will be replaced by singleQuote property of Schema interface. It is also true to the rest of the parameterized variables.

// eslint-prettier.helper.ts

import { apply, Rule, SchematicContext, template, Tree, mergeWith, url } from '@angular-devkit/schematics'
import { strings } from '@angular-devkit/core'

export function addEslintPrettier(options: Schema): Rule {
  return (tree: Tree, context: SchematicContext) => {
    const configFileName = '.eslintrc.js'
    const buffer = tree.read(configFileName)
    if (!buffer) {
      return tree
    }

    ...
    const eslintTemplate = './eslintrc-prettier.template'
    if (tree.exists(eslintTemplate)) {
       tree.delete(eslintTemplate)
    }

    const sourceTemplate = url('./files')
    const sourceParameterizedTemplate = apply(sourceTemplate, [
        template({
          ...options,
          ...strings,
        }),
    ])
    
    return mergeWith(sourceParameterizedTemplate)(tree, context)
  }
}

url('/files') reads all the files in nest-prettier/files directory; apply replaces variables with option values and mergeWith adds the templates to the source tree of the nestjs application.

As the result, the rules have set up to achieve file format in prettier and eslint configuration files. Before publishing the library, we should test the schematic in debug mode and/or in a local nestjs application

Test in debug mode after authoring a nestjs library

npm run manual-test  

compiles the codes to dist/ directory and executes schematics .:nest-add.

We choose our prettier configuration in debug mode. Suppose the schematic is correct, the outcome is to overwrite .prettierrc and create eslintrc-prettier.template.

Debug mode enabled by default for local collections.
? Eslint file format. javascript
? What is the print width per line? 120
? Specify the number of spaces per indentation-level. 2
? Indent lines with tabs instead of spaces. No
? Print semicolons at the ends of statements. Yes
? Use single quotes instead of double quotes. No
? Change when properties in objects are quoted. as-needed
? Print trailing commas wherever possible in multi-line comma-separated syntactic structures. all
? Print spaces between brackets in object literals. Yes
? Include parentheses around a sole arrow function parameter. always
? Prettier can restrict itself to only format files that contain a special comment, called a pragma, at the top of the 
file. This is very useful when gradually transitioning large, unformatted codebases to prettier. No
? Prettier can insert a special @format marker at the top of files specifying that the file has been formatted with 
prettier. This works well when used in tandem with the --require-pragma option. No
    Found prettier@2.7.1, do not add dependency
    Found eslint-config-prettier@8.5.0, do not add dependency
    Found eslint-plugin-prettier@4.2.1, do not add dependency
    .prettierrc is overwritten
    Does not support .eslintrc.js
    Append plugin and rule from ./eslintrc-prettier.template to .eslintrc.js. Then, delete the template file.
CREATE eslintrc-prettier.template (425 bytes)
UPDATE .prettierrc (259 bytes)
Dry run enabled by default in debug mode. No files written to disk.

Test in nestjs application after authoring a nestjs library

Debug mode does not create anything; therefore, I suggest to test the library in a local nestjs application.

First, we run npm link command to link the nest-prettier dependency to nest-prettier library

npm link ../nest-prettier

Then, execute nest-add schematic of the nest-prettier library

schematics nest-prettier:nest-add
? Eslint file format. javascript
? What is the print width per line? 140
? Specify the number of spaces per indentation-level. 2
? Indent lines with tabs instead of spaces. No
? Print semicolons at the ends of statements. Yes
? Use single quotes instead of double quotes. No
? Change when properties in objects are quoted. as-needed
? Print trailing commas wherever possible in multi-line comma-separated syntactic structures. all
? Print spaces between brackets in object literals. Yes
? Include parentheses around a sole arrow function parameter. always
? Prettier can restrict itself to only format files that contain a special comment, called a pragma, at the top of the file.
 This is very useful when gradually transitioning large, unformatted codebases to prettier. No
? Prettier can insert a special @format marker at the top of files specifying that the file has been formatted with 
prettier. This works well when used in tandem with the --require-pragma option. No
    Found prettier@2.7.1, do not add dependency
    Found eslint-config-prettier@8.5.0, do not add dependency
    Found eslint-plugin-prettier@4.2.1, do not add dependency
    .prettierrc is overwritten
    Does not support .eslintrc.js
    Append plugin and rule from ./eslintrc-prettier.template to .eslintrc.js. Then, delete the template file.
CREATE eslintrc-prettier.template (425 bytes)
UPDATE .prettierrc (259 bytes)
✔ Packages installed successfully.

We verify that schematic has replaced the variables of .eslintrc-prettier.template with option values

extends: ['plugin:prettier/recommended']
rules: {
    'prettier/prettier': ['error', {
        'singleQuote': false, 
        'trailingComma': 'all', 
        'tabWidth': 2, 
        'semi': true, 
        'printWidth': 140, 
        'bracketSpacing': true, 
        'useTabs': false,
        'arrowParens': 'always',
        'quoteProps': 'as-needed',
        'requirePragma': false,
        'insertPragma': false, 
    }]
}

The manual work is to copy “prettier/prettier” and prettier plugin to .eslintrc.js

// .eslintrc.js

extends: [
  'plugin:@typescript-eslint/recommended',
  'plugin:prettier/recommended'
],

rules: {
    '@typescript-eslint/interface-name-prefix': 'off',
    '@typescript-eslint/explicit-function-return-type': 'off',
    '@typescript-eslint/explicit-module-boundary-types': 'off',
    '@typescript-eslint/no-explicit-any': 'off',
    'prettier/prettier': ['error', {
      'singleQuote': false, 
      'trailingComma': 'all', 
      'tabWidth': 2, 
      'semi': true, 
      'printWidth': 140, 
      'bracketSpacing': true, 
      'useTabs': false,
      'arrowParens': 'always',
      'quoteProps': 'as-needed',
      'requirePragma': false,
      'insertPragma': false, 
    }]
},

Publish the nestjs library

My experience is to publish the library on the command line. We need to create an account in npmjs.com and enable 2FA to secure the account.

Subsequently, go back to the command line to add user on the machine

npm adduser

npm notice Log in on https://registry.npmjs.org/
Username:
Password: 
Email: (this IS public)

Then, publish the library and input OTP to deploy to npmjs.com

npm publish
Enter OTP:

Final thoughts

In this post, we have seen that writing nestjs library is made possible by Angular schematics. After testing the library locally, we can publish it to npmjs.com, execute nest add command to install the library and generates code in nestjs application

This is the end of the blog post and I hope you like the content and continue to follow my learning experience in Angular and other technologies.

Resources:

  1. nest-prettier library: https://www.npmjs.com/package/nest-prettier
  2. Generating code using schematics: https://angular.io/guide/schematics