Skip to content

Report a build error when a macro returns an unsupported platform object - #32844

Closed
robobun wants to merge 1 commit into
mainfrom
farm/d72ebee1/macro-return-coercion
Closed

robobun wants to merge 1 commit into
mainfrom
farm/d72ebee1/macro-return-coercion

Conversation

@robobun

@robobun robobun commented Jun 27, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • A macro that returns a platform object other than Response, Request, or Blob (Headers, FormData, ...) is inlined as "" at every call site. The build succeeds with wrong constants baked in. On 1.4.1, bun build of a file that calls export function f() { return new Headers(); } emits console.log(JSON.stringify("")) and exits 0.
  • The cause is the T::Private arm of Run::coerce in src/js_parser_jsc/Macro.rs. After the Response/Request/Blob downcasts miss, it returns Ok(Expr::init(E::EString::EMPTY, ..)). Every other unsupported return type (Symbol, Date, function, URL through its custom inspect hook) already reports cannot coerce <Class> (<JSType>) to Bun's AST. Please return a simpler type.

Fix

  • The fall-through now reports that same build error. The message formatting moves into Run::unsupported_value_error, and the two existing error sites call it too, so the bottom-of-coerce path gains the class name it lacked.
  • Correct because docs/bundler/macros.mdx ("Serializability") says instances of classes other than those three are not serializable. A build error is the documented contract. A silent placeholder is the worst outcome for a build-time inliner.
  • Verified: test/bundler/transpiler/macro-test.test.ts gains three cases (Headers, FormData, an object with nested Headers). They fail on 1.4.1 with "" inlined and pass with this change. The rest of that file, transpiler.test.js, and the macro regression tests also pass.

Background

  • A macro runs at bundle time. Run::coerce turns its JS return value into an AST node by the formatter tag of the value. T::Private is the tag for a DOMWrapper cell, that is, a native platform object.
  • Response/Request are reduced to their body Blob, and a Blob is inlined by its content type in expr_from_blob. Those three are the only platform objects with an AST shape.
Notes

The first version of this PR also fixed expr_from_blob, which compared the whole Content-Type against application/json and so demoted application/json;charset=utf-8 to a base64 data URL. #40700 landed that fix on main through MimeType::init, so this PR now carries only the platform object change. #40700 keeps the type match case-sensitive on purpose, to agree with the runtime's blob classification, so the case-insensitive variant from an earlier revision of this PR is dropped too.

#40059 rewrites the whole coercion path and makes the fall-through impossible as a side effect. This is the small fix that can land ahead of it.

Probe on 1.4.1 of what each platform object becomes when returned from a macro:

new Headers({ "x-a": "b" })   =>  ""
new FormData()                =>  ""
{ list: [new Headers()] }     =>  { list: [""] }
new URL("https://bun.com")    =>  error: cannot coerce URL (JSType(238))
new URLSearchParams("a=1")    =>  error: cannot coerce JSType(241)

Related: #7115


[review] gate passed · iteration 6 · 2 files touched

fails on main (without fix)
ASAN without fix: 3 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/bundler/transpiler/macro-test.test.ts
bun test v1.4.1 (65362b53b)

test/bundler/transpiler/macro-test.test.ts:
[macro] call escapeHTML
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call escape
[macro] call addStrings
[macro] call addStrings
[macro] call addStrings
[macro] call addStrings
[macro] call addStrings
[macro] call addStrings
[macro] call addStrings
[macro] call addStrings
[macro] call addStrings
[macro] call addStrings
[macro] call addStrings
[macro] call addStrings
[macro] c
... (truncated)

release without fix: 4 FAILED
bun test v1.4.1-canary.1 (65362b53b)

