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.
- package/README.md +65 -5
- package/dist/cli.js +397 -46
- 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
|
|
91
|
-
import
|
|
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
|
|
95
|
+
import path5 from "node:path";
|
|
96
96
|
import os from "node:os";
|
|
97
|
-
import { promises as
|
|
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
|
|
713
|
+
import path3 from "node:path";
|
|
547
714
|
import fsSync from "node:fs";
|
|
548
|
-
function globMatch(pattern,
|
|
715
|
+
function globMatch(pattern, path4) {
|
|
549
716
|
const patternParts = pattern.split("/");
|
|
550
|
-
const pathParts =
|
|
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 =
|
|
755
|
+
const patternPath = path3.resolve(rootPath, pattern);
|
|
589
756
|
if (pattern.includes("/*") && !pattern.includes("/**")) {
|
|
590
757
|
try {
|
|
591
|
-
const baseDir =
|
|
592
|
-
const patternName =
|
|
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 =
|
|
597
|
-
const pkgJsonPath =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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(
|
|
734
|
-
bunLockb: await fileExists(
|
|
735
|
-
npmLock: await fileExists(
|
|
736
|
-
yarnLock: await fileExists(
|
|
737
|
-
pnpmLock: await fileExists(
|
|
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
|
|
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
|
|
934
|
+
const base = await fs3.mkdtemp(path5.join(os.tmpdir(), "bun-ready-"));
|
|
762
935
|
const cleanup = async () => {
|
|
763
936
|
try {
|
|
764
|
-
await
|
|
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(
|
|
771
|
-
await copyIfExists(
|
|
772
|
-
await copyIfExists(
|
|
773
|
-
await copyIfExists(
|
|
774
|
-
await copyIfExists(
|
|
775
|
-
await copyIfExists(
|
|
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 ||
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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} | \`${
|
|
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
|
-
|
|
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
|
|
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 =
|
|
1382
|
-
await
|
|
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
|
+
"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",
|