mirror of
https://github.com/oven-sh/bun
synced 2026-02-17 06:12:08 +00:00
Compare commits
3 Commits
claude/str
...
claude/fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dbeecb97f9 | ||
|
|
529fda21d2 | ||
|
|
20f5f45653 |
@@ -344,7 +344,7 @@ pub fn loadFromDir(
|
||||
defer writer_allocating.deinit();
|
||||
const writer = &writer_allocating.writer;
|
||||
|
||||
TextLockfile.Stringifier.saveFromBinary(allocator, result.ok.lockfile, &result, &manager.?.options, writer) catch |err| {
|
||||
TextLockfile.Stringifier.saveFromBinary(allocator, result.ok.lockfile, &manager.?.options, writer) catch |err| {
|
||||
Output.panic("failed to convert binary lockfile to text lockfile: {s}", .{@errorName(err)});
|
||||
};
|
||||
|
||||
@@ -1243,7 +1243,7 @@ pub fn saveToDisk(this: *Lockfile, load_result: *const LoadResult, options: *con
|
||||
defer writer_allocating.deinit();
|
||||
const writer = &writer_allocating.writer;
|
||||
|
||||
TextLockfile.Stringifier.saveFromBinary(bun.default_allocator, this, load_result, options, writer) catch |err| switch (err) {
|
||||
TextLockfile.Stringifier.saveFromBinary(bun.default_allocator, this, options, writer) catch |err| switch (err) {
|
||||
error.WriteFailed => bun.outOfMemory(),
|
||||
};
|
||||
|
||||
|
||||
@@ -27,10 +27,10 @@ pub const Stringifier = struct {
|
||||
// _ = this;
|
||||
// }
|
||||
|
||||
pub fn saveFromBinary(allocator: std.mem.Allocator, lockfile: *BinaryLockfile, load_result: *const LoadResult, options: *const PackageManager.Options, writer: *std.Io.Writer) std.Io.Writer.Error!void {
|
||||
return bun.handleOom(saveFromBinary_inner(allocator, lockfile, load_result, options, writer));
|
||||
pub fn saveFromBinary(allocator: std.mem.Allocator, lockfile: *BinaryLockfile, options: *const PackageManager.Options, writer: *std.Io.Writer) std.Io.Writer.Error!void {
|
||||
return bun.handleOom(saveFromBinary_inner(allocator, lockfile, options, writer));
|
||||
}
|
||||
pub fn saveFromBinary_inner(allocator: std.mem.Allocator, lockfile: *BinaryLockfile, load_result: *const LoadResult, options: *const PackageManager.Options, writer: *std.Io.Writer) !void {
|
||||
pub fn saveFromBinary_inner(allocator: std.mem.Allocator, lockfile: *BinaryLockfile, options: *const PackageManager.Options, writer: *std.Io.Writer) !void {
|
||||
const buf = lockfile.buffers.string_bytes.items;
|
||||
const extern_strings = lockfile.buffers.extern_strings.items;
|
||||
const deps_buf = lockfile.buffers.dependencies.items;
|
||||
@@ -60,32 +60,8 @@ pub const Stringifier = struct {
|
||||
var optional_peers_buf = std.array_list.Managed(String).init(allocator);
|
||||
defer optional_peers_buf.deinit();
|
||||
|
||||
var pkg_map = PkgMap(void).init(allocator);
|
||||
defer pkg_map.deinit();
|
||||
|
||||
var pkgs_iter = BinaryLockfile.Tree.Iterator(.pkg_path).init(lockfile);
|
||||
|
||||
var path_buf: bun.PathBuffer = undefined;
|
||||
|
||||
// if we loaded from a binary lockfile and we're migrating it to a text lockfile, ensure
|
||||
// peer dependencies have resolutions, and mark them optional if they don't
|
||||
if (load_result.loadedFromBinaryLockfile()) {
|
||||
while (pkgs_iter.next({})) |node| {
|
||||
for (node.dependencies) |dep_id| {
|
||||
const dep = deps_buf[dep_id];
|
||||
|
||||
// clobber, there isn't data
|
||||
try pkg_map.put(try std.fmt.allocPrint(allocator, "{s}{s}{s}", .{
|
||||
node.relative_path,
|
||||
if (node.depth == 0) "" else "/",
|
||||
dep.name.slice(buf),
|
||||
}), {});
|
||||
}
|
||||
}
|
||||
|
||||
pkgs_iter.reset();
|
||||
}
|
||||
|
||||
var _indent: u32 = 0;
|
||||
const indent = &_indent;
|
||||
try writer.writeAll("{\n");
|
||||
@@ -113,11 +89,9 @@ pub const Stringifier = struct {
|
||||
buf,
|
||||
extern_strings,
|
||||
deps_buf,
|
||||
resolution_buf,
|
||||
lockfile.workspace_versions,
|
||||
&optional_peers_buf,
|
||||
&pkg_map,
|
||||
"",
|
||||
&path_buf,
|
||||
);
|
||||
|
||||
var workspace_sort_buf: std.ArrayListUnmanaged(PackageID) = .{};
|
||||
@@ -164,11 +138,9 @@ pub const Stringifier = struct {
|
||||
buf,
|
||||
extern_strings,
|
||||
deps_buf,
|
||||
resolution_buf,
|
||||
lockfile.workspace_versions,
|
||||
&optional_peers_buf,
|
||||
&pkg_map,
|
||||
pkg_names[workspace_pkg_id].slice(buf),
|
||||
&path_buf,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -502,14 +474,12 @@ pub const Stringifier = struct {
|
||||
dep.behavior,
|
||||
deps_buf,
|
||||
pkg_deps_sort_buf.items,
|
||||
resolution_buf,
|
||||
&pkg_meta,
|
||||
&pkg_bin,
|
||||
buf,
|
||||
&optional_peers_buf,
|
||||
extern_strings,
|
||||
&pkg_map,
|
||||
relative_path,
|
||||
&path_buf,
|
||||
);
|
||||
|
||||
try writer.writeByte(']');
|
||||
@@ -525,14 +495,12 @@ pub const Stringifier = struct {
|
||||
dep.behavior,
|
||||
deps_buf,
|
||||
pkg_deps_sort_buf.items,
|
||||
resolution_buf,
|
||||
&pkg_meta,
|
||||
&pkg_bin,
|
||||
buf,
|
||||
&optional_peers_buf,
|
||||
extern_strings,
|
||||
&pkg_map,
|
||||
relative_path,
|
||||
&path_buf,
|
||||
);
|
||||
|
||||
try writer.writeByte(']');
|
||||
@@ -548,14 +516,12 @@ pub const Stringifier = struct {
|
||||
dep.behavior,
|
||||
deps_buf,
|
||||
pkg_deps_sort_buf.items,
|
||||
resolution_buf,
|
||||
&pkg_meta,
|
||||
&pkg_bin,
|
||||
buf,
|
||||
&optional_peers_buf,
|
||||
extern_strings,
|
||||
&pkg_map,
|
||||
relative_path,
|
||||
&path_buf,
|
||||
);
|
||||
|
||||
try writer.writeByte(']');
|
||||
@@ -571,14 +537,12 @@ pub const Stringifier = struct {
|
||||
dep.behavior,
|
||||
deps_buf,
|
||||
pkg_deps_sort_buf.items,
|
||||
resolution_buf,
|
||||
&pkg_meta,
|
||||
&pkg_bin,
|
||||
buf,
|
||||
&optional_peers_buf,
|
||||
extern_strings,
|
||||
&pkg_map,
|
||||
relative_path,
|
||||
&path_buf,
|
||||
);
|
||||
|
||||
try writer.writeByte(']');
|
||||
@@ -602,14 +566,12 @@ pub const Stringifier = struct {
|
||||
dep.behavior,
|
||||
deps_buf,
|
||||
pkg_deps_sort_buf.items,
|
||||
resolution_buf,
|
||||
&pkg_meta,
|
||||
&pkg_bin,
|
||||
buf,
|
||||
&optional_peers_buf,
|
||||
extern_strings,
|
||||
&pkg_map,
|
||||
relative_path,
|
||||
&path_buf,
|
||||
);
|
||||
|
||||
try writer.print(", \"{f}\"]", .{
|
||||
@@ -634,14 +596,12 @@ pub const Stringifier = struct {
|
||||
dep.behavior,
|
||||
deps_buf,
|
||||
pkg_deps_sort_buf.items,
|
||||
resolution_buf,
|
||||
&pkg_meta,
|
||||
&pkg_bin,
|
||||
buf,
|
||||
&optional_peers_buf,
|
||||
extern_strings,
|
||||
&pkg_map,
|
||||
relative_path,
|
||||
&path_buf,
|
||||
);
|
||||
|
||||
try writer.print(", {f}]", .{
|
||||
@@ -670,14 +630,12 @@ pub const Stringifier = struct {
|
||||
dep_behavior: Dependency.Behavior,
|
||||
deps_buf: []const Dependency,
|
||||
pkg_dep_ids: []const DependencyID,
|
||||
resolution_buf: []const PackageID,
|
||||
meta: *const Meta,
|
||||
bin: *const Install.Bin,
|
||||
buf: string,
|
||||
optional_peers_buf: *std.array_list.Managed(String),
|
||||
extern_strings: []const ExternalString,
|
||||
pkg_map: *const PkgMap(void),
|
||||
relative_path: string,
|
||||
path_buf: []u8,
|
||||
) error{ OutOfMemory, WriteFailed }!void {
|
||||
defer optional_peers_buf.clearRetainingCapacity();
|
||||
|
||||
@@ -715,10 +673,10 @@ pub const Stringifier = struct {
|
||||
bun.fmt.formatJSONStringUTF8(dep.version.literal.slice(buf), .{}),
|
||||
});
|
||||
|
||||
if (dep.behavior.peer and !dep.behavior.optional and pkg_map.map.count() > 0) {
|
||||
pkg_map.findResolution(relative_path, dep, buf, path_buf) catch {
|
||||
try optional_peers_buf.append(dep.name);
|
||||
};
|
||||
// If a required peer dependency is not resolved, add it to optionalPeers
|
||||
// to prevent lockfile loading errors when the peer is not installed.
|
||||
if (dep.behavior.peer and !dep.behavior.optional and resolution_buf[dep_id] == invalid_package_id) {
|
||||
try optional_peers_buf.append(dep.name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -816,11 +774,9 @@ pub const Stringifier = struct {
|
||||
buf: string,
|
||||
extern_strings: []const ExternalString,
|
||||
deps_buf: []const Dependency,
|
||||
resolution_buf: []const PackageID,
|
||||
workspace_versions: BinaryLockfile.VersionHashMap,
|
||||
optional_peers_buf: *std.array_list.Managed(String),
|
||||
pkg_map: *const PkgMap(void),
|
||||
relative_path: string,
|
||||
path_buf: []u8,
|
||||
) error{ OutOfMemory, WriteFailed }!void {
|
||||
defer optional_peers_buf.clearRetainingCapacity();
|
||||
// any - have any properties been written
|
||||
@@ -878,7 +834,10 @@ pub const Stringifier = struct {
|
||||
const group_name, const group_behavior = group;
|
||||
|
||||
var first = true;
|
||||
for (pkg_deps[pkg_id].get(deps_buf)) |*dep| {
|
||||
const dep_slice = pkg_deps[pkg_id];
|
||||
for (dep_slice.begin()..dep_slice.end()) |_dep_id| {
|
||||
const dep_id: DependencyID = @intCast(_dep_id);
|
||||
const dep = &deps_buf[dep_id];
|
||||
if (!dep.behavior.includes(group_behavior)) continue;
|
||||
|
||||
if (dep.behavior.isOptionalPeer()) {
|
||||
@@ -914,12 +873,10 @@ pub const Stringifier = struct {
|
||||
bun.fmt.formatJSONStringUTF8(version, .{}),
|
||||
});
|
||||
|
||||
if (dep.behavior.peer and !dep.behavior.optional and pkg_map.map.count() > 0) {
|
||||
pkg_map.findResolution(relative_path, dep, buf, path_buf) catch |err| {
|
||||
if (err == error.Unresolvable) {
|
||||
try optional_peers_buf.append(dep.name);
|
||||
}
|
||||
};
|
||||
// If a required peer dependency is not resolved, add it to optionalPeers
|
||||
// to prevent lockfile loading errors when the peer is not installed.
|
||||
if (dep.behavior.peer and !dep.behavior.optional and resolution_buf[dep_id] == invalid_package_id) {
|
||||
try optional_peers_buf.append(dep.name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2253,7 +2210,6 @@ const invalid_package_id = Install.invalid_package_id;
|
||||
|
||||
const BinaryLockfile = bun.install.Lockfile;
|
||||
const DependencySlice = BinaryLockfile.DependencySlice;
|
||||
const LoadResult = BinaryLockfile.LoadResult;
|
||||
const Meta = BinaryLockfile.Package.Meta;
|
||||
|
||||
const Npm = Install.Npm;
|
||||
|
||||
@@ -288,6 +288,9 @@ exports[`yarn.lock migration basic migration with realistic complex yarn.lock: c
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
},
|
||||
"optionalPeers": [
|
||||
"react",
|
||||
],
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { beforeAll, describe, expect, setDefaultTimeout, test } from "bun:test";
|
||||
import fs from "fs";
|
||||
import { bunEnv, bunExe, tempDirWithFiles } from "harness";
|
||||
import { join } from "path";
|
||||
|
||||
beforeAll(() => {
|
||||
setDefaultTimeout(1000 * 60 * 5);
|
||||
});
|
||||
|
||||
describe("yarn.lock migration basic", () => {
|
||||
test("simple yarn.lock migration produces correct bun.lock", async () => {
|
||||
const tempDir = tempDirWithFiles("yarn-migration-simple", {
|
||||
|
||||
153
test/regression/issue/26046.test.ts
Normal file
153
test/regression/issue/26046.test.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { bunEnv, bunExe, tempDir } from "harness";
|
||||
|
||||
// Test for GitHub issue #26046
|
||||
// `bun pm ls` fails with `error: Error loading lockfile: InvalidPackageInfo`
|
||||
// when a package has required (non-optional) peer dependencies that are not installed.
|
||||
|
||||
test("lockfile with optionalPeers loads successfully for bun pm ls", async () => {
|
||||
// This test verifies that when a lockfile properly includes unresolved peer deps
|
||||
// in optionalPeers (which our fix ensures during lockfile writing), the lockfile
|
||||
// can be loaded without errors by commands like `bun pm ls`.
|
||||
|
||||
// Create a lockfile that simulates the FIXED behavior:
|
||||
// - Package has a peer dependency that is NOT resolved (not installed)
|
||||
// - The peer dependency IS in optionalPeers (because our fix adds it there)
|
||||
const lockfile = {
|
||||
lockfileVersion: 1,
|
||||
configVersion: 1,
|
||||
workspaces: {
|
||||
"": {
|
||||
name: "test-issue-26046",
|
||||
devDependencies: {
|
||||
"test-pkg": "1.0.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
packages: {
|
||||
"test-pkg": [
|
||||
"test-pkg@1.0.0",
|
||||
"",
|
||||
{
|
||||
peerDependencies: {
|
||||
"unresolved-peer": "*",
|
||||
},
|
||||
// Key: the unresolved peer IS in optionalPeers
|
||||
// Our fix ensures this happens during lockfile writing
|
||||
optionalPeers: ["unresolved-peer"],
|
||||
},
|
||||
"sha512-test==",
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
using dir = tempDir("issue-26046", {
|
||||
"package.json": JSON.stringify({
|
||||
name: "test-issue-26046",
|
||||
devDependencies: {
|
||||
"test-pkg": "1.0.0",
|
||||
},
|
||||
}),
|
||||
"bun.lock": JSON.stringify(lockfile, null, 2),
|
||||
// Create a fake node_modules structure so bun pm ls doesn't try to install
|
||||
"node_modules/test-pkg/package.json": JSON.stringify({
|
||||
name: "test-pkg",
|
||||
version: "1.0.0",
|
||||
peerDependencies: {
|
||||
"unresolved-peer": "*",
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
// Run bun pm ls - with the fix, this should work because the unresolved
|
||||
// peer dep is properly marked as optional in the lockfile
|
||||
await using lsProc = Bun.spawn({
|
||||
cmd: [bunExe(), "pm", "ls"],
|
||||
env: bunEnv,
|
||||
cwd: String(dir),
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [lsStdout, lsStderr, lsExitCode] = await Promise.all([
|
||||
lsProc.stdout.text(),
|
||||
lsProc.stderr.text(),
|
||||
lsProc.exited,
|
||||
]);
|
||||
|
||||
// Should not have InvalidPackageInfo error
|
||||
expect(lsStderr).not.toContain("InvalidPackageInfo");
|
||||
expect(lsStdout).toContain("test-pkg");
|
||||
expect(lsExitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("lockfile WITHOUT optionalPeers fails for unresolved required peer deps", async () => {
|
||||
// This test documents the bug that existed before the fix:
|
||||
// When a lockfile has a peer dep that's NOT in optionalPeers and NOT installed,
|
||||
// loading the lockfile fails with InvalidPackageInfo.
|
||||
//
|
||||
// NOTE: This test shows the BROKEN behavior that users experienced.
|
||||
// The fix prevents new lockfiles from being written in this broken state.
|
||||
|
||||
// Create a lockfile that simulates the BROKEN (pre-fix) behavior:
|
||||
// - Package has a required peer dependency that is NOT resolved
|
||||
// - The peer dependency is NOT in optionalPeers (the bug)
|
||||
const lockfile = {
|
||||
lockfileVersion: 1,
|
||||
configVersion: 1,
|
||||
workspaces: {
|
||||
"": {
|
||||
name: "test-issue-26046",
|
||||
devDependencies: {
|
||||
"test-pkg": "1.0.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
packages: {
|
||||
"test-pkg": [
|
||||
"test-pkg@1.0.0",
|
||||
"",
|
||||
{
|
||||
peerDependencies: {
|
||||
"unresolved-peer": "*",
|
||||
},
|
||||
// Bug: the unresolved peer is NOT in optionalPeers
|
||||
// This causes InvalidPackageInfo when loading
|
||||
},
|
||||
"sha512-test==",
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
using dir = tempDir("issue-26046-broken", {
|
||||
"package.json": JSON.stringify({
|
||||
name: "test-issue-26046",
|
||||
devDependencies: {
|
||||
"test-pkg": "1.0.0",
|
||||
},
|
||||
}),
|
||||
"bun.lock": JSON.stringify(lockfile, null, 2),
|
||||
"node_modules/test-pkg/package.json": JSON.stringify({
|
||||
name: "test-pkg",
|
||||
version: "1.0.0",
|
||||
peerDependencies: {
|
||||
"unresolved-peer": "*",
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
// Run bun pm ls - this should fail with InvalidPackageInfo
|
||||
await using lsProc = Bun.spawn({
|
||||
cmd: [bunExe(), "pm", "ls"],
|
||||
env: bunEnv,
|
||||
cwd: String(dir),
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [_, lsStderr, lsExitCode] = await Promise.all([lsProc.stdout.text(), lsProc.stderr.text(), lsProc.exited]);
|
||||
|
||||
// This shows the broken behavior: lockfile loading fails
|
||||
expect(lsStderr).toContain("InvalidPackageInfo");
|
||||
expect(lsExitCode).toBe(1);
|
||||
});
|
||||
Reference in New Issue
Block a user