angular-compat-matrix 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,7 +4,7 @@ Which Angular versions does your published library really work with?
4
4
 
5
5
  Run it with `npx angular-compat-matrix`, or install the package and run `ng-matrix`: both names start the same command.
6
6
 
7
- ng-matrix builds and packs your Angular library, installs the tarball the way a user would into a fresh app for each Angular major (made with that major's own CLI), builds it, and reports a matrix. It tests the package your users get, not the source you see.
7
+ ng-matrix builds and packs your Angular library, installs the tarball into a fresh app for each Angular major (created with that major's own CLI), builds it, and reports a compatibility matrix. It tests the exact package your users get, not your local source.
8
8
 
9
9
  ```
10
10
  $ npx angular-compat-matrix
@@ -20,107 +20,387 @@ Angular 22 ✗ ✓ ✓ ✓ ✓
20
20
  ✗ 19: partial: Needs Angular 20.0.0 or later: highest minVersion 20.0.0, in
21
21
  MyDropdownComponent. Build the library with the oldest Angular you support, or take
22
22
  this version out of the peer range.
23
- ✗ 20: strict types: A template using the package type-checks only loosely…
23
+ ✗ 20: strict types: Fails with strictTemplates: true and skipLibCheck: false.
24
24
  ! 22: works, but peerDependencies doesn't allow it (@angular/core "^19.0.0 || ^20.0.0 ||
25
25
  ^21.0.0"). Widen the range to include it.
26
26
  ```
27
27
 
28
- ## Usage
28
+ ---
29
29
 
30
- Run it in your library's workspace: an Angular CLI workspace with a library project, or a folder with `ng-package.json`.
30
+ ## Quick Start
31
+
32
+ Run it in your library's workspace: an Angular CLI workspace with a library project, or any folder with `ng-package.json`.
31
33
 
32
34
  ```sh
33
- npx angular-compat-matrix # supported majors plus every major your peer range claims
34
- npx angular-compat-matrix --versions 18-22 # an explicit range (or 22, or 20,22)
35
- npx angular-compat-matrix --project my-lib # one library in a workspace with several
36
- npx angular-compat-matrix --consumer ./example # build your own example app instead of a generated one
37
- npx angular-compat-matrix --tests # also run your library's tests on each version
38
- npx angular-compat-matrix --pm pnpm # npm (default) or pnpm
39
- npx angular-compat-matrix --format markdown # a table for your README; also json
40
- npx angular-compat-matrix --package some-lib@3 # check a published package instead of building
35
+ # Check supported majors plus every major your peer range claims
36
+ npx angular-compat-matrix
37
+
38
+ # Check a specific range or list of majors
39
+ npx angular-compat-matrix --versions 18-22
40
+ npx angular-compat-matrix --versions 20,22
41
+
42
+ # Check one library in a workspace with several
43
+ npx angular-compat-matrix --project my-lib
44
+
45
+ # Check a published package from npm (no local build needed)
46
+ npx angular-compat-matrix --package primeng@19.0.0
47
+
48
+ # Generate Markdown for your README or GitHub Actions summary
49
+ npx angular-compat-matrix --format markdown
50
+
51
+ # Machine-readable JSON output for CI pipelines
52
+ npx angular-compat-matrix --json
53
+
54
+ # Verbose output (shows exact commands executed and full failure logs)
55
+ npx angular-compat-matrix --verbose
41
56
  ```
42
57
 
43
- `npx angular-compat-matrix --help` lists every option.
58
+ ---
59
+
60
+ ## How to Read the Matrix
61
+
62
+ The matrix tests each Angular major in order. A failure on any step marks subsequent checks `—`, ensuring the first true incompatibility is reported clearly.
44
63
 
45
- **Exit code:** 1 when a version inside your peer range fails, so CI goes red only for broken promises. A version outside the range that fails is a note. 2 means ng-matrix couldn't check a version at all.
64
+ ### Symbols
46
65
 
47
- **Node:** ng-matrix runs every Angular version on the Node it runs on, and stops early if that Node is too old for one of them. Node 24.15 or later covers Angular 17 to 22. Angular's `engines` ranges only set minimums; [Angular's own support table](https://angular.dev/reference/versions) is narrower.
66
+ | Terminal | Markdown | Meaning |
67
+ | :---: | :---: | :--- |
68
+ | `✓` | `✅` | **Passed:** The check succeeded. |
69
+ | `✗` | `❌` | **Failed:** The check failed. |
70
+ | `✓?` | `✅*` | **Advisory pass:** Passed on `minVersion` alone, but because the library was compiled with a newer Angular major, `minVersion` has been historically unreliable. The build check decides compatibility. |
71
+ | `—` | `—` | **Not run:** Skipped because an earlier check failed. |
72
+ | `·` | `n/a` | **None:** Nothing to check (e.g. no `@angular/*` peer declared). |
73
+ | `!` | `⚠️` | **Setup error:** ng-matrix itself could not set up the app on this machine (e.g. disk full, network timeout). This is not a library failure. |
48
74
 
49
- ## What it checks
75
+ ---
50
76
 
51
- For each Angular major, in order. A failure marks the later checks `—`, so the first real failure is the one reported. The peer range is reported but never stops the other checks.
77
+ ## What It Checks
52
78
 
53
- | Check | Question | How |
79
+ | Check | What it asks | How it verifies |
54
80
  | --- | --- | --- |
55
- | Peer range | Does the package claim this version? | Every `@angular/*` peer is compared, not just `core`: a CDK range can leave a version out too |
56
- | Install | Does it install in a fresh app without peer conflicts? | npm: `--legacy-peer-deps` with the peers added explicitly, then `npm ls` reports every invalid peer. pnpm: `strict-peer-dependencies`. For a version the range doesn't claim, only that expected conflict is ignored |
57
- | Partial | Can this version's linker load the compiled code? | The highest `minVersion` in the package's `ɵɵngDeclare…` calls. ✗ is proof; ✓ is only advisory (`✓?`) when the package was compiled with a newer major, because `minVersion` has been too low before |
58
- | AOT build | Does a production build of an app using it succeed? | `ng build` of a smoke app that uses every public export, with `strictTemplates: false` and no size budgets |
59
- | Strict types | Do templates using it type-check? | The same build with `strictTemplates: true` and `skipLibCheck: false`, which also catches typings that use types an older `@angular/core` lacks |
60
- | Tests | Do your own tests pass on this version? | With `--tests`: your test command in a copy of your workspace, with Angular swapped to this version |
81
+ | **Peer range** | Does your package claim this version? | Compares every declared `@angular/*` peer (e.g. `@angular/core`, `@angular/cdk`, `@angular/material`). Never stops other checks. |
82
+ | **Install** | Does it install cleanly without peer conflicts? | Installs the packed tarball into a fresh Angular app using your package manager (`npm` with `--legacy-peer-deps` + `npm ls`, or `pnpm` with `strictPeerDependencies: false`). For versions outside your peer range, expected peer conflicts are safely ignored. |
83
+ | **Partial** | Can this version's Angular linker load the compiled code? | Scans every `ɵɵngDeclare…` call in the published code for `minVersion`. A failure is absolute proof of incompatibility. |
84
+ | **AOT build** | Does a production app build with your library? | Runs `ng build --configuration production` (`strictTemplates: false`, `skipLibCheck: true`) on a smoke app using every public component, directive, pipe, service, and entry point. |
85
+ | **Strict types** | Do consumer templates type-check under strict settings? | Re-runs the build with `strictTemplates: true` and `skipLibCheck: false`. Catches template type errors and invalid typings. (Errors strictly inside third-party `node_modules/**/*.d.ts` are reported as warnings and do not fail the check). |
86
+ | **Tests** | Do your own unit tests pass on this version? | (Opt-in with `--tests`): Copies your workspace, swaps Angular dependencies to the target major, and executes your test command (`npm test` or `--test-command`). |
61
87
 
62
- ### Why build, when there's minVersion?
88
+ ### The Generated Smoke App
63
89
 
64
- The Angular compiler writes a `minVersion` into each compiled declaration: the oldest Angular whose linker can handle the features it uses. It's set per feature, not from the Angular that compiled the code, and it has been too low more than once. Angular 16.1's input transforms broke 16.0 apps ([angular/angular#51413](https://github.com/angular/angular/pull/51413)). In Angular 22, factory declarations claim `minVersion: "12.0.0"` while using `ɵɵFactoryTarget.Service`, which only 22 has; an Angular 21 linker crashes on them with `Unsupported enum value`. So `minVersion` can prove a version fails, never that it works.
90
+ When not using `--consumer`, ng-matrix inspects your package typings and generates a standalone smoke app:
91
+ - Imports all entry points exposed in your `package.json` `exports` map.
92
+ - Instantiates each component, directive, and pipe in its own isolated component.
93
+ - Supplies correctly-typed mock values for required `@Input()` fields.
94
+ - Uses attribute selectors bare (`<div myFlag>`) when they accept booleans or strings, matching real user patterns.
95
+ - Handles undeclared dependencies in secondary entry points: if a secondary entry point (e.g. `my-lib/chart`) fails because an undeclared extra (like `chart.js`) is missing, ng-matrix omits that entry point and warns you rather than failing the whole library. If the package's *main* entry point (`.`) imports an undeclared package, AOT fails immediately with a clear explanation.
65
96
 
66
- ### The smoke app
97
+ If your library requires specialized providers, wrappers, or mock services, supply your own example app with `--consumer ./path-to-app`.
67
98
 
68
- ng-matrix reads your package's typings, where Angular records each declaration's selector, inputs and standalone flag. It writes a small app that:
99
+ ---
69
100
 
70
- - imports every entry point in your `exports` map, and installs your optional peers too, since some entry point may need them
71
- - uses each component, directive and pipe once, each in a component of its own (as an app would), with a correctly typed value for every required input
72
- - writes attribute selectors that take a boolean or string bare (`<div myFlag>`), the way users do, and binds the rest
73
- - imports each NgModule and uses what it exports, and injects your services
101
+ ## Install-Script Safety Default & `--allow-scripts`
74
102
 
75
- If your library needs more setup than that (providers, content, a parent component), give ng-matrix an example app with `--consumer`. It's copied, its Angular dependencies are moved to each version, and your package is installed from the tarball.
103
+ By default, ng-matrix **blocks arbitrary install scripts** (`preinstall`, `install`, `postinstall`) during `npm` and `pnpm` installations.
104
+
105
+ Only native build tools essential to the Angular toolchain are permitted to run:
106
+ - `esbuild`
107
+ - `@parcel/watcher`
108
+ - `lmdb`
109
+ - `@swc/core`
110
+
111
+ If your library or one of its dependencies requires an install script to compile native bindings, opt in explicitly:
112
+ ```sh
113
+ npx angular-compat-matrix --allow-scripts
114
+ ```
115
+
116
+ If an installation or build fails while scripts were blocked, ng-matrix explicitly lists which packages were skipped in the output findings so you know whether `--allow-scripts` is needed.
117
+
118
+ ---
119
+
120
+ ## Cache Management
121
+
122
+ ng-matrix caches npm registry metadata and downloaded tools (such as pinned pnpm binaries) to keep runs fast and enable `--offline` usage.
123
+
124
+ ```sh
125
+ # View cache location, disk usage, and file count
126
+ ng-matrix cache
127
+
128
+ # Clear the cache entirely
129
+ ng-matrix cache clean
130
+ ```
131
+
132
+ ### Cache Locations by OS
133
+
134
+ - **Linux:** `$XDG_CACHE_HOME/ng-matrix` or `~/.cache/ng-matrix`
135
+ - **macOS:** `~/.cache/ng-matrix` (or `$XDG_CACHE_HOME/ng-matrix`)
136
+ - **Windows:** `%LOCALAPPDATA%\ng-matrix\Cache`
137
+ - **Custom Location:** Set the `NG_MATRIX_CACHE` environment variable to override the default on any OS.
138
+
139
+ ---
140
+
141
+ ## Exit Codes
142
+
143
+ ng-matrix returns standard POSIX exit codes suitable for CI pipelines:
144
+
145
+ - **`0` (Success):** Every version claimed by the library's peer dependencies passed all checks. (If the library declares no `@angular/*` peer dependencies, 0 is returned if any checked version passes).
146
+ - **`1` (Broken Promise):** At least one Angular major claimed by your `peerDependencies` failed a check (install, partial, AOT, strict types, or tests).
147
+ - **`2` (Environment / Setup Error):** ng-matrix could not check a version due to machine or network failure (e.g. disk full ENOSPC, network down, step timeout, invalid CLI options).
148
+
149
+ ---
76
150
 
77
151
  ## In CI
78
152
 
79
- A GitHub Actions job that checks the supported majors on every push:
153
+ ### GitHub Actions
154
+
155
+ Add a step to your workflow to verify compatibility and post the matrix directly to your workflow run summary:
80
156
 
81
157
  ```yaml
158
+ name: Compatibility Matrix
159
+
160
+ on:
161
+ push:
162
+ branches: [main]
163
+ pull_request:
164
+
82
165
  jobs:
83
- angular-matrix:
166
+ compat:
84
167
  runs-on: ubuntu-latest
85
168
  steps:
86
- - uses: actions/checkout@v7
87
- - uses: actions/setup-node@v7
169
+ - uses: actions/checkout@v4
170
+ - uses: actions/setup-node@v4
88
171
  with:
89
172
  node-version: 24
90
173
  - run: npm ci
91
- - run: npx angular-compat-matrix --format markdown >> "$GITHUB_STEP_SUMMARY"
174
+ - name: Run Angular Compatibility Matrix
175
+ run: npx angular-compat-matrix --format markdown >> "$GITHUB_STEP_SUMMARY"
92
176
  ```
93
177
 
94
- (`ng-matrix ci github`, which writes a workflow with one job per Angular version, is coming in v0.2.)
95
-
96
- ## Where version data comes from
178
+ ---
179
+
180
+ ## JSON Output & Stable Schema Contract
181
+
182
+ When running with `--json` or `--format json`, ng-matrix outputs structured JSON with a fixed schema contract.
183
+
184
+ The top-level field `schemaVersion` is guaranteed. Any future additions or modifications to the JSON shape will increment `schemaVersion`.
185
+
186
+ ### Schema Version 1 Specification
187
+
188
+ ```json
189
+ {
190
+ "schemaVersion": 1,
191
+ "tool": {
192
+ "name": "angular-compat-matrix",
193
+ "version": "0.1.0"
194
+ },
195
+ "checkedAt": "2026-10-26T12:00:00.000Z",
196
+ "library": {
197
+ "name": "my-lib",
198
+ "version": "1.0.0",
199
+ "source": "package",
200
+ "compiledWith": "20.2.0",
201
+ "highestMinVersion": "20.0.0",
202
+ "highestMinVersionIn": "component (dist/my-lib.mjs)",
203
+ "declarations": 12
204
+ },
205
+ "packageManager": "npm",
206
+ "installScripts": "toolchain-only",
207
+ "node": "v24.19.0",
208
+ "versions": [
209
+ {
210
+ "major": 21,
211
+ "angularVersion": "21.2.0",
212
+ "supported": true,
213
+ "prerelease": false,
214
+ "claimed": true,
215
+ "peerMisses": [],
216
+ "checks": {
217
+ "peer": {
218
+ "status": "pass",
219
+ "message": "Claimed: @angular/core@^21.0.0.",
220
+ "errorLines": [],
221
+ "durationMs": 0
222
+ },
223
+ "install": {
224
+ "status": "pass",
225
+ "message": "Installed with npm.",
226
+ "errorLines": [],
227
+ "durationMs": 4200
228
+ },
229
+ "partial": {
230
+ "status": "pass",
231
+ "advisory": false,
232
+ "message": "minVersion is 20.0.0.",
233
+ "errorLines": [],
234
+ "durationMs": 12
235
+ },
236
+ "aot": {
237
+ "status": "pass",
238
+ "message": "Production build succeeded.",
239
+ "errorLines": [],
240
+ "durationMs": 6100
241
+ },
242
+ "strict": {
243
+ "status": "pass",
244
+ "message": "Builds with strictTemplates: true and skipLibCheck: false.",
245
+ "errorLines": [],
246
+ "durationMs": 5900
247
+ },
248
+ "tests": {
249
+ "status": "off",
250
+ "message": "Not requested (--tests).",
251
+ "errorLines": [],
252
+ "durationMs": 0
253
+ }
254
+ },
255
+ "skippedInstallScripts": [],
256
+ "omittedEntryPoints": [],
257
+ "dtsErrors": false,
258
+ "durationMs": 16212
259
+ }
260
+ ],
261
+ "findings": [
262
+ {
263
+ "level": "error",
264
+ "majors": [19],
265
+ "text": "AOT build: Production build failed."
266
+ }
267
+ ],
268
+ "exitCode": 0
269
+ }
270
+ ```
97
271
 
98
- Nothing about Angular's versions is hard-coded. It all comes from the npm registry when ng-matrix runs, and is cached for a day (`--offline` reuses the cache):
272
+ ### JSON Fields Reference
273
+
274
+ - **`schemaVersion`** (`number`): The JSON schema version (currently `1`).
275
+ - **`tool`** (`{ name: string, version: string }`): CLI name and version.
276
+ - **`checkedAt`** (`string`): ISO-8601 timestamp when the check was performed.
277
+ - **`library`** (`object`):
278
+ - `name`: Package name.
279
+ - `version`: Package version.
280
+ - `source`: How the package was acquired (`workspace`, `tarball`, or `package`).
281
+ - `compiledWith` (`string?`): The Angular version that built this library.
282
+ - `highestMinVersion` (`string?`): Highest `minVersion` found across declarations.
283
+ - `highestMinVersionIn` (`string?`): Component or declaration file with the highest `minVersion`.
284
+ - `declarations` (`number`): Count of Angular declarables detected.
285
+ - **`packageManager`** (`string`): `'npm'` or `'pnpm'`.
286
+ - **`installScripts`** (`'toolchain-only' | 'all'`): Whether `--allow-scripts` was active.
287
+ - **`node`** (`string`): Node.js runtime version.
288
+ - **`versions`** (`VersionReport[]`): Results per Angular major:
289
+ - `major`: Angular major version number.
290
+ - `angularVersion`: Resolved patch version checked.
291
+ - `supported`: Whether this major is currently within Angular's official 18-month support window.
292
+ - `prerelease`: Whether this major came from a pre-release tag (`next`).
293
+ - `claimed`: `true` if peer dependencies include this version, `false` if excluded, `null` if no Angular peers declared.
294
+ - `peerMisses`: Array of declared peers whose ranges excluded this major (`{ name, range, version }`).
295
+ - `checks`: Object mapping check names (`peer`, `install`, `partial`, `aot`, `strict`, `tests`) to check results:
296
+ - `status`: `'pass' | 'fail' | 'not-run' | 'off' | 'none'`.
297
+ - `message`: Summary message.
298
+ - `errorLines`: First relevant error lines extracted from build or install logs.
299
+ - `fullOutput` (`string?`): Full command stdout and stderr on failure (optional; present only when `--verbose` is used).
300
+ - `hint` (`string?`): Plain-language diagnostic hint.
301
+ - `durationMs`: Execution duration in milliseconds.
302
+ - `advisory` (`boolean?`): `true` when partial check passed only on `minVersion` despite being built with newer Angular.
303
+ - `setupError` (`string?`): Description if the run failed due to machine/environment reasons.
304
+ - `skippedInstallScripts` (`string[]?`): Packages whose install scripts were blocked.
305
+ - `omittedEntryPoints` (`{ entry: string, missingPackage: string }[]?`): Secondary entry points skipped due to undeclared dependencies.
306
+ - `dtsErrors` (`boolean?`): `true` if strict build had errors confined to `node_modules/**/*.d.ts`.
307
+ - `durationMs`: Total duration for checking this major.
308
+ - **`findings`** (`Finding[]`): Merged diagnostics across all checked versions:
309
+ - `level`: `'error' | 'warning' | 'note'`.
310
+ - `majors`: Array of major version numbers affected.
311
+ - `text`: Human-readable problem description and suggested resolution.
312
+ - `suggestedPeerDependencies` (`Record<string, string>?`): Recommended peer dependencies when none were declared.
313
+ - **`exitCode`** (`number`): `0`, `1`, or `2`.
314
+
315
+ ---
316
+
317
+ ## Troubleshooting Real-World Issues
318
+
319
+ Here are the most common real-world incompatibilities identified across popular Angular packages and how to solve them:
320
+
321
+ ### 1. Linker Crash on Older Angular (`Unsupported enum value` / `ɵɵFactoryTarget.Service`)
322
+ - **Symptom:** The library claims support for Angular 20 and 21, but on Angular 20 the linker crashes with `Unsupported enum value` or `Cannot create property 'message' on string`.
323
+ - **Cause:** The library was compiled with Angular 21 or 22. Newer Angular compilers emit bytecode constructs that older linkers do not recognize, even though `minVersion` was claimed as `12.0.0`.
324
+ - **Fix:** Compile your library with the **oldest** Angular major you support, or narrow your `peerDependencies` range to exclude older majors.
325
+
326
+ ### 2. Secondary Entry Points with Undeclared Dependencies
327
+ - **Symptom:** A component in a secondary entry point (e.g. `primeng/chart` or `ng-zorro-antd/graph`) fails because `chart.js` or `dagre-d3-es` is not found.
328
+ - **Handling:** ng-matrix omits secondary entry points that fail exclusively because of undeclared packages and flags a warning finding. However, if your **main** entry point (`.`) imports an undeclared package, ng-matrix fails the AOT check because all consumers would experience build failures.
329
+ - **Fix:** Move undeclared packages into `peerDependencies` (or `dependencies` if required).
330
+
331
+ ### 3. Library Typings Errors under `skipLibCheck: false`
332
+ - **Symptom:** Strict build fails with errors inside declaration files under `node_modules/**/*.d.ts`.
333
+ - **Handling:** When errors are confined strictly to third-party declaration files, ng-matrix marks the strict check as passed with an informational warning finding (`"library typings have errors with skipLibCheck: false"`). It does not fail the version or trigger an exit code 1.
334
+ - **Fix:** Keep your third-party typing dependencies aligned with target TypeScript versions.
335
+
336
+ ### 4. Disk Full (ENOSPC)
337
+ - **Symptom:** Exit code 2 with `ENOSPC: no space left on device`.
338
+ - **Fix:** Clear temporary build folders and caches using `ng-matrix cache clean`, or point `NG_MATRIX_CACHE` and `TMPDIR`/`TEMP` to a drive with sufficient disk space.
339
+
340
+ > **Tip for library authors:** If your library accepts bare attributes like `<comp myFlag>`, give boolean inputs `@Input({ transform: booleanAttribute })` so consumers can write bare attributes under strict template checking (ng-matrix does not check this).
341
+
342
+ ---
343
+
344
+ ## Options Reference
99
345
 
100
- - **Majors:** the published `@angular/core` versions, latest patch of each. A major counts as supported for 18 months after its x.0.0 release. `--next` adds whatever the `next` tag points to, which may be a minor pre-release.
101
- - **Node:** the `engines` of that version's `@angular/core` and `@angular/cli`.
102
- - **TypeScript** (for `--tests`): `@angular/compiler-cli`'s peer range. **RxJS and zone.js:** `@angular/core`'s.
103
- - **Your other peers** (Material, CDK…): the newest version whose own peer ranges accept that Angular.
346
+ ```
347
+ Usage
348
+ npx angular-compat-matrix [options]
349
+ ng-matrix [options] (once installed)
350
+ ng-matrix cache (show cache location and size)
351
+ ng-matrix cache clean (delete the cache)
352
+
353
+ Options
354
+ --versions <list> Angular majors to check: 22, 18-22, 20,22. Default: supported
355
+ majors, claimed majors (from 17), and the newest
356
+ --next Also check whatever the `next` tag points to
357
+ --project <name> The library project, in a workspace with several
358
+ --consumer <dir> Build your own example app instead of a generated smoke app
359
+ --tests Also run the library's tests on each version (slow)
360
+ --test-command <cmd> The test command for --tests (default: <pm> test)
361
+ --format <format> terminal (default), markdown or json
362
+ --json Shorthand for --format json
363
+ --pm <npm|pnpm> Package manager for the apps (default: pnpm if lockfile present, else npm)
364
+ --build <cmd> Build the library with this command instead of the detected one
365
+ --skip-build Pack the existing build output as is
366
+ --tarball <file> Check this packed .tgz instead of building
367
+ --package <spec> Check a published package, e.g. some-lib@3.1.0
368
+ --concurrency <n> Versions checked at once (default: based on available memory)
369
+ --step-timeout <min> Maximum minutes any single step may take (default: 20)
370
+ --keep Keep the temporary apps after completion for debugging
371
+ --allow-scripts Permit all package install scripts to run (default: toolchain only)
372
+ --offline Use only cached registry answers and packages
373
+ --cwd <dir> The library's workspace (default: current directory)
374
+ --verbose Show commands run and full build/install output for failures
375
+ -h, --help Show help
376
+ -v, --version Show version
377
+ ```
104
378
 
105
- ## Scope of v0.1
379
+ ---
106
380
 
107
- In: Angular CLI workspaces and standalone ng-packagr libraries; npm 11 and a pinned pnpm (12.15.0); the six checks; terminal, Markdown and JSON output.
381
+ ## Known Limitations & Roadmap
108
382
 
109
- Coming in v0.2: Nx workspaces (for now, run ng-matrix in the library folder with `--build "npx nx build my-lib"`), `ng-matrix ci github`, and SSR and with-zone.js columns.
383
+ - **Supported Workspaces:** Angular CLI workspaces and standalone `ng-packagr` libraries.
384
+ - **Node.js:** Node 24.15 or later is recommended to cover all Angular majors from 17 through 22.
385
+ - **Planned:**
386
+ - `ng-matrix ci github`: Generates dedicated GitHub Actions workflows with matrix parallelism.
387
+ - First-class Nx workspace discovery and integration.
388
+ - Dedicated SSR (Server-Side Rendering) and `with-zone.js` validation columns.
389
+ - **Out of Scope:** Yarn and Bun (npm and pnpm cover package testing; JSON output makes custom wrappers simple), app migration tooling, and runtime browser testing.
110
390
 
111
- Not planned: Yarn and Bun (the JSON output makes them easy to add), checking apps for upgrade readiness, and anything that only shows up at runtime in a browser.
391
+ ---
112
392
 
113
393
  ## Development
114
394
 
115
395
  ```sh
116
396
  npm install
117
- npm test # unit tests, against recorded registry answers
118
- npm run build
119
- npm ci --prefix fixtures # the Angular 22 toolchain the fixture libraries build with
120
- node test/fixtures/run.ts # every fixture case (or name one, e.g. healthy-npm)
397
+ npm test # run all unit tests
398
+ npm run build # compile TypeScript
399
+ npm ci --prefix fixtures # install Angular fixture toolchain
400
+ node test/fixtures/run.ts # run fixture integration tests
121
401
  ```
122
402
 
123
- `fixtures/libs` holds small libraries that each fail exactly one check: a feature newer than an older claimed major, a `minVersion` that's too low, peer ranges narrower and wider than reality, a CDK peer that leaves a version out, a type error only strict templates catch, typings only `skipLibCheck: false` catches, a kit over the default size budget, full compilation output, and tests that use a newer API. `fixtures/workspaces/cli-workspace` is an Angular CLI workspace, and `fixtures/consumers/healthy-example` an example app for `--consumer`. The suite also checks published packages: Angular Material 22, angular2-multiselect-dropdown 10, ngx-toastr and angular-calendar. [docs/real-world-round.md](docs/real-world-round.md) has the results of a run against 15 popular libraries.
403
+ ---
124
404
 
125
405
  ## License
126
406
 
package/dist/app.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
- import { errorLines, exec, quietEnv } from "./exec.js";
3
+ import { environmentProblem, errorLines, exec, quietEnv, stepTimeout } from "./exec.js";
4
4
  import { binOf, ensureTool } from "./tools.js";
5
5
  /** One flag set that works, without prompts, for every CLI from 17 to 22. */
6
6
  export function newAppArgs(pm) {
@@ -21,11 +21,12 @@ export async function generateApp(angular, pm, parentDir, cacheDir, offline) {
21
21
  const result = await exec(process.execPath, [binOf(cli, 'ng'), ...newAppArgs(pm)], {
22
22
  cwd: parentDir,
23
23
  env: quietEnv({ CI: '1' }),
24
- timeoutMs: 10 * 60_000,
24
+ timeoutMs: stepTimeout(),
25
25
  });
26
26
  const appDir = join(parentDir, 'smoke');
27
27
  if (result.code !== 0 || !existsSync(join(appDir, 'angular.json'))) {
28
- throw new Error(`ng new (Angular CLI ${angular.cliVersion}) failed:\n${errorLines(result.output).join('\n')}`);
28
+ const why = environmentProblem(result);
29
+ throw new Error(`ng new (Angular CLI ${angular.cliVersion}) failed${why ? `: ${why}` : ''}:\n${errorLines(result.output).join('\n')}`);
29
30
  }
30
31
  return appDir;
31
32
  }
package/dist/build.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createRequire } from 'node:module';
2
2
  import { dirname, join } from 'node:path';
3
3
  import { writeTsconfig } from "./app.js";
4
- import { errorLines, exec, quietEnv } from "./exec.js";
4
+ import { environmentProblem, errorLines, exec, quietEnv, stepTimeout } from "./exec.js";
5
5
  export async function ngBuild(appDir, baseTsConfig, mode, files) {
6
6
  writeTsconfig(appDir, baseTsConfig, mode, files);
7
7
  const require = createRequire(join(appDir, 'package.json'));
@@ -10,12 +10,19 @@ export async function ngBuild(appDir, baseTsConfig, mode, files) {
10
10
  const result = await exec(process.execPath, [ng, 'build', '--configuration', 'production'], {
11
11
  cwd: appDir,
12
12
  env: quietEnv({ CI: '1' }),
13
- timeoutMs: 20 * 60_000,
13
+ timeoutMs: stepTimeout(),
14
14
  });
15
15
  if (result.code === 0)
16
16
  return { pass: true, errorLines: [], durationMs: result.durationMs };
17
- const lines = result.timedOut ? ['The build timed out after 20 minutes.'] : errorLines(result.output);
18
- return { pass: false, errorLines: lines, hint: hintFor(result.output), durationMs: result.durationMs };
17
+ const lines = errorLines(result.output);
18
+ return {
19
+ pass: false,
20
+ errorLines: lines,
21
+ fullOutput: result.output,
22
+ hint: hintFor(result.output) ?? lines[0],
23
+ durationMs: result.durationMs,
24
+ environment: environmentProblem(result),
25
+ };
19
26
  }
20
27
  /** Plain-language explanations for build failures whose messages don't say what's wrong. */
21
28
  export function hintFor(output) {
@@ -39,8 +46,122 @@ export function hintFor(output) {
39
46
  if (/has no exported member(?: named)? '([\w$ɵ]+)'/.test(output) && /\.d\.ts/.test(output)) {
40
47
  return "The package's typings use a type this Angular doesn't export. Apps with skipLibCheck: false fail to compile.";
41
48
  }
49
+ if (isDtsOnlyFailure(errorLines(output))) {
50
+ return 'library typings have errors with skipLibCheck: false';
51
+ }
42
52
  if (/error NG8002|error NG8008|is not assignable to type|TS2322/.test(output)) {
43
53
  return 'A template using the package type-checks only loosely: with strict templates, Angular reports the error above. If it comes from a bare attribute like <div myDirective>, give that boolean input a booleanAttribute transform.';
44
54
  }
45
55
  return undefined;
46
56
  }
57
+ /**
58
+ * Whether the build failed only because of errors inside declaration files (.d.ts)
59
+ * of a library, with no errors in the smoke app's templates or components.
60
+ */
61
+ export function isDtsOnlyFailure(lines) {
62
+ if (!lines.length)
63
+ return false;
64
+ let hasDtsError = false;
65
+ for (const line of lines) {
66
+ if (/error NG\d+/.test(line))
67
+ return false;
68
+ if (/(?:src[\\/]ngm-smoke|src[\\/]ngm-main)\.ts/i.test(line))
69
+ return false;
70
+ if (/\.d\.ts(?::|\s|$)/i.test(line))
71
+ hasDtsError = true;
72
+ }
73
+ return hasDtsError;
74
+ }
75
+ function packageNameFromSpecifier(specifier) {
76
+ if (specifier.startsWith('@')) {
77
+ const parts = specifier.split('/');
78
+ return parts.slice(0, 2).join('/');
79
+ }
80
+ return specifier.split('/')[0];
81
+ }
82
+ function isDeclared(pkg, manifest) {
83
+ if (pkg === manifest.name)
84
+ return true;
85
+ if (manifest.dependencies && pkg in manifest.dependencies)
86
+ return true;
87
+ if (manifest.peerDependencies && pkg in manifest.peerDependencies)
88
+ return true;
89
+ if (manifest.optionalDependencies && pkg in manifest.optionalDependencies)
90
+ return true;
91
+ return false;
92
+ }
93
+ function findEntryPointForFile(filePath, manifest, entries) {
94
+ if (entries.length <= 1)
95
+ return entries[0] ?? manifest.name;
96
+ const normalizedPath = filePath.replace(/\\/g, '/');
97
+ const exportsMap = manifest.exports;
98
+ if (exportsMap && typeof exportsMap === 'object') {
99
+ for (const [key, value] of Object.entries(exportsMap)) {
100
+ const entryName = key === '.' ? manifest.name : `${manifest.name}/${key.slice(2)}`;
101
+ if (typeof value === 'string') {
102
+ const clean = value.replace(/^\.\//, '');
103
+ if (normalizedPath.endsWith(clean))
104
+ return entryName;
105
+ }
106
+ else if (value && typeof value === 'object') {
107
+ for (const target of Object.values(value)) {
108
+ if (typeof target === 'string') {
109
+ const clean = target.replace(/^\.\//, '');
110
+ if (normalizedPath.endsWith(clean))
111
+ return entryName;
112
+ }
113
+ }
114
+ }
115
+ }
116
+ }
117
+ for (const entry of entries) {
118
+ if (entry === manifest.name)
119
+ continue;
120
+ const sub = entry.slice(manifest.name.length + 1);
121
+ if (normalizedPath.includes(sub) || normalizedPath.includes(sub.replace(/\//g, '-'))) {
122
+ return entry;
123
+ }
124
+ }
125
+ return entries[0] ?? manifest.name;
126
+ }
127
+ /**
128
+ * Spots when a build fails ONLY because entry points import packages that aren't declared
129
+ * in dependencies, peerDependencies or optionalDependencies (e.g. chart.js, lodash, cron-parser).
130
+ */
131
+ export function findUndeclaredImports(lines, manifest, entries) {
132
+ const undeclared = [];
133
+ const seen = new Set();
134
+ let hasOtherErrors = false;
135
+ for (let i = 0; i < lines.length; i++) {
136
+ const line = lines[i];
137
+ const esbuildMatch = /Could not resolve ["']([^"']+)["']/.exec(line);
138
+ const webpackMatch = /Can't resolve '([^']+)'/.exec(line);
139
+ const specifier = esbuildMatch?.[1] ?? webpackMatch?.[1];
140
+ if (specifier) {
141
+ const pkg = packageNameFromSpecifier(specifier);
142
+ if (!isDeclared(pkg, manifest)) {
143
+ let filePath = '';
144
+ for (let j = i + 1; j < Math.min(i + 6, lines.length); j++) {
145
+ const locMatch = /(?:^|\s+)([A-Za-z]:[\\/][^\s:]+|node_modules[\\/][^\s:]+|[^\s:]+):\d+:\d+:/.exec(lines[j]);
146
+ if (locMatch) {
147
+ filePath = locMatch[1];
148
+ break;
149
+ }
150
+ }
151
+ const entry = findEntryPointForFile(filePath, manifest, entries);
152
+ const key = `${entry}\0${pkg}`;
153
+ if (!seen.has(key)) {
154
+ seen.add(key);
155
+ undeclared.push({ entry, missingPackage: pkg });
156
+ }
157
+ }
158
+ else {
159
+ hasOtherErrors = true;
160
+ }
161
+ }
162
+ else if (/^X \[ERROR\]/.test(line) && !line.includes('Could not resolve')) {
163
+ hasOtherErrors = true;
164
+ }
165
+ }
166
+ return hasOtherErrors ? [] : undeclared;
167
+ }