pr-checkmate 3.0.0 → 3.0.1
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 +15 -0
- package/dist/core/checks/quality/package-size.js +130 -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,15 @@
|
|
|
1
|
+
import { Check } from '../../types';
|
|
2
|
+
/**
|
|
3
|
+
* Reports how much the published package grew since the last release.
|
|
4
|
+
*
|
|
5
|
+
* The question this answers is "is it growing without anyone noticing", not "has
|
|
6
|
+
* it crossed a line" — so the comparison is against the previously published
|
|
7
|
+
* version rather than a threshold somebody had to invent. A number a client never
|
|
8
|
+
* configured is a number they will not trust, and a check that fires on every
|
|
9
|
+
* release teaches people to skim the whole report.
|
|
10
|
+
*
|
|
11
|
+
* Advisory by design. Growth is usually earned — a language profile was added, a
|
|
12
|
+
* WASM binary landed — and a red build for it would be wrong. `severity:
|
|
13
|
+
* { "Package Size": "error" }` makes it a gate for a repository that wants one.
|
|
14
|
+
*/
|
|
15
|
+
export declare const packageSizeCheck: Check;
|
|
@@ -0,0 +1,130 @@
|
|
|
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
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const utils_1 = require("../../../utils");
|
|
10
|
+
const execa_1 = require("../../../utils/execa");
|
|
11
|
+
const types_1 = require("../../types");
|
|
12
|
+
const findings_1 = require("../findings");
|
|
13
|
+
/** Growth that is worth a row in the report, unless the client says otherwise. */
|
|
14
|
+
const DEFAULT_MAX_GROWTH = 20;
|
|
15
|
+
/** How many of the files behind the growth are named. */
|
|
16
|
+
const NAMED_FILES = 5;
|
|
17
|
+
/**
|
|
18
|
+
* Ask npm what a tarball would contain, without producing one.
|
|
19
|
+
*
|
|
20
|
+
* `--dry-run` writes nothing to the working tree — which matters, because a check
|
|
21
|
+
* whose side effect is a `.tgz` beside the source shows up in the client's next
|
|
22
|
+
* commit. Given a package spec instead of nothing, npm answers for the PUBLISHED
|
|
23
|
+
* version, with the same shape and the same per-file detail; that is what makes a
|
|
24
|
+
* real comparison possible rather than a bare number.
|
|
25
|
+
*
|
|
26
|
+
* `--ignore-scripts` is not negotiable. `npm pack` runs `prepare` and `prepack`,
|
|
27
|
+
* and in a client's repository those are arbitrary commands — frequently a full
|
|
28
|
+
* build. A quality check that executes them has stopped being a check and started
|
|
29
|
+
* being a build step with side effects nobody asked for. We are a supply-chain
|
|
30
|
+
* tool; the same rule already governs the install path and the Docker build.
|
|
31
|
+
*
|
|
32
|
+
* @param cwd Repository to run in.
|
|
33
|
+
* @param spec `undefined` for the working tree, or `name@tag` for a release.
|
|
34
|
+
*/
|
|
35
|
+
async function packManifest(cwd, spec) {
|
|
36
|
+
const args = ['pack', ...(spec ? [spec] : []), '--dry-run', '--json', '--ignore-scripts'];
|
|
37
|
+
const result = await (0, execa_1.execa)('npm', args, { cwd, reject: false });
|
|
38
|
+
if (result.exitCode !== 0)
|
|
39
|
+
return null;
|
|
40
|
+
try {
|
|
41
|
+
const parsed = JSON.parse(result.stdout);
|
|
42
|
+
return Array.isArray(parsed) && parsed.length > 0 ? parsed[0] : null;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** `1.15 MB`, at the precision a human compares two of these at. */
|
|
49
|
+
function mb(bytes) {
|
|
50
|
+
return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Which files account for the growth, biggest first.
|
|
54
|
+
*
|
|
55
|
+
* A percentage alone is a finding nobody can act on — the same defect as a
|
|
56
|
+
* duplicate-code report with no file names (ticket 078). The published manifest
|
|
57
|
+
* carries per-file sizes, so the answer is a plain diff of the two lists: files
|
|
58
|
+
* that appeared, and files that grew.
|
|
59
|
+
*/
|
|
60
|
+
function growthByFile(before, after) {
|
|
61
|
+
const old = new Map(before.map(f => [f.path, f.size]));
|
|
62
|
+
return after
|
|
63
|
+
.map(f => ({ path: f.path, delta: f.size - (old.get(f.path) ?? 0), isNew: !old.has(f.path) }))
|
|
64
|
+
.filter(f => f.delta > 0)
|
|
65
|
+
.sort((a, b) => b.delta - a.delta)
|
|
66
|
+
.slice(0, NAMED_FILES)
|
|
67
|
+
.map(f => `${f.path} +${mb(f.delta)}${f.isNew ? ' (new)' : ''}`);
|
|
68
|
+
}
|
|
69
|
+
/** Whether this repository publishes anything to compare. */
|
|
70
|
+
function publishedName(cwd) {
|
|
71
|
+
try {
|
|
72
|
+
const pkg = JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(cwd, 'package.json'), 'utf-8'));
|
|
73
|
+
return pkg.private || !pkg.name ? null : pkg.name;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Reports how much the published package grew since the last release.
|
|
81
|
+
*
|
|
82
|
+
* The question this answers is "is it growing without anyone noticing", not "has
|
|
83
|
+
* it crossed a line" — so the comparison is against the previously published
|
|
84
|
+
* version rather than a threshold somebody had to invent. A number a client never
|
|
85
|
+
* configured is a number they will not trust, and a check that fires on every
|
|
86
|
+
* release teaches people to skim the whole report.
|
|
87
|
+
*
|
|
88
|
+
* Advisory by design. Growth is usually earned — a language profile was added, a
|
|
89
|
+
* WASM binary landed — and a red build for it would be wrong. `severity:
|
|
90
|
+
* { "Package Size": "error" }` makes it a gate for a repository that wants one.
|
|
91
|
+
*/
|
|
92
|
+
exports.packageSizeCheck = {
|
|
93
|
+
name: 'Package Size',
|
|
94
|
+
phase: 'informational',
|
|
95
|
+
weight: 'heavy',
|
|
96
|
+
applies(ctx) {
|
|
97
|
+
if (ctx.config.packageSize?.enabled === false)
|
|
98
|
+
return false;
|
|
99
|
+
// A private package is never published, so there is nothing to compare with
|
|
100
|
+
// and no consumer to pay for the growth.
|
|
101
|
+
return publishedName(ctx.cwd) !== null;
|
|
102
|
+
},
|
|
103
|
+
async run(ctx) {
|
|
104
|
+
const name = publishedName(ctx.cwd);
|
|
105
|
+
if (!name)
|
|
106
|
+
return (0, types_1.skip)('no publishable package.json');
|
|
107
|
+
const tag = ctx.config.packageSize?.compareWith ?? 'latest';
|
|
108
|
+
const maxGrowth = ctx.config.packageSize?.maxGrowthPercent ?? DEFAULT_MAX_GROWTH;
|
|
109
|
+
const current = await packManifest(ctx.cwd);
|
|
110
|
+
if (!current)
|
|
111
|
+
return (0, types_1.skip)('npm could not describe this package');
|
|
112
|
+
// Registry unreachable, or nothing published yet. Both are "we could not
|
|
113
|
+
// look", never "nothing changed" — a pass here would be a green row over a
|
|
114
|
+
// comparison that never happened (ticket 072).
|
|
115
|
+
const published = await packManifest(ctx.cwd, `${name}@${tag}`);
|
|
116
|
+
if (!published) {
|
|
117
|
+
return (0, types_1.skip)(`no published ${name}@${tag} to compare with (or registry unreachable)`);
|
|
118
|
+
}
|
|
119
|
+
const delta = current.unpackedSize - published.unpackedSize;
|
|
120
|
+
const growth = published.unpackedSize > 0 ? (delta / published.unpackedSize) * 100 : 0;
|
|
121
|
+
const summary = `${mb(published.unpackedSize)} → ${mb(current.unpackedSize)} ` +
|
|
122
|
+
`(${growth >= 0 ? '+' : ''}${growth.toFixed(1)}%) vs ${published.version}`;
|
|
123
|
+
if (growth <= maxGrowth) {
|
|
124
|
+
utils_1.logger.info(`[Package Size]: ✅ ${summary}`);
|
|
125
|
+
return (0, types_1.pass)(summary);
|
|
126
|
+
}
|
|
127
|
+
(0, findings_1.logSample)('Package Size', `${summary}:`, growthByFile(published.files, current.files));
|
|
128
|
+
return (0, types_1.warn)(summary);
|
|
129
|
+
},
|
|
130
|
+
};
|
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 {};
|