Updated May 2025
This guide and demo now use Angular 19 and also work with Angular 17 and 18.
For older Angular versions, see the previous version of this guide.
The Angular CLI's generate library
command is great for creating smaller libraries. However, as your library grows, maintaining it can become challenging, and performance may be affected. At that point, splitting the library into multiple entry points (also called secondary or subentry points) can help.
The Angular CLI does not provide a built-in command for creating additional entry points, and resources on this topic are limited. Let's start by building a new Angular library from scratch and explore how to add secondary entry points.
What are entry points
An Angular library always has a main entry point, which is the library's name, like react
(without an npm scope) or @angular/core
(with an npm scope). In a newly created Angular library, this is the only entry point, and it exports all modules, components, and services.
An npm package can also have additional entry points, called secondary or subentry points. For example, Angular Material provides a separate entry point for each component:
import { MatButtonModule } from "@angular/material/button";
import { MatCardModule } from "@angular/material/card";
Why create entry points
Subentry points can make your library more efficient and easier to manage. They offer:
- Better tree shaking, especially with UMD libraries.
- Improved code splitting for lazy-loaded modules.
- More flexible dependency management.
- Clearer separation of features within your library.
What we'll build
In this article, we will create a component library called ui-sdk
for a fictional company, mycomp. The library package will be named @mycomp/ui-sdk
.
The library will include multiple subentry points, some of which will depend on each other. We will also add an Angular app to the workspace as a showcase application to demonstrate how to use the library and its components.
Let’s get started.
Lib Implementation
Create workspace
To start, create a new Angular workspace using the Angular CLI. A workspace can host multiple applications and libraries, but for now, we'll focus on creating a single library.
ng new ui-sdk --create-application false
cd ui-sdk
Here, the workspace is named ui-sdk
, but you can choose any name. This name is only for the workspace and doesn't need to match the library's name.
The --create-application
flag skips creating an application since we only need a library for now. We'll add an application later for demonstration purposes.
Create library
With the workspace ready, we can now create the library. We'll name it @mycomp/ui-sdk
. Using an npm scope (like @mycomp
) is optional but helpful if you plan to publish multiple libraries or packages.
# Run this inside the workspace
ng generate library @mycomp/ui-sdk
This command sets up the workspace and generates the library. Let’s review the structure created by the Angular CLI.
Project structure
Here is the basic structure of the project generated by the Angular CLI (irrelevant files are omitted). At the root, you'll find the angular.json
file, which configures the workspace, and the package.json
file, which lists all dependencies.
.
├── projects/
│ └── mycomp/
│ └── ui-sdk/
│ ├── src/
│ │ ├── lib/ # (1)
│ │ │ ├── ui-sdk.component.ts
│ │ │ └── ui-sdk.service.ts
│ │ └── public-api.ts # (2)
│ ├── ng-package.json # (3)
│ └── package.json # (4)
├── angular.json
└── package.json
Now let's look at some key files:
Library Code (1)
This folder contains the library's code, including a component and a service. Currently, this is the library's only entry point.
Public API (2)
This file defines the library's public API. It exports all components, services, and other elements meant to be accessible outside the library. For now, it includes the Angular component and service from the src/lib/
folder.
ng-package.json (3)
This file configures ng-packagr, specifying the build output path and the entry file, which points to src/public-api.ts
.
package.json (4)
This is the library's own package.json
file (different from the workspace's root package.json
). It defines the library's name, version, and dependencies.
Create a subentry point
Let’s create a secondary entry point for handling internationalization, called i18n
. In a real-world scenario, this might include a translation service, a pipe, and a directive. For this example, we’ll keep it simple and add a dummy pipe.
The i18n
entry point will have its own folder under projects/mycomp/ui-sdk
. Avoid placing it in the src/lib/
subfolder, as that would result in imports like @mycomp/ui-sdk/src/lib/i18n
instead of the cleaner @mycomp/ui-sdk/i18n
.
Inside the i18n
folder, create a src
folder to hold the code for this entry point. For example, you can add a translation pipe here. You can also include other components, services, or utilities as needed.
.
└── projects/
└── mycomp/
└── ui-sdk/
├── i18n/
│ └── src/
│ ├── translate.pipe.spec.ts
│ └── translate.pipe.ts
└── src/...
Our pipe is a simple example that currently returns the input string. In a real-world scenario, it would return a translated string based on the provided input.
import { Pipe, PipeTransform } from "@angular/core";
@Pipe({
name: "translate",
})
export class TranslatePipe implements PipeTransform {
transform(value: string): string {
// Here you would translate the key
return `${value} (translated)`;
}
}
Next, create a public-api.ts
file in the i18n
folder. This file should export all elements of the subentry point that need to be accessible to applications or other subentry points. In this case, export the TranslatePipe
. Include all public Angular elements like modules, components, pipes, and any TypeScript types they use. Avoid exporting internal components or spec files. ng-packagr will throw an error if required exports are missing.
Create the public-api.ts
in the i18n
folder and export the pipe:
export * from "./src/translate.pipe";
To make this a subentry point, create an ng-package.json
file inside the i18n
folder with the following content:
{
"$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json",
"lib": {
"entryFile": "public-api.ts"
}
}
The file tells ng-packagr that it's a subentry point and specifies the entryFile
property to define what the subentry point exports.
Omitting entryFile
If you skip the entryFile
property, ng-packagr will default to looking for a file named src/public_api.ts
relative to the ng-package.json
file. You can move the public API file into the src
folder and rename it to public_api.ts
if you prefer. However, keeping the file as public-api.ts
outside the src
folder ensures consistent naming (Angular typically uses kebab-case). Choose the approach that works best for you.
With this setup, the subentry point is ready. Here's how your project structure should look now:
.
└── projects/
└── mycomp/
└── ui-sdk/
├── i18n/
│ ├── src/
│ │ ├── translate.pipe.spec.ts
│ │ └── translate.pipe.ts
│ ├── ng-package.json
│ └── public-api.ts
└── src/...
Build the lib
Before we move on, let's build the library:
npm run build
This command generates the main entry point and the new secondary entry point i18n
. If it runs successfully, the output will look like this:
Building Angular Package
-----------------------------------------------------------------
Building entry point '@mycomp/ui-sdk'
-----------------------------------------------------------------
✔ Compiling with Angular sources in Ivy partial compilation mode.
✔ Generating FESM bundles
✔ Copying assets
✔ Writing package manifest
✔ Built @mycomp/ui-sdk
-----------------------------------------------------------------
Building entry point '@mycomp/ui-sdk/i18n'
-----------------------------------------------------------------
✔ Compiling with Angular sources in Ivy partial compilation mode.
✔ Generating FESM bundles
✔ Built @mycomp/ui-sdk/i18n
-----------------------------------------------------------------
Built Angular Package
- from: /ui-sdk/projects/mycomp/ui-sdk
- to: /ui-sdk/dist/mycomp/ui-sdk
-----------------------------------------------------------------
The build process successfully created the main entry point @mycomp/ui-sdk
and the secondary entry point @mycomp/ui-sdk/i18n
.
More entry points
Now you can add another entry point following the same steps. Let’s create a button
entry point for a button component. Start by creating a button
folder with a src
subfolder. Add the button component, a public-api.ts
file to export it, and an ng-package.json
file to define the entry point.
.
└── projects/
└── mycomp/
└── ui-sdk/
├── button/
│ ├── src/
│ │ ├── button.component.spec.ts
│ │ └── button.component.ts
│ ├── ng-package.json
│ └── public-api.ts
├── i18n/...
└── src/...
Run the build again, and you should now have three entry points.
Dependencies Between Entry Points
Sometimes, entry points in your library may depend on each other. For example, the i18n
entry point might be used in the button
entry point. Importing between entry points works automatically during the build process, as ng-packagr handles these dependencies for you. However, your IDE might not recognize these imports without additional configuration.
To fix this, update the tsconfig.json
file in the root of your workspace. Replace the existing paths
mapping with the following:
{
"compilerOptions": {
"paths": {
- "@mycomp/ui-sdk": [
- "./dist/mycomp/ui-sdk"
- ]
+ "@mycomp/ui-sdk/*": [
+ "./projects/mycomp/ui-sdk/*",
+ ],
+ "@mycomp/ui-sdk": [
+ "./projects/mycomp/ui-sdk",
+ ]
},
// ... (other props omitted)
}
This tells TypeScript where to find the source code for your library and its entry points, ensuring proper IDE support.
To make imports work, add an index.ts
file in each entry point folder to re-export the public API:
export * from "./public-api";
export * from "./public-api";
Note: If you prefer, you can rename public-api.ts
to index.ts
and update the entryFile
property in the ng-package.json
file. This approach works only if public-api.ts
is not inside the src
folder.
Now, you can import the TranslatePipe
in the ButtonComponent
:
import { Component, input } from "@angular/core";
import { TranslatePipe } from "@mycomp/ui-sdk/i18n";
@Component({
selector: "lib-button",
imports: [TranslatePipe],
template: `<button>{{ label() | translate }}</button>`,
})
export class ButtonComponent {
label = input("Click me");
}
That's it, we now have a dependency between our two secondary entry points. When you build the library again you will see that the button entry point is built after the i18n entry point because it depends on the i18n entry point.
Make sure that you don't introduce circular dependencies between entry points. But don't worry, ng-packagr will detect circular dependencies and throw an error.
Tests
When running npm test
, you'll notice that only the tests for the main entry point (UI SDK Component) are executed. Tests for the secondary entry points are skipped. This happens because of the configured sourceRoot
. Update the path in the angular.json
file as shown below:
{
"projects": {
"@mycomp/ui-sdk": {
- "sourceRoot": "projects/mycomp/ui-sdk/src",
+ "sourceRoot": "projects/mycomp/ui-sdk",
}
}
}
Run npm test
again and all tests should be executed now.
Code Coverage
Angular provides built-in support for code coverage. To enable it, run ng test
with the --no-watch --code-coverage
options. After updating the sourceRoot
property as explained earlier, you can execute the tests and generate a coverage report.
ng test --no-watch --code-coverage
Main entry point
The main entry point still has the default structure, exporting a component and a service. Let’s simplify it.
Move the component and service into the src
folder and delete the lib
folder. Relocate public-api.ts
to the ui-sdk
folder and create an index.ts
file to re-export the public API. Update all imports and adjust the entryFile
property in ng-package.json
.
This setup aligns the main entry point with the structure of the subentry points.
.
└── projects/
└── mycomp/
└── ui-sdk/
├── button/...
├── i18n/...
├── src/
│ ├── ui-sdk.component.ts
│ └── ui-sdk.service.ts
├── index.ts
├── ng-package.json
└── public-api.ts
As we already have subentry points, do we still need the main entry point? Here are three options:
No main entry point:
Move all components and services into subentry points and remove the main entry point. This works well for libraries with many components, as it avoids confusion about where components belong. Note that ng-packagr requires every entry point to export something. You can export a dummy constant in your main entry point:
export const MY_COMP_UI_SDK = "@mycomp/ui-sdk";
Then, delete the src
folder.
Main + Subentry:
Keep the main entry point to export most components and add subentry points for specific features, like testing utilities. This is suitable for libraries with a single primary purpose and fewer components, like a logger library. Your subentry point can import the main entry point but the main entry point can't depend on the subentry points to avoid circular dependencies. Keeping the default src/lib/
structure is recommended for compatibility with Angular CLI (we will talk about that later).
Main + Subentries:
Keep the main entry point alongside multiple subentry points for additional features. I don't have a good use case in mind for this approach, so you should probably move everything into subentry points if you don't see a clear benefit of the main entry point.
Choose the approach that best fits your library's structure and usage.
Your library now has a clean main entry point and is well-structured to support additional subentry points in the future.
CLI Generate Command
The Angular CLI's generate
command is useful for creating components, directives, pipes, or services. However, it doesn't work well with subentry points. It appends lib/
to the path, which is hardcoded and cannot be changed. This results in an incorrect structure for subentry points.
To fix this, use the --path
option to specify the correct location. For example, to create a dropdown component in its own subentry point:
# Or use the short version: ng g m ...
ng generate component dropdown --flat --path projects/mycomp/ui-sdk/dropdown/src
You can skip the --flat
option if you prefer to create a subfolder, which is helpful for organizing multiple components in a subentry point.
Dependency Management
Some dependencies might only be needed by specific subentry points in your library. Unfortunately, subentry points cannot have their own peer dependencies, so you must add them to the main package.json
.
You have two options:
- Add the dependency as a regular peer dependency. This means all users must install it, even if they don't use the subentry point. However, unused dependencies will be tree-shaken during build.
- Mark the dependency as optional. Users only need to install it if they use the subentry point. If the dependency is missing, the build will fail when the subentry point is used. This is a better choice for dependencies specific to a single subentry point.
Here’s how to mark a peer dependency as optional:
{
"peerDependencies": {
"some-library": "^1.0.0" // add the peer dependency
},
"peerDependenciesMeta": {
"some-library": {
"optional": true // mark it as optional
}
}
}
Now we have an Angular library with multiple entry points and dependencies between them. Next, we'll create a showcase app within the same Angular workspace to demonstrate how to use the library.
App Implementation
We will now add an application to the existing workspace that uses the library. This can be a real application or a showcase/demo app for the library. These steps apply only to applications within the same workspace as the library. External applications in separate workspaces will use the library as an npm dependency.
Create app
When we created the workspace, we skipped creating an application. Now, let's add a new app to the workspace:
ng generate app showcase
You can now run the app using npm start
or build it with ng build showcase
. These commands work without additional setup.
Use the library
To use the library in the new app, we need to configure TypeScript to locate it. This is done by adding a paths
mapping in the tsconfig file.
The paths mapping in the root tsconfig.json
points to the library's source code. However, Angular recommends using the built library in applications. This is because libraries and applications are built differently (an app uses @angular-devkit/build-angular
while a lib is built with ng-packagr). Using the library's source in an app may cause issues, as it will be compiled like app code instead of library code.
You could update the existing paths mapping to point to the built library in the dist
folder, as Angular initially configured when the library was added. This works for building the library or running the app but can hurt the developer experience. For example, after adding a new component, you’d need to rebuild the library and wait for your IDE (and the Angular Language Service Plugin) to detect the changes. Also, navigating to an import’s implementation might take you to the dist
folder instead of the source code.
Another option is to remove the existing paths mapping and create two new ones: one in tsconfig.lib.json
pointing to the source code and another in tsconfig.app.json
pointing to the built library. This works for building and running but can cause issues in Visual Studio Code, as it only recognizes tsconfig.json
files and ignores others. This means library imports may show errors. If you're using WebStorm, this approach works fine since it supports all tsconfig files.
What you should do instead: keep the existing paths mapping in the root tsconfig.json
for development and add a new one in tsconfig.app.json
to point to the built library for the application:
{
"compilerOptions": {
+ "paths": {
+ "@mycomp/ui-sdk/*": [
+ "./../../dist/mycomp/ui-sdk/*"
+ ],
+ "@mycomp/ui-sdk": [
+ "./../../dist/mycomp/ui-sdk"
+ ]
+ }
}
}
When using this new configuration:
- Building the library: ng-packagr will handle imports between entry points automatically, ignoring any path mappings.
- Building the application: The app will use the new mappings to reference the built library, as recommended by Angular. The root
tsconfig.json
paths will be ignored. - Working in VS Code: The editor will use the root
tsconfig.json
paths, pointing to the library's source code for both the library and the app. This ensures a smooth development experience.
You can now use the library by importing components from the main or subentry points and adding them to the app.component.html
file.
import { ButtonComponent } from "@mycomp/ui-sdk/button";
@Component({
// ...
imports: [ButtonComponent],
})
export class AppComponent {}
Next we want to start our application with ng serve
. This requires that we build the library first.
NPM Scripts
When working on the library, you might want to run the showcase app to see changes immediately. Since the app depends on the built version of the library, you need to ensure the library is built before running the app with ng serve
. Any changes to the library require rebuilding it.
You can rebuild the library using ng build
with the --watch
flag. However, the Angular CLI doesn't provide a straightforward way to build the library and wait for it to finish before starting the app, as far as I know.
That's why we need to install three additional packages:
npm install -D npm-run-all wait-on rimraf
With npm-run-all
, you can run multiple npm scripts either in parallel or sequentially. The wait-on
package lets you wait for a specific file to exist before continuing. Here's the new setup:
{
"scripts": {
"start": "npm-run-all clean --parallel lib:watch showcase:start-waiton",
"build": "run-s lib:build showcase:build",
"test": "run-p lib:test showcase:test",
"lib:build": "ng build @mycomp/ui-sdk",
"lib:watch": "ng build @mycomp/ui-sdk --watch --configuration development",
"lib:test": "ng test @mycomp/ui-sdk",
"showcase:start-waiton": "wait-on dist/mycomp/ui-sdk/package.json && npm run showcase:start",
"showcase:start": "ng serve showcase",
"showcase:build": "ng build showcase",
"showcase:watch": "ng build showcase --watch --configuration development",
"showcase:test": "ng test showcase",
"clean": "rimraf dist"
}
}
Here’s a quick overview of the scripts:
start
: Cleans thedist
folder, startslib:watch
to build and watch the library for changes, and useswait-on
to ensure the library build is complete before serving the app. Any library updates will trigger a rebuild and reload the app.build
: Builds the library and the app sequentially usingrun-s
.test
: Runs tests for both the library and the app in parallel usingrun-p
.
The run-s
and run-p
commands are part of the npm-run-all
package.
You can now use npm start
to run the showcase app and see live updates as you modify the library.
Storybook
If you're building a component library, consider using Storybook instead of a showcase app. While the setup can be tricky and sometimes frustrating due to breaking changes or bugs, Storybook is a powerful tool. It simplifies component development, provides an interactive showcase, and allows you to document components using Markdown or MDX. It's worth exploring!
Demo
The source code of the @mycomp/ui-sdk
library and the showcase app is available on GitHub:
github.com/rothsandro/angular-library-example
Summary
We successfully built a library! 🎉
The Angular workspace now includes a library and a showcase app. We added multiple entry points with dependencies and demonstrated how to use the library in the app. This setup provides a solid foundation for creating and managing great components.
There’s more to explore, like styling and asset handling, but we’ll stop here. Hopefully, this article was helpful and you learned something new.