@mh-alikhani/bunready 0.3.3 → 0.3.4

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/CHANGELOG.md CHANGED
@@ -9,6 +9,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  Nothing yet.
11
11
 
12
+ ## [0.3.4] - 2026-09-16
13
+
14
+ ### Changed
15
+
16
+ - Every exported symbol carries a doc comment. JSR scores how much of a
17
+ package's exported surface is documented; this package was at 48% and
18
+ is now at 100%. Comments only - no behaviour changed.
19
+
20
+
12
21
  ## [0.3.3] - 2026-09-16
13
22
 
14
23
  ### Fixed
@@ -248,8 +257,8 @@ Nothing yet.
248
257
  and that claim needs a primary source. Until then the rule reports what the
249
258
  repository imports and cites the compatibility table.
250
259
 
251
- [Unreleased]: https://github.com/MHAlikhani/bunready/compare/v0.3.3...HEAD
252
- [0.3.3]: https://github.com/MHAlikhani/bunready/releases/tag/v0.3.3
260
+ [Unreleased]: https://github.com/MHAlikhani/bunready/compare/v0.3.4...HEAD
261
+ [0.3.4]: https://github.com/MHAlikhani/bunready/releases/tag/v0.3.4
253
262
  [0.3.3]: https://github.com/MHAlikhani/bunready/releases/tag/v0.3.3
254
263
  [0.3.2]: https://github.com/MHAlikhani/bunready/releases/tag/v0.3.2
255
264
  [0.3.1]: https://github.com/MHAlikhani/bunready/releases/tag/v0.3.1
package/README.md CHANGED
@@ -30,7 +30,7 @@ bunx @mh-alikhani/bunready --help # every flag
30
30
  Scanning this repository prints its findings and one verdict:
31
31
 
