Shared Angular library without NPM

February 18th 2022 Angular

Angular CLI includes all the tools you need to develop shared libraries that can be used in multiple applications, as long as they are all in the same workspace. However, it does not work without modifications if the library is in a separate workspace so that it can be shared with applications in multiple Git repositories. At least not if you want to share the library as a Git submodule instead of installing it from an NPM repository. Fortunately, this is still possible, although the process does not seem to be well documented.

In this scenario, the library can be created and developed in its own Angular workspace, just like any standalone library. You first create an empty workspace and create a new library in it:

ng new my-workspace --no-create-application
cd my-workspace
ng generate library my-lib

To use the library from an application in a separate repository, you should first include the library repository as a submodule, preferably in a sibling folder of the application workspace, for example:

  • my-app (application workspace root folder)
  • my-libs (folder for library submodule(s))
    • my-workspace (the library submodule root folder)

Next, you need to include the library in the application workspace. To do this, you can copy the project item from the library's angular.json file to the application's angular.json file and change all paths to point to the actual library folder relative to the application's root folder. For the folder structure above, these would be the correct paths:

"my-lib": {
  "projectType": "library",
  "root": "../my-libs/my-workspace/projects/my-lib",
  "sourceRoot": "../my-libs/my-workspace/projects/my-lib/src",
  "prefix": "lib",
  "architect": {
    "build": {
      "builder": "@angular-devkit/build-angular:ng-packagr",
      "options": {
        "project": "../my-libs/my-workspace/projects/my-lib/ng-package.json"
      },
      "configurations": {
        "production": {
          "tsConfig": "../my-libs/my-workspace/projects/my-lib/tsconfig.lib.prod.json"
        },
        "development": {
          "tsConfig": "../my-libs/my-workspace/projects/my-lib/tsconfig.lib.json"
        }
      },
      "defaultConfiguration": "production"
    },
    "test": {
      "builder": "@angular-devkit/build-angular:karma",
      "options": {
        "main": "../my-libs/my-workspace/projects/my-lib/src/test.ts",
        "tsConfig": "../my-libs/my-workspace/projects/my-lib/tsconfig.spec.json",
        "karmaConfig": "../my-libs/my-workspace/projects/my-lib/karma.conf.js"
      }
    }
  }
}

The library must be built before it can be referenced in the application:

ng build my-lib

For this to work from the application workspace, you must install ng-packagr as a development dependency:

npm i -D ng-packagr

For convenience, I suggest including the build of the library in a couple of NPM scripts:

  • The following post-install script takes care of the initial build of the library after cloning the application repository (it installs the library's NPM packages and builds the library):

    "postinstall": "npm --prefix ../my-libs/my-workspace ci && ng build my-lib"
    
  • To ensure that the library is up to date when the application is built, a pre-build script can be used:

    "prebuild": "ng build my-lib"
    
  • The following script should be run in the background during development to automatically rebuild the library whenever a change is made. It works great with ng serve and triggers the rebuild of the application each time the library is rebuilt:

    "watch:my-lib": "ng build my-lib --watch --configuration development"
    

When importing symbols from the library, you should use the library name instead of the relative path:

import { MyLibService } from "my-lib";

This would even allow you to use the library as an NPM package in the future without having to change the code. For this to work, you need to add a path alias to the tsconfig.json file in the application workspace:

"compilerOptions": {
  "paths": {
    "my-lib": ["../my-libs/my-workspace/dist/my-lib"]
  }
},

The previous setup already works fine until you create your first service in the library that depends on a provider from its peer dependency. Then you will most likely get the following error message in the browser console at runtime:

Error: inject() must be called from an injection context

To fix this error, you need to ensure that the library dependencies map correctly to the application dependencies by adding path aliases for the imports to the application's tsconfig.json. The following takes care of all packages with @angular scoping:

"compilerOptions": {
  "paths": {
    "@angular/*": ["./node_modules/@angular/*"]
  }
}

A final recommendation for anyone using Visual Studio Code for Angular development: Create a new Visual Studio Code workspace and open the following two folders in it:

  • application workspace root
  • library workspace root

This will ensure that all files are correctly treated as part of their own project.

You can see the full setup in my GitHub repository. I did not add the library workspace as a submodule, but the placement of the local files is identical to this scenario.

Angular CLI will properly set up your Angular workspace to allow development of your shared library in parallel with the application in the same workspace. If you want to have the library in a separate workspace so that it has its own Git repository and can be used in multiple projects without being installed from NPM, you will need to manually customise your application workspace. In this post I have described the setup that works well for me.

Get notified when a new blog post is published (usually every Friday):

If you're looking for online one-on-one mentorship on a related topic, you can find me on Codementor.
If you need a team of experienced software engineers to help you with a project, contact us at Razum.
Copyright
Creative Commons License