Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions src/resolver/resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2445,16 +2445,33 @@ impl<'a> Resolver<'a> {
/// See `assertValidCacheKey` for requirements on the input
pub fn bust_dir_cache(&mut self, path: &[u8]) -> bool {
Self::assert_valid_cache_key(path);
// A symlinked directory is also cached under its real path
// (`DirInfo.abs_real_path`), and resolve results are realpath'd, so a
// follow-up resolve reads the real-path key. Bust that alias too.
Comment thread
robobun marked this conversation as resolved.
let real_path: Option<&'static [u8]> = self
.dir_cache_mut()
.get(path)
.map(|info| strings::without_trailing_slash_windows_path(info.abs_real_path))
.filter(|real| !real.is_empty() && *real != path);
let first_bust = self.fs_mut().fs.bust_entries_cache(path);
let second_bust = self.dir_cache_mut().remove(path);
let alias_bust = match real_path {
Some(real) => {
let a = self.fs_mut().fs.bust_entries_cache(real);
let b = self.dir_cache_mut().remove(real);
a || b
}
None => false,
};
bun_core::scoped_log!(
ResolverDev,
"Bust {} = {}, {}",
"Bust {} = {}, {}, alias {}",
bstr::BStr::new(path),
first_bust,
second_bust
second_bust,
alias_bust
);
first_bust || second_bust
first_bust || second_bust || alias_bust
}

/// bust both the named file and a parent directory, because `./hello` can resolve
Expand Down
46 changes: 46 additions & 0 deletions test/js/bun/resolve/resolve.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -899,6 +899,52 @@ describe.if(isWindows)("#30839 - imports entry pointing at a scoped package", ()
});
});

// A directory reached through a symlink is cached under two keys: the symlink
// path and its real path. Requiring a file in it caches both listings; when a
// sibling file is created afterwards, only the symlink-side key was busted, so
// the follow-up resolve of the realpath'd result hit a stale listing and
// failed with "Cannot find module ... from ''". macOS always hits this because
// os.tmpdir() is behind the /var -> /private/var symlink.
// https://github.com/oven-sh/bun/issues/40585
it("requires a newly created sibling .ts file in a directory behind a symlink", async () => {
using dir = tempDir("resolve-symlink-alias", {
"main.ts": `
import { mkdir, writeFile } from "node:fs/promises";
import { createRequire } from "node:module";
import { join } from "node:path";
import { pathToFileURL } from "node:url";

const dir = join(import.meta.dir, "link", "warm");
await mkdir(dir, { recursive: true });
const a = join(dir, "a.ts");
await writeFile(a, "export const A = 1;\\n");
createRequire(pathToFileURL(a).href)(a);
const b = join(dir, "b.ts");
await writeFile(b, "export const B = 2;\\n");
const result = createRequire(pathToFileURL(b).href)(b);
console.log(JSON.stringify(result));
`,
});
const root = String(dir);
mkdirSync(join(root, "real"));
// The symlink must exist before the child process starts, like /var on
// macOS. "junction" is the Windows-appropriate symlink kind for directories.
symlinkSync(join(root, "real"), join(root, "link"), "junction");

await using proc = Bun.spawn({
cmd: [bunExe(), "main.ts"],
env: bunEnv,
cwd: root,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

expect(stderr).toBe("");
expect(stdout).toBe('{"B":2}\n');
expect(exitCode).toBe(0);
});

// dirInfoCachedMaybeLog reads the rfs.entries cache without checking the union
// tag. If readDirectory() previously failed with a non-ENOENT error (e.g.
// EACCES), a `.err` variant is stored there; re-resolving the directory after
Expand Down
Loading