diff --git a/src/js_parser/visit/visit_expr.rs b/src/js_parser/visit/visit_expr.rs index a85a34a9844f..4efa9430153a 100644 --- a/src/js_parser/visit/visit_expr.rs +++ b/src/js_parser/visit/visit_expr.rs @@ -891,6 +891,43 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O let is_call_target = matches!(p.call_target, Data::EIndex(ct) if core::ptr::eq(&raw const *e_, &raw const *ct)); let is_delete_target = matches!(p.delete_target, Data::EIndex(dt) if core::ptr::eq(&raw const *e_, &raw const *dt)); + // Check defines for a computed string-literal property access, e.g. + // `process.env["NODE_ENV"]`. This mirrors `e_dot`: it must not depend on + // minification, because the `a["b"]` => `a.b` rewrite below is gated on + // `minify_syntax` and cannot be the only route into the define lookup. + // `E::Index` has no side-effect flags, so only substitution and the + // `--drop` flag apply here. + let defines = p.define; + if let Some(mut s) = e_.index.data.e_string() { + if s.is_utf8() { + if let Some(parts) = defines.dots.get(s.slice(p.arena)) { + for define in parts.as_slice() { + if p.is_dot_define_match(expr, &define.parts) { + if in_.assign_target == js_ast::AssignTarget::None { + if !define.data.valueless() { + *e = p.value_for_define( + expr.loc, + in_.assign_target, + is_delete_target, + &define.data, + ); + return; + } + + if define.data.method_call_must_be_replaced_with_undefined() + && in_ + .property_access_for_method_call_maybe_should_replace_with_undefined + { + p.method_call_must_be_replaced_with_undefined = true; + } + } + break; + } + } + } + } + } + // "a['b']" => "a.b" if p.options.features.minify_syntax { if let Some(mut s) = e_.index.data.e_string() { @@ -921,10 +958,14 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O } let has_chain_parent = e_.optional_chain == Some(js_ast::OptionalChain::Continuation); + // Propagate the `--drop` flag through the target like `e_dot` does, so + // `--drop=a.b` also removes `a.b["c"]()`. p.visit_expr_in_out( &mut e_.target, ExprIn { has_chain_parent, + property_access_for_method_call_maybe_should_replace_with_undefined: in_ + .property_access_for_method_call_maybe_should_replace_with_undefined, ..Default::default() }, ); @@ -1009,7 +1050,14 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O }; } _ => { + // The target visit above may have set this for the enclosing + // `e_call` to consume. Hide it across the index visit so a call + // nested in the index does not consume it instead. + let method_call_must_be_replaced_with_undefined = + core::mem::replace(&mut p.method_call_must_be_replaced_with_undefined, false); p.visit_expr(&mut e_.index); + p.method_call_must_be_replaced_with_undefined = + method_call_must_be_replaced_with_undefined; let unwrapped = e_.index.unwrap_inlined(); if let Some(mut s) = unwrapped.data.e_string() { diff --git a/test/bundler/bundler_drop.test.ts b/test/bundler/bundler_drop.test.ts index 9b76730e3845..e9a3b61cd3b0 100644 --- a/test/bundler/bundler_drop.test.ts +++ b/test/bundler/bundler_drop.test.ts @@ -89,6 +89,52 @@ describe("bundler", () => { run: { stdout: "undefined" }, drop: ["Bun"], }); + // `--drop` must also match computed string-literal property accesses. + itBundled("drop/ComputedFunctionCall", { + files: { + "/a.js": `console["log"]("hello");`, + }, + run: { stdout: "" }, + drop: ["console"], + backend: "api", + }); + itBundled("drop/ComputedBecomesUndefined", { + files: { + "/a.js": `console.log(Bun["inspect"]["table"]());`, + }, + run: { stdout: "undefined" }, + drop: ["Bun.inspect.table"], + }); + itBundled("drop/ComputedBecomesUndefinedNested1", { + files: { + "/a.js": `console.log(Bun["inspect"]["table"]());`, + }, + run: { stdout: "undefined" }, + drop: ["Bun.inspect"], + }); + itBundled("drop/ComputedBecomesUndefinedNested2", { + files: { + "/a.js": `console.log(Bun["inspect"]["table"]());`, + }, + run: { stdout: "undefined" }, + drop: ["Bun"], + }); + // The drop flag set by the target must be consumed by the enclosing call, + // not by a call nested inside the computed index. Getting this wrong emits + // `console[undefined]("dropped")`, which throws at runtime. + itBundled("drop/ComputedDynamicIndex", { + files: { + "/a.js": /* js */ ` + function lvl() { + return "log"; + } + console[lvl()]("dropped"); + globalThis.console.log("done"); + `, + }, + run: { stdout: "done" }, + drop: ["console"], + }); itBundled("drop/AssignTarget", { files: { "/a.js": `console.log( diff --git a/test/bundler/bundler_edgecase.test.ts b/test/bundler/bundler_edgecase.test.ts index 53ed3d33510d..58ced8b0594d 100644 --- a/test/bundler/bundler_edgecase.test.ts +++ b/test/bundler/bundler_edgecase.test.ts @@ -186,6 +186,23 @@ describe("bundler", () => { NODE_ENV: "development", }, }); + // The bracket spelling must inline the same way as the dot spelling, and + // must not depend on --minify folding `a["b"]` into `a.b` first. + itBundled("edgecase/NodeEnvComputedPropertyAccess", { + files: { + "/entry.js": /* js */ ` + capture(process.env.NODE_ENV); + capture(process.env["NODE_ENV"]); + capture(process["env"].NODE_ENV); + capture(process["env"]["NODE_ENV"]); + `, + }, + target: "browser", + capture: ['"development"', '"development"', '"development"', '"development"'], + env: { + NODE_ENV: "development", + }, + }); itBundled("edgecase/StarExternal", { files: { diff --git a/test/bundler/transpiler/transpiler.test.js b/test/bundler/transpiler/transpiler.test.js index bf2b3db6485c..aec9c44f34b0 100644 --- a/test/bundler/transpiler/transpiler.test.js +++ b/test/bundler/transpiler/transpiler.test.js @@ -2530,6 +2530,24 @@ console.log(resolve.length) expectPrinted_("Math.log('hi')", 'console.error("hi")'); }); + // A computed string-literal member access must be substituted the same way + // as the dot form, without requiring --minify. + it("define matches computed string-literal member access", () => { + expectPrinted_(`export default process.env.NODE_ENV;`, `export default "development"`); + expectPrinted_(`export default process.env["NODE_ENV"];`, `export default "development"`); + expectPrinted_(`export default process["env"].NODE_ENV;`, `export default "development"`); + expectPrinted_(`export default process["env"]["NODE_ENV"];`, `export default "development"`); + expectPrinted_(`hello["earth"]('hi')`, 'hello.mars("hi")'); + expectPrinted_(`Math["log"]('hi')`, 'console.error("hi")'); + + // Assignment targets are never substituted, same as the dot form. + expectPrinted_(`process.env["NODE_ENV"] = 1`, `process.env["NODE_ENV"] = 1`); + + // Non-literal and non-matching computed keys are left alone. + expectPrinted_(`export default process.env[NODE_ENV];`, `export default process.env[NODE_ENV]`); + expectPrinted_(`export default process.env["OTHER"];`, `export default process.env["OTHER"]`); + }); + it("jsx symbol should work", () => { expectBunPrinted_(`var x = jsx; export default x;`, "var x = jsx;\nexport default x"); });