@mh-alikhani/bunready 0.2.0 → 0.3.1

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,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  Nothing yet.
11
11
 
12
+ ## [0.3.1] - 2026-09-19
13
+
14
+ ### Fixed
15
+
16
+ - Three polynomial-time regular expressions, reported by CodeQL as
17
+ `js/polynomial-redos`. bunready parses files it does not control - a
18
+ `pnpm-workspace.yaml` from a repository being scanned, a semver range in its
19
+ `engines` - so a pathological string could have made a scan take quadratic
20
+ time. The pnpm list item and the semver operator split are now parsed
21
+ directly, and trailing-slash stripping is a linear loop instead of `/+$`.
22
+ Behaviour is unchanged; `tests/workspaces.test.ts` and `tests/semver.test.ts`
23
+ cover the affected paths.
24
+
25
+ ## [0.3.0] - 2026-09-18
26
+
27
+ ### Added
28
+
29
+ - A programmatic entry point (`src/index.ts`, wired to `main` and `exports`), so
30
+ the scanner can be driven from a script or a test instead of a child process -
31
+ and so npm can resolve the package instead of reporting that it cannot guess an
32
+ entry point.
33
+ - `docs/brand/logo-card.svg`: a lockup that carries its own surface, used as the
34
+ README fallback where `<picture>` is stripped (npm). An SVG loaded through
35
+ `<img>` resolves `prefers-color-scheme` against the reader's operating system
36
+ rather than against the page, so a transparent lockup can render
37
+ light-on-white.
38
+
39
+ ### Fixed
40
+
41
+ - The README logo is legible on npm in both colour schemes.
42
+ - `tests/args-flags.test.ts` compared an expression with itself, which CodeQL
43
+ reported as a redundant operation (`js/redundant-operation`).
44
+
45
+ ### Docs
46
+
47
+ - Changelog link references for 0.2.0 and 0.3.0 were missing.
48
+
12
49
  ## [0.2.0] - 2026-09-16
13
50
 
14
51
  ### Added
@@ -164,5 +201,8 @@ Nothing yet.
164
201
  and that claim needs a primary source. Until then the rule reports what the
165
202
  repository imports and cites the compatibility table.
166
203
 
167
- [Unreleased]: https://github.com/MHAlikhani/bunready/compare/v0.1.0...HEAD
204
+ [Unreleased]: https://github.com/MHAlikhani/bunready/compare/v0.3.1...HEAD
205
+ [0.3.1]: https://github.com/MHAlikhani/bunready/releases/tag/v0.3.1
206
+ [0.3.0]: https://github.com/MHAlikhani/bunready/releases/tag/v0.3.0
207
+ [0.2.0]: https://github.com/MHAlikhani/bunready/releases/tag/v0.2.0
168
208
  [0.1.0]: https://github.com/MHAlikhani/bunready/releases/tag/v0.1.0
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  <picture>
4
4
  <source media="(prefers-color-scheme: dark)" srcset="docs/brand/logo-dark.svg">
5
5
  <source media="(prefers-color-scheme: light)" srcset="docs/brand/logo.svg">
6
- <img src="docs/brand/logo-auto.svg" alt="bunready" width="360">
6
+ <img src="docs/brand/logo-card.svg" alt="bunready" width="360">
7
7
  </picture>
8
8
 
9
9
  **One command that tells you what will break before you move a Node/TS repo to Bun — and gives you one clear verdict.**
@@ -0,0 +1,20 @@
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
+ The lockup for hosts that give us no background guarantee.
5
+ npm renders a README without <picture>, and an SVG loaded through <img>
6
+ resolves prefers-color-scheme against the reader's operating system rather
7
+ than the page - so a transparent lockup can end up light-on-white. This one
8
+ carries its own surface, which is why it is the fallback rather than the
9
+ default.
10
+ -->
11
+ <rect x="0.5" y="0.5" width="339" height="119" rx="18" fill="#171a1f" stroke="#3a3f4a"/>
12
+ <g fill="none" stroke-linecap="round" stroke-linejoin="round">
13
+ <rect x="34" y="34" width="52" height="52" rx="15" stroke="#e0a268" stroke-width="3"/>
14
+ <path d="M53 49 L68 60 L53 71" stroke="#fbfaf7" stroke-width="4.5"/>
15
+ <path d="M54 77 H68" stroke="#fbfaf7" stroke-width="4.5"/>
16
+ </g>
17
+ <text x="108" y="76"
18
+ font-family="ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif"
19
+ font-size="42" font-weight="620" letter-spacing="-1" fill="#f2f1ec">bunready</text>
20
+ </svg>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mh-alikhani/bunready",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
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",
@@ -27,6 +27,12 @@
27
27
  "author": "Mohammad Hosein Alikhani",
