skip to content

In an Angular CLI library built with ng-packagr, how do you add a secondary entry point such as `ui-kit/testing`, and when is one worth it?

level: seniorimportance: nice to knowfreq 30%

answer

  1. a subfolder that looks like a mini-library
  2. its own ng-package.json
  3. one build, several import paths
  4. tsconfig needs a wildcard mapping

basics

~20 s

A secondary entry point is a library subfolder with its own ng-package.json naming an entry file; the same ng build packages it as a separate import path like ui-kit/testing. Use one for optional code, heavy dependencies or test helpers.

solid answer

~40 s

Create `projects/ui-kit/testing/` with an entry file and its own `ng-package.json`, e.g. `{ "lib": { "entryFile": "index.ts" } }`. ng-packagr discovers it and `ng build ui-kit` emits it next to the primary entry point, adding it to the package's `exports` so consumers `import { ... } from 'ui-kit/testing'`. Inside the workspace the schematic's mapping only covers `ui-kit`, so add `"ui-kit/*": ["./dist/ui-kit/*"]` (or one mapping per entry point). It is worth it when part of the library is optional, pulls in extra dependencies, or should not be imported by accident, like `@angular/core/testing`; each entry point is its own flat module, which also gives bundlers a separate lazy-loading unit. Keep dependencies one-way: the secondary imports the primary by its package name.

code

ts · 10 lines
ts
// projects/ui-kit/testing/index.ts
export { ButtonHarness } from './button-harness';
export { provideFakeTheme } from './fake-theme';

// projects/ui-kit/testing/button-harness.ts
import { Button } from 'ui-kit';

export class ButtonHarness {
  constructor(readonly component: Button) {}
}

go deeper

for a junior

Recall that import paths like @angular/core/testing are secondary entry points, and that a library can have more than one.

for a middle

Explain the folder plus ng-package.json recipe, that one ng build packages all entry points, and the extra tsconfig mapping.

for a senior

Show design judgement: which code deserves its own entry point, how to keep dependencies between entry points one-way, and the path-mapping failure mode.

for a principal

Weigh entry-point granularity for a design system: lazy-loading units and dependency isolation against a larger public surface to document and version.

## What a secondary entry point is A package in the **Angular Package Format** has one **primary entry point** (`ui-kit`) and zero or more **secondary entry points** (`ui-kit/testing`, `ui-kit/charts`). Angular's own packages use them: `@angular/core/testing`, `@angular/common/http`. Each entry point is a separate import path with its own public API and its own flattened ES module file in the published package. ## How to add one with the Angular CLI The library schematic creates only the primary entry point. You add a secondary one by hand: 1. Create a subfolder inside the library project, for example `projects/ui-kit/testing/`. 2. Give it an entry file that exports its public API, for example `index.ts`. 3. Give it its own `ng-package.json` naming that entry file. ```json { "lib": { "entryFile": "index.ts" } } ``` The resulting source layout: ```text projects/ui-kit/ ng-package.json primary: lib.entryFile = src/public-api.ts package.json src/public-api.ts src/lib/... testing/ ng-package.json secondary: lib.entryFile = index.ts index.ts button-harness.ts ``` No change to `angular.json` is needed. `ng build ui-kit` runs ng-packagr on the primary `ng-package.json`, ng-packagr finds the nested one, and the output in `dist/ui-kit` contains both entry points, with the package's `exports` map listing `.` and `./testing`. ## Consuming it inside the workspace The library schematic added a single mapping, `"ui-kit": ["./dist/ui-kit"]`. That does not match `ui-kit/testing`, so an app importing the secondary entry point cannot resolve it until you add a mapping. The Angular CLI's own end-to-end test for this case exercises both forms: | Form | tsconfig `paths` | |---|---| | Wildcard | `"ui-kit": ["./dist/ui-kit"]`, `"ui-kit/*": ["./dist/ui-kit/*"]` | | Explicit | one entry per path: `"ui-kit/testing": ["./dist/ui-kit/testing"]` | The wildcard is easier to maintain once there are several entry points. ## When one is worth it - **Optional features with heavy dependencies.** A charts entry point that needs a charting package should not force that dependency on every consumer of buttons and inputs. - **Test helpers.** Harnesses, fakes and fixtures belong in `ui-kit/testing`, so production code does not import them by accident, the same reason `@angular/core/testing` is separate. - **Code-splitting granularity.** Most build tools split code at the module level, and APF publishes one flat module per entry point, so separate entry points give the consuming app finer units to lazy-load. Angular Material publishes one entry point per component family for this reason. - **Clear ownership.** A team can own `ui-kit/forms` with its own public API file and review surface. When it is not worth it: a small, cohesive library with one purpose. The Angular documentation notes that `@angular/core` keeps its runtime in a single entry point because it is used as one unit. ## Moving existing code into a secondary entry point Splitting an established library is a release event, not a refactor: 1. Create the folder, entry file and `ng-package.json`, and move the code. 2. Replace relative imports of primary code with imports from `ui-kit`. 3. Decide whether the primary `public-api.ts` keeps re-exporting the moved symbols. Removing them changes the import path consumers must use, which is a breaking change and belongs in a major version. 4. Add the new path mapping in the workspace and update every in-repo import. 5. Rebuild and check that `dist/ui-kit` now contains the new entry point. ## Rules that keep entry points healthy - **Import across entry points by package name.** The secondary imports `ui-kit`, never `../src/lib/...`; each entry point is compiled as its own unit, and reaching into another one's files breaks that boundary. - **Keep dependencies one-way.** Secondaries may depend on the primary; the primary should not depend on its secondaries, and secondaries should not form cycles. - **Treat each entry file as public API.** Everything exported from `testing/index.ts` is as much a contract as the primary `public-api.ts`. - **Document the import paths**, since consumers cannot discover a secondary entry point from the primary's exports.

  • After adding `ui-kit/testing`, the library builds but the app says it cannot find module 'ui-kit/testing'; why?
    The schematic mapped only `ui-kit` to `./dist/ui-kit` in the root `tsconfig.json`, and that exact key does not match a subpath. Add a wildcard mapping `"ui-kit/*": ["./dist/ui-kit/*"]`, or an explicit `"ui-kit/testing"` entry, and rebuild.
  • Do you need a new project in angular.json for each secondary entry point?
    No. A secondary entry point is part of the same library project and the same `ng build ui-kit`; ng-packagr discovers it through its own `ng-package.json` inside the library folder. The workspace keeps one project and one build target.

saying these in an interview costs you the question

  • Each secondary entry point needs its own project in angular.json.
  • The generated tsconfig mapping for ui-kit also resolves ui-kit/testing.
  • A secondary entry point can import primary code by relative path.
  • Every library should split into many entry points by default.