diff --git a/src/bun_core/lib.rs b/src/bun_core/lib.rs index ac446bd1f514..31385a8f5e74 100644 --- a/src/bun_core/lib.rs +++ b/src/bun_core/lib.rs @@ -2434,16 +2434,28 @@ pub(crate) mod strings_impl { if i >= self.bytes.len() { return false; } - let b = self.bytes[i]; - // TODO(port): full UTF-8 decode — bun_str owns the table-driven impl. - let (cp, w) = if b < 0x80 { - (b as i32, 1u8) - } else { - (b as i32, 1u8) - }; + let tail = &self.bytes[i..]; + let b = tail[0]; cursor.i = i; - cursor.c = cp; - cursor.width = w; + if b < 0x80 { + cursor.c = b as i32; + cursor.width = 1; + return true; + } + // Multi-byte: defer to the canonical WTF-8 decoder so this stub + // stays in lockstep with `strings::CodepointIterator::next`. + let len = wtf8_byte_sequence_length(b); + let take = (len as usize).min(tail.len()); + let mut buf = [0u8; 4]; + buf[..take].copy_from_slice(&tail[..take]); + let cp = crate::string::immutable::decode_wtf8_rune_t::(&buf, len, -1); + if cp == -1 { + cursor.c = crate::string::immutable::UNICODE_REPLACEMENT as i32; + cursor.width = 1; + } else { + cursor.c = cp; + cursor.width = len; + } true } } diff --git a/src/jsc/bindings/wtf-bindings.cpp b/src/jsc/bindings/wtf-bindings.cpp index c0b6470dac26..14a31372b1ed 100644 --- a/src/jsc/bindings/wtf-bindings.cpp +++ b/src/jsc/bindings/wtf-bindings.cpp @@ -14,7 +14,6 @@ #endif #if !OS(WINDOWS) -#include #include #include diff --git a/test/bundler/bundler_naming.test.ts b/test/bundler/bundler_naming.test.ts index e2870b399b3d..678baed2cd62 100644 --- a/test/bundler/bundler_naming.test.ts +++ b/test/bundler/bundler_naming.test.ts @@ -302,4 +302,26 @@ describe("bundler", () => { }, ], }); + // A non-ASCII basename char must collapse to a single "_" in the generated + // CommonJS wrapper symbol, not one "_" per UTF-8 byte. Regressed to + // `require_caf__utils` (the 2 bytes of "é" walked individually) because the + // identifier formatter resolved to a stub that never decoded multi-byte + // sequences (width always 1). + itBundled("naming/NonAsciiSourceFilenameSymbol", { + files: { + "/entry.js": /* js */ ` + const u = require("./café-utils.js"); + console.log(u.hi); + `, + "/café-utils.js": /* js */ ` + module.exports = { hi: 1 }; + `, + }, + target: "bun", + onAfterBundle(api) { + api.expectFile("/out.js").toContain("require_caf_utils"); + api.expectFile("/out.js").not.toContain("require_caf__utils"); + }, + run: { stdout: "1" }, + }); });