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
48 changes: 48 additions & 0 deletions src/js_parser/visit/visit_expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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()
},
);
Comment thread
robobun marked this conversation as resolved.
Expand Down Expand Up @@ -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() {
Expand Down
46 changes: 46 additions & 0 deletions test/bundler/bundler_drop.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
17 changes: 17 additions & 0 deletions test/bundler/bundler_edgecase.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
18 changes: 18 additions & 0 deletions test/bundler/transpiler/transpiler.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
Expand Down
Loading