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/peers.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import semver from 'semver';
|
|
2
|
+
import { newestSatisfying } from "./versions.js";
|
|
3
|
+
/** Packages released in lockstep with `@angular/core`: same version number, always. */
|
|
4
|
+
export const FRAMEWORK = new Set([
|
|
5
|
+
'@angular/animations',
|
|
6
|
+
'@angular/common',
|
|
7
|
+
'@angular/compiler',
|
|
8
|
+
'@angular/compiler-cli',
|
|
9
|
+
'@angular/core',
|
|
10
|
+
'@angular/elements',
|
|
11
|
+
'@angular/forms',
|
|
12
|
+
'@angular/language-service',
|
|
13
|
+
'@angular/localize',
|
|
14
|
+
'@angular/platform-browser',
|
|
15
|
+
'@angular/platform-browser-dynamic',
|
|
16
|
+
'@angular/platform-server',
|
|
17
|
+
'@angular/router',
|
|
18
|
+
'@angular/service-worker',
|
|
19
|
+
'@angular/upgrade',
|
|
20
|
+
]);
|
|
21
|
+
/** Peers a generated app already has at the right version, so they're never installed again. */
|
|
22
|
+
const APP_PROVIDED = new Set(['rxjs', 'tslib', 'zone.js', 'typescript']);
|
|
23
|
+
function isOptional(manifest, name) {
|
|
24
|
+
return Boolean(manifest.peerDependenciesMeta?.[name]?.optional);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The newest version of a non-framework peer (Angular Material, the CDK, a third-party package)
|
|
28
|
+
* that a real app on this Angular would use: one whose own `@angular/*` peers accept it,
|
|
29
|
+
* preferring versions the library's range allows.
|
|
30
|
+
*/
|
|
31
|
+
export function pickPeerVersion(packument, libraryRange, angularVersion) {
|
|
32
|
+
const pre = Boolean(semver.prerelease(angularVersion));
|
|
33
|
+
const candidates = Object.keys(packument.versions)
|
|
34
|
+
.filter((v) => semver.valid(v) && (pre || !semver.prerelease(v)))
|
|
35
|
+
.sort(semver.rcompare);
|
|
36
|
+
const acceptsAngular = (v) => {
|
|
37
|
+
const peers = packument.versions[v].peerDependencies ?? {};
|
|
38
|
+
const framework = Object.entries(peers).filter(([name]) => FRAMEWORK.has(name));
|
|
39
|
+
return framework.every(([, range]) => semver.satisfies(angularVersion, range, { includePrerelease: true }));
|
|
40
|
+
};
|
|
41
|
+
const inRange = (v) => semver.satisfies(v, libraryRange, { includePrerelease: true });
|
|
42
|
+
const hasFrameworkPeer = (v) => Object.keys(packument.versions[v].peerDependencies ?? {}).some((name) => FRAMEWORK.has(name));
|
|
43
|
+
// Packages that don't depend on Angular (say, a date library) just follow the library's range.
|
|
44
|
+
if (!candidates.some(hasFrameworkPeer))
|
|
45
|
+
return candidates.find(inRange) ?? newestSatisfying(packument, '*');
|
|
46
|
+
return (candidates.find((v) => acceptsAngular(v) && inRange(v)) ??
|
|
47
|
+
candidates.find(acceptsAngular) ??
|
|
48
|
+
candidates.find(inRange));
|
|
49
|
+
}
|
|
50
|
+
export async function planPeers(manifest, angularVersion, registry) {
|
|
51
|
+
const peers = manifest.peerDependencies ?? {};
|
|
52
|
+
const installs = {};
|
|
53
|
+
const checked = {};
|
|
54
|
+
const misses = [];
|
|
55
|
+
let angularPeers = 0;
|
|
56
|
+
for (const [name, range] of Object.entries(peers)) {
|
|
57
|
+
let version;
|
|
58
|
+
if (FRAMEWORK.has(name)) {
|
|
59
|
+
version = angularVersion;
|
|
60
|
+
installs[name] = angularVersion;
|
|
61
|
+
}
|
|
62
|
+
else if (!APP_PROVIDED.has(name)) {
|
|
63
|
+
version = pickPeerVersion(await registry.get(name), range, angularVersion);
|
|
64
|
+
if (version)
|
|
65
|
+
installs[name] = version;
|
|
66
|
+
}
|
|
67
|
+
// Optional peers are installed (the smoke app imports every entry point, and some entry point
|
|
68
|
+
// needs them, as angular-calendar's date-fns adapter needs date-fns) but claim nothing.
|
|
69
|
+
if (!name.startsWith('@angular/') || isOptional(manifest, name))
|
|
70
|
+
continue;
|
|
71
|
+
angularPeers++;
|
|
72
|
+
const against = version ?? 'none';
|
|
73
|
+
checked[name] = against;
|
|
74
|
+
if (!version || !semver.satisfies(version, range, { includePrerelease: true })) {
|
|
75
|
+
misses.push({ name, range, version: against });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return { claimed: angularPeers ? misses.length === 0 : null, misses, installs, checked };
|
|
79
|
+
}
|
package/dist/progress.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export class Progress {
|
|
2
|
+
active = new Map();
|
|
3
|
+
timer;
|
|
4
|
+
isTTY;
|
|
5
|
+
rendered = false;
|
|
6
|
+
constructor(enabled) {
|
|
7
|
+
this.isTTY = enabled && Boolean(process.stderr.isTTY);
|
|
8
|
+
if (this.isTTY) {
|
|
9
|
+
this.timer = setInterval(() => this.render(), 1000);
|
|
10
|
+
this.timer.unref();
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
step(major, step) {
|
|
14
|
+
this.active.set(major, { step, started: Date.now() });
|
|
15
|
+
if (this.isTTY)
|
|
16
|
+
this.render();
|
|
17
|
+
}
|
|
18
|
+
done(major) {
|
|
19
|
+
this.active.delete(major);
|
|
20
|
+
if (this.isTTY)
|
|
21
|
+
this.render();
|
|
22
|
+
}
|
|
23
|
+
log(line) {
|
|
24
|
+
if (this.isTTY) {
|
|
25
|
+
this.clear();
|
|
26
|
+
process.stderr.write(`${line}\n`);
|
|
27
|
+
this.render();
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
process.stderr.write(`${line}\n`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
render() {
|
|
34
|
+
if (!this.isTTY)
|
|
35
|
+
return;
|
|
36
|
+
if (this.active.size === 0) {
|
|
37
|
+
this.clear();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const parts = [];
|
|
41
|
+
const now = Date.now();
|
|
42
|
+
for (const [major, item] of this.active) {
|
|
43
|
+
const elapsed = Math.floor((now - item.started) / 1000);
|
|
44
|
+
parts.push(`Angular ${major}: ${item.step} (${elapsed}s)`);
|
|
45
|
+
}
|
|
46
|
+
const text = `[ng-matrix] ${parts.join(' | ')}`;
|
|
47
|
+
process.stderr.write(`\r\x1b[K${text}`);
|
|
48
|
+
this.rendered = true;
|
|
49
|
+
}
|
|
50
|
+
clear() {
|
|
51
|
+
if (this.rendered) {
|
|
52
|
+
process.stderr.write('\r\x1b[K');
|
|
53
|
+
this.rendered = false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
stop() {
|
|
57
|
+
if (this.timer) {
|
|
58
|
+
clearInterval(this.timer);
|
|
59
|
+
this.timer = undefined;
|
|
60
|
+
}
|
|
61
|
+
if (this.isTTY) {
|
|
62
|
+
this.clear();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
package/dist/registry.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
const DAY = 24 * 60 * 60 * 1000;
|
|
4
|
+
const FULL_DOCUMENT = new Set(['@angular/core']);
|
|
5
|
+
export class Registry {
|
|
6
|
+
memory = new Map();
|
|
7
|
+
options;
|
|
8
|
+
constructor(options) {
|
|
9
|
+
this.options = options;
|
|
10
|
+
}
|
|
11
|
+
get(name) {
|
|
12
|
+
let pending = this.memory.get(name);
|
|
13
|
+
if (!pending) {
|
|
14
|
+
pending = this.load(name);
|
|
15
|
+
this.memory.set(name, pending);
|
|
16
|
+
}
|
|
17
|
+
return pending;
|
|
18
|
+
}
|
|
19
|
+
async load(name) {
|
|
20
|
+
const file = `${encodeURIComponent(name)}.json`;
|
|
21
|
+
if (this.options.recordedDir) {
|
|
22
|
+
const path = join(this.options.recordedDir, file);
|
|
23
|
+
if (!existsSync(path))
|
|
24
|
+
throw new Error(`No recorded registry answer for ${name} (${path})`);
|
|
25
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
26
|
+
}
|
|
27
|
+
const dir = join(this.options.cacheDir, 'registry');
|
|
28
|
+
const path = join(dir, file);
|
|
29
|
+
const cached = readCached(path);
|
|
30
|
+
const maxAge = this.options.maxAgeMs ?? DAY;
|
|
31
|
+
if (cached && (this.options.offline || Date.now() - cached.fetchedAt < maxAge))
|
|
32
|
+
return cached.packument;
|
|
33
|
+
if (this.options.offline)
|
|
34
|
+
throw new Error(`--offline: ${name} isn't in the registry cache yet. Run once online first.`);
|
|
35
|
+
const base = (this.options.registryUrl ?? process.env.npm_config_registry ?? 'https://registry.npmjs.org/').replace(/\/?$/, '/');
|
|
36
|
+
const url = base + name.replace('/', '%2f');
|
|
37
|
+
// Only @angular/core's publish dates are needed (to tell which majors are supported); everything
|
|
38
|
+
// else comes from the abbreviated document, which is much smaller for packages with many versions.
|
|
39
|
+
const accept = FULL_DOCUMENT.has(name)
|
|
40
|
+
? 'application/json'
|
|
41
|
+
: 'application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8';
|
|
42
|
+
const timeoutMs = this.options.timeoutMs ?? 30_000;
|
|
43
|
+
let packument;
|
|
44
|
+
let problem = '';
|
|
45
|
+
// Two tries: one dropped connection shouldn't end a run that takes minutes.
|
|
46
|
+
for (let attempt = 1; attempt <= 2 && !packument; attempt++) {
|
|
47
|
+
try {
|
|
48
|
+
const response = await fetch(url, { headers: { accept }, signal: AbortSignal.timeout(timeoutMs) });
|
|
49
|
+
if (response.status === 404)
|
|
50
|
+
throw new RegistryError(`${name} isn't on the npm registry (${base}).`);
|
|
51
|
+
if (!response.ok) {
|
|
52
|
+
problem = `it answered ${response.status} ${response.statusText}`;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
packument = trim((await response.json()));
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
if (error instanceof RegistryError)
|
|
59
|
+
throw error;
|
|
60
|
+
problem = describeFetchError(error, timeoutMs);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (!packument) {
|
|
64
|
+
if (cached)
|
|
65
|
+
return cached.packument;
|
|
66
|
+
throw new RegistryError(`Couldn't get ${name} from the npm registry (${base}): ${problem}.`);
|
|
67
|
+
}
|
|
68
|
+
writeCached(dir, path, packument);
|
|
69
|
+
return packument;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export class RegistryError extends Error {
|
|
73
|
+
}
|
|
74
|
+
function describeFetchError(error, timeoutMs) {
|
|
75
|
+
if (error.name === 'TimeoutError' || error.name === 'AbortError')
|
|
76
|
+
return `no answer within ${timeoutMs / 1000} s`;
|
|
77
|
+
const cause = error.cause;
|
|
78
|
+
const code = cause?.code;
|
|
79
|
+
if (code === 'ENOTFOUND' || code === 'EAI_AGAIN')
|
|
80
|
+
return 'the host name could not be resolved (are you offline?)';
|
|
81
|
+
if (code === 'ECONNREFUSED')
|
|
82
|
+
return 'the connection was refused';
|
|
83
|
+
if (code === 'ECONNRESET')
|
|
84
|
+
return 'the connection was reset';
|
|
85
|
+
return cause?.message ?? error.message;
|
|
86
|
+
}
|
|
87
|
+
/** A cache entry, or nothing when it's missing or unreadable (a crashed write, a full disk): it's refetched. */
|
|
88
|
+
function readCached(path) {
|
|
89
|
+
if (!existsSync(path))
|
|
90
|
+
return undefined;
|
|
91
|
+
try {
|
|
92
|
+
const entry = JSON.parse(readFileSync(path, 'utf8'));
|
|
93
|
+
if (typeof entry.fetchedAt === 'number' && entry.packument && typeof entry.packument.versions === 'object') {
|
|
94
|
+
return entry;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// fall through
|
|
99
|
+
}
|
|
100
|
+
rmSync(path, { force: true });
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
/** Writes through a temporary file, so a crash or a full disk never leaves a half-written entry. */
|
|
104
|
+
function writeCached(dir, path, packument) {
|
|
105
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
106
|
+
try {
|
|
107
|
+
mkdirSync(dir, { recursive: true });
|
|
108
|
+
writeFileSync(tmp, JSON.stringify({ fetchedAt: Date.now(), packument }));
|
|
109
|
+
renameSync(tmp, path);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
// Caching is an optimisation; the run goes on without it.
|
|
113
|
+
rmSync(tmp, { force: true });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
export function trim(raw) {
|
|
117
|
+
const versions = {};
|
|
118
|
+
for (const [version, meta] of Object.entries(raw.versions ?? {})) {
|
|
119
|
+
const kept = { version };
|
|
120
|
+
if (meta.peerDependencies)
|
|
121
|
+
kept.peerDependencies = meta.peerDependencies;
|
|
122
|
+
if (meta.peerDependenciesMeta)
|
|
123
|
+
kept.peerDependenciesMeta = meta.peerDependenciesMeta;
|
|
124
|
+
if (meta.dependencies)
|
|
125
|
+
kept.dependencies = meta.dependencies;
|
|
126
|
+
if (meta.engines && typeof meta.engines === 'object')
|
|
127
|
+
kept.engines = meta.engines;
|
|
128
|
+
versions[version] = kept;
|
|
129
|
+
}
|
|
130
|
+
const time = {};
|
|
131
|
+
for (const version of Object.keys(versions))
|
|
132
|
+
if (raw.time?.[version])
|
|
133
|
+
time[version] = raw.time[version];
|
|
134
|
+
return { name: raw.name, 'dist-tags': raw['dist-tags'] ?? {}, versions, time };
|
|
135
|
+
}
|
package/dist/report.js
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { CHECK_LABELS, CHECKS } from "./model.js";
|
|
2
|
+
const FAILING_CHECKS = ['install', 'partial', 'aot', 'strict', 'tests'];
|
|
3
|
+
function firstFailure(version) {
|
|
4
|
+
for (const name of FAILING_CHECKS) {
|
|
5
|
+
if (version.checks[name].status === 'fail')
|
|
6
|
+
return [name, version.checks[name]];
|
|
7
|
+
}
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
/** Everything that ran, passed. */
|
|
11
|
+
function works(version) {
|
|
12
|
+
return !version.setupError && !firstFailure(version);
|
|
13
|
+
}
|
|
14
|
+
/** CI goes red only for broken promises (2 means ng-matrix itself couldn't check a version). */
|
|
15
|
+
export function exitCode(versions) {
|
|
16
|
+
if (versions.some((v) => v.setupError))
|
|
17
|
+
return 2;
|
|
18
|
+
return versions.some((v) => v.claimed === true && firstFailure(v)) ? 1 : 0;
|
|
19
|
+
}
|
|
20
|
+
export function findings(versions, manifest,
|
|
21
|
+
/** The `@angular/*` packages the published code imports. */
|
|
22
|
+
angularImports = ['@angular/core']) {
|
|
23
|
+
const raw = [];
|
|
24
|
+
for (const version of versions) {
|
|
25
|
+
const m = version.major;
|
|
26
|
+
if (version.setupError) {
|
|
27
|
+
raw.push({ level: 'error', majors: [m], key: `setup:${version.setupError}`, text: `ng-matrix couldn't set up the app: ${version.setupError}` });
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const failure = firstFailure(version);
|
|
31
|
+
if (failure) {
|
|
32
|
+
const [name, check] = failure;
|
|
33
|
+
// Partial's message names the declaration; a build's message is just "failed", so its hint says more.
|
|
34
|
+
const why = name === 'partial' && check.hint ? `${check.message} ${check.hint}` : (check.hint ?? check.errorLines?.[0] ?? check.message);
|
|
35
|
+
if (version.claimed === true) {
|
|
36
|
+
raw.push({ level: 'error', majors: [m], key: `fail:${name}:${why}`, text: `${CHECK_LABELS[name]}: ${why}` });
|
|
37
|
+
}
|
|
38
|
+
else if (version.claimed === false) {
|
|
39
|
+
raw.push({
|
|
40
|
+
level: 'note',
|
|
41
|
+
majors: [m],
|
|
42
|
+
key: `outside:${name}`,
|
|
43
|
+
text: `${CHECK_LABELS[name]} fails, but the peer range doesn't claim this version, so that's expected.`,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
raw.push({ level: 'warning', majors: [m], key: `fail:${name}:${why}`, text: `${CHECK_LABELS[name]}: ${why}` });
|
|
48
|
+
}
|
|
49
|
+
if (version.skippedInstallScripts?.length) {
|
|
50
|
+
const list = version.skippedInstallScripts.join(', ');
|
|
51
|
+
raw.push({
|
|
52
|
+
level: 'note',
|
|
53
|
+
majors: [m],
|
|
54
|
+
key: `scripts:${list}`,
|
|
55
|
+
text: `Install scripts didn't run for ${list} (the default). If the failure comes from one of them, check again with --allow-scripts.`,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
else if (version.claimed === false) {
|
|
60
|
+
const ranges = version.peerMisses.map((miss) => `${miss.name} "${miss.range}"`).join(', ');
|
|
61
|
+
raw.push({
|
|
62
|
+
level: 'warning',
|
|
63
|
+
majors: [m],
|
|
64
|
+
key: `unclaimed:${ranges}`,
|
|
65
|
+
text: `works, but peerDependencies doesn't allow it (${ranges}). Widen the range to include it.`,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
if (version.omittedEntryPoints?.length) {
|
|
69
|
+
for (const item of version.omittedEntryPoints) {
|
|
70
|
+
raw.push({
|
|
71
|
+
level: 'warning',
|
|
72
|
+
majors: [m],
|
|
73
|
+
key: `undeclared:${item.entry}:${item.missingPackage}`,
|
|
74
|
+
text: `entry point ${item.entry} imports ${item.missingPackage}, which isn't declared in dependencies or peers`,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (version.dtsErrors) {
|
|
79
|
+
raw.push({
|
|
80
|
+
level: 'warning',
|
|
81
|
+
majors: [m],
|
|
82
|
+
key: 'skipLibCheck:dts-errors',
|
|
83
|
+
text: 'library typings have errors with skipLibCheck: false',
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// One finding per distinct problem, listing every major it applies to.
|
|
88
|
+
const merged = [];
|
|
89
|
+
const byKey = new Map();
|
|
90
|
+
for (const finding of raw) {
|
|
91
|
+
const existing = byKey.get(finding.key);
|
|
92
|
+
if (existing)
|
|
93
|
+
existing.majors.push(...finding.majors);
|
|
94
|
+
else {
|
|
95
|
+
const copy = { level: finding.level, majors: [...finding.majors], text: finding.text };
|
|
96
|
+
byKey.set(finding.key, copy);
|
|
97
|
+
merged.push(copy);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const hasAngularPeer = Object.keys(manifest.peerDependencies ?? {}).some((n) => n.startsWith('@angular/'));
|
|
101
|
+
if (!hasAngularPeer)
|
|
102
|
+
merged.push(missingPeersFinding(versions, angularImports));
|
|
103
|
+
const order = { error: 0, warning: 1, note: 2 };
|
|
104
|
+
return merged.sort((a, b) => order[a.level] - order[b.level]);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* A package without `@angular/*` peers claims nothing, so nothing tells a package manager which
|
|
108
|
+
* Angular it needs. The hint suggests a range from the majors that passed every check, for each
|
|
109
|
+
* Angular package the published code imports.
|
|
110
|
+
*/
|
|
111
|
+
function missingPeersFinding(versions, angularImports) {
|
|
112
|
+
const working = versions.filter((v) => works(v) && !v.prerelease).map((v) => v.major);
|
|
113
|
+
const text = "The package declares no @angular/* peer dependency, so it claims no version and package managers can't warn about a mismatch.";
|
|
114
|
+
if (!working.length)
|
|
115
|
+
return { level: 'warning', majors: [], text: `${text} No version passed in this run, so there's no range to suggest.` };
|
|
116
|
+
const range = [...new Set(working)].sort((a, b) => a - b).map((m) => `^${m}.0.0`).join(' || ');
|
|
117
|
+
const packages = [...new Set(['@angular/core', ...angularImports])].sort();
|
|
118
|
+
const suggested = Object.fromEntries(packages.map((name) => [name, range]));
|
|
119
|
+
const lines = packages.map((name) => `"${name}": "${range}"`).join(', ');
|
|
120
|
+
return {
|
|
121
|
+
level: 'warning',
|
|
122
|
+
majors: [],
|
|
123
|
+
text: `${text} From the versions that passed: ${lines}.`,
|
|
124
|
+
suggestedPeerDependencies: suggested,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
export function majorsText(majors) {
|
|
128
|
+
const sorted = [...new Set(majors)].sort((a, b) => a - b);
|
|
129
|
+
const runs = [];
|
|
130
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
131
|
+
let j = i;
|
|
132
|
+
while (j + 1 < sorted.length && sorted[j + 1] === sorted[j] + 1)
|
|
133
|
+
j++;
|
|
134
|
+
runs.push(j > i ? `${sorted[i]}–${sorted[j]}` : String(sorted[i]));
|
|
135
|
+
i = j;
|
|
136
|
+
}
|
|
137
|
+
return runs.join(', ');
|
|
138
|
+
}
|
|
139
|
+
// ---------------------------------------------------------------- terminal
|
|
140
|
+
const SYMBOL = { pass: '✓', fail: '✗', 'not-run': '—', off: '', none: '·' };
|
|
141
|
+
function symbol(check) {
|
|
142
|
+
if (check.status === 'pass' && check.advisory)
|
|
143
|
+
return '✓?';
|
|
144
|
+
return SYMBOL[check.status];
|
|
145
|
+
}
|
|
146
|
+
function columns(report) {
|
|
147
|
+
return CHECKS.filter((c) => c !== 'tests' || report.versions.some((v) => v.checks.tests.status !== 'off'));
|
|
148
|
+
}
|
|
149
|
+
function rowLabel(version) {
|
|
150
|
+
return version.prerelease ? `Angular ${version.angularVersion}` : `Angular ${version.major}`;
|
|
151
|
+
}
|
|
152
|
+
function center(text, width) {
|
|
153
|
+
const length = [...text].length;
|
|
154
|
+
const left = Math.floor((width - length) / 2);
|
|
155
|
+
return ' '.repeat(Math.max(0, left)) + text + ' '.repeat(Math.max(0, width - length - left));
|
|
156
|
+
}
|
|
157
|
+
export function headline(report) {
|
|
158
|
+
const parts = [];
|
|
159
|
+
if (report.library.compiledWith)
|
|
160
|
+
parts.push(`compiled with Angular ${report.library.compiledWith}`);
|
|
161
|
+
if (report.library.highestMinVersion)
|
|
162
|
+
parts.push(`highest minVersion ${report.library.highestMinVersion}`);
|
|
163
|
+
return `${report.library.name}@${report.library.version}${parts.length ? ` (${parts.join('; ')})` : ''}`;
|
|
164
|
+
}
|
|
165
|
+
export function renderTerminal(report, verbose = false) {
|
|
166
|
+
const cols = columns(report);
|
|
167
|
+
const labelWidth = Math.max(...report.versions.map((v) => rowLabel(v).length), 10) + 2;
|
|
168
|
+
const widths = cols.map((c) => CHECK_LABELS[c].length + 2);
|
|
169
|
+
const lines = [headline(report), ''];
|
|
170
|
+
lines.push(' '.repeat(labelWidth) + cols.map((c, i) => center(CHECK_LABELS[c], widths[i])).join(''));
|
|
171
|
+
for (const version of report.versions) {
|
|
172
|
+
const cells = version.setupError
|
|
173
|
+
? cols.map((c, i) => center(c === 'peer' ? symbol(version.checks.peer) : '!', widths[i]))
|
|
174
|
+
: cols.map((c, i) => center(symbol(version.checks[c]), widths[i]));
|
|
175
|
+
lines.push(rowLabel(version).padEnd(labelWidth) + cells.join('').trimEnd());
|
|
176
|
+
}
|
|
177
|
+
if (report.versions.some((v) => v.checks.partial.advisory)) {
|
|
178
|
+
lines.push('', '✓? partial passed on minVersion alone, which has been too low before; the build decides.');
|
|
179
|
+
}
|
|
180
|
+
if (report.findings.length)
|
|
181
|
+
lines.push('');
|
|
182
|
+
for (const finding of report.findings) {
|
|
183
|
+
const mark = finding.level === 'error' ? '✗' : finding.level === 'warning' ? '!' : '·';
|
|
184
|
+
const where = finding.majors.length ? `${majorsText(finding.majors)}: ` : '';
|
|
185
|
+
lines.push(wrap(`${mark} ${where}${finding.text}`, 88, ' '));
|
|
186
|
+
}
|
|
187
|
+
const failing = report.versions.filter((v) => firstFailure(v) || v.setupError);
|
|
188
|
+
for (const version of failing) {
|
|
189
|
+
const failure = firstFailure(version);
|
|
190
|
+
const errors = version.setupError ? version.setupError.split('\n').slice(1) : (failure?.[1].errorLines ?? []);
|
|
191
|
+
const full = failure?.[1].fullOutput;
|
|
192
|
+
if (!errors.length && !full)
|
|
193
|
+
continue;
|
|
194
|
+
lines.push('', `${rowLabel(version)}, ${failure ? CHECK_LABELS[failure[0]] : 'setup'}:`);
|
|
195
|
+
if (verbose && full) {
|
|
196
|
+
for (const line of full.split(/\r?\n/))
|
|
197
|
+
lines.push(` ${line}`);
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
for (const line of errors.slice(0, 6))
|
|
201
|
+
lines.push(` ${line.trim()}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
lines.push('', exitSummary(report));
|
|
205
|
+
return lines.join('\n') + '\n';
|
|
206
|
+
}
|
|
207
|
+
function exitSummary(report) {
|
|
208
|
+
if (report.exitCode === 2)
|
|
209
|
+
return "Couldn't check every version; see above.";
|
|
210
|
+
if (report.exitCode === 1)
|
|
211
|
+
return 'A version the peer range claims is broken.';
|
|
212
|
+
return 'Every version the peer range claims works.';
|
|
213
|
+
}
|
|
214
|
+
function wrap(text, width, indent) {
|
|
215
|
+
const words = text.split(' ');
|
|
216
|
+
const lines = [];
|
|
217
|
+
let current = '';
|
|
218
|
+
for (const word of words) {
|
|
219
|
+
if (current && (current + ' ' + word).length > width) {
|
|
220
|
+
lines.push(current);
|
|
221
|
+
current = indent + word;
|
|
222
|
+
}
|
|
223
|
+
else
|
|
224
|
+
current = current ? `${current} ${word}` : word;
|
|
225
|
+
}
|
|
226
|
+
if (current)
|
|
227
|
+
lines.push(current);
|
|
228
|
+
return lines.join('\n');
|
|
229
|
+
}
|
|
230
|
+
// ---------------------------------------------------------------- markdown
|
|
231
|
+
const MD = { pass: '✅', fail: '❌', 'not-run': '—', off: '', none: 'n/a' };
|
|
232
|
+
export function renderMarkdown(report) {
|
|
233
|
+
const cols = columns(report);
|
|
234
|
+
const date = report.checkedAt.slice(0, 10);
|
|
235
|
+
const compiled = report.library.compiledWith ? `, compiled with Angular ${report.library.compiledWith}` : '';
|
|
236
|
+
const lines = [
|
|
237
|
+
'## Angular compatibility',
|
|
238
|
+
'',
|
|
239
|
+
`Checked on ${date} for \`${report.library.name}@${report.library.version}\`${compiled}, by installing the packed package into a fresh app of each version with ${report.packageManager} and building it ([angular-compat-matrix](https://github.com/berserker5619/angular-compat-matrix)).`,
|
|
240
|
+
'',
|
|
241
|
+
`| Angular | ${cols.map((c) => CHECK_LABELS[c][0].toUpperCase() + CHECK_LABELS[c].slice(1)).join(' | ')} |`,
|
|
242
|
+
`| :-- | ${cols.map(() => ':-:').join(' | ')} |`,
|
|
243
|
+
];
|
|
244
|
+
for (const version of report.versions) {
|
|
245
|
+
const cells = cols.map((c) => {
|
|
246
|
+
if (version.setupError && c !== 'peer')
|
|
247
|
+
return '⚠️';
|
|
248
|
+
const check = version.checks[c];
|
|
249
|
+
return check.status === 'pass' && check.advisory ? '✅*' : MD[check.status];
|
|
250
|
+
});
|
|
251
|
+
lines.push(`| ${version.prerelease ? version.angularVersion : `${version.major} (${version.angularVersion})`} | ${cells.join(' | ')} |`);
|
|
252
|
+
}
|
|
253
|
+
if (report.findings.length)
|
|
254
|
+
lines.push('');
|
|
255
|
+
for (const finding of report.findings) {
|
|
256
|
+
const mark = finding.level === 'error' ? '❌' : finding.level === 'warning' ? '⚠️' : 'ℹ️';
|
|
257
|
+
const where = finding.majors.length ? `**${majorsText(finding.majors)}:** ` : '';
|
|
258
|
+
lines.push(`- ${mark} ${where}${finding.text}`);
|
|
259
|
+
}
|
|
260
|
+
if (report.versions.some((v) => v.checks.partial.advisory)) {
|
|
261
|
+
lines.push('', '\\* Passed on `minVersion` alone, which has been too low before; the build decides.');
|
|
262
|
+
}
|
|
263
|
+
return lines.join('\n') + '\n';
|
|
264
|
+
}
|
|
265
|
+
export function renderJson(report, verbose = false) {
|
|
266
|
+
if (verbose) {
|
|
267
|
+
return JSON.stringify(report, null, 2) + '\n';
|
|
268
|
+
}
|
|
269
|
+
const stripped = {
|
|
270
|
+
...report,
|
|
271
|
+
versions: report.versions.map((v) => ({
|
|
272
|
+
...v,
|
|
273
|
+
checks: Object.fromEntries(Object.entries(v.checks).map(([name, check]) => {
|
|
274
|
+
const { fullOutput: _full, ...rest } = check;
|
|
275
|
+
return [name, rest];
|
|
276
|
+
})),
|
|
277
|
+
})),
|
|
278
|
+
};
|
|
279
|
+
return JSON.stringify(stripped, null, 2) + '\n';
|
|
280
|
+
}
|