bun-ready 0.2.0 → 0.2.5

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.
Files changed (3) hide show
  1. package/README.md +65 -5
  2. package/dist/cli.js +501 -57
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -23,7 +23,7 @@ bun-ready scan .
23
23
 
24
24
  ## Usage
25
25
  ```bash
26
- bun-ready scan <path> [--format md|json] [--out <file>] [--no-install] [--no-test] [--verbose]
26
+ bun-ready scan <path> [--format md|json] [--out <file>] [--no-install] [--no-test] [--verbose] [--detailed]
27
27
  ```
28
28
 
29
29
  ## Examples:
@@ -32,6 +32,7 @@ bun-ready scan .
32
32
  bun-ready scan . --out bun-ready.md
33
33
  bun-ready scan ./packages/api --format json
34
34
  bun-ready scan . --no-install --no-test
35
+ bun-ready scan . --detailed
35
36
  ```
36
37
 
37
38
  ## Exit codes
@@ -71,6 +72,7 @@ You can create a `bun-ready.config.json` file in your repository root to customi
71
72
  | `ignoreFindings` | Array of finding IDs to ignore | `[]` |
72
73
  | `nativeAddonAllowlist` | Packages to exclude from native addon checks | `[]` |
73
74
  | `failOn` | When to return non-zero exit code | `"red"` |
75
+ | `detailed` | Enable detailed package usage analysis | `false` |
74
76
 
75
77
  ### New CLI Flags
76
78
 
@@ -80,10 +82,68 @@ You can create a `bun-ready.config.json` file in your repository root to customi
80
82
  - `all`: Scan root and all workspace packages (default)
81
83
 
82
84
  `--fail-on green|yellow|red`
83
- - Controls when bun-ready exits with a failure code
84
- - `green`: Fail on anything not green (exit 3)
85
- - `yellow`: Fail on red only (exit 3), yellow passes (exit 0)
86
- - `red`: Default behavior - green=0, yellow=2, red=3
85
+ - Controls when bun-ready exits with a failure code
86
+ - `green`: Fail on anything not green (exit 3)
87
+ - `yellow`: Fail on red only (exit 3), yellow passes (exit 0)
88
+ - `red`: Default behavior - green=0, yellow=2, red=3
89
+
90
+ `--detailed`
91
+ - Enables detailed package usage analysis
92
+ - Shows which packages are used in which files
93
+ - Provides file-by-file breakdown of imports
94
+ - Output is written to `bun-ready-detailed.md` instead of `bun-ready.md`
95
+ - Requires scanning of all `.ts`, `.js`, `.tsx`, `.jsx` files in the project
96
+ - **Note:** This operation is slower as it needs to read and parse all source files
97
+
98
+ ## Detailed Reports
99
+
100
+ When using the `--detailed` flag, bun-ready provides comprehensive package usage information:
101
+
102
+ ### What it analyzes:
103
+ - All source files with extensions: `.ts`, `.js`, `.tsx`, `.jsx`, `.mts`, `.mjs`
104
+ - Import patterns supported:
105
+ - ES6 imports: `import ... from 'package-name'`
106
+ - Namespace imports: `import * as name from 'package-name'`
107
+ - Dynamic imports: `import('package-name')`
108
+ - CommonJS requires: `require('package-name')`
109
+ - Local imports (starting with `./` or `../`) are ignored
110
+ - Skips `node_modules` and hidden directories
111
+
112
+ ### Output format:
113
+
114
+ The detailed report shows:
115
+ 1. **Package Summary** - Total files analyzed and packages used
116
+ 2. **Per-package usage** - For each package in your dependencies:
117
+ - How many files import it
118
+ - List of all file paths where it's used
119
+ 3. **Regular findings** - All migration risk findings from standard analysis
120
+
121
+ ### Example:
122
+ ```bash
123
+ bun-ready scan . --detailed
124
+ ```
125
+
126
+ This generates `bun-ready-detailed.md` with sections like:
127
+
128
+ ```markdown
129
+ ### @nestjs/common (15 files)
130
+ - src/main.ts
131
+ - src/app.module.ts
132
+ - src/auth/auth.service.ts
133
+ ...
134
+ ```
135
+
136
+ ### Configuration:
137
+
138
+ You can also enable detailed reports via config file:
139
+
140
+ ```json
141
+ {
142
+ "detailed": true
143
+ }
144
+ ```
145
+
146
+ When `detailed` is set in config, it acts as if `--detailed` was passed, unless overridden by CLI flags.
87
147
 
88
148
  ## How Scoring Works
89
149
 
package/dist/cli.js CHANGED
@@ -1,11 +1,14 @@
1
- // src/cli.ts
2
- import { promises as fs3 } from "node:fs";
3
- import path5 from "node:path";
4
-
5
- // src/analyze.ts
6
- import path4 from "node:path";
7
- import os from "node:os";
8
- import { promises as fs2 } from "node:fs";
1
+ var __defProp = Object.defineProperty;
2
+ var __export = (target, all) => {
3
+ for (var name in all)
4
+ __defProp(target, name, {
5
+ get: all[name],
6
+ enumerable: true,
7
+ configurable: true,
8
+ set: (newValue) => all[name] = () => newValue
9
+ });
10
+ };
11
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
9
12
 
10
13
  // src/spawn.ts
11
14
  import { spawn } from "node:child_process";
@@ -18,8 +21,7 @@ var runNodeSpawn = async (cmd, args, cwd) => {
18
21
  child.stderr.on("data", (d) => err += String(d));
19
22
  child.on("close", (code) => resolve({ code: code ?? 1, stdout: out, stderr: err }));
20
23
  });
21
- };
22
- var runBunSpawn = async (cmd, args, cwd) => {
24
+ }, runBunSpawn = async (cmd, args, cwd) => {
23
25
  const BunAny = globalThis;
24
26
  const bun = BunAny.Bun;
25
27
  if (!bun)
@@ -46,10 +48,53 @@ var runBunSpawn = async (cmd, args, cwd) => {
46
48
  };
47
49
  const [stdout, stderr, code] = await Promise.all([readAll(proc.stdout), readAll(proc.stderr), proc.exited]);
48
50
  return { code, stdout, stderr };
49
- };
50
- var exec = async (cmd, args, cwd) => {
51
+ }, exec = async (cmd, args, cwd) => {
51
52
  return await runBunSpawn(cmd, args, cwd);
52
53
  };
54
+ var init_spawn = () => {};
55
+
56
+ // src/bun_check.ts
57
+ var exports_bun_check = {};
58
+ __export(exports_bun_check, {
59
+ checkBunAvailable: () => checkBunAvailable
60
+ });
61
+ async function checkBunAvailable() {
62
+ try {
63
+ const res = await exec("bun", ["--version"], process.cwd());
64
+ if (res.code === 0 && res.stdout.includes("bun")) {
65
+ return { available: true };
66
+ }
67
+ return {
68
+ available: false,
69
+ error: `Bun command returned unexpected output (exit ${res.code})`
70
+ };
71
+ } catch (error) {
72
+ const msg = error instanceof Error ? error.message : String(error);
73
+ if (msg.includes("ENOENT") || msg.includes("spawn bun ENOENT")) {
74
+ return {
75
+ available: false,
76
+ error: "Bun is not installed. Please install Bun from https://bun.sh or use --no-install and --no-test flags."
77
+ };
78
+ }
79
+ return {
80
+ available: false,
81
+ error: `Failed to check Bun availability: ${msg}`
82
+ };
83
+ }
84
+ }
85
+ var init_bun_check = __esm(() => {
86
+ init_spawn();
87
+ });
88
+
89
+ // src/cli.ts
90
+ import { promises as fs4 } from "node:fs";
91
+ import path6 from "node:path";
92
+
93
+ // src/analyze.ts
94
+ init_spawn();
95
+ import path5 from "node:path";
96
+ import os from "node:os";
97
+ import { promises as fs3 } from "node:fs";
53
98
 
54
99
  // src/util.ts
55
100
  import { promises as fs } from "node:fs";
@@ -399,7 +444,15 @@ var detectNativeAddonRiskV2 = (repo, config) => {
399
444
  const suspects = names.filter((n) => {
400
445
  if (allowlist.includes(n))
401
446
  return false;
402
- return NATIVE_SUSPECTS_V2.includes(n) || includesAny(n, ["napi", "node-gyp", "prebuild", "ffi", "bindings", "native", "native-module"]);
447
+ const inList = NATIVE_SUSPECTS_V2.includes(n);
448
+ if (inList)
449
+ return true;
450
+ const keywords = ["@napi-rs/", "napi-rs", "node-napi", "neon", "node-gyp", "prebuild", "ffi", "bindings", "native", "native-module"];
451
+ const keywordMatch = includesAny(n, keywords);
452
+ if (keywordMatch) {
453
+ return true;
454
+ }
455
+ return false;
403
456
  });
404
457
  const scriptNames = Object.keys(repo.scripts);
405
458
  const hasNodeGypRebuild = scriptNames.some((k) => {
@@ -435,6 +488,173 @@ var summarizeSeverity = (findings, installOk, testOk) => {
435
488
  sev = "red";
436
489
  return sev;
437
490
  };
491
+ var calculatePackageStats = (pkg, findings) => {
492
+ const dependencies = pkg.dependencies || {};
493
+ const devDependencies = pkg.devDependencies || {};
494
+ const riskyPackageNames = new Set;
495
+ for (const finding of findings) {
496
+ for (const detail of finding.details) {
497
+ const match = detail.match(/^([a-zA-Z0-9_@\/\.\-]+)/);
498
+ if (match && match[1]) {
499
+ const fullPkg = match[1];
500
+ const pkgName = fullPkg.split(/[@:]/)[0];
501
+ if (pkgName) {
502
+ riskyPackageNames.add(pkgName);
503
+ }
504
+ }
505
+ }
506
+ }
507
+ let cleanDependencies = 0;
508
+ let riskyDependencies = 0;
509
+ let cleanDevDependencies = 0;
510
+ let riskyDevDependencies = 0;
511
+ for (const depName of Object.keys(dependencies)) {
512
+ if (riskyPackageNames.has(depName)) {
513
+ riskyDependencies++;
514
+ } else {
515
+ cleanDependencies++;
516
+ }
517
+ }
518
+ for (const depName of Object.keys(devDependencies)) {
519
+ if (riskyPackageNames.has(depName)) {
520
+ riskyDevDependencies++;
521
+ } else {
522
+ cleanDevDependencies++;
523
+ }
524
+ }
525
+ return {
526
+ totalDependencies: Object.keys(dependencies).length,
527
+ totalDevDependencies: Object.keys(devDependencies).length,
528
+ cleanDependencies,
529
+ cleanDevDependencies,
530
+ riskyDependencies,
531
+ riskyDevDependencies
532
+ };
533
+ };
534
+ var calculateFindingsSummary = (findings) => {
535
+ let green = 0;
536
+ let yellow = 0;
537
+ let red = 0;
538
+ for (const finding of findings) {
539
+ if (finding.severity === "green") {
540
+ green++;
541
+ } else if (finding.severity === "yellow") {
542
+ yellow++;
543
+ } else if (finding.severity === "red") {
544
+ red++;
545
+ }
546
+ }
547
+ return {
548
+ green,
549
+ yellow,
550
+ red,
551
+ total: green + yellow + red
552
+ };
553
+ };
554
+
555
+ // src/usage_analyzer.ts
556
+ import { promises as fs2 } from "node:fs";
557
+ import path2 from "node:path";
558
+ var SUPPORTED_EXTENSIONS = [".ts", ".js", ".tsx", ".jsx", ".mts", ".mjs"];
559
+ var IMPORT_PATTERNS = [
560
+ /import\s+(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)\s+from\s+['"]([^./][^'"]*)['"]/g,
561
+ /import\s*\(\s*['"]([^./][^'"]*)['"]\s*\)/g,
562
+ /require\s*\(\s*['"]([^./][^'"]*)['"]\s*\)/g
563
+ ];
564
+ function extractPackageNames(content) {
565
+ const packageSet = new Set;
566
+ for (const pattern of IMPORT_PATTERNS) {
567
+ let match;
568
+ while ((match = pattern.exec(content)) !== null) {
569
+ const packageName = match[1];
570
+ if (packageName) {
571
+ packageSet.add(packageName);
572
+ }
573
+ }
574
+ }
575
+ return Array.from(packageSet);
576
+ }
577
+ async function findSourceFiles(dirPath) {
578
+ const files = [];
579
+ let entries;
580
+ try {
581
+ entries = await fs2.readdir(dirPath, { withFileTypes: true });
582
+ } catch (error) {
583
+ return files;
584
+ }
585
+ for (const entry of entries) {
586
+ const fullPath = path2.join(dirPath, entry.name);
587
+ if (entry.name === "node_modules" || entry.name.startsWith(".")) {
588
+ continue;
589
+ }
590
+ if (entry.isDirectory()) {
591
+ const subFiles = await findSourceFiles(fullPath);
592
+ files.push(...subFiles);
593
+ } else if (entry.isFile()) {
594
+ const ext = path2.extname(entry.name);
595
+ if (SUPPORTED_EXTENSIONS.includes(ext)) {
596
+ files.push(fullPath);
597
+ }
598
+ }
599
+ }
600
+ return files;
601
+ }
602
+ function getAllPackageNames(pkg) {
603
+ const packageNames = new Set;
604
+ if (pkg.dependencies) {
605
+ Object.keys(pkg.dependencies).forEach((name) => packageNames.add(name));
606
+ }
607
+ if (pkg.devDependencies) {
608
+ Object.keys(pkg.devDependencies).forEach((name) => packageNames.add(name));
609
+ }
610
+ if (pkg.optionalDependencies) {
611
+ Object.keys(pkg.optionalDependencies).forEach((name) => packageNames.add(name));
612
+ }
613
+ return packageNames;
614
+ }
615
+ var analyzePackageUsageAsync = async (pkg, packagePath, includeDetails = true) => {
616
+ const packageNames = getAllPackageNames(pkg);
617
+ const totalPackages = packageNames.size;
618
+ const sourceFiles = await findSourceFiles(packagePath);
619
+ const analyzedFiles = sourceFiles.length;
620
+ const usageByPackage = new Map;
621
+ for (const pkgName of packageNames) {
622
+ usageByPackage.set(pkgName, {
623
+ packageName: pkgName,
624
+ fileCount: 0,
625
+ filePaths: []
626
+ });
627
+ }
628
+ for (const filePath of sourceFiles) {
629
+ try {
630
+ const content = await fs2.readFile(filePath, "utf-8");
631
+ const importedPackages = extractPackageNames(content);
632
+ for (const importedPkg of importedPackages) {
633
+ const usage = usageByPackage.get(importedPkg);
634
+ if (usage) {
635
+ usage.fileCount++;
636
+ if (includeDetails) {
637
+ const relativePath = path2.relative(packagePath, filePath);
638
+ usage.filePaths.push(relativePath);
639
+ }
640
+ usageByPackage.set(importedPkg, usage);
641
+ }
642
+ }
643
+ } catch (error) {
644
+ continue;
645
+ }
646
+ }
647
+ if (includeDetails) {
648
+ for (const usage of usageByPackage.values()) {
649
+ usage.filePaths.sort();
650
+ }
651
+ }
652
+ return {
653
+ totalPackages,
654
+ analyzedFiles,
655
+ usageByPackage
656
+ };
657
+ };
438
658
 
439
659
  // src/bun_logs.ts
440
660
  function parseInstallLogs(logs) {
@@ -490,11 +710,11 @@ function parseInstallLogs(logs) {
490
710
  }
491
711
 
492
712
  // src/workspaces.ts
493
- import path2 from "node:path";
713
+ import path3 from "node:path";
494
714
  import fsSync from "node:fs";
495
- function globMatch(pattern, path3) {
715
+ function globMatch(pattern, path4) {
496
716
  const patternParts = pattern.split("/");
497
- const pathParts = path3.split("/");
717
+ const pathParts = path4.split("/");
498
718
  let patternIdx = 0;
499
719
  let pathIdx = 0;
500
720
  while (patternIdx < patternParts.length && pathIdx < pathParts.length) {
@@ -532,16 +752,16 @@ function discoverFromWorkspaces(rootPath, workspaces) {
532
752
  const patterns = Array.isArray(workspaces) ? workspaces : workspaces.packages;
533
753
  const packages = [];
534
754
  for (const pattern of patterns) {
535
- const patternPath = path2.resolve(rootPath, pattern);
755
+ const patternPath = path3.resolve(rootPath, pattern);
536
756
  if (pattern.includes("/*") && !pattern.includes("/**")) {
537
757
  try {
538
- const baseDir = path2.dirname(patternPath);
539
- const patternName = path2.basename(patternPath);
758
+ const baseDir = path3.dirname(patternPath);
759
+ const patternName = path3.basename(patternPath);
540
760
  const entries = fsSync.readdirSync(baseDir, { withFileTypes: true });
541
761
  for (const entry of entries) {
542
762
  if (entry.isDirectory() && globMatch(patternName, entry.name)) {
543
- const packagePath = path2.join(baseDir, entry.name);
544
- const pkgJsonPath = path2.join(packagePath, "package.json");
763
+ const packagePath = path3.join(baseDir, entry.name);
764
+ const pkgJsonPath = path3.join(packagePath, "package.json");
545
765
  if (fileExistsSync(pkgJsonPath)) {
546
766
  packages.push(packagePath);
547
767
  }
@@ -562,7 +782,7 @@ function fileExistsSync(filePath) {
562
782
  }
563
783
  async function discoverWorkspaces(rootPath) {
564
784
  const packages = [];
565
- const rootPkgJson = path2.join(rootPath, "package.json");
785
+ const rootPkgJson = path3.join(rootPath, "package.json");
566
786
  if (!await fileExists(rootPkgJson)) {
567
787
  return packages;
568
788
  }
@@ -576,7 +796,7 @@ async function discoverWorkspaces(rootPath) {
576
796
  }
577
797
  const packagePaths = discoverFromWorkspaces(rootPath, workspaces);
578
798
  for (const pkgPath of packagePaths) {
579
- const pkgJsonPath = path2.join(pkgPath, "package.json");
799
+ const pkgJsonPath = path3.join(pkgPath, "package.json");
580
800
  try {
581
801
  const pkg = await readJsonFile(pkgJsonPath);
582
802
  if (pkg.name) {
@@ -594,7 +814,7 @@ async function discoverWorkspaces(rootPath) {
594
814
  return packages;
595
815
  }
596
816
  async function hasWorkspaces(rootPath) {
597
- const rootPkgJson = path2.join(rootPath, "package.json");
817
+ const rootPkgJson = path3.join(rootPath, "package.json");
598
818
  if (!await fileExists(rootPkgJson)) {
599
819
  return false;
600
820
  }
@@ -603,10 +823,10 @@ async function hasWorkspaces(rootPath) {
603
823
  }
604
824
 
605
825
  // src/config.ts
606
- import path3 from "node:path";
826
+ import path4 from "node:path";
607
827
  var CONFIG_FILE_NAME = "bun-ready.config.json";
608
828
  async function readConfig(rootPath) {
609
- const configPath = path3.join(rootPath, CONFIG_FILE_NAME);
829
+ const configPath = path4.join(rootPath, CONFIG_FILE_NAME);
610
830
  if (!await fileExists(configPath)) {
611
831
  return null;
612
832
  }
@@ -650,13 +870,16 @@ function validateConfig(config) {
650
870
  result.failOn = cfg.failOn;
651
871
  }
652
872
  }
873
+ if (typeof cfg.detailed === "boolean") {
874
+ result.detailed = cfg.detailed;
875
+ }
653
876
  if (Object.keys(result).length === 0) {
654
877
  return null;
655
878
  }
656
879
  return result;
657
880
  }
658
881
  function mergeConfigWithOpts(config, opts) {
659
- if (!config && !opts.failOn) {
882
+ if (!config && !opts.failOn && opts.detailed === undefined) {
660
883
  return null;
661
884
  }
662
885
  const result = {
@@ -665,49 +888,64 @@ function mergeConfigWithOpts(config, opts) {
665
888
  if (opts.failOn) {
666
889
  result.failOn = opts.failOn;
667
890
  }
891
+ if (opts.detailed !== undefined) {
892
+ result.detailed = opts.detailed;
893
+ }
668
894
  return Object.keys(result).length > 0 ? result : null;
669
895
  }
670
896
 
671
897
  // src/analyze.ts
672
898
  async function readRepoInfo(packagePath) {
673
- const packageJsonPath = path4.join(packagePath, "package.json");
899
+ const packageJsonPath = path5.join(packagePath, "package.json");
674
900
  const pkg = await readJsonFile(packageJsonPath);
675
901
  const scripts = pkg.scripts ?? {};
676
902
  const dependencies = pkg.dependencies ?? {};
677
903
  const devDependencies = pkg.devDependencies ?? {};
678
904
  const optionalDependencies = pkg.optionalDependencies ?? {};
679
905
  const lockfiles = {
680
- bunLock: await fileExists(path4.join(packagePath, "bun.lock")),
681
- bunLockb: await fileExists(path4.join(packagePath, "bun.lockb")),
682
- npmLock: await fileExists(path4.join(packagePath, "package-lock.json")),
683
- yarnLock: await fileExists(path4.join(packagePath, "yarn.lock")),
684
- pnpmLock: await fileExists(path4.join(packagePath, "pnpm-lock.yaml"))
906
+ bunLock: await fileExists(path5.join(packagePath, "bun.lock")),
907
+ bunLockb: await fileExists(path5.join(packagePath, "bun.lockb")),
908
+ npmLock: await fileExists(path5.join(packagePath, "package-lock.json")),
909
+ yarnLock: await fileExists(path5.join(packagePath, "yarn.lock")),
910
+ pnpmLock: await fileExists(path5.join(packagePath, "pnpm-lock.yaml"))
685
911
  };
686
912
  return { pkg, scripts, dependencies, devDependencies, optionalDependencies, lockfiles };
687
913
  }
688
914
  async function copyIfExists(from, to) {
689
915
  try {
690
- await fs2.copyFile(from, to);
916
+ await fs3.copyFile(from, to);
691
917
  } catch {
692
918
  return;
693
919
  }
694
920
  }
695
921
  async function runBunInstallDryRun(packagePath) {
696
- const base = await fs2.mkdtemp(path4.join(os.tmpdir(), "bun-ready-"));
922
+ const { checkBunAvailable: checkBunAvailable2 } = await Promise.resolve().then(() => (init_bun_check(), exports_bun_check));
923
+ const bunCheck = await checkBunAvailable2();
924
+ if (!bunCheck.available) {
925
+ const skipReason = bunCheck.error;
926
+ return {
927
+ ok: false,
928
+ summary: `Skipped: ${skipReason}`,
929
+ logs: [],
930
+ installAnalysis: { blockedDeps: [], trustedDepsMentioned: [], notes: [] },
931
+ skipReason
932
+ };
933
+ }
934
+ const base = await fs3.mkdtemp(path5.join(os.tmpdir(), "bun-ready-"));
697
935
  const cleanup = async () => {
698
936
  try {
699
- await fs2.rm(base, { recursive: true, force: true });
937
+ await fs3.rm(base, { recursive: true, force: true });
700
938
  } catch {
701
939
  return;
702
940
  }
703
941
  };
704
942
  try {
705
- await copyIfExists(path4.join(packagePath, "package.json"), path4.join(base, "package.json"));
706
- await copyIfExists(path4.join(packagePath, "bun.lock"), path4.join(base, "bun.lock"));
707
- await copyIfExists(path4.join(packagePath, "bun.lockb"), path4.join(base, "bun.lockb"));
708
- await copyIfExists(path4.join(packagePath, "package-lock.json"), path4.join(base, "package-lock.json"));
709
- await copyIfExists(path4.join(packagePath, "yarn.lock"), path4.join(base, "yarn.lock"));
710
- await copyIfExists(path4.join(packagePath, "pnpm-lock.yaml"), path4.join(base, "pnpm-lock.yaml"));
943
+ await copyIfExists(path5.join(packagePath, "package.json"), path5.join(base, "package.json"));
944
+ await copyIfExists(path5.join(packagePath, "bun.lock"), path5.join(base, "bun.lock"));
945
+ await copyIfExists(path5.join(packagePath, "bun.lockb"), path5.join(base, "bun.lockb"));
946
+ await copyIfExists(path5.join(packagePath, "package-lock.json"), path5.join(base, "package-lock.json"));
947
+ await copyIfExists(path5.join(packagePath, "yarn.lock"), path5.join(base, "yarn.lock"));
948
+ await copyIfExists(path5.join(packagePath, "pnpm-lock.yaml"), path5.join(base, "pnpm-lock.yaml"));
711
949
  const res = await exec("bun", ["install", "--dry-run"], base);
712
950
  const combined = [...res.stdout ? res.stdout.split(`
