Compare commits

...

5 Commits

Author SHA1 Message Date
Claude Bot
d5985e731c fix: use strings.eqlComptime and exercise stat path in dotted-dir test
- Replace std.mem.eql with strings.eqlComptime for "." and ".." checks
  to follow project conventions.
- Remove trailing slash from dotted-directory test so the stat-based
  detection is actually exercised for directories with dots in their name.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-06 23:55:31 +00:00
Claude Bot
934ac544fd fix: use @intCast for stat.mode in ISDIR check for Windows compat
On Windows, uv_stat_t.mode is u64 while S.ISDIR expects i32.
Use @intCast to match the pattern used elsewhere in the codebase.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-06 23:49:09 +00:00
Claude Bot
90682504c2 ci: retrigger build after cancellation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-06 23:36:25 +00:00
Claude Bot
34638cb73f fix: use stat-based detection for file vs directory in Bun.resolveSync
Address review feedback: replace the pure dot-heuristic with a more
robust approach that first tries a filesystem stat to definitively
determine if the `from` path is a file or directory. Falls back to the
extension heuristic only when the path does not exist on disk.

Also handle edge cases: "." and ".." as explicit directories, trailing
dots, and directories with dots in their names. Adds test coverage for
directory paths without trailing slashes and directories with dots.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-06 23:34:07 +00:00
Claude Bot
1d246c7b3f fix(resolve): Bun.resolveSync now accepts file paths as from argument
When `Bun.resolveSync(specifier, from)` is called with a file path as the
`from` argument (e.g., `args.importer` in plugin onResolve hooks), the
resolver now correctly extracts the directory and resolves relative to it.

Previously, `from` was always treated as a directory path, so passing a
file path like `/path/to/main.ts` would cause the resolver to look for
files inside a non-existent directory named `main.ts`.

The fix auto-detects whether `from` is a file path by checking if the
last path component has a file extension. File paths like `main.ts` or
`App.svelte` are detected and handled correctly, while directory paths
(with or without trailing slash) continue to work as before.

Closes #27864

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-06 23:20:33 +00:00
2 changed files with 147 additions and 8 deletions

View File

