In this workshop, we will create an Angular library, publishable to NPM, with multiple entry points, unit tests and CI/CD in GitHub. Source code for each step will be provided, as well as complete solution in a GitHub repo. Licensed under MIT, use it however you wish. You can skip the intro and see the steps here.
Why this article?
A little introduction into why I think an article like this is necessary and why existing internet resources are not enough. When I was researching this topic, there were a lot of diverse opinions about the best way to do this, each going in a different direction. Some suggest opening an existing GitHub code such as PrimeNG and copying what they did there. The problem - it is often done with hacks and against Angular’s guidelines in regards to where things should be and how they should work.
Angular library generation CLI also has an issue, because it links the library via
dist folder, where my IDE has a problem with intellisense, and so it fails to produce pretty import links.
Finally, I checked NX and while it is possible to create the intended result with it - and it’s fairly simply with only a handful of steps, using NX adds unnecessary complexity to the application. So unless you need to add hundreds of entry points, and/or have hundreds of developers working on the same project, then you probably don’t need it. NX also has Jest as the default test runner. Turns out Jest does not support visual test debug. It also requires use of 3rd party plugins for almost anything useful. They probably had a good reason for this choice, but I still prefer Karma as my test runner.
In this article I will describe how to solve all of these problems and a few others that will come up along the way, using minimal changes to the CLI generated boilerplate. Minimal changes means it is impossible to make fewer changes and still end up with the same result. Not as a challenge to the reader, but rather a promise of quality here.
Also regarding quality, we will pretend that our code will be used in an enterprise environment and so despite having only a few components with one unit test each, we need to prepare for long term maintenance of it.
I decided to break it down into a series of simple steps. Each code change is involved is clearly marked with a valid commit hash which links to an actual GitHub commit.
#31d0f527 Remove all HTML inside of
app.component.htmland add a few lines in a shape of
Hello World. I added this:
<div>Hello, NPM lib!</div> <div>Future common component</div>
Test your changes in http://localhost:4200, this ensures your development setup works, before we get further. Here and later I will assume you are using IntelliJ Rider or any of Jetbrains IDEs such as WebStorm.
#831dc672 Use Angular CLI to generate a library inside your main app.
ng generate library my-lib
build:libscript in package.json. This will generate production output that you can publish to NPM.
"build:lib": "ng build my-lib",
#f47230a9 Remove the default component inside a library generated by the CLI, and use CLI to generate a new component we will be using. Don’t ask me why, but each of this approaches produces a different file structure and slightly different code.
We also need to update
public-apito export our new component and update
tsconfigto make our library available via NPM import path when using a local file reference. This way there is no difference from using an external NPM package, as far as our code is concerned. I will explain later why this is useful.
#a60c4c75 Add a secondary component using Angular CLI, export from
public-apiand start using in the main app.
#c0bea754 Move secondary component inside the secondary entry point inside
my-lib, adjust tsconfig and main app’s imports.
(optional) Remember in step 5 we imported our components as if using NPM via
package.json? The whole app can be built in hot reload mode via
npm run start, including both the library and the demo app. We could use a file reference in the
import. Here is why we did not. First, build your library if you haven’t already. Next, open tsconfig and comment out paths starting with
projects/, then uncomment those starting with
Now build the main project. It should build the same way, but now you are using the actual production binaries that would be downloaded from NPM. But wait, there is more. If you now publish to NPM, you can comment out the paths in tsconfig and start using the library from NPM, all without touching the imports in the source files. This enables testing at various deployment stages.
Realistically though, you only need to test that your library builds a prod bundle, and that your main app builds without errors. This covers 99.9% of errors. It is not sufficient to just build the main app, even though it includes the library portion in hot reload mode. This is because a dedicated library build will perform additional checks such as verifying no cycles on the dependency graph - if you have multiple libraries or entry points linking to each other. It also builds all entry points, regardless of what is actually used. This helps ensure a valid package.
Yep, this is it, we are done here! If you didn’t start your research on this topic, hopefully this saves you as little as a few hours and as much as a couple of days. This tutorial uses a recent version of Angular, v14, so it should be up-to-date for a couple of years at least. If not, send me an email, I’ll see how to make it valid in future versions.
Out of Scope
The following topics are out of scope for this article.
- OnPush change detection. All components are implement using the default change detection strategy. OnPush is better for performance, but since performance is not the goal here, I did not bother changing. Always use OnPush. Easter egg here - all OnPush links are different, and each is a good starting point, pick the one you like.
- How to create deployment previews for your PRs, where each commit is deployed in a staging environment and can be tested before merging to your production branch. You can use Netlify to accomplish that. It is fairly straightforward and can be done in 5 minutes if you know where to go.
- How to get unit test runner or anything else covered in this article working in VS Code. I use IntelliJ Rider for an IDE.
- Publishing to NPM, enterprise/team workflows and such. If you are okay with manual steps, increment your package version in package.json, then build, then go inside the
dist/yourlibraryfolder and publish it using npm publish. If you want something more advanced, check this out.