bun-ready 0.1.0 → 0.2.0
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 +139 -0
- package/dist/cli.js +921 -135
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -39,6 +39,145 @@ bun-ready scan . --no-install --no-test
|
|
|
39
39
|
- 2 → YELLOW
|
|
40
40
|
- 3 → RED
|
|
41
41
|
|
|
42
|
+
## Monorepo / Workspaces Support
|
|
43
|
+
|
|
44
|
+
bun-ready now supports scanning monorepo projects with multiple workspace packages.
|
|
45
|
+
|
|
46
|
+
### Workspace Discovery
|
|
47
|
+
|
|
48
|
+
If your `package.json` contains a `workspaces` field (array or object), bun-ready will:
|
|
49
|
+
- Discover all workspace packages automatically
|
|
50
|
+
- Analyze each package individually
|
|
51
|
+
- Aggregate results into a single report
|
|
52
|
+
|
|
53
|
+
### Configuration
|
|
54
|
+
|
|
55
|
+
You can create a `bun-ready.config.json` file in your repository root to customize the scan:
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"ignorePackages": ["packages/legacy"],
|
|
60
|
+
"ignoreFindings": ["scripts.pm_assumptions"],
|
|
61
|
+
"nativeAddonAllowlist": ["fsevents"],
|
|
62
|
+
"failOn": "yellow"
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Options
|
|
67
|
+
|
|
68
|
+
| Option | Description | Default |
|
|
69
|
+
|---------|-------------|----------|
|
|
70
|
+
| `ignorePackages` | Array of package paths to ignore | `[]` |
|
|
71
|
+
| `ignoreFindings` | Array of finding IDs to ignore | `[]` |
|
|
72
|
+
| `nativeAddonAllowlist` | Packages to exclude from native addon checks | `[]` |
|
|
73
|
+
| `failOn` | When to return non-zero exit code | `"red"` |
|
|
74
|
+
|
|
75
|
+
### New CLI Flags
|
|
76
|
+
|
|
77
|
+
`--scope root|packages|all`
|
|
78
|
+
- `root`: Scan only the root package.json
|
|
79
|
+
- `packages`: Scan only workspace packages
|
|
80
|
+
- `all`: Scan root and all workspace packages (default)
|
|
81
|
+
|
|
82
|
+
`--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
|
|
87
|
+
|
|
88
|
+
## How Scoring Works
|
|
89
|
+
|
|
90
|
+
bun-ready uses a combination of heuristics to determine migration readiness:
|
|
91
|
+
|
|
92
|
+
### Severity Levels
|
|
93
|
+
|
|
94
|
+
**🟢 GREEN** - Ready to migrate
|
|
95
|
+
- No critical issues detected
|
|
96
|
+
- Bun install succeeds
|
|
97
|
+
- Tests pass (if compatible)
|
|
98
|
+
- No native addon risks or properly handled
|
|
99
|
+
|
|
100
|
+
**🟡 YELLOW** - Migration possible with manual fixes
|
|
101
|
+
- Lifecycle scripts present (verify npm compatibility)
|
|
102
|
+
- Native addons detected (may need updates)
|
|
103
|
+
- Package manager assumptions in scripts
|
|
104
|
+
- Node version < 18
|
|
105
|
+
- Dev tools that may need configuration
|
|
106
|
+
|
|
107
|
+
**🔴 RED** - Migration blocked
|
|
108
|
+
- Bun install fails
|
|
109
|
+
- Native build tools (node-gyp, node-sass)
|
|
110
|
+
- Critical missing dependencies
|
|
111
|
+
- Tests fail
|
|
112
|
+
|
|
113
|
+
### Findings Categories
|
|
114
|
+
|
|
115
|
+
- `scripts.lifecycle` - Lifecycle scripts in root or dependencies
|
|
116
|
+
- `scripts.npm_specific` - npm/yarn/pnpm-specific commands
|
|
117
|
+
- `scripts.pm_assumptions` - Package manager assumptions
|
|
118
|
+
- `deps.native_addons` - Native addon dependencies
|
|
119
|
+
- `runtime.node_version` - Node.js version requirements
|
|
120
|
+
- `runtime.dev_tools` - Testing frameworks (jest, vitest, etc.)
|
|
121
|
+
- `runtime.build_tools` - Build tools (webpack, babel, etc.)
|
|
122
|
+
- `runtime.ts_execution` - TypeScript runtime execution
|
|
123
|
+
- `lockfile.missing` - No lockfile detected
|
|
124
|
+
- `lockfile.migration` - Non-Bun lockfile present
|
|
125
|
+
- `install.blocked_scripts` - Scripts blocked by Bun
|
|
126
|
+
- `install.trusted_deps` - Trusted dependencies mentioned
|
|
127
|
+
|
|
128
|
+
## FAQ
|
|
129
|
+
|
|
130
|
+
### Why yellow when there's a postinstall script?
|
|
131
|
+
|
|
132
|
+
Bun runs your project's lifecycle scripts during install, but **does not run** lifecycle scripts of dependencies unless they're in `trustedDependencies`. The yellow warning reminds you to verify these scripts work correctly with Bun.
|
|
133
|
+
|
|
134
|
+
### What are trustedDependencies?
|
|
135
|
+
|
|
136
|
+
Bun's `trustedDependencies` configuration controls which packages are allowed to run their lifecycle scripts. You can add trusted packages to this field in your `package.json`:
|
|
137
|
+
|
|
138
|
+
```json
|
|
139
|
+
{
|
|
140
|
+
"trustedDependencies": ["some-package"]
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### How do I handle monorepo scanning?
|
|
145
|
+
|
|
146
|
+
For monorepos, bun-ready automatically detects workspaces and scans all packages. Use `--scope` to control what's scanned:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
# Scan everything (default)
|
|
150
|
+
bun-ready scan .
|
|
151
|
+
|
|
152
|
+
# Scan only root package
|
|
153
|
+
bun-ready scan . --scope root
|
|
154
|
+
|
|
155
|
+
# Scan only workspace packages
|
|
156
|
+
bun-ready scan . --scope packages
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Can I ignore certain findings?
|
|
160
|
+
|
|
161
|
+
Yes! Create a `bun-ready.config.json` file:
|
|
162
|
+
|
|
163
|
+
```json
|
|
164
|
+
{
|
|
165
|
+
"ignoreFindings": ["scripts.pm_assumptions"]
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### What if a package is in the native addon list but works with Bun?
|
|
170
|
+
|
|
171
|
+
Add it to the allowlist:
|
|
172
|
+
|
|
173
|
+
```json
|
|
174
|
+
{
|
|
175
|
+
"nativeAddonAllowlist": ["fsevents"]
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Some packages have optional native modules that can be disabled or work fine with Bun.
|
|
180
|
+
|
|
42
181
|
## What it checks (MVP)
|
|
43
182
|
- package.json presence & shape
|
|
44
183
|
- lockfiles (npm/yarn/pnpm/bun)
|
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
1
|
// src/cli.ts
|
|
4
2
|
import { promises as fs3 } from "node:fs";
|
|
5
|
-
import
|
|
3
|
+
import path5 from "node:path";
|
|
6
4
|
|
|
7
5
|
// src/analyze.ts
|
|
8
|
-
import
|
|
6
|
+
import path4 from "node:path";
|
|
9
7
|
import os from "node:os";
|
|
10
8
|
import { promises as fs2 } from "node:fs";
|
|
11
9
|
|
|
@@ -81,7 +79,7 @@ var truncateLines = (lines, max) => {
|
|
|
81
79
|
};
|
|
82
80
|
|
|
83
81
|
// src/heuristics.ts
|
|
84
|
-
var
|
|
82
|
+
var NATIVE_SUSPECTS_V2 = [
|
|
85
83
|
"node-gyp",
|
|
86
84
|
"node-pre-gyp",
|
|
87
85
|
"prebuild-install",
|
|
@@ -97,7 +95,112 @@ var NATIVE_SUSPECTS = [
|
|
|
97
95
|
"argon2",
|
|
98
96
|
"bufferutil",
|
|
99
97
|
"utf-8-validate",
|
|
100
|
-
"fsevents"
|
|
98
|
+
"fsevents",
|
|
99
|
+
"grpc",
|
|
100
|
+
"@grpc/grpc-js",
|
|
101
|
+
"grpc-js",
|
|
102
|
+
"bcryptjs",
|
|
103
|
+
"bcrypt",
|
|
104
|
+
"sodium",
|
|
105
|
+
"libsodium",
|
|
106
|
+
"leveldb",
|
|
107
|
+
"level",
|
|
108
|
+
"rocksdb",
|
|
109
|
+
"mysql2",
|
|
110
|
+
"pg",
|
|
111
|
+
"oracledb",
|
|
112
|
+
"nodegit",
|
|
113
|
+
"ffi-napi",
|
|
114
|
+
"node-ffi",
|
|
115
|
+
"ref-napi",
|
|
116
|
+
"skia-canvas",
|
|
117
|
+
"jimp",
|
|
118
|
+
"pdfkit",
|
|
119
|
+
"sharp",
|
|
120
|
+
"pixelmatch",
|
|
121
|
+
"cheerio",
|
|
122
|
+
"node-wav",
|
|
123
|
+
"lamejs",
|
|
124
|
+
"flac-bindings",
|
|
125
|
+
"opus-recorder",
|
|
126
|
+
"silk-wasm",
|
|
127
|
+
"zeromq",
|
|
128
|
+
"zeromq.js",
|
|
129
|
+
"mongodb",
|
|
130
|
+
"redis",
|
|
131
|
+
"ioredis",
|
|
132
|
+
"elasticsearch",
|
|
133
|
+
"snappy",
|
|
134
|
+
"snappyjs",
|
|
135
|
+
"iltorb",
|
|
136
|
+
"brotli",
|
|
137
|
+
"node-sha3",
|
|
138
|
+
"ursa",
|
|
139
|
+
"node-forge",
|
|
140
|
+
"jsonwebtoken",
|
|
141
|
+
"node-cron",
|
|
142
|
+
"bull",
|
|
143
|
+
"bullmq"
|
|
144
|
+
];
|
|
145
|
+
var DEV_TOOL_SUSPECTS = [
|
|
146
|
+
"jest",
|
|
147
|
+
"vitest",
|
|
148
|
+
"mocha",
|
|
149
|
+
"chai",
|
|
150
|
+
"ava",
|
|
151
|
+
"tap",
|
|
152
|
+
"jasmine",
|
|
153
|
+
"karma",
|
|
154
|
+
"cypress",
|
|
155
|
+
"playwright",
|
|
156
|
+
"puppeteer",
|
|
157
|
+
"selenium-webdriver",
|
|
158
|
+
"webdriverio",
|
|
159
|
+
"nightwatch",
|
|
160
|
+
"testcafe",
|
|
161
|
+
"protractor"
|
|
162
|
+
];
|
|
163
|
+
var RUNTIME_TOOL_SUSPECTS = [
|
|
164
|
+
"ts-node",
|
|
165
|
+
"tsx",
|
|
166
|
+
"ts-node-dev",
|
|
167
|
+
"nodemon",
|
|
168
|
+
"babel",
|
|
169
|
+
"@babel/core",
|
|
170
|
+
"@babel/node",
|
|
171
|
+
"babel-cli",
|
|
172
|
+
"babel-register",
|
|
173
|
+
"babel-preset-env",
|
|
174
|
+
"webpack",
|
|
175
|
+
"webpack-cli",
|
|
176
|
+
"rollup",
|
|
177
|
+
"@rollup/plugin",
|
|
178
|
+
"esbuild",
|
|
179
|
+
"vite",
|
|
180
|
+
"@vitejs/plugin",
|
|
181
|
+
"swc",
|
|
182
|
+
"@swc/core",
|
|
183
|
+
"@swc/register",
|
|
184
|
+
"turbopack",
|
|
185
|
+
"snowpack",
|
|
186
|
+
"parcel",
|
|
187
|
+
"browserify"
|
|
188
|
+
];
|
|
189
|
+
var PM_SPECIFIC_COMMANDS = [
|
|
190
|
+
"npm ci",
|
|
191
|
+
"npm run ci",
|
|
192
|
+
"pnpm -r",
|
|
193
|
+
"pnpm recursive",
|
|
194
|
+
"pnpm workspaces",
|
|
195
|
+
"yarn workspaces",
|
|
196
|
+
"yarn workspace",
|
|
197
|
+
"yarn -w",
|
|
198
|
+
"lerna run",
|
|
199
|
+
"nx run",
|
|
200
|
+
"npx lerna",
|
|
201
|
+
"npx nx",
|
|
202
|
+
"turbo run",
|
|
203
|
+
"rushx"
|
|
101
204
|
];
|
|
102
205
|
var includesAny = (s, needles) => {
|
|
103
206
|
const lower = s.toLowerCase();
|
|
@@ -144,31 +247,6 @@ var detectScriptRisks = (repo) => {
|
|
|
144
247
|
}
|
|
145
248
|
return findings;
|
|
146
249
|
};
|
|
147
|
-
var detectNativeAddonRisk = (repo) => {
|
|
148
|
-
const allDeps = {
|
|
149
|
-
...repo.dependencies,
|
|
150
|
-
...repo.devDependencies,
|
|
151
|
-
...repo.optionalDependencies
|
|
152
|
-
};
|
|
153
|
-
const names = Object.keys(allDeps);
|
|
154
|
-
const suspects = stableSort(names.filter((n) => NATIVE_SUSPECTS.includes(n) || includesAny(n, ["napi", "node-gyp", "prebuild", "ffi"])), (x) => x);
|
|
155
|
-
if (suspects.length === 0)
|
|
156
|
-
return [];
|
|
157
|
-
const hardRed = suspects.some((n) => n === "node-gyp" || n === "node-sass");
|
|
158
|
-
const severity = hardRed ? "red" : "yellow";
|
|
159
|
-
return [
|
|
160
|
-
{
|
|
161
|
-
id: "deps.native_addons",
|
|
162
|
-
title: "Potential native addons / node-gyp toolchain risk",
|
|
163
|
-
severity,
|
|
164
|
-
details: suspects.map((n) => `${n}@${allDeps[n] ?? ""}`.trim()),
|
|
165
|
-
hints: [
|
|
166
|
-
"Native addons often require toolchains and can be sensitive to runtime differences.",
|
|
167
|
-
"If you see install/build failures, try upgrading these packages or switching to pure-JS alternatives."
|
|
168
|
-
]
|
|
169
|
-
}
|
|
170
|
-
];
|
|
171
|
-
};
|
|
172
250
|
var detectLockfileSignals = (repo) => {
|
|
173
251
|
const { bunLock, bunLockb, npmLock, yarnLock, pnpmLock } = repo.lockfiles;
|
|
174
252
|
if (bunLock || bunLockb)
|
|
@@ -208,6 +286,145 @@ var detectLockfileSignals = (repo) => {
|
|
|
208
286
|
}
|
|
209
287
|
];
|
|
210
288
|
};
|
|
289
|
+
var detectRuntimeApiRisks = (repo) => {
|
|
290
|
+
const findings = [];
|
|
291
|
+
if (repo.packageJson?.engines?.node) {
|
|
292
|
+
const nodeVersion = repo.packageJson.engines.node;
|
|
293
|
+
const match = nodeVersion.match(/>=?(\d+)/);
|
|
294
|
+
if (match && match[1]) {
|
|
295
|
+
const minVersion = parseInt(match[1], 10);
|
|
296
|
+
if (minVersion < 18) {
|
|
297
|
+
findings.push({
|
|
298
|
+
id: "runtime.node_version",
|
|
299
|
+
title: "Node.js version requirement is below Bun's baseline (v18+)",
|
|
300
|
+
severity: "yellow",
|
|
301
|
+
details: [`engines.node: ${nodeVersion}`],
|
|
302
|
+
hints: [
|
|
303
|
+
"Bun targets Node 18+ compatibility. Packages requiring older Node versions may need updates.",
|
|
304
|
+
"Check if packages have updates or if version constraints can be relaxed."
|
|
305
|
+
]
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
const allDeps = { ...repo.dependencies, ...repo.devDependencies, ...repo.optionalDependencies };
|
|
311
|
+
const deps = Object.keys(allDeps);
|
|
312
|
+
const devToolHits = deps.filter((d) => DEV_TOOL_SUSPECTS.includes(d) || deps.some((x) => x.startsWith(`${d}/`)));
|
|
313
|
+
const relevantDevTools = devToolHits.filter((d) => allDeps[d]);
|
|
314
|
+
if (relevantDevTools.length > 0) {
|
|
315
|
+
findings.push({
|
|
316
|
+
id: "runtime.dev_tools",
|
|
317
|
+
title: "Dev tools that may need Bun compatibility checks",
|
|
318
|
+
severity: "yellow",
|
|
319
|
+
details: relevantDevTools.map((d) => `${d}@${allDeps[d]}`),
|
|
320
|
+
hints: [
|
|
321
|
+
"Testing frameworks like jest/vitest may work, but consider migrating to bun:test for optimal performance.",
|
|
322
|
+
"Build tools like webpack/esbuild/vite typically work with Bun, but verify your build pipeline.",
|
|
323
|
+
"Check documentation for each tool's Bun compatibility status."
|
|
324
|
+
]
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
const runtimeToolHits = deps.filter((d) => RUNTIME_TOOL_SUSPECTS.includes(d) || deps.some((x) => x.startsWith(`${d}/`)));
|
|
328
|
+
const relevantRuntimeTools = runtimeToolHits.filter((d) => allDeps[d]);
|
|
329
|
+
if (relevantRuntimeTools.length > 0) {
|
|
330
|
+
findings.push({
|
|
331
|
+
id: "runtime.build_tools",
|
|
332
|
+
title: "Runtime/build tools that may need Bun compatibility verification",
|
|
333
|
+
severity: "yellow",
|
|
334
|
+
details: relevantRuntimeTools.map((d) => `${d}@${allDeps[d]}`),
|
|
335
|
+
hints: [
|
|
336
|
+
"Tools like ts-node/tsx work with Bun, but verify your TypeScript configuration.",
|
|
337
|
+
"Bundlers like webpack/vite/esbuild typically work with Bun, but verify your build scripts.",
|
|
338
|
+
"Consider switching to Bun's native build capabilities for improved performance."
|
|
339
|
+
]
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
const scriptNames = Object.keys(repo.scripts);
|
|
343
|
+
const tsRuntimeScripts = scriptNames.filter((k) => {
|
|
344
|
+
const script = repo.scripts[k]?.toLowerCase() || "";
|
|
345
|
+
return script.includes("ts-node") || script.includes("tsx") || script.includes("babel-node");
|
|
346
|
+
});
|
|
347
|
+
if (tsRuntimeScripts.length > 0) {
|
|
348
|
+
findings.push({
|
|
349
|
+
id: "runtime.ts_execution",
|
|
350
|
+
title: "Scripts use TypeScript runtime execution (ts-node/tsx/babel-node)",
|
|
351
|
+
severity: "yellow",
|
|
352
|
+
details: tsRuntimeScripts.map((k) => `${k}: ${repo.scripts[k]}`),
|
|
353
|
+
hints: [
|
|
354
|
+
"Bun supports TypeScript natively, but verify tsconfig compatibility.",
|
|
355
|
+
"Consider migrating scripts to use `bun run` directly with TypeScript files.",
|
|
356
|
+
"Test execution of affected scripts with Bun before full migration."
|
|
357
|
+
]
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
return findings;
|
|
361
|
+
};
|
|
362
|
+
var detectPmAssumptions = (repo) => {
|
|
363
|
+
const findings = [];
|
|
364
|
+
const scriptNames = Object.keys(repo.scripts);
|
|
365
|
+
const pmSpecificHits = [];
|
|
366
|
+
for (const scriptName of scriptNames) {
|
|
367
|
+
const script = repo.scripts[scriptName] || "";
|
|
368
|
+
const lowerScript = script.toLowerCase();
|
|
369
|
+
for (const cmd of PM_SPECIFIC_COMMANDS) {
|
|
370
|
+
if (lowerScript.includes(cmd.toLowerCase())) {
|
|
371
|
+
pmSpecificHits.push({ name: scriptName, command: cmd });
|
|
372
|
+
break;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
if (pmSpecificHits.length > 0) {
|
|
377
|
+
findings.push({
|
|
378
|
+
id: "scripts.pm_assumptions",
|
|
379
|
+
title: "Scripts contain package-manager-specific commands",
|
|
380
|
+
severity: "yellow",
|
|
381
|
+
details: pmSpecificHits.map((h) => `${h.name}: uses "${h.command}"`),
|
|
382
|
+
hints: [
|
|
383
|
+
"Package-manager-specific commands may need adaptation for Bun.",
|
|
384
|
+
"Test each affected script with Bun to ensure compatibility.",
|
|
385
|
+
"Consider rewriting scripts to be package-manager agnostic where possible."
|
|
386
|
+
]
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
return findings;
|
|
390
|
+
};
|
|
391
|
+
var detectNativeAddonRiskV2 = (repo, config) => {
|
|
392
|
+
const allDeps = {
|
|
393
|
+
...repo.dependencies,
|
|
394
|
+
...repo.devDependencies,
|
|
395
|
+
...repo.optionalDependencies
|
|
396
|
+
};
|
|
397
|
+
const names = Object.keys(allDeps);
|
|
398
|
+
const allowlist = config?.nativeAddonAllowlist || [];
|
|
399
|
+
const suspects = names.filter((n) => {
|
|
400
|
+
if (allowlist.includes(n))
|
|
401
|
+
return false;
|
|
402
|
+
return NATIVE_SUSPECTS_V2.includes(n) || includesAny(n, ["napi", "node-gyp", "prebuild", "ffi", "bindings", "native", "native-module"]);
|
|
403
|
+
});
|
|
404
|
+
const scriptNames = Object.keys(repo.scripts);
|
|
405
|
+
const hasNodeGypRebuild = scriptNames.some((k) => {
|
|
406
|
+
const script = repo.scripts[k]?.toLowerCase() || "";
|
|
407
|
+
return script.includes("node-gyp") || script.includes("node-gyp rebuild");
|
|
408
|
+
});
|
|
409
|
+
if (suspects.length === 0 && !hasNodeGypRebuild)
|
|
410
|
+
return [];
|
|
411
|
+
const hardRed = suspects.some((n) => n === "node-gyp" || n === "node-sass") || hasNodeGypRebuild;
|
|
412
|
+
const severity = hardRed ? "red" : "yellow";
|
|
413
|
+
return [
|
|
414
|
+
{
|
|
415
|
+
id: "deps.native_addons",
|
|
416
|
+
title: "Potential native addons / node-gyp toolchain risk",
|
|
417
|
+
severity,
|
|
418
|
+
details: suspects.map((n) => `${n}@${allDeps[n]}`),
|
|
419
|
+
hints: [
|
|
420
|
+
"Native addons often require toolchains and can be sensitive to runtime differences.",
|
|
421
|
+
"If you see install/build failures, try upgrading these packages or switching to pure-JS alternatives.",
|
|
422
|
+
"Some packages offer optional native modules that can be disabled via configuration.",
|
|
423
|
+
"Check if native modules are in use or just installed for optional features."
|
|
424
|
+
]
|
|
425
|
+
}
|
|
426
|
+
];
|
|
427
|
+
};
|
|
211
428
|
var summarizeSeverity = (findings, installOk, testOk) => {
|
|
212
429
|
let sev = "green";
|
|
213
430
|
for (const f of findings)
|
|
@@ -219,33 +436,264 @@ var summarizeSeverity = (findings, installOk, testOk) => {
|
|
|
219
436
|
return sev;
|
|
220
437
|
};
|
|
221
438
|
|
|
439
|
+
// src/bun_logs.ts
|
|
440
|
+
function parseInstallLogs(logs) {
|
|
441
|
+
const blockedDeps = [];
|
|
442
|
+
const trustedDepsMentioned = [];
|
|
443
|
+
const notes = [];
|
|
444
|
+
const blockedKeywords = ["blocked", "not allowed", "lifecycle script", "postinstall blocked", "prepare blocked"];
|
|
445
|
+
const trustedKeywords = ["trustedDependencies", "trusted", "trust"];
|
|
446
|
+
for (const log of logs) {
|
|
447
|
+
const lowerLog = log.toLowerCase();
|
|
448
|
+
for (const keyword of blockedKeywords) {
|
|
449
|
+
if (lowerLog.includes(keyword)) {
|
|
450
|
+
let pkgMatch = log.match(/blocked:\s+(?:lifecycle\s+script\s+(?:for\s+)?)?(@?[a-z0-9-]+\/[a-z0-9-]+|@?[a-z0-9-]+)@[\d.^]+/i);
|
|
451
|
+
if (pkgMatch && pkgMatch[1] && !blockedDeps.includes(pkgMatch[1])) {
|
|
452
|
+
blockedDeps.push(pkgMatch[1]);
|
|
453
|
+
} else {
|
|
454
|
+
const fallbackMatch = log.match(/blocked:\s+(?:lifecycle\s+script\s+(?:for\s+)?)?(@?[a-z0-9-]+\/[a-z0-9-]+|@?[a-z0-9-]+)/i);
|
|
455
|
+
if (fallbackMatch && fallbackMatch[1] && !blockedDeps.includes(fallbackMatch[1]) && !blockedKeywords.includes(fallbackMatch[1].toLowerCase())) {
|
|
456
|
+
blockedDeps.push(fallbackMatch[1]);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
break;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
for (const keyword of trustedKeywords) {
|
|
463
|
+
if (lowerLog.includes(keyword)) {
|
|
464
|
+
if (!notes.some((n) => n.toLowerCase().includes(keyword))) {
|
|
465
|
+
notes.push(log.trim());
|
|
466
|
+
}
|
|
467
|
+
const pkgMatch = log.match(/@?[a-z0-9-]+\/[a-z0-9-]+|@?[a-z0-9-]+/gi);
|
|
468
|
+
if (pkgMatch) {
|
|
469
|
+
for (const pkg of pkgMatch) {
|
|
470
|
+
if (pkg.toLowerCase() !== "trusteddependencies" && !trustedDepsMentioned.includes(pkg)) {
|
|
471
|
+
trustedDepsMentioned.push(pkg);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
break;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
if (lowerLog.includes("warning") || lowerLog.includes("warn")) {
|
|
479
|
+
notes.push(log.trim());
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
blockedDeps.sort((a, b) => a.localeCompare(b));
|
|
483
|
+
trustedDepsMentioned.sort((a, b) => a.localeCompare(b));
|
|
484
|
+
notes.sort();
|
|
485
|
+
return {
|
|
486
|
+
blockedDeps,
|
|
487
|
+
trustedDepsMentioned,
|
|
488
|
+
notes
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// src/workspaces.ts
|
|
493
|
+
import path2 from "node:path";
|
|
494
|
+
import fsSync from "node:fs";
|
|
495
|
+
function globMatch(pattern, path3) {
|
|
496
|
+
const patternParts = pattern.split("/");
|
|
497
|
+
const pathParts = path3.split("/");
|
|
498
|
+
let patternIdx = 0;
|
|
499
|
+
let pathIdx = 0;
|
|
500
|
+
while (patternIdx < patternParts.length && pathIdx < pathParts.length) {
|
|
501
|
+
const patternPart = patternParts[patternIdx];
|
|
502
|
+
const pathPart = pathParts[pathIdx];
|
|
503
|
+
if (patternPart === "**") {
|
|
504
|
+
if (patternIdx === patternParts.length - 1) {
|
|
505
|
+
return true;
|
|
506
|
+
}
|
|
507
|
+
const nextPatternPart = patternParts[patternIdx + 1];
|
|
508
|
+
while (pathIdx < pathParts.length && pathParts[pathIdx] !== nextPatternPart) {
|
|
509
|
+
pathIdx++;
|
|
510
|
+
}
|
|
511
|
+
patternIdx++;
|
|
512
|
+
} else if (patternPart === "*") {
|
|
513
|
+
patternIdx++;
|
|
514
|
+
pathIdx++;
|
|
515
|
+
} else {
|
|
516
|
+
if (patternPart !== pathPart) {
|
|
517
|
+
return false;
|
|
518
|
+
}
|
|
519
|
+
patternIdx++;
|
|
520
|
+
pathIdx++;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
if (patternIdx < patternParts.length) {
|
|
524
|
+
const remaining = patternParts.slice(patternIdx);
|
|
525
|
+
if (remaining.length === 1 && remaining[0] === "**") {
|
|
526
|
+
return true;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return patternIdx === patternParts.length && pathIdx === pathParts.length;
|
|
530
|
+
}
|
|
531
|
+
function discoverFromWorkspaces(rootPath, workspaces) {
|
|
532
|
+
const patterns = Array.isArray(workspaces) ? workspaces : workspaces.packages;
|
|
533
|
+
const packages = [];
|
|
534
|
+
for (const pattern of patterns) {
|
|
535
|
+
const patternPath = path2.resolve(rootPath, pattern);
|
|
536
|
+
if (pattern.includes("/*") && !pattern.includes("/**")) {
|
|
537
|
+
try {
|
|
538
|
+
const baseDir = path2.dirname(patternPath);
|
|
539
|
+
const patternName = path2.basename(patternPath);
|
|
540
|
+
const entries = fsSync.readdirSync(baseDir, { withFileTypes: true });
|
|
541
|
+
for (const entry of entries) {
|
|
542
|
+
if (entry.isDirectory() && globMatch(patternName, entry.name)) {
|
|
543
|
+
const packagePath = path2.join(baseDir, entry.name);
|
|
544
|
+
const pkgJsonPath = path2.join(packagePath, "package.json");
|
|
545
|
+
if (fileExistsSync(pkgJsonPath)) {
|
|
546
|
+
packages.push(packagePath);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
} catch {}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
return packages;
|
|
554
|
+
}
|
|
555
|
+
function fileExistsSync(filePath) {
|
|
556
|
+
try {
|
|
557
|
+
fsSync.accessSync(filePath);
|
|
558
|
+
return true;
|
|
559
|
+
} catch {
|
|
560
|
+
return false;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
async function discoverWorkspaces(rootPath) {
|
|
564
|
+
const packages = [];
|
|
565
|
+
const rootPkgJson = path2.join(rootPath, "package.json");
|
|
566
|
+
if (!await fileExists(rootPkgJson)) {
|
|
567
|
+
return packages;
|
|
568
|
+
}
|
|
569
|
+
const rootPkg = await readJsonFile(rootPkgJson);
|
|
570
|
+
if (!rootPkg.workspaces && !rootPkg.packages) {
|
|
571
|
+
return packages;
|
|
572
|
+
}
|
|
573
|
+
const workspaces = rootPkg.workspaces || rootPkg.packages;
|
|
574
|
+
if (!workspaces) {
|
|
575
|
+
return packages;
|
|
576
|
+
}
|
|
577
|
+
const packagePaths = discoverFromWorkspaces(rootPath, workspaces);
|
|
578
|
+
for (const pkgPath of packagePaths) {
|
|
579
|
+
const pkgJsonPath = path2.join(pkgPath, "package.json");
|
|
580
|
+
try {
|
|
581
|
+
const pkg = await readJsonFile(pkgJsonPath);
|
|
582
|
+
if (pkg.name) {
|
|
583
|
+
packages.push({
|
|
584
|
+
name: pkg.name,
|
|
585
|
+
path: pkgPath,
|
|
586
|
+
packageJsonPath: pkgJsonPath
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
} catch {
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
packages.sort((a, b) => a.name.localeCompare(b.name));
|
|
594
|
+
return packages;
|
|
595
|
+
}
|
|
596
|
+
async function hasWorkspaces(rootPath) {
|
|
597
|
+
const rootPkgJson = path2.join(rootPath, "package.json");
|
|
598
|
+
if (!await fileExists(rootPkgJson)) {
|
|
599
|
+
return false;
|
|
600
|
+
}
|
|
601
|
+
const rootPkg = await readJsonFile(rootPkgJson);
|
|
602
|
+
return Boolean(rootPkg.workspaces || rootPkg.packages);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// src/config.ts
|
|
606
|
+
import path3 from "node:path";
|
|
607
|
+
var CONFIG_FILE_NAME = "bun-ready.config.json";
|
|
608
|
+
async function readConfig(rootPath) {
|
|
609
|
+
const configPath = path3.join(rootPath, CONFIG_FILE_NAME);
|
|
610
|
+
if (!await fileExists(configPath)) {
|
|
611
|
+
return null;
|
|
612
|
+
}
|
|
613
|
+
try {
|
|
614
|
+
const config = await readJsonFile(configPath);
|
|
615
|
+
return validateConfig(config);
|
|
616
|
+
} catch (error) {
|
|
617
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
618
|
+
process.stderr.write(`Warning: Invalid ${CONFIG_FILE_NAME}: ${msg}
|
|
619
|
+
`);
|
|
620
|
+
return null;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
function validateConfig(config) {
|
|
624
|
+
if (!config || typeof config !== "object") {
|
|
625
|
+
return null;
|
|
626
|
+
}
|
|
627
|
+
const cfg = config;
|
|
628
|
+
const result = {};
|
|
629
|
+
if (Array.isArray(cfg.ignorePackages)) {
|
|
630
|
+
const validIgnore = cfg.ignorePackages.filter((p) => typeof p === "string");
|
|
631
|
+
if (validIgnore.length > 0) {
|
|
632
|
+
result.ignorePackages = validIgnore;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
if (Array.isArray(cfg.ignoreFindings)) {
|
|
636
|
+
const validIgnore = cfg.ignoreFindings.filter((f) => typeof f === "string");
|
|
637
|
+
if (validIgnore.length > 0) {
|
|
638
|
+
result.ignoreFindings = validIgnore;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
if (Array.isArray(cfg.nativeAddonAllowlist)) {
|
|
642
|
+
const validAllowlist = cfg.nativeAddonAllowlist.filter((p) => typeof p === "string");
|
|
643
|
+
if (validAllowlist.length > 0) {
|
|
644
|
+
result.nativeAddonAllowlist = validAllowlist;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
if (typeof cfg.failOn === "string") {
|
|
648
|
+
const validFailOn = ["green", "yellow", "red"].includes(cfg.failOn);
|
|
649
|
+
if (validFailOn) {
|
|
650
|
+
result.failOn = cfg.failOn;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
if (Object.keys(result).length === 0) {
|
|
654
|
+
return null;
|
|
655
|
+
}
|
|
656
|
+
return result;
|
|
657
|
+
}
|
|
658
|
+
function mergeConfigWithOpts(config, opts) {
|
|
659
|
+
if (!config && !opts.failOn) {
|
|
660
|
+
return null;
|
|
661
|
+
}
|
|
662
|
+
const result = {
|
|
663
|
+
...config || {}
|
|
664
|
+
};
|
|
665
|
+
if (opts.failOn) {
|
|
666
|
+
result.failOn = opts.failOn;
|
|
667
|
+
}
|
|
668
|
+
return Object.keys(result).length > 0 ? result : null;
|
|
669
|
+
}
|
|
670
|
+
|
|
222
671
|
// src/analyze.ts
|
|
223
|
-
|
|
224
|
-
const packageJsonPath =
|
|
672
|
+
async function readRepoInfo(packagePath) {
|
|
673
|
+
const packageJsonPath = path4.join(packagePath, "package.json");
|
|
225
674
|
const pkg = await readJsonFile(packageJsonPath);
|
|
226
675
|
const scripts = pkg.scripts ?? {};
|
|
227
676
|
const dependencies = pkg.dependencies ?? {};
|
|
228
677
|
const devDependencies = pkg.devDependencies ?? {};
|
|
229
678
|
const optionalDependencies = pkg.optionalDependencies ?? {};
|
|
230
|
-
const hasWorkspaces = Boolean(pkg.workspaces);
|
|
231
679
|
const lockfiles = {
|
|
232
|
-
bunLock: await fileExists(
|
|
233
|
-
bunLockb: await fileExists(
|
|
234
|
-
npmLock: await fileExists(
|
|
235
|
-
yarnLock: await fileExists(
|
|
236
|
-
pnpmLock: await fileExists(
|
|
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"))
|
|
237
685
|
};
|
|
238
|
-
return {
|
|
239
|
-
}
|
|
240
|
-
|
|
686
|
+
return { pkg, scripts, dependencies, devDependencies, optionalDependencies, lockfiles };
|
|
687
|
+
}
|
|
688
|
+
async function copyIfExists(from, to) {
|
|
241
689
|
try {
|
|
242
690
|
await fs2.copyFile(from, to);
|
|
243
691
|
} catch {
|
|
244
692
|
return;
|
|
245
693
|
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
const base = await fs2.mkdtemp(
|
|
694
|
+
}
|
|
695
|
+
async function runBunInstallDryRun(packagePath) {
|
|
696
|
+
const base = await fs2.mkdtemp(path4.join(os.tmpdir(), "bun-ready-"));
|
|
249
697
|
const cleanup = async () => {
|
|
250
698
|
try {
|
|
251
699
|
await fs2.rm(base, { recursive: true, force: true });
|
|
@@ -254,42 +702,147 @@ var runBunInstallDryRun = async (repoPath) => {
|
|
|
254
702
|
}
|
|
255
703
|
};
|
|
256
704
|
try {
|
|
257
|
-
await copyIfExists(
|
|
258
|
-
await copyIfExists(
|
|
259
|
-
await copyIfExists(
|
|
260
|
-
await copyIfExists(
|
|
261
|
-
await copyIfExists(
|
|
262
|
-
await copyIfExists(
|
|
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"));
|
|
263
711
|
const res = await exec("bun", ["install", "--dry-run"], base);
|
|
264
712
|
const combined = [...res.stdout ? res.stdout.split(`
|
|
265
713
|
`) : [], ...res.stderr ? res.stderr.split(`
|
|
266
714
|
`) : []].filter((l) => l.trim().length > 0);
|
|
267
715
|
const logs = truncateLines(combined, 60);
|
|
268
|
-
|
|
716
|
+
const installAnalysis = parseInstallLogs(logs);
|
|
717
|
+
return res.code === 0 ? { ok: true, summary: "bun install --dry-run succeeded", logs, installAnalysis } : { ok: false, summary: `bun install --dry-run failed (exit ${res.code})`, logs, installAnalysis };
|
|
269
718
|
} finally {
|
|
270
719
|
await cleanup();
|
|
271
720
|
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
const t =
|
|
721
|
+
}
|
|
722
|
+
function shouldRunBunTest(scripts) {
|
|
723
|
+
const t = scripts["test"];
|
|
275
724
|
if (!t)
|
|
276
725
|
return false;
|
|
277
726
|
return t.toLowerCase().includes("bun test") || t.toLowerCase().trim() === "bun test";
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
const res = await exec("bun", ["test"],
|
|
727
|
+
}
|
|
728
|
+
async function runBunTest(packagePath) {
|
|
729
|
+
const res = await exec("bun", ["test"], packagePath);
|
|
281
730
|
const combined = [...res.stdout ? res.stdout.split(`
|
|
282
731
|
`) : [], ...res.stderr ? res.stderr.split(`
|
|
283
732
|
`) : []].filter((l) => l.trim().length > 0);
|
|
284
733
|
const logs = truncateLines(combined, 120);
|
|
285
734
|
return res.code === 0 ? { ok: true, summary: "bun test succeeded", logs } : { ok: false, summary: `bun test failed (exit ${res.code})`, logs };
|
|
286
|
-
}
|
|
287
|
-
|
|
735
|
+
}
|
|
736
|
+
function filterFindings(findings, config) {
|
|
737
|
+
const ignoreList = config?.ignoreFindings;
|
|
738
|
+
if (!ignoreList || ignoreList.length === 0) {
|
|
739
|
+
return findings;
|
|
740
|
+
}
|
|
741
|
+
return findings.filter((f) => !ignoreList.includes(f.id));
|
|
742
|
+
}
|
|
743
|
+
async function analyzeSinglePackage(packagePath, opts, config, pkgName) {
|
|
744
|
+
const info = await readRepoInfo(packagePath);
|
|
745
|
+
const name = pkgName || info.pkg.name || path4.basename(packagePath);
|
|
746
|
+
let findings = [
|
|
747
|
+
...detectLockfileSignals({ packageJsonPath: packagePath, lockfiles: info.lockfiles, scripts: info.scripts, dependencies: info.dependencies, devDependencies: info.devDependencies, optionalDependencies: info.optionalDependencies, hasWorkspaces: false, packageJson: info.pkg }),
|
|
748
|
+
...detectScriptRisks({ packageJsonPath: packagePath, lockfiles: info.lockfiles, scripts: info.scripts, dependencies: info.dependencies, devDependencies: info.devDependencies, optionalDependencies: info.optionalDependencies, hasWorkspaces: false, packageJson: info.pkg }),
|
|
749
|
+
...detectNativeAddonRiskV2({ packageJsonPath: packagePath, lockfiles: info.lockfiles, scripts: info.scripts, dependencies: info.dependencies, devDependencies: info.devDependencies, optionalDependencies: info.optionalDependencies, hasWorkspaces: false, packageJson: info.pkg }, config || undefined),
|
|
750
|
+
...detectRuntimeApiRisks({ packageJsonPath: packagePath, lockfiles: info.lockfiles, scripts: info.scripts, dependencies: info.dependencies, devDependencies: info.devDependencies, optionalDependencies: info.optionalDependencies, hasWorkspaces: false, packageJson: info.pkg }),
|
|
751
|
+
...detectPmAssumptions({ packageJsonPath: packagePath, lockfiles: info.lockfiles, scripts: info.scripts, dependencies: info.dependencies, devDependencies: info.devDependencies, optionalDependencies: info.optionalDependencies, hasWorkspaces: false, packageJson: info.pkg })
|
|
752
|
+
];
|
|
753
|
+
findings = filterFindings(findings, config);
|
|
754
|
+
let install = null;
|
|
755
|
+
let installOk = null;
|
|
756
|
+
if (opts.runInstall) {
|
|
757
|
+
const installResult = await runBunInstallDryRun(packagePath);
|
|
758
|
+
install = {
|
|
759
|
+
ok: installResult.ok,
|
|
760
|
+
summary: installResult.summary,
|
|
761
|
+
logs: installResult.logs
|
|
762
|
+
};
|
|
763
|
+
installOk = installResult.ok;
|
|
764
|
+
if (installResult.installAnalysis.blockedDeps.length > 0) {
|
|
765
|
+
findings.push({
|
|
766
|
+
id: "install.blocked_scripts",
|
|
767
|
+
title: "Lifecycle scripts blocked by Bun",
|
|
768
|
+
severity: "red",
|
|
769
|
+
details: installResult.installAnalysis.blockedDeps,
|
|
770
|
+
hints: [
|
|
771
|
+
"Bun blocks lifecycle scripts of dependencies unless they are in trustedDependencies.",
|
|
772
|
+
"Add the blocked packages to trustedDependencies in root package.json.",
|
|
773
|
+
"Review if these packages are necessary or can be replaced with alternatives."
|
|
774
|
+
]
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
if (installResult.installAnalysis.trustedDepsMentioned.length > 0) {
|
|
778
|
+
findings.push({
|
|
779
|
+
id: "install.trusted_deps",
|
|
780
|
+
title: "Trusted dependencies mentioned in install output",
|
|
781
|
+
severity: "yellow",
|
|
782
|
+
details: installResult.installAnalysis.trustedDepsMentioned,
|
|
783
|
+
hints: [
|
|
784
|
+
"Review the trustedDependencies configuration in your package.json.",
|
|
785
|
+
"Consider adding packages to trustedDependencies if you trust them."
|
|
786
|
+
]
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
let test = null;
|
|
791
|
+
let testOk = null;
|
|
792
|
+
if (opts.runTest && shouldRunBunTest(info.scripts)) {
|
|
793
|
+
const testResult = await runBunTest(packagePath);
|
|
794
|
+
test = {
|
|
795
|
+
ok: testResult.ok,
|
|
796
|
+
summary: testResult.summary,
|
|
797
|
+
logs: testResult.logs
|
|
798
|
+
};
|
|
799
|
+
testOk = testResult.ok;
|
|
800
|
+
}
|
|
801
|
+
const severity = summarizeSeverity(findings, installOk, testOk);
|
|
802
|
+
const summaryLines = [];
|
|
803
|
+
summaryLines.push(`Lockfiles: ${info.lockfiles.bunLock || info.lockfiles.bunLockb ? "bun" : "non-bun or missing"}`);
|
|
804
|
+
summaryLines.push(`Lifecycle scripts: ${Object.keys(info.scripts).some((k) => ["postinstall", "prepare", "preinstall", "install"].includes(k)) ? "present" : "none"}`);
|
|
805
|
+
summaryLines.push(`Native addon risk: ${findings.some((f) => f.id === "deps.native_addons") ? "yes" : "no"}`);
|
|
806
|
+
summaryLines.push(`bun install dry-run: ${install ? install.ok ? "ok" : "failed" : "skipped"}`);
|
|
807
|
+
summaryLines.push(`bun test: ${test ? test.ok ? "ok" : "failed" : "skipped"}`);
|
|
808
|
+
return {
|
|
809
|
+
name,
|
|
810
|
+
path: packagePath,
|
|
811
|
+
severity,
|
|
812
|
+
summaryLines,
|
|
813
|
+
findings,
|
|
814
|
+
install,
|
|
815
|
+
test,
|
|
816
|
+
scripts: info.scripts,
|
|
817
|
+
dependencies: info.dependencies,
|
|
818
|
+
devDependencies: info.devDependencies,
|
|
819
|
+
optionalDependencies: info.optionalDependencies,
|
|
820
|
+
lockfiles: info.lockfiles
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
function aggregateSeverity(packages, overallSeverity) {
|
|
824
|
+
if (overallSeverity === "red")
|
|
825
|
+
return "red";
|
|
826
|
+
if (overallSeverity === "yellow")
|
|
827
|
+
return "yellow";
|
|
828
|
+
for (const pkg of packages) {
|
|
829
|
+
if (pkg.severity === "red")
|
|
830
|
+
return "red";
|
|
831
|
+
}
|
|
832
|
+
for (const pkg of packages) {
|
|
833
|
+
if (pkg.severity === "yellow")
|
|
834
|
+
return "yellow";
|
|
835
|
+
}
|
|
836
|
+
return "green";
|
|
837
|
+
}
|
|
838
|
+
async function analyzeRepoOverall(opts) {
|
|
288
839
|
const repoPath = normalizeRepoPath(opts.repoPath);
|
|
289
|
-
const packageJsonPath =
|
|
840
|
+
const packageJsonPath = path4.join(repoPath, "package.json");
|
|
290
841
|
const hasPkg = await fileExists(packageJsonPath);
|
|
842
|
+
const config = await readConfig(repoPath);
|
|
291
843
|
if (!hasPkg) {
|
|
292
844
|
return {
|
|
845
|
+
version: "0.2",
|
|
293
846
|
severity: "red",
|
|
294
847
|
summaryLines: ["package.json not found"],
|
|
295
848
|
findings: [
|
|
@@ -305,35 +858,90 @@ var analyzeRepo = async (opts) => {
|
|
|
305
858
|
test: null,
|
|
306
859
|
repo: {
|
|
307
860
|
packageJsonPath,
|
|
861
|
+
hasWorkspaces: false,
|
|
862
|
+
rootPackage: { name: "", version: "" },
|
|
308
863
|
lockfiles: { bunLock: false, bunLockb: false, npmLock: false, yarnLock: false, pnpmLock: false },
|
|
309
864
|
scripts: {},
|
|
310
865
|
dependencies: {},
|
|
311
866
|
devDependencies: {},
|
|
312
|
-
optionalDependencies: {}
|
|
313
|
-
|
|
314
|
-
|
|
867
|
+
optionalDependencies: {}
|
|
868
|
+
},
|
|
869
|
+
packages: [],
|
|
870
|
+
config
|
|
315
871
|
};
|
|
316
872
|
}
|
|
317
|
-
const
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
...
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
873
|
+
const rootInfo = await readRepoInfo(repoPath);
|
|
874
|
+
const rootHasWorkspaces = await hasWorkspaces(repoPath);
|
|
875
|
+
const workspacePackages = [];
|
|
876
|
+
if (rootHasWorkspaces) {
|
|
877
|
+
workspacePackages.push(...await discoverWorkspaces(repoPath));
|
|
878
|
+
}
|
|
879
|
+
let packagesToAnalyze = [];
|
|
880
|
+
let packagesToReport = [];
|
|
881
|
+
if (opts.scope === "root") {
|
|
882
|
+
packagesToAnalyze = [repoPath];
|
|
883
|
+
packagesToReport = [repoPath];
|
|
884
|
+
} else if (opts.scope === "packages") {
|
|
885
|
+
packagesToAnalyze = workspacePackages.map((wp) => wp.path);
|
|
886
|
+
packagesToReport = workspacePackages.map((wp) => wp.path);
|
|
887
|
+
} else {
|
|
888
|
+
packagesToAnalyze = [repoPath, ...workspacePackages.map((wp) => wp.path)];
|
|
889
|
+
packagesToReport = [repoPath, ...workspacePackages.map((wp) => wp.path)];
|
|
890
|
+
}
|
|
891
|
+
const ignorePackages = config?.ignorePackages;
|
|
892
|
+
if (ignorePackages && ignorePackages.length > 0) {
|
|
893
|
+
packagesToAnalyze = packagesToAnalyze.filter((p) => {
|
|
894
|
+
return !ignorePackages.some((ignore) => p.includes(ignore));
|
|
895
|
+
});
|
|
896
|
+
packagesToReport = packagesToReport.filter((p) => {
|
|
897
|
+
return !ignorePackages.some((ignore) => p.includes(ignore));
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
const packages = [];
|
|
901
|
+
for (const packagePath of packagesToAnalyze) {
|
|
902
|
+
const wp = workspacePackages.find((w) => w.path === packagePath);
|
|
903
|
+
const analysis = await analyzeSinglePackage(packagePath, opts, config, wp?.name);
|
|
904
|
+
packages.push(analysis);
|
|
905
|
+
}
|
|
906
|
+
const rootAnalysis = packages.find((p) => p.path === repoPath);
|
|
907
|
+
let overallSeverity = "green";
|
|
908
|
+
if (rootAnalysis) {
|
|
909
|
+
overallSeverity = rootAnalysis.severity;
|
|
910
|
+
}
|
|
911
|
+
overallSeverity = aggregateSeverity(packages, overallSeverity);
|
|
912
|
+
const overallFindings = rootAnalysis ? rootAnalysis.findings : [];
|
|
913
|
+
const overallSummaryLines = [];
|
|
914
|
+
overallSummaryLines.push(`Total packages analyzed: ${packages.length}`);
|
|
915
|
+
overallSummaryLines.push(`Workspaces detected: ${rootHasWorkspaces ? "yes" : "no"}`);
|
|
916
|
+
if (rootHasWorkspaces) {
|
|
917
|
+
overallSummaryLines.push(`Workspace packages: ${workspacePackages.length}`);
|
|
918
|
+
}
|
|
919
|
+
overallSummaryLines.push(`Root package severity: ${rootAnalysis ? rootAnalysis.severity : "unknown"}`);
|
|
920
|
+
overallSummaryLines.push(`Overall severity: ${overallSeverity}`);
|
|
921
|
+
return {
|
|
922
|
+
version: "0.2",
|
|
923
|
+
severity: overallSeverity,
|
|
924
|
+
summaryLines: overallSummaryLines,
|
|
925
|
+
findings: overallFindings,
|
|
926
|
+
install: rootAnalysis ? rootAnalysis.install : null,
|
|
927
|
+
test: rootAnalysis ? rootAnalysis.test : null,
|
|
928
|
+
repo: {
|
|
929
|
+
packageJsonPath,
|
|
930
|
+
hasWorkspaces: rootHasWorkspaces,
|
|
931
|
+
rootPackage: {
|
|
932
|
+
name: rootInfo.pkg.name || "",
|
|
933
|
+
version: rootInfo.pkg.version || ""
|
|
934
|
+
},
|
|
935
|
+
lockfiles: rootInfo.lockfiles,
|
|
936
|
+
scripts: rootInfo.scripts,
|
|
937
|
+
dependencies: rootInfo.dependencies,
|
|
938
|
+
devDependencies: rootInfo.devDependencies,
|
|
939
|
+
optionalDependencies: rootInfo.optionalDependencies
|
|
940
|
+
},
|
|
941
|
+
packages,
|
|
942
|
+
config
|
|
943
|
+
};
|
|
944
|
+
}
|
|
337
945
|
|
|
338
946
|
// src/report_md.ts
|
|
339
947
|
var badge = (s) => {
|
|
@@ -343,7 +951,24 @@ var badge = (s) => {
|
|
|
343
951
|
return "\uD83D\uDFE1 YELLOW";
|
|
344
952
|
return "\uD83D\uDD34 RED";
|
|
345
953
|
};
|
|
346
|
-
var
|
|
954
|
+
var getTopFindings = (pkg, count = 3) => {
|
|
955
|
+
const sorted = [...pkg.findings].sort((a, b) => {
|
|
956
|
+
const severityOrder = { red: 0, yellow: 1, green: 2 };
|
|
957
|
+
if (severityOrder[a.severity] !== severityOrder[b.severity]) {
|
|
958
|
+
return severityOrder[a.severity] - severityOrder[b.severity];
|
|
959
|
+
}
|
|
960
|
+
return a.id.localeCompare(b.id);
|
|
961
|
+
});
|
|
962
|
+
return sorted.slice(0, count).map((f) => `${badge(f.severity)} ${f.title}`);
|
|
963
|
+
};
|
|
964
|
+
var packageRow = (pkg) => {
|
|
965
|
+
const name = pkg.name;
|
|
966
|
+
const path5 = pkg.path.replace(/\\/g, "/");
|
|
967
|
+
const severity = badge(pkg.severity);
|
|
968
|
+
const topFindings = getTopFindings(pkg, 2).join(", ") || "No issues";
|
|
969
|
+
return `| ${name} | \`${path5}\` | ${severity} | ${topFindings} |`;
|
|
970
|
+
};
|
|
971
|
+
function renderMarkdown(r) {
|
|
347
972
|
const lines = [];
|
|
348
973
|
lines.push(`# bun-ready report`);
|
|
349
974
|
lines.push(``);
|
|
@@ -353,77 +978,157 @@ var renderMarkdown = (r) => {
|
|
|
353
978
|
for (const l of r.summaryLines)
|
|
354
979
|
lines.push(`- ${l}`);
|
|
355
980
|
lines.push(``);
|
|
356
|
-
|
|
981
|
+
if (r.version) {
|
|
982
|
+
lines.push(`**Report version:** ${r.version}`);
|
|
983
|
+
lines.push(``);
|
|
984
|
+
}
|
|
985
|
+
if (r.config) {
|
|
986
|
+
lines.push(`**Configuration:**`);
|
|
987
|
+
const configInfo = [];
|
|
988
|
+
if (r.config.ignorePackages && r.config.ignorePackages.length > 0) {
|
|
989
|
+
configInfo.push(`Ignored packages: ${r.config.ignorePackages.join(", ")}`);
|
|
990
|
+
}
|
|
991
|
+
if (r.config.ignoreFindings && r.config.ignoreFindings.length > 0) {
|
|
992
|
+
configInfo.push(`Ignored findings: ${r.config.ignoreFindings.join(", ")}`);
|
|
993
|
+
}
|
|
994
|
+
if (r.config.nativeAddonAllowlist && r.config.nativeAddonAllowlist.length > 0) {
|
|
995
|
+
configInfo.push(`Native addon allowlist: ${r.config.nativeAddonAllowlist.join(", ")}`);
|
|
996
|
+
}
|
|
997
|
+
if (r.config.failOn) {
|
|
998
|
+
configInfo.push(`Fail on: ${r.config.failOn}`);
|
|
999
|
+
}
|
|
1000
|
+
if (configInfo.length > 0) {
|
|
1001
|
+
for (const info of configInfo) {
|
|
1002
|
+
lines.push(`- ${info}`);
|
|
1003
|
+
}
|
|
1004
|
+
} else {
|
|
1005
|
+
lines.push(`- Using default configuration`);
|
|
1006
|
+
}
|
|
1007
|
+
lines.push(``);
|
|
1008
|
+
}
|
|
1009
|
+
lines.push(`## Root Package`);
|
|
357
1010
|
lines.push(`- Path: \`${r.repo.packageJsonPath.replace(/\\/g, "/")}\``);
|
|
358
1011
|
lines.push(`- Workspaces: ${r.repo.hasWorkspaces ? "yes" : "no"}`);
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
lock.push("bun.lock");
|
|
362
|
-
if (r.repo.lockfiles.bunLockb)
|
|
363
|
-
lock.push("bun.lockb");
|
|
364
|
-
if (r.repo.lockfiles.npmLock)
|
|
365
|
-
lock.push("package-lock.json");
|
|
366
|
-
if (r.repo.lockfiles.yarnLock)
|
|
367
|
-
lock.push("yarn.lock");
|
|
368
|
-
if (r.repo.lockfiles.pnpmLock)
|
|
369
|
-
lock.push("pnpm-lock.yaml");
|
|
370
|
-
lines.push(`- Lockfiles: ${lock.length === 0 ? "none" : lock.join(", ")}`);
|
|
1012
|
+
lines.push(`- Name: ${r.repo.rootPackage?.name || "unknown"}`);
|
|
1013
|
+
lines.push(`- Version: ${r.repo.rootPackage?.version || "unknown"}`);
|
|
371
1014
|
lines.push(``);
|
|
372
|
-
if (r.
|
|
1015
|
+
if (r.packages && (r.packages.length > 1 || r.packages.length === 1 && r.repo.hasWorkspaces)) {
|
|
1016
|
+
lines.push(`## Packages Overview`);
|
|
1017
|
+
lines.push(`| Package | Path | Status | Key Findings |`);
|
|
1018
|
+
lines.push(`|---------|------|--------|--------------|`);
|
|
1019
|
+
const sortedPackages = stableSort(r.packages, (p) => p.name);
|
|
1020
|
+
for (const pkg of sortedPackages) {
|
|
1021
|
+
lines.push(packageRow(pkg));
|
|
1022
|
+
}
|
|
1023
|
+
lines.push(``);
|
|
1024
|
+
}
|
|
1025
|
+
const rootPkg = r.packages?.find((p) => p.path === r.repo.packageJsonPath);
|
|
1026
|
+
if (rootPkg?.install) {
|
|
373
1027
|
lines.push(`## bun install (dry-run)`);
|
|
374
|
-
lines.push(`- Result: ${
|
|
375
|
-
lines.push(`- Summary: ${
|
|
376
|
-
if (
|
|
1028
|
+
lines.push(`- Result: ${rootPkg.install.ok ? "ok" : "failed"}`);
|
|
1029
|
+
lines.push(`- Summary: ${rootPkg.install.summary}`);
|
|
1030
|
+
if (rootPkg.install.logs.length > 0) {
|
|
377
1031
|
lines.push(``);
|
|
378
1032
|
lines.push("```text");
|
|
379
|
-
for (const l of
|
|
1033
|
+
for (const l of rootPkg.install.logs)
|
|
380
1034
|
lines.push(l);
|
|
381
1035
|
lines.push("```");
|
|
382
1036
|
}
|
|
383
1037
|
lines.push(``);
|
|
384
1038
|
}
|
|
385
|
-
if (
|
|
1039
|
+
if (rootPkg?.test) {
|
|
386
1040
|
lines.push(`## bun test`);
|
|
387
|
-
lines.push(`- Result: ${
|
|
388
|
-
lines.push(`- Summary: ${
|
|
389
|
-
if (
|
|
1041
|
+
lines.push(`- Result: ${rootPkg.test.ok ? "ok" : "failed"}`);
|
|
1042
|
+
lines.push(`- Summary: ${rootPkg.test.summary}`);
|
|
1043
|
+
if (rootPkg.test.logs.length > 0) {
|
|
390
1044
|
lines.push(``);
|
|
391
1045
|
lines.push("```text");
|
|
392
|
-
for (const l of
|
|
1046
|
+
for (const l of rootPkg.test.logs)
|
|
393
1047
|
lines.push(l);
|
|
394
1048
|
lines.push("```");
|
|
395
1049
|
}
|
|
396
1050
|
lines.push(``);
|
|
397
1051
|
}
|
|
398
|
-
lines.push(`## Findings`);
|
|
1052
|
+
lines.push(`## Root Findings`);
|
|
399
1053
|
if (r.findings.length === 0) {
|
|
400
|
-
lines.push(`No findings
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
1054
|
+
lines.push(`No findings for root package.`);
|
|
1055
|
+
} else {
|
|
1056
|
+
const findings = stableSort(r.findings, (f) => `${f.severity}:${f.id}`);
|
|
1057
|
+
for (const f of findings) {
|
|
1058
|
+
lines.push(`### ${f.title} (${badge(f.severity)})`);
|
|
1059
|
+
lines.push(``);
|
|
1060
|
+
for (const d of f.details)
|
|
1061
|
+
lines.push(`- ${d}`);
|
|
1062
|
+
if (f.hints.length > 0) {
|
|
1063
|
+
lines.push(``);
|
|
1064
|
+
lines.push(`**Hints:**`);
|
|
1065
|
+
for (const h of f.hints)
|
|
1066
|
+
lines.push(`- ${h}`);
|
|
1067
|
+
}
|
|
1068
|
+
lines.push(``);
|
|
1069
|
+
}
|
|
404
1070
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
lines.push(
|
|
411
|
-
|
|
1071
|
+
lines.push(``);
|
|
1072
|
+
if (r.packages && r.packages.length > 0) {
|
|
1073
|
+
const sortedPackages = stableSort(r.packages, (p) => p.name);
|
|
1074
|
+
for (const pkg of sortedPackages) {
|
|
1075
|
+
lines.push(`## Package: ${pkg.name} (${badge(pkg.severity)})`);
|
|
1076
|
+
lines.push(``);
|
|
1077
|
+
lines.push(`**Path:** \`${pkg.path.replace(/\\/g, "/")}\``);
|
|
1078
|
+
lines.push(``);
|
|
1079
|
+
for (const l of pkg.summaryLines)
|
|
1080
|
+
lines.push(`- ${l}`);
|
|
1081
|
+
lines.push(``);
|
|
1082
|
+
if (pkg.install) {
|
|
1083
|
+
lines.push(`**bun install (dry-run):** ${pkg.install.ok ? "ok" : "failed"}`);
|
|
1084
|
+
if (pkg.install.logs.length > 0 && pkg.install.logs.length < 10) {
|
|
1085
|
+
lines.push(``);
|
|
1086
|
+
lines.push("```text");
|
|
1087
|
+
for (const l of pkg.install.logs)
|
|
1088
|
+
lines.push(l);
|
|
1089
|
+
lines.push("```");
|
|
1090
|
+
}
|
|
1091
|
+
lines.push(``);
|
|
1092
|
+
}
|
|
1093
|
+
if (pkg.test) {
|
|
1094
|
+
lines.push(`**bun test:** ${pkg.test.ok ? "ok" : "failed"}`);
|
|
1095
|
+
if (pkg.test.logs.length > 0 && pkg.test.logs.length < 10) {
|
|
1096
|
+
lines.push(``);
|
|
1097
|
+
lines.push("```text");
|
|
1098
|
+
for (const l of pkg.test.logs)
|
|
1099
|
+
lines.push(l);
|
|
1100
|
+
lines.push("```");
|
|
1101
|
+
}
|
|
1102
|
+
lines.push(``);
|
|
1103
|
+
}
|
|
1104
|
+
lines.push(`**Findings:**`);
|
|
1105
|
+
if (pkg.findings.length === 0) {
|
|
1106
|
+
lines.push(`No findings for this package.`);
|
|
1107
|
+
} else {
|
|
1108
|
+
const findings = stableSort(pkg.findings, (f) => `${f.severity}:${f.id}`);
|
|
1109
|
+
for (const f of findings) {
|
|
1110
|
+
lines.push(``);
|
|
1111
|
+
lines.push(`### ${f.title} (${badge(f.severity)})`);
|
|
1112
|
+
for (const d of f.details)
|
|
1113
|
+
lines.push(`- ${d}`);
|
|
1114
|
+
if (f.hints.length > 0) {
|
|
1115
|
+
lines.push(`**Hints:**`);
|
|
1116
|
+
for (const h of f.hints)
|
|
1117
|
+
lines.push(`- ${h}`);
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
412
1121
|
lines.push(``);
|
|
413
|
-
lines.push(`**Hints:**`);
|
|
414
|
-
for (const h of f.hints)
|
|
415
|
-
lines.push(`- ${h}`);
|
|
416
1122
|
}
|
|
417
|
-
lines.push(``);
|
|
418
1123
|
}
|
|
419
1124
|
return lines.join(`
|
|
420
1125
|
`);
|
|
421
|
-
}
|
|
1126
|
+
}
|
|
422
1127
|
|
|
423
1128
|
// src/report_json.ts
|
|
424
|
-
|
|
1129
|
+
function renderJson(r) {
|
|
425
1130
|
return JSON.stringify(r, null, 2);
|
|
426
|
-
}
|
|
1131
|
+
}
|
|
427
1132
|
|
|
428
1133
|
// src/cli.ts
|
|
429
1134
|
var usage = () => {
|
|
@@ -431,10 +1136,22 @@ var usage = () => {
|
|
|
431
1136
|
"bun-ready",
|
|
432
1137
|
"",
|
|
433
1138
|
"Usage:",
|
|
434
|
-
" bun-ready scan <path> [--format md|json] [--out <file>] [--no-install] [--no-test] [--verbose]",
|
|
1139
|
+
" bun-ready scan <path> [--format md|json] [--out <file>] [--no-install] [--no-test] [--verbose] [--scope root|packages|all] [--fail-on green|yellow|red]",
|
|
1140
|
+
"",
|
|
1141
|
+
"Options:",
|
|
1142
|
+
" --format md|json Output format (default: md)",
|
|
1143
|
+
" --out <file> Output file path (default: bun-ready.md or bun-ready.json)",
|
|
1144
|
+
" --no-install Skip bun install --dry-run",
|
|
1145
|
+
" --no-test Skip bun test",
|
|
1146
|
+
" --verbose Show detailed output",
|
|
1147
|
+
" --scope root|packages|all Scan scope for monorepos (default: all)",
|
|
1148
|
+
" --fail-on green|yellow|red Fail policy (default: red)",
|
|
435
1149
|
"",
|
|
436
1150
|
"Exit codes:",
|
|
437
|
-
" 0
|
|
1151
|
+
" 0 green",
|
|
1152
|
+
" 2 yellow",
|
|
1153
|
+
" 3 red",
|
|
1154
|
+
" 1 invalid command"
|
|
438
1155
|
].join(`
|
|
439
1156
|
`);
|
|
440
1157
|
};
|
|
@@ -444,7 +1161,15 @@ var parseArgs = (argv) => {
|
|
|
444
1161
|
if (cmd !== "scan") {
|
|
445
1162
|
return {
|
|
446
1163
|
cmd,
|
|
447
|
-
opts: {
|
|
1164
|
+
opts: {
|
|
1165
|
+
repoPath: ".",
|
|
1166
|
+
format: "md",
|
|
1167
|
+
outFile: null,
|
|
1168
|
+
runInstall: true,
|
|
1169
|
+
runTest: true,
|
|
1170
|
+
verbose: false,
|
|
1171
|
+
scope: "all"
|
|
1172
|
+
}
|
|
448
1173
|
};
|
|
449
1174
|
}
|
|
450
1175
|
const repoPath = args[1] && !args[1].startsWith("-") ? args[1] : ".";
|
|
@@ -453,6 +1178,8 @@ var parseArgs = (argv) => {
|
|
|
453
1178
|
let runInstall = true;
|
|
454
1179
|
let runTest = true;
|
|
455
1180
|
let verbose = false;
|
|
1181
|
+
let scope = "all";
|
|
1182
|
+
let failOn;
|
|
456
1183
|
for (let i = 2;i < args.length; i++) {
|
|
457
1184
|
const a = args[i] ?? "";
|
|
458
1185
|
if (a === "--format") {
|
|
@@ -479,10 +1206,56 @@ var parseArgs = (argv) => {
|
|
|
479
1206
|
verbose = true;
|
|
480
1207
|
continue;
|
|
481
1208
|
}
|
|
1209
|
+
if (a === "--scope") {
|
|
1210
|
+
const v = args[i + 1] ?? "";
|
|
1211
|
+
if (v === "root" || v === "packages" || v === "all")
|
|
1212
|
+
scope = v;
|
|
1213
|
+
i++;
|
|
1214
|
+
continue;
|
|
1215
|
+
}
|
|
1216
|
+
if (a === "--fail-on") {
|
|
1217
|
+
const v = args[i + 1] ?? "";
|
|
1218
|
+
if (v === "green" || v === "yellow" || v === "red")
|
|
1219
|
+
failOn = v;
|
|
1220
|
+
i++;
|
|
1221
|
+
continue;
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
const baseOpts = {
|
|
1225
|
+
repoPath,
|
|
1226
|
+
format,
|
|
1227
|
+
outFile,
|
|
1228
|
+
runInstall,
|
|
1229
|
+
runTest,
|
|
1230
|
+
verbose,
|
|
1231
|
+
scope
|
|
1232
|
+
};
|
|
1233
|
+
if (failOn !== undefined) {
|
|
1234
|
+
baseOpts.failOn = failOn;
|
|
482
1235
|
}
|
|
483
|
-
return {
|
|
1236
|
+
return {
|
|
1237
|
+
cmd,
|
|
1238
|
+
opts: baseOpts
|
|
1239
|
+
};
|
|
484
1240
|
};
|
|
485
|
-
var exitCode = (sev) => {
|
|
1241
|
+
var exitCode = (sev, failOn) => {
|
|
1242
|
+
if (!failOn) {
|
|
1243
|
+
if (sev === "green")
|
|
1244
|
+
return 0;
|
|
1245
|
+
if (sev === "yellow")
|
|
1246
|
+
return 2;
|
|
1247
|
+
return 3;
|
|
1248
|
+
}
|
|
1249
|
+
if (failOn === "green") {
|
|
1250
|
+
if (sev === "green")
|
|
1251
|
+
return 0;
|
|
1252
|
+
return 3;
|
|
1253
|
+
}
|
|
1254
|
+
if (failOn === "yellow") {
|
|
1255
|
+
if (sev === "red")
|
|
1256
|
+
return 3;
|
|
1257
|
+
return 0;
|
|
1258
|
+
}
|
|
486
1259
|
if (sev === "green")
|
|
487
1260
|
return 0;
|
|
488
1261
|
if (sev === "yellow")
|
|
@@ -496,14 +1269,27 @@ var main = async () => {
|
|
|
496
1269
|
`);
|
|
497
1270
|
process.exit(1);
|
|
498
1271
|
}
|
|
499
|
-
const
|
|
1272
|
+
const config = await mergeConfigWithOpts(null, opts);
|
|
1273
|
+
const scanOpts = {
|
|
1274
|
+
repoPath: opts.repoPath,
|
|
1275
|
+
format: opts.format,
|
|
1276
|
+
outFile: opts.outFile,
|
|
1277
|
+
runInstall: opts.runInstall,
|
|
1278
|
+
runTest: opts.runTest,
|
|
1279
|
+
verbose: opts.verbose,
|
|
1280
|
+
scope: opts.scope
|
|
1281
|
+
};
|
|
1282
|
+
if (opts.failOn !== undefined) {
|
|
1283
|
+
scanOpts.failOn = opts.failOn;
|
|
1284
|
+
}
|
|
1285
|
+
const res = await analyzeRepoOverall(scanOpts);
|
|
500
1286
|
const out = opts.format === "json" ? renderJson(res) : renderMarkdown(res);
|
|
501
1287
|
const target = opts.outFile ?? (opts.format === "json" ? "bun-ready.json" : "bun-ready.md");
|
|
502
|
-
const resolved =
|
|
1288
|
+
const resolved = path5.resolve(process.cwd(), target);
|
|
503
1289
|
await fs3.writeFile(resolved, out, "utf8");
|
|
504
1290
|
process.stdout.write(`Wrote ${opts.format.toUpperCase()} report to ${resolved}
|
|
505
1291
|
`);
|
|
506
|
-
process.exit(exitCode(res.severity));
|
|
1292
|
+
process.exit(exitCode(res.severity, config?.failOn || opts.failOn));
|
|
507
1293
|
};
|
|
508
1294
|
main().catch((e) => {
|
|
509
1295
|
const msg = e instanceof Error ? e.message : String(e);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bun-ready",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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",
|