diff --git a/test/bundler/transpiler/jsx-dev/jsx-dev.tsx b/test/bundler/transpiler/jsx-dev/jsx-dev.tsx
deleted file mode 100644
index ee854a800268..000000000000
--- a/test/bundler/transpiler/jsx-dev/jsx-dev.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import { renderToReadableStream } from "react-dom/server.browser";
-
-const HelloWorld = () => {
- return
Hello World
;
-};
-
-const stream = new Response(await renderToReadableStream());
-
-console.log(await stream.text());
-
-if (!process.env.NO_BUILD) {
- const self = await Bun.build({
- entrypoints: [import.meta.path],
- define: {
- "process.env.NODE_ENV": JSON.stringify(process.env.CHILD_NODE_ENV),
- "process.env.NO_BUILD": "1",
- },
- });
- const code = await self.outputs[0].text();
- let shouldHaveJSXDev = process.env.CHILD_NODE_ENV === "development";
- let shouldHaveJSX = process.env.CHILD_NODE_ENV === "production";
-
- if (shouldHaveJSXDev) {
- if (!code.includes("jsx_dev_runtime.jsxDEV")) {
- throw new Error("jsxDEV is not included");
- }
- }
-
- if (shouldHaveJSX) {
- if (!code.includes("jsx_runtime.jsx")) {
- throw new Error("Jsx is not included");
- }
- }
-
- const url = URL.createObjectURL(self.outputs[0]);
- await import(url);
-}
diff --git a/test/bundler/transpiler/jsx-dev/tsconfig.json b/test/bundler/transpiler/jsx-dev/tsconfig.json
deleted file mode 100644
index 12a075f81a97..000000000000
--- a/test/bundler/transpiler/jsx-dev/tsconfig.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "extends": "../../tsconfig.json",
- "compilerOptions": {
- "jsx": "react-jsxdev"
- }
-}
diff --git a/test/bundler/transpiler/jsx-production-entry.ts b/test/bundler/transpiler/jsx-production-entry.ts
deleted file mode 100644
index eec766abee15..000000000000
--- a/test/bundler/transpiler/jsx-production-entry.ts
+++ /dev/null
@@ -1 +0,0 @@
-import "./jsx-production";
diff --git a/test/bundler/transpiler/jsx-production.test.ts b/test/bundler/transpiler/jsx-production.test.ts
index 537efad04e7b..5fa717789adc 100644
--- a/test/bundler/transpiler/jsx-production.test.ts
+++ b/test/bundler/transpiler/jsx-production.test.ts
@@ -1,46 +1,80 @@
import { describe, expect, test } from "bun:test";
-import { bunEnv, bunExe } from "harness";
-import path from "path";
+import { bunEnv, bunExe, tempDir } from "harness";
// https://github.com/oven-sh/bun/issues/3768
-describe.concurrent("jsx", () => {
- for (const node_env of ["production", "development", "test", ""]) {
- for (const child_node_env of ["production", "development", "test", ""]) {
- test(`react-jsxDEV parent: ${node_env} child: ${child_node_env} should work`, async () => {
- const env = { ...bunEnv };
- env.NODE_ENV = node_env;
- env.CHILD_NODE_ENV = child_node_env;
- env.TSCONFIG_JSX = "react-jsxdev";
- await using proc = Bun.spawn({
- cmd: [bunExe(), "run", path.join(import.meta.dirname, "jsx-dev", "jsx-dev.tsx")],
- cwd: import.meta.dirname,
- env: env,
- stdout: "pipe",
- stderr: "inherit",
- stdin: "ignore",
- });
- const out = await new Response(proc.stdout).text();
- expect(out).toBe("Hello World
" + "\n" + "Hello World
" + "\n");
- expect(await proc.exited).toBe(0);
+//
+// app.tsx is transpiled twice per case: once by `bun run` (NODE_ENV comes from the
+// environment) and once by the Bun.build() call inside it, which defines
+// process.env.NODE_ENV to CHILD_NODE_ENV and then executes the bundle it produced.
+// The stub `react` runtimes return their own name, so each pass prints which runtime
+// the entry point and the file it imports were compiled against. (This used to render
+// with the real react-dom and bundle it; that took several seconds per case on debug
+// builds, and the 32 concurrent cases timed out.)
+const files = {
+ "node_modules/react/jsx-runtime.js": /* js */ `
+ export const Fragment = Symbol.for("react.fragment");
+ export const jsx = () => "jsx";
+ export const jsxs = jsx;
+ `,
+ "node_modules/react/jsx-dev-runtime.js": /* js */ `
+ export const Fragment = Symbol.for("react.fragment");
+ export const jsxDEV = () => "jsxDEV";
+ `,
+ "child.tsx": /* tsx */ `
+ export const child = ;
+ `,
+ "app.tsx": /* tsx */ `
+ import { child } from "./child";
+
+ console.log(process.env.BUNDLED ? "bundle:" : "run:", , child);
+
+ if (!process.env.BUNDLED) {
+ const build = await Bun.build({
+ entrypoints: [import.meta.path],
+ define: {
+ "process.env.NODE_ENV": JSON.stringify(process.env.CHILD_NODE_ENV),
+ "process.env.BUNDLED": "1",
+ },
});
+ await import(URL.createObjectURL(build.outputs[0]));
+ }
+ `,
+};
- test(`react-jsx parent: ${node_env} child: ${child_node_env} should work`, async () => {
- const env = { ...bunEnv };
- env.NODE_ENV = node_env;
- env.CHILD_NODE_ENV = child_node_env;
- env.TSCONFIG_JSX = "react-jsx";
- await using proc = Bun.spawn({
- cmd: [bunExe(), "run", path.join(import.meta.dirname, "jsx-production-entry.ts")],
- cwd: import.meta.dirname,
- env: env,
- stdout: "pipe",
- stderr: "inherit",
- stdin: "ignore",
+const runtimes = ["jsx", "jsxDEV"];
+const nodeEnvs = ["production", "development", "test", ""];
+
+describe.concurrent("jsx", () => {
+ for (const jsx of ["react-jsx", "react-jsxdev"]) {
+ for (const node_env of nodeEnvs) {
+ for (const child_node_env of nodeEnvs) {
+ test(`tsconfig ${jsx}, NODE_ENV=${JSON.stringify(node_env)}, bundle defines NODE_ENV=${JSON.stringify(child_node_env)}`, async () => {
+ using dir = tempDir("jsx-production", {
+ ...files,
+ "tsconfig.json": JSON.stringify({ compilerOptions: { jsx } }),
+ });
+ await using proc = Bun.spawn({
+ cmd: [bunExe(), "run", "app.tsx"],
+ cwd: String(dir),
+ env: { ...bunEnv, BUN_ENV: undefined, NODE_ENV: node_env, CHILD_NODE_ENV: child_node_env },
+ stdout: "pipe",
+ stderr: "pipe",
+ });
+ const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
+
+ // NODE_ENV=production selects the production runtime in both passes, and a bundle
+ // whose define says "development" selects jsxDEV no matter what the parent process
+ // or tsconfig said. The remaining combinations fall back to the tsconfig and the
+ // parent environment; they are not pinned here, but both passes still have to pick
+ // a runtime that exists and compile both files against the same one.
+ const runPass = node_env === "production" ? ["jsx"] : runtimes;
+ const bundlePass =
+ child_node_env === "production" ? ["jsx"] : child_node_env === "development" ? ["jsxDEV"] : runtimes;
+ expect(stderr).toBe("");
+ expect(stdout).toBeOneOf(runPass.flatMap(r => bundlePass.map(b => `run: ${r} ${r}\nbundle: ${b} ${b}\n`)));
+ expect(exitCode).toBe(0);
});
- const out = await new Response(proc.stdout).text();
- expect(out).toBe("Hello World
" + "\n" + "Hello World
" + "\n");
- expect(await proc.exited).toBe(0);
- });
+ }
}
}
});
diff --git a/test/bundler/transpiler/jsx-production.tsx b/test/bundler/transpiler/jsx-production.tsx
deleted file mode 100644
index ee854a800268..000000000000
--- a/test/bundler/transpiler/jsx-production.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import { renderToReadableStream } from "react-dom/server.browser";
-
-const HelloWorld = () => {
- return Hello World
;
-};
-
-const stream = new Response(await renderToReadableStream());
-
-console.log(await stream.text());
-
-if (!process.env.NO_BUILD) {
- const self = await Bun.build({
- entrypoints: [import.meta.path],
- define: {
- "process.env.NODE_ENV": JSON.stringify(process.env.CHILD_NODE_ENV),
- "process.env.NO_BUILD": "1",
- },
- });
- const code = await self.outputs[0].text();
- let shouldHaveJSXDev = process.env.CHILD_NODE_ENV === "development";
- let shouldHaveJSX = process.env.CHILD_NODE_ENV === "production";
-
- if (shouldHaveJSXDev) {
- if (!code.includes("jsx_dev_runtime.jsxDEV")) {
- throw new Error("jsxDEV is not included");
- }
- }
-
- if (shouldHaveJSX) {
- if (!code.includes("jsx_runtime.jsx")) {
- throw new Error("Jsx is not included");
- }
- }
-
- const url = URL.createObjectURL(self.outputs[0]);
- await import(url);
-}