Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
0b006e6bdc Fix frozen lockfile false positive when packages have install scripts
When a workspace dependency appears in both `dependencies` and
`devDependencies`, and there are packages with install scripts in the
dependency tree (commonly from GitHub/tarball dependencies with many
transitive deps), `bun install --frozen-lockfile` would incorrectly
fail claiming the lockfile had changed.

Root cause: The `lockfile_before_clean` snapshot was taken before
lifecycle scripts were appended to the lockfile (lines 651-712), but
the frozen lockfile comparison happened after. This meant we were
comparing the lockfile with scripts appended against the snapshot
without scripts, causing a false mismatch.

The fix moves the snapshot to after script appending, ensuring we
compare lockfiles in the same state. This issue typically manifests
with GitHub dependencies since they often have many transitive
dependencies, increasing the likelihood of packages with install
scripts being present.

Fixes the scenario where:
- Workspace dep in both deps and devDeps
- GitHub/tarball dependency with transitive deps that have scripts
- Running `bun install --frozen-lockfile` or `bun ci`

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-21 23:39:26 +00:00
2 changed files with 55 additions and 3 deletions

View File

@@ -588,9 +588,6 @@ pub fn installWithManager(
try manager.log.print(Output.errorWriter());
manager.log.reset();
// This operation doesn't perform any I/O, so it should be relatively cheap.
const lockfile_before_clean = manager.lockfile;
manager.lockfile = try manager.lockfile.cleanWithLogger(
manager,
manager.update_requests,
@@ -717,6 +714,9 @@ pub fn installWithManager(
const packages_len_before_install = manager.lockfile.packages.len;
// Take the snapshot AFTER scripts are appended so frozen lockfile comparison is accurate
const lockfile_before_clean = manager.lockfile;
if (manager.options.enable.frozen_lockfile and load_result != .not_found) frozen_lockfile: {
if (load_result.loadedFromTextLockfile()) {
if (bun.handleOom(manager.lockfile.eql(lockfile_before_clean, packages_len_before_install, manager.allocator))) {

View File

@@ -20,6 +20,7 @@ import {
joinP,
readdirSorted,
runBunInstall,
tempDir,
tempDirWithFiles,
textLockfile,
toBeValidBin,
@@ -6314,6 +6315,57 @@ registry = "${root_url}"
expect(await exited).toBe(1);
});
it("frozen lockfile should work with workspace deps in both deps and devDeps", async () => {
using dir = tempDir("frozen-lockfile-workspace", {
"package.json": JSON.stringify({
name: "root",
private: true,
workspaces: ["workspaces/*"],
}),
"workspaces/api/package.json": JSON.stringify({
name: "@app/api",
dependencies: {
"@app/env": "workspace:*",
},
devDependencies: {
"@app/env": "workspace:*",
"@vercel/style-guide": "github:nikuscs/style-guide#canary",
},
}),
"workspaces/env/package.json": JSON.stringify({
name: "@app/env",
version: "1.0.0",
}),
});
// First install
await using proc1 = Bun.spawn({
cmd: [bunExe(), "install"],
cwd: String(dir),
env: bunEnv,
stderr: "pipe",
stdout: "pipe",
});
const [stdout1, stderr1, exitCode1] = await Promise.all([proc1.stdout.text(), proc1.stderr.text(), proc1.exited]);
expect(exitCode1).toBe(0);
// Second install with --frozen-lockfile should succeed
await using proc2 = Bun.spawn({
cmd: [bunExe(), "install", "--frozen-lockfile"],
cwd: String(dir),
env: bunEnv,
stderr: "pipe",
stdout: "pipe",
});
const [stdout2, stderr2, exitCode2] = await Promise.all([proc2.stdout.text(), proc2.stderr.text(), proc2.exited]);
if (exitCode2 !== 0) {
console.error("STDOUT:", stdout2);
console.error("STDERR:", stderr2);
}
expect(exitCode2).toBe(0);
});
it("should perform bin-linking across multiple dependencies", async () => {
const foo_package = JSON.stringify({
name: "foo",