Creating Secondary Entry Points for your Angular Library
Developing an Angular Library is easier than ever nowadays since the release of Angular Library feature (from Angular 7). The Angular Library itself is equipped with a community-driven package named ng-packagr
, which is pretty much the core. In this article, we will take a look at how we can utilise ng-packagr
secondary entry points to split our Angular Library even further!
Why do we need secondary entry points?
One of the reasons we want to have secondary entry points is to enable us split our dependencies. Let’s look at an example where one module has peerDependencies
, while another does not have any.
Suppose we have the following Angular Library example:
- There is only one library named
my-awesome-lib
. - Inside
my-awesome-lib/src
, there are 2 modules:awesome-plain
andawesome-text
.
Now, let’s look at the contents of awesome-plain
component:
import { Component } from '@angular/core';@Component({
selector: 'awesome-plain',
template: `
<div>Hey I'm just a plain text with no dependencies!</div>
`
})
export class AwesomePlainComponent {}
and awesome-time
component:
import { Component } from '@angular/core';
import * as moment_ from 'moment';const moment = moment_;
@Component({
selector: 'awesome-time',
template: `
<div>Hey, Awesome Time:</div>
<div>{{ time }}</div>
`
})
export class AwesomeTimeComponent {
time: string;
constructor() {
this.time = moment().format();
}
}
- The
awesome-plain
component does NOT have any dependencies. - The
awesome-time
component depends onmoment
.
And this is how moment
is specified under my-awesome-lib/package.json
:
{
"name": "my-awesome-lib",
"version": "0.0.1",
"peerDependencies": {
"@angular/common": "^8.2.14",
"@angular/core": "^8.2.14",
"moment": "^2.26.0"
}
}
Notice that these
peerDependencies
are put under the scope of librarymy-awesome-lib
, not on the individual module (files inside the library).
Finally, the following is how awesome-plain
and awesome-time
are exported under my-awesome-lib/src/public-api.ts
:
/*
* Public API Surface of my-awesome-lib
*/export * from './awesome-plain/awesome-plain.component';
export * from './awesome-plain/awesome-plain.module';export * from './awesome-time/awesome-time.component';
export * from './awesome-time/awesome-time.module';
It is clear from the above explanations that inside my-awesome-lib
there are 2 files: awesome-plain
does not have any dependencies, while awesome-time
has peerDependencies
(which is moment
). Now, where can it go wrong though?
The problem: Client needs to install ALL peer dependencies from the library
Suppose we have an Angular application that wants to consume my-awesome-lib
. The first thing that the Client App (Angular application) needs to do is installing the library:
npm i my-awesome-lib
After it is installed, the Client App then proceeds to import and use, for example only awesome-plain
component. Here is the code in the Client App may look alike:
// app.module.ts
import { AwesomePlainModule } from 'my-awesome-lib';@NgModule({
...,
imports: [
...,
AwesomePlainModule,
],
bootstrap: [AppComponent]
})
export class AppModule {}// app.component.html
<awesome-plain></awesome-plain>
When we run ng serve
, suddenly we got this error in our terminal:
ERROR in ./node_modules/my-awesome-lib/fesm2015/my-awesome-lib.js Module not found: Error: Can't resolve 'moment' in '/app-showcase-v8/node_modules/my-awesome-lib/fesm2015'
It says that it cannot find moment
installed in the Client App. Well, this is what happens. Although the Client App only imports and uses awesome-plain
, the Angular compiler will still ask to install all peerDependencies
defined in my-awesome-lib
, which is moment
in this case.
The current condition might be fine if the Client App uses both awesome-plain
and awesome-time
. However, imagine if the library grows large and there are more than just 2 modules, let’s say there are 10 modules instead. Let’s exaggerate a bit more; what if 5 out of 10 modules have distinct peerDependencies
? If there’s a Client App that consumes this library and uses only 1 module that does not have any peerDependencies
, the Client App still has to install all 5 peerDependencies
! Surely, there should be a better approach than this, right?
Enter the secondary entry points!
Fortunately, it is quite possible to optimise the current approach. So far, the approach used in the library is only using something called primary entry points. This is denoted by package.json
file that only exists under my-awesome-lib/package.json
, which is where all peerDependencies
for the entire library is defined.
With secondary entry points, we can further split the peerDependencies
beyond the library level; it makes it feasible to define peerDependencies
at folders or modules inside the library. For instance, by making awesome-time
into secondary entry points, we can have another package.json
file inside the sub directory, which contains peerDependencies
only for awesome-time
module. As a result, we are not defining the peerDependencies
at the library-level anymore; we define them at the sub directory instead.
Also, secondary entry points enable us to import the library like the following:
// Primary entry points
import { AwesomePlainModule } from 'my-awesome-lib';// Secondary entry points
import { AwesomeTimeModule } from 'my-awesome-lib/awesome-time';
This way, if the Client App only uses AwesomePlainModule
, the compiler won’t ask to install moment
anymore!
Implement secondary entry points
Hopefully the above explanations give you all a rough idea on why we want to use secondary entry points. The good news is, implementing secondary entry points is fairly simple and straightforward because ng-packagr
will do most of the work behind the scenes!
We will use my-awesome-lib
as a context for the following implementation guides. In this case, we are going to set awesome-time
as secondary entry points, while awesome-plain
will stay as is (still primary entry points).
1. Place the folders for secondary entry points directly under the library folder.
According to ng-packagr
documentation, one of the folder layout examples for secondary entry points is to have it like the following:
my_package
├── src
| ├── public_api.ts
| └── *.ts
├── ng-package.json
├── package.json
└── testing (secondary entry points)
├── src
| ├── public_api.ts
| └── *.ts
└── package.json
As can be seen, the folder for secondary entry points is placed directly under
/my_package
, which differs from the primary entry point folders that are put under/my_package/src
.
Interestingly, this is a similar folder layout used in @angular/common
package, which has @angular/common
as the primary entry points, while @angular/common/testing
as the secondary entry points.
With that, the current library folder layout should be like the following:
2. Create additional package.json and public-api.ts files in secondary entry points folder.
To make secondary entry points, we need to tell ng-packagr
which folder to look for. This can be achieved by creating another package.json
and public-api.ts
files under the /my-awesome-lib/awesome-time
folder in addition to the one for primary entry points. By doing only this, secondary entry points will be dynamically discovered by ng-packagr
.
The contents of /my-awesome-lib/awesome-time/package.json
can be like:
{
"ngPackage": {
"lib": {
"entryFile": "public-api.ts",
"umdModuleIds": {
"moment": "moment"
}
}
},
"peerDependencies": {
"moment": "^2.26.0"
}
}
Notice that we put
moment
aspeerDependencies
here as of now. Also, the"umdModuleIds"
is for removing the warning fromng-packagr
when building the library.
and the contents of /my-awesome-lib/awesome-time/public-api.ts
as follows:
/*
* Public API Surface of my-awesome-lib/awesome-time
*/export * from './awesome-time.component';
export * from './awesome-time.module';
By now, the library folder layout should be like the following:
3. Remove secondary entry points peer dependencies from the main package.json and secondary entry point exported files from the main public-api.ts.
This step is also crucial as we would like to explicitly tell ng-packagr
to completely exclude secondary entry points from the primary entry points. To do that, we need to remove peerDependencies
that are only used for secondary entry points from the main package.json
. In this case, we are going to remove moment
.
The main my-awesome-lib/package.json
should look like:
{
"name": "my-awesome-lib",
"version": "0.0.1",
"peerDependencies": {
"@angular/common": "^8.2.14",
"@angular/core": "^8.2.14"
}
}
The package
moment
has been removed as it is already defined in/my-awesome-lib/awesome-time/package.json
.
Also, we are going to remove awesome-time
files that are exported in the main my-awesome-lib/src/public-api.ts
. The file should only export awesome-plain
files now, which look like the following:
/*
* Public API Surface of my-awesome-lib
*/export * from './awesome-plain/awesome-plain.component';
export * from './awesome-plain/awesome-plain.module';
4. Build the library.
Now that everything has been setup, we can now try to build the library by executing the command ng build my-awesome-lib
. If it’s done properly, you should see the following in the terminal:
Also, if you open the library build folder dist/my-awesome-library
, there should be some additional files named my-awesome-lib-awesome-time.*.js
inside the folders, such as dist/fesm2015
and dist/bundles
. If you compare it with the one without secondary entry points, the build folders normally contain just my-awesome-lib.*.js
, which is the build only for the library itself.
5. Install and import the library in the Client App.
The final step is to finally consume it within an Angular app. There would be slight changes in the import path as we have moved awesome-time
from the primary entry points. To use the new library folder structures in the Client App, it should be like the following:
// Primary entry points
import { AwesomePlainModule } from 'my-awesome-lib';// Secondary entry points
import { AwesomeTimeModule } from 'my-awesome-lib/awesome-time';
Now, if the Client App only uses AwesomePlainModule
, we should be able to run the app without installing moment
(that is used only in AwesomeTimeModule
).
Bear in mind that implementing secondary entry points might cause Breaking Changes to your Angular Library. The reason is because the Client App that is consuming your library will have to update the import paths. Otherwise, their app will break as the secondary entry point files are now imported NOT from
'your-lib'
anymore. As such, this change is not backward compatible.
Should there be any primary entry points at all? Is it okay to only have secondary entry points for the library?
You might be wondering, should we even use primary entry points? In my opinion, it is alright to only have secondary entry points mainly because @angular/material
uses only secondary entry points. On the other hand, it is also generally recommended to use primary entry points for functions or features that are logically similar. The following is written in Angular Package Format documentation:
The general rule in the Angular Package Format is to produce a FESM file for the smallest set of logically connected code. For example, the Angular package has a single FESM for
@angular/core
. When a developer uses the Component symbol from@angular/core
they are very likely to also use symbols such asInjectable
,Directive
,NgModule
, etc as well — either directly or indirectly. Therefore, all these pieces should be bundled together into a single FESM. For most library cases a single logical group should be grouped together into a single NgModule, and all these files should be bundled together as a single FESM file within the package which represents a single entry point in the npm package.
Additionally, in my case, the rule of thumb is to make some modules as secondary entry points if they have distinct peerDependencies
. This is to prevent the Client App to be forced into manually installing all dependencies although they are not using all of them.
Conclusions
To sum up, secondary entry points is a neat feature that allows us to split our Angular Library even further, especially when dealing with peerDependencies
. It is also quite easy to implement as ng-packagr
will dynamically discover the secondary entry points through package.json
of the sub-directories.
One of the benefits is that it will reduce the chance for the Client App to be forced to install all dependencies even though the app is not importing/using any of the library functions that depend on the installed dependencies.
P.S. This article is very much inspired by an amazing article written by Kevin Kreuzer, which talks about how to implement secondary entry points even further by using something called “signpost”. I recommend to check out the article!
GitHub repo
In case the explanation written in this article is unclear, you may check the example library’s GitHub repo:
References
- Improve SPA performance by splitting your Angular Libraries in multiple chunks by Kevin Kreuzer → https://medium.com/angular-in-depth/improve-spa-performance-by-splitting-your-angular-libraries-in-multiple-chunks-8c68103692d0
- How to make secondary entry points in
ng-packagr
→ https://github.com/ng-packagr/ng-packagr/blob/master/docs/secondary-entrypoints.md - Angular Package Format (APF) v10.0 document → https://docs.google.com/document/d/1CZC2rcpxffTDfRDs6p1cfbmKNLA6x5O-NtkJglDaBVs/preview