Making an Addable Angular Package Using Schematics

August 11, 2018 14 min read Angular Angular CLI

Let’s try to create an Angular library that's consumable easily by making an "ng add" schematic. This library will provide a simple exported Angular Element.

Angular CLI is the best thing which happened to Angular” - that’s how I was thinking for a long time. Simply, it lets you do a lot of stuff with no effort.

But then, Angular announced a new idea, called Angular Labs, which has a purpose of initiating new explorations. Basically, that’s the place that Angular Elements and Schematics came from. In this way, the Angular team made my heart skip a beat once again. 😍

Angular Elements, Angular CLI and Angular Labs

Awesome projects by the Angular team

So, how about integrating these awesome things and taking it for a test drive?

The Goal

What we are going to do is create an Angular workspace that contains a library - which will register an Angular Element as a custom element.

But that’s not all, our library will install the necessary dependencies, including polyfills, and will inject the exported module into the root module of a host application. In other words, we’re going to implement a basic example for ng add schematic.

In the end, we’ll get an Angular Package which consists of the library and that schematic we just mentioned.

Along with that - this article assumes you’re familiar with Angular Workspace and Angular Elements. Moreover, we’re not going to explain how to serve an Angular Element for non-Angular application. In case you’re curious and want to see a workaround which demonstrates this process, check out a previous article I’ve written.

Initializing a Workspace

Let’s not waste any time and take the Angular Element example from an article I referred above - with some little modifications, obviously.

This is the project in question:

It’s easy to perceive that:

  • There is a single component (MadeWithLoveComponent) which receives couple of inputs.
  • The component is declared in AppModule and registered as entry component.
  • A custom element is created from the component by AppModule and then registers inside CustomElementRegistry.
  • We don’t have a bootstrap component, therefore, we trigger the bootstrapping process manually.

Let’s start with the fun!

At first, we create a new workspace:

ng new made-with-love-workspace --style=scss

Notice that we choose to define SCSS as our CSS preprocessor - for the sake of comfort only.

This workspace will include the library which we’re going to implement in the next step, beside the main application which will import the module that’s exported by the library and use the custom element we saw above.

Internal Library

Creating the Library

With Angular CLI v6, it’s so easy to create a library that transpiles to the Angular Package Format. 😉

We just have to run the following schematic:

ng generate library made-with-love

Then will be added a new internal project under projects:

A new library is created as an internal project

A new library is created as an internal project

Well, we get a new directory made-with-love that contains src. A little deeper, there is lib, which is the place we’re going to shed our code.

Let’s create the made-with-love.component.ts file:

To simplify, we use an inline template and styles. Beyond that, Angular v6.1.0 arrives with a new view encapsulation for ShadowDOM v1, instead of the deprecated ViewEncapsulation.Native - so let’s take advantage of it. 😎

Next step is to create a feature module file, which called made-with-love.module.ts:

Notice we don’t export the component - which defeats the purpose. Hence, we’ll have to apply CUSTOM_ELEMENTS_SCHEMA on our host module. It makes Angular accept non-Angular elements.

It’s clear that we want to export our module inside public_api.ts (the public barrel file) so it will be accessible externally using ES6 modules:

Indeed, we’re making progress! 💪

Using the Library

So far we created an Angular workspace with an internal library.

Intuitively, we’d like to load our library’s module inside app.module.ts:

Note: Definitely, we apply the schema for custom elements.

But, once we use ng start for running our host application - we’re supposed to get a transpilation error:

ERROR in src/app/app.module.ts(3,36): error TS2307: Cannot find module 'made-with-love'.

It’s absolutely justified. We haven’t installed our module before (or any package which depends on it) - what means there is no package under node_modules that’s called made-with-love.

To solve it, the library should be built. Therefore, we should run ng run made-with-love:build. This will bundle our library in Angular Package Format and place it within dist directory:

Bundling the library in Angular Package Format

Bundling the library in Angular Package Format

Now, we can use the component we made inside app.component.ts:

Here’s the result:

Rendering the component we made

Everything works perfectly!

Wait, we shed the library’s output into dist instead of node_modules - so how is the ES6 import resolved? 🤔

The trick here is related to the compiler’s module resolution. We used a schematic for generating the library, which also appended a path mapping configuration to the main tsconfig.json file:

The paths above make the compiler to resolve the module out of dist directory. Notice that the mapping is relative to baseUrl - so it must be specified.

Schematics Project

We’ve just reached the interesting part. We’re going to create a schematics project with a simple collection and implement the ng add schematic.

If you’re not familiar with Schematics in general, read about it here. You can watch the following lecture also:

Let’s get to work! 👷

Creating the Project

Of course, there’s an automatic way to generate a schematics project:

schematics blank --name=schematics

Note: Assuming @angular-devkit/schematics-cli is installed globally.

