mirror of
https://github.com/oven-sh/bun
synced 2026-02-21 00:02:19 +00:00
Compare commits
15 Commits
claude/fix
...
claude/bun
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08215d9d20 | ||
|
|
21d93e97fa | ||
|
|
7cdf07e36e | ||
|
|
f1789680b4 | ||
|
|
ca1bf28023 | ||
|
|
fd3daa2661 | ||
|
|
3fc2644bc2 | ||
|
|
7d74e8e02c | ||
|
|
daaa6a1c31 | ||
|
|
41d2ceb8d3 | ||
|
|
03275767b8 | ||
|
|
a489198dd5 | ||
|
|
555cd5808e | ||
|
|
f975a66cf3 | ||
|
|
4ee36d8d3c |
@@ -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` | |
|
||||
|
||||
@@ -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:
|
||||
|
||||
46
packages/bun-types/bun.d.ts
vendored
46
packages/bun-types/bun.d.ts
vendored
@@ -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
51
packages/bun-types/bundle.d.ts
vendored
Normal 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;
|
||||
}
|
||||
1
packages/bun-types/index.d.ts
vendored
1
packages/bun-types/index.d.ts
vendored
@@ -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" />
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
364
test/bundler/bundler_feature_flag.test.ts
Normal file
364
test/bundler/bundler_feature_flag.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -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: {
|
||||
|
||||
246
test/integration/bun-types/fixture/bundle.ts
Normal file
246
test/integration/bun-types/fixture/bundle.ts
Normal 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"]);
|
||||
Reference in New Issue
Block a user