713
951
  `) : [], ...res.stderr ? res.stderr.split(`
@@ -726,6 +964,17 @@ function shouldRunBunTest(scripts) {
726
964
  return t.toLowerCase().includes("bun test") || t.toLowerCase().trim() === "bun test";
727
965
  }
728
966
  async function runBunTest(packagePath) {
967
+ const { checkBunAvailable: checkBunAvailable2 } = await Promise.resolve().then(() => (init_bun_check(), exports_bun_check));
968
+ const bunCheck = await checkBunAvailable2();
969
+ if (!bunCheck.available) {
970
+ const skipReason = bunCheck.error;
971
+ return {
972
+ ok: false,
973
+ summary: `Skipped: ${skipReason}`,
974
+ logs: [],
975
+ skipReason
976
+ };
977
+ }
729
978
  const res = await exec("bun", ["test"], packagePath);
730
979
  const combined = [...res.stdout ? res.stdout.split(`
731
980
  `) : [], ...res.stderr ? res.stderr.split(`
@@ -742,7 +991,7 @@ function filterFindings(findings, config) {
742
991
  }
743
992
  async function analyzeSinglePackage(packagePath, opts, config, pkgName) {
744
993
  const info = await readRepoInfo(packagePath);
745
- const name = pkgName || info.pkg.name || path4.basename(packagePath);
994
+ const name = pkgName || info.pkg.name || path5.basename(packagePath);
746
995
  let findings = [
747
996
  ...detectLockfileSignals({ packageJsonPath: packagePath, lockfiles: info.lockfiles, scripts: info.scripts, dependencies: info.dependencies, devDependencies: info.devDependencies, optionalDependencies: info.optionalDependencies, hasWorkspaces: false, packageJson: info.pkg }),
748
997
  ...detectScriptRisks({ packageJsonPath: packagePath, lockfiles: info.lockfiles, scripts: info.scripts, dependencies: info.dependencies, devDependencies: info.devDependencies, optionalDependencies: info.optionalDependencies, hasWorkspaces: false, packageJson: info.pkg }),
@@ -758,7 +1007,8 @@ async function analyzeSinglePackage(packagePath, opts, config, pkgName) {
758
1007
  install = {
759
1008
  ok: installResult.ok,
760
1009
  summary: installResult.summary,
761
- logs: installResult.logs
1010
+ logs: installResult.logs,
1011
+ ...installResult.skipReason !== undefined ? { skipReason: installResult.skipReason } : {}
762
1012
  };
763
1013
  installOk = installResult.ok;
764
1014
  if (installResult.installAnalysis.blockedDeps.length > 0) {
@@ -794,18 +1044,28 @@ async function analyzeSinglePackage(packagePath, opts, config, pkgName) {
794
1044
  test = {
795
1045
  ok: testResult.ok,
796
1046
  summary: testResult.summary,
797
- logs: testResult.logs
1047
+ logs: testResult.logs,
1048
+ ...testResult.skipReason !== undefined ? { skipReason: testResult.skipReason } : {}
798
1049
  };
799
1050
  testOk = testResult.ok;
800
1051
  }
801
1052
  const severity = summarizeSeverity(findings, installOk, testOk);
1053
+ const stats = calculatePackageStats(info.pkg, findings);
1054
+ const findingsSummary = calculateFindingsSummary(findings);
1055
+ let packageUsage;
1056
+ if (opts.detailed) {
1057
+ try {
1058
+ const usage = await analyzePackageUsageAsync(info.pkg, packagePath, true);
1059
+ packageUsage = usage;
1060
+ } catch (error) {}
1061
+ }
802
1062
  const summaryLines = [];
803
1063
  summaryLines.push(`Lockfiles: ${info.lockfiles.bunLock || info.lockfiles.bunLockb ? "bun" : "non-bun or missing"}`);
804
1064
  summaryLines.push(`Lifecycle scripts: ${Object.keys(info.scripts).some((k) => ["postinstall", "prepare", "preinstall", "install"].includes(k)) ? "present" : "none"}`);
805
1065
  summaryLines.push(`Native addon risk: ${findings.some((f) => f.id === "deps.native_addons") ? "yes" : "no"}`);
806
1066
  summaryLines.push(`bun install dry-run: ${install ? install.ok ? "ok" : "failed" : "skipped"}`);
807
1067
  summaryLines.push(`bun test: ${test ? test.ok ? "ok" : "failed" : "skipped"}`);
808
- return {
1068
+ const result = {
809
1069
  name,
810
1070
  path: packagePath,
811
1071
  severity,
@@ -817,8 +1077,14 @@ async function analyzeSinglePackage(packagePath, opts, config, pkgName) {
817
1077
  dependencies: info.dependencies,
818
1078
  devDependencies: info.devDependencies,
819
1079
  optionalDependencies: info.optionalDependencies,
820
- lockfiles: info.lockfiles
1080
+ lockfiles: info.lockfiles,
1081
+ stats,
1082
+ findingsSummary
821
1083
  };
1084
+ if (packageUsage !== undefined) {
1085
+ result.packageUsage = packageUsage;
1086
+ }
1087
+ return result;
822
1088
  }
823
1089
  function aggregateSeverity(packages, overallSeverity) {
824
1090
  if (overallSeverity === "red")
@@ -837,7 +1103,7 @@ function aggregateSeverity(packages, overallSeverity) {
837
1103
  }
838
1104
  async function analyzeRepoOverall(opts) {
839
1105
  const repoPath = normalizeRepoPath(opts.repoPath);
840
- const packageJsonPath = path4.join(repoPath, "package.json");
1106
+ const packageJsonPath = path5.join(repoPath, "package.json");
841
1107
  const hasPkg = await fileExists(packageJsonPath);
842
1108
  const config = await readConfig(repoPath);
843
1109
  if (!hasPkg) {
@@ -951,6 +1217,27 @@ var badge = (s) => {
951
1217
  return "\uD83D\uDFE1 YELLOW";
952
1218
  return "\uD83D\uDD34 RED";
953
1219
  };
1220
+ var getReadinessMessage = (severity, hasRedFindings) => {
1221
+ if (severity === "green" && !hasRedFindings) {
1222
+ return "✅ Вітаю, ви готові до переходу на Bun!";
1223
+ }
1224
+ if (severity === "yellow") {
1225
+ return "⚠️ Нажаль ви не готові до переходу на Bun, але це можливо з деякими змінами";
1226
+ }
1227
+ return "❌ Нажаль ви не готові до переходу на Bun через критичні проблеми";
1228
+ };
1229
+ var formatFindingsTable = (summary) => {
1230
+ const lines = [];
1231
+ lines.push(`## Findings Summary`);
1232
+ lines.push(`| Status | Count |`);
1233
+ lines.push(`|--------|-------|`);
1234
+ lines.push(`| \uD83D\uDFE2 Green | ${summary.green} |`);
1235
+ lines.push(`| \uD83D\uDFE1 Yellow | ${summary.yellow} |`);
1236
+ lines.push(`| \uD83D\uDD34 Red | ${summary.red} |`);
1237
+ lines.push(`| **Total** | **${summary.total}** |`);
1238
+ return lines.join(`
1239
+ `);
1240
+ };
954
1241
  var getTopFindings = (pkg, count = 3) => {
955
1242
  const sorted = [...pkg.findings].sort((a, b) => {
956
1243
  const severityOrder = { red: 0, yellow: 1, green: 2 };
@@ -963,14 +1250,44 @@ var getTopFindings = (pkg, count = 3) => {
963
1250
  };
964
1251
  var packageRow = (pkg) => {
965
1252
  const name = pkg.name;
966
- const path5 = pkg.path.replace(/\\/g, "/");
1253
+ const path6 = pkg.path.replace(/\\/g, "/");
967
1254
  const severity = badge(pkg.severity);
968
1255
  const topFindings = getTopFindings(pkg, 2).join(", ") || "No issues";
969
- return `| ${name} | \`${path5}\` | ${severity} | ${topFindings} |`;
1256
+ return `| ${name} | \`${path6}\` | ${severity} | ${topFindings} |`;
1257
+ };
1258
+ var formatPackageStats = (pkg) => {
1259
+ const lines = [];
1260
+ if (pkg.stats) {
1261
+ lines.push(`- Total dependencies: ${pkg.stats.totalDependencies}`);
1262
+ lines.push(`- Total devDependencies: ${pkg.stats.totalDevDependencies}`);
1263
+ lines.push(`- Clean dependencies: ${pkg.stats.cleanDependencies}`);
1264
+ lines.push(`- Clean devDependencies: ${pkg.stats.cleanDevDependencies}`);
1265
+ lines.push(`- Dependencies with findings: ${pkg.stats.riskyDependencies}`);
1266
+ lines.push(`- DevDependencies with findings: ${pkg.stats.riskyDevDependencies}`);
1267
+ }
1268
+ if (pkg.packageUsage) {
1269
+ lines.push(`- **Total files analyzed**: ${pkg.packageUsage.analyzedFiles}`);
1270
+ const usedPackages = Array.from(pkg.packageUsage.usageByPackage.values()).filter((u) => u.fileCount > 0).length;
1271
+ lines.push(`- **Packages used in code**: ${usedPackages}`);
1272
+ }
1273
+ return lines;
970
1274
  };
971
1275
  function renderMarkdown(r) {
972
1276
  const lines = [];
973
- lines.push(`# bun-ready report`);
1277
+ const bunVersion = process.version;
1278
+ const hasRedFindings = r.findings.some((f) => f.severity === "red");
1279
+ const readinessMessage = getReadinessMessage(r.severity, hasRedFindings);
1280
+ lines.push(`# bun-ready report - Tested with Bun ${bunVersion}`);
1281
+ lines.push(``);
1282
+ lines.push(readinessMessage);
1283
+ lines.push(``);
1284
+ const rootFindingsSummary = {
1285
+ green: r.findings.filter((f) => f.severity === "green").length,
1286
+ yellow: r.findings.filter((f) => f.severity === "yellow").length,
1287
+ red: r.findings.filter((f) => f.severity === "red").length,
1288
+ total: r.findings.length
1289
+ };
1290
+ lines.push(formatFindingsTable(rootFindingsSummary));
974
1291
  lines.push(``);
975
1292
  lines.push(`**Overall:** ${badge(r.severity)}`);
976
1293
  lines.push(``);
@@ -1049,6 +1366,13 @@ function renderMarkdown(r) {
1049
1366
  }
1050
1367
  lines.push(``);
1051
1368
  }
1369
+ const rootPkgForStats = r.packages?.find((p) => p.path === r.repo.packageJsonPath);
1370
+ if (rootPkgForStats && rootPkgForStats.stats) {
1371
+ lines.push(`## Package Summary`);
1372
+ for (const l of formatPackageStats(rootPkgForStats))
1373
+ lines.push(l);
1374
+ lines.push(``);
1375
+ }
1052
1376
  lines.push(`## Root Findings`);
1053
1377
  if (r.findings.length === 0) {
1054
1378
  lines.push(`No findings for root package.`);
@@ -1079,6 +1403,12 @@ function renderMarkdown(r) {
1079
1403
  for (const l of pkg.summaryLines)
1080
1404
  lines.push(`- ${l}`);
1081
1405
  lines.push(``);
1406
+ if (pkg.stats) {
1407
+ lines.push(`**Package Summary**`);
1408
+ for (const l of formatPackageStats(pkg))
1409
+ lines.push(l);
1410
+ lines.push(``);
1411
+ }
1082
1412
  if (pkg.install) {
1083
1413
  lines.push(`**bun install (dry-run):** ${pkg.install.ok ? "ok" : "failed"}`);
1084
1414
  if (pkg.install.logs.length > 0 && pkg.install.logs.length < 10) {
@@ -1124,6 +1454,90 @@ function renderMarkdown(r) {
1124
1454
  return lines.join(`
1125
1455
  `);
1126
1456
  }
1457
+ var renderDetailedReport = (r) => {
1458
+ const lines = [];
1459
+ const bunVersion = process.version;
1460
+ const hasRedFindings = r.findings.some((f) => f.severity === "red");
1461
+ const readinessMessage = getReadinessMessage(r.severity, hasRedFindings);
1462
+ lines.push(`# bun-ready detailed report - Tested with Bun ${bunVersion}`);
1463
+ lines.push(``);
1464
+ lines.push(readinessMessage);
1465
+ lines.push(``);
1466
+ const rootFindingsSummary = {
1467
+ green: r.findings.filter((f) => f.severity === "green").length,
1468
+ yellow: r.findings.filter((f) => f.severity === "yellow").length,
1469
+ red: r.findings.filter((f) => f.severity === "red").length,
1470
+ total: r.findings.length
1471
+ };
1472
+ lines.push(formatFindingsTable(rootFindingsSummary));
1473
+ lines.push(``);
1474
+ lines.push(`**Overall:** ${badge(r.severity)}`);
1475
+ lines.push(``);
1476
+ lines.push(`## Detailed Package Usage`);
1477
+ lines.push(``);
1478
+ let hasUsageInfo = false;
1479
+ if (r.packages && r.packages.length > 0) {
1480
+ const sortedPackages = stableSort(r.packages, (p) => p.name);
1481
+ for (const pkg of sortedPackages) {
1482
+ if (!pkg.packageUsage)
1483
+ continue;
1484
+ hasUsageInfo = true;
1485
+ lines.push(`### ${pkg.name}`);
1486
+ lines.push(``);
1487
+ lines.push(`**Total files analyzed:** ${pkg.packageUsage.analyzedFiles}`);
1488
+ lines.push(`**Total packages:** ${pkg.packageUsage.totalPackages}`);
1489
+ lines.push(``);
1490
+ const sortedUsage = Array.from(pkg.packageUsage.usageByPackage.values()).filter((u) => u.fileCount > 0).sort((a, b) => b.fileCount - a.fileCount);
1491
+ if (sortedUsage.length === 0) {
1492
+ lines.push(`No package usage detected in source files.`);
1493
+ lines.push(``);
1494
+ continue;
1495
+ }
1496
+ for (const usage of sortedUsage) {
1497
+ const depVersion = pkg.dependencies[usage.packageName] || pkg.devDependencies[usage.packageName] || "";
1498
+ const versionStr = depVersion ? `@${depVersion}` : "";
1499
+ lines.push(`#### ${usage.packageName}${versionStr} (${usage.fileCount} file${usage.fileCount !== 1 ? "s" : ""})`);
1500
+ lines.push(``);
1501
+ if (usage.filePaths.length > 0) {
1502
+ for (const filePath of usage.filePaths) {
1503
+ lines.push(`- ${filePath}`);
1504
+ }
1505
+ } else {
1506
+ lines.push(`- No file paths collected`);
1507
+ }
1508
+ lines.push(``);
1509
+ }
1510
+ }
1511
+ }
1512
+ if (!hasUsageInfo) {
1513
+ lines.push(`No package usage information available. Run with --detailed flag to enable usage analysis.`);
1514
+ lines.push(``);
1515
+ }
1516
+ lines.push(`---`);
1517
+ lines.push(``);
1518
+ lines.push(`## Root Findings`);
1519
+ if (r.findings.length === 0) {
1520
+ lines.push(`No findings for root package.`);
1521
+ } else {
1522
+ const findings = stableSort(r.findings, (f) => `${f.severity}:${f.id}`);
1523
+ for (const f of findings) {
1524
+ lines.push(`### ${f.title} (${badge(f.severity)})`);
1525
+ lines.push(``);
1526
+ for (const d of f.details)
1527
+ lines.push(`- ${d}`);
1528
+ if (f.hints.length > 0) {
1529
+ lines.push(``);
1530
+ lines.push(`**Hints:**`);
1531
+ for (const h of f.hints)
1532
+ lines.push(`- ${h}`);
1533
+ }
1534
+ lines.push(``);
1535
+ }
1536
+ }
1537
+ lines.push(``);
1538
+ return lines.join(`
1539
+ `);
1540
+ };
1127
1541
 
1128
1542
  // src/report_json.ts
1129
1543
  function renderJson(r) {
@@ -1136,7 +1550,7 @@ var usage = () => {
1136
1550
  "bun-ready",
1137
1551
  "",
1138
1552
  "Usage:",
1139
- " bun-ready scan <path> [--format md|json] [--out <file>] [--no-install] [--no-test] [--verbose] [--scope root|packages|all] [--fail-on green|yellow|red]",
1553
+ " bun-ready scan <path> [--format md|json] [--out <file>] [--no-install] [--no-test] [--verbose] [--detailed] [--scope root|packages|all] [--fail-on green|yellow|red]",
1140
1554
  "",
1141
1555
  "Options:",
1142
1556
  " --format md|json Output format (default: md)",
@@ -1144,6 +1558,7 @@ var usage = () => {
1144
1558
  " --no-install Skip bun install --dry-run",
1145
1559
  " --no-test Skip bun test",
1146
1560
  " --verbose Show detailed output",
1561
+ " --detailed Show detailed package usage report with file paths",
1147
1562
  " --scope root|packages|all Scan scope for monorepos (default: all)",
1148
1563
  " --fail-on green|yellow|red Fail policy (default: red)",
1149
1564
  "",
@@ -1168,6 +1583,7 @@ var parseArgs = (argv) => {
1168
1583
  runInstall: true,
1169
1584
  runTest: true,
1170
1585
  verbose: false,
1586
+ detailed: false,
1171
1587
  scope: "all"
1172
1588
  }
1173
1589
  };
@@ -1178,6 +1594,7 @@ var parseArgs = (argv) => {
1178
1594
  let runInstall = true;
1179
1595
  let runTest = true;
1180
1596
  let verbose = false;
1597
+ let detailed = false;
1181
1598
  let scope = "all";
1182
1599
  let failOn;
1183
1600
  for (let i = 2;i < args.length; i++) {
@@ -1206,6 +1623,10 @@ var parseArgs = (argv) => {
1206
1623
  verbose = true;
1207
1624
  continue;
1208
1625
  }
1626
+ if (a === "--detailed") {
1627
+ detailed = true;
1628
+ continue;
1629
+ }
1209
1630
  if (a === "--scope") {
1210
1631
  const v = args[i + 1] ?? "";
1211
1632
  if (v === "root" || v === "packages" || v === "all")
@@ -1228,6 +1649,7 @@ var parseArgs = (argv) => {
1228
1649
  runInstall,
1229
1650
  runTest,
1230
1651
  verbose,
1652
+ detailed,
1231
1653
  scope
1232
1654
  };
1233
1655
  if (failOn !== undefined) {
@@ -1269,7 +1691,14 @@ var main = async () => {
1269
1691
  `);
1270
1692
  process.exit(1);
1271
1693
  }
1272
- const config = await mergeConfigWithOpts(null, opts);
1694
+ const configOpts = {};
1695
+ if (opts.failOn !== undefined) {
1696
+ configOpts.failOn = opts.failOn;
1697
+ }
1698
+ if (opts.detailed !== undefined) {
1699
+ configOpts.detailed = opts.detailed;
1700
+ }
1701
+ const config = await mergeConfigWithOpts(null, configOpts);
1273
1702
  const scanOpts = {
1274
1703
  repoPath: opts.repoPath,
1275
1704
  format: opts.format,
@@ -1283,10 +1712,25 @@ var main = async () => {
1283
1712
  scanOpts.failOn = opts.failOn;
1284
1713
  }
1285
1714
  const res = await analyzeRepoOverall(scanOpts);
1286
- const out = opts.format === "json" ? renderJson(res) : renderMarkdown(res);
1287
- const target = opts.outFile ?? (opts.format === "json" ? "bun-ready.json" : "bun-ready.md");
1288
- const resolved = path5.resolve(process.cwd(), target);
1289
- await fs3.writeFile(resolved, out, "utf8");
1715
+ if (res.install?.skipReason || res.test?.skipReason) {
1716
+ const skipWarnings = [];
1717
+ if (res.install?.skipReason) {
1718
+ skipWarnings.push(`Install check skipped: ${res.install.skipReason}`);
1719
+ }
1720
+ if (res.test?.skipReason) {
1721
+ skipWarnings.push(`Test run skipped: ${res.test.skipReason}`);
1722
+ }
1723
+ if (skipWarnings.length > 0) {
1724
+ process.stderr.write(`WARNING:
1725
+ ${skipWarnings.map((w) => ` - ${w}`).join(`
1726
+ `)}
1727
+ `);
1728
+ }
1729
+ }
1730
+ const out = opts.format === "json" ? renderJson(res) : opts.detailed ? renderDetailedReport(res) : renderMarkdown(res);
1731
+ const target = opts.outFile ?? (opts.format === "json" ? "bun-ready.json" : opts.detailed ? "bun-ready-detailed.md" : "bun-ready.md");
1732
+ const resolved = path6.resolve(process.cwd(), target);
1733
+ await fs4.writeFile(resolved, out, "utf8");
1290
1734
  process.stdout.write(`Wrote ${opts.format.toUpperCase()} report to ${resolved}
1291
1735
  `);
1292
1736
  process.exit(exitCode(res.severity, config?.failOn || opts.failOn));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bun-ready",
3
- "version": "0.2.0",
3
+ "version": "0.2.5",
4
4
  "description": "CLI that estimates how painful migrating a Node.js repo to Bun might be. Generates a green/yellow/red Markdown report with reasons.",
5
5
  "author": "Pas7 Studio",
6
6
  "license": "Apache-2.0",