This is Part 3 of a 6-part series on Angular Schematics.
- ✅ Part 1 – Understanding Angular Schematics: Architecture & Core Concepts
- ✅ Part 2 – Creating Custom Generators with ng generate
- Part 3 – Building Installation Schematics with ng add ← You are here
- 🔜 Part 4 – Writing Migration Schematics with ng update
- 🔜 Part 5 – Testing Schematics with Angular DevKit
- 🔜 Part 6 – Advanced Patterns, Publishing & Nx Integration
🤖 A note on this article: I used Claude to help reformat and structure the content to make it clearer and more presentable for publication.
References
- Angular DevKit Schematics – Full README – Complete reference for
@angular-devkit/schematics - Angular CLI – Adding Libraries – Official guide for ng add schematic support
The previous installment walked us through crafting an ng generate schematic that produces a component carrying your chosen selector prefix. By now, the workspace structure, collection.json, schema.json, factory functions, and template files should all feel familiar.
This time the challenge shifts. We are no longer dealing with generating artifacts into a project that already depends on your library. Instead, the focus lands on what happens at the moment of first-time installation, before anything else has been set up.
The scenario starts when a developer types:
ng add @acme/ui
Users shouldn't be forced to wade through a lengthy manual setup. The package ought to handle its own configuration—installing peer dependencies, modifying angular.json, wiring up its module, including any necessary styles, and ensuring the workspace remains functional. This is precisely the role of an ng add schematic.
Once you finish this piece, you'll have assembled a full-featured ng add schematic that manages every phase of a library's installation process.
Table of Contents
- What Happens When You Run ng add
- How ng add Differs from ng generate
- Registering the ng-add Entry Point
- The Installation Schematic – Full Walkthrough
- Step 1 – Adding Dependencies to package.json
- Step 2 – Scheduling npm install
- Step 3 – Updating angular.json
- Step 4 – Adding Global Styles
- Putting It All Together
- Running and Testing Locally
- Why This Matters – The Developer Experience Argument
- ng add vs ng generate vs ng update – The Complete Picture
- Summary & What’s Next
What Happens When You Run ng add
The ng add command stands as a core Angular CLI feature with a solitary purpose: bringing a library in and setting it up from square one. Executing it triggers the following series of steps in the CLI:
ng add @acme/ui
│
▼
1. npm install @acme/ui (if not already installed)
│
▼
2. Read package.json of @acme/ui
→ find "schematics": "./schematics/collection.json"
│
▼
3. Read collection.json
→ find the "ng-add" schematic entry
│
▼
4. Execute the ng-add factory function
→ Tree mutations staged
→ Tasks scheduled (e.g. NodePackageInstallTask)
│
▼
5. Commit staged Tree to disk
6. Run scheduled tasks (npm install for peer deps)
What sets this apart from ng generate is the first step: the CLI handles the package installation on its own, and only then does it invoke the schematic. As a result, when your factory function executes, the package has already been placed into node_modules, and its corresponding package.json file is accessible. This gives you the version details, peer dependency specifications, and every asset bundled with the package.

