Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
59fe054
feat(minify): optimize Error constructors by removing 'new' keyword
Sep 8, 2025
b5f92c9
feat(minify): convert Object/Array constructors to literals
Sep 8, 2025
30ebd57
test: update source map positions and file size for ReactSSR test
Sep 8, 2025
f3fd6ec
Merge branch 'main' into claude/minify-error-constructors
Sep 8, 2025
c30688d
[autofix.ci] apply automated fixes
autofix-ci[bot] Sep 8, 2025
69ca5ae
fix: address CodeRabbit feedback
Sep 8, 2025
8170f7d
[autofix.ci] apply automated fixes
autofix-ci[bot] Sep 8, 2025
852115e
refactor: extract callFromNew helper to reduce code duplication
Sep 8, 2025
9a381e5
Fix Array constructor optimization to preserve sparse array semantics
Sep 8, 2025
a0a8bbe
Fix: Don't apply minification to node_modules code at runtime
Sep 8, 2025
64bd06e
Fix test expectations for updated minification
Sep 8, 2025
b11b823
Fix
Sep 8, 2025
657ce27
[autofix.ci] apply automated fixes
autofix-ci[bot] Sep 8, 2025
a11cc56
Update inspect-error.test.js
Jarred-Sumner Sep 8, 2025
91153ba
Apply suggestion from @Jarred-Sumner
Jarred-Sumner Sep 8, 2025
943a112
Extend Array minification to handle object/array literals
Sep 8, 2025
d27a2fc
[autofix.ci] apply automated fixes
autofix-ci[bot] Sep 8, 2025
84ebe74
Remove RegExp optimization to preserve identity semantics
Sep 8, 2025
1237cc8
Fix tests
Jarred-Sumner Sep 8, 2025
db6e0ed
more minification
dylan-conway Sep 9, 2025
53ff1ed
[autofix.ci] apply automated fixes
autofix-ci[bot] Sep 9, 2025
e6f98ca
fix bug
dylan-conway Sep 9, 2025
ae2bf8a
revert
dylan-conway Sep 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 164 additions & 16 deletions src/ast/KnownGlobal.zig
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,166 @@ pub const KnownGlobal = enum {
Response,
TextEncoder,
TextDecoder,
Error,
TypeError,
SyntaxError,
RangeError,
ReferenceError,
EvalError,
URIError,
AggregateError,
Array,
Object,
Function,
RegExp,

pub const map = bun.ComptimeEnumMap(KnownGlobal);

pub noinline fn maybeMarkConstructorAsPure(noalias e: *E.New, symbols: []const Symbol) void {
const id = if (e.target.data == .e_identifier) e.target.data.e_identifier.ref else return;
inline fn callFromNew(e: *E.New, loc: logger.Loc) js_ast.Expr {
const call = E.Call{
.target = e.target,
.args = e.args,
.close_paren_loc = e.close_parens_loc,
.can_be_unwrapped_if_unused = e.can_be_unwrapped_if_unused,
};
return js_ast.Expr.init(E.Call, call, loc);
}

pub noinline fn minifyGlobalConstructor(allocator: std.mem.Allocator, noalias e: *E.New, symbols: []const Symbol, loc: logger.Loc, minify_whitespace: bool) ?js_ast.Expr {
const id = if (e.target.data == .e_identifier) e.target.data.e_identifier.ref else return null;
const symbol = &symbols[id.innerIndex()];
if (symbol.kind != .unbound)
return;
return null;

const constructor = map.get(symbol.original_name) orelse return null;

return switch (constructor) {
// Error constructors can be called without 'new' with identical behavior
.Error, .TypeError, .SyntaxError, .RangeError, .ReferenceError, .EvalError, .URIError, .AggregateError => {
// Convert `new Error(...)` to `Error(...)` to save bytes
return callFromNew(e, loc);
},

const constructor = map.get(symbol.original_name) orelse return;
.Object => {
const n = e.args.len;

switch (constructor) {
if (n == 0) {
// new Object() -> {}
return js_ast.Expr.init(E.Object, E.Object{}, loc);
}

if (n == 1) {
const arg = e.args.ptr[0];
switch (arg.data) {
.e_object, .e_array => {
// new Object({a: 1}) -> {a: 1}
// new Object([1, 2]) -> [1, 2]
return arg;
},
.e_null, .e_undefined => {
// new Object(null) -> {}
// new Object(undefined) -> {}
return js_ast.Expr.init(E.Object, E.Object{}, loc);
},
else => {},
}
}

// For other cases, just remove 'new'
return callFromNew(e, loc);
},

.Array => {
const n = e.args.len;

return switch (n) {
0 => {
// new Array() -> []
return js_ast.Expr.init(E.Array, E.Array{}, loc);
},
1 => {
// For single argument, only convert to literal if we're SURE it's not a number
const arg = e.args.ptr[0];

// Check if it's an object or array literal first
switch (arg.data) {
.e_object, .e_array => {
// new Array({}) -> [{}], new Array([1]) -> [[1]]
// These are definitely not numbers, safe to convert
return js_ast.Expr.init(E.Array, .{ .items = e.args }, loc);
},
else => {},
}

// For other types, check via knownPrimitive
const primitive = arg.knownPrimitive();
// Only convert if we know for certain it's not a number
// unknown could be a number at runtime, so we must preserve Array() call
switch (primitive) {
.null, .undefined, .boolean, .string, .bigint => {
// These are definitely not numbers, safe to convert
return js_ast.Expr.init(E.Array, .{ .items = e.args }, loc);
},
.number => {
const val = arg.data.e_number.value;
if (
// only want this with whitespace minification
minify_whitespace and
(val == 0 or
val == 1 or
val == 2 or
val == 3 or
val == 4 or
val == 5 or
val == 6 or
val == 7 or
val == 8 or
val == 9 or
val == 10))
{
const arg_loc = arg.loc;
var list = e.args.listManaged(allocator);
list.clearRetainingCapacity();
bun.handleOom(list.appendNTimes(js_ast.Expr{ .data = js_parser.Prefill.Data.EMissing, .loc = arg_loc }, @intFromFloat(val)));
return js_ast.Expr.init(E.Array, .{ .items = .fromList(list) }, loc);
}
return callFromNew(e, loc);
},
.unknown, .mixed => {
// Could be a number, preserve Array() call
return callFromNew(e, loc);
},
}
},
// > 1
else => {
// new Array(1, 2, 3) -> [1, 2, 3]
// But NOT new Array(3) which creates an array with 3 empty slots
return js_ast.Expr.init(E.Array, .{ .items = e.args }, loc);
},
};
},

.Function => {
// Just remove 'new' for Function
return callFromNew(e, loc);
},
.RegExp => {
// Don't optimize RegExp - the semantics are too complex:
// - new RegExp(re) creates a copy, but RegExp(re) returns the same instance
// - This affects object identity and lastIndex behavior
// - The difference only applies when flags are undefined
// Keep the original new RegExp() call to preserve correct semantics
return null;
},
.WeakSet, .WeakMap => {
const n = e.args.len;

if (n == 0) {
// "new WeakSet()" is pure
e.can_be_unwrapped_if_unused = .if_unused;

return;
return null;
}

if (n == 1) {
Expand All @@ -50,6 +190,7 @@ pub const KnownGlobal = enum {
},
}
}
return null;
},
.Date => {
const n = e.args.len;
Expand All @@ -58,7 +199,7 @@ pub const KnownGlobal = enum {
// "new Date()" is pure
e.can_be_unwrapped_if_unused = .if_unused;

return;
return null;
}

if (n == 1) {
Expand All @@ -78,6 +219,7 @@ pub const KnownGlobal = enum {
},
}
}
return null;
},

.Set => {
Expand All @@ -86,7 +228,7 @@ pub const KnownGlobal = enum {
if (n == 0) {
// "new Set()" is pure
e.can_be_unwrapped_if_unused = .if_unused;
return;
return null;
}

if (n == 1) {
Expand All @@ -102,6 +244,7 @@ pub const KnownGlobal = enum {
},
}
}
return null;
},

.Headers => {
Expand All @@ -111,8 +254,9 @@ pub const KnownGlobal = enum {
// "new Headers()" is pure
e.can_be_unwrapped_if_unused = .if_unused;

return;
return null;
}
return null;
},

.Response => {
Expand All @@ -122,7 +266,7 @@ pub const KnownGlobal = enum {
// "new Response()" is pure
e.can_be_unwrapped_if_unused = .if_unused;

return;
return null;
}

if (n == 1) {
Expand All @@ -142,6 +286,7 @@ pub const KnownGlobal = enum {
},
}
}
return null;
},
.TextDecoder, .TextEncoder => {
const n = e.args.len;
Expand All @@ -151,11 +296,12 @@ pub const KnownGlobal = enum {
// "new TextDecoder()" is pure
e.can_be_unwrapped_if_unused = .if_unused;

return;
return null;
}

// We _could_ validate the encoding argument
// But let's not bother
return null;
},

.Map => {
Expand All @@ -164,7 +310,7 @@ pub const KnownGlobal = enum {
if (n == 0) {
// "new Map()" is pure
e.can_be_unwrapped_if_unused = .if_unused;
return;
return null;
}

if (n == 1) {
Expand Down Expand Up @@ -193,18 +339,20 @@ pub const KnownGlobal = enum {
},
}
}
return null;
},
}
};
}
};

