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
2 changes: 1 addition & 1 deletion src/jsc/RuntimeTranspilerStore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -994,7 +994,7 @@ impl TranspilerJob {
_ => (ptr::null_mut(), 0),
};
self.resolved_source = OwnedResolvedSource::from(ResolvedSource {
source_code: String::clone_latin1(&parse_result.source.contents),
source_code: String::clone_utf8(&parse_result.source.contents),
already_bundled: true,
bytecode_cache,
bytecode_cache_size,
Expand Down
17 changes: 16 additions & 1 deletion src/runtime/jsc_hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2787,8 +2787,15 @@ fn transpile_source_code_inner(
}
_ => (core::ptr::null_mut(), 0),
};
if is_main {
// Same as the transpiler-cache-hit return below: leaving
// `has_loaded` false sends unknown-extension imports to
// the `Loader::Tsx` fallback instead of `Loader::File`.
// SAFETY: per fn contract — `jsc_vm` is the live per-thread VM.
unsafe { (*jsc_vm).has_loaded = true };
}
return Ok(OwnedResolvedSource::from(ResolvedSource {
source_code: bun_core::String::clone_latin1(&source.contents),
source_code: bun_core::String::clone_utf8(&source.contents),
Comment thread
robobun marked this conversation as resolved.
specifier: input_specifier.dupe_ref(),
source_url: create_if_different(input_specifier, path.text),
already_bundled: true,
Expand Down Expand Up @@ -2931,6 +2938,14 @@ fn transpile_source_code_inner(
};
let (bytecode_cache, bytecode_cache_size) =
node_compile_cache_blob.unwrap_or((core::ptr::null_mut(), 0));
if is_main {
// Without this, a cache hit for the entry point leaves
// `has_loaded` false and later imports with unknown
// extensions fall back to `Loader::Tsx` instead of
// `Loader::File`.
// SAFETY: per fn contract — `jsc_vm` is the live per-thread VM.
unsafe { (*jsc_vm).has_loaded = true };
}
return Ok(OwnedResolvedSource::from(ResolvedSource {
source_code,
specifier: input_specifier.dupe_ref(),
Expand Down
51 changes: 51 additions & 0 deletions test/bundler/transpiler/runtime-transpiler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,57 @@ describe("// @bun", () => {
expect(stdout.toString()).toBe("Hello world!\n");
expect(exitCode).toBe(0);
});

// https://github.com/oven-sh/bun/issues/37161
const nonAscii = "\u2014\u00b7\u2713 K\u00e4ufer";
test("raw utf-8 decodes as utf-8, not latin-1 (import)", async () => {
using dir = tempDir("bun-pragma-utf8-import", {
"pragma.js": `// @bun\nexport const text = "${nonAscii}";\n`,
});
const { text } = await import(`${dir}/pragma.js`);
expect(text).toBe(nonAscii);
});

test("raw utf-8 decodes as utf-8, not latin-1 (require)", async () => {
using dir = tempDir("bun-pragma-utf8-require", {
"pragma.js": `// @bun\nexport const text = "${nonAscii}";\n`,
});
const { text } = require(`${dir}/pragma.js`);
expect(text).toBe(nonAscii);
});

test("entry point with pragma keeps unknown-extension imports on the file loader", async () => {
using dir = tempDir("bun-pragma-file-loader", {
"asset.hbs": "<!DOCTYPE html>\n<html></html>\n",
"entry.js": `// @bun\nimport asset from "./asset.hbs";\nconsole.log(typeof asset);\n`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "entry.js"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(stdout).toBe("string\n");
expect(exitCode).toBe(0);
});

test("raw utf-8 decodes as utf-8, not latin-1 (entry point)", async () => {
using dir = tempDir("bun-pragma-utf8-entry", {
"pragma.js": `// @bun\nconst DASH = "\u2014";\nconsole.log([...DASH].map(c => c.codePointAt(0).toString(16)).join(" "));\n`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "pragma.js"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(stdout).toBe("2014\n");
expect(exitCode).toBe(0);
});
});

describe("json imports", () => {
Expand Down
11 changes: 11 additions & 0 deletions test/cli/run/transpiler-cache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,17 @@ describe("transpiler cache", () => {
expect(await bunRun(join(temp_dir, "a.js"), env)).toSpawn("a");
expect(!existsSync(cache_dir)).toBeTrue();
});
test("cache hit on the entry point keeps unknown-extension imports on the file loader", async () => {
writeFileSync(join(temp_dir, "asset.hbs"), "<!DOCTYPE html>\n<html></html>\n");
const padding = "// " + Buffer.alloc(8 * 1024, "x").toString() + "\n";
writeFileSync(join(temp_dir, "a.js"), `import asset from "./asset.hbs";\n${padding}console.log(typeof asset);\n`);
expect(await bunRun(join(temp_dir, "a.js"), env)).toSpawn("string");
expect(newCacheCount()).toBe(1);
// The second run loads the entry point from the transpiler cache; the
// unknown extension must still get the file loader, not the tsx fallback.
expect(await bunRun(join(temp_dir, "a.js"), env)).toSpawn("string");
expect(newCacheCount()).toBe(0);
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
test("it is indeed content addressable", async () => {
writeFileSync(join(temp_dir, "a.js"), dummyFile(50 * 1024, "1", "b"));
expect(await bunRun(join(temp_dir, "a.js"), env)).toSpawn("b");
Expand Down