Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix loading react-jsxdev instead of react-jsx #17013

Merged
merged 6 commits into from
Feb 3, 2025
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/bun.js/api/JSBundler.zig
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ pub const JSBundler = struct {
rootdir: OwnedString = OwnedString.initEmpty(bun.default_allocator),
serve: Serve = .{},
jsx: options.JSX.Pragma = .{},
force_development: bool = false,
code_splitting: bool = false,
minify: Minify = .{},
no_macros: bool = false,
Expand Down
4 changes: 4 additions & 0 deletions src/bun.js/api/server/HTMLBundle.zig
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,10 @@ pub const HTMLBundleRoute = struct {

if (!server.config().development) {
config.define.put("process.env.NODE_ENV", "\"production\"") catch bun.outOfMemory();
config.jsx.development = false;
} else {
config.force_development = true;
config.jsx.development = true;
}

config.source_map = .linked;
Expand Down
14 changes: 12 additions & 2 deletions src/bundler/bundle_v2.zig
Original file line number Diff line number Diff line change
Expand Up @@ -913,7 +913,10 @@ pub const BundleV2 = struct {
task.tree_shaking = this.linker.options.tree_shaking;
task.is_entry_point = is_entry_point;
task.known_target = target;
task.jsx.development = this.bundlerForTarget(target).options.jsx.development;
{
const bundler = this.bundlerForTarget(target);
task.jsx.development = bundler.options.force_development orelse bundler.options.jsx.development;
}

// Handle onLoad plugins as entry points
if (!this.enqueueOnLoadPluginIfNeeded(task)) {
Expand Down Expand Up @@ -1753,6 +1756,9 @@ pub const BundleV2 = struct {
);
transpiler.options.env.behavior = config.env_behavior;
transpiler.options.env.prefix = config.env_prefix.slice();
if (config.force_development) {
transpiler.options.force_development = true;
}

transpiler.options.entry_points = config.entry_points.keys();
transpiler.options.jsx = config.jsx;
Expand Down Expand Up @@ -2875,7 +2881,11 @@ pub const BundleV2 = struct {
resolve_task.secondary_path_for_commonjs_interop = secondary_path_to_copy;
resolve_task.known_target = target;
resolve_task.jsx = resolve_result.jsx;
resolve_task.jsx.development = this.bundlerForTarget(target).options.jsx.development;

{
const bundler = this.bundlerForTarget(target);
resolve_task.jsx.development = bundler.options.force_development orelse bundler.options.jsx.development;
}

// Figure out the loader.
{
Expand Down
5 changes: 4 additions & 1 deletion src/js_parser.zig
Original file line number Diff line number Diff line change
Expand Up @@ -6812,7 +6812,10 @@ fn NewParser_(

if (p.lexer.jsx_pragma.jsxRuntime()) |runtime| {
if (options.JSX.RuntimeMap.get(runtime.text)) |jsx_runtime| {
p.options.jsx.runtime = jsx_runtime;
p.options.jsx.runtime = jsx_runtime.runtime;
if (jsx_runtime.development) |dev| {
p.options.jsx.development = dev;
}
} else {
// make this a warning instead of an error because we don't support "preserve" right now
try p.log.addRangeWarningFmt(p.source, runtime.range, p.allocator, "Unsupported JSX runtime: \"{s}\"", .{runtime.text});
Expand Down
27 changes: 18 additions & 9 deletions src/options.zig
Original file line number Diff line number Diff line change
Expand Up @@ -991,13 +991,18 @@ pub const ESMConditions = struct {
};

pub const JSX = struct {
pub const RuntimeMap = bun.ComptimeStringMap(JSX.Runtime, .{
.{ "classic", .classic },
.{ "automatic", .automatic },
.{ "react", .classic },
.{ "react-jsx", .automatic },
.{ "react-jsxdev", .automatic },
.{ "solid", .solid },
const RuntimeDevelopmentPair = struct {
runtime: JSX.Runtime,
development: ?bool,
};

pub const RuntimeMap = bun.ComptimeStringMap(RuntimeDevelopmentPair, .{
.{ "classic", RuntimeDevelopmentPair{ .runtime = .classic, .development = null } },
.{ "automatic", RuntimeDevelopmentPair{ .runtime = .automatic, .development = true } },
.{ "react", RuntimeDevelopmentPair{ .runtime = .classic, .development = null } },
.{ "react-jsx", RuntimeDevelopmentPair{ .runtime = .automatic, .development = true } },
paperclover marked this conversation as resolved.
Show resolved Hide resolved
.{ "react-jsxdev", RuntimeDevelopmentPair{ .runtime = .automatic, .development = true } },
.{ "solid", RuntimeDevelopmentPair{ .runtime = .solid, .development = null } },
});

pub const Pragma = struct {
Expand Down Expand Up @@ -1575,13 +1580,17 @@ pub const BundleOptions = struct {

supports_multiple_outputs: bool = true,

force_development: ?bool = null,
paperclover marked this conversation as resolved.
Show resolved Hide resolved

pub fn isTest(this: *const BundleOptions) bool {
return this.rewrite_jest_for_tests;
}

pub fn setProduction(this: *BundleOptions, value: bool) void {
this.production = value;
this.jsx.development = !value;
if (this.force_development == null) {
this.production = value;
this.jsx.development = !value;
}
}

pub const default_unwrap_commonjs_packages = [_]string{
Expand Down
10 changes: 6 additions & 4 deletions src/resolver/tsconfig_json.zig
Original file line number Diff line number Diff line change
Expand Up @@ -216,12 +216,14 @@ pub const TSConfigJSON = struct {
defer allocator.free(str_lower);
_ = strings.copyLowercase(str, str_lower);
// - We don't support "preserve" yet
// - We rely on NODE_ENV for "jsx" or "jsxDEV"
// - We treat "react-jsx" and "react-jsxDEV" identically
// because it is too easy to auto-import the wrong one.
if (options.JSX.RuntimeMap.get(str_lower)) |runtime| {
result.jsx.runtime = runtime;
result.jsx.runtime = runtime.runtime;
result.jsx_flags.insert(.runtime);

if (runtime.development) |dev| {
result.jsx.development = dev;
result.jsx_flags.insert(.development);
}
}
}
}
Expand Down
23 changes: 20 additions & 3 deletions src/transpiler.zig
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,7 @@ pub const Transpiler = struct {
const has_production_env = this.env.isProduction();
if (!was_production and has_production_env) {
this.options.setProduction(true);
this.resolver.opts.setProduction(true);
}

if (this.options.isTest() or this.env.isTest()) {
Expand All @@ -567,6 +568,7 @@ pub const Transpiler = struct {
this.env.loadProcess();
if (this.env.isProduction()) {
this.options.setProduction(true);
this.resolver.opts.setProduction(true);
}
},
else => {},
Expand All @@ -590,7 +592,7 @@ pub const Transpiler = struct {

try this.runEnvLoader(false);

this.options.jsx.setProduction(this.env.isProduction());
var is_production = this.env.isProduction();

js_ast.Expr.Data.Store.create();
js_ast.Stmt.Data.Store.create();
Expand All @@ -600,11 +602,26 @@ pub const Transpiler = struct {

try this.options.loadDefines(this.allocator, this.env, &this.options.env);

var is_development = false;
if (this.options.define.dots.get("NODE_ENV")) |NODE_ENV| {
if (NODE_ENV.len > 0 and NODE_ENV[0].data.value == .e_string and NODE_ENV[0].data.value.e_string.eqlComptime("production")) {
this.options.production = true;
if (NODE_ENV.len > 0 and NODE_ENV[0].data.value == .e_string) {
if (NODE_ENV[0].data.value.e_string.eqlComptime("production")) {
is_production = true;
} else if (NODE_ENV[0].data.value.e_string.eqlComptime("development")) {
is_development = true;
}
}
}

if (is_development) {
this.options.setProduction(false);
this.resolver.opts.setProduction(false);
this.options.force_development = true;
this.resolver.opts.force_development = true;
} else if (is_production) {
this.options.setProduction(true);
this.resolver.opts.setProduction(true);
}
}

pub fn resetStore(_: *const Transpiler) void {
Expand Down
37 changes: 37 additions & 0 deletions test/bundler/transpiler/jsx-dev/jsx-dev.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { renderToReadableStream } from "react-dom/server.browser";

const HelloWorld = () => {
return <div>Hello World</div>;
};

const stream = new Response(await renderToReadableStream(<HelloWorld />));

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);
}
6 changes: 6 additions & 0 deletions test/bundler/transpiler/jsx-dev/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"jsx": "react-jsxdev"
}
}
1 change: 1 addition & 0 deletions test/bundler/transpiler/jsx-production-entry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import "./jsx-production";
36 changes: 36 additions & 0 deletions test/bundler/transpiler/jsx-production.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { describe, expect, test, afterAll } from "bun:test";
import path from "path";
import { bunExe, bunEnv } from "harness";

const original_node_env = bunEnv.NODE_ENV;

// https://github.com/oven-sh/bun/issues/3768
describe("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 () => {
bunEnv.NODE_ENV = node_env;
bunEnv.CHILD_NODE_ENV = child_node_env;
bunEnv.TSCONFIG_JSX = "react-jsxdev";
expect([path.join(import.meta.dirname, "jsx-dev", "jsx-dev.tsx")]).toRun(
"<div>Hello World</div>" + "\n" + "<div>Hello World</div>" + "\n",
);
});

test(`react-jsx parent: ${node_env} child: ${child_node_env} should work`, async () => {
bunEnv.NODE_ENV = node_env;
bunEnv.CHILD_NODE_ENV = child_node_env;
bunEnv.TSCONFIG_JSX = "react-jsx";
expect([path.join(import.meta.dirname, "jsx-production-entry.ts")]).toRun(
"<div>Hello World</div>" + "\n" + "<div>Hello World</div>" + "\n",
);
});
}
}

afterAll(() => {
bunEnv.NODE_ENV = original_node_env;
delete bunEnv.CHILD_NODE_ENV;
delete bunEnv.TSCONFIG_JSX;
});
});
37 changes: 37 additions & 0 deletions test/bundler/transpiler/jsx-production.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { renderToReadableStream } from "react-dom/server.browser";

const HelloWorld = () => {
return <div>Hello World</div>;
};

const stream = new Response(await renderToReadableStream(<HelloWorld />));

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);
}
6 changes: 6 additions & 0 deletions test/bundler/transpiler/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"jsx": "react-jsx"
}
}
53 changes: 53 additions & 0 deletions test/js/bun/http/bun-serve-html.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -716,3 +716,56 @@ test("wildcard static routes", async () => {
}
}
});

test("serve html with JSX runtime in development mode", async () => {
const dir = join(import.meta.dir, "jsx-runtime");
const { default: html } = await import(join(dir, "index.html"));

using server = Bun.serve({
port: 0,
development: true,
static: {
"/": html,
},
fetch(req) {
return new Response("Not found", { status: 404 });
},
});

const response = await fetch(server.url);
expect(response.status).toBe(200);
const htmlText = await response.text();
const jsSrc = htmlText.match(/<script type="module" crossorigin src="([^"]+)"/)?.[1]!;
const js = await (await fetch(new URL(jsSrc, server.url))).text();

// Development mode should use jsxDEV
expect(js).toContain("jsx_dev_runtime.jsxDEV");
expect(js).not.toContain("jsx_runtime.jsx");
});

test("serve html with JSX runtime in production mode", async () => {
const dir = join(import.meta.dir, "jsx-runtime");
const { default: html } = await import(join(dir, "index.html"));

using server = Bun.serve({
port: 0,
development: false,
static: {
"/": html,
},
fetch(req) {
return new Response("Not found", { status: 404 });
},
});

const response = await fetch(server.url);
expect(response.status).toBe(200);
const htmlText = await response.text();
const jsSrc = htmlText.match(/<script type="module" crossorigin src="([^"]+)"/)?.[1]!;
const js = await (await fetch(new URL(jsSrc, server.url))).text();
// jsxDEV looks like this:
// jsxDEV("button", {
// children: "Click me"
// }, undefined, false, undefined, this)
expect(js).toContain(`("h1",{children:"Hello from JSX"})`);
});
13 changes: 13 additions & 0 deletions test/js/bun/http/jsx-runtime/app.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from "react";
import { createRoot } from "react-dom/client";

const App = () => {
return (
<div>
<h1>Hello from JSX</h1>
<button>Click me</button>
</div>
);
};

createRoot(document.getElementById("root")).render(<App />);
10 changes: 10 additions & 0 deletions test/js/bun/http/jsx-runtime/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<!doctype html>
<html>
<head>
<title>JSX Runtime Test</title>
<script type="module" src="app.jsx"></script>
</head>
<body>
<div id="root"></div>
</body>
</html>