Compare commits

...

4 Commits

Author SHA1 Message Date
Claude Bot
3f3a88e1c4 test: normalize path separators for cross-platform comparison
Normalize both the output and expected paths to forward slashes
to ensure the comparison works on Windows where bun may output
native backslash separators.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 23:14:52 +00:00
Claude Bot
96a44d0d2c test: use platform-independent path for absolute cache test
Use tempDirWithFiles instead of hardcoded /tmp/ path to ensure
the test works on Windows.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 23:00:51 +00:00
Claude Bot
ca0b5f23ee address review feedback
- expandTilde: always duplicate input to ensure memory ownership
- expandTilde: fix Windows path separator logic
- tests: use cloned env to avoid race conditions with shared object
- tests: use 'await using' pattern for proper resource cleanup

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 22:45:02 +00:00
Claude Bot
1a0785d486 fix(install): expand tilde (~) in cache directory paths from config files
Previously, Bun would treat `~` as a literal character in cache paths
specified in `.npmrc` or `bunfig.toml`, creating directories with `~` in
their names (e.g., `/project/~/.dev/npm`) instead of expanding to the
user's home directory.

This adds a `strings.expandTilde` utility function and uses it when
parsing the `cache` option in `.npmrc` and `bunfig.toml` configuration
files, matching npm's behavior.

Closes #26715

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 22:30:48 +00:00
4 changed files with 198 additions and 3 deletions

View File

@@ -696,7 +696,7 @@ pub const Bunfig = struct {
}
if (cache.asString(allocator)) |value| {
install.cache_directory = value;
install.cache_directory = try bun.strings.expandTilde(allocator, value);
break :load;
}
@@ -715,7 +715,7 @@ pub const Bunfig = struct {
if (cache.get("dir")) |directory| {
if (directory.asString(allocator)) |value| {
install.cache_directory = value;
install.cache_directory = try bun.strings.expandTilde(allocator, value);
}
}
}

View File

