Disclaimer: while I did use AI to help me investigate and write up this issue, this is 100% a real problem that has caused me to lose sleep while debugging at work.
Command
add
Is it a regression?
Unknown - We cannot confirm whether this used to work as we don't have access to older CLI versions paired with older private package versions on GitHub Packages to test with
Description:
When running ng add with a package that provides schematics (via schematics and ng-add fields in package.json), the Angular CLI fails to detect and execute the schematics on the first attempt. The CLI reports "The package does not provide any ng add actions" even though the package does include schematics.
On subsequent runs of ng add (when the package is already installed), the schematics are correctly detected and executed.
This issue occurs because the CLI uses two different code paths with different rules to determine if schematics exist: one path queries the registry metadata, while another (when the package is already installed) uses the filesystem directly. When a registry doesn't expose the schematics field in its metadata API, the first path incorrectly fails.
Steps to Reproduce
-
Create a new Angular application:
ng new test-app --skip-install
cd test-app
npm install
-
Add a package with ng-add schematics from a private GitHub Packages registry:
ng add @scope/private-package-name@version
-
Observe output (first run - FAILS to detect schematics):
✔ Determining Package Manager
Using package manager: npm
✔ Loading package information
✔ Confirming installation
✔ Installing package
Package installed successfully. The package does not provide any `ng add` actions, so no further actions would be taken.
-
Run the same command again:
ng add @scope/private-package-name@version
-
Observe correct behavior (second run - works):
Skipping installation: Package already installed
UPDATE package.json (1043 bytes)
UPDATE src/app/app.config.ts (712 bytes)
UPDATE angular.json (2334 bytes)
✔ Packages installed successfully.
Root Cause
The Angular CLI uses two different code paths that use different rules:
Path 1 (during installation): Queries registry metadata to set hasSchematics:
// Line 524
context.hasSchematics = !!manifest.schematics;
Path 2 (when already installed): Runs schematic directly from node_modules at lines 176-188, bypassing registry check.
When the registry (e.g., GitHub Packages) doesn't include the schematics field in metadata, Path 1 incorrectly reports no schematics exist, while Path 2 would find them.
Verification
The installed package contains schematics:
$ cat node_modules/@scope/private-package/package.json | jq '{schematics, "ng-add"}'
{
"schematics": "./collection.json",
"ng-add": { "save": "dependencies" }
}
But registry metadata (from GitHub Packages) has no schematics field:
$ npm view @scope/private-package@version --json --registry=https://npm.pkg.github.com | grep schematics
(empty)
Proposed Fix
The installed package's package.json should be authoritative. Verification at lines 248-261 should run regardless of registry metadata when the package is installed.
Exception / Error
Package installed successfully. The package does not provide any ng add actions, so no further actions would be taken.
Environment:
- Angular CLI: 21.2.8 (and likely earlier versions)
- Node.js: v22.22.2
- Package Manager: npm
- OS: Linux - Debian 13
- Registry: GitHub Packages (npm.pkg.github.com)
Additional Context
This issue was discovered while using an Angular library with ng-add schematics published on GitHub Packages. Despite the package being published with all correct metadata (schematics and ng-add fields in package.json), the npm view query from GitHub Packages does not return these fields.
While it would be ideal if GitHub Packages properly exposed the schematics field in registry metadata, that is a separate concern. The core issue reported here is that the Angular CLI uses two different code paths with different rules to determine schematic existence, which is unintuitive and leads to confusing behavior where the same command must be run twice to succeed.
We also tried to write an integration test to prove that this bug existed, but we discovered that if the package is installed from the tarball used to npm publish instead of the remote package itself, it would always work. This is likely because it wouldn't take the code path of determining the schematic's existence based on registry metadata and instead inspected the package.json file inside the tarball after installation.
Related Code
Key files to examine:
Disclaimer: while I did use AI to help me investigate and write up this issue, this is 100% a real problem that has caused me to lose sleep while debugging at work.
Command
addIs it a regression?
Unknown - We cannot confirm whether this used to work as we don't have access to older CLI versions paired with older private package versions on GitHub Packages to test with
Description:
When running
ng addwith a package that provides schematics (viaschematicsandng-addfields in package.json), the Angular CLI fails to detect and execute the schematics on the first attempt. The CLI reports "The package does not provide anyng addactions" even though the package does include schematics.On subsequent runs of
ng add(when the package is already installed), the schematics are correctly detected and executed.This issue occurs because the CLI uses two different code paths with different rules to determine if schematics exist: one path queries the registry metadata, while another (when the package is already installed) uses the filesystem directly. When a registry doesn't expose the
schematicsfield in its metadata API, the first path incorrectly fails.Steps to Reproduce
Create a new Angular application:
ng new test-app --skip-install cd test-app npm installAdd a package with ng-add schematics from a private GitHub Packages registry:
Observe output (first run - FAILS to detect schematics):
Run the same command again:
Observe correct behavior (second run - works):
Root Cause
The Angular CLI uses two different code paths that use different rules:
Path 1 (during installation): Queries registry metadata to set
hasSchematics:Path 2 (when already installed): Runs schematic directly from node_modules at lines 176-188, bypassing registry check.
When the registry (e.g., GitHub Packages) doesn't include the
schematicsfield in metadata, Path 1 incorrectly reports no schematics exist, while Path 2 would find them.Verification
The installed package contains schematics:
But registry metadata (from GitHub Packages) has no schematics field:
$ npm view @scope/private-package@version --json --registry=https://npm.pkg.github.com | grep schematics (empty)Proposed Fix
The installed package's package.json should be authoritative. Verification at lines 248-261 should run regardless of registry metadata when the package is installed.
Exception / Error
Environment:
Additional Context
This issue was discovered while using an Angular library with ng-add schematics published on GitHub Packages. Despite the package being published with all correct metadata (schematics and ng-add fields in package.json), the
npm viewquery from GitHub Packages does not return these fields.While it would be ideal if GitHub Packages properly exposed the
schematicsfield in registry metadata, that is a separate concern. The core issue reported here is that the Angular CLI uses two different code paths with different rules to determine schematic existence, which is unintuitive and leads to confusing behavior where the same command must be run twice to succeed.We also tried to write an integration test to prove that this bug existed, but we discovered that if the package is installed from the tarball used to
npm publishinstead of the remote package itself, it would always work. This is likely because it wouldn't take the code path of determining the schematic's existence based on registry metadata and instead inspected the package.json file inside the tarball after installation.Related Code
Key files to examine:
packages/angular/cli/src/commands/add/cli.ts(lines 248-261, 524)packages/angular/cli/src/package-managers/package-manager.ts(getManifest function)