However, we want to learn something today so let’s do it from scratch.

At first, we create a new directory called schematics inside projects. It’s possible to generate it on the root level, but in this article we’re going to consider it as an internal project.

Then, we install the necessary devDependencies:

npm install @angular-devkit/core @angular-devkit/schematics --save-dev

Let’s explain:

  • @angular-devkit/core - is a package with shared utilities for Angular DevKit. In practice it enables us to run schematics from CLI.
  • @angular-devkit/schematics - is a package with the core of Schematics.

Well, we’re talking about an internal project. So in order to make it consistent with other internal projects, we’ll create the src directory, package.json, and tsconfig.json.

Let’s start with the package.json:

The schematics property is what indicates our collection file - which describes a list of the implemented schematics in our project. Don’t worry, we’ll create a file like that later.

Next file is tsconfig.json:

Notice we can extend the tsconfig.json from the root level, but let’s keep it simple. Along with that, the configuration above is directly taken from a project which was generated using schematics-cli.

Alright. This is how our workspace is supposed to seem right now:

Creating an additional internal project for Schematics

Creating an additional internal project for Schematics

Adding a Schematic

Well, we’ve an empty project for schematics and it’s about time to prepare our custom ng-add schematic.

This schematic will perform the following steps:

  1. It will add @angular/elements, @webcomponents/custom-elements and angular-made-with-love into package.json.
  2. It will run npm install.
  3. It will import MadeWithLoveModule into the root module of the host application.
  4. It will inject the polyfill’s script file into the scripts of the host application.

Note: We could use ng add @angular/elements, but here we want to learn how to implement it on our own.

First up, we have an unsolved debt to create a collection file. So, let’s create collection.json and place it within src:

As you can see, we describe only one schematic and that’s ng-add. The factory property represents a reference for a file which contains the schematic’s exported factory function. Next to collection file, we create the ng-add directory that contains an index.ts file.

Before we start to implement the steps we mentioned above - we’ll install a utility library for Schematics, which is called schematics-utilities.

To be honest with you, it’s a library I created a while ago:

This library provides us several utilities that we’re going to take advantage of.

So let’s install it:

npm install schematics-utilities --save-dev

Disclaimer: Please take the following implementations with a grain of salt - these are just a couple of examples.

Spoiler: The final package will be published as angular-made-with-love so from now on we’ll adopt that package name.

Now, let’s navigate to ng-add/index.ts and start implementing the first step - so our schematic will be able to add to package.json the necessary packages.

We create a RuleFactory function which does that:

All we do in the code above is to iterate a list of packages and invoke addPackageJsonDependency for each of them. The addPackageJsonDependency function is imported out of schematics-utilities (we’ll see the imports in the final result).

In case you get confused, Rule is essentially a function that returns a Tree:

Notice that Rule function could return Observable<Tree> or void type as well.

Next step is installing the packages. This following RuleFactory function performs it simply:

Basically, NodePackageInstallTask does all the job for us. It’s boilerplate code for a schematic task which is imported out of @angular-devkit and installs the packages using a package manager.

Next step is importing MadeWithLoveModule into the root module of the host application.

Here’s an appropriate RuleFactory function:

What we can see above is - retrieving the workspace of the host application using getWorkspace. Likewise, we pass it through getProjectFromWorkspace along with a project name in order to retrieve the project’s configuration.

Then, we just invoke addModuleImportToRootModule and pass into that:

  • host - A Tree representation for host workspace.
  • moduleName - A module to inject (MadeWithLoveModule in our case).
  • angular-made-with-love - The package which the module is imported from.
  • project - A project’s configuration which the module will be injected.

Great, we’re almost done!

The last step is, as we declared before, to inject the polyfill’s script file into the scripts array of the host application.

This RuleFactory function is a little complicated from the previous functions:

Just for the record, the code above is inspired by ng-add schematic of @angular/elements (check it out). Well, what happens there is that we read and parse the angular.json file and then retrieve the project’s configuration. Right after that, we push into the scripts array a new object with the polyfill’s path, and eventually we stringify and replace the file’s content with the new configuration.

All that’s left is to do is creating a default RuleFactory function which operates the functions we’ve just created.

Here we go:

Obviously, the default exported RuleFactory function is supposed to return a Rule. In the last code snippet, we use chain exactly for that objective - returning a single combined Rule from multiple RuleFactory functions. From here, it’s pretty straightforward - we invoke these functions according to the steps we previously defined.

Note that we allow skipping the steps by providing respective CLI parameters: skipPackageJson, skipModuleImport and skipPolyfill.

Finally, here’s the final result:

Running a Schematic

Our schematics project is written in TypeScript. This means, as you guess, that we need to transpile the source code to plain JavaScript somehow before running a specific schematic.

Let’s run the following line on the root level:

tsc -p projects/schematics/tsconfig.json

