angular-compat-matrix 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nizamudeen S
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,127 @@
1
- # Temporary Holding Version
1
+ # angular-compat-matrix
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Which Angular versions does your published library really work with?
4
+
5
+ Run it with `npx angular-compat-matrix`, or install the package and run `ng-matrix`: both names start the same command.
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.
8
+
9
+ ```
10
+ $ npx angular-compat-matrix
11
+
12
+ my-dropdown@3.0.0 (compiled with Angular 21.2.0; highest minVersion 20.0.0)
13
+
14
+ peer range install partial AOT build strict types
15
+ Angular 19 ✓ ✓ ✗ — —
16
+ Angular 20 ✓ ✓ ✓ ✓ ✗
17
+ Angular 21 ✓ ✓ ✓ ✓ ✓
18
+ Angular 22 ✗ ✓ ✓ ✓ ✓
19
+
20
+ ✗ 19: partial: Needs Angular 20.0.0 or later: highest minVersion 20.0.0, in
21
+ MyDropdownComponent. Build the library with the oldest Angular you support, or take
22
+ this version out of the peer range.
23
+ ✗ 20: strict types: A template using the package type-checks only loosely…
24
+ ! 22: works, but peerDependencies doesn't allow it (@angular/core "^19.0.0 || ^20.0.0 ||
25
+ ^21.0.0"). Widen the range to include it.
26
+ ```
27
+
28
+ ## Usage
29
+
30
+ Run it in your library's workspace: an Angular CLI workspace with a library project, or a folder with `ng-package.json`.
31
+
32
+ ```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
41
+ ```
42
+
43
+ `npx angular-compat-matrix --help` lists every option.
44
+
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.
46
+
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.
48
+
49
+ ## What it checks
50
+
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.
52
+
53
+ | Check | Question | How |
54
+ | --- | --- | --- |
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 |
61
+
62
+ ### Why build, when there's minVersion?
63
+
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.
65
+
66
+ ### The smoke app
67
+
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:
69
+
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
74
+
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.
76
+
77
+ ## In CI
78
+
79
+ A GitHub Actions job that checks the supported majors on every push:
80
+
81
+ ```yaml
82
+ jobs:
83
+ angular-matrix:
84
+ runs-on: ubuntu-latest
85
+ steps:
86
+ - uses: actions/checkout@v7
87
+ - uses: actions/setup-node@v7
88
+ with:
89
+ node-version: 24
90
+ - run: npm ci
91
+ - run: npx angular-compat-matrix --format markdown >> "$GITHUB_STEP_SUMMARY"
92
+ ```
93
+
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
97
+
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):
99
+
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.
104
+
105
+ ## Scope of v0.1
106
+
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.
108
+
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.
110
+
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.
112
+
113
+ ## Development
114
+
115
+ ```sh
116
+ 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)
121
+ ```
122
+
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.
124
+
125
+ ## License
126
+
127
+ MIT © 2026 Nizamudeen S
package/dist/app.js ADDED
@@ -0,0 +1,90 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { errorLines, exec, quietEnv } from "./exec.js";
4
+ import { binOf, ensureTool } from "./tools.js";
5
+ /** One flag set that works, without prompts, for every CLI from 17 to 22. */
6
+ export function newAppArgs(pm) {
7
+ return [
8
+ 'new',
9
+ 'smoke',
10
+ '--minimal',
11
+ '--skip-git',
12
+ '--skip-install',
13
+ '--defaults',
14
+ '--interactive=false',
15
+ '--ssr=false',
16
+ `--package-manager=${pm}`,
17
+ ];
18
+ }
19
+ export async function generateApp(angular, pm, parentDir, cacheDir, offline) {
20
+ const cli = await ensureTool(cacheDir, '@angular/cli', angular.cliVersion, offline);
21
+ const result = await exec(process.execPath, [binOf(cli, 'ng'), ...newAppArgs(pm)], {
22
+ cwd: parentDir,
23
+ env: quietEnv({ CI: '1' }),
24
+ timeoutMs: 10 * 60_000,
25
+ });
26
+ const appDir = join(parentDir, 'smoke');
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')}`);
29
+ }
30
+ return appDir;
31
+ }
32
+ export function readJson(path) {
33
+ // Generated tsconfig files start with /* comments */; JSON.parse can't take them.
34
+ const text = readFileSync(path, 'utf8').replace(/^\s*\/\*[\s\S]*?\*\/\s*/gm, '').replace(/^\s*\/\/.*$/gm, '');
35
+ return JSON.parse(text);
36
+ }
37
+ export function writeJson(path, value) {
38
+ writeFileSync(path, JSON.stringify(value, null, 2) + '\n');
39
+ }
40
+ /** The app project in angular.json: the generated one, or the first application of a --consumer app. */
41
+ export function appProject(appDir) {
42
+ const workspace = readJson(join(appDir, 'angular.json'));
43
+ const entry = Object.entries(workspace.projects).find(([, p]) => (p.projectType ?? 'application') === 'application');
44
+ if (!entry)
45
+ throw new Error(`${appDir}/angular.json has no application project.`);
46
+ const [name, project] = entry;
47
+ const tsConfig = String(project.architect?.build?.options?.tsConfig ?? 'tsconfig.app.json');
48
+ return { name, tsConfig };
49
+ }
50
+ /**
51
+ * Points the build at ng-matrix's own tsconfig (and, for a generated app, its own main file),
52
+ * and removes the production budgets so a large component kit doesn't fail the build on size.
53
+ */
54
+ export function configureBuild(appDir, mainFile) {
55
+ const path = join(appDir, 'angular.json');
56
+ const workspace = readJson(path);
57
+ const project = appProject(appDir);
58
+ const build = workspace.projects[project.name].architect.build;
59
+ build.options ??= {};
60
+ if (mainFile) {
61
+ // `browser` for the application builder (17+), `main` for the older webpack one.
62
+ if ('main' in build.options && !('browser' in build.options))
63
+ build.options.main = mainFile;
64
+ else
65
+ build.options.browser = mainFile;
66
+ }
67
+ build.options.tsConfig = 'tsconfig.ngm.json';
68
+ for (const configuration of Object.values(build.configurations ?? {})) {
69
+ if ('budgets' in configuration)
70
+ configuration.budgets = [];
71
+ }
72
+ writeJson(path, workspace);
73
+ return project;
74
+ }
75
+ /**
76
+ * Writes tsconfig.ngm.json extending the app's own tsconfig, with the template strictness set
77
+ * explicitly: Angular 17 to 21 apps write strictTemplates: true, and 22 makes it the default.
78
+ */
79
+ export function writeTsconfig(appDir, baseTsConfig, mode, files) {
80
+ const config = {
81
+ extends: `./${baseTsConfig.replace(/^\.\//, '')}`,
82
+ compilerOptions: { skipLibCheck: mode.skipLibCheck },
83
+ angularCompilerOptions: { strictTemplates: mode.strictTemplates },
84
+ };
85
+ if (files) {
86
+ config.files = files;
87
+ config.include = [];
88
+ }
89
+ writeJson(join(appDir, 'tsconfig.ngm.json'), config);
90
+ }
package/dist/build.js ADDED
@@ -0,0 +1,46 @@
1
+ import { createRequire } from 'node:module';
2
+ import { dirname, join } from 'node:path';
3
+ import { writeTsconfig } from "./app.js";
4
+ import { errorLines, exec, quietEnv } from "./exec.js";
5
+ export async function ngBuild(appDir, baseTsConfig, mode, files) {
6
+ writeTsconfig(appDir, baseTsConfig, mode, files);
7
+ const require = createRequire(join(appDir, 'package.json'));
8
+ const cliManifest = require.resolve('@angular/cli/package.json');
9
+ const ng = join(dirname(cliManifest), 'bin', 'ng.js');
10
+ const result = await exec(process.execPath, [ng, 'build', '--configuration', 'production'], {
11
+ cwd: appDir,
12
+ env: quietEnv({ CI: '1' }),
13
+ timeoutMs: 20 * 60_000,
14
+ });
15
+ if (result.code === 0)
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 };
19
+ }
20
+ /** Plain-language explanations for build failures whose messages don't say what's wrong. */
21
+ export function hintFor(output) {
22
+ if (/Unsupported enum value|Cannot create property 'message' on string/.test(output)) {
23
+ return 'The compiled code uses features newer than this Angular, though its minVersion says otherwise: the linker crashed on it. Build the library with the oldest Angular you support.';
24
+ }
25
+ const linker = /depends upon a library published using Angular version ([\d.]+), which requires Angular version ([\d.]+) or newer/.exec(output);
26
+ if (linker) {
27
+ return `The package was compiled with Angular ${linker[1]} and needs Angular ${linker[2]} or newer. Build it with the oldest Angular you support.`;
28
+ }
29
+ const missing = /No matching export in "[^"]*node_modules[\\/](@angular[\\/][\w-]+)[^"]*" for import "([\w$ɵ]+)"/.exec(output);
30
+ if (missing) {
31
+ return `The package imports ${missing[2]} from ${missing[1].replace('\\', '/')}, which this Angular version doesn't have.`;
32
+ }
33
+ // TS2305 inside a .d.ts: `Module '"@angular/core"' has no exported member 'X'`, with the file on the next line.
34
+ const member = /Module '"([^"]+)"' has no exported member(?: named)? '([\w$ɵ]+)'[\s\S]{0,300}?node_modules[\\/](?:\.pnpm[\\/]\S*?[\\/]node_modules[\\/])?((?:@[\w.-]+[\\/])?[\w.-]+)[\\/]\S*\.d\.ts/.exec(output);
35
+ if (member) {
36
+ const pkg = member[3].replace('\\', '/');
37
+ return `${pkg}'s typings use ${member[2]}, which this version's ${member[1]} doesn't export. Apps with skipLibCheck: false, or that use the type, fail to compile.`;
38
+ }
39
+ if (/has no exported member(?: named)? '([\w$ɵ]+)'/.test(output) && /\.d\.ts/.test(output)) {
40
+ return "The package's typings use a type this Angular doesn't export. Apps with skipLibCheck: false fail to compile.";
41
+ }
42
+ if (/error NG8002|error NG8008|is not assignable to type|TS2322/.test(output)) {
43
+ 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
+ }
45
+ return undefined;
46
+ }
package/dist/cli.js ADDED
@@ -0,0 +1,210 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { dirname, join, resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { parseArgs } from 'node:util';
7
+ import semver from 'semver';
8
+ import { LibraryError, prepareLibrary } from "./library.js";
9
+ import { angularImports, scanPackage, summarize } from "./partial.js";
10
+ import { Registry } from "./registry.js";
11
+ import { exitCode, findings, renderJson, renderMarkdown, renderTerminal } from "./report.js";
12
+ import { defaultConcurrency, runAll } from "./run.js";
13
+ import { defaultCacheDir } from "./tools.js";
14
+ import { defaultMajors, describeMajor, loadVersionSources, majorsOf, nextVersion, nodeSatisfies, parseVersionList, } from "./versions.js";
15
+ const HELP = `ng-matrix: which Angular versions does your published library really work with?
16
+
17
+ It builds and packs your library, installs the tarball into a fresh app for each Angular
18
+ major (made with that major's own CLI), builds it, and reports a matrix.
19
+
20
+ Usage
21
+ npx angular-compat-matrix [options]
22
+ ng-matrix [options] (once installed)
23
+
24
+ Options
25
+ --versions <list> Angular majors to check: 22, 18-22, 20,22. Default: the supported
26
+ majors, every major the peer range claims (from 17), and the newest
27
+ --next Also check whatever the \`next\` tag points to
28
+ --project <name> The library project, in a workspace with several
29
+ --consumer <dir> Build your own example app instead of a generated smoke app
30
+ --tests Also run the library's tests on each version (slow)
31
+ --test-command <cmd> The test command for --tests (default: <pm> test)
32
+ --format <format> terminal (default), markdown or json
33
+ --pm <npm|pnpm> Package manager for the apps (default: pnpm if there's a
34
+ pnpm-lock.yaml, else npm)
35
+ --build <cmd> Build the library with this command instead of the detected one
36
+ --skip-build Pack the existing build output as is
37
+ --tarball <file> Check this packed .tgz instead of building
38
+ --package <spec> Check a published package, e.g. some-lib@3.1.0
39
+ --concurrency <n> Versions checked at once (default: as many as free memory allows)
40
+ --keep Keep the temporary apps, for debugging
41
+ --offline Use only cached registry answers and packages
42
+ --cwd <dir> The library's workspace (default: the current folder)
43
+ --verbose Show every error line, not just the first few
44
+ -h, --help This help
45
+ -v, --version ng-matrix's version
46
+
47
+ Exit code: 1 when a version inside the peer range fails, 2 when a version couldn't be
48
+ checked at all, 0 otherwise. A version outside the range that fails is a note, not an error.
49
+ `;
50
+ function toolVersion() {
51
+ const manifest = join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json');
52
+ return JSON.parse(readFileSync(manifest, 'utf8')).version;
53
+ }
54
+ /** A problem ng-matrix explains and stops on, with exit code 2. */
55
+ class Stop extends Error {
56
+ lines;
57
+ constructor(message, lines = []) {
58
+ super(message);
59
+ this.lines = lines;
60
+ }
61
+ }
62
+ function fail(message, lines = []) {
63
+ throw new Stop(message, lines);
64
+ }
65
+ function explain(error) {
66
+ if (error instanceof Stop || error instanceof LibraryError) {
67
+ process.stderr.write(`ng-matrix: ${error.message}\n${error.lines.map((l) => ` ${l}\n`).join('')}`);
68
+ }
69
+ else if (String(error.code).startsWith('ERR_PARSE_ARGS')) {
70
+ process.stderr.write(`ng-matrix: ${error.message}. See --help.\n`);
71
+ }
72
+ else {
73
+ process.stderr.write(`ng-matrix: unexpected error\n${error.stack ?? String(error)}\n`);
74
+ }
75
+ return 2;
76
+ }
77
+ export async function main(argv) {
78
+ const { values, positionals } = parseArgs({
79
+ args: argv,
80
+ allowPositionals: true,
81
+ options: {
82
+ versions: { type: 'string' },
83
+ next: { type: 'boolean', default: false },
84
+ project: { type: 'string' },
85
+ consumer: { type: 'string' },
86
+ tests: { type: 'boolean', default: false },
87
+ 'test-command': { type: 'string' },
88
+ format: { type: 'string', default: 'terminal' },
89
+ pm: { type: 'string' },
90
+ build: { type: 'string' },
91
+ 'skip-build': { type: 'boolean', default: false },
92
+ tarball: { type: 'string' },
93
+ package: { type: 'string' },
94
+ concurrency: { type: 'string' },
95
+ keep: { type: 'boolean', default: false },
96
+ offline: { type: 'boolean', default: false },
97
+ cwd: { type: 'string' },
98
+ verbose: { type: 'boolean', default: false },
99
+ help: { type: 'boolean', short: 'h', default: false },
100
+ version: { type: 'boolean', short: 'v', default: false },
101
+ },
102
+ });
103
+ if (values.help) {
104
+ process.stdout.write(HELP);
105
+ return 0;
106
+ }
107
+ if (values.version) {
108
+ process.stdout.write(`${toolVersion()}\n`);
109
+ return 0;
110
+ }
111
+ if (positionals[0] === 'ci')
112
+ fail('`ng-matrix ci github` is planned for v0.2.');
113
+ if (positionals.length)
114
+ fail(`Unknown argument "${positionals[0]}". See --help.`);
115
+ const format = values.format;
116
+ if (!['terminal', 'markdown', 'json'].includes(format))
117
+ fail(`--format must be terminal, markdown or json, not "${format}".`);
118
+ const cwd = resolve(values.cwd ?? process.cwd());
119
+ const pm = (values.pm ?? (existsSync(join(cwd, 'pnpm-lock.yaml')) ? 'pnpm' : 'npm'));
120
+ if (pm !== 'npm' && pm !== 'pnpm')
121
+ fail(`--pm must be npm or pnpm (yarn and bun come later), not "${pm}".`);
122
+ const consumer = values.consumer ? resolve(cwd, values.consumer) : undefined;
123
+ const log = (line) => process.stderr.write(`[ng-matrix] ${line}\n`);
124
+ const cacheDir = defaultCacheDir();
125
+ const registry = new Registry({
126
+ cacheDir,
127
+ offline: values.offline,
128
+ recordedDir: process.env.NG_MATRIX_RECORDED_REGISTRY,
129
+ });
130
+ const workDir = mkdtempSync(join(tmpdir(), 'ng-matrix-'));
131
+ if (values.keep)
132
+ log(`Working in ${workDir} (--keep)`);
133
+ try {
134
+ const library = await prepareLibrary({
135
+ cwd,
136
+ project: values.project,
137
+ build: values.build,
138
+ skipBuild: values['skip-build'],
139
+ tarball: values.tarball,
140
+ packageSpec: values.package,
141
+ }, workDir, log);
142
+ const summary = summarize(scanPackage(library.dir));
143
+ const now = new Date();
144
+ const sources = await loadVersionSources(registry);
145
+ const majors = values.versions
146
+ ? parseVersionList(values.versions)
147
+ : defaultMajors(sources.core, library.manifest.peerDependencies?.['@angular/core'], now);
148
+ const published = new Set(majorsOf(sources.core));
149
+ for (const major of majors)
150
+ if (!published.has(major))
151
+ fail(`Angular ${major} isn't published on npm.`);
152
+ const angular = majors.map((major) => describeMajor(sources, major, now));
153
+ const next = values.next ? nextVersion(sources.core) : undefined;
154
+ if (values.next && !next)
155
+ log('The `next` tag is not ahead of `latest` right now; nothing extra to check.');
156
+ if (next)
157
+ angular.push(describeMajor(sources, semver.major(next), now, next));
158
+ // One Node for every version: Angular's engines ranges only set minimums.
159
+ const tooOld = angular.filter((a) => !nodeSatisfies(process.version, a.nodeRanges));
160
+ if (tooOld.length) {
161
+ fail(`This Node (${process.version}) can't run ${tooOld.map((a) => `Angular ${a.major}`).join(', ')}.`, tooOld.map((a) => `Angular ${a.major} needs Node ${a.nodeRanges.join(' and ')}`).concat('Use a Node that satisfies every version you check (24.15 or later covers 17 to 22), or check fewer versions.'));
162
+ }
163
+ const concurrency = values.concurrency ? Math.max(1, Number(values.concurrency)) : defaultConcurrency(angular.length);
164
+ log(`Checking ${library.manifest.name}@${library.manifest.version} on ${angular.map((a) => a.version).join(', ')} with ${pm}, ${concurrency} at a time`);
165
+ const versions = await runAll({
166
+ library,
167
+ summary,
168
+ pm,
169
+ registry,
170
+ cacheDir,
171
+ offline: values.offline,
172
+ workDir,
173
+ workspace: values.tarball || values.package ? undefined : cwd,
174
+ tests: values.tests,
175
+ testCommand: values['test-command'],
176
+ consumer,
177
+ keep: values.keep,
178
+ log,
179
+ }, angular, concurrency);
180
+ const report = {
181
+ tool: { name: 'angular-compat-matrix', version: toolVersion() },
182
+ checkedAt: now.toISOString(),
183
+ library: {
184
+ name: library.manifest.name,
185
+ version: library.manifest.version,
186
+ source: library.source,
187
+ compiledWith: summary.compiledWith,
188
+ highestMinVersion: summary.highest?.minVersion,
189
+ highestMinVersionIn: summary.highest ? `${summary.highest.type} (${summary.highest.file})` : undefined,
190
+ declarations: summary.declarations,
191
+ },
192
+ packageManager: pm,
193
+ node: process.version,
194
+ versions,
195
+ findings: findings(versions, library.manifest, angularImports(library.dir)),
196
+ exitCode: exitCode(versions),
197
+ };
198
+ const render = format === 'json' ? renderJson : format === 'markdown' ? renderMarkdown : (r) => renderTerminal(r, values.verbose);
199
+ process.stdout.write((format === 'terminal' ? '\n' : '') + render(report));
200
+ return report.exitCode;
201
+ }
202
+ catch (error) {
203
+ return explain(error);
204
+ }
205
+ finally {
206
+ if (!values.keep)
207
+ rmSync(workDir, { recursive: true, force: true, maxRetries: 3 });
208
+ }
209
+ }
210
+ main(process.argv.slice(2)).then((code) => process.exit(code), (error) => process.exit(explain(error)));
package/dist/exec.js ADDED
@@ -0,0 +1,110 @@
1
+ import { spawn, spawnSync } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+ const ANSI = /\u001b\[[0-9;?]*[A-Za-z]|\u001b\][^\u0007]*\u0007/g;
5
+ export function stripAnsi(text) {
6
+ return text.replace(ANSI, '');
7
+ }
8
+ /** Environment every child gets: no prompts, no analytics, no colour codes to strip later. */
9
+ export function quietEnv(extra = {}) {
10
+ return {
11
+ ...process.env,
12
+ NG_CLI_ANALYTICS: 'false',
13
+ NG_DISABLE_VERSION_CHECK: '1',
14
+ NO_UPDATE_NOTIFIER: '1',
15
+ NPM_CONFIG_UPDATE_NOTIFIER: 'false',
16
+ NPM_CONFIG_FUND: 'false',
17
+ NPM_CONFIG_AUDIT: 'false',
18
+ FORCE_COLOR: '0',
19
+ ...extra,
20
+ };
21
+ }
22
+ export function exec(command, args, options) {
23
+ const started = Date.now();
24
+ return new Promise((resolve) => {
25
+ const child = options.shell
26
+ ? spawn([command, ...args].join(' '), { cwd: options.cwd, env: options.env ?? quietEnv(), shell: true })
27
+ : spawn(command, args, { cwd: options.cwd, env: options.env ?? quietEnv() });
28
+ let output = '';
29
+ let timedOut = false;
30
+ const append = (chunk) => (output += chunk.toString('utf8'));
31
+ child.stdout?.on('data', append);
32
+ child.stderr?.on('data', append);
33
+ const timer = options.timeoutMs
34
+ ? setTimeout(() => {
35
+ timedOut = true;
36
+ killTree(child.pid);
37
+ }, options.timeoutMs)
38
+ : undefined;
39
+ const finish = (code) => {
40
+ if (timer)
41
+ clearTimeout(timer);
42
+ resolve({ code, output: stripAnsi(output), durationMs: Date.now() - started, timedOut });
43
+ };
44
+ child.on('error', (error) => {
45
+ output += `\n${error.message}`;
46
+ finish(127);
47
+ });
48
+ child.on('close', (code) => finish(code ?? 1));
49
+ });
50
+ }
51
+ function killTree(pid) {
52
+ if (!pid)
53
+ return;
54
+ if (process.platform === 'win32') {
55
+ spawnSync('taskkill', ['/pid', String(pid), '/T', '/F']);
56
+ }
57
+ else {
58
+ try {
59
+ process.kill(pid, 'SIGKILL');
60
+ }
61
+ catch {
62
+ // already gone
63
+ }
64
+ }
65
+ }
66
+ /**
67
+ * npm's own CLI script, so npm runs as `node npm-cli.js` without a shell
68
+ * (spawning `npm.cmd` on Windows needs one, and then every argument needs quoting).
69
+ */
70
+ export function npmCli() {
71
+ const candidates = [
72
+ process.env.npm_execpath,
73
+ join(dirname(process.execPath), 'node_modules', 'npm', 'bin', 'npm-cli.js'),
74
+ join(dirname(process.execPath), '..', 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js'),
75
+ ];
76
+ for (const candidate of candidates) {
77
+ if (candidate && /npm-cli\.js$/.test(candidate) && existsSync(candidate))
78
+ return [process.execPath, candidate];
79
+ }
80
+ throw new Error("Couldn't find npm next to this Node. ng-matrix needs the npm that ships with Node.");
81
+ }
82
+ export function npm(args, options) {
83
+ const [node, cli] = npmCli();
84
+ return exec(node, [cli, ...args], options);
85
+ }
86
+ /** The first lines that look like errors, with a little context, for reports. */
87
+ export function errorLines(output, max = 12) {
88
+ const lines = output.split(/\r?\n/).map((line) => line.trimEnd());
89
+ // Errors first; warnings (Angular prints NG8113 "not used" ones, say) only when there's nothing else.
90
+ const errors = /(\[ERROR\]|\berror TS\d+|\bERR_PNPM_|npm error|^\s*Error:|✘|✕|^\s*X |\berror NG\d{4}\b|ERESOLVE|unmet peer|invalid: )/i;
91
+ const anything = /(\[WARNING\]|\bNG\d{4}\b|\bwarn)/i;
92
+ for (const marker of [errors, anything]) {
93
+ const picked = [];
94
+ for (let i = 0; i < lines.length && picked.length < max; i++) {
95
+ if (!marker.test(lines[i]))
96
+ continue;
97
+ for (let j = i; j < Math.min(lines.length, i + 6) && picked.length < max; j++) {
98
+ const line = lines[j];
99
+ if (j > i && (errors.test(line) || anything.test(line)) && !marker.test(line))
100
+ break;
101
+ if (line.trim() && !picked.includes(line))
102
+ picked.push(line);
103
+ }
104
+ i += 5;
105
+ }
106
+ if (picked.length)
107
+ return picked;
108
+ }
109
+ return lines.filter((line) => line.trim()).slice(-max);
110
+ }