Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
b72329bfef fix(bundler): default NODE_ENV to production for --compile builds
When using `bun build --compile`, default `process.env.NODE_ENV` to
`"production"` to enable dead code elimination for conditional requires
like React's:

```javascript
if (process.env.NODE_ENV === 'production') {
  module.exports = require('./cjs/react.production.js');
} else {
  module.exports = require('./cjs/react.development.js');
}
```

Without this fix, the bundler cannot evaluate the condition as a constant
and tries to resolve both branches, causing failures when development
files don't exist (e.g., in Next.js standalone output).

This also sets `jsx.development = false` for --compile builds, matching
the behavior of --production.

Users can still override this with:
- CLI: `--define 'process.env.NODE_ENV="development"'`
- API: `define: { 'process.env.NODE_ENV': '"development"' }`

Fixes #26244

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 08:15:05 +00:00
3 changed files with 180 additions and 3 deletions

View File

@@ -1988,6 +1988,10 @@ pub const BundleV2 = struct {
if (transpiler.options.compile) {
// Emitting DCE annotations is nonsensical in --compile.
transpiler.options.emit_dce_annotations = false;
// Default to production mode for --compile builds to enable dead code elimination
// for conditional requires like React's process.env.NODE_ENV checks.
// Users can override this with define: { 'process.env.NODE_ENV': '"development"' }
try transpiler.env.map.put("NODE_ENV", "production");
}
transpiler.configureLinker();
@@ -1996,6 +2000,12 @@ pub const BundleV2 = struct {
if (!transpiler.options.production) {
try transpiler.options.conditions.appendSlice(&.{"development"});
}
// Allow tsconfig.json overriding, but always set it to false for --compile builds.
if (transpiler.options.compile) {
transpiler.options.jsx.development = false;
}
transpiler.resolver.env_loader = transpiler.env;
transpiler.resolver.opts = transpiler.options;
}

View File

@@ -196,7 +196,10 @@ pub const BuildCommand = struct {
this_transpiler.options.env.behavior = ctx.bundler_options.env_behavior;
this_transpiler.options.env.prefix = ctx.bundler_options.env_prefix;
if (ctx.bundler_options.production) {
// Default to production mode for --compile builds to enable dead code elimination
// for conditional requires like React's process.env.NODE_ENV checks.
// Users can override this with --define 'process.env.NODE_ENV="development"'
if (ctx.bundler_options.production or ctx.bundler_options.compile) {
try this_transpiler.env.map.put("NODE_ENV", "production");
}
@@ -210,8 +213,8 @@ pub const BuildCommand = struct {
this_transpiler.resolver.opts = this_transpiler.options;
this_transpiler.resolver.env_loader = this_transpiler.env;
// Allow tsconfig.json overriding, but always set it to false if --production is passed.
if (ctx.bundler_options.production) {
// Allow tsconfig.json overriding, but always set it to false if --production or --compile is passed.
if (ctx.bundler_options.production or ctx.bundler_options.compile) {
this_transpiler.options.jsx.development = false;
this_transpiler.resolver.opts.jsx.development = false;
}

View File

@@ -0,0 +1,164 @@
import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe, isWindows, tempDir } from "harness";
import { join } from "path";
// https://github.com/oven-sh/bun/issues/26244
// bun build --compile should default NODE_ENV to production for dead code elimination
describe("Issue #26244", () => {
test("--compile defaults NODE_ENV to production (CLI)", async () => {
using dir = tempDir("compile-node-env-cli", {
// This simulates React's conditional require pattern
"index.js": `
if (process.env.NODE_ENV === 'production') {
module.exports = require('./prod.js');
} else {
module.exports = require('./dev.js');
}
`,
"prod.js": `module.exports = { mode: "production" };`,
// Note: dev.js intentionally not created to simulate Next.js standalone output
// where development files are stripped
});
const outfile = join(dir + "", isWindows ? "app.exe" : "app");
// This should succeed because NODE_ENV defaults to production,
// enabling dead code elimination of the dev.js branch
const buildProc = Bun.spawn({
cmd: [bunExe(), "build", "--compile", join(dir + "", "index.js"), "--outfile", outfile],
cwd: dir + "",
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [buildStdout, buildStderr, buildExitCode] = await Promise.all([
new Response(buildProc.stdout).text(),
new Response(buildProc.stderr).text(),
buildProc.exited,
]);
// Build should succeed - the dead branch with dev.js should be eliminated
expect(buildStderr).not.toContain("Could not resolve");
expect(buildExitCode).toBe(0);
});
test("--compile defaults NODE_ENV to production (API)", async () => {
using dir = tempDir("compile-node-env-api", {
// This simulates React's conditional require pattern
"index.js": `
if (process.env.NODE_ENV === 'production') {
module.exports = require('./prod.js');
} else {
module.exports = require('./dev.js');
}
`,
"prod.js": `module.exports = { mode: "production" };`,
// Note: dev.js intentionally not created to simulate Next.js standalone output
// where development files are stripped
});
const outfile = join(dir + "", isWindows ? "app.exe" : "app");
// This should succeed because NODE_ENV defaults to production,
// enabling dead code elimination of the dev.js branch
const result = await Bun.build({
entrypoints: [join(dir + "", "index.js")],
compile: {
outfile,
},
});
// Build should succeed - the dead branch with dev.js should be eliminated
expect(result.success).toBe(true);
expect(result.outputs.length).toBe(1);
});
test("--compile with conditional require eliminates dead branch (CLI)", async () => {
using dir = tempDir("compile-dead-code-cli", {
"entry.js": `
// This is the pattern used by React
if (process.env.NODE_ENV === 'production') {
console.log("Using production build");
} else {
// This branch references a non-existent file
// and should be eliminated by dead code elimination
require('./non-existent-dev-file.js');
}
`,
});
const outfile = join(dir + "", isWindows ? "app.exe" : "app");
// Should succeed - the require('./non-existent-dev-file.js') should be
// eliminated because NODE_ENV defaults to 'production'
const buildProc = Bun.spawn({
cmd: [bunExe(), "build", "--compile", join(dir + "", "entry.js"), "--outfile", outfile],
cwd: dir + "",
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [buildStdout, buildStderr, buildExitCode] = await Promise.all([
new Response(buildProc.stdout).text(),
new Response(buildProc.stderr).text(),
buildProc.exited,
]);
expect(buildStderr).not.toContain("Could not resolve");
expect(buildExitCode).toBe(0);
});
test("--compile can override NODE_ENV with --define", async () => {
using dir = tempDir("compile-define-override", {
"entry.js": `console.log(process.env.NODE_ENV);`,
});
const outfile = join(dir + "", isWindows ? "app.exe" : "app");
// Use CLI to test --define override
const buildProc = Bun.spawn({
cmd: [
bunExe(),
"build",
"--compile",
join(dir + "", "entry.js"),
"--outfile",
outfile,
"--define",
'process.env.NODE_ENV="development"',
],
cwd: dir + "",
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [buildStdout, buildStderr, buildExitCode] = await Promise.all([
new Response(buildProc.stdout).text(),
new Response(buildProc.stderr).text(),
buildProc.exited,
]);
expect(buildExitCode).toBe(0);
// Run the compiled binary
const runProc = Bun.spawn({
cmd: [outfile],
cwd: dir + "",
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(runProc.stdout).text(),
new Response(runProc.stderr).text(),
runProc.exited,
]);
expect(stdout.trim()).toBe("development");
expect(exitCode).toBe(0);
});
});