angular-compat-matrix 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +335 -55
- package/dist/app.js +4 -3
- package/dist/build.js +125 -4
- package/dist/cleanup.js +117 -0
- package/dist/cli.js +62 -10
- package/dist/exec.js +60 -4
- package/dist/install.js +27 -18
- package/dist/library.js +19 -3
- package/dist/model.js +1 -0
- package/dist/progress.js +65 -0
- package/dist/registry.js +66 -16
- package/dist/report.js +53 -6
- package/dist/run.js +86 -6
- package/dist/scripts.js +73 -0
- package/dist/smoke.js +8 -6
- package/dist/tests.js +10 -5
- package/dist/tools.js +70 -4
- package/package.json +1 -1
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
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { existsSync,
|
|
3
|
-
import {
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { cleanUpOnInterrupt, createWorkDir, removeDir, sweepCache, sweepWorkDirs } from "./cleanup.js";
|
|
4
4
|
import { dirname, join, resolve } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { parseArgs } from 'node:util';
|
|
7
7
|
import semver from 'semver';
|
|
8
8
|
import { LibraryError, prepareLibrary } from "./library.js";
|
|
9
|
+
import { SCHEMA_VERSION } from "./model.js";
|
|
9
10
|
import { angularImports, scanPackage, summarize } from "./partial.js";
|
|
10
|
-
import { Registry } from "./registry.js";
|
|
11
|
+
import { Registry, RegistryError } from "./registry.js";
|
|
11
12
|
import { exitCode, findings, renderJson, renderMarkdown, renderTerminal } from "./report.js";
|
|
12
13
|
import { defaultConcurrency, runAll } from "./run.js";
|
|
13
|
-
import {
|
|
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";
|
|
14
18
|
import { defaultMajors, describeMajor, loadVersionSources, majorsOf, nextVersion, nodeSatisfies, parseVersionList, } from "./versions.js";
|
|
15
19
|
const HELP = `ng-matrix: which Angular versions does your published library really work with?
|
|
16
20
|
|
|
@@ -20,6 +24,8 @@ major (made with that major's own CLI), builds it, and reports a matrix.
|
|
|
20
24
|
Usage
|
|
21
25
|
npx angular-compat-matrix [options]
|
|
22
26
|
ng-matrix [options] (once installed)
|
|
27
|
+
ng-matrix cache (show cache location and size)
|
|
28
|
+
ng-matrix cache clean (delete the cache)
|
|
23
29
|
|
|
24
30
|
Options
|
|
25
31
|
--versions <list> Angular majors to check: 22, 18-22, 20,22. Default: the supported
|
|
@@ -30,6 +36,7 @@ Options
|
|
|
30
36
|
--tests Also run the library's tests on each version (slow)
|
|
31
37
|
--test-command <cmd> The test command for --tests (default: <pm> test)
|
|
32
38
|
--format <format> terminal (default), markdown or json
|
|
39
|
+
--json Shorthand for --format json
|
|
33
40
|
--pm <npm|pnpm> Package manager for the apps (default: pnpm if there's a
|
|
34
41
|
pnpm-lock.yaml, else npm)
|
|
35
42
|
--build <cmd> Build the library with this command instead of the detected one
|
|
@@ -37,10 +44,13 @@ Options
|
|
|
37
44
|
--tarball <file> Check this packed .tgz instead of building
|
|
38
45
|
--package <spec> Check a published package, e.g. some-lib@3.1.0
|
|
39
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)
|
|
40
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...)
|
|
41
51
|
--offline Use only cached registry answers and packages
|
|
42
52
|
--cwd <dir> The library's workspace (default: the current folder)
|
|
43
|
-
--verbose Show
|
|
53
|
+
--verbose Show commands run and full build/install output for failures
|
|
44
54
|
-h, --help This help
|
|
45
55
|
-v, --version ng-matrix's version
|
|
46
56
|
|
|
@@ -66,6 +76,9 @@ function explain(error) {
|
|
|
66
76
|
if (error instanceof Stop || error instanceof LibraryError) {
|
|
67
77
|
process.stderr.write(`ng-matrix: ${error.message}\n${error.lines.map((l) => ` ${l}\n`).join('')}`);
|
|
68
78
|
}
|
|
79
|
+
else if (error instanceof RegistryError) {
|
|
80
|
+
process.stderr.write(`ng-matrix: ${error.message}\n`);
|
|
81
|
+
}
|
|
69
82
|
else if (String(error.code).startsWith('ERR_PARSE_ARGS')) {
|
|
70
83
|
process.stderr.write(`ng-matrix: ${error.message}. See --help.\n`);
|
|
71
84
|
}
|
|
@@ -85,13 +98,16 @@ export async function main(argv) {
|
|
|
85
98
|
consumer: { type: 'string' },
|
|
86
99
|
tests: { type: 'boolean', default: false },
|
|
87
100
|
'test-command': { type: 'string' },
|
|
101
|
+
'allow-scripts': { type: 'boolean', default: false },
|
|
88
102
|
format: { type: 'string', default: 'terminal' },
|
|
103
|
+
json: { type: 'boolean', default: false },
|
|
89
104
|
pm: { type: 'string' },
|
|
90
105
|
build: { type: 'string' },
|
|
91
106
|
'skip-build': { type: 'boolean', default: false },
|
|
92
107
|
tarball: { type: 'string' },
|
|
93
108
|
package: { type: 'string' },
|
|
94
109
|
concurrency: { type: 'string' },
|
|
110
|
+
'step-timeout': { type: 'string' },
|
|
95
111
|
keep: { type: 'boolean', default: false },
|
|
96
112
|
offline: { type: 'boolean', default: false },
|
|
97
113
|
cwd: { type: 'string' },
|
|
@@ -108,28 +124,58 @@ export async function main(argv) {
|
|
|
108
124
|
process.stdout.write(`${toolVersion()}\n`);
|
|
109
125
|
return 0;
|
|
110
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
|
+
}
|
|
111
142
|
if (positionals[0] === 'ci')
|
|
112
143
|
fail('`ng-matrix ci github` is planned for v0.2.');
|
|
113
144
|
if (positionals.length)
|
|
114
145
|
fail(`Unknown argument "${positionals[0]}". See --help.`);
|
|
115
|
-
const format = values.format;
|
|
146
|
+
const format = values.json ? 'json' : values.format;
|
|
116
147
|
if (!['terminal', 'markdown', 'json'].includes(format))
|
|
117
148
|
fail(`--format must be terminal, markdown or json, not "${format}".`);
|
|
149
|
+
if (values.verbose)
|
|
150
|
+
setVerbose(true);
|
|
118
151
|
const cwd = resolve(values.cwd ?? process.cwd());
|
|
119
152
|
const pm = (values.pm ?? (existsSync(join(cwd, 'pnpm-lock.yaml')) ? 'pnpm' : 'npm'));
|
|
120
153
|
if (pm !== 'npm' && pm !== 'pnpm')
|
|
121
154
|
fail(`--pm must be npm or pnpm (yarn and bun come later), not "${pm}".`);
|
|
122
155
|
const consumer = values.consumer ? resolve(cwd, values.consumer) : undefined;
|
|
123
|
-
|
|
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}`);
|
|
124
164
|
const cacheDir = defaultCacheDir();
|
|
125
165
|
const registry = new Registry({
|
|
126
166
|
cacheDir,
|
|
127
167
|
offline: values.offline,
|
|
128
168
|
recordedDir: process.env.NG_MATRIX_RECORDED_REGISTRY,
|
|
129
169
|
});
|
|
130
|
-
|
|
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);
|
|
131
175
|
if (values.keep)
|
|
132
176
|
log(`Working in ${workDir} (--keep)`);
|
|
177
|
+
if (values['allow-scripts'])
|
|
178
|
+
log(ALLOW_SCRIPTS_WARNING);
|
|
133
179
|
try {
|
|
134
180
|
const library = await prepareLibrary({
|
|
135
181
|
cwd,
|
|
@@ -175,9 +221,12 @@ export async function main(argv) {
|
|
|
175
221
|
testCommand: values['test-command'],
|
|
176
222
|
consumer,
|
|
177
223
|
keep: values.keep,
|
|
224
|
+
allowScripts: values['allow-scripts'],
|
|
178
225
|
log,
|
|
226
|
+
progress,
|
|
179
227
|
}, angular, concurrency);
|
|
180
228
|
const report = {
|
|
229
|
+
schemaVersion: SCHEMA_VERSION,
|
|
181
230
|
tool: { name: 'angular-compat-matrix', version: toolVersion() },
|
|
182
231
|
checkedAt: now.toISOString(),
|
|
183
232
|
library: {
|
|
@@ -190,12 +239,13 @@ export async function main(argv) {
|
|
|
190
239
|
declarations: summary.declarations,
|
|
191
240
|
},
|
|
192
241
|
packageManager: pm,
|
|
242
|
+
installScripts: values['allow-scripts'] ? 'all' : 'toolchain-only',
|
|
193
243
|
node: process.version,
|
|
194
244
|
versions,
|
|
195
245
|
findings: findings(versions, library.manifest, angularImports(library.dir)),
|
|
196
246
|
exitCode: exitCode(versions),
|
|
197
247
|
};
|
|
198
|
-
const render = format === 'json' ? renderJson : format === 'markdown' ? renderMarkdown : (r) => renderTerminal(r, values.verbose);
|
|
248
|
+
const render = format === 'json' ? (r) => renderJson(r, values.verbose) : format === 'markdown' ? renderMarkdown : (r) => renderTerminal(r, values.verbose);
|
|
199
249
|
process.stdout.write((format === 'terminal' ? '\n' : '') + render(report));
|
|
200
250
|
return report.exitCode;
|
|
201
251
|
}
|
|
@@ -203,8 +253,10 @@ export async function main(argv) {
|
|
|
203
253
|
return explain(error);
|
|
204
254
|
}
|
|
205
255
|
finally {
|
|
256
|
+
progress.stop();
|
|
257
|
+
stopListening();
|
|
206
258
|
if (!values.keep)
|
|
207
|
-
|
|
259
|
+
removeDir(workDir);
|
|
208
260
|
}
|
|
209
261
|
}
|
|
210
262
|
main(process.argv.slice(2)).then((code) => process.exit(code), (error) => process.exit(explain(error)));
|
package/dist/exec.js
CHANGED
|
@@ -19,12 +19,35 @@ export function quietEnv(extra = {}) {
|
|
|
19
19
|
...extra,
|
|
20
20
|
};
|
|
21
21
|
}
|
|
22
|
+
/** Every child still running, so an interrupt can stop them all. */
|
|
23
|
+
const running = new Set();
|
|
24
|
+
/** Stops every running child and everything it started (npm, then the build it runs). */
|
|
25
|
+
export function killAllChildren() {
|
|
26
|
+
for (const child of running)
|
|
27
|
+
killTree(child.pid);
|
|
28
|
+
running.clear();
|
|
29
|
+
}
|
|
30
|
+
let verboseEnabled = false;
|
|
31
|
+
export function setVerbose(enabled) {
|
|
32
|
+
verboseEnabled = enabled;
|
|
33
|
+
}
|
|
34
|
+
export function isVerbose() {
|
|
35
|
+
return verboseEnabled;
|
|
36
|
+
}
|
|
22
37
|
export function exec(command, args, options) {
|
|
23
38
|
const started = Date.now();
|
|
39
|
+
if (verboseEnabled) {
|
|
40
|
+
const fullCmd = options.shell ? [command, ...args].join(' ') : `${command} ${args.join(' ')}`;
|
|
41
|
+
process.stderr.write(`[ng-matrix] > ${fullCmd}\n`);
|
|
42
|
+
}
|
|
24
43
|
return new Promise((resolve) => {
|
|
44
|
+
// On Linux and macOS each child leads its own process group, so killing the group stops
|
|
45
|
+
// its children too. Windows has taskkill /T for that.
|
|
46
|
+
const detached = process.platform !== 'win32';
|
|
25
47
|
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() });
|
|
48
|
+
? spawn([command, ...args].join(' '), { cwd: options.cwd, env: options.env ?? quietEnv(), shell: true, detached })
|
|
49
|
+
: spawn(command, args, { cwd: options.cwd, env: options.env ?? quietEnv(), detached });
|
|
50
|
+
running.add(child);
|
|
28
51
|
let output = '';
|
|
29
52
|
let timedOut = false;
|
|
30
53
|
const append = (chunk) => (output += chunk.toString('utf8'));
|
|
@@ -37,6 +60,7 @@ export function exec(command, args, options) {
|
|
|
37
60
|
}, options.timeoutMs)
|
|
38
61
|
: undefined;
|
|
39
62
|
const finish = (code) => {
|
|
63
|
+
running.delete(child);
|
|
40
64
|
if (timer)
|
|
41
65
|
clearTimeout(timer);
|
|
42
66
|
resolve({ code, output: stripAnsi(output), durationMs: Date.now() - started, timedOut });
|
|
@@ -56,10 +80,15 @@ function killTree(pid) {
|
|
|
56
80
|
}
|
|
57
81
|
else {
|
|
58
82
|
try {
|
|
59
|
-
process.kill(pid, 'SIGKILL');
|
|
83
|
+
process.kill(-pid, 'SIGKILL');
|
|
60
84
|
}
|
|
61
85
|
catch {
|
|
62
|
-
|
|
86
|
+
try {
|
|
87
|
+
process.kill(pid, 'SIGKILL');
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// already gone
|
|
91
|
+
}
|
|
63
92
|
}
|
|
64
93
|
}
|
|
65
94
|
}
|
|
@@ -108,3 +137,30 @@ export function errorLines(output, max = 12) {
|
|
|
108
137
|
}
|
|
109
138
|
return lines.filter((line) => line.trim()).slice(-max);
|
|
110
139
|
}
|
|
140
|
+
/** How long any one step (ng new, an install, a build, the tests) may take: --step-timeout. */
|
|
141
|
+
let stepTimeoutMs = 20 * 60_000;
|
|
142
|
+
export function setStepTimeout(ms) {
|
|
143
|
+
stepTimeoutMs = ms;
|
|
144
|
+
}
|
|
145
|
+
export function stepTimeout() {
|
|
146
|
+
return stepTimeoutMs;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Why a step failed when it's the machine's fault, not the package's: it timed out, the disk is
|
|
150
|
+
* full, or the network dropped. Such a version is reported as not checked (exit 2), not as failing.
|
|
151
|
+
*/
|
|
152
|
+
export function environmentProblem(result) {
|
|
153
|
+
if (result.timedOut)
|
|
154
|
+
return `timed out after ${formatMinutes(stepTimeoutMs)} (raise the limit with --step-timeout <minutes>)`;
|
|
155
|
+
if (/\bENOSPC\b|no space left on device|not enough space on the disk/i.test(result.output)) {
|
|
156
|
+
return 'the disk is full (ENOSPC). Free some space, or point TMPDIR/TEMP and NG_MATRIX_CACHE somewhere with room';
|
|
157
|
+
}
|
|
158
|
+
const network = /^.*\b(EAI_AGAIN|ENOTFOUND|ECONNREFUSED|ECONNRESET|ETIMEDOUT|ERR_SOCKET_TIMEOUT|ERR_PNPM_META_FETCH_FAIL|socket hang up)\b.*$/im.exec(result.output);
|
|
159
|
+
if (network)
|
|
160
|
+
return `a network error: ${network[0].trim().slice(0, 200)}`;
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
function formatMinutes(ms) {
|
|
164
|
+
const minutes = ms / 60_000;
|
|
165
|
+
return minutes >= 1 ? `${Number(minutes.toFixed(1))} min` : `${Math.round(ms / 1000)} s`;
|
|
166
|
+
}
|
package/dist/install.js
CHANGED
|
@@ -2,7 +2,8 @@ import { copyFileSync, writeFileSync } from 'node:fs';
|
|
|
2
2
|
import { basename, join } from 'node:path';
|
|
3
3
|
import semver from 'semver';
|
|
4
4
|
import { readJson, writeJson } from "./app.js";
|
|
5
|
-
import { errorLines, exec, npm, quietEnv } from "./exec.js";
|
|
5
|
+
import { environmentProblem, errorLines, exec, npm, quietEnv, stepTimeout } from "./exec.js";
|
|
6
|
+
import { npmInstall, pnpmBuildSettings, skippedByNpm, skippedByPnpm } from "./scripts.js";
|
|
6
7
|
import { ensureTool, PNPM_VERSION, pnpmEntry } from "./tools.js";
|
|
7
8
|
export function addDependencies(appDir, deps, extra = {}) {
|
|
8
9
|
const path = join(appDir, 'package.json');
|
|
@@ -15,7 +16,7 @@ export async function installLibrary(request) {
|
|
|
15
16
|
const tgz = basename(request.tarball);
|
|
16
17
|
copyFileSync(request.tarball, join(request.appDir, tgz));
|
|
17
18
|
const deps = { ...request.plan.installs, [request.libraryName]: `file:${tgz}` };
|
|
18
|
-
return request.pm === 'npm' ? installWithNpm(request, deps) : installWithPnpm(request, deps
|
|
19
|
+
return request.pm === 'npm' ? installWithNpm(request, deps) : installWithPnpm(request, deps);
|
|
19
20
|
}
|
|
20
21
|
/**
|
|
21
22
|
* npm 11 fails on any peer conflict and can't ignore just one. So: install without peer
|
|
@@ -26,13 +27,22 @@ async function installWithNpm(request, deps) {
|
|
|
26
27
|
const args = ['install', '--legacy-peer-deps', '--no-audit', '--no-fund'];
|
|
27
28
|
if (request.offline)
|
|
28
29
|
args.push('--offline');
|
|
29
|
-
const result = await
|
|
30
|
+
const result = await npmInstall(args, { cwd: request.appDir, timeoutMs: stepTimeout() }, request.allowScripts);
|
|
31
|
+
const skippedScripts = request.allowScripts ? [] : skippedByNpm(request.appDir);
|
|
30
32
|
if (result.code !== 0) {
|
|
31
|
-
return {
|
|
33
|
+
return {
|
|
34
|
+
pass: false,
|
|
35
|
+
message: 'npm install failed.',
|
|
36
|
+
errorLines: errorLines(result.output),
|
|
37
|
+
fullOutput: result.output,
|
|
38
|
+
ignored: [],
|
|
39
|
+
environment: environmentProblem(result),
|
|
40
|
+
skippedScripts,
|
|
41
|
+
};
|
|
32
42
|
}
|
|
33
43
|
const ls = await npm(['ls', '--all'], { cwd: request.appDir });
|
|
34
44
|
const problems = parseNpmLs(ls.output);
|
|
35
|
-
return judgeProblems(request, problems);
|
|
45
|
+
return { ...judgeProblems(request, problems), skippedScripts };
|
|
36
46
|
}
|
|
37
47
|
/** Reads `npm ls --all` text output: `@angular/core@21.2.25 deduped invalid: "^22.0.0" from node_modules/@angular/cdk`. */
|
|
38
48
|
export function parseNpmLs(output) {
|
|
@@ -79,10 +89,10 @@ function judgeProblems(request, problems) {
|
|
|
79
89
|
}
|
|
80
90
|
/**
|
|
81
91
|
* pnpm only warns about peers by default, so strictPeerDependencies is on, with allowedVersions
|
|
82
|
-
* for the one expected conflict. strictDepBuilds is off
|
|
83
|
-
*
|
|
92
|
+
* for the one expected conflict. strictDepBuilds is off, so a package whose install script isn't
|
|
93
|
+
* allowed is skipped rather than failing the install; only the toolchain's builds are allowed.
|
|
84
94
|
*/
|
|
85
|
-
async function installWithPnpm(request, deps
|
|
95
|
+
async function installWithPnpm(request, deps) {
|
|
86
96
|
addDependencies(request.appDir, deps, { packageManager: `pnpm@${PNPM_VERSION}` });
|
|
87
97
|
const allowed = {};
|
|
88
98
|
if (request.plan.claimed === false) {
|
|
@@ -91,20 +101,24 @@ async function installWithPnpm(request, deps, tgz) {
|
|
|
91
101
|
allowed[`${request.libraryName}>${miss.name}`] = major;
|
|
92
102
|
}
|
|
93
103
|
}
|
|
94
|
-
writeFileSync(join(request.appDir, 'pnpm-workspace.yaml'), pnpmWorkspaceYaml(request.
|
|
104
|
+
writeFileSync(join(request.appDir, 'pnpm-workspace.yaml'), pnpmWorkspaceYaml(allowed, request.allowScripts));
|
|
95
105
|
const pnpm = await ensureTool(request.cacheDir, 'pnpm', PNPM_VERSION, request.offline);
|
|
96
106
|
const args = [pnpmEntry(pnpm), 'install', '--no-frozen-lockfile'];
|
|
97
107
|
if (request.offline)
|
|
98
108
|
args.push('--offline');
|
|
99
|
-
const result = await exec(process.execPath, args, { cwd: request.appDir, env: quietEnv(), timeoutMs:
|
|
109
|
+
const result = await exec(process.execPath, args, { cwd: request.appDir, env: quietEnv(), timeoutMs: stepTimeout() });
|
|
100
110
|
const ignored = Object.entries(allowed).map(([key, major]) => `${key} allowed at ${major}.x`);
|
|
111
|
+
const skippedScripts = request.allowScripts ? [] : skippedByPnpm(result.output);
|
|
101
112
|
if (result.code !== 0) {
|
|
102
113
|
const peer = /ERR_PNPM_PEER_DEP_ISSUES|unmet peer|Issues with peer dependencies/i.test(result.output);
|
|
103
114
|
return {
|
|
104
115
|
pass: false,
|
|
105
116
|
message: peer ? 'Peer conflict (pnpm strict-peer-dependencies).' : 'pnpm install failed.',
|
|
106
117
|
errorLines: errorLines(result.output),
|
|
118
|
+
fullOutput: result.output,
|
|
107
119
|
ignored,
|
|
120
|
+
environment: environmentProblem(result),
|
|
121
|
+
skippedScripts,
|
|
108
122
|
};
|
|
109
123
|
}
|
|
110
124
|
return {
|
|
@@ -112,16 +126,11 @@ async function installWithPnpm(request, deps, tgz) {
|
|
|
112
126
|
message: ignored.length ? `Installed; allowed the expected conflict: ${ignored.join('; ')}.` : 'Installed.',
|
|
113
127
|
errorLines: [],
|
|
114
128
|
ignored,
|
|
129
|
+
skippedScripts,
|
|
115
130
|
};
|
|
116
131
|
}
|
|
117
|
-
export function pnpmWorkspaceYaml(
|
|
118
|
-
const lines = [
|
|
119
|
-
'strictPeerDependencies: true',
|
|
120
|
-
'strictDepBuilds: false',
|
|
121
|
-
'allowBuilds:',
|
|
122
|
-
// pnpm keys a tarball dependency by its full spec; the bare name doesn't match it.
|
|
123
|
-
` ${JSON.stringify(`${libraryName}@file:${tgz}`)}: true`,
|
|
124
|
-
];
|
|
132
|
+
export function pnpmWorkspaceYaml(allowed, allowScripts = false) {
|
|
133
|
+
const lines = ['strictPeerDependencies: true', ...pnpmBuildSettings(allowScripts)];
|
|
125
134
|
if (Object.keys(allowed).length) {
|
|
126
135
|
lines.push('peerDependencyRules:', ' allowedVersions:');
|
|
127
136
|
for (const [key, range] of Object.entries(allowed))
|
package/dist/library.js
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync } from 'node
|
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
3
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
4
4
|
import * as tar from 'tar';
|
|
5
|
-
import { errorLines, exec, npm, quietEnv } from "./exec.js";
|
|
5
|
+
import { environmentProblem, errorLines, exec, npm, quietEnv } from "./exec.js";
|
|
6
6
|
export class LibraryError extends Error {
|
|
7
7
|
lines;
|
|
8
8
|
constructor(message, lines = []) {
|
|
@@ -126,15 +126,31 @@ export function fullCompilationEvidence(dir) {
|
|
|
126
126
|
async function pack(source, into) {
|
|
127
127
|
const result = await npm(['pack', source, '--pack-destination', into, '--json', '--ignore-scripts'], { cwd: into });
|
|
128
128
|
if (result.code !== 0)
|
|
129
|
-
throw new LibraryError(
|
|
129
|
+
throw new LibraryError(packFailure(source, result.output), errorLines(result.output));
|
|
130
130
|
const json = result.output.slice(result.output.indexOf('['));
|
|
131
131
|
const [info] = JSON.parse(json);
|
|
132
132
|
// Scoped packages: npm reports "@scope/name-1.0.0.tgz" but writes "scope-name-1.0.0.tgz".
|
|
133
133
|
return join(into, info.filename.replace(/^@/, '').replace('/', '-'));
|
|
134
134
|
}
|
|
135
|
+
/** What went wrong with `npm pack <spec>`, in words: most often the name or the version doesn't exist. */
|
|
136
|
+
export function packFailure(spec, output) {
|
|
137
|
+
if (/\bE404\b|404 Not Found/.test(output))
|
|
138
|
+
return `There's no package ${spec.replace(/(.)@[^@]*$/, '$1')} on the npm registry.`;
|
|
139
|
+
if (/\bETARGET\b|No matching version/.test(output))
|
|
140
|
+
return `${spec} doesn't match any published version.`;
|
|
141
|
+
const why = environmentProblem({ output, timedOut: false });
|
|
142
|
+
if (why)
|
|
143
|
+
return `Couldn't download ${spec}: ${why}.`;
|
|
144
|
+
return `npm pack ${spec} failed.`;
|
|
145
|
+
}
|
|
135
146
|
export async function extract(tarball, into) {
|
|
136
147
|
mkdirSync(into, { recursive: true });
|
|
137
|
-
|
|
148
|
+
try {
|
|
149
|
+
await tar.x({ file: tarball, cwd: into, strict: true });
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
throw new LibraryError(`${tarball} isn't a package tarball (.tgz): ${error.message}`);
|
|
153
|
+
}
|
|
138
154
|
const dir = join(into, 'package');
|
|
139
155
|
if (!existsSync(join(dir, 'package.json')))
|
|
140
156
|
throw new LibraryError(`${tarball} has no package/package.json.`);
|
package/dist/model.js
CHANGED
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
|
+
}
|