mirror of
https://github.com/oven-sh/bun
synced 2026-02-03 15:38:46 +00:00
Compare commits
8 Commits
dylan/pyth
...
claude/man
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a588f8df86 | ||
|
|
180e5f438d | ||
|
|
db8305c446 | ||
|
|
6647525297 | ||
|
|
8aa27b2855 | ||
|
|
01e9aac2cb | ||
|
|
facc302bab | ||
|
|
94d3703d8b |
@@ -144,7 +144,7 @@ In Bun's CLI, simple boolean flags like `--minify` do not accept an argument. Ot
|
||||
| `logOverride` | n/a | Not supported |
|
||||
| `mainFields` | n/a | Not supported |
|
||||
| `mangleCache` | n/a | Not supported |
|
||||
| `mangleProps` | n/a | Not supported |
|
||||
| `mangleProps` | `mangleProps` | Supports regex pattern for property mangling. Use `--reserve-props` to exclude specific properties. |
|
||||
| `mangleQuoted` | n/a | Not supported |
|
||||
| `metafile` | n/a | Not supported |
|
||||
| `minify` | `minify` | In Bun, `minify` can be a boolean or an object.<br/><br/>`ts<br/>await Bun.build({<br/> entrypoints: ['./index.tsx'],<br/> // enable all minification<br/> minify: true<br/><br/> // granular options<br/> minify: {<br/> identifiers: true,<br/> syntax: true,<br/> whitespace: true<br/> }<br/>})<br/>` |
|
||||
|
||||
@@ -850,15 +850,15 @@ x
|
||||
|
||||
### Property mangling
|
||||
|
||||
**Mode:** `--minify-identifiers` (with configuration)
|
||||
**Mode:** `--mangle-props` (requires explicit pattern)
|
||||
|
||||
Renames object properties to shorter names when configured.
|
||||
Renames object properties to shorter names when configured with a regex pattern. See the [Property Mangling](#property-mangling) section below for detailed usage.
|
||||
|
||||
```ts Input
|
||||
obj.longPropertyName
|
||||
obj.privateValue_
|
||||
```
|
||||
|
||||
```js Output (with property mangling enabled)
|
||||
```js Output (with --mangle-props='_$')
|
||||
obj.a
|
||||
```
|
||||
|
||||
@@ -1244,6 +1244,99 @@ await Bun.build({
|
||||
|
||||
This preserves the `.name` property on functions and classes while still minifying the actual identifier names in the code.
|
||||
|
||||
## Property Mangling
|
||||
|
||||
Property mangling renames object properties to shorter names, providing additional size reduction beyond identifier minification. Unlike identifier minification which only renames local variables, property mangling can rename property accesses like `obj.longPropertyName` to `obj.a`.
|
||||
|
||||
**Important:** Property mangling is opt-in and requires specifying a regex pattern to match property names. This is because indiscriminate property mangling can break code that relies on property names at runtime (like serialization, reflection, or external APIs).
|
||||
|
||||
### CLI Usage
|
||||
|
||||
```bash
|
||||
# Mangle properties ending with underscore
|
||||
bun build ./index.ts --mangle-props='_$' --minify-syntax --outfile=out.js
|
||||
|
||||
# Mangle properties starting with underscore (private-like)
|
||||
bun build ./index.ts --mangle-props='^_' --minify-syntax --outfile=out.js
|
||||
|
||||
# Also mangle quoted properties and string literal accesses
|
||||
bun build ./index.ts --mangle-props='_$' --mangle-quoted --minify-syntax --outfile=out.js
|
||||
```
|
||||
|
||||
### JavaScript API
|
||||
|
||||
```ts
|
||||
await Bun.build({
|
||||
entrypoints: ["./index.ts"],
|
||||
outdir: "./out",
|
||||
minify: {
|
||||
syntax: true,
|
||||
},
|
||||
// Mangle properties matching this regex pattern
|
||||
mangleProps: /_$/,
|
||||
});
|
||||
```
|
||||
|
||||
### Example
|
||||
|
||||
```ts Input
|
||||
const obj = {
|
||||
publicData: 1, // Not mangled (no underscore suffix)
|
||||
privateValue_: 42, // Mangled (underscore suffix)
|
||||
secretKey_: "abc", // Mangled (underscore suffix)
|
||||
};
|
||||
console.log(obj.privateValue_, obj.secretKey_);
|
||||
```
|
||||
|
||||
```js Output (with --mangle-props='_$')
|
||||
const obj = {
|
||||
publicData: 1,
|
||||
a: 42,
|
||||
b: "abc",
|
||||
};
|
||||
console.log(obj.a, obj.b);
|
||||
```
|
||||
|
||||
### Common Patterns
|
||||
|
||||
**Suffix pattern (`_$`):** Properties ending with underscore. This is a common convention for "private" properties.
|
||||
|
||||
```ts
|
||||
const obj = {
|
||||
name_: "private", // → obj.a
|
||||
data_: [1, 2, 3], // → obj.b
|
||||
};
|
||||
```
|
||||
|
||||
**Prefix pattern (`^_`):** Properties starting with underscore. Another common private property convention.
|
||||
|
||||
```ts
|
||||
const obj = {
|
||||
_internal: true, // → obj.a
|
||||
_cache: {}, // → obj.b
|
||||
};
|
||||
```
|
||||
|
||||
### Reserved Properties
|
||||
|
||||
Certain properties are never mangled regardless of the pattern:
|
||||
|
||||
- Built-in properties: `__proto__`, `constructor`, `prototype`
|
||||
- JavaScript standard methods and properties
|
||||
|
||||
### Cross-file Consistency
|
||||
|
||||
Property names are consistently mangled across all files in a bundle. If `secret_` appears in multiple files, it will be mangled to the same short name everywhere, ensuring correct behavior:
|
||||
|
||||
```ts
|
||||
// file1.ts
|
||||
export const config = { secret_: 42 };
|
||||
|
||||
// file2.ts
|
||||
import { config } from "./file1";
|
||||
console.log(config.secret_); // Works correctly - same mangled name
|
||||
```
|
||||
|
||||
## Combined Example
|
||||
|
||||
Using all three minification modes together:
|
||||
|
||||
52
packages/bun-types/bun.d.ts
vendored
52
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,30 @@ declare module "bun" {
|
||||
*/
|
||||
drop?: string[];
|
||||
|
||||
/**
|
||||
* Mangle property names matching the given regular expression pattern.
|
||||
*
|
||||
* Properties matching this pattern will be renamed to shorter names in the output.
|
||||
* This can significantly reduce bundle size but requires careful use to avoid
|
||||
* breaking code that relies on property name strings.
|
||||
*
|
||||
* Common patterns:
|
||||
* - `/_$/` - mangle properties ending with underscore (e.g., `foo_`)
|
||||
* - `/^_/` - mangle properties starting with underscore (e.g., `_private`)
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* await Bun.build({
|
||||
* entrypoints: ['./src/index.ts'],
|
||||
* mangleProps: /_$/, // mangle properties ending with _
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* Note: Built-in properties like `__proto__`, `constructor`, and `prototype`
|
||||
* are never mangled regardless of the pattern.
|
||||
*/
|
||||
mangleProps?: RegExp;
|
||||
|
||||
/**
|
||||
* - 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 +3340,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 +5674,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`
|
||||
|
||||
@@ -70,6 +70,15 @@ ts_enums: TsEnumsMap = .{},
|
||||
has_commonjs_export_names: bool = false,
|
||||
import_meta_ref: Ref = Ref.None,
|
||||
|
||||
/// Map from original property name to its symbol ref. Used for property mangling.
|
||||
/// Property names that match the mangle_props pattern get symbols created for them,
|
||||
/// and these symbols are renamed to shorter names during linking.
|
||||
mangled_props: MangledPropsMap = .{},
|
||||
|
||||
/// Map from original property name to true if the property should NOT be mangled.
|
||||
/// This is populated by the reserve_props pattern.
|
||||
reserved_props: ReservedPropsSet = .{},
|
||||
|
||||
pub const CommonJSNamedExport = struct {
|
||||
loc_ref: LocRef,
|
||||
needs_decl: bool = true,
|
||||
@@ -81,6 +90,11 @@ pub const NamedExports = bun.StringArrayHashMapUnmanaged(NamedExport);
|
||||
pub const ConstValuesMap = std.ArrayHashMapUnmanaged(Ref, Expr, RefHashCtx, false);
|
||||
pub const TsEnumsMap = std.ArrayHashMapUnmanaged(Ref, bun.StringHashMapUnmanaged(InlinedEnumValue), RefHashCtx, false);
|
||||
|
||||
/// Map from original property name to symbol ref for property mangling
|
||||
pub const MangledPropsMap = bun.StringArrayHashMapUnmanaged(Ref);
|
||||
/// Set of property names that should NOT be mangled (from reserve_props pattern)
|
||||
pub const ReservedPropsSet = bun.StringArrayHashMapUnmanaged(void);
|
||||
|
||||
pub fn fromParts(parts: []Part) Ast {
|
||||
return Ast{
|
||||
.parts = Part.List.fromOwnedSlice(parts),
|
||||
|
||||
@@ -50,6 +50,9 @@ target: bun.options.Target = .browser,
|
||||
// const_values: ConstValuesMap = .{},
|
||||
ts_enums: Ast.TsEnumsMap = .{},
|
||||
|
||||
/// Map from original property name to its symbol ref. Used for property mangling.
|
||||
mangled_props: Ast.MangledPropsMap = .{},
|
||||
|
||||
flags: BundledAst.Flags = .{},
|
||||
|
||||
pub const Flags = packed struct(u8) {
|
||||
@@ -108,6 +111,8 @@ pub fn toAST(this: *const BundledAst) Ast {
|
||||
// .const_values = this.const_values,
|
||||
.ts_enums = this.ts_enums,
|
||||
|
||||
.mangled_props = this.mangled_props,
|
||||
|
||||
.uses_exports_ref = this.flags.uses_exports_ref,
|
||||
.uses_module_ref = this.flags.uses_module_ref,
|
||||
// .uses_require_ref = ast.uses_require_ref,
|
||||
@@ -158,6 +163,8 @@ pub fn init(ast: Ast) BundledAst {
|
||||
// .const_values = ast.const_values,
|
||||
.ts_enums = ast.ts_enums,
|
||||
|
||||
.mangled_props = ast.mangled_props,
|
||||
|
||||
.flags = .{
|
||||
.uses_exports_ref = ast.uses_exports_ref,
|
||||
.uses_module_ref = ast.uses_module_ref,
|
||||
@@ -203,6 +210,7 @@ pub fn addUrlForCss(
|
||||
|
||||
pub const CommonJSNamedExports = Ast.CommonJSNamedExports;
|
||||
pub const ConstValuesMap = Ast.ConstValuesMap;
|
||||
pub const MangledPropsMap = Ast.MangledPropsMap;
|
||||
pub const NamedExports = Ast.NamedExports;
|
||||
pub const NamedImports = Ast.NamedImports;
|
||||
pub const TopLevelSymbolToParts = Ast.TopLevelSymbolToParts;
|
||||
|
||||
@@ -292,6 +292,12 @@ pub fn NewParser_(
|
||||
|
||||
jest: Jest = .{},
|
||||
|
||||
/// Map from property name to symbol ref for property mangling.
|
||||
/// Only populated when options.mangle_props is set.
|
||||
mangled_props: js_ast.Ast.MangledPropsMap = .{},
|
||||
/// Set of property names that should NOT be mangled.
|
||||
reserved_props: js_ast.Ast.ReservedPropsSet = .{},
|
||||
|
||||
// Imports (both ES6 and CommonJS) are tracked at the top level
|
||||
import_records: ImportRecordList,
|
||||
import_records_for_current_part: List(u32) = .{},
|
||||
@@ -2894,6 +2900,75 @@ pub fn NewParser_(
|
||||
};
|
||||
}
|
||||
|
||||
/// Permanent reserved properties that must NEVER be mangled due to JavaScript semantics.
|
||||
/// These properties have special meaning in the language.
|
||||
const permanent_reserved_props = bun.ComptimeStringMap(void, .{
|
||||
.{ "__proto__", {} },
|
||||
.{ "constructor", {} },
|
||||
.{ "prototype", {} },
|
||||
});
|
||||
|
||||
/// Check if a property name should be mangled based on the mangle_props regex.
|
||||
/// Returns true if the property matches the mangle pattern and is not reserved.
|
||||
pub fn isMangledProp(p: *P, name: []const u8) bool {
|
||||
// If mangle_props is not enabled, nothing is mangled
|
||||
const mangle_regex = p.options.mangle_props orelse return false;
|
||||
|
||||
// Check permanent reserved properties
|
||||
if (permanent_reserved_props.has(name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if already reserved
|
||||
if (p.reserved_props.contains(name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create bun.String once for all regex checks
|
||||
const name_str = bun.String.borrowUTF8(name);
|
||||
|
||||
// Check if name matches the reserve_props pattern
|
||||
if (p.options.reserve_props) |reserve_regex| {
|
||||
if (reserve_regex.matches(name_str)) {
|
||||
// Add to reserved set so we don't check again
|
||||
p.reserved_props.put(p.allocator, name, {}) catch {};
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if name matches the mangle_props pattern
|
||||
return mangle_regex.matches(name_str);
|
||||
}
|
||||
|
||||
/// Get or create a symbol for a mangled property name.
|
||||
/// The symbol is stored in mangled_props map for later use during linking.
|
||||
pub fn symbolForMangledProp(p: *P, name: []const u8) !Ref {
|
||||
// During substitution revisits, return existing symbol without modifications
|
||||
// to avoid double-counting (mirrors recordUsage behavior)
|
||||
if (p.is_revisit_for_substitution) {
|
||||
if (p.mangled_props.get(name)) |existing_ref| {
|
||||
return existing_ref;
|
||||
}
|
||||
}
|
||||
|
||||
// Get or create a symbol for this property
|
||||
const gop = try p.mangled_props.getOrPut(p.allocator, name);
|
||||
const ref = if (gop.found_existing)
|
||||
gop.value_ptr.*
|
||||
else blk: {
|
||||
const new_ref = try p.newSymbol(.mangled_prop, name);
|
||||
gop.value_ptr.* = new_ref;
|
||||
break :blk new_ref;
|
||||
};
|
||||
|
||||
// Only increment use count if not in dead code path and not revisiting
|
||||
if (!p.is_control_flow_dead and !p.is_revisit_for_substitution) {
|
||||
p.symbols.items[ref.innerIndex()].use_count_estimate += 1;
|
||||
}
|
||||
|
||||
return ref;
|
||||
}
|
||||
|
||||
pub fn defaultNameForExpr(p: *P, expr: Expr, loc: logger.Loc) LocRef {
|
||||
switch (expr.data) {
|
||||
.e_function => |func_container| {
|
||||
@@ -6558,6 +6633,10 @@ pub fn NewParser_(
|
||||
.symbols = js_ast.Symbol.List.moveFromList(&p.symbols),
|
||||
.parts = bun.BabyList(js_ast.Part).moveFromList(parts),
|
||||
.import_records = ImportRecord.List.moveFromList(&p.import_records),
|
||||
|
||||
// Property mangling data
|
||||
.mangled_props = p.mangled_props,
|
||||
.reserved_props = p.reserved_props,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,11 @@ pub const Parser = struct {
|
||||
bundle: bool = false,
|
||||
package_version: string = "",
|
||||
|
||||
/// Regex pattern for property names to mangle
|
||||
mangle_props: ?*bun.jsc.RegularExpression = null,
|
||||
/// Regex pattern for property names to exclude from mangling
|
||||
reserve_props: ?*bun.jsc.RegularExpression = null,
|
||||
|
||||
macro_context: *MacroContextType() = undefined,
|
||||
|
||||
warn_about_unbundled_modules: bool = true,
|
||||
|
||||
@@ -179,7 +179,7 @@ pub fn slotNamespace(this: *const Symbol) SlotNamespace {
|
||||
}
|
||||
|
||||
return switch (kind) {
|
||||
// .mangled_prop => .mangled_prop,
|
||||
.mangled_prop => .mangled_prop,
|
||||
.label => .label,
|
||||
else => .default,
|
||||
};
|
||||
@@ -270,6 +270,11 @@ pub const Kind = enum {
|
||||
// CSS identifiers that are renamed to be unique to the file they are in
|
||||
local_css,
|
||||
|
||||
/// A symbol used for property name mangling. These symbols are created
|
||||
/// for property names that match the mangle props pattern, and are
|
||||
/// renamed during linking to shorter names.
|
||||
mangled_prop,
|
||||
|
||||
/// This annotates all other symbols that don't have special behavior.
|
||||
other,
|
||||
|
||||
|
||||
@@ -917,6 +917,23 @@ pub fn VisitExpr(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Property mangling: Convert unquoted property accesses to mangled symbols
|
||||
// This converts "obj.foo_" to effectively "obj[mangledSymbol]" where the
|
||||
// mangled symbol will be renamed to a short name during linking.
|
||||
if (p.isMangledProp(e_.name)) {
|
||||
if (p.symbolForMangledProp(e_.name)) |ref| {
|
||||
return p.newExpr(
|
||||
E.Index{
|
||||
.target = e_.target,
|
||||
.index = p.newExpr(E.NameOfSymbol{ .ref = ref }, e_.name_loc),
|
||||
.optional_chain = e_.optional_chain,
|
||||
},
|
||||
expr.loc,
|
||||
);
|
||||
} else |_| {}
|
||||
}
|
||||
|
||||
return expr;
|
||||
}
|
||||
pub fn e_if(p: *P, expr: Expr, _: ExprIn) Expr {
|
||||
@@ -1049,7 +1066,7 @@ pub fn VisitExpr(
|
||||
for (e_.properties.slice()) |*property| {
|
||||
if (property.kind != .spread) {
|
||||
property.key = p.visitExpr(property.key orelse Output.panic("Expected property key", .{}));
|
||||
const key = property.key.?;
|
||||
var key = property.key.?;
|
||||
// Forbid duplicate "__proto__" properties according to the specification
|
||||
if (!property.flags.contains(.is_computed) and
|
||||
!property.flags.contains(.was_shorthand) and
|
||||
@@ -1068,6 +1085,19 @@ pub fn VisitExpr(
|
||||
}
|
||||
has_proto = true;
|
||||
}
|
||||
|
||||
// Property mangling for object literal keys: Convert unquoted property keys
|
||||
// to mangled symbols. This transforms { foo_: 1 } to { [mangledSymbol]: 1 }.
|
||||
if (!property.flags.contains(.is_computed) and key.data.isStringValue()) {
|
||||
const name = key.data.e_string.slice(p.allocator);
|
||||
if (p.isMangledProp(name)) {
|
||||
if (p.symbolForMangledProp(name)) |ref| {
|
||||
key = p.newExpr(E.NameOfSymbol{ .ref = ref }, key.loc);
|
||||
property.key = key;
|
||||
property.flags.insert(.is_computed);
|
||||
} else |_| {}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
has_spread = true;
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ pub const JSBundler = struct {
|
||||
force_node_env: options.BundleOptions.ForceNodeEnv = .unspecified,
|
||||
code_splitting: bool = false,
|
||||
minify: Minify = .{},
|
||||
mangle_props: MangleProps = .{},
|
||||
no_macros: bool = false,
|
||||
ignore_dce_annotations: bool = false,
|
||||
emit_dce_annotations: ?bool = null,
|
||||
@@ -478,6 +479,61 @@ pub const JSBundler = struct {
|
||||
}
|
||||
}
|
||||
|
||||
// Parse mangleProps option
|
||||
// Can be a string (regex pattern), a RegExp object, or an object with props, reserve, and quoted fields
|
||||
if (try config.getTruthy(globalThis, "mangleProps")) |mangle_props| {
|
||||
if (mangle_props.isString()) {
|
||||
var slice = try mangle_props.toSliceOrNull(globalThis);
|
||||
defer slice.deinit();
|
||||
try this.mangle_props.props.appendSliceExact(slice.slice());
|
||||
} else if (mangle_props.isRegExp()) {
|
||||
// Extract the source pattern from the RegExp object
|
||||
// Use getTruthyComptime to get accessor properties like 'source'
|
||||
if (try mangle_props.getTruthyComptime(globalThis, "source")) |source_value| {
|
||||
if (source_value.isString()) {
|
||||
var slice = try source_value.toSliceOrNull(globalThis);
|
||||
defer slice.deinit();
|
||||
try this.mangle_props.props.appendSliceExact(slice.slice());
|
||||
}
|
||||
}
|
||||
} else if (mangle_props.isObject()) {
|
||||
// Handle props field - can be string or RegExp
|
||||
if (try mangle_props.getTruthy(globalThis, "props")) |props_value| {
|
||||
if (props_value.isString()) {
|
||||
var slice = try props_value.toSliceOrNull(globalThis);
|
||||
defer slice.deinit();
|
||||
try this.mangle_props.props.appendSliceExact(slice.slice());
|
||||
} else if (props_value.isRegExp()) {
|
||||
if (try props_value.getTruthyComptime(globalThis, "source")) |source_value| {
|
||||
if (source_value.isString()) {
|
||||
var slice = try source_value.toSliceOrNull(globalThis);
|
||||
defer slice.deinit();
|
||||
try this.mangle_props.props.appendSliceExact(slice.slice());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Handle reserve field - can be string or RegExp
|
||||
if (try mangle_props.getTruthy(globalThis, "reserve")) |reserve_value| {
|
||||
if (reserve_value.isString()) {
|
||||
var slice = try reserve_value.toSliceOrNull(globalThis);
|
||||
defer slice.deinit();
|
||||
try this.mangle_props.reserve.appendSliceExact(slice.slice());
|
||||
} else if (reserve_value.isRegExp()) {
|
||||
if (try reserve_value.getTruthyComptime(globalThis, "source")) |source_value| {
|
||||
if (source_value.isString()) {
|
||||
var slice = try source_value.toSliceOrNull(globalThis);
|
||||
defer slice.deinit();
|
||||
try this.mangle_props.reserve.appendSliceExact(slice.slice());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return globalThis.throwInvalidArguments("Expected mangleProps to be a string (regex pattern), a RegExp, or an object", .{});
|
||||
}
|
||||
}
|
||||
|
||||
if (try config.getArray(globalThis, "entrypoints") orelse try config.getArray(globalThis, "entryPoints")) |entry_points| {
|
||||
var iter = try entry_points.arrayIterator(globalThis);
|
||||
while (try iter.next()) |entry_point| {
|
||||
@@ -761,6 +817,22 @@ pub const JSBundler = struct {
|
||||
keep_names: bool = false,
|
||||
};
|
||||
|
||||
pub const MangleProps = struct {
|
||||
/// Regex pattern string for property names to mangle
|
||||
props: OwnedString = OwnedString.initEmpty(bun.default_allocator),
|
||||
/// Regex pattern string for property names to exclude from mangling
|
||||
reserve: OwnedString = OwnedString.initEmpty(bun.default_allocator),
|
||||
|
||||
pub fn deinit(self: *MangleProps) void {
|
||||
self.props.deinit();
|
||||
self.reserve.deinit();
|
||||
}
|
||||
|
||||
pub fn hasPattern(self: *const MangleProps) bool {
|
||||
return self.props.list.items.len > 0;
|
||||
}
|
||||
};
|
||||
|
||||
pub const Serve = struct {
|
||||
handler_path: OwnedString = OwnedString.initEmpty(bun.default_allocator),
|
||||
prefix: OwnedString = OwnedString.initEmpty(bun.default_allocator),
|
||||
@@ -811,6 +883,7 @@ pub const JSBundler = struct {
|
||||
self.env_prefix.deinit();
|
||||
self.footer.deinit();
|
||||
self.tsconfig_override.deinit();
|
||||
self.mangle_props.deinit();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1466,6 +1466,115 @@ pub const LinkerContext = struct {
|
||||
}
|
||||
}
|
||||
|
||||
/// Collect all mangled property symbols from all files and assign them
|
||||
/// short names (a, b, c, ...). This is similar to how esbuild handles
|
||||
/// property name mangling in the linker phase.
|
||||
pub fn mangleJsProps(c: *LinkerContext) !void {
|
||||
const all_mangled_props: []const JSAst.MangledPropsMap = c.graph.ast.items(.mangled_props);
|
||||
|
||||
// Collect all mangled property symbols across all files
|
||||
// We merge symbols with the same name by linking them together,
|
||||
// so the printer can follow the link chain to find the canonical ref
|
||||
var merged_props = bun.StringArrayHashMap(Ref).init(c.allocator());
|
||||
defer merged_props.deinit();
|
||||
|
||||
// Count of property usages for sorting (most used gets shortest name)
|
||||
var usage_counts = bun.StringArrayHashMap(u32).init(c.allocator());
|
||||
defer usage_counts.deinit();
|
||||
|
||||
for (all_mangled_props) |mangled_props| {
|
||||
// Get mangled props from this file's AST
|
||||
for (mangled_props.keys(), mangled_props.values()) |name, ref| {
|
||||
const entry = try merged_props.getOrPut(name);
|
||||
if (entry.found_existing) {
|
||||
// Link this symbol to the canonical one for this property name
|
||||
// The printer uses symbols.follow() to resolve the link chain
|
||||
// IMPORTANT: Use c.graph.symbols (not c.graph.ast.items(.symbols))
|
||||
// because the renamer uses c.graph.symbols which is a cloned copy
|
||||
const canonical_ref = entry.value_ptr.*;
|
||||
const symbol = c.graph.symbols.get(ref).?;
|
||||
symbol.link = canonical_ref;
|
||||
} else {
|
||||
// First occurrence - this becomes the canonical ref
|
||||
entry.value_ptr.* = ref;
|
||||
}
|
||||
|
||||
// Track usage counts
|
||||
const symbol = c.graph.symbols.getConst(ref).?;
|
||||
const count_entry = try usage_counts.getOrPut(name);
|
||||
if (count_entry.found_existing) {
|
||||
count_entry.value_ptr.* += symbol.use_count_estimate;
|
||||
} else {
|
||||
count_entry.value_ptr.* = symbol.use_count_estimate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (merged_props.count() == 0) return;
|
||||
|
||||
// Sort properties by usage count (descending) so most used get shortest names
|
||||
const PropWithCount = struct {
|
||||
name: []const u8,
|
||||
ref: Ref,
|
||||
count: u32,
|
||||
};
|
||||
|
||||
var props_list = try std.ArrayList(PropWithCount).initCapacity(c.allocator(), merged_props.count());
|
||||
defer props_list.deinit(c.allocator());
|
||||
|
||||
for (merged_props.keys(), merged_props.values()) |name, ref| {
|
||||
const count = usage_counts.get(name) orelse 0;
|
||||
props_list.appendAssumeCapacity(.{
|
||||
.name = name,
|
||||
.ref = ref,
|
||||
.count = count,
|
||||
});
|
||||
}
|
||||
|
||||
// Sort by count descending
|
||||
std.mem.sort(PropWithCount, props_list.items, {}, struct {
|
||||
fn lessThan(_: void, a: PropWithCount, b: PropWithCount) bool {
|
||||
return a.count > b.count;
|
||||
}
|
||||
}.lessThan);
|
||||
|
||||
// Assign short names (a, b, c, ..., aa, ab, ...)
|
||||
var reserved_names = bun.StringHashMap(void).init(c.allocator());
|
||||
defer reserved_names.deinit();
|
||||
|
||||
// Reserve JavaScript keywords and strict mode reserved words
|
||||
// Use the tables from js_lexer to avoid duplication
|
||||
for (lex.Keywords.kvs) |kv| {
|
||||
try reserved_names.put(kv.key, {});
|
||||
}
|
||||
for (lex.StrictModeReservedWords.kvs) |kv| {
|
||||
try reserved_names.put(kv.key, {});
|
||||
}
|
||||
|
||||
// Also reserve any existing property names in the merged set that don't match
|
||||
// the mangle pattern (these might be used alongside mangled props)
|
||||
for (merged_props.keys()) |name| {
|
||||
try reserved_names.put(name, {});
|
||||
}
|
||||
|
||||
var name_index: i32 = 0;
|
||||
for (props_list.items) |prop| {
|
||||
// Generate a short name, avoiding reserved names
|
||||
var mangled_name: []const u8 = undefined;
|
||||
while (true) {
|
||||
mangled_name = try js_ast.NameMinifier.defaultNumberToMinifiedName(c.allocator(), name_index);
|
||||
name_index += 1;
|
||||
if (!reserved_names.contains(mangled_name)) break;
|
||||
}
|
||||
|
||||
// Store the mangled name for the canonical ref only
|
||||
// Other refs with the same property name are linked to this one,
|
||||
// so the printer will follow the link and find this mapping
|
||||
try c.mangled_props.put(c.allocator(), prop.ref, mangled_name);
|
||||
try reserved_names.put(mangled_name, {});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn appendIsolatedHashesForImportedChunks(
|
||||
c: *LinkerContext,
|
||||
hash: *ContentHasher,
|
||||
|
||||
@@ -1199,6 +1199,10 @@ fn runWithSourceCode(
|
||||
|
||||
opts.ignore_dce_annotations = transpiler.options.ignore_dce_annotations and !source.index.isRuntime();
|
||||
|
||||
// Set up mangle props options from transpiler
|
||||
opts.mangle_props = transpiler.options.mangle_props;
|
||||
opts.reserve_props = transpiler.options.reserve_props;
|
||||
|
||||
// For files that are not user-specified entrypoints, set `import.meta.main` to `false`.
|
||||
// Entrypoints will have `import.meta.main` set as "unknown", unless we use `--compile`,
|
||||
// in which we inline `true`.
|
||||
|
||||
@@ -1903,6 +1903,36 @@ pub const BundleV2 = struct {
|
||||
transpiler.options.banner = config.banner.slice();
|
||||
transpiler.options.footer = config.footer.slice();
|
||||
|
||||
// Set up mangle props options
|
||||
if (config.mangle_props.hasPattern()) {
|
||||
bun.jsc.initialize(false);
|
||||
const pattern = bun.String.borrowUTF8(config.mangle_props.props.list.items);
|
||||
transpiler.options.mangle_props = bun.jsc.RegularExpression.init(pattern, .none) catch |err| blk: {
|
||||
try completion.log.addErrorFmt(
|
||||
null,
|
||||
Logger.Loc.Empty,
|
||||
alloc,
|
||||
"Invalid mangleProps regex pattern: {s} ({})",
|
||||
.{ config.mangle_props.props.list.items, err },
|
||||
);
|
||||
break :blk null;
|
||||
};
|
||||
|
||||
if (config.mangle_props.reserve.list.items.len > 0) {
|
||||
const reserve_pattern = bun.String.borrowUTF8(config.mangle_props.reserve.list.items);
|
||||
transpiler.options.reserve_props = bun.jsc.RegularExpression.init(reserve_pattern, .none) catch |err| blk: {
|
||||
try completion.log.addErrorFmt(
|
||||
null,
|
||||
Logger.Loc.Empty,
|
||||
alloc,
|
||||
"Invalid reserveProps regex pattern: {s} ({})",
|
||||
.{ config.mangle_props.reserve.list.items, err },
|
||||
);
|
||||
break :blk null;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (transpiler.options.compile) {
|
||||
// Emitting DCE annotations is nonsensical in --compile.
|
||||
transpiler.options.emit_dce_annotations = false;
|
||||
|
||||
@@ -7,6 +7,7 @@ pub fn generateChunksInParallel(
|
||||
defer trace.end();
|
||||
|
||||
c.mangleLocalCss();
|
||||
try c.mangleJsProps();
|
||||
|
||||
var has_js_chunk = false;
|
||||
var has_css_chunk = false;
|
||||
|
||||
@@ -438,6 +438,8 @@ pub const Command = struct {
|
||||
minify_whitespace: bool = false,
|
||||
minify_identifiers: bool = false,
|
||||
keep_names: bool = false,
|
||||
mangle_props: []const u8 = "",
|
||||
reserve_props: []const u8 = "",
|
||||
ignore_dce_annotations: bool = false,
|
||||
emit_dce_annotations: bool = true,
|
||||
output_format: options.Format = .esm,
|
||||
|
||||
@@ -179,6 +179,8 @@ pub const build_only_params = [_]ParamType{
|
||||
clap.parseParam("--minify-whitespace Minify whitespace") catch unreachable,
|
||||
clap.parseParam("--minify-identifiers Minify identifiers") catch unreachable,
|
||||
clap.parseParam("--keep-names Preserve original function and class names when minifying") catch unreachable,
|
||||
clap.parseParam("--mangle-props <STR> Mangle property names matching regex pattern (e.g. '^_' for underscore-prefixed)") catch unreachable,
|
||||
clap.parseParam("--reserve-props <STR> Exclude property names matching regex pattern from mangling") catch unreachable,
|
||||
clap.parseParam("--css-chunking Chunk CSS files together to reduce duplicated CSS loaded in a browser. Only has an effect when multiple entrypoints import CSS") catch unreachable,
|
||||
clap.parseParam("--dump-environment-variables") catch unreachable,
|
||||
clap.parseParam("--conditions <STR>... Pass custom conditions to resolve") catch unreachable,
|
||||
@@ -925,6 +927,37 @@ pub fn parse(allocator: std.mem.Allocator, ctx: Command.Context, comptime cmd: C
|
||||
ctx.bundler_options.minify_identifiers = minify_flag or args.flag("--minify-identifiers");
|
||||
ctx.bundler_options.keep_names = args.flag("--keep-names");
|
||||
|
||||
if (args.option("--mangle-props")) |mangle_props| {
|
||||
// Validate regex pattern at parse time
|
||||
_ = RegularExpression.init(bun.String.fromBytes(mangle_props), RegularExpression.Flags.none) catch {
|
||||
Output.prettyErrorln(
|
||||
"<r><red>error<r>: --mangle-props expects a valid regular expression but received {f}",
|
||||
.{
|
||||
bun.fmt.QuotedFormatter{
|
||||
.text = mangle_props,
|
||||
},
|
||||
},
|
||||
);
|
||||
Global.exit(1);
|
||||
};
|
||||
ctx.bundler_options.mangle_props = mangle_props;
|
||||
}
|
||||
if (args.option("--reserve-props")) |reserve_props| {
|
||||
// Validate regex pattern at parse time
|
||||
_ = RegularExpression.init(bun.String.fromBytes(reserve_props), RegularExpression.Flags.none) catch {
|
||||
Output.prettyErrorln(
|
||||
"<r><red>error<r>: --reserve-props expects a valid regular expression but received {f}",
|
||||
.{
|
||||
bun.fmt.QuotedFormatter{
|
||||
.text = reserve_props,
|
||||
},
|
||||
},
|
||||
);
|
||||
Global.exit(1);
|
||||
};
|
||||
ctx.bundler_options.reserve_props = reserve_props;
|
||||
}
|
||||
|
||||
ctx.bundler_options.css_chunking = args.flag("--css-chunking");
|
||||
|
||||
ctx.bundler_options.emit_dce_annotations = args.flag("--emit-dce-annotations") or
|
||||
|
||||
@@ -79,6 +79,28 @@ pub const BuildCommand = struct {
|
||||
this_transpiler.options.emit_dce_annotations = ctx.bundler_options.emit_dce_annotations;
|
||||
this_transpiler.options.ignore_dce_annotations = ctx.bundler_options.ignore_dce_annotations;
|
||||
|
||||
// Compile mangle props regex patterns
|
||||
// Initialize JSC once if either pattern is present (needed for Yarr regex engine)
|
||||
if (ctx.bundler_options.mangle_props.len > 0 or ctx.bundler_options.reserve_props.len > 0) {
|
||||
bun.jsc.initialize(false);
|
||||
}
|
||||
if (ctx.bundler_options.mangle_props.len > 0) {
|
||||
const pattern = bun.String.borrowUTF8(ctx.bundler_options.mangle_props);
|
||||
this_transpiler.options.mangle_props = bun.jsc.RegularExpression.init(pattern, .none) catch {
|
||||
Output.prettyErrorln("<r><red>error<r><d>:<r> invalid --mangle-props regex pattern: \"{s}\"", .{ctx.bundler_options.mangle_props});
|
||||
Global.exit(1);
|
||||
return;
|
||||
};
|
||||
}
|
||||
if (ctx.bundler_options.reserve_props.len > 0) {
|
||||
const pattern = bun.String.borrowUTF8(ctx.bundler_options.reserve_props);
|
||||
this_transpiler.options.reserve_props = bun.jsc.RegularExpression.init(pattern, .none) catch {
|
||||
Output.prettyErrorln("<r><red>error<r><d>:<r> invalid --reserve-props regex pattern: \"{s}\"", .{ctx.bundler_options.reserve_props});
|
||||
Global.exit(1);
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
this_transpiler.options.banner = ctx.bundler_options.banner;
|
||||
this_transpiler.options.footer = ctx.bundler_options.footer;
|
||||
this_transpiler.options.drop = ctx.args.drop;
|
||||
|
||||
@@ -1800,6 +1800,13 @@ pub const BundleOptions = struct {
|
||||
dead_code_elimination: bool = true,
|
||||
css_chunking: bool,
|
||||
|
||||
/// Regex pattern for property names to mangle. Properties matching this pattern
|
||||
/// will be renamed to shorter names (e.g., "^_" matches properties starting with underscore).
|
||||
mangle_props: ?*bun.jsc.RegularExpression = null,
|
||||
/// Regex pattern for property names to exclude from mangling. Properties matching
|
||||
/// this pattern will NOT be mangled even if they match mangle_props.
|
||||
reserve_props: ?*bun.jsc.RegularExpression = null,
|
||||
|
||||
ignore_dce_annotations: bool = false,
|
||||
emit_dce_annotations: bool = false,
|
||||
bytecode: bool = false,
|
||||
|
||||
1028
test/bundler/bundler_mangle_props.test.ts
Normal file
1028
test/bundler/bundler_mangle_props.test.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -6063,7 +6063,6 @@ describe("bundler", () => {
|
||||
`,
|
||||
},
|
||||
mangleProps: /_/,
|
||||
mangleQuoted: false,
|
||||
});
|
||||
itBundled("default/MangleNoQuotedPropsMinifySyntax", {
|
||||
// GENERATED
|
||||
@@ -6084,92 +6083,11 @@ describe("bundler", () => {
|
||||
`,
|
||||
},
|
||||
mangleProps: /_/,
|
||||
mangleQuoted: false,
|
||||
minifySyntax: true,
|
||||
});
|
||||
itBundled("default/MangleQuotedProps", {
|
||||
files: {
|
||||
"/keep.js": /* js */ `
|
||||
foo("_keepThisProperty");
|
||||
foo((x, "_keepThisProperty"));
|
||||
foo(x ? "_keepThisProperty" : "_keepThisPropertyToo");
|
||||
x[foo("_keepThisProperty")];
|
||||
x?.[foo("_keepThisProperty")];
|
||||
({ [foo("_keepThisProperty")]: x });
|
||||
(class { [foo("_keepThisProperty")] = x });
|
||||
var { [foo("_keepThisProperty")]: x } = y;
|
||||
foo("_keepThisProperty") in x;
|
||||
`,
|
||||
"/mangle.js": /* js */ `
|
||||
x['_mangleThis'];
|
||||
x?.['_mangleThis'];
|
||||
x[y ? '_mangleThis' : z];
|
||||
x?.[y ? '_mangleThis' : z];
|
||||
x[y ? z : '_mangleThis'];
|
||||
x?.[y ? z : '_mangleThis'];
|
||||
x[y, '_mangleThis'];
|
||||
x?.[y, '_mangleThis'];
|
||||
({ '_mangleThis': x });
|
||||
({ ['_mangleThis']: x });
|
||||
({ [(y, '_mangleThis')]: x });
|
||||
(class { '_mangleThis' = x });
|
||||
(class { ['_mangleThis'] = x });
|
||||
(class { [(y, '_mangleThis')] = x });
|
||||
var { '_mangleThis': x } = y;
|
||||
var { ['_mangleThis']: x } = y;
|
||||
var { [(z, '_mangleThis')]: x } = y;
|
||||
'_mangleThis' in x;
|
||||
(y ? '_mangleThis' : z) in x;
|
||||
(y ? z : '_mangleThis') in x;
|
||||
(y, '_mangleThis') in x;
|
||||
`,
|
||||
},
|
||||
entryPoints: ["/keep.js", "/mangle.js"],
|
||||
mangleProps: /_/,
|
||||
mangleQuoted: true,
|
||||
});
|
||||
itBundled("default/MangleQuotedPropsMinifySyntax", {
|
||||
files: {
|
||||
"/keep.js": /* js */ `
|
||||
foo("_keepThisProperty");
|
||||
foo((x, "_keepThisProperty"));
|
||||
foo(x ? "_keepThisProperty" : "_keepThisPropertyToo");
|
||||
x[foo("_keepThisProperty")];
|
||||
x?.[foo("_keepThisProperty")];
|
||||
({ [foo("_keepThisProperty")]: x });
|
||||
(class { [foo("_keepThisProperty")] = x });
|
||||
var { [foo("_keepThisProperty")]: x } = y;
|
||||
foo("_keepThisProperty") in x;
|
||||
`,
|
||||
"/mangle.js": /* js */ `
|
||||
x['_mangleThis'];
|
||||
x?.['_mangleThis'];
|
||||
x[y ? '_mangleThis' : z];
|
||||
x?.[y ? '_mangleThis' : z];
|
||||
x[y ? z : '_mangleThis'];
|
||||
x?.[y ? z : '_mangleThis'];
|
||||
x[y, '_mangleThis'];
|
||||
x?.[y, '_mangleThis'];
|
||||
({ '_mangleThis': x });
|
||||
({ ['_mangleThis']: x });
|
||||
({ [(y, '_mangleThis')]: x });
|
||||
(class { '_mangleThis' = x });
|
||||
(class { ['_mangleThis'] = x });
|
||||
(class { [(y, '_mangleThis')] = x });
|
||||
var { '_mangleThis': x } = y;
|
||||
var { ['_mangleThis']: x } = y;
|
||||
var { [(z, '_mangleThis')]: x } = y;
|
||||
'_mangleThis' in x;
|
||||
(y ? '_mangleThis' : z) in x;
|
||||
(y ? z : '_mangleThis') in x;
|
||||
(y, '_mangleThis') in x;
|
||||
`,
|
||||
},
|
||||
entryPoints: ["/keep.js", "/mangle.js"],
|
||||
mangleProps: /_/,
|
||||
mangleQuoted: true,
|
||||
minifySyntax: true,
|
||||
});
|
||||
// mangleQuoted is not supported - these tests are skipped
|
||||
// itBundled("default/MangleQuotedProps", { ... });
|
||||
// itBundled("default/MangleQuotedPropsMinifySyntax", { ... });
|
||||
// we dont check debug messages
|
||||
// itBundled("default/IndirectRequireMessage", {
|
||||
// // GENERATED
|
||||
|
||||
@@ -201,7 +201,6 @@ export interface BundlerTestInput {
|
||||
legalComments?: "none" | "inline" | "eof" | "linked" | "external";
|
||||
loader?: Record<`.${string}`, Loader>;
|
||||
mangleProps?: RegExp;
|
||||
mangleQuoted?: boolean;
|
||||
mainFields?: string[];
|
||||
metafile?: boolean | string;
|
||||
minifyIdentifiers?: boolean;
|
||||
@@ -455,6 +454,7 @@ function expectBundled(
|
||||
legalComments,
|
||||
loader,
|
||||
mainFields,
|
||||
mangleProps,
|
||||
matchesReference,
|
||||
metafile,
|
||||
minifyIdentifiers,
|
||||
@@ -1113,6 +1113,7 @@ function expectBundled(
|
||||
define: define ?? {},
|
||||
throw: _throw ?? false,
|
||||
compile,
|
||||
mangleProps,
|
||||
jsx: jsx
|
||||
? {
|
||||
runtime: jsx.runtime,
|
||||
|
||||
Reference in New Issue
Block a user