test/bundler/transpiler/macro-test.test.ts:
(pass) bun builtins can be used in macros [0.04ms]
(pass) latin1 string [0.02ms]
(pass) ascii string [0.01ms]
(pass) type coercion [0.07ms]
(pass) escaping [0.30ms]
(pass) utf16 string [0.02ms]
(pass) import aliases [0.03ms]
(pass) default import [0.02ms]
(pass) namespace import [0.03ms]
(pass) ireturnapromise [0.09ms]
150 |     cwd: String(dir),
151 |     stdout: "pipe",
152 |     stderr: "pipe",
153 |   });
154 |   const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
155 |   expect({ stdout, stderr, exitCode }).toMatchObject({
                                             ^
error: expect(received).toMatchObject(expected)

  {
-   "exitCode": 1,
-   "stderr": StringContaining "cannot coerce Headers",
+   "exitCode": 0,
+   "stderr": "",
+   "stdout": 
+ "// index.ts
+ console.log(JSON.stringify(""));
+ "
+ ,
  }

- Expected  - 2
+ Received  + 7

      at <anonymous> (/workspace/bun/test/bundler/transpiler/macro-test.test.ts:155:40)
(fail) macro returning Headers is a build error [20.23ms]
150 |     cwd: String(dir),
151 |     stdo
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/bundler/transpiler/macro-test.test.ts
bun test v1.4.1 (65362b53b)

test/bundler/transpiler/macro-test.test.ts:
[macro] call escapeHTML
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call escape
[macro] call addStrings
[macro] call addStrings
[macro] call addStrings
[macro] call addStrings
[macro] call addStrings
[macro] call addStrings
[macro] call addStrings
[macro] call addStrings
[macro] call addStrings
[macro] call addStrings
[macro] call addStrings
[macro] call addStrings
[macro] c
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped)
  target       linux-x64-gnu
  build type   Release
  build dir    ./build/release
  revision     1a5755b823
  features     baseline

23 deps, 131 codegen, 1172 objects in 987ms

ninja: Entering directory `/workspace/bun/build/release'
[1/1244] gen bindgenv2
[2/1244] gen ErrorCode+*.h
[3/1244] gen ProcessBindingConstants.lut.h
Generating /workspace/bun/build/release/codegen/ProcessBindingConstants.lut.h from /workspace/bun/src/jsc/bindings/ProcessBindingConstants.cpp
[4/1244] gen JSBuffer.lut.h
Generating /workspace/bun/build/release/codegen/JSBuffer.lut.h from /workspace/bun/src/jsc/bindings/JSBuffer.cpp
[5/1244] fetch tinycc
[tinycc] up to date
[6/1243] gen .bind.ts → GeneratedBindings.cpp
[7/1243] fetch zlib
[zlib] up to date
[8/1243] install /workspace/bun
bun install v1.4.1-canary.1 (65362b53b)

Checked 26 installs across 63 packages (no changes) [215.00ms]
[9/1243] gen ProcessBindingBuffer.lut.h
Generating /workspace/bun/build/release/codegen/ProcessBindingBuffer.lut.h from /workspace/bun/src/jsc/bindings/ProcessBindingBuffer.cpp
[10/1243] gen ProcessBindingHTTPParser.lut.h

... (truncated)
diff hotspot
src/js_parser_jsc/Macro.rs                 | 44 ++++++++++++------------------
 test/bundler/transpiler/macro-test.test.ts | 27 ++++++++++++++++++
 2 files changed, 45 insertions(+), 26 deletions(-)

gate history · 1 passed · 0 rejected · iteration 6

evidence per changed file
file                                        reads  edits  tests
src/js_parser_jsc/Macro.rs                     11      9     11
test/bundler/transpiler/macro-test.test.ts      5      6     11

@coderabbitai

coderabbitai Bot commented Jun 27, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The macro transpiler now routes unsupported macro values through a shared coercion error path, and blob serialization normalizes MIME types before deciding JSON or text inlining. Tests add coverage for unsupported platform objects and MIME values with parameters and case variation.

Changes

Macro coercion and blob MIME classification

Layer / File(s) Summary
Unsupported value errors
src/js_parser_jsc/Macro.rs, test/bundler/transpiler/macro-test.test.ts
Runner::coerce now sends unsupported values through unsupported_value_error, including the DOMWrapper fallback and default case; tests check build failures for URL/Headers macros and verify the output is not replaced with an empty string.
Normalized MIME classification
src/js_parser_jsc/Macro.rs, test/bundler/transpiler/macro-test.test.ts
expr_from_blob now strips MIME parameters, trims whitespace, and lowercases the essence before JSON and text-like checks; tests cover parameterized and case-varied application/json and text/plain responses.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the primary change: build errors are reported when macros return unsupported platform objects.
Description check ✅ Passed The description explains the problem, fix, expected behavior, documentation basis, and verification results. It does not use the exact template headings, but it includes the required information and i…
Full details: Description check

Explanation

The description explains the problem, fix, expected behavior, documentation basis, and verification results. It does not use the exact template headings, but it includes the required information and is complete.


Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Jun 27, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 11:37 PM PT - Aug 27th, 2026

@robobun, your commit 1a5755b is building: #107437

@robobun

robobun commented Jun 27, 2026 •

Copy link
Copy Markdown
Collaborator Author

Reproduced on 1.4.1 before the fix:

$ cat macro.ts
export function h() { return new Headers({ "x-a": "b" }); }
$ cat index.ts
import { h } from "./macro.ts" with { type: "macro" };
console.log(JSON.stringify(h()));
$ bun build index.ts   # 1.4.1
// index.ts
console.log(JSON.stringify(""));
$ echo $?
0

With this branch the build fails with error: cannot coerce Headers (JSType(238)) to Bun's AST. Please return a simpler type.

The three new cases in test/bundler/transpiler/macro-test.test.ts fail on the released binary and pass with bun bd test.

Scope note: the first revision of this PR also fixed the application/json;charset=utf-8 demotion to a base64 data URL. That landed on main separately in #40700, so this PR was rebased onto it and now carries only the platform object change.

Comment thread src/js_parser_jsc/Macro.rs Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — thanks for folding in the case-insensitive essence match (and the extra tests for it).

Extended reasoning...

Overview

Two narrow fixes in src/js_parser_jsc/Macro.rs plus ~90 lines of new subprocess tests:

  1. The DOMWrapper fall-through in Run::coerce now returns the standard "cannot coerce … to Bun's AST" build error instead of silently inlining "" for unsupported platform objects (URL, Headers, …). The error-reporting logic was extracted into unsupported_value_error() and reused at all three fall-through sites, so the message format is now consistent (the bottom-of-coerce path gains the class name it previously lacked).
  2. expr_from_blob now matches on the MIME essence — slice before ;, ASCII-trimmed, lowercased via the existing strings::copy_lowercase_if_needed helper into a bump-arena scratch buffer — instead of byte-comparing the full Content-Type. This restores the pre-port behavior where application/json;charset=utf-8 (the value real servers and the Response constructor produce) inlines the parsed object rather than a base64 data URL.

Since my earlier review on 36cc777, the author pushed 8998396 which addresses my case-sensitivity nit exactly as suggested (lowercase once, then compare) and added Application/JSON; charset=UTF-8 / TEXT/Plain test cases that exercise it.

Security risks

None. This is build-time macro return-value serialization; no auth, crypto, network, or filesystem surface is touched. No new unsafe blocks. The lowercasing buffer is sized exactly to essence.len() and copy_lowercase_if_needed writes at most that many bytes.

Level of scrutiny

Moderate — it's transpiler/bundler core, but the blast radius is confined to the macro return-value coercion path. The MIME change is strictly widening (every value that matched before still matches). The platform-object change is a deliberate behavior change from silently-wrong ("") to a build error, which matches the documented contract in docs/bundler/macros.mdx and the existing handling for every other unsupported type.

Other factors

  • No CODEOWNERS entry covers src/js_parser_jsc/ or the test file.
  • The bug-hunting system found nothing on the latest revision.
  • copy_lowercase_if_needed and index_of_char are established helpers already used the same way in resolver/fs.rs and http/h2_client/encode.rs.
  • The unconditional bump.alloc_slice_fill_copy(essence.len(), 0) allocates ~20 bytes of arena scratch even when the essence is already lowercase — negligible, and the arena is per-MacroContext.
  • Tests are subprocess-based integration tests following the existing pattern in this file (tempDir + Bun.spawn + tail-match to tolerate the debug [macro] call … prefix), and the PR description confirms they fail on 1.4.0 and pass on this branch.

@robobun

robobun commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator Author

This PR needs a rebase onto #40059 once it lands, and half of it is covered there.

Checked on a debug build of the #40059 branch with this PR's test cases:

  • Platform objects: covered. Convert::value in the new src/js_parser_jsc/Macro.rs reports cannot coerce URL (JSType(238)) to Bun's AST for URL, Headers and a nested URL, with exit code 1 and no empty string in the output. The unsupported_value_error part of this diff is no longer needed.
  • MIME parameters and case: still needed. expr_from_blob keeps the exact comparisons, so application/json;charset=utf-8, Application/JSON; charset=UTF-8, a Blob with application/json; charset=utf-8, and a TEXT/Plain Response all come back as base64 data URLs (the four tests fail there).

On a rebase, keep the essence extraction in expr_from_blob (its signature there is (bytes, bump: &Arena, mime_type, log, loc)) and the three MIME test groups. Drop the unsupported_value_error hunks and keep or drop the platform object tests as you see fit, since #40059 covers them.

@robobun

robobun commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator Author

#40700 fixes the MIME parameter half of this PR on main. It routes the content type through MimeType::init and a new Category::is_text_like() instead of matching the essence by string, so the macro classifies a blob type the same way Body.rs and Blob.rs do. The platform object half is covered by #40059, as noted above.

Jarred-Sumner pushed a commit that referenced this pull request Aug 28, 2026
…0700)

### Problem
- A macro that returns `Response.json(...)`, a `fetch()` of a JSON
endpoint, or a `Blob` typed `application/json; charset=utf-8` is inlined
as a base64 `data:` URL string
(`"data:application/json;charset=utf-8;base64,eyJhIjoxfQ=="`) instead of
the parsed object. Same for a plain `application/json` header, because
the Response to Blob step normalizes it to
`application/json;charset=utf-8`.
- `expr_from_blob` (`src/js_parser_jsc/Macro.rs:1058`) compares the raw
content type with `== b"application/json"` and `starts_with(b"text/")`.
Any parameter (`;charset=utf-8`) misses every branch and falls through
to the data URL arm. The Zig code used `MimeType.init(...)` and
`category.isTextLike()`. The Rust port replaced those with the byte
compares.

### Fix
- `expr_from_blob` calls `MimeType::init(content_type, false, None)` and
branches on `mime_type.category`: `Json` parses the body,
`is_text_like()` inlines a string, everything else stays a data URL.
`MimeType::init` strips parameters. It is what `Body.rs` and `Blob.rs`
already use to classify a blob type, so the macro now agrees with the
runtime.
- Adds `Category::is_text_like()` to `src/http_types/MimeType.rs`:
javascript, html, text, css, json. This is the set the pre-port code
used. `src/CLAUDE.md` already documents the method.
- Adds `bun_http_types` to the `bun_js_parser_jsc` dependencies
(Cargo.toml and Cargo.lock).
- Verified: `test/bundler/transpiler/macro-test.test.ts` (new test,
fails on 1.4.1 with the data URL output, passes with this change). Also
ran the rest of that file, `transpiler.test.js`, the macro regression
tests, and `test/js/web/fetch/blob.test.ts`.

### Background
- A macro result is turned into an AST node by `Run::coerce`. A
`Response` or `Request` is first reduced to its body `Blob`.
`expr_from_blob` then picks the node shape from the blob's content type.
- `bun_http_types::MimeType::init` parses `type/subtype;params` into a
`MimeType` with a `Category`. Parameters are cut at the first `;`.
`application/json` and `application/geo+json` map to `Category::Json`.
`text/*` maps to `Text` (or `Css`, `Html`, `Javascript`, and
`text/plain` to the `TEXT` constant).
- `Response.json()` sets `application/json;charset=utf-8`
(`MimeType::JSON`). A `Response` with a `content-type` header and no
blob type gets its blob type from `MimeType::init` on that header
(`src/runtime/webcore/Body.rs:2146`).

<details><summary>Notes</summary>

Repro on 1.4.1:

```ts
// macro.ts
export function j() { return Response.json({ a: 1 }); }
// index.ts
import { j } from "./macro.ts" with { type: "macro" };
console.log(j());
```

`bun build index.ts` emits
`console.log("data:application/json;charset=utf-8;base64,eyJhIjoxfQ==");`.
With this change it emits `console.log({ a: 1 });`.

Behavior that changes besides the parameter handling, because the
classification now follows `MimeType::init`:

- `application/javascript`, `application/x-javascript`,
`application/ecmascript`, `application/xml` with no parameters were
inlined as strings. They are `Category::Application` in `MimeType::init`
and now become data URLs. With parameters they were data URLs before
too. The pre-port code did the same.
- `+json` suffixes other than `geo+json` (for example
`application/ld+json`) with no parameters were parsed as JSON. They are
`Category::Application` and now become data URLs. `text/json` is
`Category::Text` and becomes a string.
- The type is still case-sensitive, as it is everywhere else
`MimeType::init` is used. A Blob built in JS is lowercased by the `Blob`
constructor. A header value is used verbatim.

The data URL arm keeps the raw content type in the URL (`data:<content
type>;base64,...`), unchanged.

Related: #32844 fixes the same parameter problem with string essence
matching, plus a platform object error that #40059 covers. #40059
(`claude/macro-host`) also strips parameters inside `expr_from_blob`
with string compares. This change conflicts with it only in the body of
`expr_from_blob` and one Cargo.toml line.

The `it.todo` cases in `transpiler.test.js` ("macros can return a
Response body", "pass objects to macros") stay todo. They depend on
`transformSync(code, ctx)`. That call appends `ctx` after the call
arguments, so a macro called with no arguments receives `ctx` as its
first parameter and the fixture's second parameter is `undefined`. That
is separate from this change.

</details>

<!-- robobun:evidence:begin -->

---

**[review]** gate passed · iteration 0 · 5 files touched

<details><summary>fails on main (without fix)</summary>

```console
ASAN without fix: BUILD FAILED (no junit output)
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/bundler/transpiler/macro-test.test.ts
ninja: Entering directory `/workspace/bun/build/debug'
[0/3] cargo bun_runtime → libbun_runtime.a
FAILED: rust-target/x86_64-unknown-linux-gnu/debug/libbun_runtime.a 
/workspace/bun/build/release/bun /workspace/bun/scripts/build/stream.ts rust --console --cwd=/workspace/bun --env=CARGO_TERM_COLOR=always --env=BUN_CODEGEN_DIR=/workspace/bun/build/debug/codegen --env=CC=/usr/lib/llvm-21/bin/clang --env=CXX=/usr/lib/llvm-21/bin/clang++ --env=AR=/usr/lib/llvm-21/bin/llvm-ar --env=CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER=/usr/lib/llvm-21/bin/clang++ --env=CARGO_HOME=/root/.cargo --env=RUSTUP_HOME=/root/.rustup --env=RUSTUP_TOOLCHAIN=nightly-2026-07-20 --env=CARGO_PROFILE_RELEASE_LTO=off --env=CARGO_PROFILE_RELEASE_CODEGEN_UNITS=16 --env=CARGO_PROFILE_RELEASE_DEBUG_ASSERTIONS=true --env=CARGO_ENCODED_RUSTFLAGS='-Crelocation-model=static�-Cforce-frame-pointers=yes�-Zthreads=8�-Cllvm-args=-addrsig�-Ctarget-cpu=nehalem�--check-cfg=cfg(bun_asan)�-Zsanitizer=address�--cfg=bun_asan�--check-cfg=cfg(b
... (truncated)

release without fix: 1 FAILED
bun test v1.4.1-canary.1 (65362b5)

test/bundler/transpiler/macro-test.test.ts:
(pass) bun builtins can be used in macros [0.03ms]
(pass) latin1 string
(pass) ascii string
(pass) type coercion [0.05ms]
(pass) escaping [0.17ms]
(pass) utf16 string [0.01ms]
(pass) import aliases [0.02ms]
(pass) default import
(pass) namespace import [0.02ms]
(pass) ireturnapromise [0.08ms]
(pass) object argument with a sparse numeric key [15.56ms]
(pass) object destructuring of a macro result keeps every bound property regardless of key order or repeated keys [16.09ms]
221 |     stdout: "pipe",
222 |     stderr: "pipe",
223 |   });
224 |   const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
225 |   // Debug builds print "[macro] call <name>" to stdout before the script's own output.
226 |   expect({ lastLine: stdout.trim().split("\n").pop(), stderr }).toEqual({
                                                                      ^
error: expect(received).toEqual(expected)

  {
-   "lastLine": "[{"a":1,"b":[true,null,"x"]},{"b":2},{"from":"server"},"hello",{"c":3},"data:application/octet-stream;base64,AQID"]",
+   "lastLine": 
... (truncated)
```

</details>

<details><summary>passes on PR (with fix)</summary>

```console
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/bundler/transpiler/macro-test.test.ts
bun test v1.4.1 (65362b5)

test/bundler/transpiler/macro-test.test.ts:
[macro] call escapeHTML
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call identity
[macro] call escape
[macro] call addStrings
[macro] call addStrings
[macro] call addStrings
[macro] call addStrings
[macro] call addStrings
[macro] call addStrings
[macro] call addStrings
[macro] call addStrings
[macro] call addStrings
[macro] call addStrings
[macro] call addStrings
[macro] call addStrings
[macro] c
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped)
  target       linux-x64-gnu
  build type   Release
  build dir    ./build/release
  revision     a7583e5
  features     baseline

23 deps, 131 codegen, 1172 objects in 1405ms

ninja: Entering directory `/workspace/bun/build/release'
[1/1244] gen bindgenv2
[2/1244] fetch zlib
[zlib] up to date
[3/1244] fetch tinycc
[tinycc] up to date
[4/1243] fetch libjpeg-turbo
[libjpeg-turbo] up to date
[5/1216] install /workspace/bun
bun install v1.4.1-canary.1 (65362b5)

Checked 26 installs across 63 packages (no changes) [50.00ms]
[6/1216] gen ErrorCode+*.h
[7/1216] gen .bind.ts → GeneratedBindings.cpp
[8/1216] gen ProcessBindingConstants.lut.h
Generating /workspace/bun/build/release/codegen/ProcessBindingConstants.lut.h from /workspace/bun/src/jsc/bindings/ProcessBindingConstants.cpp
[9/1216] install /workspace/bun/packages/bun-error
bun install v1.4.1-canary.1 (65362b5)

Checked 1 install across 2 packages (no changes) [3.00ms]
[10/1216] gen JSBuffer.lut.h
Generating /workspace/bun/build/release/codegen/JSBuffer.lut.h from /workspace/bun/src/jsc/bindings/JSBuffer.cpp
[11/1216] gen b
... (truncated)
```

</details>

<details><summary>diff hotspot</summary>

```
Cargo.lock                                 |  1 +
 src/http_types/MimeType.rs                 |  7 ++++
 src/js_parser_jsc/Cargo.toml               |  1 +
 src/js_parser_jsc/Macro.rs                 | 27 ++++++---------
 test/bundler/transpiler/macro-test.test.ts | 55 ++++++++++++++++++++++++++++++
 5 files changed, 74 insertions(+), 17 deletions(-)
```

</details>

**gate history** · 1 passed · 0 rejected · iteration 0

<details><summary>evidence per changed file</summary>

```
file                                        reads  edits  tests
Cargo.lock                                      0      0      0
src/http_types/MimeType.rs                      1      1      0
src/js_parser_jsc/Cargo.toml                    1      2      0
src/js_parser_jsc/Macro.rs                      3      3      0
test/bundler/transpiler/macro-test.test.ts      1      2      0
```

</details>

<!-- robobun:evidence:end -->
@robobun
robobun force-pushed the farm/d72ebee1/macro-return-coercion branch from 8998396 to 8fc8265 Compare August 28, 2026 06:34
Comment thread src/js_parser_jsc/Macro.rs Outdated
Comment thread src/js_parser_jsc/Macro.rs Outdated
A macro returning a platform object other than Response/Request/Blob
(URL, Headers, FormData, ...) hit a fall-through in the DOMWrapper arm
of Run::coerce that inlined an empty string at every call site, so the
build succeeded with wrong constants baked in. Report the same build
error every other unsupported return type gets.
@robobun
robobun force-pushed the farm/d72ebee1/macro-return-coercion branch from 8fc8265 to 1a5755b Compare August 28, 2026 06:37
@robobun robobun changed the title Fix macro return value coercion for platform objects and parameterized MIME types Report a build error when a macro returns an unsupported platform object Aug 28, 2026
Comment on lines +155 to +160
expect({ stdout, stderr, exitCode }).toMatchObject({
stderr: expect.stringContaining(`cannot coerce ${className}`),
exitCode: 1,
});
// The bundle must not be emitted with the macro call replaced by an empty string.
expect(stdout).not.toContain('""');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 nit: expect(stdout).not.toContain('""') is a vacuous assertion that can never fail

Extended reasoning...

The preceding toMatchObject asserts exitCode: 1 and stderr contains the coercion error. When that assertion passes, bun build index.ts has failed and written nothing to stdout, so .not.toContain('""') is trivially true against an empty string. When it does not pass (the pre-fix behavior — build succeeds, exit 0, bundle on stdout containing ""), toMatchObject throws before this line is reached. Either way the stdout assertion never fires, so it does not guard the regression the comment above it describes; on the base branch the test fails on toMatchObject, not here. Per REVIEW.md ("Every assertion must be able to fail"), fold the stdout expectation into the same toMatchObject (e.g. stdout: expect.not.stringContaining('""')) so a single failure diff shows all three fields, or drop it.

Verification: nit — The assertion is vacuous as claimed. At test/bundler/transpiler/macro-test.test.ts (added block), the order is: expect({ stdout, stderr, exitCode }).toMatchObject({ stderr: expect.stringContaining(`cannot coerce ${className}`), exitCode: 1, }); // The bundle must not be emitted with the macro call replaced by an empty string. expect(stdout).not.toContain('""'); Two exhaustive…

@robobun

robobun commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator Author

Closing this in favor of #40700 and #40059.

The MIME parameter half landed on main in #40700. expr_from_blob now classifies the blob type through MimeType::init, so Response.json(), application/json;charset=utf-8, and a Blob typed application/json; charset=utf-8 inline the parsed value. I checked this on a debug build of main at f170b9c with the test cases from the first revision of this PR. One known difference: #40700 keeps the type match case-sensitive on purpose, to agree with the runtime's blob classification, so Application/JSON and TEXT/Plain still produce a data URL.

The platform object half is covered by #40059, which reworks the same coercion path. Its Convert::value reports cannot coerce Headers (JSType(238)) to Bun's AST for any DOMWrapper other than Response, Request, or Blob, instead of inlining "". On main today a Headers return still inlines "". If #40059 stalls, the change in 1a5755b is the small standalone fix.

@robobun robobun closed this Aug 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant