Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 5 additions & 1 deletion src/codegen/bundle-functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,11 @@ $$capture_start$$(${fn.async ? "async " : ""}${
entrypoints: [tmpFile],
define,
target: "bun",
minify: { syntax: true, whitespace: false, keepNames: true },
// keepNames is intentionally off: it emits __name() calls whose runtime
// helper lands outside the $$capture_start$$/$$capture_end$$ window, and
// identifiers are not minified here so .name inference from bindings
// already gives the right value.
Comment thread
robobun marked this conversation as resolved.
Resolved
minify: { syntax: true, whitespace: false },
});
// TODO: Wait a few versions before removing this
if (!build.success) {
Expand Down
74 changes: 72 additions & 2 deletions src/js_parser/p.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5238,6 +5238,55 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
}
}

#[inline]
fn should_keep_names(&self) -> bool {
self.options.features.minify_keep_names && self.options.features.allow_runtime
}

/// A user-written `static name` member owns `.name`; `__name()` must not clobber it.
pub(crate) fn class_has_custom_static_name(class: &G::Class) -> bool {
for prop in class.properties.iter() {
if !prop.flags.contains(Flags::Property::IsStatic) {
continue;
}
if let Some(key) = prop.key {
if let js_ast::ExprData::EString(s) = key.data {
if s.eql_comptime(b"name") {
return true;
}
}
}
}
false
}

pub(crate) fn keep_stmt_symbol_name(
&mut self,
loc: bun_ast::Loc,
ref_: Ref,
original_name: &[u8],
) -> Option<Stmt> {
if !self.should_keep_names() {
return None;
}
self.record_usage(ref_);
let name_str = self.new_expr(E::String::init(original_name), loc);
let call = self.call_runtime(
loc,
b"__name",
ExprNodeList::from_slice(&[Expr::init_identifier(ref_, loc), name_str]),
);
Some(self.s(
S::SExpr {
value: call,
// `__name()` only mutates the (otherwise pure) identifier it tags;
// it must not pin the containing part.
Comment thread
robobun marked this conversation as resolved.
Resolved
does_not_affect_tree_shaking: true,
},
loc,
))
Comment thread
robobun marked this conversation as resolved.
Resolved
}

pub(crate) fn value_for_this(&mut self, loc: bun_ast::Loc) -> Option<Expr> {
// Substitute "this" if we're inside a static class property initializer
if self
Expand Down Expand Up @@ -5376,8 +5425,29 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
}
}

