@mh-alikhani/bunready 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/CHANGELOG.md CHANGED
@@ -9,6 +9,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  Nothing yet.
11
11
 
12
+ ## [0.2.0] - 2026-09-16
13
+
14
+ ### Added
15
+
16
+ - **Sourced runtime gap dataset.** `src/rules/data/node-runtime.json` now carries
17
+ 18 entries read from Bun's own Node.js compatibility table (17 `partial`,
18
+ 1 `unimplemented`), each with the table as its primary source. A repository
19
+ importing a `partial` module gets a `risk`; an `unimplemented` one
20
+ (currently `node:sea`) is a `blocker`, because it breaks on import with no
21
+ user action.
22
+ - **Real-project smoke script.** `bun run smoke` downloads the Next.js
23
+ hello-world example, the NestJS starter and turborepo's basic monorepo and
24
+ prints each scan's verdict, so a release is validated against real
25
+ repositories before it ships.
26
+ - Decisions D41–D43 in `STATE.md` (optional-dependency severity, `node:`
27
+ prefix classification, the smoke script).
28
+
29
+ ### Fixed
30
+
31
+ - **Optional dependencies no longer produce blockers.** A skipped install
32
+ script on an *optional* package (the `fsevents` false positive on the NestJS
33
+ starter) is now a `risk`: the installer tolerates an absent optional package
34
+ by design, so a skipped script cannot break the install itself.
35
+ - **`node:`-prefixed imports are always classified as Node built-ins**, even
36
+ when the runtime's own `builtinModules` list lags behind Node (e.g.
37
+ `node:sea` is missing from Bun's list).
38
+ - README no longer claims `--run` is unimplemented.
39
+
12
40
  ## [0.1.0] - 2026-09-09
13
41
 
14
42
  ### Added
package/README.md CHANGED
@@ -1,6 +1,10 @@
1
1
  <div align="center">
2
2
 
3
- <img src="docs/brand/logo.svg" alt="bunready" width="360">
3
+ <picture>
4
+ <source media="(prefers-color-scheme: dark)" srcset="docs/brand/logo-dark.svg">
5
+ <source media="(prefers-color-scheme: light)" srcset="docs/brand/logo.svg">
6
+ <img src="docs/brand/logo-auto.svg" alt="bunready" width="360">
7
+ </picture>
4
8
 
5
9
  **One command that tells you what will break before you move a Node/TS repo to Bun — and gives you one clear verdict.**
6
10
 
@@ -24,9 +28,10 @@ Pre-alpha, and precise about it. `bunready <path>` reads `package.json`, any
24
28
  lockfile, and the repository's own imports, then prints a report with a verdict
25
29
  and an exit code CI can gate on.
26
30
 
