@dependably/npm-check 1.9.0 → 1.10.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 +47 -0
- package/bin/cli.js +93 -1
- package/package.json +20 -3
- package/src/audit-config.js +98 -68
- package/src/audit.js +198 -58
- package/src/exceptions.js +82 -66
- package/src/facts/collect.js +201 -0
- package/src/facts/document.js +151 -0
- package/src/facts/errors.js +19 -0
- package/src/facts/index.js +37 -0
- package/src/facts/lockfile-graph.js +467 -0
- package/src/facts/modulegraph.js +289 -0
- package/src/facts/resolve.js +493 -0
- package/src/facts/scan.js +755 -0
- package/src/facts/sourcescan.js +221 -0
- package/src/facts/specifier.js +89 -0
- package/src/facts/ts.js +47 -0
- package/src/facts/types.d.ts +579 -0
- package/src/facts/version.js +16 -0
- package/src/facts/workspace.js +278 -0
- package/src/npmrc-validator.js +2 -1
- package/src/pnpm-workspace-validator.js +8 -2
- package/src/report.js +12 -6
- package/src/schema.js +54 -0
|
@@ -0,0 +1,579 @@
|
|
|
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
|
+
/**
|
|
116
|
+
* `aliases` is per-FILE (`AliasScope`), because a tsconfig's `paths`
|
|
117
|
+
* governs its own project rather than the whole tree. A plain set is still
|
|
118
|
+
* accepted and means "these everywhere".
|
|
119
|
+
*/
|
|
120
|
+
constructor(aliases?: ReadonlySet<string> | AliasScope);
|
|
121
|
+
resolve(fromFile: string, specifier: string, mode: ResolveMode): Resolution;
|
|
122
|
+
/**
|
|
123
|
+
* The package a file belongs to, from its path alone: the directory right
|
|
124
|
+
* after the LAST `node_modules/` segment (two for a scope); undefined for
|
|
125
|
+
* a first-party path.
|
|
126
|
+
*/
|
|
127
|
+
packageOf(file: string): PackageInfo | undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** The package root for a path inside node_modules; undefined for a first-party path. */
|
|
131
|
+
export function packageRootOf(file: string): string | undefined;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Resolves a subpath (or `#import` key) against a package.json `exports` /
|
|
135
|
+
* `imports` value. Returns the target string, `null` when the map explicitly
|
|
136
|
+
* blocks it, or `undefined` when nothing matched.
|
|
137
|
+
*/
|
|
138
|
+
export function resolveExports(map: unknown, subpath: string, conditions: ReadonlySet<string>): string | null | undefined;
|
|
139
|
+
|
|
140
|
+
// --------------------------------------------------------- modulegraph ----
|
|
141
|
+
|
|
142
|
+
export interface GraphImporter {
|
|
143
|
+
/** Absolute path of the importing file. */
|
|
144
|
+
file: string;
|
|
145
|
+
line: number;
|
|
146
|
+
snippet: string;
|
|
147
|
+
kind: ImportKind;
|
|
148
|
+
bindings?: string[];
|
|
149
|
+
referenced?: string[];
|
|
150
|
+
opaque?: boolean;
|
|
151
|
+
/** Key of the package the importing file belongs to; undefined for first-party code. */
|
|
152
|
+
fromPackage?: string;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export interface ReachedPackage {
|
|
156
|
+
/**
|
|
157
|
+
* `${name}@${version}\0${root}` — one entry per INSTALLED COPY, since two
|
|
158
|
+
* copies of one version can differ only by location.
|
|
159
|
+
*/
|
|
160
|
+
key: string;
|
|
161
|
+
name: string;
|
|
162
|
+
/** See `PackageInfo.dirName`. */
|
|
163
|
+
dirName: string;
|
|
164
|
+
version: string;
|
|
165
|
+
root: string;
|
|
166
|
+
/**
|
|
167
|
+
* Every import site from OUTSIDE this package that resolved into it —
|
|
168
|
+
* first-party and other packages' files alike; a package's own internal
|
|
169
|
+
* relative imports are traversed but never listed here.
|
|
170
|
+
*/
|
|
171
|
+
importers: GraphImporter[];
|
|
172
|
+
/**
|
|
173
|
+
* The chain of package keys along which this package was FIRST reached
|
|
174
|
+
* (breadth-first over files from the roots, so short in practice, not a
|
|
175
|
+
* proven shortest package chain), this package last.
|
|
176
|
+
*/
|
|
177
|
+
chain: string[];
|
|
178
|
+
/** A file in this package has a non-literal require()/import(). */
|
|
179
|
+
dynamic: boolean;
|
|
180
|
+
/** A file in this package was not parsed (size cap, unreadable) or has a dangling relative import; its edges are unknown. */
|
|
181
|
+
incomplete: boolean;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export interface ModuleGraph {
|
|
185
|
+
reached: Map<string, ReachedPackage>;
|
|
186
|
+
/** `${name}@${version}` (lower-cased name, both `name` and `dirName` spellings) → every reached copy of it. */
|
|
187
|
+
byNameVersion: Map<string, ReachedPackage[]>;
|
|
188
|
+
/** lower-cased name (both spellings) → every reached copy, any version. */
|
|
189
|
+
byName: Map<string, ReachedPackage[]>;
|
|
190
|
+
filesParsed: number;
|
|
191
|
+
filesSkippedForSize: number;
|
|
192
|
+
/** Files that resolved after the budget was hit and were therefore not parsed. */
|
|
193
|
+
filesPastBudget: number;
|
|
194
|
+
unresolved: number;
|
|
195
|
+
/**
|
|
196
|
+
* Bare specifiers the resolver could not follow, by the PACKAGE NAME they
|
|
197
|
+
* name (lower-cased) → where and why (at most five sites per name).
|
|
198
|
+
*/
|
|
199
|
+
unresolvedByName: Map<string, { file: string; reason: string }[]>;
|
|
200
|
+
/** The file budget stopped the walk; packages past the frontier are unknown, not absent. */
|
|
201
|
+
truncated: boolean;
|
|
202
|
+
/** `name@version` of packages flagged dynamic or incomplete (deduplicated, sorted). */
|
|
203
|
+
weakPackages: string[];
|
|
204
|
+
/** node_modules files that resolved but were not parsed (unreadable, oversized), absolute paths. */
|
|
205
|
+
unanalyzable: { file: string; reason: string }[];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export interface WalkOptions {
|
|
209
|
+
/** Absolute path; used to relativize file names handed to `scan`. */
|
|
210
|
+
srcDir: string;
|
|
211
|
+
resolver: ModuleResolver;
|
|
212
|
+
/** First-party files with their already-computed scans — the roots. */
|
|
213
|
+
roots: Iterable<{ file: string; scan: ScanResult }>;
|
|
214
|
+
/** Parses a node_modules file. Injected so the caller's scanner (and any caching) is reused. */
|
|
215
|
+
scan: (relFile: string, content: string) => ScanResult;
|
|
216
|
+
/** Stop after this many node_modules files have been parsed. Default `DEFAULT_MAX_FILES`. */
|
|
217
|
+
maxFiles?: number;
|
|
218
|
+
/** Skip (and flag) files larger than this many bytes. Default `DEFAULT_MAX_FILE_BYTES`. */
|
|
219
|
+
maxFileBytes?: number;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export const DEFAULT_MAX_FILES: number;
|
|
223
|
+
export const DEFAULT_MAX_FILE_BYTES: number;
|
|
224
|
+
export function packageKey(pkg: PackageInfo): string;
|
|
225
|
+
export function walkModuleGraph(opts: WalkOptions): ModuleGraph;
|
|
226
|
+
|
|
227
|
+
// ------------------------------------------------------ lockfile-graph ----
|
|
228
|
+
|
|
229
|
+
export interface DiscoveredPackage {
|
|
230
|
+
name: string;
|
|
231
|
+
version: string;
|
|
232
|
+
/**
|
|
233
|
+
* SPDX license expression verbatim from the lockfile entry (npm mirrors
|
|
234
|
+
* the package's own `license` field). Absent when the entry never carried
|
|
235
|
+
* one; never guessed. pnpm-lock.yaml carries no license data at all.
|
|
236
|
+
*/
|
|
237
|
+
license?: string;
|
|
238
|
+
/**
|
|
239
|
+
* Whole-tree dev/runtime declaration, tri-state: `true` — reachable ONLY
|
|
240
|
+
* through devDependencies edges; `false` — some path reaches it without a
|
|
241
|
+
* dev edge ("runtime anywhere wins"); absent — the lockfile says nothing.
|
|
242
|
+
* package-lock.json states this for EVERY entry (npm's per-entry flags are
|
|
243
|
+
* a computed answer); pnpm-lock.yaml only for importer-direct packages.
|
|
244
|
+
*/
|
|
245
|
+
devDeclared?: boolean;
|
|
246
|
+
/**
|
|
247
|
+
* "optional" only, when npm asserts the package is reachable EXCLUSIVELY
|
|
248
|
+
* through optionalDependencies. Absent otherwise — never asserted.
|
|
249
|
+
*/
|
|
250
|
+
scope?: 'optional';
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** One dependency edge; both ends are "name@version" keys matching a `DiscoveredPackage`. */
|
|
254
|
+
export interface DependencyEdge {
|
|
255
|
+
from: string;
|
|
256
|
+
to: string;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export interface LockfileGraph {
|
|
260
|
+
packages: DiscoveredPackage[];
|
|
261
|
+
/** "name@version" keys the project's own package.json directly depends on. */
|
|
262
|
+
rootDependencies: string[];
|
|
263
|
+
/** Edges among the resolved closure; does not include root-level edges. */
|
|
264
|
+
edges: DependencyEdge[];
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** `discoverLockfileGraphs`' result: every lockfile under the tree merged into one graph. */
|
|
268
|
+
export interface LockfileDiscovery extends LockfileGraph {
|
|
269
|
+
/** Absolute paths of the lockfiles that parsed, in the order they were merged. */
|
|
270
|
+
files: string[];
|
|
271
|
+
/** `unparseable … at <rel>: <why>` per lockfile that failed; `NO_LOCKFILE: …` when none was found. */
|
|
272
|
+
diagnostics: string[];
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function mergeDiscovered(into: DiscoveredPackage, other: DiscoveredPackage): void;
|
|
276
|
+
export function parsePackageLockJsonGraph(path: string): LockfileGraph;
|
|
277
|
+
export function parsePackageLockJson(path: string): DiscoveredPackage[];
|
|
278
|
+
export function parsePnpmLockYamlGraph(path: string): LockfileGraph;
|
|
279
|
+
export function parsePnpmLockYaml(path: string): DiscoveredPackage[];
|
|
280
|
+
export function discoverLockfileGraphs(srcDir: string): LockfileDiscovery;
|
|
281
|
+
|
|
282
|
+
// ----------------------------------------------------------- specifier ----
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* The npm package a specifier names (lower-cased), or undefined for a
|
|
286
|
+
* relative/absolute path, a builtin, a `#imports` key, a data:/file: URL, or
|
|
287
|
+
* a tsconfig/jsconfig path alias.
|
|
288
|
+
*/
|
|
289
|
+
export function specifierToPackage(spec: string, aliasPrefixes: ReadonlySet<string>): string | undefined;
|
|
290
|
+
/** tsconfig/jsconfig `paths` keys ("@app/*", "utils") → alias bases ("@app", "utils"). */
|
|
291
|
+
export function aliasBaseFromPathsKey(key: string): string;
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Which path aliases are in scope for a given file.
|
|
295
|
+
*
|
|
296
|
+
* A `paths` map belongs to the tsconfig/jsconfig that declares it and governs
|
|
297
|
+
* that project's own files -- which is what `tsc` does. One flat
|
|
298
|
+
* workspace-wide set would let ANY config anywhere under the scanned tree
|
|
299
|
+
* delete a package's evidence in EVERY file.
|
|
300
|
+
*/
|
|
301
|
+
export interface AliasScope {
|
|
302
|
+
/** Alias bases in scope for `file`, an absolute path. */
|
|
303
|
+
for(file: string): ReadonlySet<string>;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** An `AliasScope` that answers the same set everywhere -- tests, and the empty default. */
|
|
307
|
+
export function fixedAliasScope(prefixes?: ReadonlySet<string>): AliasScope;
|
|
308
|
+
/** Accept either shape at an API boundary without making every caller care. */
|
|
309
|
+
export function asAliasScope(aliases: ReadonlySet<string> | AliasScope): AliasScope;
|
|
310
|
+
|
|
311
|
+
// ---------------------------------------------------------- sourcescan ----
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* What an in-process analyzer is allowed to exclude from its first-party
|
|
315
|
+
* source scan, and how it says so. See `sourcescan.js` for the full
|
|
316
|
+
* rationale: a directory's NAME is not evidence that the code inside it is
|
|
317
|
+
* generated -- `.gitignore` is the authority instead.
|
|
318
|
+
*/
|
|
319
|
+
export interface GitignoreLayer {
|
|
320
|
+
/** Directory the file sits in, relative to srcDir, `/`-joined; `''` for the root one. */
|
|
321
|
+
dir: string;
|
|
322
|
+
/** An `ignore` package matcher built from that directory's `.gitignore` content. */
|
|
323
|
+
matcher: { test(path: string): { ignored: boolean; unignored: boolean } };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Every `.gitignore` under `srcDir`, deepest first -- not just the root one. */
|
|
327
|
+
export function loadGitignores(srcDir: string, ignoreDirs: readonly string[]): GitignoreLayer[];
|
|
328
|
+
/** Is `rel` (relative to srcDir, `/`-joined) ignored, by git's own rules? */
|
|
329
|
+
export function isGitignored(layers: readonly GitignoreLayer[], rel: string): boolean;
|
|
330
|
+
/** Drop the absolute paths under `srcDir` that a `.gitignore` in the tree ignores. */
|
|
331
|
+
export function filterGitignored(srcDir: string, layers: readonly GitignoreLayer[], paths: string[]): string[];
|
|
332
|
+
/** Directory names that usually DO hold generated or vendored output; nothing is excluded for being on this list. */
|
|
333
|
+
export const OUTPUT_SHAPED_DIRS: readonly string[];
|
|
334
|
+
/** `OUTPUT_DIR_SCANNED`, or undefined when no such directory was scanned. A NOTE, not a warning. */
|
|
335
|
+
export function outputDirScannedDiagnostic(relPaths: readonly string[], names?: readonly string[]): string | undefined;
|
|
336
|
+
|
|
337
|
+
// ----------------------------------------------------------- workspace ----
|
|
338
|
+
|
|
339
|
+
export type DepScope = 'runtime' | 'dev';
|
|
340
|
+
|
|
341
|
+
/** One tsconfig/jsconfig's own `paths` alias bases and the directory it governs (absolute path). */
|
|
342
|
+
export interface AliasLayer {
|
|
343
|
+
dir: string;
|
|
344
|
+
prefixes: string[];
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export interface Workspace {
|
|
348
|
+
/** Names of package.json manifests found in the tree = first-party packages (lower-cased). */
|
|
349
|
+
firstPartyNames: Set<string>;
|
|
350
|
+
/** name → runtime|dev; "runtime anywhere wins" across all manifests. */
|
|
351
|
+
depScopes: Map<string, DepScope>;
|
|
352
|
+
/**
|
|
353
|
+
* Every tsconfig/jsconfig paths alias base found anywhere in the tree.
|
|
354
|
+
*
|
|
355
|
+
* FOR REPORTING ONLY -- never decide a specifier with this. A `paths` map
|
|
356
|
+
* governs the project that declares it, so use `aliasScope`, which answers
|
|
357
|
+
* per file.
|
|
358
|
+
*/
|
|
359
|
+
aliasPrefixes: Set<string>;
|
|
360
|
+
/** Which alias bases apply to a given file -- see `AliasScope`. */
|
|
361
|
+
aliasScope: AliasScope;
|
|
362
|
+
/** The raw per-config layers `aliasScope` is built from (absolute directories); carried for JSON serialization. */
|
|
363
|
+
aliasLayers: AliasLayer[];
|
|
364
|
+
/**
|
|
365
|
+
* name -> the manifests that declared it a DEV dependency, `/`-joined and
|
|
366
|
+
* relative to srcDir, for names no manifest declares runtime.
|
|
367
|
+
*
|
|
368
|
+
* A dev claim is one manifest's view, and a manifest's view covers its own
|
|
369
|
+
* subtree. Recording WHERE the claim came from is what lets a consumer
|
|
370
|
+
* refuse a vendored tool's `devDependencies` as the reason a package
|
|
371
|
+
* imported from `src/` is exempt from a build gate.
|
|
372
|
+
*/
|
|
373
|
+
devDeclaredBy: Map<string, string[]>;
|
|
374
|
+
/** First-party source files, absolute paths, sorted. */
|
|
375
|
+
sourceFiles: string[];
|
|
376
|
+
diagnostics: string[];
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export function discoverWorkspace(srcDir: string): Workspace;
|
|
380
|
+
/** Is `rel` (a `/`-joined path) inside the directory of the manifest at `manifestRel`? */
|
|
381
|
+
export function governedByManifest(manifestRel: string, rel: string): boolean;
|
|
382
|
+
|
|
383
|
+
// ------------------------------------------------------------- collect ----
|
|
384
|
+
|
|
385
|
+
/** An `ImportSite` with what it names and what it loads. */
|
|
386
|
+
export interface ResolvedSite extends ImportSite {
|
|
387
|
+
/** The package the specifier names (see `specifierToPackage`); undefined for a non-package import. */
|
|
388
|
+
package: string | undefined;
|
|
389
|
+
/**
|
|
390
|
+
* The installed package copy the specifier resolved INTO, when it did.
|
|
391
|
+
* A fact, not a match: compare `name`/`dirName` against `package` before
|
|
392
|
+
* treating it as "the installed copy of the package this site names".
|
|
393
|
+
*/
|
|
394
|
+
installed?: { name: string; dirName: string; version: string; root: string };
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export interface FirstPartyFile {
|
|
398
|
+
/** Absolute path. */
|
|
399
|
+
file: string;
|
|
400
|
+
/** `srcDir`-relative POSIX path (realpath-aware). */
|
|
401
|
+
rel: string;
|
|
402
|
+
scan: ScanResult;
|
|
403
|
+
sites: ResolvedSite[];
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export type UnanalyzableKind =
|
|
407
|
+
/** A first-party source file that could not be read; its imports are unknown. */
|
|
408
|
+
| 'file'
|
|
409
|
+
/** A first-party `.svelte` file whose `<script>` extraction reported a problem; sites are still present. */
|
|
410
|
+
| 'file-partial'
|
|
411
|
+
/** A node_modules file the walk resolved but did not parse (unreadable, oversized). */
|
|
412
|
+
| 'node-modules-file'
|
|
413
|
+
/** The walk stopped on its file budget; everything past the frontier is unobserved. */
|
|
414
|
+
| 'walk';
|
|
415
|
+
|
|
416
|
+
export interface UnanalyzableEntry {
|
|
417
|
+
/** `srcDir`-relative POSIX path (or `node_modules` for a `walk` entry). */
|
|
418
|
+
file: string;
|
|
419
|
+
kind: UnanalyzableKind;
|
|
420
|
+
reason: string;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export interface CollectOptions {
|
|
424
|
+
/** Follow imports through node_modules. Default true. */
|
|
425
|
+
moduleGraph?: boolean;
|
|
426
|
+
maxFiles?: number;
|
|
427
|
+
maxFileBytes?: number;
|
|
428
|
+
/** The scanner to use for first-party AND node_modules files. Default `scanSource`. */
|
|
429
|
+
scan?: (relFile: string, content: string) => ScanResult;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export interface ImportFacts {
|
|
433
|
+
/** Absolute. */
|
|
434
|
+
srcDir: string;
|
|
435
|
+
/** `realpath(srcDir)`, or `srcDir` when that fails. */
|
|
436
|
+
realSrcDir: string;
|
|
437
|
+
workspace: Workspace;
|
|
438
|
+
files: FirstPartyFile[];
|
|
439
|
+
/** Undefined when `moduleGraph: false`. */
|
|
440
|
+
graph: ModuleGraph | undefined;
|
|
441
|
+
/** The walk reached nothing, something was unresolved, and there is no `node_modules` directory. */
|
|
442
|
+
nodeModulesMissing: boolean;
|
|
443
|
+
lockfile: LockfileDiscovery;
|
|
444
|
+
/** ALWAYS present (empty when nothing was skipped). Non-empty means the search was incomplete. */
|
|
445
|
+
unanalyzable: UnanalyzableEntry[];
|
|
446
|
+
/** Sum of `dynamicUnknown` over first-party files. */
|
|
447
|
+
dynamicUnknownTotal: number;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
export function collectImportFacts(srcDir: string, options?: CollectOptions): ImportFacts;
|
|
451
|
+
/**
|
|
452
|
+
* Realpath-aware relativizer producing POSIX paths: of the two spellings of
|
|
453
|
+
* `srcDir` (as given, realpath), the one the file sits under with fewer `..`
|
|
454
|
+
* segments wins. `srcDir` itself relativizes to `''`, as sbom-reach's `relOf`.
|
|
455
|
+
*/
|
|
456
|
+
export function makeRelOf(srcDir: string, realSrcDir: string): (file: string) => string;
|
|
457
|
+
|
|
458
|
+
// ------------------------------------------------------------ document ----
|
|
459
|
+
|
|
460
|
+
export interface FactsSummary {
|
|
461
|
+
/** First-party source files the workspace discovery found. */
|
|
462
|
+
scanned: number;
|
|
463
|
+
/** Of those, files read and parsed — the length of `imports`. */
|
|
464
|
+
analyzed: number;
|
|
465
|
+
/** Entries in `unanalyzable`, of every kind. Non-zero means the search was incomplete. */
|
|
466
|
+
unanalyzable: number;
|
|
467
|
+
/** Import sites summed over every first-party file. */
|
|
468
|
+
imports: number;
|
|
469
|
+
moduleGraph: { filesParsed: number; reached: number; unresolved: number; truncated: boolean };
|
|
470
|
+
/** The process exit code, `0` for every successful scan. */
|
|
471
|
+
exitCode: number;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
export interface DocumentSite {
|
|
475
|
+
specifier: string;
|
|
476
|
+
package: string | null;
|
|
477
|
+
line: number;
|
|
478
|
+
snippet: string;
|
|
479
|
+
kind: ImportKind;
|
|
480
|
+
bindings: string[];
|
|
481
|
+
referenced: string[];
|
|
482
|
+
opaque: boolean;
|
|
483
|
+
installed?: { name: string; dirName: string; version: string; root: string };
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
export interface DocumentFile {
|
|
487
|
+
file: string;
|
|
488
|
+
dynamicUnknown: number;
|
|
489
|
+
parseErrors: string[];
|
|
490
|
+
sites: DocumentSite[];
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
export interface DocumentImporter {
|
|
494
|
+
file: string;
|
|
495
|
+
line: number;
|
|
496
|
+
snippet: string;
|
|
497
|
+
kind: ImportKind;
|
|
498
|
+
bindings: string[];
|
|
499
|
+
referenced: string[];
|
|
500
|
+
opaque: boolean;
|
|
501
|
+
fromPackage: string | null;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
export interface DocumentReachedPackage {
|
|
505
|
+
/** `${name}@${version}\0${relative root}`. */
|
|
506
|
+
key: string;
|
|
507
|
+
name: string;
|
|
508
|
+
dirName: string;
|
|
509
|
+
version: string;
|
|
510
|
+
root: string;
|
|
511
|
+
chain: string[];
|
|
512
|
+
dynamic: boolean;
|
|
513
|
+
incomplete: boolean;
|
|
514
|
+
importers: DocumentImporter[];
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
export interface FactsDocumentBody {
|
|
518
|
+
summary: FactsSummary;
|
|
519
|
+
workspace: {
|
|
520
|
+
firstPartyNames: string[];
|
|
521
|
+
depScopes: { name: string; scope: DepScope }[];
|
|
522
|
+
aliasPrefixes: string[];
|
|
523
|
+
/**
|
|
524
|
+
* `aliasScope`'s raw per-config layers, JSON-safe: `aliasScope` itself is
|
|
525
|
+
* a closure and cannot be serialized, so this projects the same
|
|
526
|
+
* information the live `AliasScope` answers `.for(file)` from -- one
|
|
527
|
+
* entry per tsconfig/jsconfig that declared `paths`, `dir` relative to
|
|
528
|
+
* the target and `/`-joined.
|
|
529
|
+
*/
|
|
530
|
+
aliasScope: { dir: string; prefixes: string[] }[];
|
|
531
|
+
/** `devDeclaredBy`, JSON-safe: one entry per name with an unresolved dev claim. */
|
|
532
|
+
devDeclaredBy: { name: string; manifests: string[] }[];
|
|
533
|
+
sourceFiles: number;
|
|
534
|
+
diagnostics: string[];
|
|
535
|
+
};
|
|
536
|
+
imports: DocumentFile[];
|
|
537
|
+
moduleGraph: {
|
|
538
|
+
enabled: boolean;
|
|
539
|
+
filesParsed: number;
|
|
540
|
+
filesSkippedForSize: number;
|
|
541
|
+
unresolved: number;
|
|
542
|
+
truncated: boolean;
|
|
543
|
+
nodeModulesMissing: boolean;
|
|
544
|
+
weakPackages: string[];
|
|
545
|
+
reached: DocumentReachedPackage[];
|
|
546
|
+
unresolvedByName: { package: string; sites: { file: string; reason: string }[] }[];
|
|
547
|
+
};
|
|
548
|
+
lockfile: {
|
|
549
|
+
files: string[];
|
|
550
|
+
packages: DiscoveredPackage[];
|
|
551
|
+
rootDependencies: string[];
|
|
552
|
+
edges: DependencyEdge[];
|
|
553
|
+
diagnostics: string[];
|
|
554
|
+
};
|
|
555
|
+
unanalyzable: UnanalyzableEntry[];
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/** The full document as `npm-check imports --format json` prints it. */
|
|
559
|
+
export interface FactsDocument extends FactsDocumentBody {
|
|
560
|
+
tool: 'npm-check';
|
|
561
|
+
toolVersion: string;
|
|
562
|
+
schemaVersion: string;
|
|
563
|
+
documentType: 'imports';
|
|
564
|
+
target: string;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
export function factsDocument(facts: ImportFacts, options?: { exitCode?: number }): FactsDocumentBody;
|
|
568
|
+
|
|
569
|
+
// --------------------------------------------------------------- misc ----
|
|
570
|
+
|
|
571
|
+
export const FACTS_SCHEMA_VERSION: '1.1';
|
|
572
|
+
|
|
573
|
+
export class FactsError extends Error {
|
|
574
|
+
constructor(code: string, message: string);
|
|
575
|
+
code: string;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/** The TypeScript compiler API, loaded once; throws `FactsError` (`TYPESCRIPT_MISSING`) when absent. */
|
|
579
|
+
export function loadTypeScript(): typeof import('typescript');
|
|
@@ -0,0 +1,16 @@
|
|
|
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. `src/schema.js`'s version is still `1.0`; a field added to the
|
|
6
|
+
// facts document bumps its minor, a renamed or removed one its major. Bumped
|
|
7
|
+
// to `1.1` when `workspace.aliasScope` and `workspace.devDeclaredBy` were
|
|
8
|
+
// added — both are additive, so a consumer already reading `1.0` still
|
|
9
|
+
// parses the document, but can now tell "this producer predates these
|
|
10
|
+
// fields" apart from "no manifest made a dev claim", which is the whole
|
|
11
|
+
// point of tracking the minor at all.
|
|
12
|
+
//
|
|
13
|
+
// A dependency-free leaf on purpose: `src/schema.js` (loaded by every
|
|
14
|
+
// lockfile command) imports THIS file to stamp `buildFactsEnvelope`, and the
|
|
15
|
+
// facts barrel re-exports it — neither side pulls the other's imports in.
|
|
16
|
+
export const FACTS_SCHEMA_VERSION = '1.1';
|