bun-ready 0.2.3 → 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 +397 -46
  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
@@ -87,14 +87,14 @@ var init_bun_check = __esm(() => {
87
87
  });
88
88
 
89
89
  // src/cli.ts
90
- import { promises as fs3 } from "node:fs";
91
- import path5 from "node:path";
90
+ import { promises as fs4 } from "node:fs";
91
+ import path6 from "node:path";
92
92
 
93
93
  // src/analyze.ts
94
94
  init_spawn();
95
- import path4 from "node:path";
95
+ import path5 from "node:path";
96
96
  import os from "node:os";
97
- import { promises as fs2 } from "node:fs";
97
+ import { promises as fs3 } from "node:fs";
98
98
 
99
99
  // src/util.ts
100
100
  import { promises as fs } from "node:fs";
@@ -488,6 +488,173 @@ var summarizeSeverity = (findings, installOk, testOk) => {
488
488
  sev = "red";
489
489
  return sev;
490
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
+ };
491
658
 
492
659
  // src/bun_logs.ts
493
660
  function parseInstallLogs(logs) {
@@ -543,11 +710,11 @@ function parseInstallLogs(logs) {
543
710
  }
544
711
 
545
712
  // src/workspaces.ts
546
- import path2 from "node:path";
713
+ import path3 from "node:path";
547
714
  import fsSync from "node:fs";
548
- function globMatch(pattern, path3) {
715
+ function globMatch(pattern, path4) {
549
716
  const patternParts = pattern.split("/");
550
- const pathParts = path3.split("/");
717
+ const pathParts = path4.split("/");
551
718
  let patternIdx = 0;
552
719
  let pathIdx = 0;
553
720
  while (patternIdx < patternParts.length && pathIdx < pathParts.length) {
@@ -585,16 +752,16 @@ function discoverFromWorkspaces(rootPath, workspaces) {
585
752
  const patterns = Array.isArray(workspaces) ? workspaces : workspaces.packages;
586
753
  const packages = [];
587
754
  for (const pattern of patterns) {
588
- const patternPath = path2.resolve(rootPath, pattern);
755
+ const patternPath = path3.resolve(rootPath, pattern);
589
756
  if (pattern.includes("/*") && !pattern.includes("/**")) {
590
757
  try {
591
- const baseDir = path2.dirname(patternPath);
592
- const patternName = path2.basename(patternPath);
758
+ const baseDir = path3.dirname(patternPath);
759
+ const patternName = path3.basename(patternPath);
593
760
  const entries = fsSync.readdirSync(baseDir, { withFileTypes: true });
594
761
  for (const entry of entries) {
595
762
  if (entry.isDirectory() && globMatch(patternName, entry.name)) {
596
- const packagePath = path2.join(baseDir, entry.name);
597
- const pkgJsonPath = path2.join(packagePath, "package.json");
763
+ const packagePath = path3.join(baseDir, entry.name);
764
+ const pkgJsonPath = path3.join(packagePath, "package.json");
598
765
  if (fileExistsSync(pkgJsonPath)) {
599
766
  packages.push(packagePath);
600
767
  }
@@ -615,7 +782,7 @@ function fileExistsSync(filePath) {
615
782
  }
616
783
  async function discoverWorkspaces(rootPath) {
617
784
  const packages = [];
618
- const rootPkgJson = path2.join(rootPath, "package.json");
785
+ const rootPkgJson = path3.join(rootPath, "package.json");
619
786
  if (!await fileExists(rootPkgJson)) {
620
787
  return packages;
621
788
  }
@@ -629,7 +796,7 @@ async function discoverWorkspaces(rootPath) {
629
796
  }
630
797
  const packagePaths = discoverFromWorkspaces(rootPath, workspaces);
631
798
  for (const pkgPath of packagePaths) {
632
- const pkgJsonPath = path2.join(pkgPath, "package.json");
799
+ const pkgJsonPath = path3.join(pkgPath, "package.json");
633
800
  try {
634
801
  const pkg = await readJsonFile(pkgJsonPath);
635
802
  if (pkg.name) {
@@ -647,7 +814,7 @@ async function discoverWorkspaces(rootPath) {
647
814
  return packages;
648
815
  }
649
816
  async function hasWorkspaces(rootPath) {
650
- const rootPkgJson = path2.join(rootPath, "package.json");
817
+ const rootPkgJson = path3.join(rootPath, "package.json");
651
818
  if (!await fileExists(rootPkgJson)) {
652
819
  return false;
653
820
  }
@@ -656,10 +823,10 @@ async function hasWorkspaces(rootPath) {
656
823
  }
657
824
 
658
825
  // src/config.ts
659
- import path3 from "node:path";
826
+ import path4 from "node:path";
660
827
  var CONFIG_FILE_NAME = "bun-ready.config.json";
661
828
  async function readConfig(rootPath) {
662
- const configPath = path3.join(rootPath, CONFIG_FILE_NAME);
829
+ const configPath = path4.join(rootPath, CONFIG_FILE_NAME);
663
830
  if (!await fileExists(configPath)) {
664
831
  return null;
665
832
  }
@@ -703,13 +870,16 @@ function validateConfig(config) {
703
870
  result.failOn = cfg.failOn;
704
871
  }
705
872
  }
873
+ if (typeof cfg.detailed === "boolean") {
874
+ result.detailed = cfg.detailed;
875
+ }
706
876
  if (Object.keys(result).length === 0) {
707
877
  return null;
708
878
  }
709
879
  return result;
710
880
  }
711
881
  function mergeConfigWithOpts(config, opts) {
712
- if (!config && !opts.failOn) {
882
+ if (!config && !opts.failOn && opts.detailed === undefined) {
713
883
  return null;
714
884
  }
715
885
  const result = {
@@ -718,29 +888,32 @@ function mergeConfigWithOpts(config, opts) {
718
888
  if (opts.failOn) {
719
889
  result.failOn = opts.failOn;
720
890
  }
891
+ if (opts.detailed !== undefined) {
892
+ result.detailed = opts.detailed;
893
+ }
721
894
  return Object.keys(result).length > 0 ? result : null;
722
895
  }
723
896
 
724
897
  // src/analyze.ts
725
898
  async function readRepoInfo(packagePath) {
726
- const packageJsonPath = path4.join(packagePath, "package.json");
899
+ const packageJsonPath = path5.join(packagePath, "package.json");
727
900
  const pkg = await readJsonFile(packageJsonPath);
728
901
  const scripts = pkg.scripts ?? {};
729
902
  const dependencies = pkg.dependencies ?? {};
730
903
  const devDependencies = pkg.devDependencies ?? {};
731
904
  const optionalDependencies = pkg.optionalDependencies ?? {};
732
905
  const lockfiles = {
733
- bunLock: await fileExists(path4.join(packagePath, "bun.lock")),
734
- bunLockb: await fileExists(path4.join(packagePath, "bun.lockb")),
735
- npmLock: await fileExists(path4.join(packagePath, "package-lock.json")),
736
- yarnLock: await fileExists(path4.join(packagePath, "yarn.lock")),
737
- 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"))
738
911
  };
739
912
  return { pkg, scripts, dependencies, devDependencies, optionalDependencies, lockfiles };
740
913
  }
741
914
  async function copyIfExists(from, to) {
742
915
  try {
743
- await fs2.copyFile(from, to);
916
+ await fs3.copyFile(from, to);
744
917
  } catch {
745
918
  return;
746
919
  }
@@ -758,21 +931,21 @@ async function runBunInstallDryRun(packagePath) {
758
931
  skipReason
759
932
  };
760
933
  }
761
- const base = await fs2.mkdtemp(path4.join(os.tmpdir(), "bun-ready-"));
934
+ const base = await fs3.mkdtemp(path5.join(os.tmpdir(), "bun-ready-"));
762
935
  const cleanup = async () => {
763
936
  try {
764
- await fs2.rm(base, { recursive: true, force: true });
937
+ await fs3.rm(base, { recursive: true, force: true });
765
938
  } catch {
766
939
  return;
767
940
  }
768
941
  };
769
942
  try {
770
- await copyIfExists(path4.join(packagePath, "package.json"), path4.join(base, "package.json"));
771
- await copyIfExists(path4.join(packagePath, "bun.lock"), path4.join(base, "bun.lock"));
772
- await copyIfExists(path4.join(packagePath, "bun.lockb"), path4.join(base, "bun.lockb"));
773
- await copyIfExists(path4.join(packagePath, "package-lock.json"), path4.join(base, "package-lock.json"));
774
- await copyIfExists(path4.join(packagePath, "yarn.lock"), path4.join(base, "yarn.lock"));
775
- 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"));
776
949
  const res = await exec("bun", ["install", "--dry-run"], base);
777
950
  const combined = [...res.stdout ? res.stdout.split(`
778
951
  `) : [], ...res.stderr ? res.stderr.split(`
@@ -818,7 +991,7 @@ function filterFindings(findings, config) {
818
991
  }
819
992
  async function analyzeSinglePackage(packagePath, opts, config, pkgName) {
820
993
  const info = await readRepoInfo(packagePath);
821
- const name = pkgName || info.pkg.name || path4.basename(packagePath);
994
+ const name = pkgName || info.pkg.name || path5.basename(packagePath);
822
995
  let findings = [
823
996
  ...detectLockfileSignals({ packageJsonPath: packagePath, lockfiles: info.lockfiles, scripts: info.scripts, dependencies: info.dependencies, devDependencies: info.devDependencies, optionalDependencies: info.optionalDependencies, hasWorkspaces: false, packageJson: info.pkg }),
824
997
  ...detectScriptRisks({ packageJsonPath: packagePath, lockfiles: info.lockfiles, scripts: info.scripts, dependencies: info.dependencies, devDependencies: info.devDependencies, optionalDependencies: info.optionalDependencies, hasWorkspaces: false, packageJson: info.pkg }),
@@ -877,13 +1050,22 @@ async function analyzeSinglePackage(packagePath, opts, config, pkgName) {
877
1050
  testOk = testResult.ok;
878
1051
  }
879
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
+ }
880
1062
  const summaryLines = [];
881
1063
  summaryLines.push(`Lockfiles: ${info.lockfiles.bunLock || info.lockfiles.bunLockb ? "bun" : "non-bun or missing"}`);
882
1064
  summaryLines.push(`Lifecycle scripts: ${Object.keys(info.scripts).some((k) => ["postinstall", "prepare", "preinstall", "install"].includes(k)) ? "present" : "none"}`);
883
1065
  summaryLines.push(`Native addon risk: ${findings.some((f) => f.id === "deps.native_addons") ? "yes" : "no"}`);
884
1066
  summaryLines.push(`bun install dry-run: ${install ? install.ok ? "ok" : "failed" : "skipped"}`);
885
1067
  summaryLines.push(`bun test: ${test ? test.ok ? "ok" : "failed" : "skipped"}`);
886
- return {
1068
+ const result = {
887
1069
  name,
888
1070
  path: packagePath,
889
1071
  severity,
@@ -895,8 +1077,14 @@ async function analyzeSinglePackage(packagePath, opts, config, pkgName) {
895
1077
  dependencies: info.dependencies,
896
1078
  devDependencies: info.devDependencies,
897
1079
  optionalDependencies: info.optionalDependencies,
898
- lockfiles: info.lockfiles
1080
+ lockfiles: info.lockfiles,
1081
+ stats,
1082
+ findingsSummary
899
1083
  };
1084
+ if (packageUsage !== undefined) {
1085
+ result.packageUsage = packageUsage;
1086
+ }
1087
+ return result;
900
1088
  }
901
1089
  function aggregateSeverity(packages, overallSeverity) {
902
1090
  if (overallSeverity === "red")
@@ -915,7 +1103,7 @@ function aggregateSeverity(packages, overallSeverity) {
915
1103
  }
916
1104
  async function analyzeRepoOverall(opts) {
917
1105
  const repoPath = normalizeRepoPath(opts.repoPath);
918
- const packageJsonPath = path4.join(repoPath, "package.json");
1106
+ const packageJsonPath = path5.join(repoPath, "package.json");
919
1107
  const hasPkg = await fileExists(packageJsonPath);
920
1108
  const config = await readConfig(repoPath);
921
1109
  if (!hasPkg) {
@@ -1029,6 +1217,27 @@ var badge = (s) => {
1029
1217
  return "\uD83D\uDFE1 YELLOW";
1030
1218
  return "\uD83D\uDD34 RED";
1031
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
+ };
1032
1241
  var getTopFindings = (pkg, count = 3) => {
1033
1242
  const sorted = [...pkg.findings].sort((a, b) => {
1034
1243
  const severityOrder = { red: 0, yellow: 1, green: 2 };
@@ -1041,14 +1250,44 @@ var getTopFindings = (pkg, count = 3) => {
1041
1250
  };
1042
1251
  var packageRow = (pkg) => {
1043
1252
  const name = pkg.name;
1044
- const path5 = pkg.path.replace(/\\/g, "/");
1253
+ const path6 = pkg.path.replace(/\\/g, "/");
1045
1254
  const severity = badge(pkg.severity);
1046
1255
  const topFindings = getTopFindings(pkg, 2).join(", ") || "No issues";
1047
- 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;
1048
1274
  };
1049
1275
  function renderMarkdown(r) {
1050
1276
  const lines = [];
1051
- 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));
1052
1291
  lines.push(``);
1053
1292
  lines.push(`**Overall:** ${badge(r.severity)}`);
1054
1293
  lines.push(``);
@@ -1127,6 +1366,13 @@ function renderMarkdown(r) {
1127
1366
  }
1128
1367
  lines.push(``);
1129
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
+ }
1130
1376
  lines.push(`## Root Findings`);
1131
1377
  if (r.findings.length === 0) {
1132
1378
  lines.push(`No findings for root package.`);
@@ -1157,6 +1403,12 @@ function renderMarkdown(r) {
1157
1403
  for (const l of pkg.summaryLines)
1158
1404
  lines.push(`- ${l}`);
1159
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
+ }
1160
1412
  if (pkg.install) {
1161
1413
  lines.push(`**bun install (dry-run):** ${pkg.install.ok ? "ok" : "failed"}`);
1162
1414
  if (pkg.install.logs.length > 0 && pkg.install.logs.length < 10) {
@@ -1202,6 +1454,90 @@ function renderMarkdown(r) {
1202
1454
  return lines.join(`
1203
1455
  `);
1204
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
+ };
1205
1541
 
1206
1542
  // src/report_json.ts
1207
1543
  function renderJson(r) {
@@ -1214,7 +1550,7 @@ var usage = () => {
1214
1550
  "bun-ready",
1215
1551
  "",
1216
1552
  "Usage:",
1217
- " 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]",
1218
1554
  "",
1219
1555
  "Options:",
1220
1556
  " --format md|json Output format (default: md)",
@@ -1222,6 +1558,7 @@ var usage = () => {
1222
1558
  " --no-install Skip bun install --dry-run",
1223
1559
  " --no-test Skip bun test",
1224
1560
  " --verbose Show detailed output",
1561
+ " --detailed Show detailed package usage report with file paths",
1225
1562
  " --scope root|packages|all Scan scope for monorepos (default: all)",
1226
1563
  " --fail-on green|yellow|red Fail policy (default: red)",
1227
1564
  "",
@@ -1246,6 +1583,7 @@ var parseArgs = (argv) => {
1246
1583
  runInstall: true,
1247
1584
  runTest: true,
1248
1585
  verbose: false,
1586
+ detailed: false,
1249
1587
  scope: "all"
1250
1588
  }
1251
1589
  };
@@ -1256,6 +1594,7 @@ var parseArgs = (argv) => {
1256
1594
  let runInstall = true;
1257
1595
  let runTest = true;
1258
1596
  let verbose = false;
1597
+ let detailed = false;
1259
1598
  let scope = "all";
1260
1599
  let failOn;
1261
1600
  for (let i = 2;i < args.length; i++) {
@@ -1284,6 +1623,10 @@ var parseArgs = (argv) => {
1284
1623
  verbose = true;
1285
1624
  continue;
1286
1625
  }
1626
+ if (a === "--detailed") {
1627
+ detailed = true;
1628
+ continue;
1629
+ }
1287
1630
  if (a === "--scope") {
1288
1631
  const v = args[i + 1] ?? "";
1289
1632
  if (v === "root" || v === "packages" || v === "all")
@@ -1306,6 +1649,7 @@ var parseArgs = (argv) => {
1306
1649
  runInstall,
1307
1650
  runTest,
1308
1651
  verbose,
1652
+ detailed,
1309
1653
  scope
1310
1654
  };
1311
1655
  if (failOn !== undefined) {
@@ -1347,7 +1691,14 @@ var main = async () => {
1347
1691
  `);
1348
1692
  process.exit(1);
1349
1693
  }
1350
- 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);
1351
1702
  const scanOpts = {
1352
1703
  repoPath: opts.repoPath,
1353
1704
  format: opts.format,
@@ -1376,10 +1727,10 @@ ${skipWarnings.map((w) => ` - ${w}`).join(`
1376
1727
  `);
1377
1728
  }
1378
1729
  }
1379
- const out = opts.format === "json" ? renderJson(res) : renderMarkdown(res);
1380
- const target = opts.outFile ?? (opts.format === "json" ? "bun-ready.json" : "bun-ready.md");
1381
- const resolved = path5.resolve(process.cwd(), target);
1382
- await fs3.writeFile(resolved, out, "utf8");
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");
1383
1734
  process.stdout.write(`Wrote ${opts.format.toUpperCase()} report to ${resolved}
1384
1735
  `);
1385
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.3",
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",