Files
bun.sh/test/bundler/bundler_feature_flag.test.ts
robobun c59a6997cd feat(bundler): add statically-analyzable dead-code elimination via feature flags (#25462)
## Summary
- Adds `import { feature } from "bun:bundle"` for compile-time feature
flag checking
- `feature("FLAG_NAME")` calls are replaced with `true`/`false` at
bundle time
- Enables dead-code elimination through `--feature=FLAG_NAME` CLI
argument
- Works in `bun build`, `bun run`, and `bun test`
- Available in both CLI and `Bun.build()` JavaScript API

## Usage

```ts
import { feature } from "bun:bundle";

if (feature("SUPER_SECRET")) {
  console.log("Secret feature enabled!");
} else {
  console.log("Normal mode");
}
```

### CLI
```bash
# Enable feature during build
bun build --feature=SUPER_SECRET index.ts

# Enable at runtime
bun run --feature=SUPER_SECRET index.ts

# Enable in tests
bun test --feature=SUPER_SECRET
```

### JavaScript API
```ts
await Bun.build({
  entrypoints: ['./index.ts'],
  outdir: './out',
  features: ['SUPER_SECRET', 'ANOTHER_FLAG'],
});
```

## Implementation
- Added `bundler_feature_flags` (as `*const bun.StringSet`) to
`RuntimeFeatures` and `BundleOptions`
- Added `bundler_feature_flag_ref` to Parser struct to track the
`feature` import
- Handle `bun:bundle` import at parse time (similar to macros) - capture
ref, return empty statement
- Handle `feature()` calls in `e_call` visitor - replace with boolean
based on flags
- Wire feature flags through CLI arguments and `Bun.build()` API to
bundler options
- Added `features` option to `JSBundler.zig` for JavaScript API support
- Added TypeScript types in `bun.d.ts`
- Added documentation to `docs/bundler/index.mdx`

## Test plan
- [x] Basic feature flag enabled/disabled tests (both CLI and API
backends)
- [x] Multiple feature flags test
- [x] Dead code elimination verification tests
- [x] Error handling for invalid arguments
- [x] Runtime tests with `bun run --feature=FLAG`
- [x] Test runner tests with `bun test --feature=FLAG`
- [x] Aliased import tests (`import { feature as checkFeature }`)
- [x] Ternary operator DCE tests
- [x] Tests use `itBundled` with both `backend: "cli"` and `backend:
"api"`

🤖 Generated with [Claude Code](https://claude.com/claude-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>
Co-authored-by: Alistair Smith <hi@alistair.sh>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
2025-12-11 17:44:14 -08:00

536 lines
14 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
import { itBundled } from "./expectBundled";
describe("bundler feature flags", () => {
// Test both CLI and API backends
for (const backend of ["cli", "api"] as const) {
describe(`backend: ${backend}`, () => {
itBundled(`feature_flag/${backend}/FeatureReturnsTrue`, {
backend,
files: {
"/a.js": `
import { feature } from "bun:bundle";
if (feature("SUPER_SECRET")) {
console.log("feature enabled");
} else {
console.log("feature disabled");
}
`,
},
features: ["SUPER_SECRET"],
onAfterBundle(api) {
// The output should contain `true` since the feature is enabled
api.expectFile("out.js").toInclude("true");
api.expectFile("out.js").not.toInclude("feature(");
api.expectFile("out.js").not.toInclude("bun:bundle");
},
});
itBundled(`feature_flag/${backend}/FeatureReturnsFalse`, {
backend,
files: {
"/a.js": `
import { feature } from "bun:bundle";
if (feature("SUPER_SECRET")) {
console.log("feature enabled");
} else {
console.log("feature disabled");
}
`,
},
// No features enabled
onAfterBundle(api) {
// The output should contain `false` since the feature is not enabled
api.expectFile("out.js").toInclude("false");
api.expectFile("out.js").not.toInclude("feature(");
api.expectFile("out.js").not.toInclude("bun:bundle");
},
});
itBundled(`feature_flag/${backend}/MultipleFlags`, {
backend,
files: {
"/a.js": `
import { feature } from "bun:bundle";
if (feature("FLAG_A")) console.log("FLAG_A");
if (feature("FLAG_B")) console.log("FLAG_B");
if (feature("FLAG_C")) console.log("FLAG_C");
`,
},
features: ["FLAG_A", "FLAG_C"],
onAfterBundle(api) {
// FLAG_A and FLAG_C are enabled, FLAG_B is not
api.expectFile("out.js").toInclude("true");
api.expectFile("out.js").toInclude("false");
},
});
itBundled(`feature_flag/${backend}/DeadCodeElimination`, {
backend,
files: {
"/a.js": `
import { feature } from "bun:bundle";
if (feature("ENABLED_FEATURE")) {
console.log("this should be kept");
}
if (feature("DISABLED_FEATURE")) {
console.log("this should be removed");
}
`,
},
features: ["ENABLED_FEATURE"],
minifySyntax: true,
onAfterBundle(api) {
// With minification, dead code should be eliminated
api.expectFile("out.js").toInclude("this should be kept");
api.expectFile("out.js").not.toInclude("this should be removed");
},
});
itBundled(`feature_flag/${backend}/ImportRemoved`, {
backend,
files: {
"/a.js": `
import { feature } from "bun:bundle";
if (feature("TEST")) {
console.log("test enabled");
}
`,
},
onAfterBundle(api) {
// The import should be completely removed
api.expectFile("out.js").not.toInclude("bun:bundle");
},
});
itBundled(`feature_flag/${backend}/IfBlockRemoved`, {
backend,
files: {
"/a.js": `
import { feature } from "bun:bundle";
function expensiveComputation() {
return "expensive result";
}
if (feature("DISABLED")) {
const result = expensiveComputation();
console.log("This entire block should be removed:", result);
}
console.log("This should remain");
`,
},
minifySyntax: true,
onAfterBundle(api) {
// The expensive computation and related code should be completely eliminated
api.expectFile("out.js").not.toInclude("expensiveComputation");
api.expectFile("out.js").not.toInclude("expensive result");
api.expectFile("out.js").not.toInclude("This entire block should be removed");
api.expectFile("out.js").toInclude("This should remain");
},
});
itBundled(`feature_flag/${backend}/KeepsElseBranch`, {
backend,
files: {
"/a.js": `
import { feature } from "bun:bundle";
if (feature("DISABLED")) {
console.log("if branch - should be removed");
} else {
console.log("else branch - should be kept");
}
`,
},
minifySyntax: true,
onAfterBundle(api) {
api.expectFile("out.js").not.toInclude("if branch - should be removed");
api.expectFile("out.js").toInclude("else branch - should be kept");
},
});
itBundled(`feature_flag/${backend}/RemovesElseBranch`, {
backend,
files: {
"/a.js": `
import { feature } from "bun:bundle";
if (feature("ENABLED")) {
console.log("if branch - should be kept");
} else {
console.log("else branch - should be removed");
}
`,
},
features: ["ENABLED"],
minifySyntax: true,
onAfterBundle(api) {
api.expectFile("out.js").toInclude("if branch - should be kept");
api.expectFile("out.js").not.toInclude("else branch - should be removed");
},
});
itBundled(`feature_flag/${backend}/AliasedImport`, {
backend,
files: {
"/a.js": `
import { feature as checkFeature } from "bun:bundle";
if (checkFeature("ALIASED")) {
console.log("aliased feature enabled");
} else {
console.log("aliased feature disabled");
}
`,
},
features: ["ALIASED"],
onAfterBundle(api) {
api.expectFile("out.js").toInclude("true");
api.expectFile("out.js").not.toInclude("checkFeature");
},
});
itBundled(`feature_flag/${backend}/TernaryDisabled`, {
backend,
files: {
"/a.js": `
import { feature } from "bun:bundle";
const result = feature("TERNARY_FLAG") ? "ternary_enabled" : "ternary_disabled";
console.log(result);
`,
},
minifySyntax: true,
onAfterBundle(api) {
api.expectFile("out.js").toInclude("ternary_disabled");
api.expectFile("out.js").not.toInclude("ternary_enabled");
},
});
itBundled(`feature_flag/${backend}/TernaryEnabled`, {
backend,
files: {
"/a.js": `
import { feature } from "bun:bundle";
const result = feature("TERNARY_FLAG") ? "ternary_enabled" : "ternary_disabled";
console.log(result);
`,
},
features: ["TERNARY_FLAG"],
minifySyntax: true,
onAfterBundle(api) {
api.expectFile("out.js").toInclude("ternary_enabled");
api.expectFile("out.js").not.toInclude("ternary_disabled");
},
});
});
}
// Error cases - only test with CLI since error handling might differ
itBundled("feature_flag/NonStringArgError", {
backend: "cli",
files: {
"/a.js": `
import { feature } from "bun:bundle";
const flag = "DYNAMIC";
if (feature(flag)) {
console.log("dynamic");
}
`,
},
bundleErrors: {
"/a.js": ["feature() argument must be a string literal"],
},
});
itBundled("feature_flag/NoArgsError", {
backend: "cli",
files: {
"/a.js": `
import { feature } from "bun:bundle";
if (feature()) {
console.log("no args");
}
`,
},
bundleErrors: {
"/a.js": ["feature() requires exactly one string argument"],
},
});
// Error cases for invalid usage of feature() - must be in if/ternary condition
itBundled("feature_flag/ConstAssignmentError", {
backend: "cli",
files: {
"/a.js": `
import { feature } from "bun:bundle";
const x = feature("FLAG");
console.log(x);
`,
},
bundleErrors: {
"/a.js": ['feature() from "bun:bundle" can only be used directly in an if statement or ternary condition'],
},
});
itBundled("feature_flag/LetAssignmentError", {
backend: "cli",
files: {
"/a.js": `
import { feature } from "bun:bundle";
let x = feature("FLAG");
console.log(x);
`,
},
bundleErrors: {
"/a.js": ['feature() from "bun:bundle" can only be used directly in an if statement or ternary condition'],
},
});
itBundled("feature_flag/ExportDefaultError", {
backend: "cli",
files: {
"/a.js": `
import { feature } from "bun:bundle";
export default feature("FLAG");
`,
},
bundleErrors: {
"/a.js": ['feature() from "bun:bundle" can only be used directly in an if statement or ternary condition'],
},
});
itBundled("feature_flag/FunctionArgumentError", {
backend: "cli",
files: {
"/a.js": `
import { feature } from "bun:bundle";
console.log(feature("FLAG"));
`,
},
bundleErrors: {
"/a.js": ['feature() from "bun:bundle" can only be used directly in an if statement or ternary condition'],
},
});
itBundled("feature_flag/ReturnStatementError", {
backend: "cli",
files: {
"/a.js": `
import { feature } from "bun:bundle";
function foo() {
return feature("FLAG");
}
`,
},
bundleErrors: {
"/a.js": ['feature() from "bun:bundle" can only be used directly in an if statement or ternary condition'],
},
});
itBundled("feature_flag/ArrayLiteralError", {
backend: "cli",
files: {
"/a.js": `
import { feature } from "bun:bundle";
const arr = [feature("FLAG")];
`,
},
bundleErrors: {
"/a.js": ['feature() from "bun:bundle" can only be used directly in an if statement or ternary condition'],
},
});
itBundled("feature_flag/ObjectPropertyError", {
backend: "cli",
files: {
"/a.js": `
import { feature } from "bun:bundle";
const obj = { flag: feature("FLAG") };
`,
},
bundleErrors: {
"/a.js": ['feature() from "bun:bundle" can only be used directly in an if statement or ternary condition'],
},
});
// Valid usage patterns - these should work without errors
for (const backend of ["cli", "api"] as const) {
itBundled(`feature_flag/${backend}/ValidIfStatement`, {
backend,
files: {
"/a.js": `
import { feature } from "bun:bundle";
if (feature("FLAG")) {
console.log("enabled");
}
`,
},
features: ["FLAG"],
onAfterBundle(api) {
api.expectFile("out.js").toInclude("true");
api.expectFile("out.js").not.toInclude("feature(");
},
});
itBundled(`feature_flag/${backend}/ValidTernary`, {
backend,
files: {
"/a.js": `
import { feature } from "bun:bundle";
const x = feature("FLAG") ? "yes" : "no";
console.log(x);
`,
},
features: ["FLAG"],
minifySyntax: true,
onAfterBundle(api) {
api.expectFile("out.js").toInclude("yes");
api.expectFile("out.js").not.toInclude("no");
},
});
itBundled(`feature_flag/${backend}/ValidElseIf`, {
backend,
files: {
"/a.js": `
import { feature } from "bun:bundle";
if (feature("A")) {
console.log("A");
} else if (feature("B")) {
console.log("B");
} else {
console.log("neither");
}
`,
},
features: ["B"],
minifySyntax: true,
onAfterBundle(api) {
api.expectFile("out.js").toInclude("B");
api.expectFile("out.js").not.toInclude("neither");
},
});
itBundled(`feature_flag/${backend}/ValidNestedTernary`, {
backend,
files: {
"/a.js": `
import { feature } from "bun:bundle";
const x = feature("A") ? "A" : feature("B") ? "B" : "C";
console.log(x);
`,
},
features: ["B"],
minifySyntax: true,
onAfterBundle(api) {
api.expectFile("out.js").toInclude("B");
api.expectFile("out.js").not.toInclude("A");
},
});
}
// Runtime tests - these must remain as manual tests since they test bun run and bun test
test("works correctly at runtime with bun run", async () => {
using dir = tempDir("bundler-feature-flag", {
"index.ts": `
import { feature } from "bun:bundle";
if (feature("RUNTIME_FLAG")) {
console.log("runtime flag enabled");
} else {
console.log("runtime flag disabled");
}
`,
});
// First, test without the flag
await using proc1 = Bun.spawn({
cmd: [bunExe(), "run", "./index.ts"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout1, , exitCode1] = await Promise.all([
new Response(proc1.stdout).text(),
new Response(proc1.stderr).text(),
proc1.exited,
]);
expect(stdout1.trim()).toBe("runtime flag disabled");
expect(exitCode1).toBe(0);
// Now test with the flag enabled
await using proc2 = Bun.spawn({
cmd: [bunExe(), "run", "--feature=RUNTIME_FLAG", "./index.ts"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout2, , exitCode2] = await Promise.all([
new Response(proc2.stdout).text(),
new Response(proc2.stderr).text(),
proc2.exited,
]);
expect(stdout2.trim()).toBe("runtime flag enabled");
expect(exitCode2).toBe(0);
});
test("works correctly in bun test", async () => {
using dir = tempDir("bundler-feature-flag", {
"test.test.ts": `
import { test, expect } from "bun:test";
import { feature } from "bun:bundle";
test("feature flag in test", () => {
if (feature("TEST_FLAG")) {
console.log("TEST_FLAG_ENABLED");
} else {
console.log("TEST_FLAG_DISABLED");
}
expect(true).toBe(true);
});
`,
});
// First, test without the flag
await using proc1 = Bun.spawn({
cmd: [bunExe(), "test", "./test.test.ts"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout1, , exitCode1] = await Promise.all([
new Response(proc1.stdout).text(),
new Response(proc1.stderr).text(),
proc1.exited,
]);
expect(stdout1).toContain("TEST_FLAG_DISABLED");
expect(stdout1).not.toContain("TEST_FLAG_ENABLED");
expect(exitCode1).toBe(0);
// Now test with the flag enabled
await using proc2 = Bun.spawn({
cmd: [bunExe(), "test", "--feature=TEST_FLAG", "./test.test.ts"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout2, , exitCode2] = await Promise.all([
new Response(proc2.stdout).text(),
new Response(proc2.stderr).text(),
proc2.exited,
]);
expect(stdout2).toContain("TEST_FLAG_ENABLED");
expect(stdout2).not.toContain("TEST_FLAG_DISABLED");
expect(exitCode2).toBe(0);
});
});