diff --git a/src/runtime/jsc_hooks.rs b/src/runtime/jsc_hooks.rs index fc3920a67d74..633da84745a7 100644 --- a/src/runtime/jsc_hooks.rs +++ b/src/runtime/jsc_hooks.rs @@ -3390,6 +3390,20 @@ fn transpile_source_code_inner( // SAFETY: null-checked above; `global_object` is the live per-thread // `JSGlobalObject` for the FFI call. let global = unsafe { &*global_object }; + // The bundler emits `{}` as the JS stub for a plain CSS import + // (esbuild parity); match that here instead of leaking the file path + // as the default export. `.module.css` still diverges: the bundler + // emits a class-name map there, runtime CSS-module scoping is not + // implemented. + if matches!(loader, L::Css) { + return Ok(OwnedResolvedSource::from(ResolvedSource { + jsvalue_for_export: JSValue::create_empty_object(global, 0), + specifier: input_specifier.dupe_ref(), + source_url: create_if_different(input_specifier, path.text), + tag: ResolvedSourceTag::ExportDefaultObject, + ..Default::default() + })); + } // SAFETY: per fn contract — `jsc_vm` is the live per-thread VM. let value = if !unsafe { &*jsc_vm }.origin.is_empty() { // Rewrite `specifier` against `vm.origin` so diff --git a/test/js/bun/css/css-loader.test.ts b/test/js/bun/css/css-loader.test.ts new file mode 100644 index 000000000000..ee584fe01569 --- /dev/null +++ b/test/js/bun/css/css-loader.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, test } from "bun:test"; +import { bunEnv, bunExe, tempDir } from "harness"; + +// The default export of a plain .css import is an empty object (esbuild +// parity), and `bun run` and `bun build --target=bun` agree on that shape. +describe.concurrent("css loader default export", () => { + const entry = `import c from "./s.css";\nprocess.stdout.write(typeof c + " " + JSON.stringify(c));\n`; + + test("bun run", async () => { + using dir = tempDir("css-loader-run", { + "s.css": ".c { color: red }", + "e.ts": entry, + }); + await using proc = Bun.spawn({ + cmd: [bunExe(), "e.ts"], + 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("object {}"); + expect(exitCode).toBe(0); + }); + + test("bun build --target=bun then run matches bun run", async () => { + using dir = tempDir("css-loader-build", { + "s.css": ".c { color: red }", + "e.ts": entry, + }); + await using build = Bun.spawn({ + cmd: [bunExe(), "build", "e.ts", "--target=bun", "--outdir=out"], + env: bunEnv, + cwd: String(dir), + stderr: "pipe", + }); + const [, buildErr, buildCode] = await Promise.all([build.stdout.text(), build.stderr.text(), build.exited]); + expect(buildErr).toBe(""); + expect(buildCode).toBe(0); + + await using run = Bun.spawn({ + cmd: [bunExe(), "out/e.js"], + env: bunEnv, + cwd: String(dir), + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([run.stdout.text(), run.stderr.text(), run.exited]); + expect(stderr).toBe(""); + expect(stdout).toBe("object {}"); + expect(exitCode).toBe(0); + }); +}); diff --git a/test/js/bun/resolve/import-empty.test.js b/test/js/bun/resolve/import-empty.test.js index 592686a92d69..2150bd7838f0 100644 --- a/test/js/bun/resolve/import-empty.test.js +++ b/test/js/bun/resolve/import-empty.test.js @@ -23,9 +23,9 @@ it("importing empty file with type file returns it path", async () => { // MARK: - web imports -it("importing empty css file returns its path", async () => { +it("importing empty css file returns an empty object", async () => { const empty_file_css = (await import("./empty-file", { with: { type: "css" } })).default; - expect(empty_file_css).toEqual(empty_file_path); + expect(empty_file_css).toEqual({}); }); it("importing empty html file returns HTMLBundle with its path", async () => {