feat(bundler): expose reactFastRefresh option in Bun.build API (#25731)

Fixes #25716

Adds support for a `reactFastRefresh: boolean` option in the `Bun.build`
JavaScript API, matching the existing `--react-fast-refresh` CLI flag.

```ts
const result = await Bun.build({
    reactFastRefresh: true,
    entrypoints: ["src/App.tsx"],
});
```

When enabled, the bundler adds React Fast Refresh transform code
(`$RefreshReg$`, `$RefreshSig$`) to the output.
This commit is contained in:
Tommy D. Rossi
2025-12-29 07:07:47 +01:00
committed by GitHub
parent d04b86d34f
commit 538be1399c
4 changed files with 65 additions and 0 deletions

View File

@@ -1942,6 +1942,16 @@ declare module "bun" {
development?: boolean;
};
/**
* Enable React Fast Refresh transform.
*
* This adds the necessary code transformations for React Fast Refresh (hot module
* replacement for React components), but does not emit hot-module code itself.
*
* @default false
*/
reactFastRefresh?: boolean;
outdir?: string;
}

View File

@@ -7,6 +7,7 @@ pub const JSBundler = struct {
target: Target = Target.browser,
entry_points: bun.StringSet = bun.StringSet.init(bun.default_allocator),
hot: bool = false,
react_fast_refresh: bool = false,
define: bun.StringMap = bun.StringMap.init(bun.default_allocator, false),
loaders: ?api.LoaderMap = null,
dir: OwnedString = OwnedString.initEmpty(bun.default_allocator),
@@ -341,6 +342,10 @@ pub const JSBundler = struct {
}
}
if (try config.getBooleanLoose(globalThis, "reactFastRefresh")) |react_fast_refresh| {
this.react_fast_refresh = react_fast_refresh;
}
var has_out_dir = false;
if (try config.getOptional(globalThis, "outdir", ZigString.Slice)) |slice| {
defer slice.deinit();

View File

@@ -1904,6 +1904,7 @@ pub const BundleV2 = struct {
transpiler.options.css_chunking = config.css_chunking;
transpiler.options.banner = config.banner.slice();
transpiler.options.footer = config.footer.slice();
transpiler.options.react_fast_refresh = config.react_fast_refresh;
if (transpiler.options.compile) {
// Emitting DCE annotations is nonsensical in --compile.

View File

@@ -0,0 +1,49 @@
// https://github.com/oven-sh/bun/issues/25716
// Expose `--react-fast-refresh` option in `Bun.build` JS API
import { expect, test } from "bun:test";
import { tempDirWithFiles } from "harness";
import { join } from "path";
test("Bun.build reactFastRefresh option enables React Fast Refresh transform", async () => {
const dir = tempDirWithFiles("react-fast-refresh-test", {
"component.tsx": `
import { useState } from "react";
export function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
export default function App() {
return <div><Counter /></div>;
}
`,
});
// With reactFastRefresh: true, output should contain $RefreshReg$ and $RefreshSig$
const buildEnabled = await Bun.build({
entrypoints: [join(dir, "component.tsx")],
reactFastRefresh: true,
target: "browser",
external: ["react"],
});
expect(buildEnabled.success).toBe(true);
expect(buildEnabled.outputs).toHaveLength(1);
const outputEnabled = await buildEnabled.outputs[0].text();
expect(outputEnabled).toContain("$RefreshReg$");
expect(outputEnabled).toContain("$RefreshSig$");
// Without reactFastRefresh (default), output should NOT contain refresh calls
const buildDisabled = await Bun.build({
entrypoints: [join(dir, "component.tsx")],
target: "browser",
external: ["react"],
});
expect(buildDisabled.success).toBe(true);
const outputDisabled = await buildDisabled.outputs[0].text();
expect(outputDisabled).not.toContain("$RefreshReg$");
expect(outputDisabled).not.toContain("$RefreshSig$");
});