27
- What it does not do yet: `--run` (executing the target's scripts under Bun) is
28
- accepted and reported as unimplemented, and no Node-runtime gap is claimed
29
- without a source. See [STATE.md](STATE.md) for what exists and what does not.
31
+ `--run` goes further: it copies the target to a temporary directory, installs
32
+ and boots it under Bun there, and reports the first real failure with timings.
33
+ Every compatibility claim carries a source link, so you can check the
34
+ underlying evidence yourself. See [STATE.md](STATE.md) for details.
30
35
 
31
36
  ## Install
32
37
 
@@ -57,7 +62,7 @@ bunx @mh-alikhani/bunready /path/to/node-project
57
62
  | `--help` | Print usage and exit. |
58
63
  | `--version` | Print the CLI version and exit. |
59
64
  | `--json` | Emit machine-readable JSON instead of the terminal report. |
60
- | `--run` | Planned: run the target's own scripts under Bun in a temporary copy. Not implemented yet; the flag says so before scanning. |
65
+ | `--run [script]` | Copy the target to a temporary directory, `bun install` and run a script (default `start`) there under Bun, and report the first failure with timings. Never executes in place; a timeout is a `risk`, a pass is `info`. |
61
66
  | `NO_COLOR` | Environment variable: disable ANSI color when set. |
62
67
 
63
68
  Exit codes: `0` no blockers, `1` blockers found, `2` usage error.
@@ -88,7 +93,7 @@ permissions:
88
93
 
89
94
  steps:
90
95
  - uses: actions/checkout@v7
91
- - uses: MHAlikhani/bunready@v0.1.0
96
+ - uses: MHAlikhani/bunready@v0.2.0
92
97
  with:
93
98
  path: .
94
99
  ```
@@ -97,6 +102,18 @@ The action writes a JSON report, uploads the SARIF report to code scanning and
97
102
  fails the step when findings at or above `failOn` exist. Inputs: `path`,
98
103
  `version` (`latest` or `local`), `sarif-file`, `json-file`, `upload`.
99
104
 
105
+ ## How it is different
106
+
107
+ - **Evidence, not estimates.** Every finding records what was observed in your
108
+ repository, and every compatibility claim links to Bun's documentation or an
109
+ issue. Nothing is inferred from a package name.
110
+ - **Built for CI.** Stable exit codes, `--json` with a versioned schema,
111
+ `--sarif` for code scanning, and baselines so a repository can fail on what is
112
+ new instead of on its whole history.
113
+ - **Whole-repository aware.** Workspace packages are scanned and aggregated, and
114
+ `--run` installs and boots the project in a temporary copy to catch the first
115
+ real failure rather than predicting one.
116
+
100
117
  ## Why
101
118
 
102
119
  Moving a repo to Bun is usually a pile of small unknowns: which npm lifecycle
@@ -28,6 +28,7 @@ own: every severity also prints as a word.
28
28
  | --- | --- |
29
29
  | `docs/brand/logo.svg` | Default lockup, light backgrounds. |
30
30
  | `docs/brand/logo-dark.svg` | Lockup on dark backgrounds. |
31
+ | `docs/brand/logo-auto.svg` | Theme-aware lockup: follows `prefers-color-scheme` itself. Used where `<picture>` is stripped, and as the README fallback. |
31
32
  | `docs/brand/logo-mono.svg` | Single-colour lockup; inherits `currentColor`. |
32
33
  | `docs/brand/mark.svg` | Mark alone; inherits `currentColor`. |
33
34
  | `docs/brand/favicon.svg` | Favicon: mark on ink, tuned for 16px. |
@@ -38,6 +39,7 @@ square: "you are about to run something".
38
39
 
39
40
  **Clear space:** 0.5 x the mark height on all four sides. Nothing enters it.
40
41
  **Minimum sizes:** mark 24px; lockup 120px wide.
42
+ **Colour scheme:** every lockup must stay legible on both `#FBFAF7` and `#131519`. Where the host supports it, serve `logo.svg` / `logo-dark.svg` through `<picture>` with `prefers-color-scheme`; otherwise serve `logo-auto.svg`, which switches itself. Never ship the light lockup into a dark-only context.
41
43
  **Never:** recolour outside `tokens.json`, stretch, rotate, add effects,
42
44
  place the lockup on a busy photo, or crop the mark's rounded square.
43
45
 
@@ -0,0 +1,28 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 340 120" width="340" height="120" role="img" aria-label="bunready">
2
+ <title>bunready</title>
3
+ <!--
4
+ One lockup that follows the reader's colour scheme. The light palette is
5
+ baked into the shapes so that a sanitizer which strips <style> (some
6
+ registries do) still renders a legible mark; the media query only overrides
7
+ for dark. Prefer .svg light/dark pairs with <picture> when the host supports
8
+ it, and this file when it does not.
9
+ -->
10
+ <style>
11
+ .ring { stroke: #a85b2b; }
12
+ .glyph { stroke: #171a1f; }
13
+ .wordmark { fill: #171a1f; }
14
+ @media (prefers-color-scheme: dark) {
15
+ .ring { stroke: #e0a268; }
16
+ .glyph { stroke: #f2f1ec; }
17
+ .wordmark { fill: #f2f1ec; }
18
+ }
19
+ </style>
20
+ <g fill="none" stroke-linecap="round" stroke-linejoin="round">
21
+ <rect class="ring" x="34" y="34" width="52" height="52" rx="15" stroke-width="3"/>
22
+ <path class="glyph" d="M53 49 L68 60 L53 71" stroke-width="4.5"/>
23
+ <path class="glyph" d="M54 77 H68" stroke-width="4.5"/>
24
+ </g>
25
+ <text class="wordmark" x="108" y="76"
26
+ font-family="ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif"
27
+ font-size="42" font-weight="620" letter-spacing="-1">bunready</text>
28
+ </svg>
package/package.json CHANGED
@@ -1,15 +1,27 @@
1
1
  {
2
2
  "name": "@mh-alikhani/bunready",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Bun-readiness scanner: one command that shows what will break before you move a Node/TS repo to Bun.",
5
5
  "keywords": [
6
6
  "bun",
7
+ "bun-readiness",
8
+ "node",
9
+ "nodejs",
10
+ "typescript",
7
11
  "migration",
8
12
  "compatibility",
9
- "scanner",
10
13
  "cli",
11
- "node",
12
- "typescript"
14
+ "scanner",
15
+ "static-analysis",
16
+ "monorepo",
17
+ "workspaces",
18
+ "lockfile",
19
+ "sarif",
20
+ "ci",
21
+ "code-quality",
22
+ "developer-tools",
23
+ "devtools",
24
+ "dependency-analysis"
13
25
  ],
14
26
  "license": "MIT",
15
27
  "author": "Mohammad Hosein Alikhani",
@@ -45,6 +57,7 @@
45
57
  "build": "bun build ./src/cli/index.ts --target=bun --outdir=dist",
46
58
  "check": "bun run typecheck && bun run lint && bun run test",
47
59
  "og": "bun run scripts/generate-og-image.ts",
60
+ "smoke": "bun run scripts/smoke-real.ts",
48
61
  "prepare": "simple-git-hooks"
49
62
  },
50
63
  "devDependencies": {
@@ -1,6 +1,115 @@
1
1
  {
2
- "datasetVersion": "2026-09-15.1",
2
+ "datasetVersion": "2026-09-16.1",
3
3
  "compatibilityDocs": "https://bun.com/docs/runtime/nodejs-compat",
4
- "note": "Runtime compatibility data. `gaps` is intentionally EMPTY: every entry here asserts that a specific Node built-in is partial or missing, and that claim needs a primary source that was read, not remembered. Bun's own position is that a package working in Node.js and failing in Bun is a bug in Bun, so a module is only listed once a specific issue or documentation entry establishes the gap. Until then bunready reports what the repository imports and points at the compatibility table instead of guessing.",
5
- "gaps": []
4
+ "note": "Runtime compatibility data. Every entry below was read from Bun's own Node.js compatibility table (the `source` URL); see docs/adr/0001-data-source-policy.md. `status: partial` means part of the module's surface is missing; `status: unimplemented` means importing/using it fails outright.",
5
+ "gaps": [
6
+ {
7
+ "name": "sea",
8
+ "status": "unimplemented",
9
+ "note": "Single-executable applications are not implemented in Bun; Bun documents `bun build --compile` as the replacement.",
10
+ "source": "https://bun.com/docs/runtime/nodejs-compat"
11
+ },
12
+ {
13
+ "name": "async_hooks",
14
+ "status": "partial",
15
+ "note": "AsyncLocalStorage and AsyncResource work; createHook and related are stubs with async ids always 0, and context does not propagate into MessagePort/BroadcastChannel/Worker events.",
16
+ "source": "https://bun.com/docs/runtime/nodejs-compat"
17
+ },
18
+ {
19
+ "name": "child_process",
20
+ "status": "partial",
21
+ "note": "IPC handles net/dgram sockets but not http server sockets; 'advanced' serialization only works between Bun processes; channel.ref()/unref() are missing.",
22
+ "source": "https://bun.com/docs/runtime/nodejs-compat"
23
+ },
24
+ {
25
+ "name": "cluster",
26
+ "status": "partial",
27
+ "note": "net/dgram sharing works as in Node; HTTP load-balancing across processes works only on Linux via SO_REUSEPORT and is not battle-tested.",
28
+ "source": "https://bun.com/docs/runtime/nodejs-compat"
29
+ },
30
+ {
31
+ "name": "crypto",
32
+ "status": "partial",
33
+ "note": "BoringSSL-backed: ed448, x448, rsa-pss, dsa, dh, ML-KEM-512, secp256k1 and several ciphers are unavailable; encapsulate/decapsulate are missing; setEngine() throws and setFips() is a no-op.",
34
+ "source": "https://bun.com/docs/runtime/nodejs-compat"
35
+ },
36
+ {
37
+ "name": "diagnostics_channel",
38
+ "status": "partial",
39
+ "note": "channel(), subscribe(), tracingChannel() and the http/http2/dgram channels work; boundedChannel() and several built-in channels are missing, and subscribers do not keep a Channel alive.",
40
+ "source": "https://bun.com/docs/runtime/nodejs-compat"
41
+ },
42
+ {
43
+ "name": "domain",
44
+ "status": "partial",
45
+ "note": "Only synchronous errors are caught in run()/bind() or by emitters added with add(); asynchronous errors are not routed to the domain.",
46
+ "source": "https://bun.com/docs/runtime/nodejs-compat"
47
+ },
48
+ {
49
+ "name": "https",
50
+ "status": "partial",
51
+ "note": "request, get, Agent and globalAgent are implemented with pooling; https.Server is http.Server with TLS options and lacks getPeerCertificate(), getCipher(), setSecureContext(), SNICallback and handshakeTimeout.",
52
+ "source": "https://bun.com/docs/runtime/nodejs-compat"
53
+ },
54
+ {
55
+ "name": "inspector",
56
+ "status": "partial",
57
+ "note": "Session supports Profiler, Runtime.enable and NodeTracing; Debugger commands are forwarded but results are not returned; Runtime.evaluate and HeapProfiler are unimplemented and there is no Network domain.",
58
+ "source": "https://bun.com/docs/runtime/nodejs-compat"
59
+ },
60
+ {
61
+ "name": "module",
62
+ "status": "partial",
63
+ "note": "Module#load(), registerHooks, findPackageJSON, stripTypeScriptTypes and source-map support APIs are missing; some methods are no-ops.",
64
+ "source": "https://bun.com/docs/runtime/nodejs-compat"
65
+ },
66
+ {
67
+ "name": "perf_hooks",
68
+ "status": "partial",
69
+ "note": "monitorEventLoopDelay, createHistogram, timerify and PerformanceObserver are implemented; gc/dns/resource entries are never emitted and eventLoopUtilization() returns zeros.",
70
+ "source": "https://bun.com/docs/runtime/nodejs-compat"
71
+ },
72
+ {
73
+ "name": "process",
74
+ "status": "partial",
75
+ "note": "process.binding is only partially available; setting process.title is a no-op on macOS/Linux; several methods return empty arrays; sourceMapsEnabled and addUncaughtExceptionCaptureCallback are missing.",
76
+ "source": "https://bun.com/docs/runtime/nodejs-compat"
77
+ },
78
+ {
79
+ "name": "repl",
80
+ "status": "partial",
81
+ "note": "Mostly implemented via bun --interactive; there are no result previews and tab-completion skips let/const/class bindings.",
82
+ "source": "https://bun.com/docs/runtime/nodejs-compat"
83
+ },
84
+ {
85
+ "name": "test",
86
+ "status": "partial",
87
+ "note": "The in-process API works under bun test; run() is limited and reporters, snapshots, coverage, --test-only and the CLI runner mode are missing.",
88
+ "source": "https://bun.com/docs/runtime/nodejs-compat"
89
+ },
90
+ {
91
+ "name": "tls",
92
+ "status": "partial",
93
+ "note": "pskCallback, OCSP stapling and cross-process session resumption are missing; renegotiate() always fails because the TLS layer is BoringSSL.",
94
+ "source": "https://bun.com/docs/runtime/nodejs-compat"
95
+ },
96
+ {
97
+ "name": "v8",
98
+ "status": "partial",
99
+ "note": "Heap snapshots/statistics and GCProfiler are implemented; serialize/deserialize use JSC's wire format; queryObjects, CPU/heap profiles, Serializer/Deserializer and promiseHooks are missing.",
100
+ "source": "https://bun.com/docs/runtime/nodejs-compat"
101
+ },
102
+ {
103
+ "name": "wasi",
104
+ "status": "partial",
105
+ "note": "args, env, preopens, wasiImport and start() work; getImportObject(), initialize() and sock_accept are missing and several options are ignored.",
106
+ "source": "https://bun.com/docs/runtime/nodejs-compat"
107
+ },
108
+ {
109
+ "name": "worker_threads",
110
+ "status": "partial",
111
+ "note": "resourceLimits and trackUnmanagedFds are ignored; some execArgv flags are only partly applied; moveMessagePortToContext and locks are missing.",
112
+ "source": "https://bun.com/docs/runtime/nodejs-compat"
113
+ }
114
+ ]
6
115
  }
@@ -25,11 +25,16 @@ export function lifecycleScriptFindings(
25
25
  lockfile: ParsedLockfile | undefined,
26
26
  ): Finding[] {
27
27
  const trusted = new Set(snapshot.manifest.trustedDependencies);
28
- const evidence = new Map<string, string>();
28
+ const evidence = new Map<string, { where: string; optional: boolean }>();
29
29
 
30
30
  for (const pkg of lockfile?.packages ?? []) {
31
31
  if (pkg.installScript) {
32
- evidence.set(pkg.name, `the lockfile marks ${pkg.name} as requiring a build step`);
32
+ evidence.set(pkg.name, {
33
+ where: `the lockfile marks ${pkg.name} as requiring a build step`,
34
+ // An optional dependency is tolerated absent by design, so a skipped
35
+ // install script can never break the install itself (e.g. fsevents).
36
+ optional: pkg.optional,
37
+ });
33
38
  }
34
39
  }
35
40
 
@@ -38,12 +43,12 @@ export function lifecycleScriptFindings(
38
43
  continue;
39
44
  }
40
45
  const scripts = probe.installScripts.map((name) => `"${name}"`).join(", ");
41
- evidence.set(probe.name, `${probe.path} declares ${scripts}`);
46
+ evidence.set(probe.name, { where: `${probe.path} declares ${scripts}`, optional: false });
42
47
  }
43
48
 
44
49
  return [...evidence.entries()]
45
50
  .sort((a, b) => a[0].localeCompare(b[0]))
46
- .map(([name, where]) => {
51
+ .map(([name, { where, optional }]) => {
47
52
  if (trusted.has(name)) {
48
53
  return {
49
54
  id: ID,
@@ -55,6 +60,19 @@ export function lifecycleScriptFindings(
55
60
  evidence: where,
56
61
  };
57
62
  }
63
+ if (optional) {
64
+ return {
65
+ id: ID,
66
+ severity: "risk" as const,
67
+ title: `${name} is an optional dependency whose install script will not run`,
68
+ package: name,
69
+ detail:
70
+ "Bun installs dependencies without running their lifecycle scripts unless the package is listed in trustedDependencies. Because this package is optional, the install still succeeds without it - but if it does install on this platform and needs its build step, it will be broken.",
71
+ evidence: where,
72
+ hint: `check whether ${name} is actually used on this platform; if it is, add it to trustedDependencies in package.json and reinstall.`,
73
+ source: LIFECYCLE_DOC,
74
+ };
75
+ }
58
76
  return {
59
77
  id: ID,
60
78
  severity: "blocker" as const,
@@ -121,9 +121,11 @@ export function runtimeBuiltinFindings(
121
121
  if (usage === undefined) {
122
122
  continue;
123
123
  }
124
+ // A `partial` module usually still runs; an `unimplemented` module breaks
125
+ // on import with no user action, which is the blocker bar (D2).
124
126
  findings.push({
125
127
  id: GAP_ID,
126
- severity: "risk",
128
+ severity: gap.status === "unimplemented" ? "blocker" : "risk",
127
129
  title: `${gap.name} is ${gap.status} in Bun and this project imports it`,
128
130
  detail: gap.note,
129
131
  evidence: `${usage.files.length} file(s), first at ${usage.files[0] ?? "unknown"}`,
@@ -270,7 +270,9 @@ export function classifySpecifier(
270
270
  if (specifier.startsWith("bun:")) {
271
271
  return "bun-builtin";
272
272
  }
273
- if (builtins.has(specifier)) {
273
+ // A `node:` prefix is unambiguous even when the runtime's own module list
274
+ // lags behind Node (e.g. node:sea is absent from Bun's builtinModules).
275
+ if (specifier.startsWith("node:") || builtins.has(specifier)) {
274
276
  return "node-builtin";
275
277
  }
276
278
  if (