What Sets ng add Apart from ng generate
They rely on the same DevKit foundations — Tree, Rule, chain, applyTemplates. The contrast lies in intent, invocation, and how the CLI wraps the schematic:
ng generate | ng add | |
|---|---|---|
| Purpose | Scaffold code in an existing project | Install and configure a library for the first time |
| Trigger | Manual, on demand, repeatable | Once per library installation |
| Package install | No – package must already be installed | Yes – CLI installs the package first |
| Entry in collection.json | Named schematic e.g. "component" | Reserved name "ng-add" |
| Idempotency expectation | Usually idempotent | Should guard against running twice |
| Typical operations | Create files, modify source | Update package.json, angular.json, AppModule, styles |
The special key "ng-add" matters here — the CLI searches specifically for this identifier within collection.json. If you use a different key, ng add won't locate it.
Registering the ng-add Entry Point
Within your library's collection.json, place the ng-add entry next to any schematics you already have:
{
"$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json",
"schematics": {
"ng-add": {
"description": "Installs and configures @acme/ui in the workspace",
"factory": "./ng-add/index#ngAdd",
"schema": "./ng-add/schema.json"
}
}
}
Meanwhile, the schematics property inside your library’s package.json needs to reference this collection:
{
"name": "@acme/ui",
"version": "1.0.0",
"schematics": "./schematics/collection.json"
}
The ng-add schematic lives in your library’s schematics directory, with the project layout described below:
@acme/ui/
├── package.json
└── schematics/
├── collection.json
└── ng-add/
├── index.ts ← factory function
├── schema.json ← options
└── schema.ts ← TypeScript interface
The Installation Schematic – Full Walkthrough
Here’s what our ng add schematic must accomplish when a developer pulls in @acme/ui:
- Ensure any necessary peer dependencies appear in
package.json - Run
npm installso those dependencies get fetched - Point
angular.jsonat the library’s stylesheet file - Include the library’s base CSS import inside the global
styles.scss
Every item above maps to its own Rule. We’ll craft each one individually, then bring them together.
Start by setting up the schema. Put a new file at schematics/ng-add/schema.json:
"$schema": "http://json-schema.org/schema",
"$id": "NgAddSchema",
"title": "ng-add schematic for @acme/ui",
"type": "object",
"properties": {
"project": {
"type": "string",
"description": "The name of the Angular project to configure",
"$default": { "$source": "projectName" }
},
"skipStyles": {
"type": "boolean",
"description": "Skip adding the global stylesheet",
"default": false
}
}
}
And schematics/ng-add/schema.ts:
export interface NgAddSchema {
project: string;
skipStyles: boolean;
}
Step 1 – Adding Dependencies to package.json
The initial rule injects any peer dependencies that the library needs into the consumer's package.json. We parse the file, append the items into the dependencies object, and then save the updated content.
import { Rule, Tree, SchematicsException } from '@angular-devkit/schematics';
function addDependencies(): Rule {
return (tree: Tree) => {
const pkgPath = 'package.json';
const buffer = tree.read(pkgPath);
if (!buffer) {
throw new SchematicsException('Could not find package.json in workspace root.');
}
const pkg = JSON.parse(buffer.toString('utf-8'));
// Add peer dependencies your library requires
pkg.dependencies = pkg.dependencies || {};
pkg.dependencies['@acme/ui'] = '^1.0.0';
pkg.dependencies['@acme/ui-icons'] = '^1.0.0'; // example peer dep
tree.overwrite(pkgPath, JSON.stringify(pkg, null, 2));
return tree;
};
}
💡 Idempotency: Before making any write operation, verify whether the dependency is already present. Re-running the schematic on a workspace that contains the package must not downgrade or replace a version the developer has explicitly locked in.
To protect against clobbering records that are already there, you can use a sturdier variant:
if (!pkg.dependencies['@acme/ui-icons']) {
pkg.dependencies['@acme/ui-icons'] = '^1.0.0';
}
Step 2 – Scheduling npm install
Merely adding items to package.json won't trigger their installation. Instead, you must schedule a NodePackageInstallTask using the SchematicContext. This task executes once every Rule has finished running and the Tree is written to disk—meaning when npm install starts, your modified package.json is fully in place.
import {
Rule, Tree, SchematicContext
} from '@angular-devkit/schematics';
import {
NodePackageInstallTask
} from '@angular-devkit/schematics/tasks';
function installDependencies(): Rule {
return (_tree: Tree, context: SchematicContext) => {
context.addTask(new NodePackageInstallTask());
context.logger.info('Scheduling npm install...');
};
}
When NodePackageInstallTask is triggered, it invokes npm install (or yarn install / pnpm install — the CLI determines which package manager is active). There’s no need to declare what should be installed; the task simply runs the package manager against the existing package.json, incorporating everything your earlier Rule introduced.
Step 3 – Updating angular.json
Libraries commonly ship resources like fonts, icon sets, and precompiled CSS that must be added to the styles or assets sections of angular.json. Modifying angular.json follows the same JSON parse-mutate-reserialise approach, but you must first locate the appropriate project entry.
import { Rule, Tree, SchematicsException } from '@angular-devkit/schematics';
import { NgAddSchema } from './schema';
function addStylesToAngularJson(options: NgAddSchema): Rule {
return (tree: Tree) => {
const angularJsonPath = 'angular.json';
const buffer = tree.read(angularJsonPath);
if (!buffer) {
throw new SchematicsException('Could not find angular.json.');
}
const angularJson = JSON.parse(buffer.toString('utf-8'));
// Resolve the target project - falls back to defaultProject
const projectName =
options.project ||
angularJson.defaultProject;
const project = angularJson.projects[projectName];
if (!project) {
throw new SchematicsException(
`Project "${projectName}" not found in angular.json.`
);
}
const buildOptions = project.architect?.build?.options;
if (!buildOptions) {
throw new SchematicsException(
`Could not find build options for project "${projectName}".`
);
}
// Add the library's pre-built stylesheet
const styleEntry = 'node_modules/@acme/ui/styles/acme-ui.css';
buildOptions.styles = buildOptions.styles || [];
if (!buildOptions.styles.includes(styleEntry)) {
buildOptions.styles.unshift(styleEntry); // prepend so it loads before app styles
tree.overwrite(angularJsonPath, JSON.stringify(angularJson, null, 2));
}
return tree;
};
}
The deliberate choice of unshift over push ensures that the library’s base styles take precedence correctly—they are inserted first so that app-specific styles, loaded afterward, can easily override them.
Step 4 – Incorporating Global Styles
Many libraries expect a foundational import to be added to the global stylesheet of the host application, such as a set of CSS variables, a font-face rule, or a normalization script. Some projects avoid relying on the styles entry in angular.json and instead prefer adding a direct @import statement to styles.scss.
import { Rule, Tree } from '@angular-devkit/schematics';
import { NgAddSchema } from './schema';
function addGlobalStyleImport(options: NgAddSchema): Rule {
return (tree: Tree) => {
if (options.skipStyles) return tree;
// Support both .scss and .css global stylesheets
const stylePaths = [
'src/styles.scss',
'src/styles.css',
'src/styles.sass',
];
const stylePath = stylePaths.find(p => tree.exists(p));
if (!stylePath) return tree; // no global stylesheet found - skip silently
const content = tree.read(stylePath)!.toString('utf-8');
const importLine = `@import '@acme/ui/styles/tokens';\n`;
if (content.includes(importLine)) return tree; // idempotent guard
tree.overwrite(stylePath, importLine + content); // prepend
return tree;
};
}
The skipStyles option, defined in schema.json, provides a way out for users whose workspaces rely on a bespoke styling pipeline that manages this independently.

