pr-checkmate 2.1.2 → 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 +63 -268
- package/cspell.json +470 -447
- package/dist/cli.js +1 -1
- package/dist/config/constants.d.ts +6 -1
- package/dist/config/constants.js +9 -7
- package/dist/config/index.d.ts +23 -11
- package/dist/config/index.js +27 -33
- package/dist/core/checks/dependencies/banned-imports.js +4 -1
- package/dist/core/checks/dependencies/circular-deps.js +14 -5
- package/dist/core/checks/dependencies/dependency-check.js +1 -1
- package/dist/core/checks/dependencies/grype-scan.js +2 -1
- package/dist/core/checks/dependencies/vuln-scan.js +2 -1
- package/dist/core/checks/findings.d.ts +46 -2
- package/dist/core/checks/findings.js +140 -2
- package/dist/core/checks/git/custom-rules.js +2 -0
- package/dist/core/checks/git/diff-smell.js +6 -1
- package/dist/core/checks/git/leftover-debug.js +8 -2
- package/dist/core/checks/git/lockfile-drift.js +6 -1
- package/dist/core/checks/git/merge-conflict.js +1 -1
- package/dist/core/checks/git/missing-tests.js +1 -1
- package/dist/core/checks/git/todo-fixme.js +10 -2
- package/dist/core/checks/languages/cpp-format.js +19 -42
- package/dist/core/checks/languages/external-checks.d.ts +77 -0
- package/dist/core/checks/languages/external-checks.js +200 -0
- package/dist/core/checks/languages/external-runner.d.ts +119 -0
- package/dist/core/checks/languages/external-runner.js +143 -0
- package/dist/core/checks/languages/go-format.d.ts +4 -4
- package/dist/core/checks/languages/go-format.js +151 -30
- package/dist/core/checks/languages/lint.js +19 -19
- package/dist/core/checks/languages/prettier.js +2 -7
- package/dist/core/checks/languages/python-typecheck.d.ts +10 -5
- package/dist/core/checks/languages/python-typecheck.js +161 -37
- 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/languages/typecheck.js +10 -8
- package/dist/core/checks/pr/commitlint.js +7 -1
- package/dist/core/checks/quality/case-collision.js +6 -4
- package/dist/core/checks/quality/config-validation.d.ts +0 -7
- package/dist/core/checks/quality/config-validation.js +30 -0
- package/dist/core/checks/quality/coverage.js +6 -1
- package/dist/core/checks/quality/dead-code.js +12 -3
- package/dist/core/checks/quality/duplicate-check.js +54 -27
- package/dist/core/checks/quality/license-header.js +7 -4
- package/dist/core/checks/quality/markdown-lint.js +1 -6
- package/dist/core/checks/quality/package-size.d.ts +15 -0
- package/dist/core/checks/quality/package-size.js +130 -0
- package/dist/core/checks/quality/spellcheck.js +34 -5
- package/dist/core/checks/quality/symlinks.js +6 -4
- package/dist/core/checks/security/diff-security.js +26 -18
- package/dist/core/checks/security/dockerfile-security.js +7 -1
- package/dist/core/checks/security/migration-safety.js +32 -4
- package/dist/core/checks/security/security.d.ts +56 -0
- package/dist/core/checks/security/security.js +135 -11
- package/dist/core/checks/security/sensitive-file-guard.js +40 -6
- package/dist/core/checks/security/workflow-security.js +7 -1
- package/dist/core/checks/tool-install.d.ts +102 -0
- package/dist/core/checks/tool-install.js +69 -0
- package/dist/core/commands.js +1 -0
- package/dist/core/context.js +9 -1
- package/dist/core/install/cache.d.ts +20 -1
- package/dist/core/install/cache.js +65 -12
- package/dist/core/install/doctor.js +49 -15
- package/dist/core/install/ensure.d.ts +31 -14
- package/dist/core/install/ensure.js +165 -61
- package/dist/core/install/lock.d.ts +2 -1
- package/dist/core/install/merge.d.ts +47 -0
- package/dist/core/install/merge.js +91 -0
- package/dist/core/install/prepare.d.ts +76 -0
- package/dist/core/install/prepare.js +147 -0
- package/dist/core/install/profiles.d.ts +17 -3
- package/dist/core/install/profiles.js +27 -0
- package/dist/core/install/reuse.d.ts +41 -0
- package/dist/core/install/reuse.js +107 -0
- package/dist/core/registry.js +12 -18
- package/dist/core/run-checks.js +74 -3
- package/dist/core/targets.d.ts +116 -5
- package/dist/core/targets.js +101 -16
- package/dist/core/types.d.ts +8 -2
- package/dist/init.js +21 -1
- package/dist/interfaces/index.d.ts +6 -0
- package/dist/utils/resolve-bin.d.ts +56 -6
- package/dist/utils/resolve-bin.js +136 -26
- package/gitleaks.toml +14 -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/go/package-lock.json +24 -0
- package/install-manifests/go/package.json +8 -0
- 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/install-manifests/pytypes/package-lock.json +45 -0
- package/install-manifests/pytypes/package.json +8 -0
- package/package.json +21 -19
- package/pr-checkmate.schema.json +41 -13
- package/src/config/cspell.json +470 -447
- package/src/config/pr-checkmate.schema.json +41 -13
- package/dist/core/checks/languages/csharp-format.d.ts +0 -13
- package/dist/core/checks/languages/csharp-format.js +0 -52
- package/dist/core/checks/languages/go-lint.d.ts +0 -8
- package/dist/core/checks/languages/go-lint.js +0 -48
- package/dist/core/checks/languages/kotlin-lint.d.ts +0 -2
- package/dist/core/checks/languages/kotlin-lint.js +0 -42
- package/dist/core/checks/languages/php-format.d.ts +0 -9
- package/dist/core/checks/languages/php-format.js +0 -49
- package/dist/core/checks/languages/ruby-lint.d.ts +0 -8
- package/dist/core/checks/languages/ruby-lint.js +0 -50
- package/dist/core/checks/languages/rust-format.d.ts +0 -9
- package/dist/core/checks/languages/rust-format.js +0 -45
- package/dist/core/checks/languages/rust-lint.d.ts +0 -8
- package/dist/core/checks/languages/rust-lint.js +0 -50
- package/dist/core/checks/languages/shellcheck.d.ts +0 -8
- package/dist/core/checks/languages/shellcheck.js +0 -55
- package/dist/core/checks/languages/swift-lint.d.ts +0 -2
- package/dist/core/checks/languages/swift-lint.js +0 -46
package/dist/core/targets.d.ts
CHANGED
|
@@ -1,4 +1,31 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type CheckContext, type CheckOutcome } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Drop files that live under any excluded directory (client `ignoreDirs` or the
|
|
4
|
+
* curated default), matched on any path segment so nesting depth doesn't matter.
|
|
5
|
+
* Applied to every check's file list so build/tooling/report dirs are never
|
|
6
|
+
* scanned, even when committed.
|
|
7
|
+
*
|
|
8
|
+
* `keepDirs` un-ignores specific segments for a single check — e.g. YAML lint
|
|
9
|
+
* keeps the CI dirs (`.github`, `.gitlab`, `.circleci`), which are ignored
|
|
10
|
+
* globally but hold the workflow files a YAML linter most needs to see.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Drop files the client excluded by path.
|
|
14
|
+
*
|
|
15
|
+
* Separate from `ignoreDirs`, which matches a directory *name* on any segment:
|
|
16
|
+
* that cannot express "this one file" and cannot express "this one path" —
|
|
17
|
+
* `ignoreDirs: ["generated"]` silences every `generated/` in the repository, not
|
|
18
|
+
* the one that is actually generated. Globs answer both, and because this sits
|
|
19
|
+
* in the one function every file-scanning check calls, a client writes the rule
|
|
20
|
+
* once instead of hunting for the right key on each check.
|
|
21
|
+
*
|
|
22
|
+
* Exported because three checks build their file list themselves and cannot go
|
|
23
|
+
* through the resolvers above — and `excludePaths` is documented as "the only
|
|
24
|
+
* exclusion that reaches every check", which was not true of them (ticket 080).
|
|
25
|
+
* Each has a real reason to skip `sourcePath` and `ignoreDirs`; none has one to
|
|
26
|
+
* ignore a client saying "do not look here".
|
|
27
|
+
*/
|
|
28
|
+
export declare function excludeClientPaths(files: string[], cwd: string): string[];
|
|
2
29
|
/**
|
|
3
30
|
* Lists the files a language check should operate on, matched by pathspec `globs`
|
|
4
31
|
* (e.g. `['*.py']`). Uses the PR diff range when available (delta mode), else
|
|
@@ -20,10 +47,94 @@ export declare function resolveTargetFiles(ctx: CheckContext, globs: string[], o
|
|
|
20
47
|
* fallback while gaining delta support on every platform via `ctx.baseSha`.
|
|
21
48
|
*/
|
|
22
49
|
export declare function resolveScopedTargetFiles(ctx: CheckContext, globs: string[], sourcePaths: string[]): Promise<string[] | null>;
|
|
50
|
+
/**
|
|
51
|
+
* The files to work on, or the outcome to return instead of working.
|
|
52
|
+
*
|
|
53
|
+
* One value rather than two, because the two-value shape is what kept going
|
|
54
|
+
* wrong. Every file-scanning check ends up writing the same triple —
|
|
55
|
+
*
|
|
56
|
+
* ```typescript
|
|
57
|
+
* const targets = await resolveTargetFiles(ctx, globs);
|
|
58
|
+
* if (targets === null) return skip('git unavailable');
|
|
59
|
+
* if (targets.length === 0) return pass('no Kotlin files changed');
|
|
60
|
+
* ```
|
|
61
|
+
*
|
|
62
|
+
* — and it was written out seventeen times, with nineteen exact copies of the
|
|
63
|
+
* middle line and seven copies of the comment explaining why the two cases are
|
|
64
|
+
* not the same (ticket 079). The point of collapsing it is not the four lines. It
|
|
65
|
+
* is that `null` means "git could not tell us" and `[]` means "git did and there
|
|
66
|
+
* is nothing", they are opposite answers, and a check that conflates them reports
|
|
67
|
+
* a clean pass over a tree it never opened — which is ticket 072, and before that
|
|
68
|
+
* ticket 067, and both were found in security checks, where it cost the most.
|
|
69
|
+
*
|
|
70
|
+
* Written this way round — an array or an outcome, not a nullable array — so the
|
|
71
|
+
* caller's one line is `if (!Array.isArray(targets)) return targets;` and there
|
|
72
|
+
* is no second case left to forget.
|
|
73
|
+
*
|
|
74
|
+
* Not exported: it is the return type of {@link resolveTargets} and every caller
|
|
75
|
+
* gets it by inference, so exporting it would only widen the public surface and
|
|
76
|
+
* show up as an unused export in our own Dead Code check.
|
|
77
|
+
*/
|
|
78
|
+
type TargetsOrOutcome = string[] | CheckOutcome;
|
|
79
|
+
/** How a check wants an empty file list phrased, and at which status. */
|
|
80
|
+
type EmptyResult = {
|
|
81
|
+
pass: string;
|
|
82
|
+
} | {
|
|
83
|
+
skip: string;
|
|
84
|
+
};
|
|
85
|
+
/** Options for {@link resolveTargets} beyond the globs themselves. */
|
|
86
|
+
interface ResolveOptions {
|
|
87
|
+
/**
|
|
88
|
+
* What no matching files means to this check, and in what words.
|
|
89
|
+
*
|
|
90
|
+
* Not defaulted, and that is deliberate: the two halves of the corpus disagree.
|
|
91
|
+
* A language check has genuinely nothing to do when its language is not in the
|
|
92
|
+
* diff, so `{ pass: 'no Kotlin files changed' }`. A whole-tree check that finds
|
|
93
|
+
* no files at all has not reviewed the repository, so `{ skip: 'no tracked
|
|
94
|
+
* files' }`. Picking one for the caller would make half of them wrong.
|
|
95
|
+
*/
|
|
96
|
+
onEmpty: EmptyResult;
|
|
97
|
+
/** Walk every tracked file instead of the PR diff range. */
|
|
98
|
+
wholeTree?: boolean;
|
|
99
|
+
/** Restrict the result to the configured `sourcePath`(s). */
|
|
100
|
+
scoped?: boolean;
|
|
101
|
+
/** Un-ignore specific directory segments, as in {@link resolveTargetFiles}. */
|
|
102
|
+
keepDirs?: string[];
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Resolve a check's file list, or the outcome it should report instead.
|
|
106
|
+
*
|
|
107
|
+
* The one entry point for checks that operate on whole files. It wraps
|
|
108
|
+
* {@link resolveTargetFiles} and {@link listTrackedFiles} rather than replacing
|
|
109
|
+
* them — a check that needs the raw list (because it distinguishes the two cases
|
|
110
|
+
* itself, or scans in several passes) still calls those directly.
|
|
111
|
+
*/
|
|
112
|
+
export declare function resolveTargets(ctx: CheckContext, globs: string[], opts: ResolveOptions): Promise<TargetsOrOutcome>;
|
|
23
113
|
/**
|
|
24
114
|
* List ALL tracked files matching `globs` (never delta), scoped to
|
|
25
|
-
* `sourcePath`(s).
|
|
26
|
-
*
|
|
27
|
-
*
|
|
115
|
+
* `sourcePath`(s).
|
|
116
|
+
*
|
|
117
|
+
* For whole-repo analyses like circular-dependency detection that need the full
|
|
118
|
+
* graph, not just the PR diff. Using `git ls-files` means untracked
|
|
119
|
+
* `node_modules` is excluded by construction.
|
|
120
|
+
*
|
|
121
|
+
* `keepDirs` un-ignores segments for one caller, exactly as in
|
|
122
|
+
* {@link resolveTargetFiles}. It was missing here, and that made Workflow
|
|
123
|
+
* Security blind on the whole-tree path: `.github` is in the default ignore list,
|
|
124
|
+
* so `git ls-files .github/workflows/*.yml` returned the workflow and this
|
|
125
|
+
* function then dropped it. A blocking security check reported a clean pass over
|
|
126
|
+
* files it had never been handed — the surviving half of ticket 067, found by
|
|
127
|
+
* level 1 of the release regression plan.
|
|
128
|
+
*
|
|
129
|
+
* `null` when git could not answer, `[]` when it answered "nothing". The two are
|
|
130
|
+
* opposites to a caller and used to be the same value: `reject: false` keeps a
|
|
131
|
+
* failed git out of the catch below, so a directory git cannot read returned an
|
|
132
|
+
* empty list, and every caller read that as "nothing to review" and passed. On a
|
|
133
|
+
* directory holding real code that produced three green rows — Diff Security,
|
|
134
|
+
* Workflow Security, Diff Smells — over files none of them had opened
|
|
135
|
+
* (ticket 072).
|
|
28
136
|
*/
|
|
29
|
-
export declare function listTrackedFiles(ctx: CheckContext, globs: string[], sourcePaths: string[]
|
|
137
|
+
export declare function listTrackedFiles(ctx: CheckContext, globs: string[], sourcePaths: string[], opts?: {
|
|
138
|
+
keepDirs?: string[];
|
|
139
|
+
}): Promise<string[] | null>;
|
|
140
|
+
export {};
|
package/dist/core/targets.js
CHANGED
|
@@ -3,8 +3,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.excludeClientPaths = excludeClientPaths;
|
|
6
7
|
exports.resolveTargetFiles = resolveTargetFiles;
|
|
7
8
|
exports.resolveScopedTargetFiles = resolveScopedTargetFiles;
|
|
9
|
+
exports.resolveTargets = resolveTargets;
|
|
8
10
|
exports.listTrackedFiles = listTrackedFiles;
|
|
9
11
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
10
12
|
const node_path_1 = __importDefault(require("node:path"));
|
|
@@ -12,6 +14,7 @@ const config_1 = require("../config");
|
|
|
12
14
|
const loader_1 = require("../config/loader");
|
|
13
15
|
const utils_1 = require("../utils");
|
|
14
16
|
const execa_1 = require("../utils/execa");
|
|
17
|
+
const types_1 = require("./types");
|
|
15
18
|
/**
|
|
16
19
|
* Drop files that live under any excluded directory (client `ignoreDirs` or the
|
|
17
20
|
* curated default), matched on any path segment so nesting depth doesn't matter.
|
|
@@ -31,11 +34,17 @@ const execa_1 = require("../utils/execa");
|
|
|
31
34
|
* the one that is actually generated. Globs answer both, and because this sits
|
|
32
35
|
* in the one function every file-scanning check calls, a client writes the rule
|
|
33
36
|
* once instead of hunting for the right key on each check.
|
|
37
|
+
*
|
|
38
|
+
* Exported because three checks build their file list themselves and cannot go
|
|
39
|
+
* through the resolvers above — and `excludePaths` is documented as "the only
|
|
40
|
+
* exclusion that reaches every check", which was not true of them (ticket 080).
|
|
41
|
+
* Each has a real reason to skip `sourcePath` and `ignoreDirs`; none has one to
|
|
42
|
+
* ignore a client saying "do not look here".
|
|
34
43
|
*/
|
|
35
|
-
function
|
|
44
|
+
function excludeClientPaths(files, cwd) {
|
|
36
45
|
let patterns = [];
|
|
37
46
|
try {
|
|
38
|
-
patterns = (0, loader_1.loadProjectConfig)().excludePaths ?? [];
|
|
47
|
+
patterns = (0, loader_1.loadProjectConfig)(cwd).excludePaths ?? [];
|
|
39
48
|
}
|
|
40
49
|
catch {
|
|
41
50
|
// A config that cannot be read is the loader's problem to report, not a
|
|
@@ -43,8 +52,8 @@ function excludeConfiguredPaths(files) {
|
|
|
43
52
|
}
|
|
44
53
|
return patterns.length > 0 ? files.filter(f => !(0, utils_1.matchesAnyGlob)(f, patterns)) : files;
|
|
45
54
|
}
|
|
46
|
-
function excludeIgnoredDirs(files, keepDirs = []) {
|
|
47
|
-
const ignored = new Set((0, config_1.getIgnoredDirs)());
|
|
55
|
+
function excludeIgnoredDirs(files, cwd, keepDirs = []) {
|
|
56
|
+
const ignored = new Set((0, config_1.getIgnoredDirs)(cwd));
|
|
48
57
|
for (const dir of keepDirs)
|
|
49
58
|
ignored.delete(dir);
|
|
50
59
|
if (ignored.size === 0)
|
|
@@ -78,15 +87,45 @@ async function resolveTargetFiles(ctx, globs, opts = {}) {
|
|
|
78
87
|
: ['ls-files', ...globs];
|
|
79
88
|
const cacheKey = `t:${args.join('')}`;
|
|
80
89
|
let stdout = ctx.cache?.get(cacheKey);
|
|
90
|
+
const fromCache = stdout !== undefined;
|
|
81
91
|
if (stdout === undefined) {
|
|
82
|
-
|
|
92
|
+
// `reject: false` keeps a failed git out of the catch below, so the exit
|
|
93
|
+
// code is the only thing separating "git could not answer" from "nothing
|
|
94
|
+
// matched". They are opposites to the caller: every caller turns `null`
|
|
95
|
+
// into a skip with a reason and `[]` into a clean pass. Without this
|
|
96
|
+
// branch a directory git cannot read produced the second — seven checks
|
|
97
|
+
// reporting ✅ over code none of them had looked at, while the docstring
|
|
98
|
+
// above had promised `null` all along. Found by ticket 062's corpus.
|
|
99
|
+
//
|
|
100
|
+
// A pathspec that matches nothing still exits 0, so the ordinary empty
|
|
101
|
+
// result cannot be swallowed here.
|
|
102
|
+
const result = await (0, execa_1.execa)('git', args, { cwd: ctx.cwd, reject: false });
|
|
103
|
+
if (result.exitCode !== 0) {
|
|
104
|
+
utils_1.logger.warn(`[targets]: ⚠️ git could not list files (exit ${result.exitCode})`);
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
stdout = result.stdout;
|
|
83
108
|
ctx.cache?.set(cacheKey, stdout);
|
|
84
109
|
}
|
|
85
|
-
const files =
|
|
110
|
+
const files = excludeClientPaths(excludeIgnoredDirs(stdout
|
|
86
111
|
.split('\n')
|
|
87
112
|
.map(f => f.trim())
|
|
88
|
-
.filter(Boolean), opts.keepDirs)).filter(f => node_fs_1.default.existsSync(node_path_1.default.join(ctx.cwd, f)));
|
|
89
|
-
|
|
113
|
+
.filter(Boolean), ctx.cwd, opts.keepDirs), ctx.cwd).filter(f => node_fs_1.default.existsSync(node_path_1.default.join(ctx.cwd, f)));
|
|
114
|
+
// One line per actual git call, not per consumer. Several checks ask for the
|
|
115
|
+
// same globs in a run and were each answered from the cache, but the line was
|
|
116
|
+
// printed after the cache branch — so four checks sharing one lookup printed
|
|
117
|
+
// four identical `Delta: 1 file(s) for *` lines and the log implied four walks
|
|
118
|
+
// of the tree.
|
|
119
|
+
//
|
|
120
|
+
// Demoted rather than dropped on a cache hit: the count is computed AFTER
|
|
121
|
+
// filtering, and two callers with the same globs but different `keepDirs` get
|
|
122
|
+
// different lists from the same cached output. Losing the second line would
|
|
123
|
+
// hide a real difference, so it moves to `debug` instead of disappearing.
|
|
124
|
+
const line = `[targets]: ${ctx.baseSha ? '📂 Delta' : 'Full scan'}: ${files.length} file(s) for ${globs.join(', ')}`;
|
|
125
|
+
if (fromCache)
|
|
126
|
+
utils_1.logger.debug(`${line} (cached)`);
|
|
127
|
+
else
|
|
128
|
+
utils_1.logger.info(line);
|
|
90
129
|
return files;
|
|
91
130
|
}
|
|
92
131
|
catch (err) {
|
|
@@ -116,23 +155,69 @@ async function resolveScopedTargetFiles(ctx, globs, sourcePaths) {
|
|
|
116
155
|
return null;
|
|
117
156
|
return scopeToSourcePaths(files, sourcePaths);
|
|
118
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* Resolve a check's file list, or the outcome it should report instead.
|
|
160
|
+
*
|
|
161
|
+
* The one entry point for checks that operate on whole files. It wraps
|
|
162
|
+
* {@link resolveTargetFiles} and {@link listTrackedFiles} rather than replacing
|
|
163
|
+
* them — a check that needs the raw list (because it distinguishes the two cases
|
|
164
|
+
* itself, or scans in several passes) still calls those directly.
|
|
165
|
+
*/
|
|
166
|
+
async function resolveTargets(ctx, globs, opts) {
|
|
167
|
+
const listOpts = opts.keepDirs ? { keepDirs: opts.keepDirs } : {};
|
|
168
|
+
// Read once, and only when something will use it. A whole-tree walk is always
|
|
169
|
+
// scoped — that is what `listTrackedFiles` promises its callers — while a delta
|
|
170
|
+
// walk is scoped only on request.
|
|
171
|
+
const sourcePaths = opts.wholeTree || opts.scoped ? (0, config_1.getSourcePaths)(ctx.cwd) : [];
|
|
172
|
+
const files = opts.wholeTree
|
|
173
|
+
? await listTrackedFiles(ctx, globs, sourcePaths, listOpts)
|
|
174
|
+
: await resolveTargetFiles(ctx, globs, listOpts);
|
|
175
|
+
if (files === null)
|
|
176
|
+
return (0, types_1.skip)('git unavailable');
|
|
177
|
+
const scoped = opts.wholeTree ? files : scopeToSourcePaths(files, sourcePaths);
|
|
178
|
+
if (scoped.length > 0)
|
|
179
|
+
return scoped;
|
|
180
|
+
return 'pass' in opts.onEmpty ? (0, types_1.pass)(opts.onEmpty.pass) : (0, types_1.skip)(opts.onEmpty.skip);
|
|
181
|
+
}
|
|
119
182
|
/**
|
|
120
183
|
* List ALL tracked files matching `globs` (never delta), scoped to
|
|
121
|
-
* `sourcePath`(s).
|
|
122
|
-
*
|
|
123
|
-
*
|
|
184
|
+
* `sourcePath`(s).
|
|
185
|
+
*
|
|
186
|
+
* For whole-repo analyses like circular-dependency detection that need the full
|
|
187
|
+
* graph, not just the PR diff. Using `git ls-files` means untracked
|
|
188
|
+
* `node_modules` is excluded by construction.
|
|
189
|
+
*
|
|
190
|
+
* `keepDirs` un-ignores segments for one caller, exactly as in
|
|
191
|
+
* {@link resolveTargetFiles}. It was missing here, and that made Workflow
|
|
192
|
+
* Security blind on the whole-tree path: `.github` is in the default ignore list,
|
|
193
|
+
* so `git ls-files .github/workflows/*.yml` returned the workflow and this
|
|
194
|
+
* function then dropped it. A blocking security check reported a clean pass over
|
|
195
|
+
* files it had never been handed — the surviving half of ticket 067, found by
|
|
196
|
+
* level 1 of the release regression plan.
|
|
197
|
+
*
|
|
198
|
+
* `null` when git could not answer, `[]` when it answered "nothing". The two are
|
|
199
|
+
* opposites to a caller and used to be the same value: `reject: false` keeps a
|
|
200
|
+
* failed git out of the catch below, so a directory git cannot read returned an
|
|
201
|
+
* empty list, and every caller read that as "nothing to review" and passed. On a
|
|
202
|
+
* directory holding real code that produced three green rows — Diff Security,
|
|
203
|
+
* Workflow Security, Diff Smells — over files none of them had opened
|
|
204
|
+
* (ticket 072).
|
|
124
205
|
*/
|
|
125
|
-
async function listTrackedFiles(ctx, globs, sourcePaths) {
|
|
206
|
+
async function listTrackedFiles(ctx, globs, sourcePaths, opts = {}) {
|
|
126
207
|
try {
|
|
127
|
-
const
|
|
128
|
-
|
|
208
|
+
const result = await (0, execa_1.execa)('git', ['ls-files', ...globs], { cwd: ctx.cwd, reject: false });
|
|
209
|
+
if (result.exitCode !== 0) {
|
|
210
|
+
utils_1.logger.warn(`[targets]: ⚠️ git could not list tracked files (exit ${result.exitCode})`);
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
const files = excludeClientPaths(excludeIgnoredDirs(result.stdout
|
|
129
214
|
.split('\n')
|
|
130
215
|
.map(f => f.trim())
|
|
131
|
-
.filter(Boolean)));
|
|
216
|
+
.filter(Boolean), ctx.cwd, opts.keepDirs), ctx.cwd);
|
|
132
217
|
return scopeToSourcePaths(files, sourcePaths);
|
|
133
218
|
}
|
|
134
219
|
catch (err) {
|
|
135
220
|
utils_1.logger.warn(`[targets]: ⚠️ Could not list tracked files\n${err}`);
|
|
136
|
-
return
|
|
221
|
+
return null;
|
|
137
222
|
}
|
|
138
223
|
}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { PRCheckMateConfig, ReporterConfig } from '../interfaces';
|
|
2
|
-
import type { Language } from '../utils/detect-language';
|
|
2
|
+
import type { Language, LanguageUsage } from '../utils/detect-language';
|
|
3
3
|
import type { CheckResult, CheckStatus } from '../utils/result-collector';
|
|
4
|
-
export type { CheckResult, CheckStatus, Language };
|
|
4
|
+
export type { CheckResult, CheckStatus, Language, LanguageUsage };
|
|
5
5
|
/**
|
|
6
6
|
* Execution phases, run strictly in order. Within a phase checks run in parallel.
|
|
7
7
|
* `format` runs last because formatters (prettier) may write files.
|
|
@@ -32,6 +32,12 @@ export interface CheckContext {
|
|
|
32
32
|
cwd: string;
|
|
33
33
|
/** Languages detected (or supplied) for this repo — drives `applies` gating. */
|
|
34
34
|
languages: Language[];
|
|
35
|
+
/**
|
|
36
|
+
* The same detection with its evidence — file counts per language. Read by the
|
|
37
|
+
* install layer, which must not confuse "this language is here" with "this
|
|
38
|
+
* language is worth a toolchain download". Absent on hand-built contexts.
|
|
39
|
+
*/
|
|
40
|
+
usage?: LanguageUsage[];
|
|
35
41
|
/** Merged pr-checkmate.json + defaults. */
|
|
36
42
|
config: PRCheckMateConfig;
|
|
37
43
|
/** When false, checks that would modify files (prettier, --fix) run read-only or skip the write. */
|
package/dist/init.js
CHANGED
|
@@ -11,6 +11,7 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
11
11
|
const path_1 = __importDefault(require("path"));
|
|
12
12
|
const inquirer_1 = __importDefault(require("inquirer"));
|
|
13
13
|
const config_1 = require("./config");
|
|
14
|
+
const findings_1 = require("./core/checks/findings");
|
|
14
15
|
const utils_1 = require("./utils");
|
|
15
16
|
const CONFIG_FILE = path_1.default.resolve(process.cwd(), 'pr-checkmate.json');
|
|
16
17
|
/**
|
|
@@ -319,7 +320,26 @@ function buildInitialConfig(sourcePath, languages) {
|
|
|
319
320
|
// Delete an entry to have that check start reporting on tests again.
|
|
320
321
|
duplicate: { ignore: [...config_1.DUPLICATE_IGNORE_DEFAULTS] },
|
|
321
322
|
deadCode: { ignoreFiles: [...config_1.DEAD_CODE_IGNORE_DEFAULTS] },
|
|
322
|
-
|
|
323
|
+
// `dataflowWindow` is written out at its default for the same reason as
|
|
324
|
+
// `max-len`: it is a number people will want to change, and a number nobody
|
|
325
|
+
// can see is a number nobody can change. It is also the one finding in this
|
|
326
|
+
// check produced by a heuristic rather than a pattern, so the caveat belongs
|
|
327
|
+
// next to the value — including the part that surprises people, which is what
|
|
328
|
+
// promoting the whole check to `error` does to it.
|
|
329
|
+
'// diffSecurity': [
|
|
330
|
+
'"ignorePaths" REPLACES the test-path defaults below — they are there because a security',
|
|
331
|
+
"test's job is to contain the exact constructs this check hunts for.",
|
|
332
|
+
'"dataflowWindow" is how many added lines a value stays tainted for, so an interpolated',
|
|
333
|
+
'string assigned on one line and passed to a query on the next are still connected.',
|
|
334
|
+
'It is a heuristic — no scopes, no reassignment tracking — so it only ever warns.',
|
|
335
|
+
'Set it to 0 to switch it off. Note that "severity": { "Diff Security": "error" } promotes',
|
|
336
|
+
'this warning to a failure along with the rest, so a repository that makes this check a',
|
|
337
|
+
'gate and does not want the heuristic blocking should set dataflowWindow to 0.',
|
|
338
|
+
].join(' '),
|
|
339
|
+
diffSecurity: {
|
|
340
|
+
dataflowWindow: findings_1.DEFAULT_DATAFLOW_WINDOW,
|
|
341
|
+
ignorePaths: [...config_1.DIFF_SECURITY_IGNORE_DEFAULTS],
|
|
342
|
+
},
|
|
323
343
|
documentation: config_1.documentation,
|
|
324
344
|
languages,
|
|
325
345
|
};
|
|
@@ -180,6 +180,11 @@ export interface PRCheckMateConfig {
|
|
|
180
180
|
typecheck?: boolean;
|
|
181
181
|
typeChecker?: 'mypy' | 'pyright';
|
|
182
182
|
};
|
|
183
|
+
packageSize?: {
|
|
184
|
+
enabled?: boolean;
|
|
185
|
+
maxGrowthPercent?: number;
|
|
186
|
+
compareWith?: string;
|
|
187
|
+
};
|
|
183
188
|
diffSmell?: {
|
|
184
189
|
enabled?: boolean;
|
|
185
190
|
ignorePaths?: string[];
|
|
@@ -299,6 +304,7 @@ export interface PRCheckMateConfig {
|
|
|
299
304
|
flags?: string;
|
|
300
305
|
}>;
|
|
301
306
|
commentHandling?: 'language' | 'legacy' | 'off';
|
|
307
|
+
dataflowWindow?: number;
|
|
302
308
|
};
|
|
303
309
|
mergeConflict?: {
|
|
304
310
|
enabled?: boolean;
|
|
@@ -1,16 +1,66 @@
|
|
|
1
|
+
/** Where a tool was found, in the order the candidates are tried. */
|
|
2
|
+
export type ToolSource = 'client' | 'profile' | 'bundled';
|
|
3
|
+
/** A resolved tool binary, and which copy of it answered. */
|
|
4
|
+
export interface ResolvedTool {
|
|
5
|
+
bin: string;
|
|
6
|
+
source: ToolSource;
|
|
7
|
+
/** The search root that produced it; `undefined` for module-relative resolution. */
|
|
8
|
+
root?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Point tool resolution at the profile directories fetched for this run.
|
|
12
|
+
*
|
|
13
|
+
* Called by the install layer once the cache is known. Passing an empty list —
|
|
14
|
+
* or never calling it at all — leaves resolution exactly as it was before
|
|
15
|
+
* profiles existed, which is what keeps a run that needs no fetching identical
|
|
16
|
+
* to a run from before there was anything to fetch.
|
|
17
|
+
*/
|
|
18
|
+
export declare function registerToolPaths(dirs: string[]): void;
|
|
19
|
+
/** Drop every registered profile root. For tests, and for a second run in-process. */
|
|
20
|
+
export declare function clearToolPaths(): void;
|
|
21
|
+
/** The root of the installed pr-checkmate package — where our own tooling lives. */
|
|
22
|
+
export declare function ownPackageRoot(): string;
|
|
23
|
+
/**
|
|
24
|
+
* Whether a package is physically present in our own dependency tree.
|
|
25
|
+
*
|
|
26
|
+
* Checked by walking `node_modules` upwards rather than by `require.resolve`,
|
|
27
|
+
* because a package that restricts its `exports` map resolves for neither its
|
|
28
|
+
* main entry nor its `package.json` while being perfectly well installed —
|
|
29
|
+
* `cspell` is one. The question here is only "is it there", and the directory
|
|
30
|
+
* answers that without an opinion about what it chooses to export.
|
|
31
|
+
*
|
|
32
|
+
* Walking up matters as much as the check itself: installed as a dependency, our
|
|
33
|
+
* own packages are hoisted to the client's `node_modules`, one level above ours.
|
|
34
|
+
*/
|
|
35
|
+
export declare function packagePresent(pkgName: string, from?: string): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Resolve a tool's binary and say which copy answered.
|
|
38
|
+
*
|
|
39
|
+
* Use this over {@link resolvePackageBin} when the answer is worth logging — the
|
|
40
|
+
* source is the difference between "the client's ESLint 8 ran" and "ours did",
|
|
41
|
+
* and that is the first thing anyone asks when a lint result looks wrong.
|
|
42
|
+
*
|
|
43
|
+
* @param pkgName Package whose binary to resolve (e.g. `eslint`).
|
|
44
|
+
* @param binRelPath Bin path relative to the package root (e.g. `bin/eslint.js`).
|
|
45
|
+
* @param options Resolution options.
|
|
46
|
+
* @param options.paths Roots to prefer ahead of everything else. Use
|
|
47
|
+
* `[process.cwd()]` to prefer the consumer project's copy.
|
|
48
|
+
*/
|
|
49
|
+
export declare function resolveTool(pkgName: string, binRelPath: string, options?: {
|
|
50
|
+
paths?: string[];
|
|
51
|
+
}): ResolvedTool | null;
|
|
1
52
|
/**
|
|
2
53
|
* Resolves a binary path for a bundled package, bypassing exports map restrictions.
|
|
3
54
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* 3. Walk up from main entry — for packages that export neither (e.g. cspell)
|
|
55
|
+
* Throws when no copy can be found anywhere, which is the honest outcome: a check
|
|
56
|
+
* that cannot find its tool has nothing to report, and the install layer turns
|
|
57
|
+
* that into a skip with a reason rather than letting it surface as a stack trace.
|
|
8
58
|
*
|
|
9
59
|
* @param pkgName - The package whose binary to resolve (e.g. `eslint`).
|
|
10
60
|
* @param binRelPath - The bin path relative to the package root (e.g. `bin/eslint.js`).
|
|
11
61
|
* @param options - Resolution options.
|
|
12
|
-
* @param options.paths - Directories to resolve from
|
|
13
|
-
* Use `[process.cwd()]` to prefer the consumer project's copy
|
|
62
|
+
* @param options.paths - Directories to resolve from, preferred over everything
|
|
63
|
+
* else. Use `[process.cwd()]` to prefer the consumer project's copy.
|
|
14
64
|
*/
|
|
15
65
|
export declare function resolvePackageBin(pkgName: string, binRelPath: string, options?: {
|
|
16
66
|
paths?: string[];
|
|
@@ -3,51 +3,161 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.registerToolPaths = registerToolPaths;
|
|
7
|
+
exports.clearToolPaths = clearToolPaths;
|
|
8
|
+
exports.ownPackageRoot = ownPackageRoot;
|
|
9
|
+
exports.packagePresent = packagePresent;
|
|
10
|
+
exports.resolveTool = resolveTool;
|
|
6
11
|
exports.resolvePackageBin = resolvePackageBin;
|
|
7
12
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
13
|
const node_path_1 = __importDefault(require("node:path"));
|
|
9
14
|
/**
|
|
10
|
-
*
|
|
15
|
+
* Extra roots to search, registered once per run by the install layer.
|
|
11
16
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
17
|
+
* Module state rather than a parameter threaded through fifteen call sites: a
|
|
18
|
+
* profile directory is a property of the *run*, not of any one check, and every
|
|
19
|
+
* check that resolves a binary would otherwise have to be handed a value it has
|
|
20
|
+
* no opinion about. `registerToolPaths` is called before the first check starts
|
|
21
|
+
* and never during one, so nothing observes it changing underneath.
|
|
22
|
+
*/
|
|
23
|
+
let profileRoots = [];
|
|
24
|
+
/**
|
|
25
|
+
* Point tool resolution at the profile directories fetched for this run.
|
|
16
26
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* Use `[process.cwd()]` to prefer the consumer project's copy of the package.
|
|
27
|
+
* Called by the install layer once the cache is known. Passing an empty list —
|
|
28
|
+
* or never calling it at all — leaves resolution exactly as it was before
|
|
29
|
+
* profiles existed, which is what keeps a run that needs no fetching identical
|
|
30
|
+
* to a run from before there was anything to fetch.
|
|
22
31
|
*/
|
|
23
|
-
function
|
|
24
|
-
|
|
25
|
-
|
|
32
|
+
function registerToolPaths(dirs) {
|
|
33
|
+
profileRoots = [...dirs];
|
|
34
|
+
}
|
|
35
|
+
/** Drop every registered profile root. For tests, and for a second run in-process. */
|
|
36
|
+
function clearToolPaths() {
|
|
37
|
+
profileRoots = [];
|
|
38
|
+
}
|
|
39
|
+
/** The root of the installed pr-checkmate package — where our own tooling lives. */
|
|
40
|
+
function ownPackageRoot() {
|
|
41
|
+
return node_path_1.default.resolve(__dirname, '../..');
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Whether a package is physically present in our own dependency tree.
|
|
45
|
+
*
|
|
46
|
+
* Checked by walking `node_modules` upwards rather than by `require.resolve`,
|
|
47
|
+
* because a package that restricts its `exports` map resolves for neither its
|
|
48
|
+
* main entry nor its `package.json` while being perfectly well installed —
|
|
49
|
+
* `cspell` is one. The question here is only "is it there", and the directory
|
|
50
|
+
* answers that without an opinion about what it chooses to export.
|
|
51
|
+
*
|
|
52
|
+
* Walking up matters as much as the check itself: installed as a dependency, our
|
|
53
|
+
* own packages are hoisted to the client's `node_modules`, one level above ours.
|
|
54
|
+
*/
|
|
55
|
+
function packagePresent(pkgName, from = ownPackageRoot()) {
|
|
56
|
+
let dir = from;
|
|
57
|
+
while (dir !== node_path_1.default.dirname(dir)) {
|
|
58
|
+
if (node_fs_1.default.existsSync(node_path_1.default.join(dir, 'node_modules', pkgName, 'package.json')))
|
|
59
|
+
return true;
|
|
60
|
+
dir = node_path_1.default.dirname(dir);
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
/** One resolution attempt against a single set of search paths. */
|
|
65
|
+
function tryResolve(pkgName, binRelPath, paths) {
|
|
66
|
+
const resolveOpts = paths ? { paths } : undefined;
|
|
67
|
+
// 1. Direct subpath — the fast path for packages that export their bin.
|
|
26
68
|
try {
|
|
27
69
|
return require.resolve(`${pkgName}/${binRelPath}`, resolveOpts);
|
|
28
70
|
}
|
|
29
71
|
catch { }
|
|
30
|
-
// 2. Via package.json
|
|
72
|
+
// 2. Via package.json — for packages that export `./package.json` but not the
|
|
73
|
+
// bin subpath (eslint).
|
|
31
74
|
try {
|
|
32
75
|
const pkgJsonPath = require.resolve(`${pkgName}/package.json`, resolveOpts);
|
|
33
76
|
return node_path_1.default.join(node_path_1.default.dirname(pkgJsonPath), binRelPath);
|
|
34
77
|
}
|
|
35
78
|
catch { }
|
|
36
|
-
// 3. Walk up from main entry
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
79
|
+
// 3. Walk up from the main entry, matching the package name — for packages
|
|
80
|
+
// that export neither (cspell).
|
|
81
|
+
try {
|
|
82
|
+
let dir = node_path_1.default.dirname(require.resolve(pkgName, resolveOpts));
|
|
83
|
+
while (dir !== node_path_1.default.dirname(dir)) {
|
|
84
|
+
const pkgJsonPath = node_path_1.default.join(dir, 'package.json');
|
|
85
|
+
if (node_fs_1.default.existsSync(pkgJsonPath)) {
|
|
86
|
+
try {
|
|
87
|
+
const pkg = JSON.parse(node_fs_1.default.readFileSync(pkgJsonPath, 'utf-8'));
|
|
88
|
+
if (pkg.name === pkgName)
|
|
89
|
+
return node_path_1.default.join(dir, binRelPath);
|
|
46
90
|
}
|
|
91
|
+
catch { }
|
|
47
92
|
}
|
|
48
|
-
|
|
93
|
+
dir = node_path_1.default.dirname(dir);
|
|
49
94
|
}
|
|
50
|
-
dir = node_path_1.default.dirname(dir);
|
|
51
95
|
}
|
|
96
|
+
catch { }
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The roots to try, in order, and what finding a tool in each one means.
|
|
101
|
+
*
|
|
102
|
+
* The order is the policy, and it is deliberate:
|
|
103
|
+
*
|
|
104
|
+
* 1. **The client's own copy**, when a caller asked for it. A repository pinned
|
|
105
|
+
* to ESLint 8 with a legacy `.eslintrc` must keep working, and only its copy
|
|
106
|
+
* can do that.
|
|
107
|
+
* 2. **A fetched profile**, for tooling that does not ship inside the package.
|
|
108
|
+
* 3. **Our bundle** — module-relative, exactly as resolution worked before
|
|
109
|
+
* profiles existed.
|
|
110
|
+
*
|
|
111
|
+
* Branch 3 is passed `undefined` rather than our package root on purpose: that is
|
|
112
|
+
* literally the previous behaviour, so a run with nothing registered resolves
|
|
113
|
+
* every tool along the identical path it always did.
|
|
114
|
+
*/
|
|
115
|
+
function candidates(optPaths) {
|
|
116
|
+
const list = [];
|
|
117
|
+
if (optPaths && optPaths.length > 0)
|
|
118
|
+
list.push({ paths: optPaths, source: 'client' });
|
|
119
|
+
for (const root of profileRoots)
|
|
120
|
+
list.push({ paths: [root], source: 'profile' });
|
|
121
|
+
list.push({ source: 'bundled' });
|
|
122
|
+
return list;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Resolve a tool's binary and say which copy answered.
|
|
126
|
+
*
|
|
127
|
+
* Use this over {@link resolvePackageBin} when the answer is worth logging — the
|
|
128
|
+
* source is the difference between "the client's ESLint 8 ran" and "ours did",
|
|
129
|
+
* and that is the first thing anyone asks when a lint result looks wrong.
|
|
130
|
+
*
|
|
131
|
+
* @param pkgName Package whose binary to resolve (e.g. `eslint`).
|
|
132
|
+
* @param binRelPath Bin path relative to the package root (e.g. `bin/eslint.js`).
|
|
133
|
+
* @param options Resolution options.
|
|
134
|
+
* @param options.paths Roots to prefer ahead of everything else. Use
|
|
135
|
+
* `[process.cwd()]` to prefer the consumer project's copy.
|
|
136
|
+
*/
|
|
137
|
+
function resolveTool(pkgName, binRelPath, options) {
|
|
138
|
+
for (const candidate of candidates(options?.paths)) {
|
|
139
|
+
const bin = tryResolve(pkgName, binRelPath, candidate.paths);
|
|
140
|
+
if (bin)
|
|
141
|
+
return { bin, source: candidate.source, root: candidate.paths?.[0] };
|
|
142
|
+
}
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Resolves a binary path for a bundled package, bypassing exports map restrictions.
|
|
147
|
+
*
|
|
148
|
+
* Throws when no copy can be found anywhere, which is the honest outcome: a check
|
|
149
|
+
* that cannot find its tool has nothing to report, and the install layer turns
|
|
150
|
+
* that into a skip with a reason rather than letting it surface as a stack trace.
|
|
151
|
+
*
|
|
152
|
+
* @param pkgName - The package whose binary to resolve (e.g. `eslint`).
|
|
153
|
+
* @param binRelPath - The bin path relative to the package root (e.g. `bin/eslint.js`).
|
|
154
|
+
* @param options - Resolution options.
|
|
155
|
+
* @param options.paths - Directories to resolve from, preferred over everything
|
|
156
|
+
* else. Use `[process.cwd()]` to prefer the consumer project's copy.
|
|
157
|
+
*/
|
|
158
|
+
function resolvePackageBin(pkgName, binRelPath, options) {
|
|
159
|
+
const resolved = resolveTool(pkgName, binRelPath, options);
|
|
160
|
+
if (resolved)
|
|
161
|
+
return resolved.bin;
|
|
52
162
|
throw new Error(`Cannot resolve binary "${binRelPath}" for package "${pkgName}"`);
|
|
53
163
|
}
|