Skip to content
Open
4 changes: 4 additions & 0 deletions src/bundler/defines.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ impl DefineExt for Define {
let mut define = Box::new(Define {
identifiers: StringHashMap::default(),
dots: StringHashMap::default(),
injected: Vec::new(),
drop_debugger,
});
define.dots.reserve(124);
Expand Down Expand Up @@ -448,6 +449,7 @@ impl DefineDataExt for DefineData {
/* method_call_must_be_replaced_with_undefined: */
method_call_must_be_replaced_with_undefined_,
),
injected_define_index: None,
});
}

Expand Down Expand Up @@ -476,6 +478,7 @@ impl DefineDataExt for DefineData {
/* method_call_must_be_replaced_with_undefined: */
method_call_must_be_replaced_with_undefined_,
),
injected_define_index: None,
});
}

Expand Down Expand Up @@ -522,6 +525,7 @@ impl DefineDataExt for DefineData {
/* method_call_must_be_replaced_with_undefined: */
method_call_must_be_replaced_with_undefined_,
),
injected_define_index: None,
})
}

Expand Down
2 changes: 2 additions & 0 deletions src/bundler/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1366,6 +1366,7 @@ impl<'a> BundleOptions<'a> {
define: Box::new(defines::Define {
identifiers: self.define.identifiers.clone(),
dots: self.define.dots.clone(),
injected: self.define.injected.clone(),
drop_debugger: self.define.drop_debugger,
}),
drop: self.drop.clone(),
Expand Down Expand Up @@ -1633,6 +1634,7 @@ impl<'a> BundleOptions<'a> {
define: Box::new(defines::Define {
identifiers: Default::default(),
dots: Default::default(),
injected: Vec::new(),
drop_debugger: false,
}),
loaders,
Expand Down
35 changes: 34 additions & 1 deletion src/js_parser/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,8 @@ pub mod defines {
// tier up for json-parser access) can construct directly.
pub original_name: Option<Box<[u8]>>,
pub flags: Flags,
/// Index into `Define.injected` when `value` is an object/array literal.
pub injected_define_index: Option<u32>,
}

// SAFETY: `ExprData` contains `StoreRef` raw pointers into immutable,
Expand All @@ -389,6 +391,7 @@ pub mod defines {
value: ExprData::EMissing(E::Missing),
original_name: None,
flags: Flags::default(),
injected_define_index: None,
}
}
}
Expand Down Expand Up @@ -427,6 +430,7 @@ pub mod defines {
options.method_call_must_be_replaced_with_undefined,
),
original_name: options.original_name.map(Box::<[u8]>::from),
injected_define_index: None,
}
}

Expand Down Expand Up @@ -490,14 +494,29 @@ pub mod defines {
|| b.method_call_must_be_replaced_with_undefined(),
),
original_name: b.original_name,
injected_define_index: b.injected_define_index,
}
}
}

/// An object/array `--define` value the parser hoists to one shared `var`.
#[derive(Clone)]
pub struct InjectedDefine {
pub name: Box<[u8]>,
pub value: ExprData,
}

// SAFETY: see `Send` for DefineData — `value` points at immutable
// process-lifetime AST stores and is only read after `Define::init`.
unsafe impl Send for InjectedDefine {}
// SAFETY: see `Send` impl above.
unsafe impl Sync for InjectedDefine {}

#[derive(Default)]
pub struct Define {
pub identifiers: StringHashMap<IdentifierDefine>,
pub dots: StringHashMap<Vec<DotDefine>>,
pub injected: Vec<InjectedDefine>,
pub drop_debugger: bool,
}

