Compare commits

...

15 Commits

Author SHA1 Message Date
Claude Bot
08215d9d20 test(bun-types): add comprehensive type tests for bun:bundle module
- Add bundle.ts fixture with comprehensive type tests for feature()
- Add tests demonstrating type-safe feature flags using declaration merging
- Document type safety approach in bundler docs

The tests cover:
- Basic feature() usage and return types
- Feature flags in conditionals, ternaries, and logical operators
- Feature flags in functions, classes, async/generator contexts
- Import aliases
- Error cases with @ts-expect-error
- Declaration merging for type-safe overloads

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-11 03:30:32 +00:00
Claude Bot
21d93e97fa fix: Change unknown bun:bundle export from warning to error
- Unknown exports from "bun:bundle" now produce an error instead of a warning
- Remove unnecessary comment in visitStmt.zig

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-11 03:24:19 +00:00
Claude Bot
7cdf07e36e fix: Add deinit for bundler_feature_flags to prevent memory leak
- Update BundleOptions.deinit to free bundler_feature_flags if allocated
- Skip freeing static empty_bundler_feature_flags constant
- Pass allocator to deinit from Transpiler.deinit

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-11 02:51:27 +00:00
Claude Bot
f1789680b4 docs: Remove redundant adverb in feature flags docs 2025-12-11 02:45:38 +00:00
Claude Bot
ca1bf28023 fix: Add CLI flag reference to JSDoc, improve initComptime warning
- Add "Equivalent to the CLI --feature flag" to bun.d.ts JSDoc
- Improve initComptime doc comment warning about undefined behavior

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-11 02:45:12 +00:00
autofix-ci[bot]
fd3daa2661 [autofix.ci] apply automated fixes 2025-12-11 02:33:07 +00:00
Claude Bot
3fc2644bc2 docs(bundler): Add documentation for feature flags
- Add features section to bundler index.mdx with examples for CLI and JS API
- Document dead-code elimination behavior with minification
- List key behaviors and use cases
- Add feature flag entry to esbuild compatibility table

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-11 02:31:26 +00:00
autofix-ci[bot]
7d74e8e02c [autofix.ci] apply automated fixes 2025-12-11 02:27:48 +00:00
Claude Bot
daaa6a1c31 feat(bundler): Add features support to Bun.build API and use StringSet
- Add features option to Bun.build() JavaScript API
- Update JSBundler.zig to parse features array from JS config
- Wire up features in bundle_v2.zig to pass StringSet directly
- Change bundler_feature_flags type from StringHashMapUnmanaged to StringSet
- Add initComptime() to StringSet for static empty constant
- Update bun.d.ts with features option documentation
- Update expectBundled.ts to propagate features for tests
- Convert tests to use itBundled with both cli and api backends

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-11 02:26:06 +00:00
Claude Bot
41d2ceb8d3 refactor: move bun:bundle handling to parse time and optimize
Per Jarred's review feedback:
- Move bun:bundle import handling from visit time (visitStmt.zig) to
  parse time (processImportStatement in P.zig), similar to how macros
  are handled
- Move bundler_feature_flag_ref.isValid() check before the function
  call to avoid extra stack memory usage from copying values

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-11 01:39:16 +00:00
Claude Bot
03275767b8 address CodeRabbit review comments
- Avoid allocation when checking feature flag names: use underlying
  string data directly instead of calling slice() with allocator
- Add validation for duplicate feature imports and unknown specifiers
  from bun:bundle
- Consolidate initBundlerFeatureFlags into runtime.zig and use
  handleOom for allocation failures instead of silently returning
  empty map
- Reorder test assertions: check output before exitCode for better
  error messages, add non-zero exit code checks for error cases

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-11 01:29:28 +00:00
Claude Bot
a489198dd5 feat: add TypeScript definitions for bun:bundle module
Adds type definitions for the `bun:bundle` module to `bun-types` package
so TypeScript users get proper type checking and intellisense.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-11 01:16:53 +00:00
Claude Bot
555cd5808e refactor: rename bun:bundler to bun:bundle
Per review feedback, renamed the module namespace from `bun:bundler` to
`bun:bundle` for better consistency.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-11 01:15:31 +00:00
Claude Bot
f975a66cf3 fix: support aliased imports and runtime feature flags
- Fix aliased import handling: check `item.alias` instead of
  `item.original_name` to match the source module's export name
- Add feature flag support to runtime transpiler (bun run, bun test)
- Change bundler_feature_flags to pointer type with global empty default
- Add comprehensive tests for runtime and aliased imports

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-11 01:04:54 +00:00
Claude Bot
4ee36d8d3c feat(bundler): add statically-analyzable dead-code elimination via feature flags
Implement a new bundler feature that allows static dead-code elimination through
feature gating at bundle time:

```js
import { feature } from "bun:bundler";

if (feature("SUPER_SECRET")) {
   console.log("hello!");
}
```

Usage in CLI:
```sh
bun build --feature=SUPER_SECRET ./index.ts
```

When `feature("SUPER_SECRET")` is called and the flag is enabled via `--feature`,
it gets replaced with `true` at bundle time, otherwise `false`. This enables
bundlers to statically eliminate dead code branches.

