Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
a26d599500 fix(install): prevent duplicate dependencies from corrupting bun.lock
When package.json contained duplicate dependency entries within the same
group, bun install would write both duplicates into the binary lockfile
and subsequently into bun.lock. After fixing the package.json, the
corrupt lockfile would persist because:

1. parseDependency() warned about duplicates but still returned the
   duplicate entry, causing it to be appended to the dependency list.
   Now it replaces the existing entry and returns null (matching the
   existing behavior for optional dependencies).

2. parseAppendDependencies() (text lockfile reader) had no deduplication,
   so duplicate JSON keys flowed directly into the binary lockfile. Now
   it checks for existing entries and replaces them instead of appending.

Closes #17715

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-19 09:26:59 +00:00
3 changed files with 100 additions and 1 deletions

View File

@@ -1258,6 +1258,16 @@ pub fn Package(comptime SemverIntType: type) type {
"Duplicate dependency: \"{s}\" specified in package.json",
.{external_alias.slice(buf)},
);
// Replace the existing entry with the duplicate (last-write-wins)
// and return null to prevent appending a second entry.
for (package_dependencies[0..dependencies_count]) |*package_dep| {
if (package_dep.name_hash == this_dep.name_hash) {
package_dep.* = this_dep;
break;
}
}
return null;
}
}

View File

@@ -2188,7 +2188,20 @@ fn parseAppendDependencies(
}
}
try lockfile.buffers.dependencies.append(allocator, dep);
// Deduplicate: if a dependency with the same name_hash was already
// added (e.g. from a corrupt lockfile with duplicate JSON keys),
// replace the existing entry instead of appending a new one.
var found_existing = false;
for (lockfile.buffers.dependencies.items[off..]) |*existing_dep| {
if (existing_dep.name_hash == name_hash) {
existing_dep.* = dep;
found_existing = true;
break;
}
}
if (!found_existing) {
try lockfile.buffers.dependencies.append(allocator, dep);
}
}
}
}

View File

@@ -0,0 +1,76 @@
import { bunEnv, bunExe, tempDirWithFiles } from "harness";
import { join } from "path";
it("duplicate dependency in same group should not corrupt bun.lock", () => {
// Create package.json with a duplicate dependency entry in the same group.
// JSON.stringify won't produce duplicates, so we hand-craft the JSON.
const package_json = `{
"name": "bun-reproduce-17715",
"dependencies": {
"empty-package-for-bun-test-runner": "1.0.0",
"is-number": "^7.0.0",
"empty-package-for-bun-test-runner": "1.0.0"
}
}`;
const dir = tempDirWithFiles("17715", {
"package.json": package_json,
});
// First install: should warn about duplicate but not corrupt the lockfile
const proc1 = Bun.spawnSync([bunExe(), "install"], {
env: bunEnv,
cwd: dir,
stderr: "pipe",
stdout: "pipe",
});
const stderr1 = proc1.stderr.toString("utf-8");
expect(stderr1).toContain("warn: Duplicate dependency");
expect(proc1.exitCode).toBe(0);
// Verify the lockfile does NOT contain duplicate entries
const lockContent1 = require("fs").readFileSync(join(dir, "bun.lock"), "utf-8");
const occurrences1 = lockContent1.split('"empty-package-for-bun-test-runner"').length - 1;
// The package name should only appear once in the dependencies section of the workspace.
// It may appear elsewhere (e.g., in the packages section), but there should be no
// duplicate key in the workspace dependencies object.
const workspaceMatch1 = lockContent1.match(/"dependencies":\s*\{[^}]*\}/);
expect(workspaceMatch1).not.toBeNull();
const depsSection1 = workspaceMatch1![0];
const depsOccurrences1 = depsSection1.split('"empty-package-for-bun-test-runner"').length - 1;
expect(depsOccurrences1).toBe(1);
// Now fix the package.json by removing the duplicate
require("fs").writeFileSync(
join(dir, "package.json"),
JSON.stringify({
name: "bun-reproduce-17715",
dependencies: {
"empty-package-for-bun-test-runner": "1.0.0",
"is-number": "^7.0.0",
},
}),
);
// Second install: should not show the duplicate warning anymore
const proc2 = Bun.spawnSync([bunExe(), "install"], {
env: bunEnv,
cwd: dir,
stderr: "pipe",
stdout: "pipe",
});
const stderr2 = proc2.stderr.toString("utf-8");
expect(stderr2).not.toContain("warn: Duplicate");
expect(stderr2).not.toContain("Duplicate key");
expect(proc2.exitCode).toBe(0);
// Verify the lockfile is clean after the second install
const lockContent2 = require("fs").readFileSync(join(dir, "bun.lock"), "utf-8");
const workspaceMatch2 = lockContent2.match(/"dependencies":\s*\{[^}]*\}/);
expect(workspaceMatch2).not.toBeNull();
const depsSection2 = workspaceMatch2![0];
const depsOccurrences2 = depsSection2.split('"empty-package-for-bun-test-runner"').length - 1;
expect(depsOccurrences2).toBe(1);
});