angular-compat-matrix 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +406 -2
- package/dist/app.js +91 -0
- package/dist/build.js +167 -0
- package/dist/cleanup.js +117 -0
- package/dist/cli.js +262 -0
- package/dist/exec.js +166 -0
- package/dist/install.js +140 -0
- package/dist/library.js +200 -0
- package/dist/model.js +13 -0
- package/dist/partial.js +90 -0
- package/dist/peers.js +79 -0
- package/dist/progress.js +65 -0
- package/dist/registry.js +135 -0
- package/dist/report.js +280 -0
- package/dist/run.js +280 -0
- package/dist/scripts.js +73 -0
- package/dist/smoke.js +526 -0
- package/dist/tests.js +105 -0
- package/dist/tools.js +152 -0
- package/dist/versions.js +138 -0
- package/package.json +49 -4
package/dist/build.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { writeTsconfig } from "./app.js";
|
|
4
|
+
import { environmentProblem, errorLines, exec, quietEnv, stepTimeout } 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: stepTimeout(),
|
|
14
|
+
});
|
|
15
|
+
if (result.code === 0)
|
|
16
|
+
return { pass: true, errorLines: [], 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
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** Plain-language explanations for build failures whose messages don't say what's wrong. */
|
|
28
|
+
export function hintFor(output) {
|
|
29
|
+
if (/Unsupported enum value|Cannot create property 'message' on string/.test(output)) {
|
|
30
|
+
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.';
|
|
31
|
+
}
|
|
32
|
+
const linker = /depends upon a library published using Angular version ([\d.]+), which requires Angular version ([\d.]+) or newer/.exec(output);
|
|
33
|
+
if (linker) {
|
|
34
|
+
return `The package was compiled with Angular ${linker[1]} and needs Angular ${linker[2]} or newer. Build it with the oldest Angular you support.`;
|
|
35
|
+
}
|
|
36
|
+
const missing = /No matching export in "[^"]*node_modules[\\/](@angular[\\/][\w-]+)[^"]*" for import "([\w$ɵ]+)"/.exec(output);
|
|
37
|
+
if (missing) {
|
|
38
|
+
return `The package imports ${missing[2]} from ${missing[1].replace('\\', '/')}, which this Angular version doesn't have.`;
|
|
39
|
+
}
|
|
40
|
+
// TS2305 inside a .d.ts: `Module '"@angular/core"' has no exported member 'X'`, with the file on the next line.
|
|
41
|
+
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);
|
|
42
|
+
if (member) {
|
|
43
|
+
const pkg = member[3].replace('\\', '/');
|
|
44
|
+
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.`;
|
|
45
|
+
}
|
|
46
|
+
if (/has no exported member(?: named)? '([\w$ɵ]+)'/.test(output) && /\.d\.ts/.test(output)) {
|
|
47
|
+
return "The package's typings use a type this Angular doesn't export. Apps with skipLibCheck: false fail to compile.";
|
|
48
|
+
}
|
|
49
|
+
if (isDtsOnlyFailure(errorLines(output))) {
|
|
50
|
+
return 'library typings have errors with skipLibCheck: false';
|
|
51
|
+
}
|
|
52
|
+
if (/error NG8002|error NG8008|is not assignable to type|TS2322/.test(output)) {
|
|
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.';
|
|
54
|
+
}
|
|
55
|
+
return undefined;
|
|
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
|
+
}
|
package/dist/cleanup.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { killAllChildren } from "./exec.js";
|
|
5
|
+
/*
|
|
6
|
+
* Temporary apps and half-written cache entries must not outlive a run, however it ends:
|
|
7
|
+
* normally, with Ctrl+C, or killed outright. Each work folder records the pid that owns it,
|
|
8
|
+
* and each cache write goes through `<name>.<pid>.tmp`, so whatever a dead run left behind
|
|
9
|
+
* can be told apart from what a running one is still using.
|
|
10
|
+
*/
|
|
11
|
+
const PREFIX = 'ng-matrix-';
|
|
12
|
+
const OWNER = '.ng-matrix-pid';
|
|
13
|
+
export function createWorkDir() {
|
|
14
|
+
const dir = mkdtempSync(join(tmpdir(), PREFIX));
|
|
15
|
+
writeFileSync(join(dir, OWNER), String(process.pid));
|
|
16
|
+
return dir;
|
|
17
|
+
}
|
|
18
|
+
export function isAlive(pid) {
|
|
19
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
20
|
+
return false;
|
|
21
|
+
try {
|
|
22
|
+
process.kill(pid, 0);
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
return error.code === 'EPERM';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function removeDir(dir) {
|
|
30
|
+
// On Windows a just-killed build can hold files open for a moment.
|
|
31
|
+
rmSync(dir, { recursive: true, force: true, maxRetries: 10, retryDelay: 200 });
|
|
32
|
+
}
|
|
33
|
+
/** Removes work folders whose run is gone. Folders without an owner file (0.1.0) go after a day. */
|
|
34
|
+
export function sweepWorkDirs(base = tmpdir()) {
|
|
35
|
+
const removed = [];
|
|
36
|
+
let entries;
|
|
37
|
+
try {
|
|
38
|
+
entries = readdirSync(base).filter((name) => name.startsWith(PREFIX));
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return removed;
|
|
42
|
+
}
|
|
43
|
+
for (const name of entries) {
|
|
44
|
+
const dir = join(base, name);
|
|
45
|
+
try {
|
|
46
|
+
const owner = join(dir, OWNER);
|
|
47
|
+
const stale = existsSync(owner)
|
|
48
|
+
? !isAlive(Number(readFileSync(owner, 'utf8')))
|
|
49
|
+
: Date.now() - statSync(dir).mtimeMs > 24 * 60 * 60 * 1000;
|
|
50
|
+
if (stale) {
|
|
51
|
+
removeDir(dir);
|
|
52
|
+
removed.push(dir);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// Not ours to judge, or already gone.
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return removed;
|
|
60
|
+
}
|
|
61
|
+
/** Removes `<name>.<pid>.tmp` entries in the cache left by runs that are gone. */
|
|
62
|
+
export function sweepCache(cacheDir) {
|
|
63
|
+
const removed = [];
|
|
64
|
+
for (const sub of ['tools', 'registry']) {
|
|
65
|
+
const dir = join(cacheDir, sub);
|
|
66
|
+
let entries;
|
|
67
|
+
try {
|
|
68
|
+
entries = readdirSync(dir);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
for (const name of entries) {
|
|
74
|
+
const match = /\.(\d+)\.tmp$/.exec(name);
|
|
75
|
+
if (!match || isAlive(Number(match[1])))
|
|
76
|
+
continue;
|
|
77
|
+
try {
|
|
78
|
+
removeDir(join(dir, name));
|
|
79
|
+
removed.push(join(dir, name));
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// Try again next run.
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return removed;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* On Ctrl+C (or SIGTERM, or a closed terminal): stop every child, remove the work folder unless
|
|
90
|
+
* --keep, and exit. A second Ctrl+C exits at once.
|
|
91
|
+
*/
|
|
92
|
+
export function cleanUpOnInterrupt(workDir, keep, log) {
|
|
93
|
+
let stopping = false;
|
|
94
|
+
const handler = (signal) => {
|
|
95
|
+
if (stopping)
|
|
96
|
+
process.exit(130);
|
|
97
|
+
stopping = true;
|
|
98
|
+
log(`Interrupted (${signal}): stopping, then ${keep ? `keeping ${workDir} (--keep)` : `removing ${workDir}`}.`);
|
|
99
|
+
killAllChildren();
|
|
100
|
+
if (!keep) {
|
|
101
|
+
try {
|
|
102
|
+
removeDir(workDir);
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
log(`Couldn't remove ${workDir}: ${error.message}. The next run removes it.`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
process.exit(signal === 'SIGINT' || signal === 'SIGBREAK' ? 130 : 143);
|
|
109
|
+
};
|
|
110
|
+
const signals = process.platform === 'win32' ? ['SIGINT', 'SIGTERM', 'SIGBREAK'] : ['SIGINT', 'SIGTERM', 'SIGHUP'];
|
|
111
|
+
for (const signal of signals)
|
|
112
|
+
process.on(signal, handler);
|
|
113
|
+
return () => {
|
|
114
|
+
for (const signal of signals)
|
|
115
|
+
process.off(signal, handler);
|
|
116
|
+
};
|
|
117
|
+
}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { cleanUpOnInterrupt, createWorkDir, removeDir, sweepCache, sweepWorkDirs } from "./cleanup.js";
|
|
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 { SCHEMA_VERSION } from "./model.js";
|
|
10
|
+
import { angularImports, scanPackage, summarize } from "./partial.js";
|
|
11
|
+
import { Registry, RegistryError } from "./registry.js";
|
|
12
|
+
import { exitCode, findings, renderJson, renderMarkdown, renderTerminal } from "./report.js";
|
|
13
|
+
import { defaultConcurrency, runAll } from "./run.js";
|
|
14
|
+
import { setStepTimeout, setVerbose } from "./exec.js";
|
|
15
|
+
import { Progress } from "./progress.js";
|
|
16
|
+
import { ALLOW_SCRIPTS_WARNING } from "./scripts.js";
|
|
17
|
+
import { cacheStats, cleanCache, defaultCacheDir, formatBytes } from "./tools.js";
|
|
18
|
+
import { defaultMajors, describeMajor, loadVersionSources, majorsOf, nextVersion, nodeSatisfies, parseVersionList, } from "./versions.js";
|
|
19
|
+
const HELP = `ng-matrix: which Angular versions does your published library really work with?
|
|
20
|
+
|
|
21
|
+
It builds and packs your library, installs the tarball into a fresh app for each Angular
|
|
22
|
+
major (made with that major's own CLI), builds it, and reports a matrix.
|
|
23
|
+
|
|
24
|
+
Usage
|
|
25
|
+
npx angular-compat-matrix [options]
|
|
26
|
+
ng-matrix [options] (once installed)
|
|
27
|
+
ng-matrix cache (show cache location and size)
|
|
28
|
+
ng-matrix cache clean (delete the cache)
|
|
29
|
+
|
|
30
|
+
Options
|
|
31
|
+
--versions <list> Angular majors to check: 22, 18-22, 20,22. Default: the supported
|
|
32
|
+
majors, every major the peer range claims (from 17), and the newest
|
|
33
|
+
--next Also check whatever the \`next\` tag points to
|
|
34
|
+
--project <name> The library project, in a workspace with several
|
|
35
|
+
--consumer <dir> Build your own example app instead of a generated smoke app
|
|
36
|
+
--tests Also run the library's tests on each version (slow)
|
|
37
|
+
--test-command <cmd> The test command for --tests (default: <pm> test)
|
|
38
|
+
--format <format> terminal (default), markdown or json
|
|
39
|
+
--json Shorthand for --format json
|
|
40
|
+
--pm <npm|pnpm> Package manager for the apps (default: pnpm if there's a
|
|
41
|
+
pnpm-lock.yaml, else npm)
|
|
42
|
+
--build <cmd> Build the library with this command instead of the detected one
|
|
43
|
+
--skip-build Pack the existing build output as is
|
|
44
|
+
--tarball <file> Check this packed .tgz instead of building
|
|
45
|
+
--package <spec> Check a published package, e.g. some-lib@3.1.0
|
|
46
|
+
--concurrency <n> Versions checked at once (default: as many as free memory allows)
|
|
47
|
+
--step-timeout <min> Longest any one step (ng new, install, build, tests) may take (default: 20)
|
|
48
|
+
--keep Keep the temporary apps, for debugging
|
|
49
|
+
--allow-scripts Let the package's (and its dependencies') install scripts run. By
|
|
50
|
+
default only the Angular toolchain's own builds run (esbuild, lmdb...)
|
|
51
|
+
--offline Use only cached registry answers and packages
|
|
52
|
+
--cwd <dir> The library's workspace (default: the current folder)
|
|
53
|
+
--verbose Show commands run and full build/install output for failures
|
|
54
|
+
-h, --help This help
|
|
55
|
+
-v, --version ng-matrix's version
|
|
56
|
+
|
|
57
|
+
Exit code: 1 when a version inside the peer range fails, 2 when a version couldn't be
|
|
58
|
+
checked at all, 0 otherwise. A version outside the range that fails is a note, not an error.
|
|
59
|
+
`;
|
|
60
|
+
function toolVersion() {
|
|
61
|
+
const manifest = join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json');
|
|
62
|
+
return JSON.parse(readFileSync(manifest, 'utf8')).version;
|
|
63
|
+
}
|
|
64
|
+
/** A problem ng-matrix explains and stops on, with exit code 2. */
|
|
65
|
+
class Stop extends Error {
|
|
66
|
+
lines;
|
|
67
|
+
constructor(message, lines = []) {
|
|
68
|
+
super(message);
|
|
69
|
+
this.lines = lines;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function fail(message, lines = []) {
|
|
73
|
+
throw new Stop(message, lines);
|
|
74
|
+
}
|
|
75
|
+
function explain(error) {
|
|
76
|
+
if (error instanceof Stop || error instanceof LibraryError) {
|
|
77
|
+
process.stderr.write(`ng-matrix: ${error.message}\n${error.lines.map((l) => ` ${l}\n`).join('')}`);
|
|
78
|
+
}
|
|
79
|
+
else if (error instanceof RegistryError) {
|
|
80
|
+
process.stderr.write(`ng-matrix: ${error.message}\n`);
|
|
81
|
+
}
|
|
82
|
+
else if (String(error.code).startsWith('ERR_PARSE_ARGS')) {
|
|
83
|
+
process.stderr.write(`ng-matrix: ${error.message}. See --help.\n`);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
process.stderr.write(`ng-matrix: unexpected error\n${error.stack ?? String(error)}\n`);
|
|
87
|
+
}
|
|
88
|
+
return 2;
|
|
89
|
+
}
|
|
90
|
+
export async function main(argv) {
|
|
91
|
+
const { values, positionals } = parseArgs({
|
|
92
|
+
args: argv,
|
|
93
|
+
allowPositionals: true,
|
|
94
|
+
options: {
|
|
95
|
+
versions: { type: 'string' },
|
|
96
|
+
next: { type: 'boolean', default: false },
|
|
97
|
+
project: { type: 'string' },
|
|
98
|
+
consumer: { type: 'string' },
|
|
99
|
+
tests: { type: 'boolean', default: false },
|
|
100
|
+
'test-command': { type: 'string' },
|
|
101
|
+
'allow-scripts': { type: 'boolean', default: false },
|
|
102
|
+
format: { type: 'string', default: 'terminal' },
|
|
103
|
+
json: { type: 'boolean', default: false },
|
|
104
|
+
pm: { type: 'string' },
|
|
105
|
+
build: { type: 'string' },
|
|
106
|
+
'skip-build': { type: 'boolean', default: false },
|
|
107
|
+
tarball: { type: 'string' },
|
|
108
|
+
package: { type: 'string' },
|
|
109
|
+
concurrency: { type: 'string' },
|
|
110
|
+
'step-timeout': { type: 'string' },
|
|
111
|
+
keep: { type: 'boolean', default: false },
|
|
112
|
+
offline: { type: 'boolean', default: false },
|
|
113
|
+
cwd: { type: 'string' },
|
|
114
|
+
verbose: { type: 'boolean', default: false },
|
|
115
|
+
help: { type: 'boolean', short: 'h', default: false },
|
|
116
|
+
version: { type: 'boolean', short: 'v', default: false },
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
if (values.help) {
|
|
120
|
+
process.stdout.write(HELP);
|
|
121
|
+
return 0;
|
|
122
|
+
}
|
|
123
|
+
if (values.version) {
|
|
124
|
+
process.stdout.write(`${toolVersion()}\n`);
|
|
125
|
+
return 0;
|
|
126
|
+
}
|
|
127
|
+
if (positionals[0] === 'cache') {
|
|
128
|
+
const cacheDir = defaultCacheDir();
|
|
129
|
+
const sub = positionals[1];
|
|
130
|
+
if (!sub) {
|
|
131
|
+
const stats = cacheStats(cacheDir);
|
|
132
|
+
process.stdout.write(`Location: ${cacheDir}\nSize: ${formatBytes(stats.bytes)} (${stats.count} files)\n`);
|
|
133
|
+
return 0;
|
|
134
|
+
}
|
|
135
|
+
if (sub === 'clean') {
|
|
136
|
+
cleanCache(cacheDir);
|
|
137
|
+
process.stdout.write(`Cleaned cache at ${cacheDir}\n`);
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
fail(`Unknown cache command "ng-matrix cache ${sub}". See --help.`);
|
|
141
|
+
}
|
|
142
|
+
if (positionals[0] === 'ci')
|
|
143
|
+
fail('`ng-matrix ci github` is planned for v0.2.');
|
|
144
|
+
if (positionals.length)
|
|
145
|
+
fail(`Unknown argument "${positionals[0]}". See --help.`);
|
|
146
|
+
const format = values.json ? 'json' : values.format;
|
|
147
|
+
if (!['terminal', 'markdown', 'json'].includes(format))
|
|
148
|
+
fail(`--format must be terminal, markdown or json, not "${format}".`);
|
|
149
|
+
if (values.verbose)
|
|
150
|
+
setVerbose(true);
|
|
151
|
+
const cwd = resolve(values.cwd ?? process.cwd());
|
|
152
|
+
const pm = (values.pm ?? (existsSync(join(cwd, 'pnpm-lock.yaml')) ? 'pnpm' : 'npm'));
|
|
153
|
+
if (pm !== 'npm' && pm !== 'pnpm')
|
|
154
|
+
fail(`--pm must be npm or pnpm (yarn and bun come later), not "${pm}".`);
|
|
155
|
+
const consumer = values.consumer ? resolve(cwd, values.consumer) : undefined;
|
|
156
|
+
if (values['step-timeout'] !== undefined) {
|
|
157
|
+
const minutes = Number(values['step-timeout']);
|
|
158
|
+
if (!(minutes > 0))
|
|
159
|
+
fail(`--step-timeout must be a number of minutes above 0, not "${values['step-timeout']}".`);
|
|
160
|
+
setStepTimeout(minutes * 60_000);
|
|
161
|
+
}
|
|
162
|
+
const progress = new Progress(format !== 'json');
|
|
163
|
+
const log = (line) => progress.log(`[ng-matrix] ${line}`);
|
|
164
|
+
const cacheDir = defaultCacheDir();
|
|
165
|
+
const registry = new Registry({
|
|
166
|
+
cacheDir,
|
|
167
|
+
offline: values.offline,
|
|
168
|
+
recordedDir: process.env.NG_MATRIX_RECORDED_REGISTRY,
|
|
169
|
+
});
|
|
170
|
+
// Leftovers from runs that were killed before they could clean up.
|
|
171
|
+
sweepWorkDirs();
|
|
172
|
+
sweepCache(cacheDir);
|
|
173
|
+
const workDir = createWorkDir();
|
|
174
|
+
const stopListening = cleanUpOnInterrupt(workDir, values.keep, log);
|
|
175
|
+
if (values.keep)
|
|
176
|
+
log(`Working in ${workDir} (--keep)`);
|
|
177
|
+
if (values['allow-scripts'])
|
|
178
|
+
log(ALLOW_SCRIPTS_WARNING);
|
|
179
|
+
try {
|
|
180
|
+
const library = await prepareLibrary({
|
|
181
|
+
cwd,
|
|
182
|
+
project: values.project,
|
|
183
|
+
build: values.build,
|
|
184
|
+
skipBuild: values['skip-build'],
|
|
185
|
+
tarball: values.tarball,
|
|
186
|
+
packageSpec: values.package,
|
|
187
|
+
}, workDir, log);
|
|
188
|
+
const summary = summarize(scanPackage(library.dir));
|
|
189
|
+
const now = new Date();
|
|
190
|
+
const sources = await loadVersionSources(registry);
|
|
191
|
+
const majors = values.versions
|
|
192
|
+
? parseVersionList(values.versions)
|
|
193
|
+
: defaultMajors(sources.core, library.manifest.peerDependencies?.['@angular/core'], now);
|
|
194
|
+
const published = new Set(majorsOf(sources.core));
|
|
195
|
+
for (const major of majors)
|
|
196
|
+
if (!published.has(major))
|
|
197
|
+
fail(`Angular ${major} isn't published on npm.`);
|
|
198
|
+
const angular = majors.map((major) => describeMajor(sources, major, now));
|
|
199
|
+
const next = values.next ? nextVersion(sources.core) : undefined;
|
|
200
|
+
if (values.next && !next)
|
|
201
|
+
log('The `next` tag is not ahead of `latest` right now; nothing extra to check.');
|
|
202
|
+
if (next)
|
|
203
|
+
angular.push(describeMajor(sources, semver.major(next), now, next));
|
|
204
|
+
// One Node for every version: Angular's engines ranges only set minimums.
|
|
205
|
+
const tooOld = angular.filter((a) => !nodeSatisfies(process.version, a.nodeRanges));
|
|
206
|
+
if (tooOld.length) {
|
|
207
|
+
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.'));
|
|
208
|
+
}
|
|
209
|
+
const concurrency = values.concurrency ? Math.max(1, Number(values.concurrency)) : defaultConcurrency(angular.length);
|
|
210
|
+
log(`Checking ${library.manifest.name}@${library.manifest.version} on ${angular.map((a) => a.version).join(', ')} with ${pm}, ${concurrency} at a time`);
|
|
211
|
+
const versions = await runAll({
|
|
212
|
+
library,
|
|
213
|
+
summary,
|
|
214
|
+
pm,
|
|
215
|
+
registry,
|
|
216
|
+
cacheDir,
|
|
217
|
+
offline: values.offline,
|
|
218
|
+
workDir,
|
|
219
|
+
workspace: values.tarball || values.package ? undefined : cwd,
|
|
220
|
+
tests: values.tests,
|
|
221
|
+
testCommand: values['test-command'],
|
|
222
|
+
consumer,
|
|
223
|
+
keep: values.keep,
|
|
224
|
+
allowScripts: values['allow-scripts'],
|
|
225
|
+
log,
|
|
226
|
+
progress,
|
|
227
|
+
}, angular, concurrency);
|
|
228
|
+
const report = {
|
|
229
|
+
schemaVersion: SCHEMA_VERSION,
|
|
230
|
+
tool: { name: 'angular-compat-matrix', version: toolVersion() },
|
|
231
|
+
checkedAt: now.toISOString(),
|
|
232
|
+
library: {
|
|
233
|
+
name: library.manifest.name,
|
|
234
|
+
version: library.manifest.version,
|
|
235
|
+
source: library.source,
|
|
236
|
+
compiledWith: summary.compiledWith,
|
|
237
|
+
highestMinVersion: summary.highest?.minVersion,
|
|
238
|
+
highestMinVersionIn: summary.highest ? `${summary.highest.type} (${summary.highest.file})` : undefined,
|
|
239
|
+
declarations: summary.declarations,
|
|
240
|
+
},
|
|
241
|
+
packageManager: pm,
|
|
242
|
+
installScripts: values['allow-scripts'] ? 'all' : 'toolchain-only',
|
|
243
|
+
node: process.version,
|
|
244
|
+
versions,
|
|
245
|
+
findings: findings(versions, library.manifest, angularImports(library.dir)),
|
|
246
|
+
exitCode: exitCode(versions),
|
|
247
|
+
};
|
|
248
|
+
const render = format === 'json' ? (r) => renderJson(r, values.verbose) : format === 'markdown' ? renderMarkdown : (r) => renderTerminal(r, values.verbose);
|
|
249
|
+
process.stdout.write((format === 'terminal' ? '\n' : '') + render(report));
|
|
250
|
+
return report.exitCode;
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
return explain(error);
|
|
254
|
+
}
|
|
255
|
+
finally {
|
|
256
|
+
progress.stop();
|
|
257
|
+
stopListening();
|
|
258
|
+
if (!values.keep)
|
|
259
|
+
removeDir(workDir);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
main(process.argv.slice(2)).then((code) => process.exit(code), (error) => process.exit(explain(error)));
|