pr-checkmate 3.0.0 → 3.0.2
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 +1 -1
- package/cspell.json +379 -378
- package/dist/cli.js +1 -1
- package/dist/config/constants.js +1 -0
- package/dist/core/checks/languages/ruff-shared.d.ts +3 -3
- package/dist/core/checks/languages/ruff-shared.js +27 -4
- package/dist/core/checks/quality/coverage.js +6 -1
- package/dist/core/checks/quality/dead-code.js +12 -3
- package/dist/core/checks/quality/package-size.d.ts +36 -0
- package/dist/core/checks/quality/package-size.js +203 -0
- package/dist/core/commands.js +1 -0
- package/dist/core/install/cache.d.ts +20 -1
- package/dist/core/install/cache.js +23 -1
- package/dist/core/install/doctor.js +40 -15
- package/dist/core/install/ensure.d.ts +31 -14
- package/dist/core/install/ensure.js +145 -60
- package/dist/core/install/merge.d.ts +47 -0
- package/dist/core/install/merge.js +91 -0
- package/dist/core/install/prepare.d.ts +13 -12
- package/dist/core/install/prepare.js +32 -36
- package/dist/core/registry.js +2 -0
- package/dist/interfaces/index.d.ts +5 -0
- package/install-manifests/commitlint/package-lock.json +128 -250
- package/install-manifests/commitlint/package.json +2 -2
- package/install-manifests/core/package-lock.json +76 -76
- package/install-manifests/core/package.json +1 -1
- package/install-manifests/duplicate/package-lock.json +45 -45
- package/install-manifests/duplicate/package.json +1 -1
- package/install-manifests/js/package-lock.json +248 -283
- package/install-manifests/js/package.json +9 -9
- package/install-manifests/python/package-lock.json +4 -4
- package/install-manifests/python/package.json +1 -1
- package/package.json +18 -18
- package/pr-checkmate.schema.json +35 -13
- package/src/config/cspell.json +379 -378
- package/src/config/pr-checkmate.schema.json +35 -13
package/dist/cli.js
CHANGED
|
@@ -88,7 +88,7 @@ void (async () => {
|
|
|
88
88
|
const results = await (0, doctor_1.installProfiles)(process.cwd(), flags.includes('--all'));
|
|
89
89
|
const failed = results.filter(r => r.status === 'failed');
|
|
90
90
|
for (const result of failed) {
|
|
91
|
-
utils_1.logger.error(`[install]: ❌ ${result.
|
|
91
|
+
utils_1.logger.error(`[install]: ❌ ${result.profiles.join(', ') || 'tooling'}: ${result.reason}`);
|
|
92
92
|
}
|
|
93
93
|
process.exit(failed.length > 0 ? 1 : 0);
|
|
94
94
|
}
|
package/dist/config/constants.js
CHANGED
|
@@ -2,9 +2,9 @@ type RuffModule = typeof import('@astral-sh/ruff-wasm-nodejs');
|
|
|
2
2
|
type RuffWorkspace = InstanceType<RuffModule['Workspace']>;
|
|
3
3
|
export declare const PY_GLOBS: string[];
|
|
4
4
|
/**
|
|
5
|
-
* Instantiate a Ruff WASM workspace
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Instantiate a Ruff WASM workspace. Shared by the Ruff lint and format checks so
|
|
6
|
+
* their setup stays identical, and kept as a function (not a module-level
|
|
7
|
+
* singleton) so the WASM module loads only when a check runs.
|
|
8
8
|
*/
|
|
9
9
|
export declare function createRuffWorkspace(): RuffWorkspace;
|
|
10
10
|
export {};
|
|
@@ -4,11 +4,34 @@ exports.PY_GLOBS = void 0;
|
|
|
4
4
|
exports.createRuffWorkspace = createRuffWorkspace;
|
|
5
5
|
exports.PY_GLOBS = ['*.py'];
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* (
|
|
7
|
+
* The rules we lint Python with — pinned here rather than left to Ruff's defaults.
|
|
8
|
+
*
|
|
9
|
+
* `E4`/`E7`/`E9` (import placement, statement style, syntax) plus `F` (pyflakes:
|
|
10
|
+
* undefined names, unused imports, unreachable code) — the set Ruff itself
|
|
11
|
+
* shipped as its default through 0.15.
|
|
12
|
+
*
|
|
13
|
+
* Written down because leaving it implicit made our verdict change under us.
|
|
14
|
+
* Ruff 0.16 added the isort rules (`I`) to its defaults, and a routine dependency
|
|
15
|
+
* bump turned that into a 5- to 48-fold increase in findings across seventeen
|
|
16
|
+
* Python repositories — every one of them `I001`, "import block is un-sorted".
|
|
17
|
+
* Two repositories went from a clean report to a warning on code nobody had
|
|
18
|
+
* touched. Import order is a matter of taste, and a check that fires on every
|
|
19
|
+
* file in a repository is how people learn to ignore the whole report.
|
|
20
|
+
*
|
|
21
|
+
* Adding a rule to this list is a decision with a measurement behind it, which
|
|
22
|
+
* is the point: it is ours to make, not our dependency's to make for us.
|
|
23
|
+
*/
|
|
24
|
+
const SELECTED_RULES = ['E4', 'E7', 'E9', 'F'];
|
|
25
|
+
/**
|
|
26
|
+
* Instantiate a Ruff WASM workspace. Shared by the Ruff lint and format checks so
|
|
27
|
+
* their setup stays identical, and kept as a function (not a module-level
|
|
28
|
+
* singleton) so the WASM module loads only when a check runs.
|
|
10
29
|
*/
|
|
11
30
|
function createRuffWorkspace() {
|
|
12
31
|
const { Workspace, PositionEncoding } = require('@astral-sh/ruff-wasm-nodejs');
|
|
13
|
-
|
|
32
|
+
const settings = {
|
|
33
|
+
...Workspace.defaultSettings(),
|
|
34
|
+
lint: { select: SELECTED_RULES },
|
|
35
|
+
};
|
|
36
|
+
return new Workspace(settings, PositionEncoding.Utf16);
|
|
14
37
|
}
|
|
@@ -48,7 +48,12 @@ exports.coverageCheck = {
|
|
|
48
48
|
: DEFAULT_REPORTS;
|
|
49
49
|
const found = candidates.find(rel => {
|
|
50
50
|
try {
|
|
51
|
-
|
|
51
|
+
// `isFile()` as well as the size: `statSync` reports a non-zero size for
|
|
52
|
+
// a directory too, so a path that happens to be one would read as a
|
|
53
|
+
// report. Non-empty matters because a zero-byte report is what a test
|
|
54
|
+
// run that died halfway leaves behind — the case this check exists for.
|
|
55
|
+
const stat = node_fs_1.default.statSync(node_path_1.default.join(ctx.cwd, rel));
|
|
56
|
+
return stat.isFile() && stat.size > 0;
|
|
52
57
|
}
|
|
53
58
|
catch {
|
|
54
59
|
return false;
|
|
@@ -44,7 +44,7 @@ exports.deadCodeCheck = {
|
|
|
44
44
|
const ignore = compileIgnorePatterns(ctx.config.deadCode?.ignoreFiles ?? constants_1.DEAD_CODE_IGNORE_DEFAULTS);
|
|
45
45
|
// ts-unused-exports prints one `path: sym1, sym2` line per file, preceded
|
|
46
46
|
// by a `N modules with unused exports` summary line we skip.
|
|
47
|
-
const
|
|
47
|
+
const parsed = output
|
|
48
48
|
.split('\n')
|
|
49
49
|
.map(line => line.trim().match(/^(.+\.[cm]?tsx?): (.+)$/))
|
|
50
50
|
.filter((match) => match !== null)
|
|
@@ -54,8 +54,17 @@ exports.deadCodeCheck = {
|
|
|
54
54
|
.split(',')
|
|
55
55
|
.map(sym => sym.trim())
|
|
56
56
|
.filter(Boolean),
|
|
57
|
-
}))
|
|
58
|
-
|
|
57
|
+
}));
|
|
58
|
+
// The tool said something and none of it was a report. That is not a clean
|
|
59
|
+
// repository — it is an answer this check cannot read, and calling it a
|
|
60
|
+
// pass is how Circular Deps stayed green on every repository for its whole
|
|
61
|
+
// existence while grepping for console wording that had changed. Same
|
|
62
|
+
// resolution as Grype Scan: say so rather than invent a verdict.
|
|
63
|
+
if (parsed.length === 0) {
|
|
64
|
+
utils_1.logger.warn(`[Dead Code]: ⚠️ ts-unused-exports produced no parseable output:\n${output}`);
|
|
65
|
+
return (0, types_1.warn)('ts-unused-exports produced no parseable output');
|
|
66
|
+
}
|
|
67
|
+
const entries = parsed.filter(entry => !ignore.some(re => re.test(entry.file)));
|
|
59
68
|
if (entries.length === 0) {
|
|
60
69
|
utils_1.logger.info('[Dead Code]: ✅ No unused exports found');
|
|
61
70
|
return (0, types_1.pass)();
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Check } from '../../types';
|
|
2
|
+
/** Why we could not read a published version. */
|
|
3
|
+
export type QueryFailure = 'absent' | 'unauthenticated' | 'unreadable';
|
|
4
|
+
/**
|
|
5
|
+
* Tell "never published" apart from "we were not let in".
|
|
6
|
+
*
|
|
7
|
+
* Lumped into one sentence they read the same and they are not: one is a fact
|
|
8
|
+
* about the package, the other is about this run's credentials. A skip whose
|
|
9
|
+
* reason is a guess between two causes is the defect of ticket 057, one layer up.
|
|
10
|
+
*
|
|
11
|
+
* A 401 is also evidence in itself, and it points one way: a name nobody ever
|
|
12
|
+
* published answers 404 on the public registry, while 401 comes from a host that
|
|
13
|
+
* has the package and will not show it to a stranger. So the message leads with
|
|
14
|
+
* the token, which is what that situation actually needs.
|
|
15
|
+
*
|
|
16
|
+
* It deliberately does NOT suggest `"private": true` as the way out. That field
|
|
17
|
+
* means "never publish this", and a package behind a private registry is
|
|
18
|
+
* published — advising it would break the publish step of the very repository
|
|
19
|
+
* asking. `packageSize.enabled: false` silences the row and touches nothing
|
|
20
|
+
* else, so that is the opt-out worth naming.
|
|
21
|
+
*/
|
|
22
|
+
export declare function classifyQueryFailure(stderr: string): QueryFailure;
|
|
23
|
+
/**
|
|
24
|
+
* Reports how much the published package grew since the last release.
|
|
25
|
+
*
|
|
26
|
+
* The question this answers is "is it growing without anyone noticing", not "has
|
|
27
|
+
* it crossed a line" — so the comparison is against the previously published
|
|
28
|
+
* version rather than a threshold somebody had to invent. A number a client never
|
|
29
|
+
* configured is a number they will not trust, and a check that fires on every
|
|
30
|
+
* release teaches people to skim the whole report.
|
|
31
|
+
*
|
|
32
|
+
* Advisory by design. Growth is usually earned — a language profile was added, a
|
|
33
|
+
* WASM binary landed — and a red build for it would be wrong. `severity:
|
|
34
|
+
* { "Package Size": "error" }` makes it a gate for a repository that wants one.
|
|
35
|
+
*/
|
|
36
|
+
export declare const packageSizeCheck: Check;
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.packageSizeCheck = void 0;
|
|
7
|
+
exports.classifyQueryFailure = classifyQueryFailure;
|
|
8
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const utils_1 = require("../../../utils");
|
|
11
|
+
const execa_1 = require("../../../utils/execa");
|
|
12
|
+
const types_1 = require("../../types");
|
|
13
|
+
const findings_1 = require("../findings");
|
|
14
|
+
/** Growth that is worth a row in the report, unless the client says otherwise. */
|
|
15
|
+
const DEFAULT_MAX_GROWTH = 20;
|
|
16
|
+
/** How many of the files behind the growth are named. */
|
|
17
|
+
const NAMED_FILES = 5;
|
|
18
|
+
/**
|
|
19
|
+
* Ask npm what a tarball would contain, without producing one.
|
|
20
|
+
*
|
|
21
|
+
* `--dry-run` writes nothing to the working tree — which matters, because a check
|
|
22
|
+
* whose side effect is a `.tgz` beside the source shows up in the client's next
|
|
23
|
+
* commit. Given a package spec instead of nothing, npm answers for the PUBLISHED
|
|
24
|
+
* version, with the same shape and the same per-file detail; that is what makes a
|
|
25
|
+
* real comparison possible rather than a bare number.
|
|
26
|
+
*
|
|
27
|
+
* `--ignore-scripts` is not negotiable. `npm pack` runs `prepare` and `prepack`,
|
|
28
|
+
* and in a client's repository those are arbitrary commands — frequently a full
|
|
29
|
+
* build. A quality check that executes them has stopped being a check and started
|
|
30
|
+
* being a build step with side effects nobody asked for. We are a supply-chain
|
|
31
|
+
* tool; the same rule already governs the install path and the Docker build.
|
|
32
|
+
*
|
|
33
|
+
* @param cwd Repository to run in.
|
|
34
|
+
* @param spec `undefined` for the working tree, or `name@tag` for a release.
|
|
35
|
+
* @param registry Where to look for a published version; the manifest's own
|
|
36
|
+
* `publishConfig.registry` when it names one.
|
|
37
|
+
*/
|
|
38
|
+
async function packManifest(cwd, spec, registry) {
|
|
39
|
+
const args = [
|
|
40
|
+
'pack',
|
|
41
|
+
...(spec ? [spec] : []),
|
|
42
|
+
'--dry-run',
|
|
43
|
+
'--json',
|
|
44
|
+
'--ignore-scripts',
|
|
45
|
+
...(registry ? ['--registry', registry] : []),
|
|
46
|
+
];
|
|
47
|
+
const result = await (0, execa_1.execa)('npm', args, { cwd, reject: false });
|
|
48
|
+
if (result.exitCode !== 0)
|
|
49
|
+
return { failure: classifyQueryFailure(result.stderr ?? '') };
|
|
50
|
+
try {
|
|
51
|
+
const parsed = JSON.parse(result.stdout);
|
|
52
|
+
if (Array.isArray(parsed) && parsed.length > 0) {
|
|
53
|
+
return { manifest: parsed[0] };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// Fall through: npm answered with something that is not a manifest.
|
|
58
|
+
}
|
|
59
|
+
return { failure: 'unreadable' };
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Tell "never published" apart from "we were not let in".
|
|
63
|
+
*
|
|
64
|
+
* Lumped into one sentence they read the same and they are not: one is a fact
|
|
65
|
+
* about the package, the other is about this run's credentials. A skip whose
|
|
66
|
+
* reason is a guess between two causes is the defect of ticket 057, one layer up.
|
|
67
|
+
*
|
|
68
|
+
* A 401 is also evidence in itself, and it points one way: a name nobody ever
|
|
69
|
+
* published answers 404 on the public registry, while 401 comes from a host that
|
|
70
|
+
* has the package and will not show it to a stranger. So the message leads with
|
|
71
|
+
* the token, which is what that situation actually needs.
|
|
72
|
+
*
|
|
73
|
+
* It deliberately does NOT suggest `"private": true` as the way out. That field
|
|
74
|
+
* means "never publish this", and a package behind a private registry is
|
|
75
|
+
* published — advising it would break the publish step of the very repository
|
|
76
|
+
* asking. `packageSize.enabled: false` silences the row and touches nothing
|
|
77
|
+
* else, so that is the opt-out worth naming.
|
|
78
|
+
*/
|
|
79
|
+
function classifyQueryFailure(stderr) {
|
|
80
|
+
if (/\bE?40[13]\b|Unauthorized|Forbidden|authentication token/i.test(stderr)) {
|
|
81
|
+
return 'unauthenticated';
|
|
82
|
+
}
|
|
83
|
+
if (/\bE?404\b|Not found/i.test(stderr))
|
|
84
|
+
return 'absent';
|
|
85
|
+
return 'unreadable';
|
|
86
|
+
}
|
|
87
|
+
/** `1.15 MB`, at the precision a human compares two of these at. */
|
|
88
|
+
function mb(bytes) {
|
|
89
|
+
return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Which files account for the growth, biggest first.
|
|
93
|
+
*
|
|
94
|
+
* A percentage alone is a finding nobody can act on — the same defect as a
|
|
95
|
+
* duplicate-code report with no file names (ticket 078). The published manifest
|
|
96
|
+
* carries per-file sizes, so the answer is a plain diff of the two lists: files
|
|
97
|
+
* that appeared, and files that grew.
|
|
98
|
+
*/
|
|
99
|
+
function growthByFile(before, after) {
|
|
100
|
+
const old = new Map(before.map(f => [f.path, f.size]));
|
|
101
|
+
return after
|
|
102
|
+
.map(f => ({ path: f.path, delta: f.size - (old.get(f.path) ?? 0), isNew: !old.has(f.path) }))
|
|
103
|
+
.filter(f => f.delta > 0)
|
|
104
|
+
.sort((a, b) => b.delta - a.delta)
|
|
105
|
+
.slice(0, NAMED_FILES)
|
|
106
|
+
.map(f => `${f.path} +${mb(f.delta)}${f.isNew ? ' (new)' : ''}`);
|
|
107
|
+
}
|
|
108
|
+
/** What this repository publishes, and where — or `null` if it publishes nothing. */
|
|
109
|
+
function published(cwd) {
|
|
110
|
+
try {
|
|
111
|
+
const pkg = JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(cwd, 'package.json'), 'utf-8'));
|
|
112
|
+
if (pkg.private || !pkg.name)
|
|
113
|
+
return null;
|
|
114
|
+
// `publishConfig.registry` is the manifest's own statement of where this
|
|
115
|
+
// package lives, and npm uses it for `publish` but not for a fetch. Asking
|
|
116
|
+
// the default registry instead reports "no published version" for every
|
|
117
|
+
// package on GitHub Packages or a company mirror — which is not a fact about
|
|
118
|
+
// the package, it is us having asked the wrong host. Measured on a real
|
|
119
|
+
// client: the default registry answered 404 for a package published at
|
|
120
|
+
// version 1.2.15.
|
|
121
|
+
const registry = pkg.publishConfig?.registry;
|
|
122
|
+
return registry ? { name: pkg.name, registry } : { name: pkg.name };
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Reports how much the published package grew since the last release.
|
|
130
|
+
*
|
|
131
|
+
* The question this answers is "is it growing without anyone noticing", not "has
|
|
132
|
+
* it crossed a line" — so the comparison is against the previously published
|
|
133
|
+
* version rather than a threshold somebody had to invent. A number a client never
|
|
134
|
+
* configured is a number they will not trust, and a check that fires on every
|
|
135
|
+
* release teaches people to skim the whole report.
|
|
136
|
+
*
|
|
137
|
+
* Advisory by design. Growth is usually earned — a language profile was added, a
|
|
138
|
+
* WASM binary landed — and a red build for it would be wrong. `severity:
|
|
139
|
+
* { "Package Size": "error" }` makes it a gate for a repository that wants one.
|
|
140
|
+
*/
|
|
141
|
+
exports.packageSizeCheck = {
|
|
142
|
+
name: 'Package Size',
|
|
143
|
+
phase: 'informational',
|
|
144
|
+
weight: 'heavy',
|
|
145
|
+
applies(ctx) {
|
|
146
|
+
if (ctx.config.packageSize?.enabled === false)
|
|
147
|
+
return false;
|
|
148
|
+
// A private package is never published, so there is nothing to compare with
|
|
149
|
+
// and no consumer to pay for the growth.
|
|
150
|
+
return published(ctx.cwd) !== null;
|
|
151
|
+
},
|
|
152
|
+
async run(ctx) {
|
|
153
|
+
const target = published(ctx.cwd);
|
|
154
|
+
if (!target)
|
|
155
|
+
return (0, types_1.skip)('no publishable package.json');
|
|
156
|
+
const tag = ctx.config.packageSize?.compareWith ?? 'latest';
|
|
157
|
+
const maxGrowth = ctx.config.packageSize?.maxGrowthPercent ?? DEFAULT_MAX_GROWTH;
|
|
158
|
+
const spec = `${target.name}@${tag}`;
|
|
159
|
+
const local = await packManifest(ctx.cwd);
|
|
160
|
+
if (!('manifest' in local))
|
|
161
|
+
return (0, types_1.skip)('npm could not describe this package');
|
|
162
|
+
const current = local.manifest;
|
|
163
|
+
// Never a pass: a green row over a comparison that did not happen is the
|
|
164
|
+
// mistake of ticket 072. And never one reason standing in for three — the
|
|
165
|
+
// three are different situations for the reader, and only one of them is a
|
|
166
|
+
// fact about the package rather than about the run.
|
|
167
|
+
const remote = await packManifest(ctx.cwd, spec, target.registry);
|
|
168
|
+
if (!('manifest' in remote)) {
|
|
169
|
+
// The fact, and nothing past it. The remedy for a private registry is three
|
|
170
|
+
// lines in a workflow — `setup-node` with `registry-url`, a scope, and
|
|
171
|
+
// `NODE_AUTH_TOKEN`, because npm reads the token through a reference in
|
|
172
|
+
// `.npmrc` — and instructions that do not fit cannot be followed. They
|
|
173
|
+
// belong on the check's page, which the report already links to.
|
|
174
|
+
//
|
|
175
|
+
// The package name is left out on purpose: the reader owns it. The host is
|
|
176
|
+
// not, because it is the one part they cannot infer — and the difference
|
|
177
|
+
// between "we asked the wrong registry" and "we were refused" is exactly
|
|
178
|
+
// what this row got wrong before.
|
|
179
|
+
// Named only when we know it. Without `publishConfig.registry` npm used
|
|
180
|
+
// whatever the client's own config points at, and we do not know what that
|
|
181
|
+
// was — "at the registry" would be filler dressed up as information.
|
|
182
|
+
const host = target.registry ? new URL(target.registry).host : null;
|
|
183
|
+
const at = host ? ` at ${host}` : '';
|
|
184
|
+
const reason = {
|
|
185
|
+
absent: `nothing published under this name${at} yet`,
|
|
186
|
+
unauthenticated: `no credentials for ${host ?? 'the registry'}`,
|
|
187
|
+
unreadable: `could not read the published version${at}`,
|
|
188
|
+
}[remote.failure];
|
|
189
|
+
return (0, types_1.skip)(reason);
|
|
190
|
+
}
|
|
191
|
+
const previous = remote.manifest;
|
|
192
|
+
const delta = current.unpackedSize - previous.unpackedSize;
|
|
193
|
+
const growth = previous.unpackedSize > 0 ? (delta / previous.unpackedSize) * 100 : 0;
|
|
194
|
+
const summary = `${mb(previous.unpackedSize)} → ${mb(current.unpackedSize)} ` +
|
|
195
|
+
`(${growth >= 0 ? '+' : ''}${growth.toFixed(1)}%) vs ${previous.version}`;
|
|
196
|
+
if (growth <= maxGrowth) {
|
|
197
|
+
utils_1.logger.info(`[Package Size]: ✅ ${summary}`);
|
|
198
|
+
return (0, types_1.pass)(summary);
|
|
199
|
+
}
|
|
200
|
+
(0, findings_1.logSample)('Package Size', `${summary}:`, growthByFile(previous.files, current.files));
|
|
201
|
+
return (0, types_1.warn)(summary);
|
|
202
|
+
},
|
|
203
|
+
};
|
package/dist/core/commands.js
CHANGED
|
@@ -27,7 +27,26 @@ export declare function expandHome(dir: string, home?: string): string;
|
|
|
27
27
|
* @param configured `install.cacheDir`, if the client set one.
|
|
28
28
|
*/
|
|
29
29
|
export declare function resolveCacheDir(version: string, configured?: string, env?: NodeJS.ProcessEnv): CacheLocation | null;
|
|
30
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* Where fetched tooling is installed — one tree for the whole run, not one per profile.
|
|
32
|
+
*
|
|
33
|
+
* A directory per profile left npm nothing to deduplicate, and the profiles
|
|
34
|
+
* overlap heavily: `commitlint` and `js` share 30 MB, so two trees fetched both
|
|
35
|
+
* copies. Measured on a JavaScript repository, per-profile directories came to
|
|
36
|
+
* 267 MB where a single merged tree costs 213 (ticket 087).
|
|
37
|
+
*
|
|
38
|
+
* The name is deliberately not derived from which profiles are inside. What is
|
|
39
|
+
* installed is recorded in the directory's own stamp and checked as a superset,
|
|
40
|
+
* so a cache warmed by `install --all` serves every narrower run — a path keyed
|
|
41
|
+
* by the profile set would miss on the first one.
|
|
42
|
+
*/
|
|
43
|
+
export declare function toolingDir(cacheDir: string): string;
|
|
44
|
+
/**
|
|
45
|
+
* Where a single profile's tooling used to be installed.
|
|
46
|
+
*
|
|
47
|
+
* Kept only so `cache prune` can still recognise and remove the directories left
|
|
48
|
+
* by versions that used the per-profile layout.
|
|
49
|
+
*/
|
|
31
50
|
export declare function profileDir(cacheDir: string, profile: string): string;
|
|
32
51
|
/**
|
|
33
52
|
* Profile directories left behind by other versions of pr-checkmate.
|
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.expandHome = expandHome;
|
|
7
7
|
exports.resolveCacheDir = resolveCacheDir;
|
|
8
|
+
exports.toolingDir = toolingDir;
|
|
8
9
|
exports.profileDir = profileDir;
|
|
9
10
|
exports.stalePaths = stalePaths;
|
|
10
11
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
@@ -102,7 +103,28 @@ function resolveCacheDir(version, configured, env = process.env) {
|
|
|
102
103
|
}
|
|
103
104
|
return null;
|
|
104
105
|
}
|
|
105
|
-
/**
|
|
106
|
+
/**
|
|
107
|
+
* Where fetched tooling is installed — one tree for the whole run, not one per profile.
|
|
108
|
+
*
|
|
109
|
+
* A directory per profile left npm nothing to deduplicate, and the profiles
|
|
110
|
+
* overlap heavily: `commitlint` and `js` share 30 MB, so two trees fetched both
|
|
111
|
+
* copies. Measured on a JavaScript repository, per-profile directories came to
|
|
112
|
+
* 267 MB where a single merged tree costs 213 (ticket 087).
|
|
113
|
+
*
|
|
114
|
+
* The name is deliberately not derived from which profiles are inside. What is
|
|
115
|
+
* installed is recorded in the directory's own stamp and checked as a superset,
|
|
116
|
+
* so a cache warmed by `install --all` serves every narrower run — a path keyed
|
|
117
|
+
* by the profile set would miss on the first one.
|
|
118
|
+
*/
|
|
119
|
+
function toolingDir(cacheDir) {
|
|
120
|
+
return node_path_1.default.join(cacheDir, 'tooling');
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Where a single profile's tooling used to be installed.
|
|
124
|
+
*
|
|
125
|
+
* Kept only so `cache prune` can still recognise and remove the directories left
|
|
126
|
+
* by versions that used the per-profile layout.
|
|
127
|
+
*/
|
|
106
128
|
function profileDir(cacheDir, profile) {
|
|
107
129
|
return node_path_1.default.join(cacheDir, profile);
|
|
108
130
|
}
|
|
@@ -9,7 +9,6 @@ exports.installProfiles = installProfiles;
|
|
|
9
9
|
exports.runDoctor = runDoctor;
|
|
10
10
|
exports.pruneCache = pruneCache;
|
|
11
11
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
12
|
-
const node_path_1 = __importDefault(require("node:path"));
|
|
13
12
|
const loader_1 = require("../../config/loader");
|
|
14
13
|
const detect_language_1 = require("../../utils/detect-language");
|
|
15
14
|
const logger_1 = require("../../utils/logger");
|
|
@@ -32,11 +31,31 @@ async function planInstall(cwd = process.cwd(), usage) {
|
|
|
32
31
|
const location = (0, cache_1.resolveCacheDir)((0, package_version_1.packageVersion)(), config.install?.cacheDir);
|
|
33
32
|
return { decisions: (0, profiles_1.planProfiles)(detected, config), cacheDir: location?.dir ?? null };
|
|
34
33
|
}
|
|
35
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* Which profiles are usable right now, and why — fetched, or shipped with us.
|
|
36
|
+
*
|
|
37
|
+
* The cache is only half the answer, and reporting only that half was a lie in
|
|
38
|
+
* the client's favour's opposite direction: most profiles ship inside the
|
|
39
|
+
* package, are never fetched, and were listed as "not installed yet" on a machine
|
|
40
|
+
* where they worked perfectly. `doctor` exists to answer "why did my typecheck
|
|
41
|
+
* not run", so telling someone seven profiles are missing when six of them are
|
|
42
|
+
* present sends them to debug nothing.
|
|
43
|
+
*
|
|
44
|
+
* The cached half is read from the tooling directory's own stamp rather than
|
|
45
|
+
* inferred from paths: there is one tree for everything fetched, so what went
|
|
46
|
+
* into it is recorded rather than visible in the layout (ticket 087).
|
|
47
|
+
*/
|
|
36
48
|
function installedProfiles(cacheDir) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
49
|
+
const present = new Map();
|
|
50
|
+
for (const profile of profiles_1.PROFILES) {
|
|
51
|
+
if ((0, prepare_1.profileSatisfiedLocally)(profile))
|
|
52
|
+
present.set(profile.name, 'ships with the package');
|
|
53
|
+
}
|
|
54
|
+
if (cacheDir) {
|
|
55
|
+
for (const name of (0, ensure_1.cachedProfiles)(cacheDir))
|
|
56
|
+
present.set(name, 'fetched');
|
|
57
|
+
}
|
|
58
|
+
return present;
|
|
40
59
|
}
|
|
41
60
|
/**
|
|
42
61
|
* Fetch every profile this repository needs.
|
|
@@ -67,14 +86,15 @@ async function installProfiles(cwd = process.cwd(), all = false) {
|
|
|
67
86
|
logger_1.logger.info(`[install]: profile "${name}" skipped — install.deny lists ${blocked.join(', ')}`);
|
|
68
87
|
return false;
|
|
69
88
|
});
|
|
70
|
-
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
89
|
+
// One install for the whole set. Profiles that share hundreds of packages used
|
|
90
|
+
// to be fetched into a tree each, which fought over npm's cache and downloaded
|
|
91
|
+
// the shared half once per profile (ticket 087).
|
|
92
|
+
const result = await (0, ensure_1.ensureTooling)(wanted, {
|
|
93
|
+
cwd,
|
|
94
|
+
version: (0, package_version_1.packageVersion)(),
|
|
95
|
+
install,
|
|
96
|
+
});
|
|
97
|
+
return [result];
|
|
78
98
|
}
|
|
79
99
|
/**
|
|
80
100
|
* Explain what this repository needs, what it has, and what is missing — and why.
|
|
@@ -108,8 +128,13 @@ async function runDoctor(cwd = process.cwd()) {
|
|
|
108
128
|
lines.push('');
|
|
109
129
|
lines.push(' Profiles');
|
|
110
130
|
for (const decision of decisions) {
|
|
111
|
-
const
|
|
112
|
-
|
|
131
|
+
const source = present.get(decision.profile);
|
|
132
|
+
const state = decision.required ? (source ? '✅' : '⬇️ ') : '—';
|
|
133
|
+
// Where a present profile came from, because "it works" and "it works
|
|
134
|
+
// because it was downloaded onto this runner" are different situations for
|
|
135
|
+
// anyone debugging a cache or an air-gapped build.
|
|
136
|
+
const how = decision.required && source ? ` (${source})` : '';
|
|
137
|
+
lines.push(` ${state} ${decision.profile.padEnd(12)} ${decision.reason}${how}`);
|
|
113
138
|
}
|
|
114
139
|
const missing = decisions.filter(d => d.required && !present.has(d.profile));
|
|
115
140
|
if (missing.length > 0) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { InstallConfig } from '../../interfaces';
|
|
2
2
|
import type { ProfileName } from './profiles';
|
|
3
3
|
/**
|
|
4
|
-
* What happened to
|
|
4
|
+
* What happened to the tooling cache, in the words the report and `doctor` use.
|
|
5
5
|
*
|
|
6
6
|
* `skipped` and `failed` are deliberately distinct. Skipped means we chose not
|
|
7
7
|
* to fetch — offline, `strategy: "manual"`, an explicit exclusion — and the
|
|
@@ -9,12 +9,13 @@ import type { ProfileName } from './profiles';
|
|
|
9
9
|
* case that has to reach the report: ticket 057 is the standing reminder that a
|
|
10
10
|
* check quietly vanishing is worse than a check failing loudly.
|
|
11
11
|
*/
|
|
12
|
-
|
|
12
|
+
type EnsureStatus = 'cached' | 'installed' | 'skipped' | 'failed';
|
|
13
13
|
export interface EnsureResult {
|
|
14
|
-
profile: ProfileName;
|
|
15
14
|
status: EnsureStatus;
|
|
16
15
|
reason: string;
|
|
17
16
|
dir?: string;
|
|
17
|
+
/** Profiles the directory holds once this call is done. */
|
|
18
|
+
profiles: ProfileName[];
|
|
18
19
|
durationMs?: number;
|
|
19
20
|
}
|
|
20
21
|
/** Whether the fetch succeeded, and what to quote in the report when it did not. */
|
|
@@ -22,7 +23,7 @@ export interface InstallOutcome {
|
|
|
22
23
|
ok: boolean;
|
|
23
24
|
detail?: string;
|
|
24
25
|
}
|
|
25
|
-
/** Options `
|
|
26
|
+
/** Options `ensureTooling` needs that do not come from the config file. */
|
|
26
27
|
export interface EnsureOptions {
|
|
27
28
|
/** Repository under review — the cwd npm runs in, so the client's `.npmrc` applies. */
|
|
28
29
|
cwd: string;
|
|
@@ -42,19 +43,35 @@ export interface EnsureOptions {
|
|
|
42
43
|
/** Where the shipped manifests live inside the installed package. */
|
|
43
44
|
export declare function manifestDir(profile: string): string;
|
|
44
45
|
/**
|
|
45
|
-
*
|
|
46
|
+
* Which profiles the tooling cache holds, as recorded by the install that filled it.
|
|
46
47
|
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
48
|
+
* The one way to ask, so the stamp's format has a single owner. There is one tree
|
|
49
|
+
* for all fetched tooling now, so what is in it is written down rather than
|
|
50
|
+
* readable from the directory layout.
|
|
51
|
+
*/
|
|
52
|
+
export declare function cachedProfiles(cacheDir: string): ProfileName[];
|
|
53
|
+
/**
|
|
54
|
+
* Make a set of profiles' tooling available, fetching only what is not already there.
|
|
55
|
+
*
|
|
56
|
+
* Idempotent by design: this is the single entry point for both the `install`
|
|
57
|
+
* command and the lazy path taken when it was never run (`npm ci
|
|
49
58
|
* --ignore-scripts`, a `node_modules` restored from a CI cache). Two code paths
|
|
50
59
|
* for the same job is how the two drift apart and how one of them silently stops
|
|
51
60
|
* working.
|
|
52
61
|
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
62
|
+
* Everything needed goes into **one** tree. Profiles overlap heavily — `js` and
|
|
63
|
+
* `commitlint` share 30 MB — and a directory each left npm nothing to
|
|
64
|
+
* deduplicate: 267 MB against the 211 one merged tree costs for the same set
|
|
65
|
+
* (ticket 087). The set only ever grows: a run needing a profile the directory
|
|
66
|
+
* lacks reinstalls the union, so nothing already fetched is lost.
|
|
67
|
+
*
|
|
68
|
+
* The install itself is a plain `npm ci` against a manifest merged from the ones
|
|
69
|
+
* we generated from our own lockfile, run from the client's repository so their
|
|
70
|
+
* `.npmrc`, registry and proxy settings apply unchanged. `--ignore-scripts` is
|
|
71
|
+
* not negotiable: we are a supply-chain tool, and running arbitrary install hooks
|
|
72
|
+
* from a security check would be indefensible. Integrity is npm's own, over the
|
|
73
|
+
* whole transitive tree — a tampered tarball aborts with `EINTEGRITY` rather than
|
|
74
|
+
* being installed.
|
|
59
75
|
*/
|
|
60
|
-
export declare function
|
|
76
|
+
export declare function ensureTooling(wanted: ProfileName[], opts: EnsureOptions): Promise<EnsureResult>;
|
|
77
|
+
export {};
|