Alright, new index.js and index.js.map files appear within ng-add - what clearly means that something indeed was built there. Using these files, we will run our ng-add schematic.

Note: Make sure your global installed TypeScript package is up-to-date.

Generally, in order to run any schematic - the following CLI convention should be adopted:

schematics project-name:schematic-name parameters

So, in our case, we could type (assuming that we’re navigated to projects/schematics):

schematics .:ng-add --skipModuleImport
Running our schematic

Running our schematic

As we see, . represents the current directory (projects/schematics), ng-add is the schematic’s name and --skipModuleImport is just a supported parameter by the schematic.

Important to note that:

  • In case we run the schematic without --skipModuleImport, we’d get a transpilation error: Could not find (undefined) due to the fact that our schematic isn’t executed from the root of the workspace and therefore - it couldn’t locate it. Don’t worry, this problem will be solved soon when we actually use it from another project - so let’s ignore it right now (in order to avoid redundant complexity).
  • The default of running a schematic is dry-run mode, which prevents from the schematics tool from actually creating and changing files.

Publishing Process

Let’s recap what we did so far:

  • We initialized an Angular Workspace, which would contain internal projects.
  • We created an Angular library that provides a simple Angular Element.
  • We created a Schematics project that provides the ng-add schematic.

But how do we integrate these and make from them a single Angular Package which is ready to be published? 🤔

Adding npm Scripts

Until now, we used manually commands to build both internal projects. This seems like an appropriate moment to add some handy npm scripts into the package.json.

Here’s the new scripts property:

We’ve already seen the commands of lib:build and schematics:build, whereas the other commands are pretty understandable.

Great, now we can build both projects easily!

Copying Schematics to “dist”

Let’s assume we run lib:build:prod and the library is created within dist. As we remember, the package.json expects that the collection file will be placed next to it - inside a schematics directory.

However, that’s not the case here and that means we should copy the outputs of the schematics project in some way into dist/made-with-love/schematics.

There are a lot of ways to do that, though, we’re going to choose webpack for this mission.

webpack logo

Yay, another tool!

Apparently, webpack is installed indirectly, but - let’s install these packages explicitly:

npm install webpack webpack-node-externals copy-webpack-plugin --save-dev

Then, we create the webpack.config.js file on the root level:

Assuming that schematics:build was executed, webpack will copy the output of ng-add/index.ts and the collection file into the destined directory. Also, it’ll bundle some needed utilities from schematics-utilities.

Note that it’s not a best practice to include third-party code with a published package - we definitely should avoid from that. However, in our case, we’d not like that a host application would have to install schematics-utilities before running ng-add - because it misses the whole point.

In order to bundle we just have to type webpack, although, wrapping it with an npm script which builds the project before - would be much preferable:

We’re almost at the end, I promise you. 😉

Publishing the Package

Before any publish, we must build our library and include inside it the outputs of the schematics project. Hence, we add an additional npm script:

Let’s run npm run build-package and then navigate to dist/made-with-love.

Now - it’s the moment to publish! 🚀

Running npm publish there will do the job:

Publishing the package

Publishing the package

Demo Application

Let’s initialize a new project to demonstrate what we did today:

ng new angular-addable-package-example

All that we have left is to run ng add angular-made-with-love:

Installing our package using `ng add`

Installing our package using `ng add`

Summary

Today we’d a long journey on purpose to build our addable Angular Package.

We started by initializing an Angular Workspace and an internal library. We continued with implementing a simple Angular Element as part of our library. Then, we created a Schematics project from scratch and implemented there an example of a schematic. Lastly, we built everything that’s related to the final output and published it.

Here is the source code of the projects we created:

A few key points to keep in mind:

  • Creating a library that transpiles to the Angular Package Format using Angular CLI v6 is extremely easy.
  • Angular v6.1.0 arrives with a new view encapsulation for ShadowDOM v1.
  • There is no point in exporting an Angular Element’s component as part of the module.
  • In case we’re using an Angular Element inside an Angular application - we should apply CUSTOM_ELEMENTS_SCHEMA on the module which uses it.
  • Internal libraries are imported out of dist instead of node_modules by default, due to the path mapping configuration - which is added into the tsconfig.json while we generate the library using Angular CLI.
  • The @angular-devkit/core package is what lets us to run schematics from the CLI.
  • We must append a schematics property which points to a collection file, for our schematics set, inside the published package.json.
  • The schematics-utilities package is what provides us a collection of shared utilities for working with Schematics.
  • A RuleFactory is a function which returns a Rule.
  • A Rule is a function which returns a manipulated Tree.
  • A schematics project should be built before running it.
  • The dry-run mode for a schematics project prevents changes of our file system.
  • Adding npm scripts is pretty useful when we want to manipulate the various internal projects on the workspace level.

I hope this article was informative and not exhausting for you - but you must know that was written with ❤️.