const string = []const u8;

const std = @import("std");

const bun = @import("bun");
const js_parser = bun.js_parser;
const logger = bun.logger;

const js_ast = bun.ast;
const E = js_ast.E;
const Symbol = js_ast.Symbol;

const std = @import("std");
const Map = std.AutoHashMapUnmanaged;
4 changes: 3 additions & 1 deletion src/ast/visitExpr.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1492,7 +1492,9 @@ pub fn VisitExpr(
}

if (p.options.features.minify_syntax) {
KnownGlobal.maybeMarkConstructorAsPure(e_, p.symbols.items);
if (KnownGlobal.minifyGlobalConstructor(p.allocator, e_, p.symbols.items, expr.loc, p.options.features.minify_whitespace)) |minified| {
return minified;
}
}
return expr;
}
Expand Down
1 change: 1 addition & 0 deletions src/bundler/ParseTask.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1175,6 +1175,7 @@ fn runWithSourceCode(
opts.output_format = output_format;
opts.features.minify_syntax = transpiler.options.minify_syntax;
opts.features.minify_identifiers = transpiler.options.minify_identifiers;
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.hot_module_reloading = output_format == .internal_bake_dev and !source.index.isRuntime();
Expand Down
1 change: 1 addition & 0 deletions src/runtime.zig
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ pub const Runtime = struct {

minify_syntax: bool = false,
minify_identifiers: bool = false,
minify_whitespace: bool = false,
dead_code_elimination: bool = true,

Comment on lines 169 to 173

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Include minify_whitespace in the runtime transpiler cache key

Features.minify_whitespace affects emitted code but isn’t part of hash_fields_for_runtime_transpiler. Toggling the flag may return stale cached output.

Apply:

         const hash_fields_for_runtime_transpiler = .{
             .top_level_await,
             .auto_import_jsx,
             .allow_runtime,
             .inlining,
             .commonjs_named_exports,
-            .minify_syntax,
-            .minify_identifiers,
+            .minify_syntax,
+            .minify_identifiers,
+            .minify_whitespace,
             .dead_code_elimination,
             .set_breakpoint_on_first_line,
             .trim_unused_imports,
             .dont_bundle_twice,
             .commonjs_at_runtime,
             .emit_decorator_metadata,
             .lower_using,

Run to ensure no other cache-key lists exist that also need this field:

Also applies to: 212-230


🏁 Script executed:

#!/bin/bash
rg -nP --type=zig -C2 'hash_fields_for_runtime_transpiler|Wyhash|hashForRuntimeTranspiler'

Length of output: 40602


Include minify_whitespace in runtime transpiler cache key

src/runtime.zig lines 212–230:

@@ 212
             .minify_syntax,
             .minify_identifiers,
+            .minify_whitespace,
             .dead_code_elimination,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
minify_syntax: bool = false,
minify_identifiers: bool = false,
minify_whitespace: bool = false,
dead_code_elimination: bool = true,
.minify_syntax,
.minify_identifiers,
.minify_whitespace,
.dead_code_elimination,
🤖 Prompt for AI Agents
In src/runtime.zig around lines 169-173 and 212-230, the minify_whitespace
boolean option is defined but not included in the runtime transpiler cache key;
add minify_whitespace to the cache key construction where minify_syntax,
minify_identifiers, and dead_code_elimination are included so the cache
differentiates builds with different whitespace minification. Ensure you append
a deterministic representation of the boolean (e.g.,
"minify_whitespace=true"/"minify_whitespace=false" or a single byte) into the
same buffer or hash input used for the key, update any key-length or hash
calculations accordingly, and run/update tests that assert cache behavior.

set_breakpoint_on_first_line: bool = false,
Expand Down
Loading