32
32
  ```
33
- bunready 0.3.2 · 106 locked packages · bun.lock
33
+ bunready 0.3.3 · 106 locked packages · bun.lock
34
34
  /path/to/your/project
35
35
 
36
36
  info the project's own code imports 4 Node built-in module(s) (runtime/node-builtins)
@@ -62,7 +62,7 @@ permissions:
62
62
 
63
63
  steps:
64
64
  - uses: actions/checkout@v7
65
- - uses: MHAlikhani/bunready@v0.3.2
65
+ - uses: MHAlikhani/bunready@v0.3.3
66
66
  with:
67
67
  path: .
68
68
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mh-alikhani/bunready",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "description": "Bun-readiness scanner: one command that shows what will break before you move a Node/TS repo to Bun.",
5
5
  "keywords": [
6
6
  "bun",
package/src/cli/args.ts CHANGED
@@ -16,6 +16,7 @@ export interface CliOptions {
16
16
  readonly writeBaseline: string | undefined;
17
17
  }
18
18
 
19
+ /** Directory scanned when no path argument is given. */
19
20
  export const DEFAULT_TARGET = ".";
20
21
 
21
22
  /**
package/src/cli/copy.ts CHANGED
@@ -8,16 +8,20 @@ import { TOOL_NAME } from "../core/version";
8
8
  import type { Verdict } from "../report/types";
9
9
  import type { Severity } from "../rules/severity";
10
10
 
11
+ /** Product name as it appears in user-facing copy. */
11
12
  export const TOOL = TOOL_NAME;
12
13
 
14
+ /** One-line positioning statement shown with the help text. */
13
15
  export const POSITIONING = "Know what breaks before you move a Node/TS repo to Bun.";
14
16
 
17
+ /** Short tagline above the usage text. */
15
18
  export const TAGLINE = "One command. One honest verdict.";
16
19
 
17
20
  /** `--run` executes the target's code, so the help text has to say so plainly. */
18
21
  export const RUN_WARNING =
19
22
  "--run executes the target's code in a temporary copy; nothing runs in place and every command is timed";
20
23
 
24
+ /** Full help text: usage, options and exit codes. */
21
25
  export function helpText(version: string): string {
22
26
  return [
23
27
  `${TOOL} ${version}`,
package/src/cli/io.ts CHANGED
@@ -6,6 +6,7 @@ export interface Io {
6
6
  readonly isTty: boolean;
7
7
  }
8
8
 
9
+ /** The real stdin, stdout and stderr, coloured when the terminal allows it. */
9
10
  export function systemIo(): Io {
10
11
  return {
11
12
  out: (line) => {
package/src/cli/run.ts CHANGED
@@ -12,10 +12,14 @@ import { helpText, RUN_WARNING, TOOL } from "./copy";
12
12
  import { type Io, systemIo } from "./io";
13
13
  import { colorEnabled, createTheme } from "./theme";
14
14
 
15
+ /** Exit code for a scan with nothing at or above the failure threshold. */
15
16
  export const EXIT_OK = 0;
17
+ /** Exit code for findings at or above the failure threshold. */
16
18
  export const EXIT_BLOCKERS = 1;
19
+ /** Exit code for a usage error or an incomplete scan. */
17
20
  export const EXIT_USAGE = 2;
18
21
 
22
+ /** The tool version, as reported by --version. */
19
23
  export function version(): string {
20
24
  return TOOL_VERSION;
21
25
  }
package/src/cli/theme.ts CHANGED
@@ -17,6 +17,7 @@ export interface Theme {
17
17
 
18
18
  const identity = (text: string): string => text;
19
19
 
20
+ /** Decides whether a stream's output should be coloured. */
20
21
  export function colorEnabled(
21
22
  env: Readonly<Record<string, string | undefined>>,
22
23
  isTty: boolean,
@@ -30,6 +31,7 @@ export function colorEnabled(
30
31
  return isTty;
31
32
  }
32
33
 
34
+ /** Builds the colour palette used by the human-readable report. */
33
35
  export function createTheme(enabled: boolean): Theme {
34
36
  if (!enabled) {
35
37
  return {
@@ -11,27 +11,32 @@ import type { Finding } from "../report/types";
11
11
  */
12
12
  export const BASELINE_SCHEMA_VERSION = 1;
13
13
 
14
+ /** A recorded set of known findings, keyed by fingerprint. */
14
15
  export interface Baseline {
15
16
  readonly schemaVersion: number;
16
17
  readonly findings: readonly string[];
17
18
  }
18
19
 
20
+ /** What a baseline comparison found: new, known and fixed entries. */
19
21
  export interface BaselineSummary {
20
22
  readonly path: string;
21
23
  readonly known: number;
22
24
  readonly new: number;
23
25
  }
24
26
 
27
+ /** Stable identity of a finding: its rule, package and path. */
25
28
  export function fingerprint(finding: Finding): string {
26
29
  return [finding.id, finding.package ?? "", finding.path ?? ""].join("|");
27
30
  }
28
31
 
32
+ /** Renders a baseline in its on-disk JSON form. */
29
33
  export function serializeBaseline(findings: readonly Finding[]): string {
30
34
  const fingerprints = [...new Set(findings.map(fingerprint))].sort();
31
35
  const baseline: Baseline = { schemaVersion: BASELINE_SCHEMA_VERSION, findings: fingerprints };
32
36
  return `${JSON.stringify(baseline, null, 2)}\n`;
33
37
  }
34
38
 
39
+ /** Reads a baseline file, reporting malformed input as an error. */
35
40
  export function parseBaseline(text: string, source: string): Result<Baseline> {
36
41
  let raw: unknown;
37
42
  try {
@@ -7,11 +7,13 @@ import { SEVERITIES, type Severity } from "../rules/severity";
7
7
  */
8
8
  export const CONFIG_FILENAME = "bunready.config.json";
9
9
 
10
+ /** Settings for the optional run phase. */
10
11
  export interface RunConfig {
11
12
  readonly script: string | undefined;
12
13
  readonly maxCopyMegabytes: number;
13
14
  }
14
15
 
16
+ /** The parsed contents of the project's configuration file. */
15
17
  export interface BunreadyConfig {
16
18
  /** Finding ids to drop, e.g. `install/no-lockfile`. */
17
19
  readonly ignore: readonly string[];
@@ -26,6 +28,7 @@ export interface BunreadyConfig {
26
28
  readonly run: RunConfig;
27
29
  }
28
30
 
31
+ /** Configuration used when the project has no config file. */
29
32
  export const DEFAULT_CONFIG: BunreadyConfig = {
30
33
  ignore: [],
31
34
  ignorePackages: [],
@@ -35,6 +38,7 @@ export const DEFAULT_CONFIG: BunreadyConfig = {
35
38
  run: { script: undefined, maxCopyMegabytes: 250 },
36
39
  };
37
40
 
41
+ /** Copy limit before a project is refused as too large to run. */
38
42
  export const DEFAULT_MAX_COPY_MEGABYTES = 250;
39
43
 
40
44
  function isRecord(value: unknown): value is Record<string, unknown> {
@@ -51,6 +55,7 @@ function configError(source: string, detail: string, hint: string): Result<Bunre
51
55
  return { ok: false, error: defineError("E_PARSE", `${source} ${detail}`, { hint }) };
52
56
  }
53
57
 
58
+ /** Parses and validates configuration, returning errors instead of throwing. */
54
59
  export function parseConfig(text: string, source = CONFIG_FILENAME): Result<BunreadyConfig> {
55
60
  let raw: unknown;
56
61
  try {
@@ -19,24 +19,30 @@ export interface BunreadyError {
19
19
  readonly cause?: unknown;
20
20
  }
21
21
 
22
+ /** A successful result. */
22
23
  export type Ok<T> = { readonly ok: true; readonly value: T };
24
+ /** A failed result carrying an error. */
23
25
  export type Err = { readonly ok: false; readonly error: BunreadyError };
24
26
 
25
27
  /** Success or failure. Errors are values here, not control flow. */
26
28
  export type Result<T> = Ok<T> | Err;
27
29
 
30
+ /** Wraps a value in a successful result. */
28
31
  export function ok<T>(value: T): Ok<T> {
29
32
  return { ok: true, value };
30
33
  }
31
34
 
35
+ /** Wraps an error in a failed result. */
32
36
  export function err<T = never>(error: BunreadyError): Result<T> {
33
37
  return { ok: false, error };
34
38
  }
35
39
 
40
+ /** Narrows a result to its success case. */
36
41
  export function isOk<T>(result: Result<T>): result is Ok<T> {
37
42
  return result.ok;
38
43
  }
39
44
 
45
+ /** Narrows a result to its failure case. */
40
46
  export function isErr<T>(result: Result<T>): result is Err {
41
47
  return !result.ok;
42
48
  }
package/src/core/fs.ts CHANGED
@@ -17,6 +17,7 @@ export interface DirectoryEntry {
17
17
  readonly isDirectory: boolean;
18
18
  }
19
19
 
20
+ /** The file operations the scanner needs, injected so tests can run in memory. */
20
21
  export interface FileSystem {
21
22
  readonly readTextFile: (path: string) => Promise<ReadOutcome>;
22
23
  readonly writeTextFile: (path: string, text: string) => Promise<void>;
@@ -28,6 +29,7 @@ function describe(error: unknown): string {
28
29
  return error instanceof Error ? error.message : String(error);
29
30
  }
30
31
 
32
+ /** The FileSystem implementation backed by node:fs. */
31
33
  export function nodeFileSystem(): FileSystem {
32
34
  return {
33
35
  readTextFile: async (path) => {
@@ -1,4 +1,4 @@
1
- import packageJson from "../../package.json";
1
+ import packageJson from "../../package.json" with { type: "json" };
2
2
 
3
3
  /**
4
4
  * Tool identity, in a module with no dependencies, so anything (CLI, scanner,
@@ -6,4 +6,5 @@ import packageJson from "../../package.json";
6
6
  */
7
7
  export const TOOL_NAME = "bunready";
8
8
 
9
+ /** The tool's own version, used in reports. */
9
10
  export const TOOL_VERSION: string = packageJson.version;
@@ -22,6 +22,7 @@ function label(finding: Finding, theme: Theme): string {
22
22
  }
23
23
  }
24
24
 
25
+ /** Renders a report for a terminal: findings by severity, then the verdict. */
25
26
  export function renderHumanReport(report: ScanReport, theme: Theme): string {
26
27
  const lines: string[] = [];
27
28
  const facts: string[] = [];
@@ -27,6 +27,7 @@ function worstByRule(
27
27
  return byRule;
28
28
  }
29
29
 
30
+ /** Renders findings as SARIF 2.1.0 for code scanning. */
30
31
  export function renderSarifReport(report: ScanReport): string {
31
32
  const byRule = worstByRule(report.findings);
32
33
 
@@ -29,6 +29,7 @@ export interface Finding {
29
29
  readonly hint?: string;
30
30
  }
31
31
 
32
+ /** Overall judgement for a scan: blocked, risky or ready. */
32
33
  export type Verdict = "ready" | "risky" | "blocked";
33
34
 
34
35
  /** One scanned directory in a multi-package repository. */
@@ -110,6 +111,7 @@ export function sortFindings(findings: readonly Finding[]): Finding[] {
110
111
  });
111
112
  }
112
113
 
114
+ /** The judgement a set of findings amounts to. */
113
115
  export function verdictFor(findings: readonly Finding[]): Verdict {
114
116
  let verdict: Verdict = "ready";
115
117
  for (const finding of findings) {
@@ -16,10 +16,12 @@ import type { TargetSnapshot } from "../../scanner/target";
16
16
  * facts; nothing is inferred from package names.
17
17
  */
18
18
  export const LIFECYCLE_DOC = "https://bun.com/docs/pm/lifecycle";
19
+ /** Explains trustedDependencies, attached to lifecycle-script findings. */
19
20
  export const TRUSTED_DEPENDENCIES_GUIDE = "https://bun.com/guides/install/trusted";
20
21
 
21
22
  const ID = "install/lifecycle-script";
22
23
 
24
+ /** Flags install scripts that will not run unless the package is trusted. */
23
25
  export function lifecycleScriptFindings(
24
26
  snapshot: TargetSnapshot,
25
27
  lockfile: ParsedLockfile | undefined,
@@ -13,6 +13,7 @@ const BINARY_LOCKFILE_ID = "install/binary-lockfile";
13
13
  const UNPARSED_LOCKFILE_ID = "install/unparsed-lockfile";
14
14
  const MULTIPLE_LOCKFILES_ID = "install/multiple-lockfiles";
15
15
 
16
+ /** Reports lockfiles that are missing, unreadable or Bun-unfriendly. */
16
17
  export function lockfileFindings(snapshot: TargetSnapshot): Finding[] {
17
18
  const findings: Finding[] = [];
18
19
 
@@ -3,7 +3,7 @@ import type { DependencyGraph } from "../../scanner/graph";
3
3
  import { knownPackageNames } from "../../scanner/graph";
4
4
  import type { ParsedLockfile } from "../../scanner/lockfile";
5
5
  import type { TargetSnapshot } from "../../scanner/target";
6
- import nativeDataset from "../data/native-packages.json";
6
+ import nativeDataset from "../data/native-packages.json" with { type: "json" };
7
7
 
8
8
  /**
9
9
  * Native-addon detection, in evidence order:
@@ -23,6 +23,7 @@ export interface NativePackageEntry {
23
23
  readonly source: string;
24
24
  }
25
25
 
26
+ /** The build-tool packages the native-addon rule recognises. */
26
27
  export const BUILD_TOOL_PACKAGES = [
27
28
  "node-gyp",
28
29
  "node-pre-gyp",
@@ -56,6 +57,7 @@ export function readDataset(raw: unknown = nativeDataset): NativePackageEntry[]
56
57
  return entries;
57
58
  }
58
59
 
60
+ /** Flags native addons that need a compiler toolchain. */
59
61
  export function nativeAddonFindings(
60
62
  snapshot: TargetSnapshot,
61
63
  graph: DependencyGraph,
@@ -21,6 +21,7 @@ function megabytes(bytes: number): string {
21
21
  return `${Math.round(bytes / (1024 * 1024))} MB`;
22
22
  }
23
23
 
24
+ /** Turns the outcome of a run into findings. */
24
25
  export function runFindings(
25
26
  outcome: RunOutcome,
26
27
  options: RunOptions = DEFAULT_RUN_OPTIONS,
@@ -1,6 +1,6 @@
1
1
  import type { Finding } from "../../report/types";
2
2
  import { classifySpecifier, type SourceScan } from "../../scanner/sources";
3
- import runtimeDataset from "../data/node-runtime.json";
3
+ import runtimeDataset from "../data/node-runtime.json" with { type: "json" };
4
4
 
5
5
  /**
6
6
  * Runtime-phase rule: what Node surface the repository actually depends on.
@@ -17,6 +17,7 @@ const GAP_ID = "runtime/known-gap";
17
17
  const COVERAGE_ID = "runtime/scan-coverage";
18
18
  const MAX_LISTED_MODULES = 8;
19
19
 
20
+ /** One row of the Node runtime gap dataset. */
20
21
  export interface RuntimeGapEntry {
21
22
  readonly name: string;
22
23
  readonly status: "partial" | "unimplemented";
@@ -24,11 +25,13 @@ export interface RuntimeGapEntry {
24
25
  readonly source: string;
25
26
  }
26
27
 
28
+ /** The shipped dataset of Node runtime gaps, each with its source. */
27
29
  export interface RuntimeDataset {
28
30
  readonly compatibilityDocs: string | undefined;
29
31
  readonly gaps: readonly RuntimeGapEntry[];
30
32
  }
31
33
 
34
+ /** A Node built-in the project imports, with where it is used. */
32
35
  export interface BuiltinUsage {
33
36
  readonly name: string;
34
37
  readonly files: readonly string[];
@@ -92,6 +95,7 @@ function normalise(name: string): string {
92
95
  return name.startsWith("node:") ? name.slice(5) : name;
93
96
  }
94
97
 
98
+ /** Reports the Node built-ins the project's own code imports. */
95
99
  export function runtimeBuiltinFindings(
96
100
  scan: SourceScan,
97
101
  usages: readonly BuiltinUsage[],
@@ -9,6 +9,7 @@
9
9
 
10
10
  export const SEVERITIES = ["blocker", "risk", "info"] as const;
11
11
 
12
+ /** Severity levels, ordered from most to least serious. */
12
13
  export type Severity = (typeof SEVERITIES)[number];
13
14
 
14
15
  /** Lower rank sorts first: blockers surface above everything else. */
@@ -18,10 +19,12 @@ export const SEVERITY_RANK: Readonly<Record<Severity, number>> = {
18
19
  info: 2,
19
20
  };
20
21
 
22
+ /** Orders severities for sorting, blockers first. */
21
23
  export function compareSeverity(a: Severity, b: Severity): number {
22
24
  return SEVERITY_RANK[a] - SEVERITY_RANK[b];
23
25
  }
24
26
 
27
+ /** Counts findings per severity. */
25
28
  export function countBySeverity(
26
29
  severities: readonly Severity[],
27
30
  ): Readonly<Record<Severity, number>> {
@@ -23,8 +23,10 @@ export const COPY_EXCLUDES = [
23
23
  ".cache",
24
24
  ] as const;
25
25
 
26
+ /** Script names the run phase is willing to execute, in order of preference. */
26
27
  export const RUNNABLE_SCRIPTS = ["start", "test"] as const;
27
28
 
29
+ /** Exit status and captured output of one child process. */
28
30
  export interface ProcessResult {
29
31
  readonly code: number | null;
30
32
  readonly stdout: string;
@@ -33,15 +35,18 @@ export interface ProcessResult {
33
35
  readonly durationMs: number;
34
36
  }
35
37
 
38
+ /** Options for a single command invocation. */
36
39
  export interface RunCommandOptions {
37
40
  readonly cwd: string;
38
41
  readonly timeoutMs: number;
39
42
  }
40
43
 
44
+ /** Runs a command; injected so tests need not spawn processes. */
41
45
  export interface CommandRunner {
42
46
  readonly run: (command: readonly string[], options: RunCommandOptions) => Promise<ProcessResult>;
43
47
  }
44
48
 
49
+ /** What the run phase needs: a scratch directory and a way to run commands. */
45
50
  export interface RunEnvironment {
46
51
  readonly runner: CommandRunner;
47
52
  readonly makeTempDir: () => Promise<string>;
@@ -50,11 +55,13 @@ export interface RunEnvironment {
50
55
  readonly measureTreeBytes: (path: string) => Promise<number>;
51
56
  }
52
57
 
58
+ /** A failure observed while running the project's scripts. */
53
59
  export interface RunFailure {
54
60
  readonly message: string;
55
61
  readonly frames: readonly string[];
56
62
  }
57
63
 
64
+ /** What the run phase ended up doing, successful or not. */
58
65
  export interface RunOutcome {
59
66
  readonly workDir: string;
60
67
  readonly script: string | undefined;
@@ -67,6 +74,7 @@ export interface RunOutcome {
67
74
  readonly copyTooLarge: boolean;
68
75
  }
69
76
 
77
+ /** Limits and toggles for the run phase. */
70
78
  export interface RunOptions {
71
79
  readonly installTimeoutMs: number;
72
80
  readonly scriptTimeoutMs: number;
@@ -75,6 +83,7 @@ export interface RunOptions {
75
83
  readonly script?: string;
76
84
  }
77
85
 
86
+ /** The default run settings. */
78
87
  export const DEFAULT_RUN_OPTIONS: RunOptions = {
79
88
  installTimeoutMs: 180_000,
80
89
  scriptTimeoutMs: 120_000,
@@ -116,6 +125,7 @@ export function firstFailure(output: string): RunFailure | undefined {
116
125
  return { message: (lines[hinted] ?? "").trim(), frames: [] };
117
126
  }
118
127
 
128
+ /** Chooses the script to run, when the project defines a runnable one. */
119
129
  export function pickScript(manifest: Manifest, requested?: string): string | undefined {
120
130
  if (requested !== undefined) {
121
131
  return typeof manifest.scripts[requested] === "string" ? requested : undefined;
@@ -123,10 +133,12 @@ export function pickScript(manifest: Manifest, requested?: string): string | und
123
133
  return RUNNABLE_SCRIPTS.find((name) => typeof manifest.scripts[name] === "string");
124
134
  }
125
135
 
136
+ /** Whether a path is left out of the temporary copy the run phase works in. */
126
137
  export function isExcludedFromCopy(path: string): boolean {
127
138
  return (COPY_EXCLUDES as readonly string[]).includes(basename(path));
128
139
  }
129
140
 
141
+ /** The CommandRunner that spawns commands with Bun. */
130
142
  export function bunCommandRunner(): CommandRunner {
131
143
  return {
132
144
  run: async (command, options) => {
@@ -189,6 +201,7 @@ async function treeBytes(path: string): Promise<number> {
189
201
  return total;
190
202
  }
191
203
 
204
+ /** The real run environment: working directory, temp directory and runner. */
192
205
  export function systemRunEnvironment(): RunEnvironment {
193
206
  return {
194
207
  runner: bunCommandRunner(),
@@ -250,6 +263,7 @@ async function perform(
250
263
  };
251
264
  }
252
265
 
266
+ /** Copies the project, installs, runs the chosen script and reports what happened. */
253
267
  export async function executeProject(
254
268
  dir: string,
255
269
  manifest: Manifest,
@@ -14,6 +14,7 @@ export interface DependencyGraph {
14
14
  readonly duplicates: readonly DuplicateVersion[];
15
15
  }
16
16
 
17
+ /** A dependency resolved to more than one version. */
17
18
  export interface DuplicateVersion {
18
19
  readonly name: string;
19
20
  readonly versions: readonly string[];
@@ -17,8 +17,10 @@ import { defineError, type Result } from "../core/errors";
17
17
 
18
18
  export const LOCKFILE_KINDS = ["bun", "npm", "yarn", "pnpm"] as const;
19
19
 
20
+ /** The lockfile formats the scanner understands. */
20
21
  export type LockfileKind = (typeof LOCKFILE_KINDS)[number];
21
22
 
23
+ /** Lockfile filenames, in the order they are preferred. */
22
24
  export const LOCKFILE_FILENAMES: Readonly<Record<LockfileKind, string>> = {
23
25
  bun: "bun.lock",
24
26
  npm: "package-lock.json",
@@ -26,6 +28,7 @@ export const LOCKFILE_FILENAMES: Readonly<Record<LockfileKind, string>> = {
26
28
  pnpm: "pnpm-lock.yaml",
27
29
  };
28
30
 
31
+ /** One package as recorded by a lockfile. */
29
32
  export interface LockedPackage {
30
33
  readonly name: string;
31
34
  readonly version: string;
@@ -36,6 +39,7 @@ export interface LockedPackage {
36
39
  readonly installScript: boolean;
37
40
  }
38
41
 
42
+ /** A parsed lockfile: its format, packages and direct dependencies. */
39
43
  export interface ParsedLockfile {
40
44
  readonly kind: LockfileKind;
41
45
  readonly lockfileVersion: string | undefined;
@@ -527,6 +531,7 @@ function parsePnpmLock(text: string, path: string): Result<ParsedLockfile> {
527
531
  return { ok: true, value: { kind: "pnpm", lockfileVersion, packages: sortPackages(collected) } };
528
532
  }
529
533
 
534
+ /** Parses lockfile text into packages, or reports why it could not. */
530
535
  export function parseLockfile(
531
536
  kind: LockfileKind,
532
537
  text: string,
@@ -68,6 +68,7 @@ function readTrustedDependencies(value: unknown): string[] {
68
68
  return [];
69
69
  }
70
70
 
71
+ /** Reads package.json, reporting malformed JSON as an error. */
71
72
  export function parseManifest(text: string, source: string): Result<Manifest> {
72
73
  let parsed: unknown;
73
74
  try {
@@ -40,6 +40,7 @@ export function detectRuntime(): RuntimeInfo {
40
40
  return { bun: Bun.version, node: process.versions.node };
41
41
  }
42
42
 
43
+ /** Options for scanning a target: configuration, runtime versions and paths. */
43
44
  export interface ScanOptions {
44
45
  readonly fs?: FileSystem;
45
46
  readonly runtime?: RuntimeInfo;
@@ -27,6 +27,7 @@ type Disjunction = readonly Conjunction[];
27
27
 
28
28
  const VERSION_PATTERN = /^(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-([0-9A-Za-z.-]+))?$/;
29
29
 
30
+ /** Parses a semantic version, ignoring any surrounding operator. */
30
31
  export function parseVersion(text: string): ParsedVersion | undefined {
31
32
  const cleaned = text.trim().replace(/^[=v]+/, "");
32
33
  const match = VERSION_PATTERN.exec(cleaned);
@@ -52,6 +53,7 @@ function writtenParts(text: string): number {
52
53
  );
53
54
  }
54
55
 
56
+ /** Orders two parsed versions. */
55
57
  export function compareVersions(a: ParsedVersion, b: ParsedVersion): number {
56
58
  if (a.major !== b.major) {
57
59
  return a.major < b.major ? -1 : 1;
@@ -161,6 +163,7 @@ function comparatorsForToken(token: string): Comparator[] | undefined {
161
163
  return exact === undefined ? undefined : [{ operator: "=", version: exact }];
162
164
  }
163
165
 
166
+ /** Parses a version range into its operator and version parts. */
164
167
  export function parseRange(range: string): Disjunction | undefined {
165
168
  const trimmed = range.trim();
166
169
  if (trimmed === "" || trimmed === "*" || trimmed === "latest") {
@@ -212,6 +215,7 @@ const COMPARATOR_TESTS: Readonly<Record<Operator, (order: number) => boolean>> =
212
215
  "=": (order) => order === 0,
213
216
  };
214
217
 
218
+ /** Whether a version satisfies a range. */
215
219
  export function satisfies(candidate: string, range: string): boolean | undefined {
216
220
  const parsedRange = parseRange(range);
217
221
  const parsedVersion = parseVersion(candidate);
@@ -23,6 +23,7 @@ export const SOURCE_EXTENSIONS = [
23
23
  ".cjs",
24
24
  ] as const;
25
25
 
26
+ /** Directories the source walk never descends into. */
26
27
  export const IGNORED_DIRECTORIES = [
27
28
  "node_modules",
28
29
  ".git",
@@ -38,21 +39,26 @@ export const IGNORED_DIRECTORIES = [
38
39
  ".cache",
39
40
  ] as const;
40
41
 
42
+ /** Cap on how many source files are read per target. */
41
43
  export const MAX_SOURCE_FILES = 2000;
42
44
 
45
+ /** How a module was imported: static import, dynamic import or require. */
43
46
  export type ImportKind = "esm" | "cjs" | "dynamic";
44
47
 
48
+ /** One import found in a file: its specifier, kind and line. */
45
49
  export interface ImportRef {
46
50
  readonly specifier: string;
47
51
  readonly kind: ImportKind;
48
52
  readonly line: number;
49
53
  }
50
54
 
55
+ /** A source file read during the scan. */
51
56
  export interface SourceFile {
52
57
  readonly path: string;
53
58
  readonly imports: readonly ImportRef[];
54
59
  }
55
60
 
61
+ /** What the walk found: files, imports, and whether the cap truncated it. */
56
62
  export interface SourceScan {
57
63
  readonly files: readonly SourceFile[];
58
64
  readonly filesScanned: number;
@@ -333,8 +339,10 @@ export function nodeBuiltinNames(): Set<string> {
333
339
  return names;
334
340
  }
335
341
 
342
+ /** The categories an import specifier can fall into. */
336
343
  export type SpecifierKind = "node-builtin" | "bun-builtin" | "relative" | "absolute" | "package";
337
344
 
345
+ /** Classifies an import specifier. */
338
346
  export function classifySpecifier(
339
347
  specifier: string,
340
348
  builtins: ReadonlySet<string> = nodeBuiltinNames(),
@@ -366,6 +374,7 @@ function hasSourceExtension(name: string): boolean {
366
374
  return SOURCE_EXTENSIONS.some((extension) => lower.endsWith(extension));
367
375
  }
368
376
 
377
+ /** Limits for the source walk. */
369
378
  export interface ScanSourcesOptions {
370
379
  readonly maxFiles?: number;
371
380
  /** Substrings matched against each file path; a match skips the file. */
@@ -25,18 +25,21 @@ export interface PackageEvidence {
25
25
  readonly installScripts: readonly string[];
26
26
  }
27
27
 
28
+ /** A lockfile that was found and parsed. */
28
29
  export interface LoadedLockfile {
29
30
  readonly kind: LockfileKind;
30
31
  readonly path: string;
31
32
  readonly parsed: ParsedLockfile;
32
33
  }
33
34
 
35
+ /** A lockfile that was found but could not be parsed. */
34
36
  export interface UnparsedLockfile {
35
37
  readonly kind: LockfileKind;
36
38
  readonly path: string;
37
39
  readonly message: string;
38
40
  }
39
41
 
42
+ /** Everything read from one target directory: manifest, lockfiles, sources and config. */
40
43
  export interface TargetSnapshot {
41
44
  readonly dir: string;
42
45
  readonly manifestPath: string;
@@ -116,6 +119,7 @@ async function probeInstalledPackage(
116
119
  };
117
120
  }
118
121
 
122
+ /** Reads a directory's manifest, lockfiles, sources and configuration. */
119
123
  export async function readTarget(
120
124
  dir: string,
121
125
  fs: FileSystem = nodeFileSystem(),
@@ -23,11 +23,13 @@ export const WORKSPACE_EXCLUDES = [
23
23
 
24
24
  const MAX_DEPTH = 3;
25
25
 
26
+ /** A workspace package: its directory and its manifest. */
26
27
  export interface WorkspacePackage {
27
28
  /** Directory relative to the workspace root, using forward slashes. */
28
29
  readonly relative: string;
29
30
  }
30
31
 
32
+ /** Reads the workspace globs from package.json or pnpm-workspace.yaml. */
31
33
  export function workspacePatterns(manifest: Manifest, pnpmWorkspace: string | undefined): string[] {
32
34
  if (manifest.workspaces.length > 0) {
33
35
  return [...manifest.workspaces]
@@ -145,6 +147,7 @@ function stripTrailingSlashes(value: string): string {
145
147
  return value.slice(0, end);
146
148
  }
147
149
 
150
+ /** Resolves workspace patterns to the packages they match. */
148
151
  export async function findWorkspacePackages(
149
152
  root: string,
150
153
  patterns: readonly string[],
@@ -177,6 +180,7 @@ export async function findWorkspacePackages(
177
180
  return [...found.values()].sort((a, b) => a.relative.localeCompare(b.relative));
178
181
  }
179
182
 
183
+ /** Filename of the pnpm workspace file. */
180
184
  export const PNPM_WORKSPACE_FILENAME = "pnpm-workspace.yaml";
181
185
 
182
186
  /** Read and parse `pnpm-workspace.yaml`, if it is there. */