Write Ng Add Schematics for Library in Monorepo

In our monorepo, we have about 30-40 projects under library. We use custom schematics to integrate our library with Angular CLI. This post shows how we create schematic for ng add  to enhance the initial installation process for our consumers.

Our initial workspace/library structure is shown as:

/<workspace>/
  apps/
  libs/
    autocomplete/
    button/              

Set Up

1) First, we'd create two folders: i) a schematics directory at each project's root level and ii) ng-add directory under schematics.

/<workspace>/
  apps/
  libs/
    autocomplete/
      schematics/
        ng-add/
    button/
      schematics/
        ng-add/

2) Inside the schematics folder, create a collection.json file with the content as:

{
  "$schema": "../../../../node_modules/@angular-devkit/schematics/collection-schema.json",
  "schematics": {
    "ng-add": {
      "description": "Add required package to the module.",
      "factory": "./ng-add/index#ngAdd"
    }
  }
}
<workspace>/libs/{projectName}/schematics/collection.json

3) Then create  index.ts inside ng-add folder.

import {
  SchematicContext,
  Tree,
} from '@angular-devkit/schematics';
import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks';
import {
  addPackageJsonDependency,
  NodeDependency,
  NodeDependencyType,
} from '@schematics/angular/utility/dependencies';

export const ngAdd = () => (tree: Tree, context: SchematicContext): Tree => {
  [
    '@angular/cdk: ^9.2.4',
    '@angular/common: ^9.1.0',
    '@angular/core: ^9.1.0',
    '@angular/flex-layout: ~9.0.0-beta.29',
    '@angular/forms: ^9.1.0',
    '@angular/material: ^9.1.0',
    '@angular/platform-browser: ^9.1.0',
    'autocomplete: ^1.0.0', 
  ].map(p => {
    const individualPackage = p.split(':');
    const nodeDependency: NodeDependency = {
      type: NodeDependencyType.Default,
      name: individualPackage[0],
      version: individualPackage[1],
      overwrite: false,
    };
    addPackageJsonDependency(tree, nodeDependency);
    context.logger.info(
      `Added dependency: ${individualPackage[0]}@${
        individualPackage[1]
      }`,
    );
    context.addTask(new NodePackageInstallTask());
  });
  return tree;
};
<workspace>/libs/{projectName}/schematics/ng-add/index.ts

All the packages specified inside ngAdd function are the dependency packages for this library. The dependency package's version could also be retrieved via NPM to get latest one. They are specified here due to our library projects are required to stay at certain version.

addPackageJsonDependency helper function injects all the specified dependency packages as well as their version numbers into consumer's package.json. Then context.addTask(new NodePackageInstallTask()) is to install all those packages.

The file structure now looks as follows:

/<workspace>/
  apps/
  libs/
    autocomplete/
      schematics/
        ng-add/
          index.ts
        collection.json
    button/
      schematics/
        ng-add/
          index.ts
        collection.json

Build Schematics

Now we've got our schematics scripts ready, next we'd craft schematics build script.

1) At project level, add tsconfig.schematics.json

{
  "compilerOptions": {
    "resolveJsonModule": true,
    "esModuleInterop": true,
    "baseUrl": ".",
    "lib": [
      "es2018",
      "dom"
    ],
    "declaration": true,
    "module": "commonjs",
    "moduleResolution": "node",
    "noEmitOnError": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitAny": true,
    "noImplicitThis": true,
    "noUnusedParameters": true,
    "noUnusedLocals": true,
    "rootDir": "schematics",
    "outDir": "../../../dist/libs/{projectName}/schematics",
    "skipDefaultLibCheck": true,
    "skipLibCheck": true,
    "sourceMap": true,
    "strictNullChecks": true,
    "target": "es6",
    "types": [
      "jest",
      "node"
    ]
  },
  "include": [
    "schematics/**/*"
  ],
  "exclude": [
    "schematics/*/files/**/*"
  ]
}
<workspace>/libs/{projectName}/tsconfig.schematics.json

The rootDir specifies the schematics/ folder. The outDir maps to the library's output folder.

2) Add script to project's package.json to compile schematics source files into library bundle

"scripts": {
   "build": "../../../node_modules/.bin/tsc -p tsconfig.schematics.json",
   "copy:collection": "cp schematics/collection.json ../../../dist/libs/ui/{projectName}/schematics/collection.json",
   "postbuild": "npm run copy:collection"
 },
 "schematics": "./schematics/collection.json"
/workspace/libs/{projectName}/package.json

Till now, all the set up and build script have been done. Next we compile the schematics into our build. First, run the project build. In our example, we use nx for our build.

nx build {projectName}

Then we'd build our schematics. Go to project level and run

yarn run build

Make sure you run the project build first, before building schematics. Because schematics needs to be added into the build bundle, at appropriate directory.

Test

We could test the schematics we just built locally. Suppose we have an Angular project living in the same level as our <workspace>, named schematics-test.

<workspace>/
schematics-test/
  package.json

In order for schematics-test links to the library that we just built, at schematics-test root level we run:

npm link ../dist/libs/autocomplete

Then we could use ng add

ng add @customer/autocomplete

And check out all the dependency packages that we specified above in index.ts would be added to schematics-test/package.json and installation has been run as well.

    '@angular/cdk: ^9.2.4',
    '@angular/common: ^9.1.0',
    '@angular/core: ^9.1.0',
    '@angular/flex-layout: ~9.0.0-beta.29',
    '@angular/forms: ^9.1.0',
    '@angular/material: ^9.1.0',
    '@angular/platform-browser: ^9.1.0',
    'autocomplete: ^1.0.0', 
schematics-test/package.json

Now we successfully built and tested our ng add schematics!