Expand All @@ -522,8 +541,22 @@ pub mod defines {
pub fn insert(
&mut self,
key: &[u8],
value: DefineData,
mut value: DefineData,
) -> Result<(), bun_alloc::AllocError> {
// `deep_clone` normalises JSON-parsed values to EObject/EArray.
if !value.valueless()
&& matches!(
value.value.tag(),
bun_ast::expr::Tag::EObject | bun_ast::expr::Tag::EArray
)
{
value.injected_define_index = Some(self.injected.len() as u32);
self.injected.push(InjectedDefine {
name: Box::from(key),
value: value.value,
});
}

// If it has a dot, then it's a DotDefine. e.g. process.env.NODE_ENV
if let Some(last_dot) = strings::last_index_of_char(key, b'.') {
let tail = &key[last_dot + 1..key.len()];
Expand Down
45 changes: 45 additions & 0 deletions src/js_parser/p.rs
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,9 @@ pub struct P<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> {
pub temp_refs_to_declare: List<'a, TempRef>,
pub temp_ref_count: i32,

// Indexed by `DefineData.injected_define_index`.
pub injected_define_refs: List<'a, Ref>,

// When bundling, hoisted top-level local variables declared with "var" in
// nested scopes are moved up to be declared in the top-level scope instead.
// The old "var" statements are turned into regular assignments instead. This
Expand Down Expand Up @@ -2835,6 +2838,35 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
self.filename_ref =
self.declare_common_js_symbol(js_ast::symbol::Kind::Unbound, b"__filename")?;

if !self.define.injected.is_empty() {
self.injected_define_refs
.reserve(self.define.injected.len());
let will_use_renamer = self.will_use_renamer();
for injected in self.define.injected.iter() {
let sanitized = bun_core::MutableString::ensure_valid_identifier(&injected.name)?;
// No renamer => printed verbatim, so suffix a hash for collision safety.
let name: &'a [u8] = if will_use_renamer {
bun_alloc::arena_format!(
in self.arena,
"define_{}_default",
bstr::BStr::new(&sanitized)
)
} else {
bun_alloc::arena_format!(
in self.arena,
"define_{}_default_{}",
bstr::BStr::new(&sanitized),
bun_core::fmt::truncated_hash32(bun_wyhash::hash(&injected.name))
)
}
.into_bump_str()
.as_bytes();
let ref_ = self.new_symbol(js_ast::symbol::Kind::Other, name);
VecExt::append(&mut self.module_scope_mut().generated, ref_);
self.injected_define_refs.push(ref_);
}
}

if self.options.features.inject_jest_globals {
self.jest.test =
self.declare_common_js_symbol(js_ast::symbol::Kind::Unbound, b"test")?;
Expand Down Expand Up @@ -6303,6 +6335,18 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
is_delete_target: bool,
define_data: &DefineData,
) -> Expr {
if let Some(idx) = define_data.injected_define_index {
if let Some(&ref_) = self.injected_define_refs.get(idx as usize) {
self.record_usage(ref_);
return Expr {
data: js_ast::ExprData::EIdentifier(
E::Identifier::init(ref_).with_can_be_removed_if_unused(true),
),
loc,
};
}
}

// Callers gate on `!valueless()` before reaching here, so `value` is a
// real Expr.Data by contract.
let value = define_data.value;
Expand Down Expand Up @@ -8794,6 +8838,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
await_target: None,
temp_refs_to_declare: BumpVec::new_in(arena),
temp_ref_count: 0,
injected_define_refs: BumpVec::new_in(arena),
relocated_top_level_vars: BumpVec::new_in(arena),
after_arrow_body_loc: bun_ast::Loc::EMPTY,
const_values: Default::default(),
Expand Down
40 changes: 40 additions & 0 deletions src/js_parser/parse/parse_entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1091,6 +1091,46 @@ impl<'a> Parser<'a> {
}
}

// `var define_X_default = {...}` for each referenced injected define.
for i in 0..p.injected_define_refs.len() {
let ref_ = p.injected_define_refs[i];
if p.symbols.as_slice()[ref_.inner_index() as usize].use_count_estimate == 0 {
continue;
}
let value = p.define.injected[i].value;
let mut declared_symbols =
bun_ast::DeclaredSymbolList::init_capacity(1).expect("unreachable");
declared_symbols.append_assume_capacity(DeclaredSymbol {
ref_,
is_top_level: true,
});
let binding = p.b(B::Identifier { r#ref: ref_ }, bun_ast::Loc::EMPTY);
let part_stmts = p.arena.alloc_slice_fill_with(1, |_| {
let mut decls = G::DeclList::init_capacity(1);
decls.append_assume_capacity(G::Decl {
binding,
value: Some(Expr {
data: value,
loc: bun_ast::Loc::EMPTY,
}),
});
p.s(
S::Local {
kind: js_ast::LocalKind::KVar,
decls,
..Default::default()
},
bun_ast::Loc::EMPTY,
)
});
before.push(js_ast::Part {
stmts: part_stmts.into(),
declared_symbols,
can_be_removed_if_unused: true,
..Default::default()
});
}

// This is a workaround for broken module environment checks in packages like lodash-es
// https://github.com/lodash/lodash/issues/5660
let mut force_esm = false;
Expand Down
17 changes: 17 additions & 0 deletions src/js_parser/visit/visit_expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1219,7 +1219,24 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
}
}
Op::UnDelete => {
let name_before = match e_.value.data {
Data::EIdentifier(id) => Some(p.load_name_from_ref(id.ref_)),
_ => None,
};
p.visit_expr_in_out(&mut e_.value, ExprIn::default());
// `delete <ident>` is a strict-mode early error; wrap substituted operands.
if let Data::EIdentifier(id) = e_.value.data {
let name_after = p.symbols[id.ref_.inner_index() as usize]
.original_name
.slice();
if name_before != Some(name_after) {
e_.value = Expr {
loc: e_.value.loc,
data: prefill::data::ZERO,
}
.join_with_comma(e_.value);
}
}
Comment thread
robobun marked this conversation as resolved.
}
_ => {
let assign_target = Op::unary_assign_target(e_.op);
Expand Down
92 changes: 92 additions & 0 deletions test/bundler/esbuild/extra.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,98 @@ describe("bundler", () => {
run: true,
});

// Object/array define values should be hoisted to a single shared binding so
// identity holds and mutations are visible across references (esbuild parity).
itBundled("extra/DefineObjectIdentity", {
files: {
"in.js": /* js */ `
const a = CFG, b = CFG;
if (a !== b) throw "identity: a !== b";
if (a.nested !== b.nested) throw "identity: a.nested !== b.nested";
a.k = 99;
if (b.k !== 99) throw "mutation: b.k !== 99";
console.log("ok");
`,
},
define: { CFG: '{"k":1,"nested":{"x":2}}' },
run: { stdout: "ok" },
onAfterBundle(api) {
const out = api.readFile("out.js");
const literals = out.match(/\{\s*k:\s*1/g);
if ((literals?.length ?? 0) !== 1) {
throw new Error("expected exactly one { k: 1 ... } literal in output, got " + (literals?.length ?? 0));
}
},
});
itBundled("extra/DefineObjectIdentityDot", {
files: {
"in.js": /* js */ `
const a = process.env.CFG, b = process.env.CFG;
if (a !== b) throw "identity: a !== b";
if (ARR !== ARR) throw "identity: ARR !== ARR";
const arr = ARR;
arr.push(4);
if (ARR.length !== 4) throw "mutation: ARR.length !== 4";
console.log("ok");
`,
},
define: { "process.env.CFG": '{"k":1}', "ARR": "[1,2,3]" },
run: { stdout: "ok" },
});
itBundled("extra/DefineObjectIdentityNoBundle", {
files: {
"in.js": /* js */ `
const a = CFG, b = CFG;
if (a !== b) throw "identity: a !== b";
console.log("ok");
`,
},
bundling: false,
define: { CFG: '{"k":1}' },
run: { stdout: "ok" },
});
// `delete` on a hoisted-object define must not print `delete <bare identifier>`
// (strict-mode early error); the operand has to be wrapped.
itBundled("extra/DefineObjectDeleteTarget", {
files: {
"in.mjs": /* js */ `
export {};
console.log(delete process.env.CFG, delete import.meta.CFG, process.env.CFG.x);
`,
},
define: { "process.env.CFG": '{"x":1}', "import.meta.CFG": "[1,2]" },
run: { stdout: "true true 1" },
onAfterBundle(api) {
const out = api.readFile("out.js");
if (/\bdelete\s+[A-Za-z_$][\w$]*\s*[,;]/.test(out)) {
throw new Error("output contains `delete <bare identifier>` (strict-mode SyntaxError):\n" + out);
}
},
});
itBundled("extra/DefineObjectDeleteBareIdentifier", {
files: {
"in.js": /* js */ `
// sloppy-mode source: delete <unbound> is valid here
var keep = 1;
console.log(delete CFG, delete ARR, delete keep);
`,
},
format: "iife",
define: { CFG: '{"k":1}', ARR: "[1,2,3]" },
// iife + node keeps the bundle sloppy so `delete keep` stays valid syntax at runtime.
run: { runtime: "node", stdout: "true true false" },
onAfterBundle(api) {
const out = api.readFile("out.js");
if (/\bdelete\s+define_[\w$]*\s*[,;)]/.test(out)) {
throw new Error("hoisted define emitted as bare `delete <identifier>`:\n" + out);
}
// Source-written `delete keep` must stay a bare identifier reference.
if (!/\bdelete\s+keep\b/.test(out)) {
throw new Error("source-written `delete keep` was wrapped:\n" + out);
}
},
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Various ESM cases
itBundled("extra/CatchScope1", {
files: {
Expand Down
Loading