@@ -808,14 +808,69 @@ fn doResolve(globalThis: *jsc.JSGlobalObject, arguments: []const JSValue) bun.JS
defer specifier_str.deref();
const from_str = try from.toBunString(globalThis);
defer from_str.deref();
return doResolveWithArgs(
globalThis,
specifier_str,
from_str,
is_esm,
false,
false,
);
// Auto-detect whether `from` is a file path or directory path.
// Plugin onResolve hooks pass `args.importer` which is a file path
// (e.g., "/path/to/main.ts"), while users may also pass directory paths
// (e.g., "/path/to/dir/" or import.meta.dir which has no trailing slash).
// When it's a file path, the resolver needs to extract the directory;
// when it's already a directory, use it as-is.
// Heuristic: if the last path component has a file extension (dot after
// the last separator), treat it as a file path.
if (fromLooksLikeFilePath(from_str)) {
return doResolveWithArgs(globalThis, specifier_str, from_str, is_esm, true, false);
}
return doResolveWithArgs(globalThis, specifier_str, from_str, is_esm, false, false);
}
/// Determine whether the `from` argument to Bun.resolveSync/Bun.resolve is a
/// file path (e.g., `args.importer` in plugin onResolve hooks) or a directory.
///
/// Strategy:
/// 1. If the path ends with a separator, it is a directory.
/// 2. Attempt a filesystem stat — if the path exists, use the result.
/// 3. If the path does not exist on disk, fall back to a heuristic:
/// treat it as a file if the last component contains a file extension.
fn fromLooksLikeFilePath(from: bun.String) bool {
const len = from.length();
if (len == 0) return false;
const last_char: u8 = @truncate(from.charAt(len - 1));
if (bun.path.isSepAny(last_char)) return false;
// Convert to a null-terminated path for stat.
const from_utf8 = from.toUTF8(bun.default_allocator);
defer from_utf8.deinit();
const slice = from_utf8.slice();
if (slice.len == 0 or slice.len >= bun.MAX_PATH_BYTES) return false;
var path_buf: bun.PathBuffer = undefined;
@memcpy(path_buf[0..slice.len], slice);
path_buf[slice.len] = 0;
const path_z: [:0]const u8 = path_buf[0..slice.len :0];
switch (bun.sys.stat(path_z)) {
.result => |stat| {
return !bun.S.ISDIR(@intCast(stat.mode));
},
.err => {},
}
// Path does not exist — use a heuristic: scan backwards for a dot
// before a separator. If the last component has an extension
// (e.g., "main.ts", "App.svelte"), treat it as a file path.
// Explicit relative dir names like "." and ".." are not file paths.
if (strings.eqlComptime(slice, ".") or strings.eqlComptime(slice, "..")) return false;
var i = len;
while (i > 0) {
i -= 1;
const c: u8 = @truncate(from.charAt(i));
if (c == '.') return i + 1 < len; // dot must not be trailing
if (bun.path.isSepAny(c)) return false;
}
return false;
}
fn doResolveWithArgs(ctx: *jsc.JSGlobalObject, specifier: bun.String, from: bun.String, is_esm: bool, comptime is_file_path: bool, is_user_require_resolve: bool) bun.JSError!jsc.JSValue {

View File

@@ -0,0 +1,84 @@
import { describe, expect, test } from "bun:test";
import { writeFileSync } from "fs";
import { tempDir } from "harness";
import { join } from "path";
describe("Bun.resolveSync with file path as second argument", () => {
test("resolves when 'from' is a file path", () => {
using dir = tempDir("resolve-file-path", {
"importer.ts": "export {}",
"target.ts": "export const x = 1;",
});
// When passing a file path (like args.importer in plugin onResolve hooks),
// Bun.resolveSync should extract the directory and resolve relative to it.
const result = Bun.resolveSync("./target.ts", join(String(dir), "importer.ts"));
expect(result).toBe(join(String(dir), "target.ts"));
});
test("resolves when 'from' is a directory path with trailing slash", () => {
using dir = tempDir("resolve-dir-path", {
"target.ts": "export const x = 1;",
});
// When passing a directory path with trailing slash, it should work as before.
const result = Bun.resolveSync("./target.ts", String(dir) + "/");
expect(result).toBe(join(String(dir), "target.ts"));
});
test("resolves when 'from' is a directory path without trailing slash", () => {
using dir = tempDir("resolve-dir-no-slash", {
"target.ts": "export const x = 1;",
});
// import.meta.dir returns a directory path without trailing slash.
// This must continue to work as a directory, not be mistaken for a file.
const result = Bun.resolveSync("./target.ts", String(dir));
expect(result).toBe(join(String(dir), "target.ts"));
});
test("resolves newly created files when 'from' is a file path", () => {
using dir = tempDir("resolve-new-file", {
"importer.ts": "export {}",
});
// First, resolve the importer itself to prime the resolver cache
Bun.resolveSync("./importer.ts", String(dir) + "/");
// Create a new file after the cache is primed
const newFilePath = join(String(dir), "new-module.ts");
writeFileSync(newFilePath, "export const y = 2;");
// Resolving the newly created file using a file path as 'from' should work
const result = Bun.resolveSync("./new-module.ts", join(String(dir), "importer.ts"));
expect(result).toBe(newFilePath);
});
test("resolves newly created files when 'from' is a directory path", () => {
using dir = tempDir("resolve-new-file-dir", {
"importer.ts": "export {}",
});
// Prime the resolver cache
Bun.resolveSync("./importer.ts", String(dir) + "/");
// Create a new file
const newFilePath = join(String(dir), "new-module.ts");
writeFileSync(newFilePath, "export const y = 2;");
// Resolving the newly created file using a directory path should work
const result = Bun.resolveSync("./new-module.ts", String(dir) + "/");
expect(result).toBe(newFilePath);
});
test("resolves with directory containing dots in its name", () => {
using dir = tempDir("resolve-dir.with.dots", {
"target.ts": "export const x = 1;",
});
// A directory whose name contains dots (no trailing slash) should be
// correctly identified as a directory via stat, not misclassified as a file.
const result = Bun.resolveSync("./target.ts", String(dir));
expect(result).toBe(join(String(dir), "target.ts"));
});
});