Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
496829384c fix(install): prevent --no-save from writing lockfile during migration
When using `bun install --no-save`, the lockfile should never be written
to disk, even during format/version conversions. Previously, lockfile
format conversions (binary to text) and version updates would bypass the
`--no-save` flag.

This fix distinguishes between `--no-save` and `--frozen-lockfile`:
- `--no-save`: Never writes any lockfile (new behavior)
- `--frozen-lockfile`: Still allows format/version conversions

The distinction is made by checking `write_package_json`, which is only
false when `--no-save` is used (both flags set `save_lockfile` to false).

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-11 23:51:22 +00:00
2 changed files with 179 additions and 1 deletions

View File

@@ -838,7 +838,10 @@ pub fn installWithManager(
// It's unnecessary work to re-save the lockfile if there are no changes
const should_save_lockfile =
(load_result == .ok and ((load_result.ok.format == .binary and save_format == .text) or
// Format/version conversions are allowed with --frozen-lockfile but NOT with --no-save
// When --no-save is used, save_lockfile is false AND write_package_json is false
// When --frozen-lockfile is used, only save_lockfile is false
(manager.options.do.write_package_json and load_result == .ok and ((load_result.ok.format == .binary and save_format == .text) or
// make sure old versions are updated
load_result.ok.format == .text and save_format == .text and manager.lockfile.text_lockfile_version != TextLockfile.Version.current)) or

View File

@@ -0,0 +1,175 @@
import { expect, test } from "bun:test";
import { existsSync, mkdtempSync, writeFileSync } from "fs";
import { bunEnv, bunExe } from "harness";
import { tmpdir } from "os";
import { join } from "path";
test("--no-save should not create bun.lock when migrating from package-lock.json", async () => {
const tempDir = mkdtempSync(join(tmpdir(), "bun-test-no-save-"));
// Create a package.json
writeFileSync(
join(tempDir, "package.json"),
JSON.stringify({
name: "test-no-save",
dependencies: {
"is-number": "^7.0.0",
},
}),
);
// Create a package-lock.json (simulating npm project)
writeFileSync(
join(tempDir, "package-lock.json"),
JSON.stringify({
name: "test-no-save",
version: "1.0.0",
lockfileVersion: 2,
requires: true,
packages: {
"": {
name: "test-no-save",
dependencies: {
"is-number": "^7.0.0",
},
},
"node_modules/is-number": {
version: "7.0.0",
resolved: "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
integrity: "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
},
},
dependencies: {
"is-number": {
version: "7.0.0",
resolved: "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
integrity: "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
},
},
}),
);
// Run bun install --no-save
await using proc = Bun.spawn({
cmd: [bunExe(), "install", "--no-save"],
env: bunEnv,
cwd: tempDir,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(0);
// Verify node_modules was created and package was installed
expect(existsSync(join(tempDir, "node_modules", "is-number"))).toBe(true);
// Verify bun.lock was NOT created
expect(existsSync(join(tempDir, "bun.lock"))).toBe(false);
expect(existsSync(join(tempDir, "bun.lockb"))).toBe(false);
});
test("--no-save should not create bun.lock when migrating from yarn.lock", async () => {
const tempDir = mkdtempSync(join(tmpdir(), "bun-test-no-save-yarn-"));
// Create a package.json
writeFileSync(
join(tempDir, "package.json"),
JSON.stringify({
name: "test-no-save-yarn",
dependencies: {
"is-odd": "^3.0.0",
},
}),
);
// Create a yarn.lock (simulating yarn project)
writeFileSync(
join(tempDir, "yarn.lock"),
`# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
is-number@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/is-number/-/is-number-6.0.0.tgz#2d1df8dd6d179fb8e8b69e1d0d5a2e8e3f2e3c5e"
integrity sha512-XXXXXXXxXXXXXXXXXXXXXXXXXXXXXXXXXXXXX==
is-odd@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/is-odd/-/is-odd-3.0.1.tgz#4c4c837a7d1e8e5a2a7b1f7b3b1f4b7e7e1c8f9e"
integrity sha512-XXXXXXXxXXXXXXXXXXXXXXXXXXXXXXXXXXXXX==
dependencies:
is-number "^6.0.0"
`,
);
// Run bun install --no-save
await using proc = Bun.spawn({
cmd: [bunExe(), "install", "--no-save"],
env: bunEnv,
cwd: tempDir,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(0);
// Verify node_modules was created and packages were installed
expect(existsSync(join(tempDir, "node_modules", "is-odd"))).toBe(true);
// Verify bun.lock was NOT created
expect(existsSync(join(tempDir, "bun.lock"))).toBe(false);
expect(existsSync(join(tempDir, "bun.lockb"))).toBe(false);
});
test("--no-save should not update existing bun.lock format/version", async () => {
const tempDir = mkdtempSync(join(tmpdir(), "bun-test-no-save-existing-"));
// Create a package.json
writeFileSync(
join(tempDir, "package.json"),
JSON.stringify({
name: "test-no-save-existing",
dependencies: {
"is-number": "^7.0.0",
},
}),
);
// Create an old-format bun.lockb by running install first
await using proc1 = Bun.spawn({
cmd: [bunExe(), "install"],
env: bunEnv,
cwd: tempDir,
stdout: "pipe",
stderr: "pipe",
});
await proc1.exited;
// Get the initial lockfile
const lockbPath = join(tempDir, "bun.lockb");
const lockPath = join(tempDir, "bun.lock");
const initialLockbExists = existsSync(lockbPath);
const initialLockExists = existsSync(lockPath);
// Run bun install --no-save (which might try to convert format)
await using proc2 = Bun.spawn({
cmd: [bunExe(), "install", "--no-save"],
env: bunEnv,
cwd: tempDir,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc2.stdout.text(), proc2.stderr.text(), proc2.exited]);
expect(exitCode).toBe(0);
// Verify lockfile format hasn't changed
expect(existsSync(lockbPath)).toBe(initialLockbExists);
expect(existsSync(lockPath)).toBe(initialLockExists);
});