mirror of
https://github.com/oven-sh/bun
synced 2026-02-19 23:31:45 +00:00
Compare commits
1 Commits
claude/fix
...
claude/fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8fe8691b2b |
@@ -67,6 +67,7 @@ const bufs = struct {
|
||||
pub threadlocal var check_browser_map: bun.PathBuffer = undefined;
|
||||
pub threadlocal var remap_path: bun.PathBuffer = undefined;
|
||||
pub threadlocal var load_as_file: bun.PathBuffer = undefined;
|
||||
pub threadlocal var load_as_file2: bun.PathBuffer = undefined;
|
||||
pub threadlocal var remap_path_trailing_slash: bun.PathBuffer = undefined;
|
||||
pub threadlocal var path_in_global_disk_cache: bun.PathBuffer = undefined;
|
||||
pub threadlocal var abs_to_rel: bun.PathBuffer = undefined;
|
||||
@@ -3896,6 +3897,18 @@ pub const Resolver = struct {
|
||||
@memcpy(buffer[segment.len..buffer.len][0..ext_to_replace.len], ext_to_replace);
|
||||
|
||||
if (entries.get(buffer)) |query| {
|
||||
// If the case-insensitive lookup matched a differently-cased file,
|
||||
// verify the stem matches to avoid confusing different files.
|
||||
if (query.diff_case != null) {
|
||||
const actual_base = query.entry.base();
|
||||
const actual_stem_end = std.mem.lastIndexOfScalar(u8, actual_base, '.') orelse actual_base.len;
|
||||
if (!bun.strings.eqlLong(segment, actual_base[0..actual_stem_end], false)) {
|
||||
if (r.debug_logs) |*debug| {
|
||||
debug.addNoteFmt("Skipping \"{s}\" (stem case mismatch during rewrite)", .{actual_base});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (query.entry.kind(rfs, r.store_fd) == .file) {
|
||||
if (r.debug_logs) |*debug| {
|
||||
debug.addNoteFmt("Rewrote to \"{s}\" ", .{buffer});
|
||||
@@ -3904,12 +3917,12 @@ pub const Resolver = struct {
|
||||
return LoadResult{
|
||||
.path = brk: {
|
||||
if (query.entry.abs_path.isEmpty()) {
|
||||
const actual_name = query.entry.base();
|
||||
if (query.entry.dir.len > 0 and query.entry.dir[query.entry.dir.len - 1] == std.fs.path.sep) {
|
||||
const parts = [_]string{ query.entry.dir, buffer };
|
||||
const parts = [_]string{ query.entry.dir, actual_name };
|
||||
query.entry.abs_path = PathString.init(r.fs.filename_store.append(@TypeOf(parts), parts) catch unreachable);
|
||||
// the trailing path CAN be missing here
|
||||
} else {
|
||||
const parts = [_]string{ query.entry.dir, std.fs.path.sep_str, buffer };
|
||||
const parts = [_]string{ query.entry.dir, std.fs.path.sep_str, actual_name };
|
||||
query.entry.abs_path = PathString.init(r.fs.filename_store.append(@TypeOf(parts), parts) catch unreachable);
|
||||
}
|
||||
}
|
||||
@@ -3954,18 +3967,38 @@ pub const Resolver = struct {
|
||||
}
|
||||
|
||||
if (entries.get(file_name)) |query| {
|
||||
if (query.diff_case != null) {
|
||||
// The case-insensitive lookup matched a file with different casing.
|
||||
// Check if the import stem (e.g. "todos") matches the found file's
|
||||
// stem (e.g. "Todos"). If the stems differ, this is a different file
|
||||
// that only matched because the directory lookup is case-insensitive.
|
||||
// Skip it so the resolver can continue trying other extensions.
|
||||
// (e.g. "todos" + ".tsx" should NOT match "Todos.tsx" when "todos.ts" exists)
|
||||
const actual_base = query.entry.base();
|
||||
const stem_end = std.mem.lastIndexOfScalar(u8, actual_base, '.') orelse actual_base.len;
|
||||
const actual_stem = actual_base[0..stem_end];
|
||||
if (!bun.strings.eqlLong(base, actual_stem, false)) {
|
||||
if (r.debug_logs) |*debug| {
|
||||
debug.addNoteFmt("Skipping \"{s}\" (stem case mismatch: \"{s}\" vs \"{s}\")", .{ actual_base, base, actual_stem });
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (query.entry.kind(rfs, r.store_fd) == .file) {
|
||||
if (r.debug_logs) |*debug| {
|
||||
debug.addNoteFmt("Found file \"{s}\" ", .{buffer});
|
||||
}
|
||||
|
||||
// now that we've found it, we allocate it.
|
||||
// Use the entry's actual base name to build the absolute path,
|
||||
// not the query buffer, to ensure correct casing on case-sensitive
|
||||
// filesystems.
|
||||
return .{
|
||||
.path = brk: {
|
||||
query.entry.abs_path = if (query.entry.abs_path.isEmpty())
|
||||
PathString.init(r.fs.dirname_store.append(@TypeOf(buffer), buffer) catch unreachable)
|
||||
else
|
||||
query.entry.abs_path;
|
||||
if (query.entry.abs_path.isEmpty()) {
|
||||
const abs_path_parts = [_]string{ query.entry.dir, query.entry.base() };
|
||||
query.entry.abs_path = PathString.init(r.fs.dirname_store.append(string, r.fs.absBuf(&abs_path_parts, bufs(.load_as_file2))) catch unreachable);
|
||||
}
|
||||
|
||||
break :brk query.entry.abs_path.slice();
|
||||
},
|
||||
|
||||
@@ -12,79 +12,27 @@ state: union(enum) {
|
||||
|
||||
pub fn start(this: *Echo) Yield {
|
||||
var args = this.bltn().argsSlice();
|
||||
const no_newline = args.len >= 1 and std.mem.eql(u8, bun.sliceTo(args[0], 0), "-n");
|
||||
|
||||
// Parse flags: echo accepts -n, -e, -E in any combination.
|
||||
// Flag parsing stops at the first arg that doesn't start with '-'
|
||||
// or contains an invalid flag character.
|
||||
var no_newline = false;
|
||||
var escape_sequences = false;
|
||||
var flags_done = false;
|
||||
var args_start: usize = 0;
|
||||
|
||||
for (args) |arg| {
|
||||
if (flags_done) break;
|
||||
const flag = std.mem.span(arg);
|
||||
if (flag.len < 2 or flag[0] != '-') {
|
||||
flags_done = true;
|
||||
break;
|
||||
}
|
||||
// Validate all characters are valid echo flags
|
||||
var valid = true;
|
||||
for (flag[1..]) |c| {
|
||||
switch (c) {
|
||||
'n', 'e', 'E' => {},
|
||||
else => {
|
||||
valid = false;
|
||||
break;
|
||||
},
|
||||
}
|
||||
}
|
||||
if (!valid) {
|
||||
flags_done = true;
|
||||
break;
|
||||
}
|
||||
// Apply flags (last -e/-E wins)
|
||||
for (flag[1..]) |c| {
|
||||
switch (c) {
|
||||
'n' => no_newline = true,
|
||||
'e' => escape_sequences = true,
|
||||
'E' => escape_sequences = false,
|
||||
else => unreachable,
|
||||
}
|
||||
}
|
||||
args_start += 1;
|
||||
}
|
||||
|
||||
args = args[args_start..];
|
||||
args = args[if (no_newline) 1 else 0..];
|
||||
const args_len = args.len;
|
||||
var has_leading_newline: bool = false;
|
||||
var stop_output = false;
|
||||
|
||||
// TODO: Should flush buffer after it gets to a certain size
|
||||
for (args, 0..) |arg, i| {
|
||||
if (stop_output) break;
|
||||
const thearg = std.mem.span(arg);
|
||||
const is_last = i == args_len - 1;
|
||||
|
||||
if (escape_sequences) {
|
||||
stop_output = appendWithEscapes(&this.output, thearg);
|
||||
} else {
|
||||
if (is_last) {
|
||||
if (thearg.len > 0 and thearg[thearg.len - 1] == '\n') {
|
||||
has_leading_newline = true;
|
||||
}
|
||||
bun.handleOom(this.output.appendSlice(bun.strings.trimSubsequentLeadingChars(thearg, '\n')));
|
||||
} else {
|
||||
bun.handleOom(this.output.appendSlice(thearg));
|
||||
}
|
||||
}
|
||||
|
||||
if (!stop_output and !is_last) {
|
||||
if (i < args_len - 1) {
|
||||
bun.handleOom(this.output.appendSlice(thearg));
|
||||
bun.handleOom(this.output.append(' '));
|
||||
} else {
|
||||
if (thearg.len > 0 and thearg[thearg.len - 1] == '\n') {
|
||||
has_leading_newline = true;
|
||||
}
|
||||
bun.handleOom(this.output.appendSlice(bun.strings.trimSubsequentLeadingChars(thearg, '\n')));
|
||||
}
|
||||
}
|
||||
|
||||
if (!stop_output and !has_leading_newline and !no_newline) bun.handleOom(this.output.append('\n'));
|
||||
if (!has_leading_newline and !no_newline) bun.handleOom(this.output.append('\n'));
|
||||
|
||||
if (this.bltn().stdout.needsIO()) |safeguard| {
|
||||
this.state = .waiting;
|
||||
@@ -95,109 +43,6 @@ pub fn start(this: *Echo) Yield {
|
||||
return this.bltn().done(0);
|
||||
}
|
||||
|
||||
/// Appends `input` to `output`, interpreting backslash escape sequences.
|
||||
/// Returns true if a \c escape was encountered (meaning stop all output).
|
||||
fn appendWithEscapes(output: *std.array_list.Managed(u8), input: []const u8) bool {
|
||||
var i: usize = 0;
|
||||
while (i < input.len) {
|
||||
if (input[i] == '\\' and i + 1 < input.len) {
|
||||
switch (input[i + 1]) {
|
||||
'\\' => {
|
||||
bun.handleOom(output.append('\\'));
|
||||
i += 2;
|
||||
},
|
||||
'a' => {
|
||||
bun.handleOom(output.append('\x07'));
|
||||
i += 2;
|
||||
},
|
||||
'b' => {
|
||||
bun.handleOom(output.append('\x08'));
|
||||
i += 2;
|
||||
},
|
||||
'c' => {
|
||||
// \c: produce no further output
|
||||
return true;
|
||||
},
|
||||
'e', 'E' => {
|
||||
bun.handleOom(output.append('\x1b'));
|
||||
i += 2;
|
||||
},
|
||||
'f' => {
|
||||
bun.handleOom(output.append('\x0c'));
|
||||
i += 2;
|
||||
},
|
||||
'n' => {
|
||||
bun.handleOom(output.append('\n'));
|
||||
i += 2;
|
||||
},
|
||||
'r' => {
|
||||
bun.handleOom(output.append('\r'));
|
||||
i += 2;
|
||||
},
|
||||
't' => {
|
||||
bun.handleOom(output.append('\t'));
|
||||
i += 2;
|
||||
},
|
||||
'v' => {
|
||||
bun.handleOom(output.append('\x0b'));
|
||||
i += 2;
|
||||
},
|
||||
'0' => {
|
||||
// \0nnn: octal value (up to 3 octal digits)
|
||||
i += 2; // skip \0
|
||||
var val: u8 = 0;
|
||||
var digits: usize = 0;
|
||||
while (digits < 3 and i < input.len and input[i] >= '0' and input[i] <= '7') {
|
||||
val = val *% 8 +% (input[i] - '0');
|
||||
i += 1;
|
||||
digits += 1;
|
||||
}
|
||||
bun.handleOom(output.append(val));
|
||||
},
|
||||
'x' => {
|
||||
// \xHH: hex value (up to 2 hex digits)
|
||||
i += 2; // skip \x
|
||||
var val: u8 = 0;
|
||||
var digits: usize = 0;
|
||||
while (digits < 2 and i < input.len) {
|
||||
const hex_val = hexDigitValue(input[i]);
|
||||
if (hex_val) |hv| {
|
||||
val = val *% 16 +% hv;
|
||||
i += 1;
|
||||
digits += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (digits > 0) {
|
||||
bun.handleOom(output.append(val));
|
||||
} else {
|
||||
// No valid hex digits: output \x literally
|
||||
bun.handleOom(output.appendSlice("\\x"));
|
||||
}
|
||||
},
|
||||
else => {
|
||||
// Unknown escape: output backslash and the character as-is
|
||||
bun.handleOom(output.append('\\'));
|
||||
bun.handleOom(output.append(input[i + 1]));
|
||||
i += 2;
|
||||
},
|
||||
}
|
||||
} else {
|
||||
bun.handleOom(output.append(input[i]));
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
fn hexDigitValue(c: u8) ?u8 {
|
||||
if (c >= '0' and c <= '9') return c - '0';
|
||||
if (c >= 'a' and c <= 'f') return c - 'a' + 10;
|
||||
if (c >= 'A' and c <= 'F') return c - 'A' + 10;
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn onIOWriterChunk(this: *Echo, _: usize, e: ?jsc.SystemError) Yield {
|
||||
if (comptime bun.Environment.allow_assert) {
|
||||
assert(this.state == .waiting or this.state == .waiting_write_err);
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
import { $ } from "bun";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
describe("echo -e flag support", () => {
|
||||
test("echo -e does not output -e as literal text", async () => {
|
||||
const result = await $`echo -e hello`.text();
|
||||
expect(result).toBe("hello\n");
|
||||
});
|
||||
|
||||
test("echo -e interprets backslash-n", async () => {
|
||||
const result = await $`echo -e ${"hello\\nworld"}`.text();
|
||||
expect(result).toBe("hello\nworld\n");
|
||||
});
|
||||
|
||||
test("echo -e interprets backslash-t", async () => {
|
||||
const result = await $`echo -e ${"hello\\tworld"}`.text();
|
||||
expect(result).toBe("hello\tworld\n");
|
||||
});
|
||||
|
||||
test("echo -e interprets backslash-backslash", async () => {
|
||||
const result = await $`echo -e ${"hello\\\\world"}`.text();
|
||||
expect(result).toBe("hello\\world\n");
|
||||
});
|
||||
|
||||
test("echo -e interprets \\a (bell)", async () => {
|
||||
const result = await $`echo -e ${"\\a"}`.text();
|
||||
expect(result).toBe("\x07\n");
|
||||
});
|
||||
|
||||
test("echo -e interprets \\b (backspace)", async () => {
|
||||
const result = await $`echo -e ${"a\\bb"}`.text();
|
||||
expect(result).toBe("a\bb\n");
|
||||
});
|
||||
|
||||
test("echo -e interprets \\r (carriage return)", async () => {
|
||||
const result = await $`echo -e ${"hello\\rworld"}`.text();
|
||||
expect(result).toBe("hello\rworld\n");
|
||||
});
|
||||
|
||||
test("echo -e interprets \\f (form feed)", async () => {
|
||||
const result = await $`echo -e ${"\\f"}`.text();
|
||||
expect(result).toBe("\f\n");
|
||||
});
|
||||
|
||||
test("echo -e interprets \\v (vertical tab)", async () => {
|
||||
const result = await $`echo -e ${"\\v"}`.text();
|
||||
expect(result).toBe("\v\n");
|
||||
});
|
||||
|
||||
test("echo -e interprets \\0nnn (octal)", async () => {
|
||||
// \0101 = 'A' (65 decimal)
|
||||
const result = await $`echo -e ${"\\0101"}`.text();
|
||||
expect(result).toBe("A\n");
|
||||
});
|
||||
|
||||
test("echo -e interprets \\xHH (hex)", async () => {
|
||||
// \x41 = 'A'
|
||||
const result = await $`echo -e ${"\\x41\\x42\\x43"}`.text();
|
||||
expect(result).toBe("ABC\n");
|
||||
});
|
||||
|
||||
test("echo -e \\c stops output", async () => {
|
||||
const result = await $`echo -e ${"hello\\cworld"}`.text();
|
||||
expect(result).toBe("hello");
|
||||
});
|
||||
|
||||
test("echo -e with \\e (escape character)", async () => {
|
||||
const result = await $`echo -e ${"\\e"}`.text();
|
||||
expect(result).toBe("\x1b\n");
|
||||
});
|
||||
|
||||
test("echo -E disables escape interpretation", async () => {
|
||||
const result = await $`echo -E ${"hello\\nworld"}`.text();
|
||||
expect(result).toBe("hello\\nworld\n");
|
||||
});
|
||||
|
||||
test("echo -eE (last wins: -E disables)", async () => {
|
||||
const result = await $`echo -eE ${"hello\\tworld"}`.text();
|
||||
expect(result).toBe("hello\\tworld\n");
|
||||
});
|
||||
|
||||
test("echo -Ee (last wins: -e enables)", async () => {
|
||||
const result = await $`echo -Ee ${"hello\\tworld"}`.text();
|
||||
expect(result).toBe("hello\tworld\n");
|
||||
});
|
||||
|
||||
test("echo -ne (no newline + escapes)", async () => {
|
||||
const result = await $`echo -ne ${"hello\\tworld"}`.text();
|
||||
expect(result).toBe("hello\tworld");
|
||||
});
|
||||
|
||||
test("echo -en (same as -ne)", async () => {
|
||||
const result = await $`echo -en ${"hello\\tworld"}`.text();
|
||||
expect(result).toBe("hello\tworld");
|
||||
});
|
||||
|
||||
test("echo -n still works (no newline)", async () => {
|
||||
const result = await $`echo -n hello`.text();
|
||||
expect(result).toBe("hello");
|
||||
});
|
||||
|
||||
test("echo with invalid flag outputs literally", async () => {
|
||||
const result = await $`echo -x hello`.text();
|
||||
expect(result).toBe("-x hello\n");
|
||||
});
|
||||
|
||||
test("echo -e piped to cat (original issue scenario)", async () => {
|
||||
const pw = "mypassword";
|
||||
const result = await $`echo -e ${pw} | cat`.text();
|
||||
expect(result).toBe("mypassword\n");
|
||||
});
|
||||
|
||||
test("echo without -e still works normally", async () => {
|
||||
const result = await $`echo hello world`.text();
|
||||
expect(result).toBe("hello world\n");
|
||||
});
|
||||
});
|
||||
83
test/regression/issue/22686.test.ts
Normal file
83
test/regression/issue/22686.test.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { bunEnv, bunExe, tempDir } from "harness";
|
||||
|
||||
// https://github.com/oven-sh/bun/issues/22686
|
||||
// When a directory has files that differ only in case and extension
|
||||
// (e.g. todos.ts and Todos.tsx), extensionless imports should resolve
|
||||
// to the correct file based on case-sensitive stem matching.
|
||||
test("extensionless import resolves correct file when similar names differ by case", async () => {
|
||||
using dir = tempDir("issue-22686", {
|
||||
"src/todos.ts": `export const todos = ["todo1", "todo2"];`,
|
||||
"src/Todos.tsx": `export function Todos() { return "Todos Component"; }`,
|
||||
"src/index.tsx": `
|
||||
import { todos } from "./todos";
|
||||
import { Todos } from "./Todos";
|
||||
console.log(JSON.stringify(todos));
|
||||
console.log(Todos());
|
||||
`,
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "run", "src/index.tsx"],
|
||||
env: bunEnv,
|
||||
cwd: String(dir),
|
||||
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('["todo1","todo2"]\nTodos Component\n');
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("bundler resolves correct file when similar names differ by case", async () => {
|
||||
using dir = tempDir("issue-22686-bundler", {
|
||||
"src/todos.ts": `export const todos = ["todo1", "todo2"];`,
|
||||
"src/Todos.tsx": `export function Todos() { return "Todos Component"; }`,
|
||||
"src/index.tsx": `
|
||||
import { todos } from "./todos";
|
||||
import { Todos } from "./Todos";
|
||||
console.log(JSON.stringify(todos));
|
||||
console.log(Todos());
|
||||
`,
|
||||
});
|
||||
|
||||
// First, bundle
|
||||
await using buildProc = Bun.spawn({
|
||||
cmd: [bunExe(), "build", "src/index.tsx", "--outdir=dist"],
|
||||
env: bunEnv,
|
||||
cwd: String(dir),
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [buildStdout, buildStderr, buildExitCode] = await Promise.all([
|
||||
buildProc.stdout.text(),
|
||||
buildProc.stderr.text(),
|
||||
buildProc.exited,
|
||||
]);
|
||||
|
||||
expect(buildStderr).toBe("");
|
||||
expect(buildExitCode).toBe(0);
|
||||
|
||||
// Then run the bundled output
|
||||
await using runProc = Bun.spawn({
|
||||
cmd: [bunExe(), "run", "dist/index.js"],
|
||||
env: bunEnv,
|
||||
cwd: String(dir),
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [runStdout, runStderr, runExitCode] = await Promise.all([
|
||||
runProc.stdout.text(),
|
||||
runProc.stderr.text(),
|
||||
runProc.exited,
|
||||
]);
|
||||
|
||||
expect(runStderr).toBe("");
|
||||
expect(runStdout).toBe('["todo1","todo2"]\nTodos Component\n');
|
||||
expect(runExitCode).toBe(0);
|
||||
});
|
||||
Reference in New Issue
Block a user