28
28
  "type": "module",
29
29
  "private": false,
30
+ "main": "./src/index.ts",
31
+ "types": "./src/index.ts",
32
+ "exports": {
33
+ ".": "./src/index.ts",
34
+ "./package.json": "./package.json"
35
+ },
30
36
  "bin": {
31
37
  "bunready": "./src/cli/index.ts"
32
38
  },
package/src/index.ts ADDED
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Programmatic entry point.
3
+ *
4
+ * bunready is a CLI first. This module exists so the same checks can be driven
5
+ * from a script or a test without shelling out to a process, and so the package
6
+ * has a real entry point instead of only a `bin` - npm cannot resolve a package
7
+ * that declares neither `main` nor `exports`.
8
+ *
9
+ * Nothing here is re-exported by accident; if a name is in this file, it is part
10
+ * of the package's public surface.
11
+ */
12
+
13
+ export { type CliOptions, parseArgs } from "./cli/args";
14
+ export { run } from "./cli/run";
15
+ export { createTheme, type Theme } from "./cli/theme";
16
+ export {
17
+ applyBaseline,
18
+ type Baseline,
19
+ type BaselineSummary,
20
+ fingerprint,
21
+ parseBaseline,
22
+ serializeBaseline,
23
+ } from "./config/baseline";
24
+ export {
25
+ type BunreadyConfig,
26
+ CONFIG_FILENAME,
27
+ DEFAULT_CONFIG,
28
+ parseConfig,
29
+ type RunConfig,
30
+ } from "./config/config";
31
+ export {
32
+ type BunreadyError,
33
+ defineError,
34
+ type ErrorCode,
35
+ err,
36
+ formatError,
37
+ isErr,
38
+ isOk,
39
+ ok,
40
+ type Result,
41
+ } from "./core/errors";
42
+ export { type DirectoryEntry, type FileSystem, nodeFileSystem, type ReadOutcome } from "./core/fs";
43
+ export { TOOL_NAME, TOOL_VERSION } from "./core/version";
44
+ export { renderHumanReport } from "./report/human";
45
+ export { renderJsonReport } from "./report/json";
46
+ export { renderSarifReport } from "./report/sarif";
47
+ export {
48
+ type Finding,
49
+ type RunSummary,
50
+ SCHEMA_VERSION,
51
+ type ScannedTarget,
52
+ type ScanReport,
53
+ type ScanStats,
54
+ sortFindings,
55
+ type Verdict,
56
+ verdictFor,
57
+ } from "./report/types";
58
+ export { installFindings } from "./rules/install";
59
+ export { runFindings } from "./rules/run";
60
+ export { runtimeFindings } from "./rules/runtime";
61
+ export {
62
+ type BuiltinUsage,
63
+ collectNodeBuiltins,
64
+ readRuntimeDataset,
65
+ } from "./rules/runtime/builtins";
66
+ export {
67
+ countBySeverity,
68
+ exitCodeForFindings,
69
+ exitCodeForSeverities,
70
+ SEVERITIES,
71
+ type Severity,
72
+ } from "./rules/severity";
73
+ export {
74
+ executeProject,
75
+ firstFailure,
76
+ type RunEnvironment,
77
+ type RunOptions,
78
+ type RunOutcome,
79
+ systemRunEnvironment,
80
+ } from "./scanner/execute";
81
+ export { buildGraph, type DependencyGraph, knownPackageNames } from "./scanner/graph";
82
+ export {
83
+ LOCKFILE_FILENAMES,
84
+ type LockedPackage,
85
+ type ParsedLockfile,
86
+ parseLockfile,
87
+ } from "./scanner/lockfile";
88
+ export { type Manifest, parseManifest } from "./scanner/manifest";
89
+ export { detectRuntime, type ScanOptions, scanTarget } from "./scanner/scan";
90
+ export { classifySpecifier, extractImports, type SourceScan, scanSources } from "./scanner/sources";
91
+ export {
92
+ findWorkspacePackages,
93
+ type WorkspacePackage,
94
+ workspacePatterns,
95
+ } from "./scanner/workspaces";
@@ -92,12 +92,12 @@ function comparatorsForToken(token: string): Comparator[] | undefined {
92
92
  return [];
93
93
  }
