refactor: move jsxSideEffects from tsconfig to jsx build config (#22665)

## Summary
- Moved `jsxSideEffects` (now `sideEffects`) from tsconfig.json compiler
options to the jsx object in the build API
- Updated all jsx bundler tests to use the new jsx.sideEffects
configuration
- Added jsx configuration parsing to JSBundler.zig

## Changes
- Removed jsxSideEffects parsing from `src/resolver/tsconfig_json.zig`
- Added jsx configuration parsing to `src/bun.js/api/JSBundler.zig`
Config.fromJS
- Fixed TransformOptions to properly pass jsx config to the transpiler
in `src/bundler/bundle_v2.zig`
- Updated TypeScript definitions to include jsx field in BuildConfigBase
- Modified test framework to support jsx configuration in API mode
- Updated all jsx tests to use `sideEffects` in the jsx config instead
of `side_effects` in tsconfig

## Test plan
All 27 jsx bundler tests are passing with the new configuration
structure.

🤖 Generated with [Claude Code](https://claude.ai/code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
robobun
2025-09-20 05:50:30 -07:00
committed by GitHub
parent 7c0574eeb4
commit 71a8900013
7 changed files with 149 additions and 33 deletions

View File

@@ -1899,6 +1899,18 @@ declare module "bun" {
*/
tsconfig?: string;
/**
* JSX configuration options
*/
jsx?: {
runtime?: "automatic" | "classic";
importSource?: string;
factory?: string;
fragment?: string;
sideEffects?: boolean;
development?: boolean;
};
outdir?: string;
}

View File

@@ -13,7 +13,13 @@ pub const JSBundler = struct {
outdir: OwnedString = OwnedString.initEmpty(bun.default_allocator),
rootdir: OwnedString = OwnedString.initEmpty(bun.default_allocator),
serve: Serve = .{},
jsx: options.JSX.Pragma = .{},
jsx: api.Jsx = .{
.factory = "",
.fragment = "",
.runtime = .automatic,
.import_source = "",
.development = true, // Default to development mode like old Pragma
},
force_node_env: options.BundleOptions.ForceNodeEnv = .unspecified,
code_splitting: bool = false,
minify: Minify = .{},
@@ -381,6 +387,51 @@ pub const JSBundler = struct {
this.packages = packages;
}
// Parse JSX configuration
if (try config.getTruthy(globalThis, "jsx")) |jsx_value| {
if (!jsx_value.isObject()) {
return globalThis.throwInvalidArguments("jsx must be an object", .{});
}
if (try jsx_value.getOptional(globalThis, "runtime", ZigString.Slice)) |slice| {
defer slice.deinit();
var str_lower: [128]u8 = undefined;
const len = @min(slice.len, str_lower.len);
_ = strings.copyLowercase(slice.slice()[0..len], str_lower[0..len]);
if (options.JSX.RuntimeMap.get(str_lower[0..len])) |runtime| {
this.jsx.runtime = runtime.runtime;
if (runtime.development) |dev| {
this.jsx.development = dev;
}
} else {
return globalThis.throwInvalidArguments("Invalid jsx.runtime: '{s}'. Must be one of: 'classic', 'automatic', 'react', 'react-jsx', or 'react-jsxdev'", .{slice.slice()});
}
}
if (try jsx_value.getOptional(globalThis, "factory", ZigString.Slice)) |slice| {
defer slice.deinit();
this.jsx.factory = try allocator.dupe(u8, slice.slice());
}
if (try jsx_value.getOptional(globalThis, "fragment", ZigString.Slice)) |slice| {
defer slice.deinit();
this.jsx.fragment = try allocator.dupe(u8, slice.slice());
}
if (try jsx_value.getOptional(globalThis, "importSource", ZigString.Slice)) |slice| {
defer slice.deinit();
this.jsx.import_source = try allocator.dupe(u8, slice.slice());
}
if (try jsx_value.getBooleanLoose(globalThis, "development")) |dev| {
this.jsx.development = dev;
}
if (try jsx_value.getBooleanLoose(globalThis, "sideEffects")) |val| {
this.jsx.side_effects = val;
}
}
if (try config.getOptionalEnum(globalThis, "format", options.Format)) |format| {
this.format = format;
@@ -718,6 +769,19 @@ pub const JSBundler = struct {
bun.default_allocator.free(loaders.loaders);
bun.default_allocator.free(loaders.extensions);
}
// Free JSX allocated strings
if (self.jsx.factory.len > 0) {
allocator.free(self.jsx.factory);
self.jsx.factory = "";
}
if (self.jsx.fragment.len > 0) {
allocator.free(self.jsx.fragment);
self.jsx.fragment = "";
}
if (self.jsx.import_source.len > 0) {
allocator.free(self.jsx.import_source);
self.jsx.import_source = "";
}
self.names.deinit();
self.outdir.deinit();
self.rootdir.deinit();

View File

@@ -1801,6 +1801,9 @@ pub const BundleV2 = struct {
) !void {
const config = &completion.config;
// JSX config is already in API format
const jsx_api = config.jsx;
transpiler.* = try bun.Transpiler.init(
alloc,
&completion.log,
@@ -1821,6 +1824,7 @@ pub const BundleV2 = struct {
.ignore_dce_annotations = transpiler.options.ignore_dce_annotations,
.drop = config.drop.map.keys(),
.bunfig_path = transpiler.options.bunfig_path,
.jsx = jsx_api,
},
completion.env,
);
@@ -1831,7 +1835,33 @@ pub const BundleV2 = struct {
}
transpiler.options.entry_points = config.entry_points.keys();
transpiler.options.jsx = config.jsx;
// Convert API JSX config back to options.JSX.Pragma
transpiler.options.jsx = options.JSX.Pragma{
.factory = if (config.jsx.factory.len > 0)
try options.JSX.Pragma.memberListToComponentsIfDifferent(alloc, &.{}, config.jsx.factory)
else
options.JSX.Pragma.Defaults.Factory,
.fragment = if (config.jsx.fragment.len > 0)
try options.JSX.Pragma.memberListToComponentsIfDifferent(alloc, &.{}, config.jsx.fragment)
else
options.JSX.Pragma.Defaults.Fragment,
.runtime = config.jsx.runtime,
.development = config.jsx.development,
.package_name = if (config.jsx.import_source.len > 0) config.jsx.import_source else "react",
.classic_import_source = if (config.jsx.import_source.len > 0) config.jsx.import_source else "react",
.side_effects = config.jsx.side_effects,
.parse = true,
.import_source = .{
.development = if (config.jsx.import_source.len > 0)
try std.fmt.allocPrint(alloc, "{s}/jsx-dev-runtime", .{config.jsx.import_source})
else
"react/jsx-dev-runtime",
.production = if (config.jsx.import_source.len > 0)
try std.fmt.allocPrint(alloc, "{s}/jsx-runtime", .{config.jsx.import_source})
else
"react/jsx-runtime",
},
};
transpiler.options.no_macros = config.no_macros;
transpiler.options.loaders = try options.loadersFromTransformOptions(alloc, config.loaders, config.target);
transpiler.options.entry_naming = config.names.entry_point.data;

View File

@@ -1220,7 +1220,6 @@ pub const JSX = struct {
.{ "react", RuntimeDevelopmentPair{ .runtime = .classic, .development = null } },
.{ "react-jsx", RuntimeDevelopmentPair{ .runtime = .automatic, .development = true } },
.{ "react-jsxdev", RuntimeDevelopmentPair{ .runtime = .automatic, .development = true } },
.{ "solid", RuntimeDevelopmentPair{ .runtime = .solid, .development = null } },
});
pub const Pragma = struct {

View File

@@ -82,10 +82,6 @@ pub const TSConfigJSON = struct {
out.development = this.jsx.development;
}
if (this.jsx_flags.contains(.side_effects)) {
out.side_effects = this.jsx.side_effects;
}
return out;
}
@@ -229,13 +225,6 @@ pub const TSConfigJSON = struct {
result.jsx_flags.insert(.import_source);
}
}
// Parse "jsxSideEffects"
if (compiler_opts.expr.asProperty("jsxSideEffects")) |jsx_prop| {
if (jsx_prop.expr.asBool()) |val| {
result.jsx.side_effects = val;
result.jsx_flags.insert(.side_effects);
}
}
// Parse "useDefineForClassFields"
if (compiler_opts.expr.asProperty("useDefineForClassFields")) |use_define_value_prop| {

View File

@@ -449,11 +449,11 @@ describe("bundler", () => {
runtime: "classic",
factory: "React.createElement",
fragment: "React.Fragment",
side_effects: true,
sideEffects: true,
},
onAfterBundle(api) {
const file = api.readFile("out.js");
// When jsxSideEffects is true: should NOT include /* @__PURE__ */ comments
// When sideEffects is true: should NOT include /* @__PURE__ */ comments
expect(file).not.toContain("/* @__PURE__ */");
expect(file).toContain("React.createElement");
expect(normalizeBunSnapshot(file)).toMatchInlineSnapshot(`
@@ -510,11 +510,11 @@ describe("bundler", () => {
target: "bun",
jsx: {
runtime: "automatic",
side_effects: true,
sideEffects: true,
},
onAfterBundle(api) {
const file = api.readFile("out.js");
// When jsxSideEffects is true: should NOT include /* @__PURE__ */ comments
// When sideEffects is true: should NOT include /* @__PURE__ */ comments
expect(file).not.toContain("/* @__PURE__ */");
expect(normalizeBunSnapshot(file)).toMatchInlineSnapshot(`
"// @bun
@@ -573,18 +573,19 @@ describe("bundler", () => {
...helpers,
},
target: "bun",
backend: "api",
jsx: {
runtime: "classic",
factory: "React.createElement",
fragment: "React.Fragment",
side_effects: true,
sideEffects: true,
},
env: {
NODE_ENV: "production",
},
onAfterBundle(api) {
const file = api.readFile("out.js");
// When jsxSideEffects is true in production: should NOT include /* @__PURE__ */ comments
// When sideEffects is true in production: should NOT include /* @__PURE__ */ comments
expect(file).not.toContain("/* @__PURE__ */");
expect(file).toContain("React.createElement");
expect(normalizeBunSnapshot(file)).toMatchInlineSnapshot(`
@@ -639,16 +640,18 @@ describe("bundler", () => {
...helpers,
},
target: "bun",
backend: "api",
jsx: {
runtime: "automatic",
side_effects: true,
sideEffects: true,
development: false,
},
env: {
NODE_ENV: "production",
},
onAfterBundle(api) {
const file = api.readFile("out.js");
// When jsxSideEffects is true in production: should NOT include /* @__PURE__ */ comments
// When sideEffects is true in production: should NOT include /* @__PURE__ */ comments
expect(file).not.toContain("/* @__PURE__ */");
expect(normalizeBunSnapshot(file)).toMatchInlineSnapshot(`
"// @bun
@@ -709,13 +712,16 @@ describe("bundler", () => {
itBundled("jsx/sideEffectsTrueTsconfig", {
files: {
"/index.jsx": /* jsx */ `console.log(<a></a>); console.log(<></>);`,
"/tsconfig.json": /* json */ `{"compilerOptions": {"jsxSideEffects": true}}`,
"/tsconfig.json": /* json */ `{"compilerOptions": {}}`,
...helpers,
},
jsx: {
sideEffects: true,
},
target: "bun",
onAfterBundle(api) {
const file = api.readFile("out.js");
// When jsxSideEffects is true via tsconfig: should NOT include /* @__PURE__ */ comments
// When sideEffects is true via tsconfig: should NOT include /* @__PURE__ */ comments
expect(file).not.toContain("/* @__PURE__ */");
expect(normalizeBunSnapshot(file)).toMatchInlineSnapshot(`
"// @bun
@@ -743,13 +749,19 @@ describe("bundler", () => {
itBundled("jsx/sideEffectsTrueTsconfigClassic", {
files: {
"/index.jsx": /* jsx */ `console.log(<a></a>); console.log(<></>);`,
"/tsconfig.json": /* json */ `{"compilerOptions": {"jsx": "react", "jsxSideEffects": true}}`,
"/tsconfig.json": /* json */ `{"compilerOptions": {"jsx": "react"}}`,
...helpers,
},
jsx: {
runtime: "classic",
factory: "React.createElement",
fragment: "React.Fragment",
sideEffects: true,
},
target: "bun",
onAfterBundle(api) {
const file = api.readFile("out.js");
// When jsxSideEffects is true via tsconfig with classic jsx: should NOT include /* @__PURE__ */ comments
// When sideEffects is true via tsconfig with classic jsx: should NOT include /* @__PURE__ */ comments
expect(file).not.toContain("/* @__PURE__ */");
expect(file).toContain("React.createElement");
expect(normalizeBunSnapshot(file)).toMatchInlineSnapshot(`
@@ -764,13 +776,17 @@ describe("bundler", () => {
itBundled("jsx/sideEffectsTrueTsconfigAutomatic", {
files: {
"/index.jsx": /* jsx */ `console.log(<a></a>); console.log(<></>);`,
"/tsconfig.json": /* json */ `{"compilerOptions": {"jsx": "react-jsx", "jsxSideEffects": true}}`,
"/tsconfig.json": /* json */ `{"compilerOptions": {"jsx": "react-jsx"}}`,
...helpers,
},
jsx: {
runtime: "automatic",
sideEffects: true,
},
target: "bun",
onAfterBundle(api) {
const file = api.readFile("out.js");
// When jsxSideEffects is true via tsconfig with automatic jsx: should NOT include /* @__PURE__ */ comments
// When sideEffects is true via tsconfig with automatic jsx: should NOT include /* @__PURE__ */ comments
expect(file).not.toContain("/* @__PURE__ */");
expect(normalizeBunSnapshot(file)).toMatchInlineSnapshot(`
"// @bun

View File

@@ -186,6 +186,8 @@ export interface BundlerTestInput {
importSource?: string; // for automatic
factory?: string; // for classic
fragment?: string; // for classic
sideEffects?: boolean; // whether jsx has side effects
development?: boolean; // whether to use development runtime
};
root?: string;
/** Defaults to `/out.js` */
@@ -574,10 +576,6 @@ function expectBundled(
if (!backend) {
backend =
dotenv ||
jsx.factory ||
jsx.fragment ||
jsx.runtime ||
jsx.importSource ||
typeof production !== "undefined" ||
bundling === false ||
(run && target === "node") ||
@@ -773,7 +771,7 @@ function expectBundled(
// jsx.preserve && "--jsx=preserve",
jsx.factory && `--jsx-factory=${jsx.factory}`,
jsx.fragment && `--jsx-fragment=${jsx.fragment}`,
jsx.side_effects && `--jsx-side-effects`,
jsx.sideEffects && `--jsx-side-effects`,
env?.NODE_ENV !== "production" && `--jsx-dev`,
entryNaming &&
entryNaming !== "[dir]/[name].[ext]" &&
@@ -1089,6 +1087,14 @@ function expectBundled(
define: define ?? {},
throw: _throw ?? false,
compile,
jsx: jsx ? {
runtime: jsx.runtime,
importSource: jsx.importSource,
factory: jsx.factory,
fragment: jsx.fragment,
sideEffects: jsx.sideEffects,
development: jsx.development,
} : undefined,
} as BuildConfig;
if (dotenv) {