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/tools.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { delimiter, dirname, join } from 'node:path';
|
|
4
|
+
import { environmentProblem, errorLines, npm } from "./exec.js";
|
|
5
|
+
/** The pnpm ng-matrix uses, pinned so its peer and build-script rules don't shift under it. */
|
|
6
|
+
export const PNPM_VERSION = '12.15.0';
|
|
7
|
+
export function defaultCacheDir() {
|
|
8
|
+
if (process.env.NG_MATRIX_CACHE)
|
|
9
|
+
return process.env.NG_MATRIX_CACHE;
|
|
10
|
+
if (process.platform === 'win32' && process.env.LOCALAPPDATA)
|
|
11
|
+
return join(process.env.LOCALAPPDATA, 'ng-matrix', 'Cache');
|
|
12
|
+
return join(process.env.XDG_CACHE_HOME ?? join(homedir(), '.cache'), 'ng-matrix');
|
|
13
|
+
}
|
|
14
|
+
export function cacheStats(cacheDir) {
|
|
15
|
+
let bytes = 0;
|
|
16
|
+
let count = 0;
|
|
17
|
+
if (!existsSync(cacheDir))
|
|
18
|
+
return { bytes: 0, count: 0 };
|
|
19
|
+
const stack = [cacheDir];
|
|
20
|
+
while (stack.length) {
|
|
21
|
+
const current = stack.pop();
|
|
22
|
+
let entries;
|
|
23
|
+
try {
|
|
24
|
+
entries = readdirSync(current, { withFileTypes: true });
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
for (const entry of entries) {
|
|
30
|
+
const full = join(current, entry.name);
|
|
31
|
+
if (entry.isDirectory()) {
|
|
32
|
+
stack.push(full);
|
|
33
|
+
}
|
|
34
|
+
else if (entry.isFile()) {
|
|
35
|
+
try {
|
|
36
|
+
bytes += statSync(full).size;
|
|
37
|
+
count++;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// ignore transient access errors
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return { bytes, count };
|
|
46
|
+
}
|
|
47
|
+
export function formatBytes(bytes) {
|
|
48
|
+
if (bytes < 1024)
|
|
49
|
+
return `${bytes} B`;
|
|
50
|
+
const units = ['KB', 'MB', 'GB', 'TB'];
|
|
51
|
+
let val = bytes / 1024;
|
|
52
|
+
for (const unit of units) {
|
|
53
|
+
if (val < 1024 || unit === 'TB') {
|
|
54
|
+
return `${val >= 10 ? val.toFixed(1) : val.toFixed(2)} ${unit}`;
|
|
55
|
+
}
|
|
56
|
+
val /= 1024;
|
|
57
|
+
}
|
|
58
|
+
return `${bytes} B`;
|
|
59
|
+
}
|
|
60
|
+
export function cleanCache(cacheDir) {
|
|
61
|
+
if (existsSync(cacheDir)) {
|
|
62
|
+
rmSync(cacheDir, { recursive: true, force: true, maxRetries: 5 });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const pending = new Map();
|
|
66
|
+
/**
|
|
67
|
+
* Installs one npm package into the cache once (say, `@angular/cli@21.2.28`) and returns its folder,
|
|
68
|
+
* so each Angular major's CLI and the pinned pnpm are downloaded once, not on every run.
|
|
69
|
+
*/
|
|
70
|
+
export function ensureTool(cacheDir, name, version, offline = false) {
|
|
71
|
+
const key = `${name}@${version}`;
|
|
72
|
+
let promise = pending.get(key);
|
|
73
|
+
if (!promise) {
|
|
74
|
+
promise = install(cacheDir, name, version, offline);
|
|
75
|
+
pending.set(key, promise);
|
|
76
|
+
}
|
|
77
|
+
return promise;
|
|
78
|
+
}
|
|
79
|
+
async function install(cacheDir, name, version, offline) {
|
|
80
|
+
const home = join(cacheDir, 'tools', `${name.replace('/', '+')}@${version}`);
|
|
81
|
+
const pkgDir = join(home, 'node_modules', ...name.split('/'));
|
|
82
|
+
if (isComplete(pkgDir))
|
|
83
|
+
return pkgDir;
|
|
84
|
+
// A damaged entry (a truncated package.json, a missing bin) is downloaded again.
|
|
85
|
+
if (existsSync(home))
|
|
86
|
+
rmSync(home, { recursive: true, force: true, maxRetries: 5 });
|
|
87
|
+
const tmp = `${home}.${process.pid}.tmp`;
|
|
88
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
89
|
+
mkdirSync(tmp, { recursive: true });
|
|
90
|
+
const args = ['install', `${name}@${version}`, '--prefix', tmp, '--no-save', '--no-package-lock', '--ignore-scripts'];
|
|
91
|
+
if (offline)
|
|
92
|
+
args.push('--offline');
|
|
93
|
+
const result = await npm(args, { cwd: tmp });
|
|
94
|
+
if (result.code !== 0) {
|
|
95
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
96
|
+
const why = environmentProblem(result);
|
|
97
|
+
throw new Error(`Couldn't download ${name}@${version}${why ? `: ${why}` : ''}:\n${errorLines(result.output).join('\n')}`);
|
|
98
|
+
}
|
|
99
|
+
mkdirSync(dirname(home), { recursive: true });
|
|
100
|
+
try {
|
|
101
|
+
renameSync(tmp, home);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// Another ng-matrix process got there first; theirs is as good as ours.
|
|
105
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
106
|
+
}
|
|
107
|
+
return pkgDir;
|
|
108
|
+
}
|
|
109
|
+
/** A cached tool is usable when its package.json parses and every bin it declares is there. */
|
|
110
|
+
export function isComplete(pkgDir) {
|
|
111
|
+
try {
|
|
112
|
+
const manifest = JSON.parse(readFileSync(join(pkgDir, 'package.json'), 'utf8'));
|
|
113
|
+
const bins = typeof manifest.bin === 'string' ? [manifest.bin] : Object.values(manifest.bin ?? {});
|
|
114
|
+
return bins.every((bin) => existsSync(join(pkgDir, bin)));
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
export function binOf(pkgDir, bin) {
|
|
121
|
+
const manifest = JSON.parse(readFileSync(join(pkgDir, 'package.json'), 'utf8'));
|
|
122
|
+
const path = typeof manifest.bin === 'string' ? manifest.bin : manifest.bin?.[bin];
|
|
123
|
+
if (!path)
|
|
124
|
+
throw new Error(`${pkgDir} has no "${bin}" command.`);
|
|
125
|
+
return join(pkgDir, path);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* pnpm 11+ is a native binary that an install script puts in place of the `pnpm` bin; tools are
|
|
129
|
+
* installed with scripts off, so ng-matrix enters through `bin/pnpm.mjs`, as Corepack does, which
|
|
130
|
+
* finds the binary in the `@pnpm/exe.<platform>` optional dependency.
|
|
131
|
+
*/
|
|
132
|
+
export function pnpmEntry(pkgDir) {
|
|
133
|
+
const entry = join(pkgDir, 'bin', 'pnpm.mjs');
|
|
134
|
+
return existsSync(entry) ? entry : binOf(pkgDir, 'pnpm');
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* A folder with `pnpm` (and `pnpm.cmd` on Windows) that runs the cached pnpm, for PATH, so an
|
|
138
|
+
* author's test command (`pnpm test`, or a script that calls pnpm) finds the same pnpm.
|
|
139
|
+
*/
|
|
140
|
+
export function pnpmShimDir(cacheDir, pkgDir) {
|
|
141
|
+
const dir = join(cacheDir, 'tools', `pnpm-shim@${PNPM_VERSION}`);
|
|
142
|
+
mkdirSync(dir, { recursive: true });
|
|
143
|
+
const entry = pnpmEntry(pkgDir);
|
|
144
|
+
writeFileSync(join(dir, 'pnpm'), `#!/bin/sh\nexec "${process.execPath}" "${entry}" "$@"\n`, { mode: 0o755 });
|
|
145
|
+
writeFileSync(join(dir, 'pnpm.cmd'), `@"${process.execPath}" "${entry}" %*\r\n`);
|
|
146
|
+
return dir;
|
|
147
|
+
}
|
|
148
|
+
/** PATH with `dir` first, under whichever spelling of the key this platform uses. */
|
|
149
|
+
export function withPath(env, dir) {
|
|
150
|
+
const key = Object.keys(env).find((k) => k.toUpperCase() === 'PATH') ?? 'PATH';
|
|
151
|
+
return { ...env, [key]: `${dir}${delimiter}${env[key] ?? ''}` };
|
|
152
|
+
}
|
package/dist/versions.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import semver from 'semver';
|
|
2
|
+
/** How long a major is supported after its x.0.0 release: 6 months active, then 12 months LTS. */
|
|
3
|
+
export const SUPPORT_MONTHS = 18;
|
|
4
|
+
/** Majors older than this are only checked on request, best effort. */
|
|
5
|
+
export const OLDEST_DEFAULT_MAJOR = 17;
|
|
6
|
+
export function stableVersions(packument) {
|
|
7
|
+
return Object.keys(packument.versions)
|
|
8
|
+
.filter((v) => semver.valid(v) && !semver.prerelease(v))
|
|
9
|
+
.sort(semver.rcompare);
|
|
10
|
+
}
|
|
11
|
+
export function latestOfMajor(packument, major) {
|
|
12
|
+
return stableVersions(packument).find((v) => semver.major(v) === major);
|
|
13
|
+
}
|
|
14
|
+
export function majorsOf(packument) {
|
|
15
|
+
return [...new Set(stableVersions(packument).map((v) => semver.major(v)))].sort((a, b) => a - b);
|
|
16
|
+
}
|
|
17
|
+
export function isSupported(packument, major, now) {
|
|
18
|
+
const released = packument.time[`${major}.0.0`];
|
|
19
|
+
const newest = Math.max(...majorsOf(packument));
|
|
20
|
+
if (major === newest)
|
|
21
|
+
return true;
|
|
22
|
+
if (!released)
|
|
23
|
+
return false;
|
|
24
|
+
const end = new Date(released);
|
|
25
|
+
end.setMonth(end.getMonth() + SUPPORT_MONTHS);
|
|
26
|
+
return now < end;
|
|
27
|
+
}
|
|
28
|
+
export function supportedMajors(packument, now) {
|
|
29
|
+
return majorsOf(packument).filter((major) => isSupported(packument, major, now));
|
|
30
|
+
}
|
|
31
|
+
/** The `next` tag, when it's ahead of `latest`. It may be a minor pre-release, not a new major. */
|
|
32
|
+
export function nextVersion(packument) {
|
|
33
|
+
const next = packument['dist-tags'].next;
|
|
34
|
+
const latest = packument['dist-tags'].latest;
|
|
35
|
+
if (!next || !semver.valid(next))
|
|
36
|
+
return undefined;
|
|
37
|
+
if (latest && semver.lte(next, latest))
|
|
38
|
+
return undefined;
|
|
39
|
+
return next;
|
|
40
|
+
}
|
|
41
|
+
/** Newest version of a package (pre-releases only when asked) that satisfies a range. */
|
|
42
|
+
export function newestSatisfying(packument, range, includePrerelease = false) {
|
|
43
|
+
return Object.keys(packument.versions)
|
|
44
|
+
.filter((v) => semver.valid(v) && (includePrerelease || !semver.prerelease(v)))
|
|
45
|
+
.sort(semver.rcompare)
|
|
46
|
+
.find((v) => semver.satisfies(v, range, { includePrerelease: true }));
|
|
47
|
+
}
|
|
48
|
+
export async function loadVersionSources(registry) {
|
|
49
|
+
const [core, cli, compilerCli] = await Promise.all([
|
|
50
|
+
registry.get('@angular/core'),
|
|
51
|
+
registry.get('@angular/cli'),
|
|
52
|
+
registry.get('@angular/compiler-cli'),
|
|
53
|
+
]);
|
|
54
|
+
return { core, cli, compilerCli };
|
|
55
|
+
}
|
|
56
|
+
export function describeMajor(sources, major, now, useNext) {
|
|
57
|
+
const version = useNext && semver.major(useNext) === major ? useNext : latestOfMajor(sources.core, major);
|
|
58
|
+
if (!version)
|
|
59
|
+
throw new Error(`Angular ${major} isn't published on npm.`);
|
|
60
|
+
const prerelease = Boolean(semver.prerelease(version));
|
|
61
|
+
const cliVersion = (prerelease ? sameOrNewestPrerelease(sources.cli, version) : latestOfMajor(sources.cli, major)) ?? version;
|
|
62
|
+
const compilerVersion = sources.compilerCli.versions[version] ? version : latestOfMajor(sources.compilerCli, major);
|
|
63
|
+
const coreMeta = sources.core.versions[version];
|
|
64
|
+
const cliMeta = sources.cli.versions[cliVersion];
|
|
65
|
+
const compilerMeta = compilerVersion ? sources.compilerCli.versions[compilerVersion] : undefined;
|
|
66
|
+
const nodeRanges = [coreMeta.engines?.node, cliMeta?.engines?.node].filter((r) => Boolean(r));
|
|
67
|
+
return {
|
|
68
|
+
major,
|
|
69
|
+
version,
|
|
70
|
+
cliVersion,
|
|
71
|
+
releasedAt: sources.core.time[`${major}.0.0`],
|
|
72
|
+
supported: isSupported(sources.core, major, now),
|
|
73
|
+
nodeRanges,
|
|
74
|
+
typescriptRange: compilerMeta?.peerDependencies?.typescript,
|
|
75
|
+
rxjsRange: coreMeta.peerDependencies?.rxjs,
|
|
76
|
+
zoneRange: coreMeta.peerDependencies?.['zone.js'],
|
|
77
|
+
prerelease,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function sameOrNewestPrerelease(packument, version) {
|
|
81
|
+
if (packument.versions[version])
|
|
82
|
+
return version;
|
|
83
|
+
const major = semver.major(version);
|
|
84
|
+
return Object.keys(packument.versions)
|
|
85
|
+
.filter((v) => semver.valid(v) && semver.major(v) === major)
|
|
86
|
+
.sort(semver.rcompare)[0];
|
|
87
|
+
}
|
|
88
|
+
/** Parses `--versions`: `22`, `18-22`, `20,22`, or a mix. */
|
|
89
|
+
export function parseVersionList(text) {
|
|
90
|
+
const majors = new Set();
|
|
91
|
+
for (const part of text.split(',').map((p) => p.trim()).filter(Boolean)) {
|
|
92
|
+
const range = /^(\d+)\s*-\s*(\d+)$/.exec(part);
|
|
93
|
+
if (range) {
|
|
94
|
+
const [from, to] = [Number(range[1]), Number(range[2])].sort((a, b) => a - b);
|
|
95
|
+
for (let m = from; m <= to; m++)
|
|
96
|
+
majors.add(m);
|
|
97
|
+
}
|
|
98
|
+
else if (/^\d+$/.test(part)) {
|
|
99
|
+
majors.add(Number(part));
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
throw new Error(`--versions: "${part}" isn't a major or a range like 18-22`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return [...majors].sort((a, b) => a - b);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Default majors: the supported ones, plus every major the package's `@angular/core` range claims
|
|
109
|
+
* (back to Angular 17), plus the newest, so "works but not claimed" shows up.
|
|
110
|
+
*/
|
|
111
|
+
export function defaultMajors(core, coreRange, now) {
|
|
112
|
+
const all = majorsOf(core);
|
|
113
|
+
const chosen = new Set(supportedMajors(core, now));
|
|
114
|
+
chosen.add(Math.max(...all));
|
|
115
|
+
if (coreRange) {
|
|
116
|
+
for (const major of all) {
|
|
117
|
+
if (major < OLDEST_DEFAULT_MAJOR)
|
|
118
|
+
continue;
|
|
119
|
+
const version = latestOfMajor(core, major);
|
|
120
|
+
if (version && claimsMajor(coreRange, major, version))
|
|
121
|
+
chosen.add(major);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return [...chosen].sort((a, b) => a - b);
|
|
125
|
+
}
|
|
126
|
+
function claimsMajor(range, major, latest) {
|
|
127
|
+
try {
|
|
128
|
+
return (semver.satisfies(latest, range, { includePrerelease: true }) ||
|
|
129
|
+
semver.intersects(range, `>=${major}.0.0 <${major + 1}.0.0-0`, { includePrerelease: true }));
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/** Whether this Node can run a major, reading `engines` as minimums (they have no upper bound). */
|
|
136
|
+
export function nodeSatisfies(node, ranges) {
|
|
137
|
+
return ranges.every((range) => semver.satisfies(node, range, { includePrerelease: true }));
|
|
138
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,51 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "angular-compat-matrix",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Find out which Angular versions your published library really works with: it installs the packed package into a fresh app for each Angular major, builds it, and reports a matrix.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"angular",
|
|
7
|
+
"library",
|
|
8
|
+
"compatibility",
|
|
9
|
+
"peer-dependencies",
|
|
10
|
+
"ng-packagr",
|
|
11
|
+
"ci"
|
|
12
|
+
],
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/berserker5619/angular-compat-matrix.git"
|
|
16
|
+
},
|
|
17
|
+
"bugs": "https://github.com/berserker5619/angular-compat-matrix/issues",
|
|
18
|
+
"homepage": "https://github.com/berserker5619/angular-compat-matrix#readme",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"author": "Nizamudeen S",
|
|
21
|
+
"type": "module",
|
|
22
|
+
"bin": {
|
|
23
|
+
"angular-compat-matrix": "dist/cli.js",
|
|
24
|
+
"ng-matrix": "dist/cli.js"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"README.md",
|
|
29
|
+
"LICENSE"
|
|
30
|
+
],
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=22.12.0"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsc -p tsconfig.build.json",
|
|
36
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
37
|
+
"test": "node --test \"test/unit/*.test.ts\"",
|
|
38
|
+
"test:fixtures": "node test/fixtures/run.ts",
|
|
39
|
+
"record-registry": "node scripts/record-registry.ts",
|
|
40
|
+
"prepublishOnly": "npm run typecheck && npm test && npm run build"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"semver": "^7.7.3",
|
|
44
|
+
"tar": "^7.5.2"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/node": "^24.10.0",
|
|
48
|
+
"@types/semver": "^7.7.1",
|
|
49
|
+
"typescript": "~5.9.3"
|
|
50
|
+
}
|
|
51
|
+
}
|