@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
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// src/facts/specifier.js
|
|
2
|
+
// Module specifier → npm package name. This coexists with usage-scanner.js's
|
|
3
|
+
// `specifierToPackageName` on purpose: that one answers "which declared
|
|
4
|
+
// dependency does this mention count towards" for the unused-dependency
|
|
5
|
+
// heuristic (and so treats `@types/foo` specially); this one is the language
|
|
6
|
+
// fact — the package a specifier names, or nothing when it names no package.
|
|
7
|
+
import { builtinModules } from 'node:module';
|
|
8
|
+
|
|
9
|
+
const BUILTINS = new Set(builtinModules);
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Map a module specifier to the npm package it belongs to (lower-cased, since
|
|
13
|
+
* npm names are case-insensitively unique), or undefined when the specifier is
|
|
14
|
+
* not a package import: a relative/absolute path, a builtin, a `#imports`
|
|
15
|
+
* key, a data:/file: URL, or a tsconfig/jsconfig path alias.
|
|
16
|
+
*
|
|
17
|
+
* @param {string} spec
|
|
18
|
+
* @param {ReadonlySet<string>} aliasPrefixes
|
|
19
|
+
* @returns {string | undefined}
|
|
20
|
+
*/
|
|
21
|
+
export function specifierToPackage(spec, aliasPrefixes) {
|
|
22
|
+
if (spec.length === 0) return undefined;
|
|
23
|
+
if (spec.startsWith('.') || spec.startsWith('/') || spec.startsWith('#')) return undefined;
|
|
24
|
+
if (spec.startsWith('node:') || spec.startsWith('data:') || spec.startsWith('file:')) {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
if (BUILTINS.has(spec.split('/')[0])) return undefined;
|
|
28
|
+
|
|
29
|
+
for (const alias of aliasPrefixes) {
|
|
30
|
+
if (spec === alias || spec.startsWith(`${alias}/`)) return undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const parts = spec.split('/');
|
|
34
|
+
if (spec.startsWith('@')) {
|
|
35
|
+
if (parts.length < 2) return undefined;
|
|
36
|
+
return `${parts[0]}/${parts[1]}`.toLowerCase();
|
|
37
|
+
}
|
|
38
|
+
return parts[0].toLowerCase();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* tsconfig/jsconfig `paths` keys ("@app/*", "utils") → alias bases ("@app", "utils").
|
|
43
|
+
* @param {string} key
|
|
44
|
+
* @returns {string}
|
|
45
|
+
*/
|
|
46
|
+
export function aliasBaseFromPathsKey(key) {
|
|
47
|
+
return key.endsWith('/*') ? key.slice(0, -2) : key;
|
|
48
|
+
}
|
package/src/facts/ts.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// src/facts/ts.js
|
|
2
|
+
// `typescript` is an OPTIONAL peer dependency: the lockfile commands never need
|
|
3
|
+
// it, and a consumer of the package root must not pay for a 10 MB compiler it
|
|
4
|
+
// never loads. It is therefore resolved lazily and synchronously (createRequire,
|
|
5
|
+
// the same pattern `parser.js` uses for `yaml`), the first time a facts module
|
|
6
|
+
// actually parses something, and its absence is a coded error — the CLI maps
|
|
7
|
+
// `TYPESCRIPT_MISSING` to a usage error (exit 2) with an install hint rather
|
|
8
|
+
// than a stack trace.
|
|
9
|
+
//
|
|
10
|
+
// NOTHING here runs at module load. `createRequire` is built inside
|
|
11
|
+
// `loadTypeScript()`, from `__filename` when a CJS bundle defines it and from
|
|
12
|
+
// `import.meta.url` otherwise: esbuild rewrites `import.meta.url` to
|
|
13
|
+
// `undefined` in a CJS bundle, and `createRequire(undefined)` throws
|
|
14
|
+
// `ERR_INVALID_ARG_VALUE` — at import time, as a plain Error no
|
|
15
|
+
// `TYPESCRIPT_MISSING` handler would ever see (adversarial review). Importing
|
|
16
|
+
// the barrel must never throw; only a parse can.
|
|
17
|
+
import { createRequire } from 'node:module';
|
|
18
|
+
import { FactsError } from './errors.js';
|
|
19
|
+
|
|
20
|
+
/** @type {typeof import('typescript') | undefined} */
|
|
21
|
+
let cached;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The TypeScript compiler API, loaded once.
|
|
25
|
+
* @returns {typeof import('typescript')}
|
|
26
|
+
* @throws {FactsError} `TYPESCRIPT_MISSING` when the peer is not installed.
|
|
27
|
+
*/
|
|
28
|
+
export function loadTypeScript() {
|
|
29
|
+
if (cached) return cached;
|
|
30
|
+
// `__filename` is only defined in a CommonJS context (a bundle); in ESM the
|
|
31
|
+
// `typeof` guard keeps the reference from throwing.
|
|
32
|
+
const anchor = typeof __filename !== 'undefined' ? __filename : import.meta.url;
|
|
33
|
+
const require = createRequire(anchor);
|
|
34
|
+
try {
|
|
35
|
+
cached = /** @type {typeof import('typescript')} */ (require('typescript'));
|
|
36
|
+
} catch (err) {
|
|
37
|
+
const code = err && typeof err === 'object' && 'code' in err ? err.code : undefined;
|
|
38
|
+
if (code !== 'MODULE_NOT_FOUND' && code !== 'ERR_MODULE_NOT_FOUND') throw err;
|
|
39
|
+
throw new FactsError(
|
|
40
|
+
'TYPESCRIPT_MISSING',
|
|
41
|
+
'import facts need the `typescript` package (an optional peer dependency of @dependably/npm-check), ' +
|
|
42
|
+
'which is not installed. Install it alongside npm-check: `npm install --save-dev typescript` ' +
|
|
43
|
+
'(any 5.6+ release).'
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
return cached;
|
|
47
|
+
}
|
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
// src/facts/types.d.ts
|
|
2
|
+
// Hand-written declarations for `@dependably/npm-check/facts`. The JS
|
|
3
|
+
// modules under src/facts/ reference these through JSDoc
|
|
4
|
+
// (`@typedef {import('./types.d.ts').X} X`) and are type-checked against
|
|
5
|
+
// them with `npm run typecheck`; consumers get them through the subpath
|
|
6
|
+
// export's `types` condition. Keep every exported runtime symbol declared
|
|
7
|
+
// here too — this file IS the public surface.
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------- scan ----
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* How a source file references a module. The string values are the same
|
|
13
|
+
* ones sbom-reach's `EvidenceKind` uses for its npm evidence, so a consumer
|
|
14
|
+
* can carry them through unchanged.
|
|
15
|
+
*/
|
|
16
|
+
export type ImportKind = 'import' | 'require' | 'dynamic-import' | 'export-from' | 'type-only-import';
|
|
17
|
+
|
|
18
|
+
/** One module reference found in a source file. */
|
|
19
|
+
export interface ImportSite {
|
|
20
|
+
specifier: string;
|
|
21
|
+
/** 1-based. */
|
|
22
|
+
line: number;
|
|
23
|
+
/** The statement's text, whitespace-collapsed, capped at 200 chars. */
|
|
24
|
+
snippet: string;
|
|
25
|
+
kind: ImportKind;
|
|
26
|
+
/**
|
|
27
|
+
* Imported/required binding names visible at this site: named-import /
|
|
28
|
+
* named-export-from ORIGINAL names (`import { a, b as c }` → "a", "b" —
|
|
29
|
+
* never the local alias), `require()`/dynamic-`import()` destructured
|
|
30
|
+
* names, and exactly ONE level of property access on a default/namespace
|
|
31
|
+
* import's local identifier (`_.template(x)` → "template"). Parse-level
|
|
32
|
+
* only: no type checker, no cross-file aliasing, no chasing past one
|
|
33
|
+
* property-access level. Absent/empty means "no binding names observed at
|
|
34
|
+
* this site" — NOT proof nothing was used; see `opaque`.
|
|
35
|
+
*/
|
|
36
|
+
bindings?: string[];
|
|
37
|
+
/**
|
|
38
|
+
* The subset of `bindings` whose local identifier is actually REFERENCED
|
|
39
|
+
* somewhere in the module body after the declaration. A property-access
|
|
40
|
+
* binding is a reference by construction. Any occurrence counts (call,
|
|
41
|
+
* argument, spread, re-export, shorthand property): this is "referenced",
|
|
42
|
+
* not "called" — over-reporting use is the safe direction. A shadowing
|
|
43
|
+
* local is deliberately NOT excluded for the same reason. Absent when
|
|
44
|
+
* nothing was referenced.
|
|
45
|
+
*/
|
|
46
|
+
referenced?: string[];
|
|
47
|
+
/**
|
|
48
|
+
* True when this site's default/namespace binding (or a bare
|
|
49
|
+
* `export * from` / `export * as ns from`) is used in a way a parse-level
|
|
50
|
+
* scan cannot resolve to specific property names — assigned, passed as an
|
|
51
|
+
* argument, spread, exported, returned, computed access, OR called /
|
|
52
|
+
* constructed / tagged / rendered directly. An opaque site "could use
|
|
53
|
+
* anything": a consumer must never read it as evidence that some symbol
|
|
54
|
+
* was NOT used.
|
|
55
|
+
*/
|
|
56
|
+
opaque?: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface ScanResult {
|
|
60
|
+
sites: ImportSite[];
|
|
61
|
+
/** Count of require()/import() calls with non-literal arguments. */
|
|
62
|
+
dynamicUnknown: number;
|
|
63
|
+
/**
|
|
64
|
+
* Set only for `.svelte` files where `<script>` extraction is suspected of
|
|
65
|
+
* having gone wrong — the extracted text failed to parse, or a
|
|
66
|
+
* `<script`/`</script>` tag was found outside every span the extraction
|
|
67
|
+
* accounted for. Sites collected before/around the problem are still
|
|
68
|
+
* present; their absence for some package is not a clean negative.
|
|
69
|
+
*/
|
|
70
|
+
parseErrors?: string[];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Parse-only scan of one source file with the TypeScript compiler API.
|
|
75
|
+
* `fileName` decides the script kind (`.tsx`, `.svelte`, …) and nothing else.
|
|
76
|
+
* Throws `FactsError` (`TYPESCRIPT_MISSING`) when `typescript` is not installed.
|
|
77
|
+
*/
|
|
78
|
+
export function scanSource(fileName: string, content: string): ScanResult;
|
|
79
|
+
|
|
80
|
+
// ------------------------------------------------------------- resolve ----
|
|
81
|
+
|
|
82
|
+
export interface PackageInfo {
|
|
83
|
+
/** The name from the package's own package.json (falls back to the directory name). */
|
|
84
|
+
name: string;
|
|
85
|
+
/**
|
|
86
|
+
* The node_modules directory name (scope included). Differs from `name`
|
|
87
|
+
* for an aliased install (`"string-width-cjs": "npm:string-width@^4"`).
|
|
88
|
+
*/
|
|
89
|
+
dirName: string;
|
|
90
|
+
version: string;
|
|
91
|
+
/** Absolute path of the package root — the directory directly under node_modules. */
|
|
92
|
+
root: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export type Resolution =
|
|
96
|
+
/** A source file the walker should parse. */
|
|
97
|
+
| { kind: 'file'; path: string; pkg: PackageInfo | undefined }
|
|
98
|
+
/** A non-code file (JSON, WASM, CSS, native addon…) — a real edge, nothing to parse. */
|
|
99
|
+
| { kind: 'asset'; path: string; pkg: PackageInfo | undefined }
|
|
100
|
+
| { kind: 'builtin' }
|
|
101
|
+
/** A first-party path alias (tsconfig paths). */
|
|
102
|
+
| { kind: 'alias' }
|
|
103
|
+
| { kind: 'unresolved'; reason: string };
|
|
104
|
+
|
|
105
|
+
export type ResolveMode = 'import' | 'require';
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* A Node-style module resolver: symlink-aware (pnpm's `.pnpm` layout),
|
|
109
|
+
* `exports`/`imports` maps with import-vs-require conditions, `main`,
|
|
110
|
+
* `module`, `index.*`, `.js`→`.ts` probing, builtins. Reports rather than
|
|
111
|
+
* guesses: aliases, `.d.ts`-only targets, blocked exports and uninstalled
|
|
112
|
+
* packages come back as their own `Resolution` kinds.
|
|
113
|
+
*/
|
|
114
|
+
export class ModuleResolver {
|
|
115
|
+
constructor(aliasPrefixes?: ReadonlySet<string>);
|
|
116
|
+
readonly aliasPrefixes: ReadonlySet<string>;
|
|
117
|
+
resolve(fromFile: string, specifier: string, mode: ResolveMode): Resolution;
|
|
118
|
+
/**
|
|
119
|
+
* The package a file belongs to, from its path alone: the directory right
|
|
120
|
+
* after the LAST `node_modules/` segment (two for a scope); undefined for
|
|
121
|
+
* a first-party path.
|
|
122
|
+
*/
|
|
123
|
+
packageOf(file: string): PackageInfo | undefined;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** The package root for a path inside node_modules; undefined for a first-party path. */
|
|
127
|
+
export function packageRootOf(file: string): string | undefined;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Resolves a subpath (or `#import` key) against a package.json `exports` /
|
|
131
|
+
* `imports` value. Returns the target string, `null` when the map explicitly
|
|
132
|
+
* blocks it, or `undefined` when nothing matched.
|
|
133
|
+
*/
|
|
134
|
+
export function resolveExports(map: unknown, subpath: string, conditions: ReadonlySet<string>): string | null | undefined;
|
|
135
|
+
|
|
136
|
+
// --------------------------------------------------------- modulegraph ----
|
|
137
|
+
|
|
138
|
+
export interface GraphImporter {
|
|
139
|
+
/** Absolute path of the importing file. */
|
|
140
|
+
file: string;
|
|
141
|
+
line: number;
|
|
142
|
+
snippet: string;
|
|
143
|
+
kind: ImportKind;
|
|
144
|
+
bindings?: string[];
|
|
145
|
+
referenced?: string[];
|
|
146
|
+
opaque?: boolean;
|
|
147
|
+
/** Key of the package the importing file belongs to; undefined for first-party code. */
|
|
148
|
+
fromPackage?: string;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export interface ReachedPackage {
|
|
152
|
+
/**
|
|
153
|
+
* `${name}@${version}\0${root}` — one entry per INSTALLED COPY, since two
|
|
154
|
+
* copies of one version can differ only by location.
|
|
155
|
+
*/
|
|
156
|
+
key: string;
|
|
157
|
+
name: string;
|
|
158
|
+
/** See `PackageInfo.dirName`. */
|
|
159
|
+
dirName: string;
|
|
160
|
+
version: string;
|
|
161
|
+
root: string;
|
|
162
|
+
/**
|
|
163
|
+
* Every import site from OUTSIDE this package that resolved into it —
|
|
164
|
+
* first-party and other packages' files alike; a package's own internal
|
|
165
|
+
* relative imports are traversed but never listed here.
|
|
166
|
+
*/
|
|
167
|
+
importers: GraphImporter[];
|
|
168
|
+
/**
|
|
169
|
+
* The chain of package keys along which this package was FIRST reached
|
|
170
|
+
* (breadth-first over files from the roots, so short in practice, not a
|
|
171
|
+
* proven shortest package chain), this package last.
|
|
172
|
+
*/
|
|
173
|
+
chain: string[];
|
|
174
|
+
/** A file in this package has a non-literal require()/import(). */
|
|
175
|
+
dynamic: boolean;
|
|
176
|
+
/** A file in this package was not parsed (size cap, unreadable) or has a dangling relative import; its edges are unknown. */
|
|
177
|
+
incomplete: boolean;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export interface ModuleGraph {
|
|
181
|
+
reached: Map<string, ReachedPackage>;
|
|
182
|
+
/** `${name}@${version}` (lower-cased name, both `name` and `dirName` spellings) → every reached copy of it. */
|
|
183
|
+
byNameVersion: Map<string, ReachedPackage[]>;
|
|
184
|
+
/** lower-cased name (both spellings) → every reached copy, any version. */
|
|
185
|
+
byName: Map<string, ReachedPackage[]>;
|
|
186
|
+
filesParsed: number;
|
|
187
|
+
filesSkippedForSize: number;
|
|
188
|
+
/** Files that resolved after the budget was hit and were therefore not parsed. */
|
|
189
|
+
filesPastBudget: number;
|
|
190
|
+
unresolved: number;
|
|
191
|
+
/**
|
|
192
|
+
* Bare specifiers the resolver could not follow, by the PACKAGE NAME they
|
|
193
|
+
* name (lower-cased) → where and why (at most five sites per name).
|
|
194
|
+
*/
|
|
195
|
+
unresolvedByName: Map<string, { file: string; reason: string }[]>;
|
|
196
|
+
/** The file budget stopped the walk; packages past the frontier are unknown, not absent. */
|
|
197
|
+
truncated: boolean;
|
|
198
|
+
/** `name@version` of packages flagged dynamic or incomplete (deduplicated, sorted). */
|
|
199
|
+
weakPackages: string[];
|
|
200
|
+
/** node_modules files that resolved but were not parsed (unreadable, oversized), absolute paths. */
|
|
201
|
+
unanalyzable: { file: string; reason: string }[];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export interface WalkOptions {
|
|
205
|
+
/** Absolute path; used to relativize file names handed to `scan`. */
|
|
206
|
+
srcDir: string;
|
|
207
|
+
resolver: ModuleResolver;
|
|
208
|
+
/** First-party files with their already-computed scans — the roots. */
|
|
209
|
+
roots: Iterable<{ file: string; scan: ScanResult }>;
|
|
210
|
+
/** Parses a node_modules file. Injected so the caller's scanner (and any caching) is reused. */
|
|
211
|
+
scan: (relFile: string, content: string) => ScanResult;
|
|
212
|
+
/** Stop after this many node_modules files have been parsed. Default `DEFAULT_MAX_FILES`. */
|
|
213
|
+
maxFiles?: number;
|
|
214
|
+
/** Skip (and flag) files larger than this many bytes. Default `DEFAULT_MAX_FILE_BYTES`. */
|
|
215
|
+
maxFileBytes?: number;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export const DEFAULT_MAX_FILES: number;
|
|
219
|
+
export const DEFAULT_MAX_FILE_BYTES: number;
|
|
220
|
+
export function packageKey(pkg: PackageInfo): string;
|
|
221
|
+
export function walkModuleGraph(opts: WalkOptions): ModuleGraph;
|
|
222
|
+
|
|
223
|
+
// ------------------------------------------------------ lockfile-graph ----
|
|
224
|
+
|
|
225
|
+
export interface DiscoveredPackage {
|
|
226
|
+
name: string;
|
|
227
|
+
version: string;
|
|
228
|
+
/**
|
|
229
|
+
* SPDX license expression verbatim from the lockfile entry (npm mirrors
|
|
230
|
+
* the package's own `license` field). Absent when the entry never carried
|
|
231
|
+
* one; never guessed. pnpm-lock.yaml carries no license data at all.
|
|
232
|
+
*/
|
|
233
|
+
license?: string;
|
|
234
|
+
/**
|
|
235
|
+
* Whole-tree dev/runtime declaration, tri-state: `true` — reachable ONLY
|
|
236
|
+
* through devDependencies edges; `false` — some path reaches it without a
|
|
237
|
+
* dev edge ("runtime anywhere wins"); absent — the lockfile says nothing.
|
|
238
|
+
* package-lock.json states this for EVERY entry (npm's per-entry flags are
|
|
239
|
+
* a computed answer); pnpm-lock.yaml only for importer-direct packages.
|
|
240
|
+
*/
|
|
241
|
+
devDeclared?: boolean;
|
|
242
|
+
/**
|
|
243
|
+
* "optional" only, when npm asserts the package is reachable EXCLUSIVELY
|
|
244
|
+
* through optionalDependencies. Absent otherwise — never asserted.
|
|
245
|
+
*/
|
|
246
|
+
scope?: 'optional';
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** One dependency edge; both ends are "name@version" keys matching a `DiscoveredPackage`. */
|
|
250
|
+
export interface DependencyEdge {
|
|
251
|
+
from: string;
|
|
252
|
+
to: string;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export interface LockfileGraph {
|
|
256
|
+
packages: DiscoveredPackage[];
|
|
257
|
+
/** "name@version" keys the project's own package.json directly depends on. */
|
|
258
|
+
rootDependencies: string[];
|
|
259
|
+
/** Edges among the resolved closure; does not include root-level edges. */
|
|
260
|
+
edges: DependencyEdge[];
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** `discoverLockfileGraphs`' result: every lockfile under the tree merged into one graph. */
|
|
264
|
+
export interface LockfileDiscovery extends LockfileGraph {
|
|
265
|
+
/** Absolute paths of the lockfiles that parsed, in the order they were merged. */
|
|
266
|
+
files: string[];
|
|
267
|
+
/** `unparseable … at <rel>: <why>` per lockfile that failed; `NO_LOCKFILE: …` when none was found. */
|
|
268
|
+
diagnostics: string[];
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function mergeDiscovered(into: DiscoveredPackage, other: DiscoveredPackage): void;
|
|
272
|
+
export function parsePackageLockJsonGraph(path: string): LockfileGraph;
|
|
273
|
+
export function parsePackageLockJson(path: string): DiscoveredPackage[];
|
|
274
|
+
export function parsePnpmLockYamlGraph(path: string): LockfileGraph;
|
|
275
|
+
export function parsePnpmLockYaml(path: string): DiscoveredPackage[];
|
|
276
|
+
export function discoverLockfileGraphs(srcDir: string): LockfileDiscovery;
|
|
277
|
+
|
|
278
|
+
// ----------------------------------------------------------- specifier ----
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* The npm package a specifier names (lower-cased), or undefined for a
|
|
282
|
+
* relative/absolute path, a builtin, a `#imports` key, a data:/file: URL, or
|
|
283
|
+
* a tsconfig/jsconfig path alias.
|
|
284
|
+
*/
|
|
285
|
+
export function specifierToPackage(spec: string, aliasPrefixes: ReadonlySet<string>): string | undefined;
|
|
286
|
+
/** tsconfig/jsconfig `paths` keys ("@app/*", "utils") → alias bases ("@app", "utils"). */
|
|
287
|
+
export function aliasBaseFromPathsKey(key: string): string;
|
|
288
|
+
|
|
289
|
+
// ----------------------------------------------------------- workspace ----
|
|
290
|
+
|
|
291
|
+
export type DepScope = 'runtime' | 'dev';
|
|
292
|
+
|
|
293
|
+
export interface Workspace {
|
|
294
|
+
/** Names of package.json manifests found in the tree = first-party packages (lower-cased). */
|
|
295
|
+
firstPartyNames: Set<string>;
|
|
296
|
+
/** name → runtime|dev; "runtime anywhere wins" across all manifests. */
|
|
297
|
+
depScopes: Map<string, DepScope>;
|
|
298
|
+
/** tsconfig/jsconfig paths alias bases; specifiers matching these are never package imports. */
|
|
299
|
+
aliasPrefixes: Set<string>;
|
|
300
|
+
/** First-party source files, absolute paths, sorted. */
|
|
301
|
+
sourceFiles: string[];
|
|
302
|
+
diagnostics: string[];
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function discoverWorkspace(srcDir: string): Workspace;
|
|
306
|
+
|
|
307
|
+
// ------------------------------------------------------------- collect ----
|
|
308
|
+
|
|
309
|
+
/** An `ImportSite` with what it names and what it loads. */
|
|
310
|
+
export interface ResolvedSite extends ImportSite {
|
|
311
|
+
/** The package the specifier names (see `specifierToPackage`); undefined for a non-package import. */
|
|
312
|
+
package: string | undefined;
|
|
313
|
+
/**
|
|
314
|
+
* The installed package copy the specifier resolved INTO, when it did.
|
|
315
|
+
* A fact, not a match: compare `name`/`dirName` against `package` before
|
|
316
|
+
* treating it as "the installed copy of the package this site names".
|
|
317
|
+
*/
|
|
318
|
+
installed?: { name: string; dirName: string; version: string; root: string };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export interface FirstPartyFile {
|
|
322
|
+
/** Absolute path. */
|
|
323
|
+
file: string;
|
|
324
|
+
/** `srcDir`-relative POSIX path (realpath-aware). */
|
|
325
|
+
rel: string;
|
|
326
|
+
scan: ScanResult;
|
|
327
|
+
sites: ResolvedSite[];
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export type UnanalyzableKind =
|
|
331
|
+
/** A first-party source file that could not be read; its imports are unknown. */
|
|
332
|
+
| 'file'
|
|
333
|
+
/** A first-party `.svelte` file whose `<script>` extraction reported a problem; sites are still present. */
|
|
334
|
+
| 'file-partial'
|
|
335
|
+
/** A node_modules file the walk resolved but did not parse (unreadable, oversized). */
|
|
336
|
+
| 'node-modules-file'
|
|
337
|
+
/** The walk stopped on its file budget; everything past the frontier is unobserved. */
|
|
338
|
+
| 'walk';
|
|
339
|
+
|
|
340
|
+
export interface UnanalyzableEntry {
|
|
341
|
+
/** `srcDir`-relative POSIX path (or `node_modules` for a `walk` entry). */
|
|
342
|
+
file: string;
|
|
343
|
+
kind: UnanalyzableKind;
|
|
344
|
+
reason: string;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export interface CollectOptions {
|
|
348
|
+
/** Follow imports through node_modules. Default true. */
|
|
349
|
+
moduleGraph?: boolean;
|
|
350
|
+
maxFiles?: number;
|
|
351
|
+
maxFileBytes?: number;
|
|
352
|
+
/** The scanner to use for first-party AND node_modules files. Default `scanSource`. */
|
|
353
|
+
scan?: (relFile: string, content: string) => ScanResult;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export interface ImportFacts {
|
|
357
|
+
/** Absolute. */
|
|
358
|
+
srcDir: string;
|
|
359
|
+
/** `realpath(srcDir)`, or `srcDir` when that fails. */
|
|
360
|
+
realSrcDir: string;
|
|
361
|
+
workspace: Workspace;
|
|
362
|
+
files: FirstPartyFile[];
|
|
363
|
+
/** Undefined when `moduleGraph: false`. */
|
|
364
|
+
graph: ModuleGraph | undefined;
|
|
365
|
+
/** The walk reached nothing, something was unresolved, and there is no `node_modules` directory. */
|
|
366
|
+
nodeModulesMissing: boolean;
|
|
367
|
+
lockfile: LockfileDiscovery;
|
|
368
|
+
/** ALWAYS present (empty when nothing was skipped). Non-empty means the search was incomplete. */
|
|
369
|
+
unanalyzable: UnanalyzableEntry[];
|
|
370
|
+
/** Sum of `dynamicUnknown` over first-party files. */
|
|
371
|
+
dynamicUnknownTotal: number;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export function collectImportFacts(srcDir: string, options?: CollectOptions): ImportFacts;
|
|
375
|
+
/**
|
|
376
|
+
* Realpath-aware relativizer producing POSIX paths: of the two spellings of
|
|
377
|
+
* `srcDir` (as given, realpath), the one the file sits under with fewer `..`
|
|
378
|
+
* segments wins. `srcDir` itself relativizes to `''`, as sbom-reach's `relOf`.
|
|
379
|
+
*/
|
|
380
|
+
export function makeRelOf(srcDir: string, realSrcDir: string): (file: string) => string;
|
|
381
|
+
|
|
382
|
+
// ------------------------------------------------------------ document ----
|
|
383
|
+
|
|
384
|
+
export interface FactsSummary {
|
|
385
|
+
/** First-party source files the workspace discovery found. */
|
|
386
|
+
scanned: number;
|
|
387
|
+
/** Of those, files read and parsed — the length of `imports`. */
|
|
388
|
+
analyzed: number;
|
|
389
|
+
/** Entries in `unanalyzable`, of every kind. Non-zero means the search was incomplete. */
|
|
390
|
+
unanalyzable: number;
|
|
391
|
+
/** Import sites summed over every first-party file. */
|
|
392
|
+
imports: number;
|
|
393
|
+
moduleGraph: { filesParsed: number; reached: number; unresolved: number; truncated: boolean };
|
|
394
|
+
/** The process exit code, `0` for every successful scan. */
|
|
395
|
+
exitCode: number;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export interface DocumentSite {
|
|
399
|
+
specifier: string;
|
|
400
|
+
package: string | null;
|
|
401
|
+
line: number;
|
|
402
|
+
snippet: string;
|
|
403
|
+
kind: ImportKind;
|
|
404
|
+
bindings: string[];
|
|
405
|
+
referenced: string[];
|
|
406
|
+
opaque: boolean;
|
|
407
|
+
installed?: { name: string; dirName: string; version: string; root: string };
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export interface DocumentFile {
|
|
411
|
+
file: string;
|
|
412
|
+
dynamicUnknown: number;
|
|
413
|
+
parseErrors: string[];
|
|
414
|
+
sites: DocumentSite[];
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export interface DocumentImporter {
|
|
418
|
+
file: string;
|
|
419
|
+
line: number;
|
|
420
|
+
snippet: string;
|
|
421
|
+
kind: ImportKind;
|
|
422
|
+
bindings: string[];
|
|
423
|
+
referenced: string[];
|
|
424
|
+
opaque: boolean;
|
|
425
|
+
fromPackage: string | null;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
export interface DocumentReachedPackage {
|
|
429
|
+
/** `${name}@${version}\0${relative root}`. */
|
|
430
|
+
key: string;
|
|
431
|
+
name: string;
|
|
432
|
+
dirName: string;
|
|
433
|
+
version: string;
|
|
434
|
+
root: string;
|
|
435
|
+
chain: string[];
|
|
436
|
+
dynamic: boolean;
|
|
437
|
+
incomplete: boolean;
|
|
438
|
+
importers: DocumentImporter[];
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export interface FactsDocumentBody {
|
|
442
|
+
summary: FactsSummary;
|
|
443
|
+
workspace: {
|
|
444
|
+
firstPartyNames: string[];
|
|
445
|
+
depScopes: { name: string; scope: DepScope }[];
|
|
446
|
+
aliasPrefixes: string[];
|
|
447
|
+
sourceFiles: number;
|
|
448
|
+
diagnostics: string[];
|
|
449
|
+
};
|
|
450
|
+
imports: DocumentFile[];
|
|
451
|
+
moduleGraph: {
|
|
452
|
+
enabled: boolean;
|
|
453
|
+
filesParsed: number;
|
|
454
|
+
filesSkippedForSize: number;
|
|
455
|
+
unresolved: number;
|
|
456
|
+
truncated: boolean;
|
|
457
|
+
nodeModulesMissing: boolean;
|
|
458
|
+
weakPackages: string[];
|
|
459
|
+
reached: DocumentReachedPackage[];
|
|
460
|
+
unresolvedByName: { package: string; sites: { file: string; reason: string }[] }[];
|
|
461
|
+
};
|
|
462
|
+
lockfile: {
|
|
463
|
+
files: string[];
|
|
464
|
+
packages: DiscoveredPackage[];
|
|
465
|
+
rootDependencies: string[];
|
|
466
|
+
edges: DependencyEdge[];
|
|
467
|
+
diagnostics: string[];
|
|
468
|
+
};
|
|
469
|
+
unanalyzable: UnanalyzableEntry[];
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/** The full document as `npm-check imports --format json` prints it. */
|
|
473
|
+
export interface FactsDocument extends FactsDocumentBody {
|
|
474
|
+
tool: 'npm-check';
|
|
475
|
+
toolVersion: string;
|
|
476
|
+
schemaVersion: string;
|
|
477
|
+
documentType: 'imports';
|
|
478
|
+
target: string;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
export function factsDocument(facts: ImportFacts, options?: { exitCode?: number }): FactsDocumentBody;
|
|
482
|
+
|
|
483
|
+
// --------------------------------------------------------------- misc ----
|
|
484
|
+
|
|
485
|
+
export const FACTS_SCHEMA_VERSION: '1.0';
|
|
486
|
+
|
|
487
|
+
export class FactsError extends Error {
|
|
488
|
+
constructor(code: string, message: string);
|
|
489
|
+
code: string;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/** The TypeScript compiler API, loaded once; throws `FactsError` (`TYPESCRIPT_MISSING`) when absent. */
|
|
493
|
+
export function loadTypeScript(): typeof import('typescript');
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// src/facts/version.js
|
|
2
|
+
// The import-facts document's schema version — a SEPARATE version line from
|
|
3
|
+
// `src/schema.js`'s `SCHEMA_VERSION` (the findings envelope's), so a change
|
|
4
|
+
// to the facts shape never implies one to the findings envelope and vice
|
|
5
|
+
// versa. Both are `1.0` today; a field added to the facts document bumps its
|
|
6
|
+
// minor, a renamed or removed one its major.
|
|
7
|
+
//
|
|
8
|
+
// A dependency-free leaf on purpose: `src/schema.js` (loaded by every
|
|
9
|
+
// lockfile command) imports THIS file to stamp `buildFactsEnvelope`, and the
|
|
10
|
+
// facts barrel re-exports it — neither side pulls the other's imports in.
|
|
11
|
+
export const FACTS_SCHEMA_VERSION = '1.0';
|