From 85b3145b6ac29d32bc42a58236633f22eecc3299 Mon Sep 17 00:00:00 2001
From: robobun <117481402+robobun@users.noreply.github.com>
Date: Fri, 18 Sep 2026 11:20:44 +0000
Subject: [PATCH 1/3] transpiler: do not print an empty `var` for a data file
with no named export
A JSON, JSONC, JSON5, TOML, YAML or XML file whose only top-level key
is "default" gets no named export. `parse_data_loader` still built
`var` with zero declarations, and `print_decls` aborts on that.
Emit only `export default` when no key gets a named export.
---
src/bundler/transpiler.rs | 68 ++++++++++++----------
test/bundler/bundler_loader.test.ts | 22 +++++++
test/bundler/expectBundled.ts | 2 +-
test/bundler/transpiler/transpiler.test.js | 25 ++++++++
4 files changed, 86 insertions(+), 31 deletions(-)
diff --git a/src/bundler/transpiler.rs b/src/bundler/transpiler.rs
index 3729996fa70e..f3d8767817e9 100644
--- a/src/bundler/transpiler.rs
+++ b/src/bundler/transpiler.rs
@@ -2057,38 +2057,46 @@ fn parse_data_loader<'a>(
}
decls.truncate(count);
- let stmt0 = bun_ast::Stmt::alloc(
- bun_ast::S::Local {
- decls: bun_ast::G::DeclList::move_from_list(decls),
- kind: bun_ast::S::Kind::KVar,
- ..Default::default()
- },
- bun_ast::Loc { start: 0 },
- );
- let stmt1 = bun_ast::Stmt::alloc(
- bun_ast::S::ExportClause {
- items: bun_ast::StoreSlice::new_mut(&mut export_clauses[..count]),
- is_single_line: false,
- },
- bun_ast::Loc { start: 0 },
- );
- let stmt2 = bun_ast::Stmt::alloc(
- bun_ast::S::ExportDefault {
- value: bun_ast::StmtOrExpr::Expr(expr),
- default_name: bun_ast::LocRef {
- loc: bun_ast::Loc::default(),
- ref_: bun_ast::Ref::NONE,
+ symbols.truncate(count);
+
+ // A `var` with no declaration cannot be printed. A file where
+ // no key gets a named export (`{"default": 1}`) is only the
+ // `export default` below.
+ if count > 0 {
+ let stmt0 = bun_ast::Stmt::alloc(
+ bun_ast::S::Local {
+ decls: bun_ast::G::DeclList::move_from_list(decls),
+ kind: bun_ast::S::Kind::KVar,
+ ..Default::default()
},
- },
- bun_ast::Loc { start: 0 },
- );
+ bun_ast::Loc { start: 0 },
+ );
+ let stmt1 = bun_ast::Stmt::alloc(
+ bun_ast::S::ExportClause {
+ items: bun_ast::StoreSlice::new_mut(&mut export_clauses[..count]),
+ is_single_line: false,
+ },
+ bun_ast::Loc { start: 0 },
+ );
+ let stmt2 = bun_ast::Stmt::alloc(
+ bun_ast::S::ExportDefault {
+ value: bun_ast::StmtOrExpr::Expr(expr),
+ default_name: bun_ast::LocRef {
+ loc: bun_ast::Loc::default(),
+ ref_: bun_ast::Ref::NONE,
+ },
+ },
+ bun_ast::Loc { start: 0 },
+ );
- let stmts =
- bun_ast::StoreSlice::new_mut(arena.alloc_slice_copy(&[stmt0, stmt1, stmt2]));
- break 'parts Box::new([bun_ast::Part {
- stmts,
- ..Default::default()
- }]);
+ let stmts = bun_ast::StoreSlice::new_mut(
+ arena.alloc_slice_copy(&[stmt0, stmt1, stmt2]),
+ );
+ break 'parts Box::new([bun_ast::Part {
+ stmts,
+ ..Default::default()
+ }]);
+ }
}
}
diff --git a/test/bundler/bundler_loader.test.ts b/test/bundler/bundler_loader.test.ts
index bab25c3a0726..7439c4d7efd1 100644
--- a/test/bundler/bundler_loader.test.ts
+++ b/test/bundler/bundler_loader.test.ts
@@ -370,6 +370,28 @@ describe("bundler", async () => {
},
});
+ // A "default" key gets no named export. When no key gets one, the module
+ // has no `var` statement: it is only `export default`.
+ const onlyDefaultKey = (ext: string, contents: string, value: unknown, loader?: Record<`.${string}`, Loader>) =>
+ itBundled(`bun/loader-${ext}-only-default-key-no-bundle`, {
+ target: "bun",
+ bundling: false,
+ entryPoints: [`/data.${ext}`],
+ loader,
+ files: {
+ [`/data.${ext}`]: contents,
+ "/check.js": `import * as data from "./out.js"; console.write(JSON.stringify(data));`,
+ },
+ run: { file: "/check.js", stdout: JSON.stringify({ default: { default: value } }) },
+ });
+ onlyDefaultKey("json", `{"default": 1}`, 1);
+ onlyDefaultKey("jsonc", `{"default": 1} // comment`, 1);
+ onlyDefaultKey("json5", `{default: 1}`, 1);
+ onlyDefaultKey("toml", `default = 1`, 1);
+ onlyDefaultKey("yaml", `default: 1`, 1);
+ // `bun build --no-bundle` gives ".xml" the file loader unless --loader names it.
+ onlyDefaultKey("xml", `1`, "1", { ".xml": "xml" });
+
// The CSS-modules lazy export builds its object through `E::Object::put`.
itBundled("bun/loader-css-module-proto-class-is-own-property", {
target: "bun",
diff --git a/test/bundler/expectBundled.ts b/test/bundler/expectBundled.ts
index 9c0e3d4d341d..bc2f6e4af6be 100644
--- a/test/bundler/expectBundled.ts
+++ b/test/bundler/expectBundled.ts
@@ -663,7 +663,7 @@ function expectBundled(
}
if (!ESBUILD && loader) {
const loaderValues = [...new Set(Object.values(loader))];
- const supportedLoaderTypes = ["js", "jsx", "ts", "tsx", "css", "json", "text", "file", "wtf", "toml"];
+ const supportedLoaderTypes = ["js", "jsx", "ts", "tsx", "css", "json", "text", "file", "wtf", "toml", "xml"];
const unsupportedLoaderTypes = loaderValues.filter(x => !supportedLoaderTypes.includes(x));
if (unsupportedLoaderTypes.length > 0) {
throw new UnsupportedOptionError(`loader '${unsupportedLoaderTypes.join("', '")}' not implemented in bun build`);
diff --git a/test/bundler/transpiler/transpiler.test.js b/test/bundler/transpiler/transpiler.test.js
index a4b6cd48696c..c4e31741417e 100644
--- a/test/bundler/transpiler/transpiler.test.js
+++ b/test/bundler/transpiler/transpiler.test.js
@@ -5256,6 +5256,31 @@ describe.concurrent("minify.identifiers on an empty source or a data loader", ()
});
});
+// The printer aborted the process on this input, so it runs in a subprocess.
+it('a data loader source whose only key is "default" is only `export default`', async () => {
+ const result = await bunRun([
+ "-e",
+ `const transpiler = new Bun.Transpiler({ minify: { whitespace: true } });
+ const output = [];
+ for (const [loader, source] of [
+ ["json", '{"default": 1}'],
+ ["jsonc", '{"default": 1} // comment'],
+ ["json5", "{default: 1}"],
+ ["toml", "default = 1"],
+ ["yaml", "default: 1"],
+ ["xml", "1"],
+ ]) {
+ output.push([loader, transpiler.transformSync(source, loader), await transpiler.transform(source, loader)]);
+ }
+ console.log(JSON.stringify(output));`,
+ ]);
+ const number = "export default {default:1};";
+ const string = 'export default {default:"1"};';
+ const expected = ["json", "jsonc", "json5", "toml", "yaml"].map(loader => [loader, number, number]);
+ expected.push(["xml", string, string]);
+ expect(result).toEqual({ stdout: JSON.stringify(expected), stderr: "", exitCode: 0, signalCode: null });
+});
+
it("runtime transpiler stack overflows", async () => {
expect(async () => await import("./fixtures/lots-of-for-loop.js")).toThrow(`Maximum call stack size exceeded`);
});
From f088b53c1ec12ed2b634f172d75805205d40e925 Mon Sep 17 00:00:00 2001
From: robobun <117481402+robobun@users.noreply.github.com>
Date: Fri, 18 Sep 2026 17:41:09 +0000
Subject: [PATCH 2/3] transpiler test: run the "default"-only data loader case
concurrently
It spawns a subprocess and shares no state, like the data loader cases
in the describe.concurrent block before it.
---
test/bundler/transpiler/transpiler.test.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/test/bundler/transpiler/transpiler.test.js b/test/bundler/transpiler/transpiler.test.js
index c4e31741417e..dd323f50906b 100644
--- a/test/bundler/transpiler/transpiler.test.js
+++ b/test/bundler/transpiler/transpiler.test.js
@@ -5257,7 +5257,7 @@ describe.concurrent("minify.identifiers on an empty source or a data loader", ()
});
// The printer aborted the process on this input, so it runs in a subprocess.
-it('a data loader source whose only key is "default" is only `export default`', async () => {
+it.concurrent('a data loader source whose only key is "default" is only `export default`', async () => {
const result = await bunRun([
"-e",
`const transpiler = new Bun.Transpiler({ minify: { whitespace: true } });
From 6a45fae716ea03b2ab9114d3d5225739b7fc267f Mon Sep 17 00:00:00 2001
From: robobun <117481402+robobun@users.noreply.github.com>
Date: Fri, 18 Sep 2026 17:44:16 +0000
Subject: [PATCH 3/3] transpiler: shorten the comment on the empty `var` guard
---
src/bundler/transpiler.rs | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/src/bundler/transpiler.rs b/src/bundler/transpiler.rs
index f3d8767817e9..08182e5cc2db 100644
--- a/src/bundler/transpiler.rs
+++ b/src/bundler/transpiler.rs
@@ -2059,9 +2059,7 @@ fn parse_data_loader<'a>(
decls.truncate(count);
symbols.truncate(count);
- // A `var` with no declaration cannot be printed. A file where
- // no key gets a named export (`{"default": 1}`) is only the
- // `export default` below.
+ // `print_decls` aborts on a `var` with no declarations (`{"default": 1}`).
if count > 0 {
let stmt0 = bun_ast::Stmt::alloc(
bun_ast::S::Local {