@dependably/npm-check 1.8.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -0
- package/bin/cli.js +93 -1
- package/package.json +24 -6
- package/src/audit-config.js +127 -75
- package/src/audit.js +235 -35
- package/src/exceptions.js +87 -70
- package/src/facts/collect.js +201 -0
- package/src/facts/document.js +140 -0
- package/src/facts/errors.js +19 -0
- package/src/facts/index.js +36 -0
- package/src/facts/lockfile-graph.js +467 -0
- package/src/facts/modulegraph.js +289 -0
- package/src/facts/resolve.js +486 -0
- package/src/facts/scan.js +755 -0
- package/src/facts/specifier.js +48 -0
- package/src/facts/ts.js +47 -0
- package/src/facts/types.d.ts +493 -0
- package/src/facts/version.js +11 -0
- package/src/facts/workspace.js +183 -0
- package/src/npmrc-validator.js +2 -1
- package/src/pnpm-workspace-validator.js +8 -2
- package/src/report.js +14 -6
- package/src/schema.js +54 -0
package/README.md
CHANGED
|
@@ -54,8 +54,55 @@ for every command, flag, and exit code.
|
|
|
54
54
|
- **Vulnerabilities & deprecations** — scan locked versions against the npm advisory endpoint and deprecation notices.
|
|
55
55
|
- **Licenses** — validate SPDX expressions against an approved list.
|
|
56
56
|
- **Pin / prune / unused** — lock ranges (incl. `overrides`) to exact versions, remove orphaned entries, flag unused deps.
|
|
57
|
+
- **Import facts** — report what the source tree imports (per-file imports, bindings, the module graph through `node_modules`, lockfile graphs) as a JSON document for another tool to consume. See below.
|
|
57
58
|
- **pnpm** — read-only checks and config validation for `pnpm-lock.yaml`.
|
|
58
59
|
|
|
60
|
+
## Import facts (`npm-check imports`)
|
|
61
|
+
|
|
62
|
+
`npm-check imports <dir>` exports what a JavaScript/TypeScript/Svelte tree
|
|
63
|
+
imports, as data for another tool. It is the same parse-only scan
|
|
64
|
+
[sbom-reach](https://github.com/dependably/sbom-reach) uses for its npm
|
|
65
|
+
reachability verdicts — the imports of every first-party file, the binding
|
|
66
|
+
names each site introduces and which of them the file actually references,
|
|
67
|
+
each site resolved to the installed copy it loads, the statically resolved
|
|
68
|
+
module graph *through* `node_modules`, and the dependency graph the lockfiles
|
|
69
|
+
record — published instead of judged. Nothing is written, nothing is gated: a
|
|
70
|
+
successful scan always exits `0`.
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
npm-check imports ./src # the facts document, as JSON
|
|
74
|
+
npm-check imports ./src --format human # its summary
|
|
75
|
+
npm-check imports ./src --no-module-graph # first-party imports only
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
npm-check reports **npm language facts only**. It knows nothing about purls,
|
|
79
|
+
SBOMs or vulnerabilities, and it draws no conclusion: whether a package is
|
|
80
|
+
*reachable* is a verdict, and verdicts belong to the consumer.
|
|
81
|
+
|
|
82
|
+
**Facts are not findings.** An import site has no severity — "lodash is
|
|
83
|
+
imported on line 3" is not a problem to fix — so squeezing it into the
|
|
84
|
+
findings envelope's `findings` array would be a lie, and would let `--fail-on`
|
|
85
|
+
gate CI on ordinary imports. `imports` therefore emits a **sibling document
|
|
86
|
+
type**: the same envelope identity every Dependably tool shares (`tool`,
|
|
87
|
+
`toolVersion`, `schemaVersion`, `target`, `summary`), with `findings` replaced
|
|
88
|
+
by the facts sections, and an explicit `documentType: "imports"`
|
|
89
|
+
discriminator so a consumer never has to guess the payload from whichever key
|
|
90
|
+
happens to be present. A document with no `documentType` is a findings
|
|
91
|
+
document (schema `1.0` predates the split, and the findings envelope is
|
|
92
|
+
unchanged). This is the same contract pycheck's `--imports` established for
|
|
93
|
+
Python.
|
|
94
|
+
|
|
95
|
+
The load-bearing part is `unanalyzable`: every file the scan could not read,
|
|
96
|
+
every `.svelte` file whose `<script>` extraction reported a problem, every
|
|
97
|
+
`node_modules` file the walk skipped, and a walk cut off by its file budget is
|
|
98
|
+
listed with a `kind` and a `reason`, so that an absence of evidence is only
|
|
99
|
+
ever read as a negative when the search actually ran. The command needs the
|
|
100
|
+
optional `typescript` peer dependency (any 5.6+ release) and exits `2` with
|
|
101
|
+
`TYPESCRIPT_MISSING` when it is not installed. The full document shape is in
|
|
102
|
+
the [CLI reference](https://github.com/dependably/npm-check/blob/main/docs/CLI.md#imports-command);
|
|
103
|
+
the library form (`@dependably/npm-check/facts`) is in the
|
|
104
|
+
[API guide](https://github.com/dependably/npm-check/blob/main/docs/API.md#import-facts-dependablynpm-checkfacts).
|
|
105
|
+
|
|
59
106
|
## Documentation
|
|
60
107
|
|
|
61
108
|
- **[CLI reference](https://github.com/dependably/npm-check/blob/main/docs/CLI.md)** — all commands, flags, exit codes, JSON output
|
package/bin/cli.js
CHANGED
|
@@ -25,6 +25,7 @@ import { loadAuditConfig, mergeConfig } from '../src/audit-config.js';
|
|
|
25
25
|
import { runReport, formatReport } from '../src/report.js';
|
|
26
26
|
import { prunePackages } from '../src/pruner.js';
|
|
27
27
|
import { findUnusedDependencies } from '../src/usage-scanner.js';
|
|
28
|
+
import { buildFactsEnvelope } from '../src/schema.js';
|
|
28
29
|
|
|
29
30
|
const argv = process.argv.slice(2);
|
|
30
31
|
|
|
@@ -49,6 +50,9 @@ Commands:
|
|
|
49
50
|
check [file] Verify integrity hashes and licenses
|
|
50
51
|
audit [file] Lint lockfile for best practices (non-zero exit on failure)
|
|
51
52
|
unused [dir] Flag declared dependencies the application never imports
|
|
53
|
+
imports [dir] Report the tree's import facts (per-file imports, bindings,
|
|
54
|
+
the module graph through node_modules, lockfile graphs) as
|
|
55
|
+
a JSON document — data for another tool, never a gate
|
|
52
56
|
|
|
53
57
|
Fix & transform (npm-only; mutate the lockfile with --write):
|
|
54
58
|
fix [file] [--write] Run automated fixer with optional write
|
|
@@ -147,6 +151,16 @@ Unused Options:
|
|
|
147
151
|
--include-dev Also check devDependencies (off by default)
|
|
148
152
|
--format human|json Output format (default: human; json is machine-readable)
|
|
149
153
|
|
|
154
|
+
Imports Options:
|
|
155
|
+
--format json|human Output format (default: json — the facts document, with
|
|
156
|
+
documentType "imports"; human prints its summary)
|
|
157
|
+
--no-module-graph Do not follow imports through node_modules
|
|
158
|
+
--max-files N Stop the node_modules walk after N files (default: 25000)
|
|
159
|
+
--max-file-bytes N Skip (and report) node_modules files larger than N bytes
|
|
160
|
+
(default: 1500000)
|
|
161
|
+
(needs the optional \`typescript\` peer dependency; exits 2 with TYPESCRIPT_MISSING
|
|
162
|
+
when it is not installed. A successful scan always exits 0 — facts are not findings.)
|
|
163
|
+
|
|
150
164
|
Audit Options:
|
|
151
165
|
--config <file> Suite config (.dependably; .dependably-check is a
|
|
152
166
|
deprecated alias), discovered by walking up to the
|
|
@@ -175,6 +189,7 @@ Examples:
|
|
|
175
189
|
npm-check pin --write # Lock down ^/~ versions
|
|
176
190
|
npm-check prune --write # Remove orphaned lockfile entries
|
|
177
191
|
npm-check unused # Flag never-imported dependencies
|
|
192
|
+
npm-check imports ./src > imports.json # Import facts for another tool to consume
|
|
178
193
|
npm-check audit # Lint with default rules
|
|
179
194
|
npm-check audit --fail-on count=0 --format json # Any warning fails the run
|
|
180
195
|
npm-check audit --rule pinned-versions:error
|
|
@@ -325,6 +340,7 @@ function parseFormatFlag(allowed, fallback, code = 2) {
|
|
|
325
340
|
const VALUED_OPTIONS = new Set([
|
|
326
341
|
'--format', '--config', '--fail-on', '--concurrency', '--timeout',
|
|
327
342
|
'--registry', '--licenses-csv', '--check', '--rule', '--keep',
|
|
343
|
+
'--max-files', '--max-file-bytes',
|
|
328
344
|
// deprecated valued aliases (still parsed)
|
|
329
345
|
'--min-severity', '--max-warnings'
|
|
330
346
|
]);
|
|
@@ -332,7 +348,7 @@ const BOOLEAN_OPTIONS = new Set([
|
|
|
332
348
|
'--offline', '--allow-unresolved',
|
|
333
349
|
'--no-integrity', '--no-vuln', '--no-deprecated', '--no-license',
|
|
334
350
|
'--include-dev', '--include-peer', '--write', '--local-fallback', '--verbose',
|
|
335
|
-
'--show-suppressed',
|
|
351
|
+
'--show-suppressed', '--no-module-graph',
|
|
336
352
|
'--version', '--help', '-h',
|
|
337
353
|
// deprecated boolean aliases (still parsed)
|
|
338
354
|
'--strict', '--fail-on-deprecated'
|
|
@@ -993,6 +1009,81 @@ function runUnusedCommand() {
|
|
|
993
1009
|
}
|
|
994
1010
|
}
|
|
995
1011
|
|
|
1012
|
+
// `imports` is a data report, not a check: it emits the import-facts document
|
|
1013
|
+
// (`documentType: "imports"` — language facts only, no verdicts, no
|
|
1014
|
+
// severities), so it defaults to `--format json`, exits 0 on every successful
|
|
1015
|
+
// scan, and NEVER exits 1 — there is nothing to gate on. Exit 2 is the usage
|
|
1016
|
+
// tier: a missing target, a bad flag value, or the optional `typescript` peer
|
|
1017
|
+
// being absent (`TYPESCRIPT_MISSING`). The facts modules are imported lazily
|
|
1018
|
+
// so the lockfile-only commands never load the TypeScript compiler.
|
|
1019
|
+
async function runImportsCommand() {
|
|
1020
|
+
const dir = getDirArg();
|
|
1021
|
+
const target = positionals()[0] ?? '.';
|
|
1022
|
+
const format = parseFormatFlag(['json', 'human'], 'json');
|
|
1023
|
+
const moduleGraph = !argv.includes('--no-module-graph');
|
|
1024
|
+
const maxFiles = parsePositiveIntFlag('--max-files', undefined, '--max-files');
|
|
1025
|
+
const maxFileBytes = parsePositiveIntFlag('--max-file-bytes', undefined, '--max-file-bytes');
|
|
1026
|
+
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
|
|
1027
|
+
console.error(`Error: Directory not found: ${dir}`);
|
|
1028
|
+
process.exit(2);
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
const { collectImportFacts, factsDocument, FactsError } = await import('../src/facts/index.js');
|
|
1032
|
+
let facts;
|
|
1033
|
+
try {
|
|
1034
|
+
facts = collectImportFacts(dir, {
|
|
1035
|
+
moduleGraph,
|
|
1036
|
+
...(maxFiles !== undefined ? { maxFiles } : {}),
|
|
1037
|
+
...(maxFileBytes !== undefined ? { maxFileBytes } : {})
|
|
1038
|
+
});
|
|
1039
|
+
} catch (error) {
|
|
1040
|
+
if (error instanceof FactsError && error.code === 'TYPESCRIPT_MISSING') {
|
|
1041
|
+
console.error(`Error: ${error.code}: ${error.message}`);
|
|
1042
|
+
process.exit(2);
|
|
1043
|
+
}
|
|
1044
|
+
throw error;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
const { summary, ...body } = factsDocument(facts, { exitCode: 0 });
|
|
1048
|
+
if (format === 'json') {
|
|
1049
|
+
// The facts envelope is the ONLY thing on stdout in json mode.
|
|
1050
|
+
console.log(JSON.stringify(buildFactsEnvelope({ target, summary, body }), null, 2));
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
printImportsSummary(target, summary, body);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// The human rendering of the facts document is its summary — the document
|
|
1057
|
+
// itself is data for another tool, and a page of import sites is not
|
|
1058
|
+
// something a person reads at a terminal.
|
|
1059
|
+
function printImportsSummary(target, summary, body) {
|
|
1060
|
+
console.log(`\nImport facts for ${target}`);
|
|
1061
|
+
console.log(` ${summary.scanned} first-party source file(s) found, ${summary.analyzed} analyzed, ${summary.unanalyzable} unanalyzable entr${summary.unanalyzable === 1 ? 'y' : 'ies'}`);
|
|
1062
|
+
const dynamic = body.imports.reduce((n, f) => n + f.dynamicUnknown, 0);
|
|
1063
|
+
console.log(` ${summary.imports} import site(s); ${dynamic} require()/import() call(s) with non-literal arguments could not be attributed`);
|
|
1064
|
+
const mg = body.moduleGraph;
|
|
1065
|
+
if (!mg.enabled) {
|
|
1066
|
+
console.log(' module graph: not walked (--no-module-graph)');
|
|
1067
|
+
} else if (mg.nodeModulesMissing) {
|
|
1068
|
+
console.log(` module graph: node_modules is not installed — ${mg.unresolved} import(s) unresolved, nothing followed`);
|
|
1069
|
+
} else {
|
|
1070
|
+
const truncated = mg.truncated ? ' (truncated at the file budget)' : '';
|
|
1071
|
+
console.log(` module graph: ${mg.filesParsed} node_modules file(s) parsed, ${summary.moduleGraph.reached} installed package copy(ies) reached, ${mg.unresolved} import(s) unresolved${truncated}`);
|
|
1072
|
+
if (mg.weakPackages.length > 0) {
|
|
1073
|
+
console.log(` ${mg.weakPackages.length} reached package(s) load modules dynamically or were not fully parsed: ${mg.weakPackages.slice(0, 5).join(', ')}${mg.weakPackages.length > 5 ? ', …' : ''}`);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
const lock = body.lockfile;
|
|
1077
|
+
if (lock.files.length > 0) {
|
|
1078
|
+
console.log(` lockfile graph: ${lock.packages.length} package(s), ${lock.edges.length} edge(s) from ${lock.files.join(', ')}`);
|
|
1079
|
+
}
|
|
1080
|
+
for (const d of [...body.workspace.diagnostics, ...lock.diagnostics]) console.log(` note: ${d}`);
|
|
1081
|
+
if (body.unanalyzable.length > 0) {
|
|
1082
|
+
console.log('\n Not analyzed (absence of evidence from these is not a negative):');
|
|
1083
|
+
for (const u of body.unanalyzable) console.log(` • ${u.file} [${u.kind}] ${u.reason}`);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
996
1087
|
// Print .dependably config notices (deprecated filename/section, unknown keys)
|
|
997
1088
|
// to stderr. Never affects exit codes or the JSON payload on stdout.
|
|
998
1089
|
function emitConfigWarnings(warnings, format) {
|
|
@@ -1559,6 +1650,7 @@ const COMMAND_HANDLERS = {
|
|
|
1559
1650
|
pin: () => runPinCommand(),
|
|
1560
1651
|
prune: () => runPruneCommand(),
|
|
1561
1652
|
unused: () => runUnusedCommand(),
|
|
1653
|
+
imports: () => runImportsCommand(),
|
|
1562
1654
|
audit: () => runAuditCommand(),
|
|
1563
1655
|
'upgrade-hashes': () => runUpgradeHashesCommand(),
|
|
1564
1656
|
dedupe: () => runDedupeCommand(),
|
package/package.json
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dependably/npm-check",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"description": "A comprehensive tool for validating, migrating, and updating npm package-lock.json files across versions 1, 2, and 3.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": "./src/index.js",
|
|
9
|
+
"./facts": {
|
|
10
|
+
"types": "./src/facts/types.d.ts",
|
|
11
|
+
"default": "./src/facts/index.js"
|
|
12
|
+
},
|
|
9
13
|
"./package.json": "./package.json"
|
|
10
14
|
},
|
|
11
15
|
"bin": {
|
|
@@ -32,7 +36,8 @@
|
|
|
32
36
|
"docker:test:node24": "docker-compose -f tests/integration/docker/docker-compose.yml run --rm test-node24-npm11",
|
|
33
37
|
"docker:test:all": "docker-compose -f tests/integration/docker/docker-compose.yml up --abort-on-container-exit",
|
|
34
38
|
"lint": "eslint .",
|
|
35
|
-
"
|
|
39
|
+
"typecheck": "tsc -p tsconfig.facts.json --noEmit",
|
|
40
|
+
"sbom":"cyclonedx-npm --output-format json --output-file sbom.json",
|
|
36
41
|
"sbom:prod": "cyclonedx-npm --omit dev --output-format json --output-file sbom-prod.json",
|
|
37
42
|
"prepublishOnly": "npm run test && npm run lint"
|
|
38
43
|
},
|
|
@@ -69,6 +74,8 @@
|
|
|
69
74
|
"npm": ">=10.0.0"
|
|
70
75
|
},
|
|
71
76
|
"dependencies": {
|
|
77
|
+
"fast-glob": "3.3.3",
|
|
78
|
+
"ignore": "7.0.5",
|
|
72
79
|
"yaml": "2.9.0"
|
|
73
80
|
},
|
|
74
81
|
"devDependencies": {
|
|
@@ -76,11 +83,21 @@
|
|
|
76
83
|
"@babel/preset-env": "7.28.6",
|
|
77
84
|
"@cyclonedx/cyclonedx-npm": "5.0.0",
|
|
78
85
|
"@eslint/js": "10.0.1",
|
|
86
|
+
"@types/node": "22.20.1",
|
|
79
87
|
"babel-jest": "30.2.0",
|
|
80
88
|
"eslint": "10.5.0",
|
|
81
89
|
"globals": "17.6.0",
|
|
82
90
|
"jest": "29.7.0",
|
|
83
|
-
"sonarqube-scanner": "
|
|
91
|
+
"sonarqube-scanner": "5.0.0",
|
|
92
|
+
"typescript": "5.9.3"
|
|
93
|
+
},
|
|
94
|
+
"peerDependencies": {
|
|
95
|
+
"typescript": ">=5.6"
|
|
96
|
+
},
|
|
97
|
+
"peerDependenciesMeta": {
|
|
98
|
+
"typescript": {
|
|
99
|
+
"optional": true
|
|
100
|
+
}
|
|
84
101
|
},
|
|
85
102
|
"allowScripts": {
|
|
86
103
|
"fsevents": true,
|
|
@@ -89,15 +106,16 @@
|
|
|
89
106
|
"overrides": {
|
|
90
107
|
"@babel/plugin-transform-modules-systemjs": "7.29.7",
|
|
91
108
|
"@ungap/structured-clone": "1.3.1",
|
|
92
|
-
"
|
|
109
|
+
"brace-expansion": "5.0.9",
|
|
110
|
+
"js-yaml": "4.3.1",
|
|
93
111
|
"picomatch": "4.0.4",
|
|
94
112
|
"glob": {
|
|
95
113
|
"minimatch": "3.1.5",
|
|
96
|
-
"brace-expansion": "1.1.
|
|
114
|
+
"brace-expansion": "1.1.18"
|
|
97
115
|
},
|
|
98
116
|
"test-exclude": {
|
|
99
117
|
"minimatch": "3.1.5",
|
|
100
|
-
"brace-expansion": "1.1.
|
|
118
|
+
"brace-expansion": "1.1.18"
|
|
101
119
|
}
|
|
102
120
|
}
|
|
103
121
|
}
|
package/src/audit-config.js
CHANGED
|
@@ -16,7 +16,8 @@ export const CONFIG_FILENAMES = ['.npm-checkrc.json', 'npm-check.config.json'];
|
|
|
16
16
|
|
|
17
17
|
// Shared, cross-tool config file (JSON, no extension) discovered by walking up
|
|
18
18
|
// from the working directory. `.dependably` is canonical; `.dependably-check` is
|
|
19
|
-
// a deprecated alias kept for the migration window (
|
|
19
|
+
// a deprecated alias kept for the migration window (config spec §7, at
|
|
20
|
+
// https://gitlab.northwardlabs.ca/moonlitlabs/dependably-spec).
|
|
20
21
|
export const SHARED_CONFIG_FILENAME = '.dependably';
|
|
21
22
|
export const DEPRECATED_SHARED_CONFIG_FILENAME = '.dependably-check';
|
|
22
23
|
// Checked in this order at each directory level (canonical wins).
|
|
@@ -34,8 +35,9 @@ export const SUPPORTED_CONFIG_VERSION = 1;
|
|
|
34
35
|
// own section but tolerated (ignored) in `common`.
|
|
35
36
|
export const APPLICABLE_SELECTORS = ['package', 'id'];
|
|
36
37
|
|
|
37
|
-
// Keys npm-check recognizes inside
|
|
38
|
-
|
|
38
|
+
// Keys npm-check recognizes inside its own section. An unrecognized key there warns;
|
|
39
|
+
// the same key in `common` is ignored, since it may belong to a sibling tool.
|
|
40
|
+
export const KNOWN_SECTION_KEYS = new Set([
|
|
39
41
|
'rules', 'exceptions', 'exclude', 'failOn',
|
|
40
42
|
'allowedRegistryHosts', 'allowedLocalFeeds', 'maxWarnings'
|
|
41
43
|
]);
|
|
@@ -58,6 +60,16 @@ export const DEFAULT_CONFIG = {
|
|
|
58
60
|
'install-scripts': ['warn', { allow: [] }],
|
|
59
61
|
'no-git-deps': 'warn',
|
|
60
62
|
'no-remote-deps': ['warn', { allowedHosts: ['registry.npmjs.org', 'npm.pkg.github.com'] }],
|
|
63
|
+
// Lockfile portability, distinct from the trust question the two rules above
|
|
64
|
+
// ask. Empty `hosts` == off: pinning is a per-project decision (a project
|
|
65
|
+
// that genuinely installs from a private registry must not be flagged), and
|
|
66
|
+
// unlike `allowedRegistryHosts` this list is NOT unioned from shared config —
|
|
67
|
+
// a pin that only ever widens would not be a pin.
|
|
68
|
+
'resolved-registry-pin': ['error', { hosts: [] }],
|
|
69
|
+
// 3 days: stricter than pnpm v11's own 1-day default and the ecosystem's
|
|
70
|
+
// 1-day baseline, deliberately — this tool is prescriptive. Lower it per
|
|
71
|
+
// project via the rule's `minDays` option.
|
|
72
|
+
'min-release-age': ['warn', { minDays: 3 }],
|
|
61
73
|
'pinned-versions': ['error', {
|
|
62
74
|
sections: ['dependencies', 'devDependencies', 'optionalDependencies'],
|
|
63
75
|
ignore: []
|
|
@@ -188,11 +200,26 @@ function unionList(a, b) {
|
|
|
188
200
|
// id replaces common's wholesale (no cross-section option deep-merge, §B.3).
|
|
189
201
|
function mergeRuleMaps(commonRules, toolRules) {
|
|
190
202
|
if (!commonRules && !toolRules) return undefined;
|
|
191
|
-
|
|
192
|
-
|
|
203
|
+
|
|
204
|
+
// A rule id in `common` that npm-check does not know belongs to a sibling tool, so it is
|
|
205
|
+
// dropped rather than merged (§8). Without this it reached mergeConfig's registry check and
|
|
206
|
+
// raised UNKNOWN_RULE, which made a shared config unusable the moment any sibling configured
|
|
207
|
+
// one of its own rules. Ids from the tool's own section are passed through untouched, so an
|
|
208
|
+
// unknown id there still errors — that one is a typo, not a sibling's.
|
|
209
|
+
const fromCommon = {};
|
|
210
|
+
if (commonRules && typeof commonRules === 'object') {
|
|
211
|
+
for (const [ruleId, value] of Object.entries(commonRules)) {
|
|
212
|
+
if (KNOWN_RULES.includes(ruleId)) fromCommon[ruleId] = value;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return { ...fromCommon, ...(toolRules && typeof toolRules === 'object' ? toolRules : {}) };
|
|
193
217
|
}
|
|
194
218
|
|
|
195
|
-
// Warn about keys npm-check does not recognize inside
|
|
219
|
+
// Warn about keys npm-check does not recognize inside its own section (§8). `common`
|
|
220
|
+
// is deliberately not checked: it is shared with the sibling tools, so a key npm-check
|
|
221
|
+
// does not know there belongs to one of them, not to a typo. This matches how unknown
|
|
222
|
+
// rule ids are already treated — tolerated in `common`, an error in the own section.
|
|
196
223
|
function unknownKeyWarnings(section, label, warnings) {
|
|
197
224
|
if (!section || typeof section !== 'object') return;
|
|
198
225
|
for (const key of Object.keys(section)) {
|
|
@@ -207,18 +234,42 @@ function unknownKeyWarnings(section, label, warnings) {
|
|
|
207
234
|
* under the npm-check section per the single merge rule (§5). Returns the audit
|
|
208
235
|
* settings plus parsed exceptions and any warnings.
|
|
209
236
|
*/
|
|
210
|
-
|
|
211
|
-
|
|
237
|
+
// Pick the tool's own section, preferring the canonical key over the deprecated
|
|
238
|
+
// alias and warning about a deprecated or duplicated section.
|
|
239
|
+
function pickToolSection(parsed, warnings) {
|
|
212
240
|
const canonical = parsed && parsed[SECTION_KEY];
|
|
213
241
|
const alias = parsed && parsed[DEPRECATED_SECTION_KEY];
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
warnings.push({ code: 'DEPRECATED_ALIAS_SECTION', message
|
|
242
|
+
if (alias !== undefined) {
|
|
243
|
+
const message = canonical === undefined
|
|
244
|
+
? `section "${DEPRECATED_SECTION_KEY}" is deprecated; rename it to "${SECTION_KEY}"`
|
|
245
|
+
: `both "${SECTION_KEY}" and "${DEPRECATED_SECTION_KEY}" sections present; using "${SECTION_KEY}"`;
|
|
246
|
+
warnings.push({ code: 'DEPRECATED_ALIAS_SECTION', message });
|
|
219
247
|
}
|
|
248
|
+
return canonical !== undefined ? canonical : alias;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// maxWarnings is a scalar: the tool section overrides common.
|
|
252
|
+
function pickMaxWarnings(common, tool) {
|
|
253
|
+
const of = (s) => (s && s.maxWarnings !== undefined ? s.maxWarnings : undefined);
|
|
254
|
+
const toolMax = of(tool);
|
|
255
|
+
return toolMax !== undefined ? toolMax : of(common);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Exceptions: common (tolerant) + own section (strict selector/rule checks).
|
|
259
|
+
function mergeExceptions(common, tool) {
|
|
260
|
+
const commonEx = parseExceptions(common && common.exceptions, {
|
|
261
|
+
source: 'common', applicableSelectors: APPLICABLE_SELECTORS
|
|
262
|
+
});
|
|
263
|
+
const ownEx = parseExceptions(tool && tool.exceptions, {
|
|
264
|
+
source: 'own', applicableSelectors: APPLICABLE_SELECTORS, knownRules: KNOWN_RULES
|
|
265
|
+
});
|
|
266
|
+
return [...commonEx, ...ownEx];
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function resolveToolSection(parsed, warnings) {
|
|
270
|
+
const common = parsed && parsed.common;
|
|
271
|
+
const tool = pickToolSection(parsed, warnings);
|
|
220
272
|
|
|
221
|
-
unknownKeyWarnings(common, 'common', warnings);
|
|
222
273
|
unknownKeyWarnings(tool, SECTION_KEY, warnings);
|
|
223
274
|
|
|
224
275
|
const settings = {};
|
|
@@ -229,22 +280,11 @@ function resolveToolSection(parsed, warnings) {
|
|
|
229
280
|
const failOn = pickFailOn(common && common.failOn, tool && tool.failOn);
|
|
230
281
|
if (failOn) settings.failOn = failOn;
|
|
231
282
|
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
const commonMax = pickMax(common);
|
|
235
|
-
if (toolMax !== undefined) settings.maxWarnings = toolMax;
|
|
236
|
-
else if (commonMax !== undefined) settings.maxWarnings = commonMax;
|
|
283
|
+
const maxWarnings = pickMaxWarnings(common, tool);
|
|
284
|
+
if (maxWarnings !== undefined) settings.maxWarnings = maxWarnings;
|
|
237
285
|
|
|
238
286
|
settings.exclude = unionList(common && common.exclude, tool && tool.exclude);
|
|
239
|
-
|
|
240
|
-
// Exceptions: common (tolerant) + own section (strict selector/rule checks).
|
|
241
|
-
const commonEx = parseExceptions(common && common.exceptions, {
|
|
242
|
-
source: 'common', applicableSelectors: APPLICABLE_SELECTORS
|
|
243
|
-
});
|
|
244
|
-
const ownEx = parseExceptions(tool && tool.exceptions, {
|
|
245
|
-
source: 'own', applicableSelectors: APPLICABLE_SELECTORS, knownRules: KNOWN_RULES
|
|
246
|
-
});
|
|
247
|
-
settings.exceptions = [...commonEx, ...ownEx];
|
|
287
|
+
settings.exceptions = mergeExceptions(common, tool);
|
|
248
288
|
|
|
249
289
|
return settings;
|
|
250
290
|
}
|
|
@@ -342,47 +382,68 @@ export function loadSharedConfig(cwd = process.cwd()) {
|
|
|
342
382
|
};
|
|
343
383
|
}
|
|
344
384
|
|
|
385
|
+
// An explicit `--config` file: either the shared (sectioned) shape or a legacy
|
|
386
|
+
// flat tool-config. Fills the same slots the discovery path does.
|
|
387
|
+
function loadExplicitConfig(explicitPath, shared) {
|
|
388
|
+
const configPath = path.resolve(explicitPath);
|
|
389
|
+
if (!fs.existsSync(configPath)) {
|
|
390
|
+
throw new AuditConfigError(`Config file not found: ${configPath}`, 'CONFIG_NOT_FOUND');
|
|
391
|
+
}
|
|
392
|
+
const parsed = readJsonConfig(configPath);
|
|
393
|
+
if (!isSharedShape(configPath, parsed)) {
|
|
394
|
+
// Legacy flat tool-config (.npm-checkrc.json shape) given explicitly.
|
|
395
|
+
return { configPath, toolConfig: parsed };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
validateSharedShape(parsed, configPath);
|
|
399
|
+
const warnings = [];
|
|
400
|
+
const settings = resolveToolSection(parsed, warnings);
|
|
401
|
+
shared.warnings.push(...warnings);
|
|
402
|
+
return {
|
|
403
|
+
configPath,
|
|
404
|
+
toolConfig: {
|
|
405
|
+
...(settings.rules ? { rules: settings.rules } : {}),
|
|
406
|
+
...(settings.maxWarnings !== undefined ? { maxWarnings: settings.maxWarnings } : {})
|
|
407
|
+
},
|
|
408
|
+
explicitSharedHosts: collectSharedHosts(parsed),
|
|
409
|
+
// { exceptions, exclude, failOn } from an explicit shared-shape file
|
|
410
|
+
explicitExtras: { exceptions: settings.exceptions, exclude: settings.exclude, failOn: settings.failOn || null }
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Discover a tool-specific config in the working directory (fallback for
|
|
415
|
+
// back-compat; the shared `.dependably` is the primary source).
|
|
416
|
+
function discoverToolConfig(cwd) {
|
|
417
|
+
for (const name of CONFIG_FILENAMES) {
|
|
418
|
+
const candidate = path.join(cwd, name);
|
|
419
|
+
if (fs.existsSync(candidate)) {
|
|
420
|
+
return { configPath: candidate, toolConfig: readJsonConfig(candidate) };
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return { configPath: null, toolConfig: {} };
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// failOn.count is the standard form of maxWarnings; a legacy maxWarnings in a
|
|
427
|
+
// tool-config still wins if it was set (it flowed through mergeConfig).
|
|
428
|
+
function applyFailOn(config, failOn, maxWarningsAlreadySet) {
|
|
429
|
+
if (!failOn) return;
|
|
430
|
+
if (failOn.count !== undefined && !maxWarningsAlreadySet) {
|
|
431
|
+
if (typeof failOn.count !== 'number' || !Number.isInteger(failOn.count) || failOn.count < 0) {
|
|
432
|
+
throw new AuditConfigError(`failOn.count must be a non-negative integer, got: ${JSON.stringify(failOn.count)}`, 'INVALID_FAIL_ON');
|
|
433
|
+
}
|
|
434
|
+
config.maxWarnings = failOn.count;
|
|
435
|
+
}
|
|
436
|
+
config.failOnSeverity = failOn.severity || null;
|
|
437
|
+
}
|
|
438
|
+
|
|
345
439
|
export function loadAuditConfig(cwd = process.cwd(), explicitPath = null) {
|
|
346
440
|
// The shared `.dependably` (discovered by walking up to the repo root) is the
|
|
347
441
|
// PRIMARY config source. A tool-specific `.npm-checkrc.json` (or an explicit
|
|
348
442
|
// `--config`) overrides it.
|
|
349
443
|
const shared = loadSharedConfig(cwd);
|
|
350
444
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
let explicitSharedHosts = [];
|
|
354
|
-
let explicitExtras = null; // { exceptions, exclude, failOn } from an explicit shared-shape file
|
|
355
|
-
|
|
356
|
-
if (explicitPath) {
|
|
357
|
-
configPath = path.resolve(explicitPath);
|
|
358
|
-
if (!fs.existsSync(configPath)) {
|
|
359
|
-
throw new AuditConfigError(`Config file not found: ${configPath}`, 'CONFIG_NOT_FOUND');
|
|
360
|
-
}
|
|
361
|
-
const parsed = readJsonConfig(configPath);
|
|
362
|
-
if (isSharedShape(configPath, parsed)) {
|
|
363
|
-
validateSharedShape(parsed, configPath);
|
|
364
|
-
const warnings = [];
|
|
365
|
-
const settings = resolveToolSection(parsed, warnings);
|
|
366
|
-
shared.warnings.push(...warnings);
|
|
367
|
-
toolConfig = { ...(settings.rules ? { rules: settings.rules } : {}), ...(settings.maxWarnings !== undefined ? { maxWarnings: settings.maxWarnings } : {}) };
|
|
368
|
-
explicitSharedHosts = collectSharedHosts(parsed);
|
|
369
|
-
explicitExtras = { exceptions: settings.exceptions, exclude: settings.exclude, failOn: settings.failOn || null };
|
|
370
|
-
} else {
|
|
371
|
-
// Legacy flat tool-config (.npm-checkrc.json shape) given explicitly.
|
|
372
|
-
toolConfig = parsed;
|
|
373
|
-
}
|
|
374
|
-
} else {
|
|
375
|
-
// Discover a tool-specific config in the working directory (fallback for
|
|
376
|
-
// back-compat; the shared `.dependably` above is the primary source).
|
|
377
|
-
for (const name of CONFIG_FILENAMES) {
|
|
378
|
-
const candidate = path.join(cwd, name);
|
|
379
|
-
if (fs.existsSync(candidate)) {
|
|
380
|
-
configPath = candidate;
|
|
381
|
-
toolConfig = readJsonConfig(candidate);
|
|
382
|
-
break;
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
}
|
|
445
|
+
const { configPath, toolConfig, explicitSharedHosts = [], explicitExtras = null } =
|
|
446
|
+
explicitPath ? loadExplicitConfig(explicitPath, shared) : discoverToolConfig(cwd);
|
|
386
447
|
|
|
387
448
|
// Shared audit settings are the base; the tool-specific config overrides them.
|
|
388
449
|
const userConfig = { ...shared.auditSettings, ...toolConfig };
|
|
@@ -395,18 +456,9 @@ export function loadAuditConfig(cwd = process.cwd(), explicitPath = null) {
|
|
|
395
456
|
extendAllowedHosts(config, hosts);
|
|
396
457
|
}
|
|
397
458
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
if (failOn) {
|
|
402
|
-
if (failOn.count !== undefined && toolConfig.maxWarnings === undefined && shared.auditSettings.maxWarnings === undefined) {
|
|
403
|
-
if (typeof failOn.count !== 'number' || !Number.isInteger(failOn.count) || failOn.count < 0) {
|
|
404
|
-
throw new AuditConfigError(`failOn.count must be a non-negative integer, got: ${JSON.stringify(failOn.count)}`, 'INVALID_FAIL_ON');
|
|
405
|
-
}
|
|
406
|
-
config.maxWarnings = failOn.count;
|
|
407
|
-
}
|
|
408
|
-
config.failOnSeverity = failOn.severity || null;
|
|
409
|
-
}
|
|
459
|
+
const maxWarningsAlreadySet =
|
|
460
|
+
toolConfig.maxWarnings !== undefined || shared.auditSettings.maxWarnings !== undefined;
|
|
461
|
+
applyFailOn(config, explicitExtras ? explicitExtras.failOn : shared.failOn, maxWarningsAlreadySet);
|
|
410
462
|
|
|
411
463
|
config.exceptions = explicitExtras ? explicitExtras.exceptions : shared.exceptions;
|
|
412
464
|
config.exclude = explicitExtras ? explicitExtras.exclude : shared.exclude;
|