94
94
 
95
- const operatorMatch = /^(>=|<=|>|<|=|\^|~)?\s*(.*)$/.exec(trimmed);
96
- if (operatorMatch === null) {
97
- return undefined;
98
- }
99
- const operator = operatorMatch[1];
100
- const rest = operatorMatch[2] ?? "";
95
+ // Anchored-prefix parse rather than a trailing \s*(.*)$, which backtracks
96
+ // quadratically on a long run of spaces - and the range comes from a
97
+ // repository bunready does not control.
98
+ const operatorMatch = /^(>=|<=|>|<|=|\^|~)?/.exec(trimmed);
99
+ const operator = operatorMatch?.[1];
100
+ const rest = trimmed.slice(operatorMatch?.[0].length ?? 0).trim();
101
101
  if (rest === "" || rest === "*" || rest === "x") {
102
102
  return [];
103
103
  }
@@ -54,9 +54,22 @@ export function patternsFromPnpmWorkspace(text: string): string[] {
54
54
  if (!inPackages) {
55
55
  continue;
56
56
  }
57
- const entry = /^-+\s*(.+?)\s*$/.exec(trimmed);
58
- if (entry?.[1] !== undefined) {
59
- patterns.push(entry[1].replace(/^['"]|['"]$/g, ""));
57
+ // Walked rather than matched: a pattern mixing a dash run, a space run and a
58
+ // catch-all is ambiguous, and CodeQL is right that it backtracks on a string
59
+ // of dashes and spaces. The value is trimmed here instead.
60
+ let cursor = 0;
61
+ while (cursor < trimmed.length && trimmed[cursor] === "-") {
62
+ cursor += 1;
63
+ }
64
+ const value =
65
+ cursor === 0
66
+ ? ""
67
+ : trimmed
68
+ .slice(cursor)
69
+ .trim()
70
+ .replace(/^['"]|['"]$/g, "");
71
+ if (value !== "") {
72
+ patterns.push(value);
60
73
  }
61
74
  }
62
75
 
@@ -123,6 +136,15 @@ async function expandGlob(root: string, segments: string[], fs: FileSystem): Pro
123
136
  return prefixes.filter((prefix) => prefix !== "" && prefix !== ".");
124
137
  }
125
138
 
139
+ /** Linear replacement for `/+$`, which backtracks on a run of slashes. */
140
+ function stripTrailingSlashes(value: string): string {
141
+ let end = value.length;
142
+ while (end > 0 && value[end - 1] === "/") {
143
+ end -= 1;
144
+ }
145
+ return value.slice(0, end);
146
+ }
147
+
126
148
  export async function findWorkspacePackages(
127
149
  root: string,
128
150
  patterns: readonly string[],
@@ -131,7 +153,7 @@ export async function findWorkspacePackages(
131
153
  const found = new Map<string, WorkspacePackage>();
132
154
 
133
155
  for (const pattern of patterns) {
134
- const cleaned = pattern.replace(/\\/g, "/").replace(/\/+$/, "").replace(/^\.\//, "");
156
+ const cleaned = stripTrailingSlashes(pattern.replace(/\\/g, "/")).replace(/^\.\//, "");
135
157
  if (cleaned === "" || cleaned === ".") {
136
158
  continue;
137
159
  }
@@ -165,6 +187,6 @@ export async function readPnpmWorkspace(root: string, fs: FileSystem): Promise<s
165
187
 
166
188
  /** Keep only the packages matching a scope string, by relative path or name. */
167
189
  export function scopeMatches(relative: string, scope: string): boolean {
168
- const needle = scope.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
190
+ const needle = stripTrailingSlashes(scope.replace(/\\/g, "/")).replace(/^\.\//, "");
169
191
  return relative === needle || relative.includes(needle);
170
192
  }