diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index e0135432b0..44a32429d5 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -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; } diff --git a/src/bun.js/api/JSBundler.zig b/src/bun.js/api/JSBundler.zig index 6e77655062..1a948f78b3 100644 --- a/src/bun.js/api/JSBundler.zig +++ b/src/bun.js/api/JSBundler.zig @@ -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(); diff --git a/src/bundler/bundle_v2.zig b/src/bundler/bundle_v2.zig index a52515b465..af6199fe68 100644 --- a/src/bundler/bundle_v2.zig +++ b/src/bundler/bundle_v2.zig @@ -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. diff --git a/test/regression/issue/25716.test.ts b/test/regression/issue/25716.test.ts new file mode 100644 index 0000000000..a21331f268 --- /dev/null +++ b/test/regression/issue/25716.test.ts @@ -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 ; + } + + export default function App() { + return
; + } + `, + }); + + // 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$"); +});