From 6af5ccc0b1abde74fa79c1975acc1d58bd40cd8a Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 12 Sep 2026 07:25:16 +0000 Subject: [PATCH 1/3] Bun.Transpiler: throw only when the parser log has errors transform(), transformSync(), scan() and scanImports() threw the log as a BuildMessage when it held warnings only. A valid source that produced a warning gave the caller no output. Check log.errors at those four sites, which matches the runtime module loader. --- src/runtime/api/JSTranspiler.rs | 8 +-- test/bundler/transpiler/transpiler.test.js | 84 ++++++++++++++++++++-- 2 files changed, 81 insertions(+), 11 deletions(-) diff --git a/src/runtime/api/JSTranspiler.rs b/src/runtime/api/JSTranspiler.rs index e5c1d8c2df70..a150b5f8ee52 100644 --- a/src/runtime/api/JSTranspiler.rs +++ b/src/runtime/api/JSTranspiler.rs @@ -839,7 +839,7 @@ impl TransformTask { fn then(&mut self, promise: &mut JSPromise, global: &JSGlobalObject) -> JsResult<()> { // The job drops this `TransformTask` (running its `Drop`: transpiler // deref etc.) right after `then` returns. - if self.log.has_any() || self.err.is_some() { + if self.log.has_errors() || self.err.is_some() { let error_value: JsResult = 'brk: { if let Some(err) = &self.err { if !self.log.has_any() { @@ -1317,7 +1317,7 @@ impl JSTranspiler { return Err(global.throw(format_args!("Failed to parse"))); }; - if (log_ref.warnings + log_ref.errors) > 0 { + if log_ref.has_errors() { return Err(global.throw_value(log_ref.to_js(global, format_args!("Parse error"))?)); } @@ -1501,7 +1501,7 @@ impl JSTranspiler { return Err(global.throw(format_args!("Failed to parse code"))); }; - if (log_ref.warnings + log_ref.errors) > 0 { + if log_ref.has_errors() { return Err(global.throw_value(log_ref.to_js(global, format_args!("Parse error"))?)); } @@ -1711,7 +1711,7 @@ impl JSTranspiler { return Err(global.throw_error(err, "Failed to scan imports")); } - if (log.warnings + log.errors) > 0 { + if log.has_errors() { return Err( global.throw_value(log.to_js(global, format_args!("Failed to scan imports"))?) ); diff --git a/test/bundler/transpiler/transpiler.test.js b/test/bundler/transpiler/transpiler.test.js index 78baaf353d75..0d111c28dd30 100644 --- a/test/bundler/transpiler/transpiler.test.js +++ b/test/bundler/transpiler/transpiler.test.js @@ -2450,6 +2450,60 @@ export default <>hi ).toThrow(); }); + describe("a parser warning does not throw", () => { + // `-->` at the start of a line is a legacy HTML comment. The parser logs a + // warning for it and keeps going. + const warnSrc = "x = 1\n--> legacy html comment\ny = 2\n"; + const warnJsx = 'import { a } from "a";\nexport default
;\n'; + + it("transformSync", () => { + const t = new Bun.Transpiler({ loader: "js" }); + expect(t.transformSync(warnSrc)).toBe("x = 1;\ny = 2;\n"); + }); + + it("transform", async () => { + const t = new Bun.Transpiler({ loader: "js" }); + expect(await t.transform(warnSrc)).toBe("x = 1;\ny = 2;\n"); + }); + + it("scan", () => { + const t = new Bun.Transpiler({ loader: "jsx" }); + expect(t.scan(warnJsx)).toEqual({ + imports: [{ kind: "import-statement", path: "a" }], + exports: ["default"], + }); + }); + + it("scanImports", () => { + const t = new Bun.Transpiler({ loader: "jsx" }); + // The key-after-spread warning makes the JSX fall back to the classic + // runtime, so the scan also lists the runtime requires. + expect(t.scanImports(warnJsx)).toEqual([ + { kind: "import-statement", path: "a" }, + { kind: "require-call", path: "react/jsx-dev-runtime" }, + { kind: "require-call", path: "react" }, + ]); + }); + + it("a parse error next to a warning still throws", () => { + const t = new Bun.Transpiler({ loader: "js" }); + const src = warnSrc + "bad??!?!?!"; + for (const fn of ["transformSync", "scan", "scanImports"]) { + let thrown; + try { + t[fn](src); + } catch (e) { + thrown = e; + } + expect(thrown).toBeInstanceOf(AggregateError); + expect(thrown.errors.map(m => [m.level, m.message])).toEqual([ + ["warn", 'Treating "-->" as the start of a legacy HTML single-line comment'], + ["error", "Unexpected ?"], + ]); + } + }); + }); + it("define with an empty-string key is ignored without leaving uninitialized slots", async () => { // `JSPropertyIterator` skips empty-name properties, but `names`/`values` were being // indexed by the property position instead of a dense counter, leaving garbage in the @@ -3794,15 +3848,31 @@ class Foo { expectParseError("class Foo { static { continue } }", 'Cannot use "continue" here'); expectParseError("x: { class Foo { static { break x } } }", 'There is no containing label named "x"'); expectParseError("x: { class Foo { static { continue x } } }", 'There is no containing label named "x"'); + }); + + it("private accessor and method misuse is a warning", async () => { + const cases = [ + ["class Foo { get #x() { this.#x = 1 } }", 'Writing to getter-only property "#x" will throw'], + ["class Foo { get #x() { this.#x += 1 } }", 'Writing to getter-only property "#x" will throw'], + ["class Foo { set #x(x) { this.#x } }", 'Reading from setter-only property "#x" will throw'], + ["class Foo { set #x(x) { this.#x += 1 } }", 'Reading from setter-only property "#x" will throw'], + ["class Foo { #x() { this.#x = 1 } }", 'Writing to read-only method "#x" will throw'], + ["class Foo { #x() { this.#x += 1 } }", 'Writing to read-only method "#x" will throw'], + ]; - expectParseError("class Foo { get #x() { this.#x = 1 } }", 'Writing to getter-only property "#x" will throw'); - expectParseError("class Foo { get #x() { this.#x += 1 } }", 'Writing to getter-only property "#x" will throw'); - expectParseError("class Foo { set #x(x) { this.#x } }", 'Reading from setter-only property "#x" will throw'); - expectParseError("class Foo { set #x(x) { this.#x += 1 } }", 'Reading from setter-only property "#x" will throw'); + // The transpiler keeps going after a warning, so the code is still printed. + for (const [code] of cases) { + expect(transpiler.transformSync(code)).toContain("class Foo"); + } - // Writing to method warnings - expectParseError("class Foo { #x() { this.#x = 1 } }", 'Writing to read-only method "#x" will throw'); - expectParseError("class Foo { #x() { this.#x += 1 } }", 'Writing to read-only method "#x" will throw'); + using dir = tempDir("private-warnings", { + "in.js": cases.map(([code]) => `{ ${code} }`).join("\n") + "\n", + }); + const result = await Bun.build({ entrypoints: [join(String(dir), "in.js")], write: false }); + expect(result.success).toBe(true); + expect(result.logs.map(log => [log.level, log.position.line, log.message])).toEqual( + cases.map(([, message], i) => ["warn", i + 1, message]), + ); }); it("class bodies keep `this` and the class name as written", () => { From a53ef51a45349b27567436cf06ac47051d8c3546 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 12 Sep 2026 07:45:56 +0000 Subject: [PATCH 2/3] test: cover the async transform path for a warning next to an error --- test/bundler/transpiler/transpiler.test.js | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/test/bundler/transpiler/transpiler.test.js b/test/bundler/transpiler/transpiler.test.js index 0d111c28dd30..3280bf2ea496 100644 --- a/test/bundler/transpiler/transpiler.test.js +++ b/test/bundler/transpiler/transpiler.test.js @@ -2485,9 +2485,13 @@ export default <>hi ]); }); - it("a parse error next to a warning still throws", () => { + it("a parse error next to a warning still throws", async () => { const t = new Bun.Transpiler({ loader: "js" }); const src = warnSrc + "bad??!?!?!"; + const expectedErrors = [ + ["warn", 'Treating "-->" as the start of a legacy HTML single-line comment'], + ["error", "Unexpected ?"], + ]; for (const fn of ["transformSync", "scan", "scanImports"]) { let thrown; try { @@ -2496,11 +2500,18 @@ export default <>hi thrown = e; } expect(thrown).toBeInstanceOf(AggregateError); - expect(thrown.errors.map(m => [m.level, m.message])).toEqual([ - ["warn", 'Treating "-->" as the start of a legacy HTML single-line comment'], - ["error", "Unexpected ?"], - ]); + expect(thrown.errors.map(m => [m.level, m.message])).toEqual(expectedErrors); } + + let rejected; + await t.transform(src).then( + () => {}, + e => { + rejected = e; + }, + ); + expect(rejected).toBeInstanceOf(AggregateError); + expect(rejected.errors.map(m => [m.level, m.message])).toEqual(expectedErrors); }); }); From 242d4e0e2aa7f74ef860da84850b25280249657a Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 12 Sep 2026 08:10:15 +0000 Subject: [PATCH 3/3] ci: retrigger