Implementation details:
- Added `bundler_feature_flags` to RuntimeFeatures struct in runtime.zig
- Added `bundler_feature_flag_ref` to Parser struct to track the feature import
- Handle `bun:bundler` import in s_import visitor to capture the feature ref
- Handle feature() call in e_call visitor to replace with boolean
- Support both e_identifier and e_import_identifier for the call target
- Added --feature CLI arg to bun build and bun test commands

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-10 23:51:55 +00:00
21 changed files with 1117 additions and 19 deletions

View File

@@ -65,6 +65,7 @@ In Bun's CLI, simple boolean flags like `--minify` do not accept an argument. Ot
| `--chunk-names` | `--chunk-naming` | Renamed for consistency with naming in JS API |
| `--color` | n/a | Always enabled |
| `--drop` | `--drop` | |
| n/a | `--feature` | Bun-specific. Enable feature flags for compile-time dead-code elimination via `import { feature } from "bun:bundle"` |
| `--entry-names` | `--entry-naming` | Renamed for consistency with naming in JS API |
| `--global-name` | n/a | Not applicable, Bun does not support `iife` output at this time |
| `--ignore-annotations` | `--ignore-dce-annotations` | |

View File

@@ -1141,6 +1141,126 @@ Remove function calls from a bundle. For example, `--drop=console` will remove a
</Tab>
</Tabs>
### features
Enable compile-time feature flags for dead-code elimination. This provides a way to conditionally include or exclude code paths at bundle time using `import { feature } from "bun:bundle"`.
```ts title="app.ts" icon="/icons/typescript.svg"
import { feature } from "bun:bundle";
if (feature("PREMIUM")) {
// Only included when PREMIUM flag is enabled
initPremiumFeatures();
}
if (feature("DEBUG")) {
// Only included when DEBUG flag is enabled
console.log("Debug mode");
}
```
<Tabs>
<Tab title="JavaScript">
```ts title="build.ts" icon="/icons/typescript.svg"
await Bun.build({
entrypoints: ['./app.ts'],
outdir: './out',
features: ["PREMIUM"], // PREMIUM=true, DEBUG=false
})
```
</Tab>
<Tab title="CLI">
```bash terminal icon="terminal"
bun build ./app.ts --outdir ./out --feature PREMIUM
```
</Tab>
</Tabs>
The `feature()` function is replaced with `true` or `false` at bundle time. Combined with minification, unreachable code is eliminated:
```ts title="Input" icon="/icons/typescript.svg"
import { feature } from "bun:bundle";
const mode = feature("PREMIUM") ? "premium" : "free";
```
```js title="Output (with --feature PREMIUM --minify)" icon="/icons/javascript.svg"
var mode = "premium";
```
```js title="Output (without --feature PREMIUM, with --minify)" icon="/icons/javascript.svg"
var mode = "free";
```
**Key behaviors:**
- `feature()` requires a string literal argument — dynamic values are not supported
- The `bun:bundle` import is completely removed from the output
- Works with `bun build`, `bun run`, and `bun test`
- Multiple flags can be enabled: `--feature FLAG_A --feature FLAG_B`
**Use cases:**
- Platform-specific code (`feature("SERVER")` vs `feature("CLIENT")`)
- Environment-based features (`feature("DEVELOPMENT")`)
- Gradual feature rollouts
- A/B testing variants
- Paid tier features
#### Type-safe feature flags
By default, `feature()` accepts any string. For better type safety, you can use TypeScript declaration merging to restrict it to known flag names. This gives you:
- Autocomplete for valid flag names
- Compile-time errors for typos
- Documentation of available flags in your codebase
Create an `env.d.ts` file in your project:
```ts title="env.d.ts" icon="/icons/typescript.svg"
declare module "bun:bundle" {
// Define your valid feature flag names
type FeatureFlag = "DEBUG" | "PREMIUM" | "BETA_FEATURES" | "ANALYTICS";
// Overload that restricts to known flags
export function feature(flag: FeatureFlag): boolean;
}
```
Now TypeScript will only allow known flags:
```ts title="app.ts" icon="/icons/typescript.svg"
import { feature } from "bun:bundle";
// These work - they're in FeatureFlag
if (feature("DEBUG")) { /* ... */ }
if (feature("PREMIUM")) { /* ... */ }
// Type error: Argument of type '"TYPO"' is not assignable...
if (feature("TYPO")) { /* ... */ }
```
For gradual adoption, you can add a fallback overload that still accepts any string:
```ts title="env.d.ts" icon="/icons/typescript.svg"
declare module "bun:bundle" {
type FeatureFlag = "DEBUG" | "PREMIUM";
// Known flags get autocomplete
export function feature(flag: FeatureFlag): boolean;
// Unknown flags still work (no autocomplete)
export function feature(flag: string): boolean;
}
```
Make sure to include your `env.d.ts` in your `tsconfig.json`:
```json title="tsconfig.json" icon="/icons/json.svg"
{
"compilerOptions": { /* ... */ },
"include": ["src", "env.d.ts"]
}
```
## Outputs
The `Bun.build` function returns a `Promise<BuildOutput>`, defined as:

View File

@@ -1740,9 +1740,9 @@ declare module "bun" {
* @default "esm"
*/
format?: /**
* ECMAScript Module format
*/
| "esm"
* ECMAScript Module format
*/
| "esm"
/**
* CommonJS format
* **Experimental**
@@ -1891,6 +1891,24 @@ declare module "bun" {
*/
drop?: string[];
/**
* Enable feature flags for dead-code elimination via `import { feature } from "bun:bundle"`.
*
* When `feature("FLAG_NAME")` is called, it returns `true` if FLAG_NAME is in this array,
* or `false` otherwise. This enables static dead-code elimination at bundle time.
*
* Equivalent to the CLI `--feature` flag.
*
* @example
* ```ts
* await Bun.build({
* entrypoints: ['./src/index.ts'],
* features: ['FEATURE_A', 'FEATURE_B'],
* });
* ```
*/
features?: string[];
/**
* - When set to `true`, the returned promise rejects with an AggregateError when a build failure happens.
* - When set to `false`, returns a {@link BuildOutput} with `{success: false}`
@@ -3316,10 +3334,10 @@ declare module "bun" {
function color(
input: ColorInput,
outputFormat?: /**
* True color ANSI color string, for use in terminals
* @example \x1b[38;2;100;200;200m
*/
| "ansi"
* True color ANSI color string, for use in terminals
* @example \x1b[38;2;100;200;200m
*/
| "ansi"
| "ansi-16"
| "ansi-16m"
/**
@@ -5650,17 +5668,11 @@ declare module "bun" {
maxBuffer?: number;
}
interface SpawnSyncOptions<In extends Writable, Out extends Readable, Err extends Readable> extends BaseOptions<
In,
Out,
Err
> {}
interface SpawnSyncOptions<In extends Writable, Out extends Readable, Err extends Readable>
extends BaseOptions<In, Out, Err> {}
interface SpawnOptions<In extends Writable, Out extends Readable, Err extends Readable> extends BaseOptions<
In,
Out,
Err
> {
interface SpawnOptions<In extends Writable, Out extends Readable, Err extends Readable>
extends BaseOptions<In, Out, Err> {
/**
* If true, stdout and stderr pipes will not automatically start reading
* data. Reading will only begin when you access the `stdout` or `stderr`

51
packages/bun-types/bundle.d.ts vendored Normal file
View File

@@ -0,0 +1,51 @@
/**
* The `bun:bundle` module provides compile-time utilities for dead-code elimination.
*
* @example
* ```ts
* import { feature } from "bun:bundle";
*
* if (feature("SUPER_SECRET")) {
* console.log("Secret feature enabled!");
* } else {
* console.log("Normal mode");
* }
* ```
*
* Enable feature flags via CLI:
* ```bash
* # During build
* bun build --feature=SUPER_SECRET index.ts
*
* # At runtime
* bun run --feature=SUPER_SECRET index.ts
*
* # In tests
* bun test --feature=SUPER_SECRET
* ```
*
* @module bun:bundle
*/
declare module "bun:bundle" {
/**
* Check if a feature flag is enabled at compile time.
*
* This function is replaced with a boolean literal (`true` or `false`) at bundle time,
* enabling dead-code elimination. The bundler will remove unreachable branches.
*
* @param flag - The name of the feature flag to check
* @returns `true` if the flag was passed via `--feature=FLAG`, `false` otherwise
*
* @example
* ```ts
* import { feature } from "bun:bundle";
*
* // With --feature=DEBUG, this becomes: if (true) { ... }
* // Without --feature=DEBUG, this becomes: if (false) { ... }
* if (feature("DEBUG")) {
* console.log("Debug mode enabled");
* }
* ```
*/
function feature(flag: string): boolean;
}

View File

@@ -23,6 +23,7 @@
/// <reference path="./serve.d.ts" />
/// <reference path="./sql.d.ts" />
/// <reference path="./security.d.ts" />
/// <reference path="./bundle.d.ts" />
/// <reference path="./bun.ns.d.ts" />

View File

@@ -1655,6 +1655,9 @@ pub const api = struct {
drop: []const []const u8 = &.{},
/// feature_flags for dead-code elimination via `import { feature } from "bun:bundle"`
feature_flags: []const []const u8 = &.{},
/// preserve_symlinks
preserve_symlinks: ?bool = null,

View File

@@ -185,6 +185,11 @@ pub fn NewParser_(
/// it to the symbol so the code generated `e_import_identifier`'s
bun_app_namespace_ref: Ref = Ref.None,
/// Used to track the `feature` function from `import { feature } from "bun:bundle"`.
/// When visiting e_call, if the target ref matches this, we replace the call with
/// a boolean based on whether the feature flag is enabled.
bundler_feature_flag_ref: Ref = Ref.None,
scopes_in_order_visitor_index: usize = 0,
has_classic_runtime_warned: bool = false,
macro_call_count: MacroCallCountType = 0,
@@ -2653,6 +2658,34 @@ pub fn NewParser_(
return p.s(S.Empty{}, loc);
}
// Handle `import { feature } from "bun:bundle"` - this is a special import
// that provides static feature flag checking at bundle time.
// We handle it here at parse time (similar to macros) rather than at visit time.
if (strings.eqlComptime(path.text, "bun:bundle")) {
// Look for the "feature" import and validate specifiers
for (stmt.items) |*item| {
// In ClauseItem from parseImportClause:
// - alias is the name from the source module ("feature")
// - original_name is the local binding name
// - name.ref is the ref for the local binding
if (strings.eqlComptime(item.alias, "feature")) {
// Check for duplicate imports of feature
if (p.bundler_feature_flag_ref.isValid()) {
try p.log.addError(p.source, item.alias_loc, "`feature` from \"bun:bundle\" may only be imported once");
continue;
}
// Declare the symbol and store the ref
const name = p.loadNameFromRef(item.name.ref.?);
const ref = try p.declareSymbol(.other, item.name.loc, name);
p.bundler_feature_flag_ref = ref;
} else {
try p.log.addErrorFmt(p.source, item.alias_loc, p.allocator, "\"bun:bundle\" has no export named \"{s}\"", .{item.alias});
}
}
// Return empty statement - the import is completely removed
return p.s(S.Empty{}, loc);
}
const macro_remap = if (comptime allow_macros)
p.options.macro_context.getRemap(path.text)
else

View File

@@ -1277,6 +1277,15 @@ pub fn VisitExpr(
}
}
// Handle `feature("FLAG_NAME")` calls from `import { feature } from "bun:bundle"`
// Check if the bundler_feature_flag_ref is set before calling the function
// to avoid stack memory usage from copying values back and forth.
if (p.bundler_feature_flag_ref.isValid()) {
if (maybeReplaceBundlerFeatureCall(p, e_, expr.loc)) |result| {
return result;
}
}
if (e_.target.data == .e_require_call_target) {
e_.can_be_unwrapped_if_unused = .never;
@@ -1631,6 +1640,59 @@ pub fn VisitExpr(
return expr;
}
/// Handles `feature("FLAG_NAME")` calls from `import { feature } from "bun:bundle"`.
/// This enables statically analyzable dead-code elimination through feature gating.
///
/// When a feature flag is enabled via `--feature=FLAG_NAME`, `feature("FLAG_NAME")`
/// is replaced with `true`, otherwise it's replaced with `false`. This allows
/// bundlers to eliminate dead code branches at build time.
///
/// Returns the replacement expression if this is a feature() call, or null otherwise.
/// Note: Caller must check `p.bundler_feature_flag_ref.isValid()` before calling.
fn maybeReplaceBundlerFeatureCall(p: *P, e_: *E.Call, loc: logger.Loc) ?Expr {
// Check if the target is the `feature` function from "bun:bundle"
// It could be e_identifier (for unbound) or e_import_identifier (for imports)
const target_ref: ?Ref = switch (e_.target.data) {
.e_identifier => |ident| ident.ref,
.e_import_identifier => |ident| ident.ref,
else => null,
};
if (target_ref == null or !target_ref.?.eql(p.bundler_feature_flag_ref)) {
return null;
}
// If control flow is dead, just return false without validation errors
if (p.is_control_flow_dead) {
return p.newExpr(E.Boolean{ .value = false }, loc);
}
// Validate: exactly one argument required
if (e_.args.len != 1) {
p.log.addError(p.source, loc, "feature() requires exactly one string argument") catch unreachable;
return p.newExpr(E.Boolean{ .value = false }, loc);
}
const arg = e_.args.slice()[0];
// Validate: argument must be a string literal
if (arg.data != .e_string) {
p.log.addError(p.source, arg.loc, "feature() argument must be a string literal") catch unreachable;
return p.newExpr(E.Boolean{ .value = false }, loc);
}
// Check if the feature flag is enabled
// Use the underlying string data directly without allocation.
// Feature flag names should be ASCII identifiers, so UTF-16 is unexpected.
const flag_string = arg.data.e_string;
if (flag_string.is_utf16) {
p.log.addError(p.source, arg.loc, "feature() flag name must be an ASCII string") catch unreachable;
return p.newExpr(E.Boolean{ .value = false }, loc);
}
const is_enabled = p.options.features.bundler_feature_flags.map.contains(flag_string.data);
return p.newExpr(E.Boolean{ .value = is_enabled }, loc);
}
};
};
}

View File

@@ -38,6 +38,7 @@ pub const JSBundler = struct {
footer: OwnedString = OwnedString.initEmpty(bun.default_allocator),
css_chunking: bool = false,
drop: bun.StringSet = bun.StringSet.init(bun.default_allocator),
features: bun.StringSet = bun.StringSet.init(bun.default_allocator),
has_any_on_before_parse: bool = false,
throw_on_error: bool = true,
env_behavior: api.DotEnvBehavior = .disable,
@@ -561,6 +562,15 @@ pub const JSBundler = struct {
}
}
if (try config.getOwnArray(globalThis, "features")) |features| {
var iter = try features.arrayIterator(globalThis);
while (try iter.next()) |entry| {
var slice = try entry.toSliceOrNull(globalThis);
defer slice.deinit();
try this.features.insert(slice.slice());
}
}
// if (try config.getOptional(globalThis, "dir", ZigString.Slice)) |slice| {
// defer slice.deinit();
// this.appendSliceExact(slice.slice()) catch unreachable;
@@ -804,6 +814,7 @@ pub const JSBundler = struct {
self.public_path.deinit();
self.conditions.deinit();
self.drop.deinit();
self.features.deinit();
self.banner.deinit();
if (self.compile) |*compile| {
compile.deinit();

View File

@@ -1622,6 +1622,15 @@ pub const StringSet = struct {
};
}
/// Initialize an empty StringSet at comptime (for use as a static constant).
/// WARNING: The resulting set must not be mutated. Any attempt to call insert(),
/// clone(), or other allocating methods will result in undefined behavior.
pub fn initComptime() StringSet {
return StringSet{
.map = Map.initContext(undefined, .{}),
};
}
pub fn isEmpty(self: *const StringSet) bool {
return self.count() == 0;
}

View File

@@ -1176,6 +1176,7 @@ fn runWithSourceCode(
opts.features.minify_whitespace = transpiler.options.minify_whitespace;
opts.features.emit_decorator_metadata = transpiler.options.emit_decorator_metadata;
opts.features.unwrap_commonjs_packages = transpiler.options.unwrap_commonjs_packages;
opts.features.bundler_feature_flags = transpiler.options.bundler_feature_flags;
opts.features.hot_module_reloading = output_format == .internal_bake_dev and !source.index.isRuntime();
opts.features.auto_polyfill_require = output_format == .esm and !opts.features.hot_module_reloading;
opts.features.react_fast_refresh = target == .browser and

View File

@@ -1836,6 +1836,8 @@ pub const BundleV2 = struct {
);
transpiler.options.env.behavior = config.env_behavior;
transpiler.options.env.prefix = config.env_prefix.slice();
// Use the StringSet directly instead of the slice passed through TransformOptions
transpiler.options.bundler_feature_flags = &config.features;
if (config.force_node_env != .unspecified) {
transpiler.options.force_node_env = config.force_node_env;
}

View File

@@ -66,6 +66,7 @@ pub const transpiler_params_ = [_]ParamType{
clap.parseParam("--tsconfig-override <STR> Specify custom tsconfig.json. Default <d>$cwd<r>/tsconfig.json") catch unreachable,
clap.parseParam("-d, --define <STR>... Substitute K:V while parsing, e.g. --define process.env.NODE_ENV:\"development\". Values are parsed as JSON.") catch unreachable,
clap.parseParam("--drop <STR>... Remove function calls, e.g. --drop=console removes all console.* calls.") catch unreachable,
clap.parseParam("--feature <STR>... Enable a feature flag for dead-code elimination, e.g. --feature=SUPER_SECRET") catch unreachable,
clap.parseParam("-l, --loader <STR>... Parse files with .ext:loader, e.g. --loader .js:jsx. Valid loaders: js, jsx, ts, tsx, json, toml, text, file, wasm, napi") catch unreachable,
clap.parseParam("--no-macros Disable macros from being executed in the bundler, transpiler and runtime") catch unreachable,
clap.parseParam("--jsx-factory <STR> Changes the function called when compiling JSX elements using the classic JSX runtime") catch unreachable,
@@ -587,6 +588,7 @@ pub fn parse(allocator: std.mem.Allocator, ctx: Command.Context, comptime cmd: C
}
opts.drop = args.options("--drop");
opts.feature_flags = args.options("--feature");
// Node added a `--loader` flag (that's kinda like `--register`). It's
// completely different from ours.

View File

@@ -82,6 +82,7 @@ pub const BuildCommand = struct {
this_transpiler.options.banner = ctx.bundler_options.banner;
this_transpiler.options.footer = ctx.bundler_options.footer;
this_transpiler.options.drop = ctx.args.drop;
this_transpiler.options.bundler_feature_flags = Runtime.Features.initBundlerFeatureFlags(allocator, ctx.args.feature_flags);
this_transpiler.options.css_chunking = ctx.bundler_options.css_chunking;
@@ -653,6 +654,7 @@ const resolve_path = @import("../resolver/resolve_path.zig");
const std = @import("std");
const BundleV2 = @import("../bundler/bundle_v2.zig").BundleV2;
const Command = @import("../cli.zig").Command;
const Runtime = @import("../runtime.zig").Runtime;
const bun = @import("bun");
const Global = bun.Global;

View File

@@ -1717,6 +1717,9 @@ pub const BundleOptions = struct {
banner: string = "",
define: *defines.Define,
drop: []const []const u8 = &.{},
/// Set of enabled feature flags for dead-code elimination via `import { feature } from "bun:bundle"`.
/// Initialized once from the CLI --feature flags.
bundler_feature_flags: *const bun.StringSet = &Runtime.Features.empty_bundler_feature_flags,
loaders: Loader.HashTable,
resolve_dir: string = "/",
jsx: JSX.Pragma = JSX.Pragma{},
@@ -1906,8 +1909,13 @@ pub const BundleOptions = struct {
this.defines_loaded = true;
}
pub fn deinit(this: *const BundleOptions) void {
pub fn deinit(this: *BundleOptions, allocator: std.mem.Allocator) void {
this.define.deinit();
// Free bundler_feature_flags if it was allocated (not the static empty set)
if (this.bundler_feature_flags != &Runtime.Features.empty_bundler_feature_flags) {
@constCast(this.bundler_feature_flags).deinit();
allocator.destroy(@constCast(this.bundler_feature_flags));
}
}
pub fn loader(this: *const BundleOptions, ext: string) Loader {
@@ -2008,6 +2016,7 @@ pub const BundleOptions = struct {
.transform_options = transform,
.css_chunking = false,
.drop = transform.drop,
.bundler_feature_flags = Runtime.Features.initBundlerFeatureFlags(allocator, transform.feature_flags),
};
analytics.Features.define += @as(usize, @intFromBool(transform.define != null));

View File

@@ -211,6 +211,27 @@ pub const Runtime = struct {
// TODO: make this a bitset of all unsupported features
lower_using: bool = true,
/// Feature flags for dead-code elimination via `import { feature } from "bun:bundle"`
/// When `feature("FLAG_NAME")` is called, it returns true if FLAG_NAME is in this set.
bundler_feature_flags: *const bun.StringSet = &empty_bundler_feature_flags,
pub const empty_bundler_feature_flags: bun.StringSet = bun.StringSet.initComptime();
/// Initialize bundler feature flags for dead-code elimination via `import { feature } from "bun:bundle"`.
/// Returns a pointer to a StringSet containing the enabled flags, or the empty set if no flags are provided.
pub fn initBundlerFeatureFlags(allocator: std.mem.Allocator, feature_flags: []const []const u8) *const bun.StringSet {
if (feature_flags.len == 0) {
return &empty_bundler_feature_flags;
}
const set = bun.handleOom(allocator.create(bun.StringSet));
set.* = bun.StringSet.init(allocator);
for (feature_flags) |flag| {
set.insert(flag) catch bun.outOfMemory();
}
return set;
}
const hash_fields_for_runtime_transpiler = .{
.top_level_await,
.auto_import_jsx,

View File

@@ -438,7 +438,7 @@ pub const Transpiler = struct {
}
pub fn deinit(this: *Transpiler) void {
this.options.deinit();
this.options.deinit(this.allocator);
this.log.deinit();
this.resolver.deinit();
this.fs.deinit();
@@ -1113,6 +1113,7 @@ pub const Transpiler = struct {
opts.features.minify_identifiers = transpiler.options.minify_identifiers;
opts.features.dead_code_elimination = transpiler.options.dead_code_elimination;
opts.features.remove_cjs_module_wrapper = this_parse.remove_cjs_module_wrapper;
opts.features.bundler_feature_flags = transpiler.options.bundler_feature_flags;
if (transpiler.macro_context == null) {
transpiler.macro_context = js_ast.Macro.MacroContext.init(transpiler);

View File

@@ -0,0 +1,364 @@
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";
const a = feature("FLAG_A");
const b = feature("FLAG_B");
const c = feature("FLAG_C");
console.log(a, b, c);
`,
},
features: ["FLAG_A", "FLAG_C"],
onAfterBundle(api) {
// FLAG_A and FLAG_C are enabled, FLAG_B is not
api.expectFile("out.js").toInclude("a = true");
api.expectFile("out.js").toInclude("b = false");
api.expectFile("out.js").toInclude("c = true");
},
});
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";
const x = feature("TEST");
console.log(x);
`,
},
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"],
},
});
// 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);
});
});

View File

@@ -161,6 +161,8 @@ export interface BundlerTestInput {
footer?: string;
define?: Record<string, string | number>;
drop?: string[];
/** Feature flags for dead-code elimination via `import { feature } from "bun:bundle"` */
features?: string[];
/** Use for resolve custom conditions */
conditions?: string[];
@@ -444,6 +446,7 @@ function expectBundled(
external,
packages,
drop = [],
features = [],
files,
footer,
format,
@@ -742,6 +745,7 @@ function expectBundled(
minifySyntax && `--minify-syntax`,
minifyWhitespace && `--minify-whitespace`,
drop?.length && drop.map(x => ["--drop=" + x]),
features?.length && features.map(x => ["--feature=" + x]),
globalName && `--global-name=${globalName}`,
jsx.runtime && ["--jsx-runtime", jsx.runtime],
jsx.factory && ["--jsx-factory", jsx.factory],
@@ -1110,6 +1114,7 @@ function expectBundled(
emitDCEAnnotations,
ignoreDCEAnnotations,
drop,
features,
define: define ?? {},
throw: _throw ?? false,
compile,

View File

@@ -357,6 +357,148 @@ describe("@types/bun integration test", () => {
});
});
describe("bun:bundle feature() type safety with overloads", () => {
// This demonstrates how users can add type-safe feature flags using declaration merging.
// By adding overloads in an env.d.ts file, users can:
// 1. Restrict feature() to only accept known flag names
// 2. Get autocomplete for valid flag names
// 3. Get compile-time errors for typos or unknown flags
const envDts = `
// env.d.ts - User's custom type declarations for feature flags
//
// This file demonstrates how to add type safety to bun:bundle's feature() function.
// By using declaration merging and function overloads, you can restrict feature()
// to only accept known flag names, getting autocomplete and compile-time checking.
//
// Add this to your project's type declarations (e.g., env.d.ts or global.d.ts)
// and include it in your tsconfig.json's "include" array.
declare module "bun:bundle" {
// Define your valid feature flag names as a union type
type ValidFeatureFlag = "DEBUG" | "PREMIUM" | "BETA_FEATURES" | "ANALYTICS";
// Overload 1: Type-safe version that accepts only valid flags
// This overload is checked first due to TypeScript's overload resolution
export function feature(flag: ValidFeatureFlag): boolean;
// Overload 2: Fallback that accepts any string (original behavior)
// This allows gradual adoption - unknown flags still work but without autocomplete
export function feature(flag: string): boolean;
}
`;
const testCode = `
import { feature } from "bun:bundle";
// These should work - they're in ValidFeatureFlag
const isDebug: boolean = feature("DEBUG");
const isPremium: boolean = feature("PREMIUM");
const isBeta: boolean = feature("BETA_FEATURES");
const hasAnalytics: boolean = feature("ANALYTICS");
// Conditional logic with type-safe flags
if (feature("DEBUG")) {
console.log("Debug mode");
}
const config = {
premium: feature("PREMIUM"),
beta: feature("BETA_FEATURES"),
};
// This also works due to the string fallback overload
// (allows gradual adoption of new flags)
const unknownFlag: boolean = feature("SOME_OTHER_FLAG");
`;
test("type-safe feature flags work with env.d.ts overloads", async () => {
const { diagnostics, emptyInterfaces } = await diagnose(TEMP_FIXTURE_DIR, {
files: {
"feature-env.d.ts": envDts,
"feature-test.ts": testCode,
},
});
expect(emptyInterfaces).toEqual(expectedEmptyInterfacesWhenNoDOM);
expect(diagnostics).toEqual([]);
});
test("demonstrates strict-only mode (no string fallback)", async () => {
// This shows how to make feature() ONLY accept known flags (no fallback)
const strictEnvDts = `
declare module "bun:bundle" {
type StrictFeatureFlag = "FEATURE_A" | "FEATURE_B";
// Only one overload - no string fallback means unknown flags are errors
export function feature(flag: StrictFeatureFlag): boolean;
}
`;
const strictTestCode = `
import { feature } from "bun:bundle";
// Valid flags work
const a: boolean = feature("FEATURE_A");
const b: boolean = feature("FEATURE_B");
`;
const { diagnostics, emptyInterfaces } = await diagnose(TEMP_FIXTURE_DIR, {
files: {
"strict-env.d.ts": strictEnvDts,
"strict-test.ts": strictTestCode,
},
});
expect(emptyInterfaces).toEqual(expectedEmptyInterfacesWhenNoDOM);
expect(diagnostics).toEqual([]);
});
test("overloads work with conditional types for advanced use cases", async () => {
// This demonstrates that overloads can be combined with conditional types
// for more advanced type inference scenarios
const advancedEnvDts = `
declare module "bun:bundle" {
type KnownFlag = "DEBUG" | "PRODUCTION";
// Overload with known flags for better autocomplete
export function feature(flag: KnownFlag): boolean;
// Original overload preserved for unknown flags
export function feature(flag: string): boolean;
}
`;
const advancedTestCode = `
import { feature } from "bun:bundle";
// Known flags work
const debug: boolean = feature("DEBUG");
const prod: boolean = feature("PRODUCTION");
// Unknown flags still work (string overload)
const custom: boolean = feature("CUSTOM_FLAG");
// Can use in conditionals
if (feature("DEBUG")) {
console.log("debug mode");
}
// Type checking ensures return type is boolean
const result: boolean = feature("DEBUG") && feature("PRODUCTION");
`;
const { diagnostics, emptyInterfaces } = await diagnose(TEMP_FIXTURE_DIR, {
files: {
"advanced-env.d.ts": advancedEnvDts,
"advanced-test.ts": advancedTestCode,
},
});
expect(emptyInterfaces).toEqual(expectedEmptyInterfacesWhenNoDOM);
expect(diagnostics).toEqual([]);
});
});
test("checks with no lib at all", async () => {
const { diagnostics, emptyInterfaces } = await diagnose(TEMP_FIXTURE_DIR, {
options: {

View File

@@ -0,0 +1,246 @@
/**
* Type tests for the "bun:bundle" module.
*
* This module provides compile-time utilities for dead-code elimination
* via feature flags. The `feature()` function is replaced with boolean
* literals at bundle time.
*/
import { feature } from "bun:bundle";
import { expectType } from "./utilities";
// Basic feature() call returns boolean
{
const result = feature("DEBUG");
expectType(result).is<boolean>();
}
// Feature flags in conditional statements
{
if (feature("FEATURE_A")) {
// This branch is included when --feature=FEATURE_A is passed
console.log("Feature A enabled");
}
if (feature("FEATURE_B")) {
console.log("Feature B enabled");
} else {
console.log("Feature B disabled");
}
}
// Feature flags with ternary operator
{
const value = feature("PREMIUM") ? "premium" : "free";
expectType(value).is<string>();
const numericValue = feature("V2") ? 2 : 1;
expectType(numericValue).is<number>();
}
// Feature flags with logical operators
{
const andResult = feature("A") && feature("B");
expectType(andResult).is<boolean>();
const orResult = feature("A") || feature("B");
expectType(orResult).is<boolean>();
const notResult = !feature("A");
expectType(notResult).is<boolean>();
}
// Feature flags used in function contexts
{
function getConfig() {
return {
debug: feature("DEBUG"),
verbose: feature("VERBOSE"),
experimental: feature("EXPERIMENTAL"),
};
}
const config = getConfig();
expectType(config.debug).is<boolean>();
expectType(config.verbose).is<boolean>();
expectType(config.experimental).is<boolean>();
}
// Feature flags as object property values
{
const features = {
enableLogs: feature("LOGS"),
enableMetrics: feature("METRICS"),
enableTracing: feature("TRACING"),
};
expectType(features).is<{ enableLogs: boolean; enableMetrics: boolean; enableTracing: boolean }>();
}
// Feature flags in array contexts
{
const flagResults = [feature("A"), feature("B"), feature("C")];
expectType(flagResults).is<boolean[]>();
}
// Feature flags with string literal argument
{
// These should all type-check correctly
feature("lowercase");
feature("UPPERCASE");
feature("Mixed_Case_123");
feature("with-dashes");
feature("with.dots");
feature("with:colons");
}
// Feature flags in conditional type narrowing
{
function conditionalLogic(): string {
if (feature("EXPERIMENTAL")) {
return "experimental path";
}
return "stable path";
}
expectType(conditionalLogic()).is<string>();
}
// Feature flags in switch statements (though typically used with if)
{
const flagValue = feature("MODE");
// The value is always boolean, so switch is unusual but valid
switch (flagValue) {
case true:
console.log("enabled");
break;
case false:
console.log("disabled");
break;
}
}
// Combining feature flags with other Bun.build options
{
Bun.build({
entrypoints: ["./index.ts"],
outdir: "./dist",
features: ["FEATURE_A", "FEATURE_B", "DEBUG"],
minify: feature("PRODUCTION"), // Can use feature() in build config too
});
}
// Feature flags in class contexts
{
class FeatureGatedClass {
isDebug = feature("DEBUG");
isProduction = feature("PRODUCTION");
getMode() {
return feature("VERBOSE") ? "verbose" : "normal";
}
}
const instance = new FeatureGatedClass();
expectType(instance.isDebug).is<boolean>();
expectType(instance.isProduction).is<boolean>();
expectType(instance.getMode()).is<string>();
}
// Feature flags with template literals
{
const message = `Debug mode: ${feature("DEBUG")}`;
expectType(message).is<string>();
}
// Feature flags stored in variables and reused
{
const isDebug = feature("DEBUG");
const isVerbose = feature("VERBOSE");
if (isDebug && isVerbose) {
console.log("Full debug output");
} else if (isDebug) {
console.log("Debug output");
}
expectType(isDebug).is<boolean>();
expectType(isVerbose).is<boolean>();
}
// Feature flags in async contexts
{
async function asyncFeatureCheck() {
if (feature("ASYNC_FEATURE")) {
return await Promise.resolve("async enabled");
}
return "async disabled";
}
expectType(asyncFeatureCheck()).is<Promise<string>>();
}
// Feature flags in generator functions
{
function* featureGenerator(): Generator<string, void, unknown> {
if (feature("GENERATOR_FEATURE")) {
yield "feature enabled";
}
yield "default";
}
const gen = featureGenerator();
expectType(gen).is<Generator<string, void, unknown>>();
}
// Import alias should also work
import { feature as checkFeature } from "bun:bundle";
{
const aliasResult = checkFeature("ALIASED_CHECK");
expectType(aliasResult).is<boolean>();
}
// Feature flags with complex boolean expressions
{
const complexCondition = (feature("A") && feature("B")) || (!feature("C") && (feature("D") || feature("E")));
expectType(complexCondition).is<boolean>();
}
// Feature flags for conditional exports pattern
{
const publicAPI = {
version: "1.0.0",
...(feature("INTERNAL") && { _internal: "secret" }),
};
// The spread with && can add properties conditionally
expectType(publicAPI.version).is<string>();
}
// Feature flags with nullish coalescing (edge case - boolean never nullish)
{
const withNullish = feature("FLAG") ?? false;
expectType(withNullish).is<boolean>();
}
// Error cases - these should produce type errors:
// @ts-expect-error - feature() requires exactly one argument
feature();
// @ts-expect-error - feature() requires a string argument
feature(123);
// @ts-expect-error - feature() requires a string argument
feature(true);
// @ts-expect-error - feature() requires a string argument
feature(null);
// @ts-expect-error - feature() requires a string argument
feature(undefined);
// @ts-expect-error - feature() doesn't accept multiple arguments
feature("A", "B");
// @ts-expect-error - feature() doesn't accept objects
feature({ flag: "DEBUG" });
// @ts-expect-error - feature() doesn't accept arrays
feature(["DEBUG"]);