@@ -999,7 +999,7 @@ pub fn loadNpmrc(
if (out.asProperty("cache")) |query| {
if (query.expr.asUtf8StringLiteral()) |str| {
install.cache_directory = try allocator.dupe(u8, str);
install.cache_directory = try bun.strings.expandTilde(allocator, str);
} else if (query.expr.asBool()) |b| {
install.disable_cache = !b;
}

View File

@@ -2383,6 +2383,36 @@ pub const basename = paths_.basename;
pub const log = bun.Output.scoped(.STR, .hidden);
pub const grapheme = @import("./immutable/grapheme.zig");
/// Expands a leading tilde (~) in a path to the user's home directory.
/// Always returns a newly allocated string owned by the allocator.
/// The tilde must be followed by a path separator or be the entire string (e.g., "~" or "~/foo").
/// Paths like "~username" are not expanded but are still duplicated.
pub fn expandTilde(allocator: std.mem.Allocator, path: []const u8) OOM![]const u8 {
if (path.len == 0 or path[0] != '~') {
return try allocator.dupe(u8, path);
}
// Must be just "~" or "~/..." or "~\..." on Windows (not "~foo" which means another user's home)
if (path.len > 1) {
const is_separator = path[1] == '/' or (Environment.isWindows and path[1] == '\\');
if (!is_separator) {
return try allocator.dupe(u8, path);
}
}
const home_dir = bun.env_var.HOME.get() orelse return try allocator.dupe(u8, path);
if (path.len == 1) {
// Just "~"
return try allocator.dupe(u8, home_dir);
}
// "~/..." - join home with the rest
const rest = path[1..]; // includes the leading /
const result = try allocator.alloc(u8, home_dir.len + rest.len);
@memcpy(result[0..home_dir.len], home_dir);
@memcpy(result[home_dir.len..], rest);
return result;
}
pub const CodePoint = i32;
const string = []const u8;

View File

@@ -0,0 +1,165 @@
import { describe, expect, test } from "bun:test";
import { rm } from "fs/promises";
import { bunEnv, bunExe, stderrForInstall, tempDirWithFiles } from "harness";
import { join } from "path";
// https://github.com/oven-sh/bun/issues/26715
// Bun should expand ~ (tilde) to the home directory in cache paths from .npmrc and bunfig.toml
describe("tilde expansion in cache paths", () => {
test(".npmrc cache path expands tilde to home directory", async () => {
const testDir = tempDirWithFiles("tilde-npmrc-", {
".npmrc": "cache=~/.bun-test-cache-dir",
"package.json": JSON.stringify({ name: "foo", version: "1.0.0" }),
});
// Remove any bunfig.toml that might override the .npmrc setting
await rm(join(testDir, "bunfig.toml"), { force: true });
// Clone env to avoid mutating shared object
const testEnv = { ...bunEnv };
delete testEnv.BUN_INSTALL_CACHE_DIR;
await using proc = Bun.spawn({
cmd: [bunExe(), "pm", "cache"],
cwd: testDir,
env: testEnv,
stdout: "pipe",
stderr: "pipe",
});
const out = (await proc.stdout.text()).trim();
const err = stderrForInstall(await proc.stderr.text());
expect(err).toBeEmpty();
// Should NOT contain literal "~" - it should be expanded to the home directory
expect(out).not.toContain("/~/");
expect(out).not.toStartWith("~");
// Should contain the home directory path
const homeDir = process.env.HOME || process.env.USERPROFILE;
expect(out).toStartWith(homeDir!);
expect(out).toEndWith(".bun-test-cache-dir");
expect(await proc.exited).toBe(0);
});
test("bunfig.toml cache string expands tilde to home directory", async () => {
const testDir = tempDirWithFiles("tilde-bunfig-str-", {
"bunfig.toml": '[install]\ncache = "~/.bun-test-cache-dir"',
"package.json": JSON.stringify({ name: "foo", version: "1.0.0" }),
});
const testEnv = { ...bunEnv };
delete testEnv.BUN_INSTALL_CACHE_DIR;
await using proc = Bun.spawn({
cmd: [bunExe(), "pm", "cache"],
cwd: testDir,
env: testEnv,
stdout: "pipe",
stderr: "pipe",
});
const out = (await proc.stdout.text()).trim();
const err = stderrForInstall(await proc.stderr.text());
expect(err).toBeEmpty();
expect(out).not.toContain("/~/");
expect(out).not.toStartWith("~");
const homeDir = process.env.HOME || process.env.USERPROFILE;
expect(out).toStartWith(homeDir!);
expect(out).toEndWith(".bun-test-cache-dir");
expect(await proc.exited).toBe(0);
});
test("bunfig.toml cache.dir expands tilde to home directory", async () => {
const testDir = tempDirWithFiles("tilde-bunfig-dir-", {
"bunfig.toml": '[install.cache]\ndir = "~/.bun-test-cache-dir"',
"package.json": JSON.stringify({ name: "foo", version: "1.0.0" }),
});
const testEnv = { ...bunEnv };
delete testEnv.BUN_INSTALL_CACHE_DIR;
await using proc = Bun.spawn({
cmd: [bunExe(), "pm", "cache"],
cwd: testDir,
env: testEnv,
stdout: "pipe",
stderr: "pipe",
});
const out = (await proc.stdout.text()).trim();
const err = stderrForInstall(await proc.stderr.text());
expect(err).toBeEmpty();
expect(out).not.toContain("/~/");
expect(out).not.toStartWith("~");
const homeDir = process.env.HOME || process.env.USERPROFILE;
expect(out).toStartWith(homeDir!);
expect(out).toEndWith(".bun-test-cache-dir");
expect(await proc.exited).toBe(0);
});
test("paths without tilde are not affected", async () => {
// Use a platform-independent absolute path within the test directory
const testDir = tempDirWithFiles("no-tilde-", {
"package.json": JSON.stringify({ name: "foo", version: "1.0.0" }),
});
const absoluteCachePath = join(testDir, "absolute-cache-dir");
// Write bunfig.toml with forward slashes (works on all platforms)
const configPath = absoluteCachePath.replace(/\\/g, "/");
await Bun.write(join(testDir, "bunfig.toml"), `[install]\ncache = "${configPath}"`);
const testEnv = { ...bunEnv };
delete testEnv.BUN_INSTALL_CACHE_DIR;
await using proc = Bun.spawn({
cmd: [bunExe(), "pm", "cache"],
cwd: testDir,
env: testEnv,
stdout: "pipe",
stderr: "pipe",
});
const out = (await proc.stdout.text()).trim();
const err = stderrForInstall(await proc.stderr.text());
expect(err).toBeEmpty();
// Normalize both paths to forward slashes for comparison
expect(out.replace(/\\/g, "/")).toBe(configPath);
expect(await proc.exited).toBe(0);
});
test("~username paths are not expanded (only ~ is expanded)", async () => {
// ~username syntax (for other users' home dirs) should not be expanded
// as it would require looking up user info
const testDir = tempDirWithFiles("tilde-user-", {
"bunfig.toml": '[install]\ncache = "~otheruser/cache"',
"package.json": JSON.stringify({ name: "foo", version: "1.0.0" }),
});
const testEnv = { ...bunEnv };
delete testEnv.BUN_INSTALL_CACHE_DIR;
await using proc = Bun.spawn({
cmd: [bunExe(), "pm", "cache"],
cwd: testDir,
env: testEnv,
stdout: "pipe",
stderr: "pipe",
});
const out = (await proc.stdout.text()).trim();
const err = stderrForInstall(await proc.stderr.text());
expect(err).toBeEmpty();
// Should still contain ~otheruser since we don't expand that form
expect(out).toContain("~otheruser");
expect(await proc.exited).toBe(0);
});
});