Repository navigation
Add bun pm sbom command - #29512
Add bun pm sbom command#29512robobun wants to merge 1 commit into
bun pm sbom command#29512Conversation
|
Updated 8:53 PM PT - Aug 17th, 2026
✅ @robobun, your commit c623d42767dc32d92308b7ec4a00cb5bf7fb4bf5 passed in 🧪 To try this PR locally: bunx bun-pr 29512That installs a local version of the PR into your bun-29512 --bun |
|
Found 1 issue this PR may fix:
🤖 Generated with Claude Code |
ef8b96f to
75a7827
Compare
StatusReady for maintainer review at c623d42, a single commit rebased onto current main (latest rebase: two keep-both conflicts with Verified with Rebase details are in the PR description. Closes #8483. |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdds a new Changes
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/cli/pm_sbom_command.zig`:
- Around line 365-388: The function collectDeps declares unused parameters
deps_buf and pkg_dependencies (they are immediately discarded with `_ =`) —
either remove these parameters from collectDeps' signature and update every call
site to stop passing those arguments, or, if intended, implement the missing
logic to use pkg_dependencies and deps_buf (e.g., filter resolved_id by
consulting pkg_dependencies and map via deps_buf) inside collectDeps; update the
symbol names mentioned (collectDeps, deps_buf, pkg_dependencies,
comp.deps.append) accordingly so callers and internal logic stay consistent.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: d795fc1d-65eb-4fda-887b-426caa6f4a0d
📒 Files selected for processing (5)
src/cli/package_manager_command.zigsrc/cli/pm_sbom_command.zigsrc/install/PackageManager/CommandLineArguments.zigsrc/install/PackageManager/PackageManagerOptions.zigtest/cli/install/bun-pm-sbom.test.ts
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/cli/pm_sbom_command.zig`:
- Around line 76-78: The writeAll call currently swallows all errors via
`Output.writer().writeAll(output) catch {}`, so change it to explicitly handle
errors: capture the result, if the error is the broken-pipe EPIPE (e.g., compare
against `error.EPIPE` or the platform-specific EPIPE constant) then ignore it,
otherwise log the error or propagate/return it (for example use `|err| { if (err
== error.EPIPE) {} else std.debug.print("stdout write failed: {}\n", .{err});
}`). Update the block around the `Output.writer().writeAll(output)` invocation
to implement this explicit branching so non-EPIPE I/O failures are not silently
discarded while preserving the common broken-pipe behavior.
- Around line 332-340: The scope selection for packages (using pkg_flags[idx],
flags, and Scope) misclassifies packages with optional=true and dev=true as
.required; update the conditional order so that after checking for required and
the dev-only excluded case you check if flags.optional and !flags.required and
return .optional (i.e., add a branch evaluating flags.optional and
!flags.required before the final else), leaving the existing .dev-only
(.excluded) and default .required branches intact so packages with only optional
non-dev edges are marked .optional.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 8131f36e-636e-4aae-8b29-bfb08554f5ed
📥 Commits
Reviewing files that changed from the base of the PR and between 75a7827 and aa6b852c0115f0935430f92c0fefa163e4ef9efa.
📒 Files selected for processing (1)
src/cli/pm_sbom_command.zig
There was a problem hiding this comment.
♻️ Duplicate comments (2)
src/cli/pm_sbom_command.zig (2)
76-78:⚠️ Potential issue | 🟡 MinorDo not silently swallow stdout write failures.
catch {}masks all output errors, which can hide real write failures in redirected/CI environments.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/cli/pm_sbom_command.zig` around lines 76 - 78, The write to stdout is silently ignored by the `Output.writer().writeAll(output) catch {}` which can hide real failures; replace the empty catch with proper error handling—either propagate the error with `try Output.writer().writeAll(output);` or handle and report it (e.g., log to stderr or return the error) inside the `pm_sbom_command` flow so write failures are not swallowed. Ensure you update any surrounding function signature to propagate the error if you choose `try`.
337-344:⚠️ Potential issue | 🟠 MajorFix scope classification when a package has both dev and optional incoming edges.
The current branch order marks
optional=true+dev=true+required=falseas.required, but your own rule says it should be.optionalwhen every non-dev edge is optional.♻️ Proposed fix
- const scope: Scope = if (flags.required or (res.tag == .root or res.tag == .workspace)) + const scope: Scope = if (flags.required or (res.tag == .root or res.tag == .workspace)) .required - else if (flags.optional and !flags.dev) - .optional else if (flags.dev and !flags.optional and !flags.required) .excluded + else if (flags.optional and !flags.required) + .optional else .required;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/cli/pm_sbom_command.zig` around lines 337 - 344, The scope classification logic in the const declaration for scope incorrectly falls through to .required when flags.optional and flags.dev are true but flags.required is false; to fix, update the branch order/conditions in the expression that sets scope (the const scope: Scope = ... using flags and res.tag) so that after the initial required check (flags.required or res.tag == .root or .workspace) you check flags.optional (e.g., else if flags.optional => .optional) before the dev-only check, then keep the dev-exclusive branch (flags.dev and !flags.optional and !flags.required => .excluded) and the fallback; adjust only the condition ordering in that const expression to ensure optional wins when every non-dev edge is optional.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@src/cli/pm_sbom_command.zig`:
- Around line 76-78: The write to stdout is silently ignored by the
`Output.writer().writeAll(output) catch {}` which can hide real failures;
replace the empty catch with proper error handling—either propagate the error
with `try Output.writer().writeAll(output);` or handle and report it (e.g., log
to stderr or return the error) inside the `pm_sbom_command` flow so write
failures are not swallowed. Ensure you update any surrounding function signature
to propagate the error if you choose `try`.
- Around line 337-344: The scope classification logic in the const declaration
for scope incorrectly falls through to .required when flags.optional and
flags.dev are true but flags.required is false; to fix, update the branch
order/conditions in the expression that sets scope (the const scope: Scope = ...
using flags and res.tag) so that after the initial required check
(flags.required or res.tag == .root or .workspace) you check flags.optional
(e.g., else if flags.optional => .optional) before the dev-only check, then keep
the dev-exclusive branch (flags.dev and !flags.optional and !flags.required =>
.excluded) and the fallback; adjust only the condition ordering in that const
expression to ensure optional wins when every non-dev edge is optional.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: f4efe230-88fd-4e71-a809-095c3179c381
📥 Commits
Reviewing files that changed from the base of the PR and between aa6b852c0115f0935430f92c0fefa163e4ef9efa and 832ef7f443f5da2979d83a726c1a236863c601e3.
📒 Files selected for processing (4)
src/cli/package_manager_command.zigsrc/cli/pm_sbom_command.zigsrc/install/PackageManager/CommandLineArguments.zigsrc/install/PackageManager/PackageManagerOptions.zig
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
src/cli/pm_sbom_command.zig (1)
76-78:⚠️ Potential issue | 🟠 MajorDo not treat every stdout write failure as success.
catch {}hides real I/O failures likeENOSPCorEBADF, so callers can get exit code 0 even though no SBOM was written. Ignoring broken pipes is fine, but other write errors should still fail the command.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/cli/pm_sbom_command.zig` around lines 76 - 78, The current Output.writer().writeAll(output) call swallows all write errors via catch {}; change it to explicitly handle broken-pipe errors only and propagate any other I/O failures so the command can fail. Replace the silent catch with a catcher that matches the write error (capture the error from Output.writer().writeAll(output) catch |err| { ... }), ignore only the broken pipe/EPIPE case (compare against std.os.EPIPE or the framework's broken-pipe error) and otherwise return or propagate the error (so the process exits non-zero); keep the write call and the output variable as-is.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/cli/pm_sbom_command.zig`:
- Around line 238-255: The cloned name returned by
json.getStringCloned(allocator, "name") is being duplicated again with
allocator.dupe for this.root.name, leaking the original clone; modify the branch
that sets root_name so that when json.getStringCloned(...) returns a value you
either (A) assign that owned buffer directly to this.root.name and avoid the
later allocator.dupe, or (B) if you must keep the pattern of using root_name,
free the original clone after creating the duplicate (use defer or explicit
free) so ownership is balanced; update construction of root_ref/this.root to use
the chosen owned buffer and ensure deinit() can free it unconditionally.
- Around line 329-334: The current uniqueness check uses the raw ref but
SpdxIdFormatter later sanitizes refs, so generate a sanitized SPDX id (using the
same sanitation logic as SpdxIdFormatter) for each ref (including the root
package) before inserting into seen_refs and before serialization; if the
sanitized id already exists, append a disambiguator like "~{idx}" to the
sanitized id (allocating with allocator as done for unique), free any replaced
allocations, then insert the sanitized id into seen_refs and use that sanitized
identifier for output. Apply the same fix in both places referenced (the shown
block around the ref handling and the similar block at 665-677) so collisions
are prevented after sanitization.
---
Duplicate comments:
In `@src/cli/pm_sbom_command.zig`:
- Around line 76-78: The current Output.writer().writeAll(output) call swallows
all write errors via catch {}; change it to explicitly handle broken-pipe errors
only and propagate any other I/O failures so the command can fail. Replace the
silent catch with a catcher that matches the write error (capture the error from
Output.writer().writeAll(output) catch |err| { ... }), ignore only the broken
pipe/EPIPE case (compare against std.os.EPIPE or the framework's broken-pipe
error) and otherwise return or propagate the error (so the process exits
non-zero); keep the write call and the output variable as-is.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: cdf93f2d-f318-4f8b-b549-c3406a602a38
📥 Commits
Reviewing files that changed from the base of the PR and between 832ef7f443f5da2979d83a726c1a236863c601e3 and 8cf31778d29d083666c891929fab30eae6fd2979.
📒 Files selected for processing (1)
src/cli/pm_sbom_command.zig
8cf3177 to
26a5d1c
Compare
|
✅ No merge conflicts detected when merging into Your branch is good to go! |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/cli/pm_sbom_command.zig`:
- Around line 666-682: The current writeSPDXRelationships function (inside
writeSPDXRelationships and the relationshipType block) stops at the first
matching raw dependency entry and returns that edge's relationship, making
output input-order dependent; instead, scan all deps/resolved pairs for the
given dep_id (using pkg_dep_resolutions, pkg_deps and the for (deps, resolved)
loop) and accumulate flags for whether any matching entry is required, optional,
or dev, then pick the relationship by precedence (required -> DEPENDS_ON,
optional -> OPTIONAL_DEPENDENCY_OF, dev -> DEV_DEPENDENCY_OF); replace the early
breaks with logic that checks all matches and sets rel_type accordingly so
duplicate resolved_ids yield the correct aggregated relationship (consistent
with collectDeps/component scope).
- Around line 345-362: The current single contains() check for seen_refs and
seen_spdx_ids can still produce collisions because the first disambiguated
candidate may already exist; change both branches (the ref disambiguation and
the spdx_id disambiguation around sanitizeSpdxId) to loop-generating new
candidates until a unique value is produced (e.g., try "{s}~{d}" or "{s}.{d}"
with an incrementing suffix) and only call try seen_refs.put(ref) / try
seen_spdx_ids.put(spdx_id) once the final unique string is chosen; ensure each
intermediate allocated string (ref or spdx_id and any generated unique
candidate) is freed via allocator.free() when it is replaced to avoid leaks, and
keep using allocator, idx, seen_refs, seen_spdx_ids, sanitizeSpdxId, ref, and
spdx_id to locate the changes.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: e48ece5c-015c-493a-a511-945a869e9037
📥 Commits
Reviewing files that changed from the base of the PR and between 8cf31778d29d083666c891929fab30eae6fd2979 and 26a5d1c.
📒 Files selected for processing (1)
src/cli/pm_sbom_command.zig
c7cfa9f to
2a72ba0
Compare
There was a problem hiding this comment.
All prior review feedback is addressed and I didn't find new issues, but this is a ~800-line new user-facing feature implementing two external SBOM specs (CycloneDX 1.7 / SPDX 2.3) with several design choices made along the way (whole-lockfile vs workspace-scoped output, BFS scope semantics, global --format/-o flags on bun pm), plus a shared test/harness.ts change — worth a human pass before merge.
Extended reasoning...
Overview
This PR adds bun pm sbom, a new CLI subcommand that generates a Software Bill of Materials from the lockfile in CycloneDX 1.7 or SPDX 2.3 JSON. The bulk of the change is the new src/cli/pm_sbom_command.zig (~790 lines) containing a Generator that walks the lockfile, runs a BFS from the root to classify each package's scope (required/optional/excluded), deduplicates bom-refs and SPDXIDs, and serializes to either format. Wiring changes in package_manager_command.zig, CommandLineArguments.zig, and PackageManagerOptions.zig are small and mechanical. A ~400-line integration test exercises both formats against a Verdaccio registry. test/harness.ts is modified to bind Verdaccio on 127.0.0.1 explicitly (affects all tests using VerdaccioRegistry, not just this one).
Security risks
No injection/auth/permission surface — the command is read-only over the lockfile and writes to a user-specified path. However, SBOMs are a supply-chain security artifact: downstream scanners (Grype, Trivy, Dependency-Track) will rely on the scope/relationship fields to decide which vulnerabilities to surface, so semantic correctness here has security implications even though the code itself isn't security-sensitive.
Level of scrutiny
This warrants human review. It is a brand-new public CLI command implementing two external specifications, and it went through ~15 rounds of bot-driven fixes during review — including two rewrites of the scope-classification logic (per-edge → BFS → BFS-from-lockfile-root), an arena-allocator refactor, and a late fix for -o cwd resolution. Several design decisions were made without human input: always describing the whole lockfile regardless of cwd (vs. a workspace-scoped SBOM), adding --format/-o to the shared bun pm clap param set, and the precise scope/relationship precedence rules. Someone familiar with how Bun wants to position this against npm sbom/cyclonedx-npm should sanity-check those choices and ideally spot-check the output against a real scanner.
Other factors
Test coverage is solid (12 tests covering both formats, scope propagation, workspaces, subdir invocation, -o resolution, error cases). All inline review threads are resolved. The test/harness.ts change is reasonable but touches shared test infrastructure used across the install test suite, which is another reason for a human glance.
023fc1c to
5826fa1
Compare
f01be16 to
de7358d
Compare
There was a problem hiding this comment.
I reviewed this PR and the automated pass found no bugs — all earlier nits (in-tree hex/date helpers, optional-dependency coverage, over-long -o handling, SPDX §7.7 VCS locators, the orphaned doc comment, and describe.concurrent) are addressed through 8bfa0d8. Because this adds a new user-facing subcommand with two spec-defined output formats and re-widens several bun_install items to pub, a maintainer look at the API surface would still be worthwhile.
What was reviewed: scope-propagation BFS and the Required > Optional > Excluded lattice; bom-ref/SPDXID dedup and the sanitizer charset; purl percent-encoding and vcs_locator against SPDX 2.3 §7.7; -o path resolution via join_abs_string_buf_checked; and test hermeticity (Verdaccio + local GITHUB_API_URL server, per-test temp dirs, concurrent describe).
Extended reasoning...
Overview
This PR adds bun pm sbom, a new subcommand that emits a Software Bill of Materials from the lockfile in CycloneDX 1.7 or SPDX 2.3 JSON. The core is a new ~1000-line src/runtime/cli/pm_sbom_command.rs that walks the lockfile, computes per-package scope via a strongest-path BFS, builds Component records (name/version/purl/download URL/integrity/deps), and hand-serializes both formats. It is wired into the pm dispatcher, CLI argument parser, options struct, help text, docs, and shell completions. To read lockfile internals cross-crate it widens Resolution::repository(), the Resolution::Tag constants, Integrity::slice() + its SHA tags, and Meta.integrity from pub(crate) to pub. shell/builtin/ls.rs::civil_from_days becomes pub(crate) so the timestamp helper can be reused rather than duplicated. A 486-line test file exercises both formats via Verdaccio, workspaces, git/github deps served locally, error paths, and CLI edge cases.
Security risks
Low. The command reads a trusted local lockfile and emits JSON; there is no network I/O in the command itself. The one user-controlled write path (-o <path>) now uses join_abs_string_buf_checked and exits cleanly on overflow rather than panicking. JSON string values are escaped via bun_core::fmt::format_json_string_utf8, so lockfile-derived names/URLs cannot break out of string context. purl encoding percent-escapes outside the RFC 3986 unreserved set. No auth, crypto, or permission logic is touched.
Level of scrutiny
High — this is a new user-facing API surface. Per the repo's landing-PR guidance, API additions call for maintainer agreement on shape: whether CycloneDX 1.7 / SPDX 2.3 are the right targets, whether the flag spellings (--format, -o/--outfile) match sibling commands, whether the pub(crate)→pub widenings in bun_install are acceptable long-term, and whether hand-rolled JSON serialization is preferred over a schema-checked emitter. The implementation itself has been through several review iterations and looks sound, but these are design calls a bot should not sign off on unilaterally.
Other factors
Test coverage is solid after the review-driven additions: required/optional/excluded scopes, transitive-dev vs mixed prod+dev paths, scoped-package purl encoding, workspaces (including running from a subdir with relative -o), git+file:// and github: deps via a local tarball server, unknown --format, missing lockfile, over-long -o, and the help-text entry. Tests are hermetic (no public network) and now run under describe.concurrent. The finder-flagged "ported from" trailer comment referencing a deleted Zig file is cosmetic and was ruled out. All prior inline threads on this PR are resolved.
There was a problem hiding this comment.
I reviewed this PR and the bug-hunting pass found no issues; all earlier review threads have been addressed. Since this adds a new user-facing CLI command (bun pm sbom) with ~1000 lines of new Rust plus visibility widening in bun_install, a maintainer sign-off on the API surface and SBOM format choices would still be worthwhile.
Checked: scope-propagation BFS and SPDX *_OF edge direction against the test assertions; -o path handling now uses join_abs_string_buf_checked (no panic on long argv); git/github downloadLocation now emits SPDX §7.7 VCS locators; hex/timestamp/percent-encoding reuse in-tree bun_core::fmt and civil_from_days; visibility changes in integrity.rs/resolution.rs/Meta.rs are read-only accessors matching what the command consumes.
Extended reasoning...
Overview
This PR adds bun pm sbom, a new CLI subcommand that reads the lockfile and emits a Software Bill of Materials in CycloneDX 1.7 or SPDX 2.3 JSON. The core is a ~1000-line new file src/runtime/cli/pm_sbom_command.rs implementing a Generator that walks the lockfile's package list, computes per-package scope via BFS from the root, builds Component records (bom-ref, SPDXID, purl, download URL, integrity hash, dep edges), and hand-serializes both output formats. Supporting changes wire the subcommand into package_manager_command.rs, add --format/--outfile options to CommandLineArguments/PackageManagerOptions, widen pub(crate) → pub on the Resolution tags, Resolution::repository(), Integrity::slice()/tag constants, and Meta.integrity so the runtime crate can read them, expose civil_from_days from shell/builtin/ls.rs as pub(crate), and add docs/completions/help text. A 486-line Verdaccio-backed test file covers both formats, scope propagation (required/optional/excluded, mixed-path), workspaces, git/github locators, -o handling, and error cases.
Security risks
Low. The command is read-only against the lockfile and writes only to the user-specified -o path (or stdout). Input surface is CLI argv and lockfile contents. The -o path now goes through join_abs_string_buf_checked and exits cleanly on overflow rather than panicking. JSON output escapes all lockfile-derived strings via bun_core::fmt::format_json_string_utf8, and SPDXID generation sanitizes to [A-Za-z0-9.-] with collision-dedup. No network, no auth, no crypto beyond reading the stored integrity digest. The vcs_locator helper reads repo.resolved/repo.committish from the lockfile string buffer with bounds derived from stored String handles. The visibility widening exposes only read-only data (union tag constants, a slice getter, a Copy field) — no new mutation surface.
Level of scrutiny
This warrants human review. It is a net-new user-facing feature with API-design decisions a maintainer should ratify: the CLI flag names (--format, -o/--outfile), the default format (CycloneDX 1.7), the SBOM field mappings (e.g. dev deps → scope: "excluded" / DEV_DEPENDENCY_OF, whole-lockfile scope even from a workspace subdir), and the choice to hand-serialize JSON rather than use a schema library. REVIEW.md's referenced landing-prs.md specifically calls out API design as requiring a targeted read before adding user-facing surface. The visibility widening in bun_install is a small architectural change that a maintainer should acknowledge. None of this is risky per se — it's additive, well-tested, and follows the sibling-command patterns (pm_why_command.rs, audit_command.rs) — but ~1500 net-new lines of feature code is beyond the "simple/mechanical/obvious" bar for auto-approval.
Other factors
The PR has been through many rounds of automated review over its lifetime (the timeline shows dozens of resolved threads from CodeRabbit, comment-cop, and prior bug-hunter passes), and every finding from earlier runs of this reviewer — optional-dep coverage, in-tree helper reuse, -o overflow, SPDX VCS-locator syntax, orphaned doc comment, describe.concurrent — was addressed in follow-up commits and resolved. Test coverage is solid: 14 concurrent Verdaccio-backed tests exercising both formats, all three scope states, transitive-dev vs mixed-path, workspaces from root and subdir, git+file:// and github: (via local GITHUB_API_URL), -o relative/absolute/overlong, unknown --format, and missing lockfile. No human reviewer has commented on the PR.
There was a problem hiding this comment.
I reviewed this PR and the bug-hunting pass found no issues; all earlier inline findings have been addressed. Because this adds a new user-facing bun pm subcommand (~1000 lines of new Rust plus ~500 lines of tests) and makes API-surface choices — CycloneDX 1.7 vs SPDX 2.3, purl/VCS-locator formatting, scope semantics, and re-widening several bun_install items to pub — a maintainer sign-off on the design would still be worthwhile.
What was reviewed:
- Scope-propagation BFS and SPDX relationship direction (
*_OFsubject/object ordering) — covered by tests for required/optional/excluded and mixed prod+dev paths. - JSON serialization: all user-derived strings go through
json_str/format_json_string_utf8; SPDXIDs are sanitized and independently deduplicated from bom-refs. -opath handling usesjoin_abs_string_buf_checked(no panic on over-long argv); write errors exit non-zero.- Visibility widenings in
integrity.rs/resolution.rs/Meta.rsare read-only accessors and tag constants; no new mutation surface.
Extended reasoning...
Overview
This PR adds bun pm sbom, a new CLI subcommand that reads the lockfile and emits a Software Bill of Materials in CycloneDX 1.7 or SPDX 2.3 JSON. The bulk of the change is a new file src/runtime/cli/pm_sbom_command.rs (~1000 lines) plus wiring in package_manager_command.rs, CLI argument plumbing in CommandLineArguments.rs/PackageManagerOptions.rs, help text, docs, shell completions, and a 500-line Verdaccio-backed test file. It also widens a handful of bun_install items from pub(crate) to pub (the Resolution::Tag constants, Resolution::repository(), Integrity::slice() and its tag constants, Meta.integrity) so the new command can read them cross-crate, and makes civil_from_days in shell/builtin/ls.rs pub(crate) for reuse.
Security risks
Low. The command is read-only against the lockfile and writes either to stdout or to a user-specified file. All strings interpolated into the JSON output (package names, versions, URLs, refs) are routed through bun_core::fmt::format_json_string_utf8, so lockfile content cannot break out of a JSON string. SPDXIDs are sanitized to [A-Za-z0-9.-] and independently deduplicated. The -o path is joined against the original cwd via join_abs_string_buf_checked and errors cleanly on overflow rather than panicking. No network, no auth, no untrusted-input parsing beyond what PackageManager::init already does.
Level of scrutiny
High — this is a new user-facing CLI subcommand with externally-specified output formats. Per the repo guidance, adding user-facing API surface calls for the API-design section of .claude/docs/landing-prs.md and maintainer agreement on the shape (flag names, default format, spec versions, whether --format/-o on bun pm collide with future subcommands). The implementation itself is self-contained and well-tested, but the design choices — CycloneDX 1.7 as default, SPDX 2.3 field selection, NOASSERTION for licenses, scope: excluded for dev deps, VCS-locator formatting — are the kind of thing a maintainer should ratify before they become a compatibility surface for downstream SBOM consumers.
Other factors
The PR has been through many review iterations (all inline threads resolved): optional-dependency coverage, in-tree helper reuse (hex_lower, hex_byte_upper, civil_from_days), -o path-length hardening, SPDX §7.7-conformant VCS locators, describe.concurrent with per-test cache/tmp isolation, and dead-comment cleanup. Test coverage is thorough (14 tests: both formats, purl encoding, scope propagation including transitive-dev and mixed paths, workspaces from root and subdir, git+github locators via a local server, error paths). This run's bug-hunting pass found nothing. The remaining reason to defer is scope, not correctness concerns.
…ilter/--catalog, nested overrides, transitive update, and workspace fixes (#38333) Brings Bun's package manager to parity with pnpm for monorepo workflows, and fixes the bugs found while checking every command against pnpm's implementation, pnpm's test suites, pnpm's open issue tracker, pnpm's docs, npm's arborist fixtures, and — for `bun update` — running real pnpm and Bun side by side on the same projects. ### What does this PR do? #### New commands - **`bun dedupe [--check]`** — collapses duplicate versions in `bun.lock` onto the smallest set that still satisfies every dependent's range, using only versions already in the lockfile, then installs. Never downgrades a direct dependency unless that is the only way to drop a version; keeps patched versions (and anything needed to reach them, and says so); refuses to run on a lockfile that is behind `package.json`. `--check` exits 1 without writing. - **`bun prune [--production | --omit=…] [--dry-run] [--filter <ws>]`** — removes everything in `node_modules` that the lockfile does not put there; `--production` leaves exactly what `bun install --production` would. Hoisted and isolated layouts, Windows junctions and shims, workspace links, bundled deps; refuses when `package.json` and `bun.lock` disagree or when `node_modules` was laid out by a different linker; understands turbo-pruned checkouts. - **`bun pm licenses [--json] [--prod|--dev] [--long] [--filter <ws>]`** — installed packages grouped by license, with a `(dev)` marker and `paths`/`license`/`description` in `--json`. - **`bun audit fix [--latest] [--dry-run] [--json]`** — moves each vulnerable package to the lowest safe version its dependents accept, per installed instance; rewrites exact pins when that is the only way; `--latest` also rewrites your own declared ranges (root, workspace, catalog) so a semver-major fix can be taken, and every blocked or unfixable item is followed by the command that resolves it (`bun audit fix --latest`, `bun audit --ignore GHSA-…`); re-audits the tree it actually installed and reports/exits from that second response (npm's `_submitQuickAudit`), so an advisory that starts at the version it moved to is not missed; works across registries; security fixes bypass `minimumReleaseAge` with an annotation. `bun audit --json` honors `--audit-level`/`--ignore` for its exit code; `--omit` is honored by `audit` and `licenses`. #### `bun update` semantics (pnpm's model) - A bare `bun update` re-resolves **transitive** packages too — every edge moves to the newest version its own range (or dist-tag) allows, per dependent, so `bun.lock` no longer stays stale after an update; overrides/catalogs changed since the last install are honored. From a workspace member or `--filter`, only what the selected workspaces reach is re-resolved; from the root, everything. - `bun update <name>` reaches any depth, matches `npm:` aliases by real name, updates in place, never adds to `package.json`, and errors on a name nothing selected depends on. `-r`/`--filter` fan a named update out across workspaces. `--latest` never downgrades a locked version that is ahead of the tag, and `update <name> --latest` also refreshes that package's own dependencies. - Plain updates keep dist-tag literals and non-caret ranges (`*`, `1.x`, `^1 || ^2`) exactly as written and only move the lockfile; `--latest` rewrites them to the resolved version as before. `bun update -i` applies only what you selected. New: positional patterns (`bun update '@types/*'`), `--dev`/`--prod`/`--no-optional`, `-L`, `bun up`. - `package.json` is written after resolution and `bun.lock`'s declared ranges, overrides and catalogs are re-derived from the final `package.json`, replacing the per-command literal rewriting; a no-op update leaves the file byte-identical. #### Overrides - **Nested overrides** (#6608): npm's nested objects, yarn's `a/b` paths and pnpm's `a>b` selectors, applied to the direct parent→child edge; **version-scoped targets** (`"lodash@<4.17.21": "4.17.21"`, the shape `pnpm audit --fix` writes), matched against the dependent's declared range as pnpm does. Rules persist inside the `overrides` section and the file is stamped `lockfileVersion: 3` **only when such rules exist** — existing lockfiles are byte-identical. Flat overrides additionally fix `$ref` to workspace-member deps, catalog-valued rules going stale, and warn on pnpm's `-` / `pkg@` forms. #### Workspaces and filters - `bun add|remove|update … --filter <ws>` (also `-F`, also `bun install <pkg> --filter`) edits the selected workspaces' `package.json` files and **links only those workspaces**, like `bun install --filter`. Filters gain pnpm's relation selectors (`foo...`, `...foo`, `foo^...`, `...^foo`) and `{dir}` subtrees, for the install family **and** `bun run --filter`; `--filter` may precede the subcommand; every command warns about patterns that match nothing; `add`/`remove` no longer select the root implicitly. - `bun add <pkg> --catalog[=name]` reuses an existing catalog entry, keeps a range an explicit version fits, catalogs the range a package already declares, decides per target, and refuses workspace names and local paths; a plain `bun add` uses a default-catalog entry when one exists. A package defined in both `catalog` and `catalogs.default` is an error. `catalog:` peers of registry packages bind to the importer's copy instead of the root catalog. - `--frozen-lockfile` / `bun ci` on turbo-pruned monorepos: pruned-away workspaces are tolerated, a survivor depending on a pruned workspace is an error, catalog subsets are accepted, and an overrides/catalogs change is a frozen failure. #### One output vocabulary Every command here prints the install family's shapes: header, glyph rows (`+`/`-`/`↑`, dedupe's `↳ name old → new`), exactly one noun-first summary line with counts and a duration (`2 duplicate versions removed, 3 packages installed (checked 5 packages) [12ms]`, `N packages removed (checked C) [t]`), no-ops that say what was checked, remedies printed as copy-pasteable command lines, warnings as `warn:`, `--silent` printing nothing, and errors with their remedy together on stderr. Transitive and named updates render as the summary's `↑` rows (once per package; `--dry-run` prints the same rows plus `N packages would be updated`); dedupe reports after the install it triggers, so lifecycle-script output never splits it. A lockfile whose bytes did not change is no longer rewritten (`Saved lockfile` only prints on a real write; `--lockfile-only` no-ops print `Done! Checked N packages (no changes)`). This came out of running every command against fixtures and comparing with `install`/`add`/`remove` (95 findings, all fixed). #### Config precedence A project's `bunfig.toml` now beats any `.npmrc` (project or user-level) for the same key (npmrc files → bunfig's set fields → CLI); npmrc-only settings such as `//host/:_authToken` still attach to bunfig-declared registries, matched by host and path regardless of how either file spells the trailing slash. #### Lockfile migration - `package-lock.json`: rebuilt around a reachability walk that derives each resolution from the entry itself. Fixes `git+https://github.com/…` resolutions being written unparseably (the next install threw the lockfile away), root `bundleDependencies` migrating to an **empty** lockfile, lockfileVersion 1 (and npm's upcoming 4) making `bun install` exit 1 instead of resolving fresh, dependency-level bundles, `dependencies`+`optionalDependencies` double edges, unreferenced entries aborting the migration, duplicate packages for identical `name@version` at nested paths, lost `optionalPeers`, lost integrity when a bundled copy was seen first, and `overrides` not being carried over. All 57 of arborist's v2/v3 fixture projects are vendored and migrated under snapshot. - `pnpm-lock.yaml` v9: bare-hash `patchedDependencies`, snapshot aliases, `catalog:default`, recorded tarball URLs, git `path:`, multi-document files, `runtime:` entries, named registries, peer-suffixed keys chosen per importer, injected workspaces, manifest-only importer deps. #### Isolated linker - An existing store entry whose dependencies re-resolved (override, dedupe, update) now has its links refreshed on the next install (measured cost below). `bun prune` builds the same store the installer builds, so stale `name@version+<peerhash>` variants left by peer bumps are removed and a kept package's real entry never is (whether it was installed with full or `--production` features); on the hoisted linker, dedupe / audit fix / update delete the nested copies whose rows they collapsed instead of leaving the old copy loadable. - Blocked entries resume through per-entry intrusive waiter lists instead of a scan of every store entry after each completion (robobun's #25983/#28425 attempted this). Measured on the reporter's repro from #25799 (2,259 store entries) and a synthetic 6,425-entry monorepo, PR build vs merge-base build: main-thread CPU in the link phase drops 0.85 → 0.33 s and 5.5 → 0.85 s (the removed work grows quadratically); wall time is unchanged with spare cores and 16% / 26% faster pinned to one CPU, the CI/Docker shape in those reports. (The minute-long installs originally reported were peer resolution, fixed before this PR's base.) - Two pre-existing leaks surfaced by the new LSan-enabled tests are fixed: the header buffer of every authenticated registry request, and the per-entry lifecycle-script lists. #### Other bug fixes `bun add x@npm:pkg` writes a range; `bun add --trust a b` no longer drops `b` when `a` was already trusted; `bun add x --dev` no longer rewrote every group in `bun.lock`; `catalog:` peer hoisting; `dependency::Version::eql` treated all `catalog:` specifiers as equal; a `file:` package whose dependencies reach itself (its own name, an `npm:` alias under its own name, two link targets depending on each other — the shapes a `package-lock.json` migration produces, and #25202's `workspace:.` self-reference) hung `bun install` forever in the hoisting tree — the migration shapes now install, and #25202's literal shape now terminates with `Workspace dependency "foo" not found` rather than installing as npm does; a peer of a `file:` package that was only placed nested was also written to `optionalPeers` in a migrated bun.lock, so the next `--frozen-lockfile` failed and a plain install rewrote the lockfile; `catalog:` literals in `bun update`; alias output in the install summary; help/completions for everything above. #### Behavior changes to note in the release notes - `bun update` moves transitive packages; `bun update <name>` no longer adds an undeclared package (exit 1); `--production`/`--prod` on update means "only update `dependencies` and `optionalDependencies`" (a group filter like `--dev`, not the install flag) and `-r`+names with no match is an error; `-i` updates only the selection. - Project `bunfig.toml` overrides any `.npmrc`. - `bun install <pkg> --filter x` edits `x` (not the root); `bun add y --filter x` no longer installs a package named `x`; `add`/`remove --filter '*'` no longer includes the root. - A plain `bun add x` in a workspace whose default catalog lists `x` writes `catalog:`; `audit fix` may rewrite exact pins; `--frozen-lockfile --lockfile-only` writes nothing; overrides/catalog changes fail frozen installs. - One-time lockfile churn after upgrading for projects with `catalog:` peers or dead `pkg@range` override rows; lockfiles that use nested/scoped overrides are v3 and unreadable by older Bun (only when opted in). Turborepo, Nx and Dependabot have been checked; the needed upstream changes are open (nrwl/nx#36666 covers v2 and v3; vercel/turborepo#13740 accepts v3 and preserves the object rows through prune — turborepo main today parses v2 and rejects v3; dependabot needs nothing). Note v2 itself only exists on the 1.4 line. - `bun audit --json` keeps npm's contract: `--audit-level`/`--ignore` decide the exit code, the JSON document is the full registry report (closes #31013 as won't-change). Automatic removal of stale `node_modules` entries on plain `bun install` (#32974) is separate from this PR: `bun prune` is the manual form, and dedupe / audit fix / update now clean up the nested copies they collapse on the hoisted linker; #32974 should reuse prune's planner, and #29512 (sbom) is sequenced after this so it can build on `reachable.rs` instead of carrying its own walk. - Deliberately kept where we differ from pnpm: root `bun update` covers the whole workspace; dependents whose ranges allow follow a moved version (one copy, not two); `--latest` works on transitive names; `--no-save` touches neither file; prune deletion failures exit 1; audit requests stay per-registry. #### Performance Measured on a 1,113-package Next/Prisma/MUI app (PR build vs a PR build of the merge base, interleaved, plus canary and 1.3.14): every hoisted cell is within noise except no-op install, +0.8 ms (+2%, identical syscalls); isolated no-op is +1.9 ms (+3.9%) — the deliberate cost of re-checking existing entries' links every install rather than persisting a stamp file. Everything added is otherwise off the plain-install path (gated on the feature being used or on a diff), and id-indexed sets are bitsets. ### How did you verify your code works? ~1,100 new or ported test cases across the install suites (designed behavior, cases ported from pnpm's suites, pinning tests for pnpm bugs this implementation is immune to, arborist's fixtures, and the CI review findings), all `toStrictEqual`; the whole `test/cli/install` directory passes locally and the existing suites are unchanged except where a pre-existing expectation was deliberately changed (each listed above). `bun update` was additionally verified with a rerunnable differential harness that runs pnpm 11 and this branch on 26 scenario families against one registry and diffs the resulting resolutions edge by edge — after this PR only the deliberate differences above remain. Ecosystem: Turborepo, Nx and Dependabot were checked against the new lockfile output. Co-authored work absorbed with credit: @kjanat's #38190 (alias handling, co-author on the commit), @charpeni's #31143 and @crystalin's #34407 (both superseded), and the tests of the earlier `bun update` PRs (#31752 by @zlotnika, #33127, #36381, #36729, #38224). robobun's #34688 (folder-dependency cycles; its tests are lifted, co-author on the commit) and #37289 (migrated optionalPeers; its test is lifted, co-author on the commit) were fixed independently here and are closed by this PR. #28422's quadratic scan is fixed here as well (already closed). Related but not closed — `bun prune` gives these a manual fix while the automatic-cleanup asks stay open: #8662, #26305, #29793, #21216, #16176. Also related: #10930, #26970, #26751. Fixes #1343 Fixes #3605 Fixes #14719 Fixes #24122 Fixes #18612 Fixes #20238 Fixes #25826 Fixes #23615 Fixes #26973 Fixes #20593 Closes #31013 Fixes #28959 Fixes #28402 Fixes #27897 Fixes #26675 Fixes #10949 Fixes #18504 Fixes #13388 Fixes #24523 Fixes #6608 Fixes #19059 Fixes #16569 Fixes #8262 Fixes #11901 Fixes #13469 Fixes #25202 Closes #29664 Closes #31143 Closes #34407 Closes #34688 Closes #37289 Closes #38190 --------- Co-authored-by: Kaj Kowalski <info@kajkowalski.nl> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: robobun <117481402+robobun@users.noreply.github.com>
f04bc62 to
7f4d005
Compare
7f4d005 to
36ae2ab
Compare
There was a problem hiding this comment.
The rebase regression I flagged on the licenses dispatch arm is fixed — both licenses and sbom now terminate with their own Global::exit(0). This pass found no further issues. Given the size (~1000 lines of new Rust implementing a user-facing bun pm sbom subcommand against two external specs, plus visibility widening across bun_install), a human sign-off on the API surface and SBOM spec conformance is still worthwhile.
What was reviewed: the package_manager_command.rs dispatch chain (verified licenses/sbom/pkg each exit correctly); scope-propagation BFS and ref/SPDXID dedup in Generator::init; vcs_locator against SPDX §7.7; the -o path handling (checked variant, pooled buffer). Ruled out: test temp-dir leakage via newProject() (verifiers confirmed harmless).
Extended reasoning...
Overview
This PR adds bun pm sbom, a new subcommand that generates CycloneDX 1.7 or SPDX 2.3 SBOM documents from the lockfile. The bulk of the change is a new ~1000-line file src/runtime/cli/pm_sbom_command.rs plus ~500 lines of Verdaccio-backed integration tests. Supporting changes wire the subcommand into CLI parsing, help text, docs, and completions, and re-widen a handful of bun_install items (Resolution::repository(), Integrity::slice(), several Tag consts, Meta.integrity) from pub(crate) to pub so the runtime crate can read them. One incidental change makes civil_from_days in shell/builtin/ls.rs pub(crate) for reuse.
Prior finding resolved
My previous review (2026-08-14) flagged a 🔴 rebase mistake where the new sbom arm was inserted between PmLicensesCommand::exec(...)? and its Global::exit(0), causing bun pm licenses to fall through to the unknown-command handler. The current diff shows this is fixed: the licenses arm at package_manager_command.rs:754-756 now ends with its own Global::exit(0) before the sbom arm begins, and sbom has its own terminator. All earlier nits (SPDX VCS locator format, join_abs_string_buf_checked, orphaned doc comment, describe.concurrent, in-tree helper reuse) were also addressed in prior commits and remain in place.
Security risks
Low. The command reads the lockfile and package.json from the workspace root and writes JSON to stdout or a user-specified file. Output-path handling uses the checked path-join variant with a clean error on overflow. The SBOM output includes package names, versions, tarball URLs, and integrity hashes — all already present in bun.lock. No network I/O, no untrusted input parsing beyond the lockfile Bun already trusts.
Level of scrutiny
Moderate-to-high. This is a new user-facing CLI subcommand implementing two external specifications (CycloneDX 1.7 JSON schema, SPDX 2.3). The JSON is hand-serialized line-by-line rather than via a structured serializer, so field ordering, escaping, and comma placement are all manual. The test suite validates the required schema fields for both formats, but spec conformance for edge cases (unusual package names, git URL shapes, purl encoding subtleties) is the kind of thing a maintainer familiar with SBOM tooling should eyeball. The visibility widening on bun_install types is minimal and read-only, but reverses part of #36184's narrowing and should be acknowledged.
Other factors
The PR has been through ~10 rounds of automated review with every finding addressed. Test coverage is solid: both formats, scoped packages, workspaces, git+github deps via a local server, dev/optional/transitive scope propagation, error paths, and the -o path-too-long guard. The bug-hunting pass this run raised only two candidate findings about test temp-dir cleanup, both refuted by verifiers. I'm deferring rather than approving because a ~1500-line new feature that defines public CLI surface and claims conformance to two external standards is exactly the case where a human should confirm the API shape and output format before it ships.
Generates a Software Bill of Materials from the lockfile in either CycloneDX 1.7 (default) or SPDX 2.3 JSON format. bun pm sbom # CycloneDX to stdout bun pm sbom --format spdx -o sbom.json # SPDX to file For each package in the lockfile the SBOM includes the package name, resolved version, purl identifier, download location (registry tarball URL, or a git+<url>@<revision> VCS locator for git and github packages), and integrity hash. Dependency relationships are emitted as the CycloneDX `dependencies` graph or SPDX `relationships`. Scope classification is computed via BFS from the lockfile root: a package's scope is the strongest (required > optional > excluded) over all paths that reach it, where a path's scope is the weakest edge along it, so transitive deps of a root devDependency are `excluded`, but a package reachable via both a dev path and a prod path is `required`. This matches what `bun install --production` would actually install. The SBOM always describes the whole lockfile, regardless of which workspace subdirectory the command is run from. Relative `-o` paths resolve against the invocation directory. Re-widens to `pub` the bun_install items this command reads cross-crate (the remaining Resolution tags, Resolution::repository, Integrity::slice and its SHA1/384/512 tags, Meta.integrity), which #36184 had narrowed because nothing outside the crate used them at the time, and exposes the shell's civil_from_days within the runtime crate for the timestamp. Closes #8483.
36ae2ab to
c623d42
Compare
There was a problem hiding this comment.
I reviewed this PR and the automated bug hunt found no issues this pass. Because it adds a new user-facing bun pm subcommand (~1000 lines) that emits two external spec formats (CycloneDX 1.7 / SPDX 2.3) consumed by downstream scanners, a maintainer sign-off on the API surface and spec conformance would still be worthwhile.
What was reviewed:
- Dispatch chain in
package_manager_command.rs— thelicensesarm'sGlobal::exit(0)is restored; only thesbomarm is added. - Visibility widenings in
resolution.rs/integrity.rs/Meta.rs— read-only accessors, no behavior change. -opath handling usesjoin_abs_string_buf_checkedand exits cleanly on overflow.- Scope BFS, ref/SPDXID dedup, VCS locator formatting, and JSON string escaping traced against the test assertions.
Extended reasoning...
Overview
This PR adds bun pm sbom, a new CLI subcommand that reads the lockfile and emits a Software Bill of Materials in CycloneDX 1.7 or SPDX 2.3 JSON. The bulk of the change is a new ~1000-line file (src/runtime/cli/pm_sbom_command.rs) plus a ~500-line test file. Supporting changes are mechanical: CLI arg plumbing in CommandLineArguments.rs/PackageManagerOptions.rs, a new dispatch arm in package_manager_command.rs, help-text/docs/completions, and pub(crate)→pub widenings on a handful of bun_install items the generator reads (resolution tags, Resolution::repository(), Integrity::slice/tags, Meta.integrity). civil_from_days in shell/builtin/ls.rs is made pub(crate) for reuse.
Security risks
Low. The command is read-only over the lockfile and writes either to stdout or to a user-specified file via bun_sys::File::write_file. All string values written into the JSON go through bun_core::fmt::format_json_string_utf8, so lockfile-sourced bytes cannot break JSON structure. The -o path is resolved with the checked path-join variant against the invocation cwd. No network, no auth, no untrusted input beyond the user's own lockfile and CLI args.
Level of scrutiny
High, for two reasons: (1) it introduces new user-facing API surface (a subcommand with two flags and two output formats), which per this repo's review guidance is a design decision that wants maintainer agreement; and (2) the output is a standards document that third-party tools (Grype, Trivy, Dependency-Track) will parse — spec-conformance mistakes are silent and only surface downstream. Earlier review rounds already caught and fixed several such issues (SPDX §7.7 VCS locator syntax, purl percent-encoding, SPDXID charset), which is exactly the class of thing a human familiar with these specs should spot-check once more.
Other factors
The PR has been through many automated review rounds over four months; every prior finding is addressed and marked resolved, including a rebase regression that briefly broke bun pm licenses (now restored — verified in the current diff that both the licenses and sbom arms end in Global::exit(0)). Test coverage is thorough (14 concurrent Verdaccio-backed tests covering both formats, scoped packages, workspaces, git/github deps, error paths, and the long -o path). No human maintainer has reviewed yet; robobun noted in May it was "waiting on a maintainer to review and merge". Given the size and the new-API nature, deferring rather than approving.
Adds
bun pm sbom, which generates a Software Bill of Materials from the lockfile.Usage
What's in the SBOM
For every package in the lockfile:
name,versionname,versionInfobom-ref(e.g.lodash@4.17.21)SPDXID(SPDXRef-Package-lodash-4.17.21)purl(pkg:npm/lodash@4.17.21, scopes %-encoded)externalRefs[].referenceLocatorexternalReferences[].urldownloadLocationhashes[](SHA-512hex)checksums[](SHA512hex)dependencies[].dependsOnrelationships[]Registry packages use the tarball URL as the download location; git and github packages use an SPDX 2.3 VCS locator (
git+<url>@<revision>). Dev dependencies are marked withscope: "excluded"(CycloneDX) /DEV_DEPENDENCY_OF(SPDX); optional dependencies usescope: "optional"/OPTIONAL_DEPENDENCY_OF. Workspaces, folders, git/github, and tarball resolutions are all represented.The root component is emitted under
metadata.component(CycloneDX) and as thedocumentDescribestarget (SPDX), with Bun listed as the generating tool.Example (CycloneDX, abridged)
{ "bomFormat": "CycloneDX", "specVersion": "1.7", "serialNumber": "urn:uuid:...", "metadata": { "timestamp": "2026-04-20T12:34:56Z", "tools": { "components": [{ "type": "application", "name": "bun", "version": "1.x.x" }] }, "component": { "type": "application", "bom-ref": "my-app@1.0.0", "name": "my-app", "version": "1.0.0" } }, "components": [ { "type": "library", "bom-ref": "lodash@4.17.21", "name": "lodash", "version": "4.17.21", "scope": "required", "purl": "pkg:npm/lodash@4.17.21", "externalReferences": [{ "type": "distribution", "url": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" }], "hashes": [{ "alg": "SHA-512", "content": "bf690311..." }] } ], "dependencies": [ { "ref": "my-app@1.0.0", "dependsOn": ["lodash@4.17.21"] }, { "ref": "lodash@4.17.21", "dependsOn": [] } ] }Tests
test/cli/install/bun-pm-sbom.test.tsuses the Verdaccio test registry to install real packages, then validates both output formats against the required fields from the CycloneDX 1.7 and SPDX 2.3 JSON schemas (serial number pattern, timestamp format, purl encoding, hash hex length, SPDXID charset, dependency graph integrity, relationship types).Fixes #8483
Rebase notes
src/was restructured and Zig left the build (src/runtime/cli/pm_sbom_command.rs, followingaudit_command.rs/pm_why_command.rs).bun_installvisibility, this PR re-widens topubexactly the items it reads cross-crate: the remainingResolutiontags,Resolution::repository(),Integrity::sliceand its SHA1/384/512 tags, andMeta.integrity.test/harness.tsVerdaccio127.0.0.1bind fix landed on main independently and is no longer part of this diff.bun pm diff(Addbun pm diff#39229) landed alongside; the two conflicts were keep-both inCommandLineArguments.rs(thepmoption fields and their parsing). Its new flags do not overlap with--format/-o, --outfile;bun-pm-sbom,bun-pm-licensesandbun-pm-difftest files all pass on the rebased build.bun pm licenses): the conflicts were keep-both in thepmargument struct, dispatch chain and docs page. The missing-lockfile test now matches the reworded shared error (missing lockfile). The first push of that rebase dropped theGlobal::exit(0)at the end of thelicensesdispatch arm (CI caught it viabun-pm-licenses.test.ts); it is restored, and the dispatch chain now differs from main only by the addedsbomarm.no test proof · iteration 18 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/cli/install/bun-pm-sbom.test.ts