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 {