Putting It All Together
Every one of the four Rules gets merged by the factory function into one unified chain:
import {
Rule, SchematicContext, Tree, chain
} from '@angular-devkit/schematics';
import { NgAddSchema } from './schema';
export function ngAdd(options: NgAddSchema): Rule {
return (tree: Tree, context: SchematicContext) => {
context.logger.info('Setting up @acme/ui...');
return chain([
addDependencies(),
installDependencies(),
addStylesToAngularJson(options),
addGlobalStyleImport(options),
])(tree, context);
};
}
Sequence is critical in this case. addDependencies() needs to execute first because installDependencies() relies on package.json being updated before npm install reads that file. While all other operations could happen in any arrangement, it is good style to batch file-related work together and reserve the install step for last.
Here is what appears in the terminal when a developer invokes ng add @acme/ui:
✔ Package successfully installed.
UPDATE package.json (1842 bytes)
UPDATE angular.json (4103 bytes)
UPDATE src/styles.scss (112 bytes)
✔ Packages installed successfully.
All modified files appear in the output. The developer can review precisely what was altered prior to committing.
Running and Testing Locally
Since ng add first installs the package, testing it locally calls for a method that differs from npm link:
# Pack the library as a tarball
cd your-library
npm run build
npm pack
# Produces: acme-ui-1.0.0.tgz
# In your test Angular workspace
ng add ./path/to/acme-ui-1.0.0.tgz
# Or install from a local path and run ng add separately
npm install ../your-library
ng add @acme/ui
# Preview without committing
ng add @acme/ui --dry-run
The --dry-run flag proves especially useful for ng add schematics, since their modifications are distributed across a number of files. Inspecting the complete diff prior to committing reassures the developer about the exact impact the schematic will have on their workspace.
Why This Matters – The Developer Experience Argument
When a library lacks an ng add schematic, its users are forced to follow documentation, grasp the configuration details, and carry out a series of manual steps with precision. Every single one of those steps introduces potential for mistakes, and misconfigurations during setup often lead to confusing errors that only surface at runtime.
With a thoughtfully designed ng add schematic, users are required to do just one thing: execute a single command. Everything else is handled automatically.
This goes far beyond simple convenience – it determines whether a library gains traction or fades into obscurity. Developers often judge a library based on the smoothness of its initial setup. If the first encounter with your library is a seamless ng add that leaves the workspace properly set up and functional right away, it serves as a clear indicator that the library is well-cared for and that its maintainers prioritize developer experience.
The Angular community has made ng add the standard route for incorporating any serious library. Angular Material, NgRx, Angular CDK, Transloco, TailwindCSS, PrimeNG – each of these ships its own ng add schematic. Users now anticipate this as a given. Omitting it from your library makes it an outlier – and not in a favorable way.
Summary & What’s Next
Together, we constructed a full-fledged ng add schematic that manages the entire installation process: including dependencies in package.json, queuing npm install, modifying angular.json to register the library’s stylesheet, and inserting a global style import – all triggered by a single ng add command.
Here are the crucial points to remember:
- The special
"ng-add"entry withincollection.jsonis what enablesng addto function – the CLI ignores any other name. - The CLI installs the package prior to executing the schematic – your factory function operates with the package already present in
node_modules. NodePackageInstallTaskexecutes after every Rule has finished – always place it last in order to capture your modifiedpackage.json.- Every file operation ought to be protected by both an existence verification and an idempotency verification – running
ng addagain on a pre-configured workspace should be harmless. - The
--dry-runflag is an invaluable aid while developing – always use it for your initial tests.
In Part 4, we shift focus to the last of the three schematic variants: ng update migrations. We’ll dive into migrations.json, version-specific constraints, and leveraging the TypeScript Compiler API to safely modify pre-existing source code – the foundation that supports managing breaking changes across large projects.
Series Roadmap
| Part | Topic | Status |
|---|---|---|
| Part 1 | Understanding Angular Schematics – Architecture & Core Concepts | ✅ Published |
| Part 2 | Creating Custom Generators with ng generate | ✅ Published |
| Part 3 | Building Installation Schematics with ng add | ✅ You are here |
| Part 4 | Writing Migration Schematics with ng update | 🔜 Coming Soon |
| Part 5 | Testing Schematics with Angular DevKit | 🔜 Coming Soon |
| Part 6 | Advanced Patterns, Publishing & Nx Integration | 🔜 Coming Soon |
Created using Angular v21 along with @angular-devkit/schematics, @angular-devkit/schematics/tasks, and @angular-devkit/core.