pub(crate) fn keep_expr_symbol_name(&mut self, _value: Expr, _name: &[u8]) -> Expr {
_value
pub(crate) fn keep_expr_symbol_name(&mut self, value: Expr, name: &[u8]) -> Expr {
if !self.should_keep_names() {
return value;
}
if let js_ast::ExprData::EClass(cls) = value.data {
if Self::class_has_custom_static_name(&cls) {
return value;
}
}
Comment thread
robobun marked this conversation as resolved.
Resolved
let loc = value.loc;
let name_str = self.new_expr(E::String::init(name), loc);
let target = self.runtime_identifier(loc, b"__name");
self.new_expr(
E::Call {
target,
args: ExprNodeList::from_slice(&[value, name_str]),
// The only side effect of `__name` is on `value` itself, so if the
// whole expression is unused tree-shaking may drop it.
Comment thread
robobun marked this conversation as resolved.
Resolved
can_be_unwrapped_if_unused: js_ast::e::CallUnwrap::IfUnused,
..Default::default()
},
loc,
)
}

pub(crate) fn is_simple_parameter_list(args: &[G::Arg], has_rest_arg: bool) -> bool {
Expand Down
12 changes: 12 additions & 0 deletions src/js_parser/visit/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,19 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
let dup: Option<&mut StringVoidMap> = duplicate_args_check.as_deref_mut();
self.visit_binding(arg.binding, dup);
if let Some(default) = arg.default.as_mut() {
let was_anonymous_named_expr = default.is_anonymous_named();
self.visit_expr(default);
if let BData::BIdentifier(bind_) = arg.binding.data {
let bind_ = bind_.get();
let name: &'a [u8] = self.symbols[bind_.r#ref.inner_index() as usize]
.original_name
.slice();
arg.default = Some(self.maybe_keep_expr_symbol_name(
arg.default.expect("unreachable"),
name,
was_anonymous_named_expr,
));
}
}
}
}
Expand Down
27 changes: 21 additions & 6 deletions src/js_parser/visit/visit_binary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -703,15 +703,30 @@ impl BinaryExpressionVisitor {
);
}
}
Op::Code::BinNullishCoalescingAssign | Op::Code::BinLogicalOrAssign => {
Op::Code::BinNullishCoalescingAssign
| Op::Code::BinLogicalOrAssign
| Op::Code::BinLogicalAndAssign => {
// Optionally preserve the name; `x ??= <anon>` performs
// NamedEvaluation with the LHS name, same as plain `=`.
Comment thread
robobun marked this conversation as resolved.
Resolved
if let ExprData::EIdentifier(ident) = e_.left.data {
let name = p.symbols[ident.ref_.inner_index() as usize].original_name;
e_.right = p.maybe_keep_expr_symbol_name(
e_.right,
name.slice(),
was_anonymous_named_expr,
);
}

// Special case `{}.field ??= value` to minify to `value`
// This optimization is specifically to target this pattern in HMR:
// `import.meta.hot.data.etc ??= init()`
if let Some(dot) = e_.left.data.e_dot() {
if let Some(obj) = dot.target.data.e_object() {
if obj.properties.len_u32() == 0 {
if dot.name != b"__proto__" {
return e_.right;
if e_.op != Op::Code::BinLogicalAndAssign {
if let Some(dot) = e_.left.data.e_dot() {
if let Some(obj) = dot.target.data.e_object() {
if obj.properties.len_u32() == 0 {
if dot.name != b"__proto__" {
return e_.right;
}
}
}
}
Expand Down
42 changes: 27 additions & 15 deletions src/js_parser/visit/visit_expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2628,28 +2628,40 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
let decorator_name_from_context = p.decorator_class_name;
p.decorator_class_name = None;

let original_class_name = e_.class_name;
let _ = p.visit_class(expr.loc, &mut e_, Ref::NONE);

// Decorator lowering rewrites `properties`, so decide before lowering.
let has_custom_static_name = Self::class_has_custom_static_name(&e_);

// Lower standard decorators for class expressions
if e_.should_lower_standard_decorators {
*e = p.lower_standard_decorators_expr(&mut e_, expr.loc, decorator_name_from_context);
return;
} else {
// Remove unused class names when minifying (only when bundling is enabled)
// unless --keep-names is specified
Comment thread
robobun marked this conversation as resolved.
Resolved
if p.options.features.minify_syntax
&& p.options.bundle
&& !p.options.features.minify_keep_names
// SAFETY: current_scope is a live arena ptr while the parser exists.
&& !p.current_scope().contains_direct_eval
&& e_.class_name.is_some()
&& !e_.class_name.unwrap().ref_.is_empty()
&& p.symbols[e_.class_name.unwrap().ref_.inner_index() as usize]
.use_count_estimate
== 0
{
e_.class_name = None;
}
}

// Remove unused class names when minifying (only when bundling is enabled)
// unless --keep-names is specified
if p.options.features.minify_syntax
&& p.options.bundle
&& !p.options.features.minify_keep_names
// SAFETY: current_scope is a live arena ptr while the parser exists.
&& !p.current_scope().contains_direct_eval
&& e_.class_name.is_some()
&& !e_.class_name.unwrap().ref_.is_empty()
&& p.symbols[e_.class_name.unwrap().ref_.inner_index() as usize]
.use_count_estimate
== 0
{
e_.class_name = None;
if let Some(name) = original_class_name {
if !name.ref_.is_empty() && !has_custom_static_name {
let original_name: &[u8] = p.symbols[name.ref_.inner_index() as usize]
.original_name
.slice();
*e = p.keep_expr_symbol_name(*e, original_name);
}
}
Comment thread
robobun marked this conversation as resolved.
Resolved
}
}
60 changes: 58 additions & 2 deletions src/js_parser/visit/visit_stmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -766,6 +766,12 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
stmts.push(*stmt);
}

if let Some(keep) =
p.keep_stmt_symbol_name(stmt.loc, data.default_name.ref_, name)
{
stmts.push(keep);
}

p.react_refresh.hook_ctx_storage = prev;
restore_dead!();
record_on_exit!();
Expand Down Expand Up @@ -811,6 +817,16 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
data.default_name = p.create_default_name(stmt.loc);
}

// Decorator-name injection, `lower_class`, and the server-components
// wrap all mutate `class.class` in place; capture what keep-names
// needs while it still reflects the user-written shape.
Comment thread
robobun marked this conversation as resolved.
Resolved
let has_custom_static_name =
Self::class_has_custom_static_name(&class.class);
let keep_name: &[u8] = match class.class.class_name {
Some(n) if !n.ref_.is_empty() => p.load_name_from_ref(n.ref_),
_ => js_ast::ClauseItem::DEFAULT_ALIAS,
};

// We only inject a name into classes when decorator lowering
// needs one: legacy TS decorators (`has_decorators`) or
// standard decorator lowering, which also covers classes with
Expand Down Expand Up @@ -845,6 +861,16 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
data.value = js_ast::StmtOrExpr::Stmt(class_stmts[class_stmt_idx]);
stmts.push(*stmt);

// `__name()` must land before the decorator suffix so decorator
// callbacks observe the preserved name.
Comment thread
robobun marked this conversation as resolved.
Resolved
if !has_custom_static_name {
if let Some(keep) =
p.keep_stmt_symbol_name(stmt.loc, data.default_name.ref_, keep_name)
{
stmts.push(keep);
}
}

// Emit any suffix statements after the export default
if class_stmt_idx + 1 < class_stmts.len() {
stmts.extend_from_slice(&class_stmts[class_stmt_idx + 1..]);
Expand Down Expand Up @@ -925,6 +951,9 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
.expect("infallible: in namespace");
stmts.reserve(3);
stmts.push(*stmt);
if let Some(keep) = p.keep_stmt_symbol_name(stmt.loc, name_ref, original_name) {
stmts.push(keep);
}
let func_name = data.func.name.expect("infallible: name checked");
stmts.push(Stmt::assign(
p.new_expr(
Expand Down Expand Up @@ -977,6 +1006,9 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
));
} else {
stmts.push(*stmt);
if let Some(keep) = p.keep_stmt_symbol_name(stmt.loc, name_ref, original_name) {
stmts.push(keep);
}
Comment thread
claude[bot] marked this conversation as resolved.
Resolved
}
} else if mark_as_dead {
if let Some(replacement) = p
Expand Down Expand Up @@ -1062,12 +1094,36 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
data.is_export = false;
}

// `lower_class` may rewrite/hoist static members, so decide this first.
let has_custom_static_name = Self::class_has_custom_static_name(&data.class);

// Lower class field syntax for browsers that don't support it
let lowered = p.lower_class(js_ast::StmtOrExpr::Stmt(*stmt));

if !mark_as_dead || was_export_inside_namespace {
// Lower class field syntax for browsers that don't support it
stmts.extend_from_slice(lowered);
// `__name()` must land immediately after the class so lowered decorator
// callbacks (which run after the class) observe the preserved name.
Comment thread
robobun marked this conversation as resolved.
Resolved
let keep = data
.class
.class_name
.filter(|_| !has_custom_static_name)
.and_then(|name| {
let original_name = p.symbols[name.ref_.inner_index() as usize]
.original_name
.slice();
p.keep_stmt_symbol_name(stmt.loc, name.ref_, original_name)
});
if let Some(keep) = keep {
let class_idx = lowered
.iter()
.position(|cs| matches!(cs.data, StmtData::SClass(_)))
.unwrap_or(0);
stmts.extend_from_slice(&lowered[..=class_idx]);
stmts.push(keep);
stmts.extend_from_slice(&lowered[class_idx + 1..]);
} else {
stmts.extend_from_slice(lowered);
}
Comment thread
robobun marked this conversation as resolved.
Resolved
} else {
let ref_ = data
.class
Expand Down
Loading
Loading