Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
19 changes: 11 additions & 8 deletions src/resolver/resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2445,9 +2445,9 @@ impl<'a> Resolver<'a> {
}
}

/// Bust the directory cache for the given path.
/// See `assertValidCacheKey` for requirements on the input
/// Bust the directory cache for the given path, which may end in a separator.
pub fn bust_dir_cache(&mut self, path: &[u8]) -> bool {
let path = strings::without_trailing_slash_windows_path(path);
Self::assert_valid_cache_key(path);
let first_bust = self.fs_mut().fs.bust_entries_cache(path);
let second_bust = self.dir_cache_mut().remove(path);
Expand All @@ -2469,10 +2469,7 @@ impl<'a> Resolver<'a> {
specifier: &[u8],
) -> bool {
if bun_paths::is_absolute(specifier) {
let dir = bun_paths::dirname_platform(specifier, bun_paths::Platform::AUTO);
let a = self.bust_dir_cache(dir);
let b = self.bust_dir_cache(specifier);
return a || b;
return self.bust_dir_cache_and_parent(specifier);
}

if !(specifier.starts_with(b"./") || specifier.starts_with(b"../")) {
Expand All @@ -2487,10 +2484,16 @@ impl<'a> Resolver<'a> {
bun_paths::Platform::AUTO,
specifier,
);
let dir = bun_paths::dirname_platform(joined, bun_paths::Platform::AUTO);
self.bust_dir_cache_and_parent(joined)
}

fn bust_dir_cache_and_parent(&mut self, path: &[u8]) -> bool {
// Strip before taking the dirname: on Windows the dirname of `a\hello\` is `a\hello`.
let path = strings::without_trailing_slash_windows_path(path);
let dir = bun_paths::dirname_platform(path, bun_paths::Platform::AUTO);

let a = self.bust_dir_cache(dir);
let b = self.bust_dir_cache(joined);
let b = self.bust_dir_cache(path);
a || b
Comment thread
claude[bot] marked this conversation as resolved.
}

Expand Down
27 changes: 27 additions & 0 deletions test/bake/dev/bundle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,33 @@ devTest("importing a file before it is created", {
await c.expectMessage("value: 456");
},
});
// The dev server busts the resolver's directory cache after every failed
// resolution. The cache keys derived from a specifier ending in a slash kept
// that slash, which fails the resolver's cache key assertion in debug and ASAN
// builds and took the whole dev server down instead of showing the error.
devTest("importing a directory with a trailing slash before it is created", {
files: {
"index.html": emptyHtmlFile({
styles: [],
scripts: ["index.ts"],
}),
"index.ts": `
import { abc } from './second/';
console.log('value: ' + abc);
`,
},
async test(dev) {
await using c = await dev.client("/", {
errors: [`index.ts:1:21: error: Could not resolve: "./second/"`],
});

await c.expectReload(async () => {
await dev.write("index.ts", `console.log('value: ' + 789);`);
});

await c.expectMessage("value: 789");
},
});
devTest("default export same-scope handling", {
files: {
"index.html": emptyHtmlFile({
Expand Down
54 changes: 54 additions & 0 deletions test/bundler/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -579,3 +579,57 @@ describe.concurrent("modules that fail to print", () => {
expect(exitCode).toBe(1);
});
});

describe.concurrent("bun build --watch", () => {
// Resolves once `needle` has been written to `stream`. The stream only ends
// early if the process dies, in which case the output so far (the crash
// report) is the error message.
async function outputUntil(stream: ReadableStream<Uint8Array>, needle: string): Promise<string> {
const reader = stream.getReader();
const decoder = new TextDecoder();
let output = "";
try {
while (!output.includes(needle)) {
const { value, done } = await reader.read();
if (done) throw new Error(`stream ended before ${JSON.stringify(needle)} appeared. Output:\n${output}`);
output += decoder.decode(value, { stream: true });
}
} finally {
reader.releaseLock();
}
return output;
}

// In watch mode every failed resolution busts the resolver's directory
// cache for the specifier. The cache keys derived from these specifiers
// ended in a separator, which fails the resolver's cache key assertion in
// debug and ASAN builds, so the process aborted instead of reporting the
// resolution error.
test.each([
["relative import ending in a slash", (_dir: string) => "./missing/"],
["absolute import ending in a slash", (dir: string) => join(dir, "missing") + "/"],
["absolute import with a doubled separator", (dir: string) => dir + path.sep + path.sep + "missing"],
])("reports an unresolvable %s and keeps watching", async (_kind, specifierFor) => {
using dir = tempDir("build-watch-trailing-slash", {});
const specifier = specifierFor(String(dir));
const entry = join(String(dir), "index.ts");
writeFileSync(entry, `import ${JSON.stringify(specifier)};\n`);

await using proc = Bun.spawn({
cmd: [bunExe(), "build", "--watch", "index.ts", "--outdir", "dist"],
env: bunEnv,
cwd: String(dir),
stdin: "ignore",
stdout: "pipe",
stderr: "pipe",
});

await outputUntil(proc.stderr, `error: Could not resolve: "${specifier}"`);
expect(proc.exitCode).toBeNull();

// The failed build left the watcher running: fixing the entry point rebuilds.
writeFileSync(entry, `console.log("fixed");\n`);
expect(await outputUntil(proc.stdout, "index.js")).toContain("Bundled 1 module");
expect(await Bun.file(join(String(dir), "dist", "index.js")).text()).toContain("fixed");
});
});
Loading