mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
fix(codegen): preserve quotes for cjs-module-lexer equality strings (#22551)
## Summary - Force plain string quotes for `"default"` / `"__esModule"` operands of `==`/`===`/`!=`/`!==` during minification, so `cjs-module-lexer` can recognise the `Object.keys(mod).forEach(key => key === "default" || ...)` re-export filter emitted by Babel / TypeScript / Rollup. - Follow-up to #22475 (which fixed `require("…")`). Same root cause: `calculate_quote_maybe_backtick`'s equal-cost tie-breaker prefers backtick, which the lexer's heuristic parser doesn't accept. - Caught by monitor-oxc's `whitespace` runner: minifying `@babel/types/lib/index.js` previously broke `import { isIdentifier } from "@babel/types"` under `--experimental-require-module`. Raw minified size is unchanged across all `minsize` fixtures. ## Example Input: ```js Object.keys(mod).forEach(function (key) { if (key === "default" || key === "__esModule") return; }); ``` Before (broken — `cjs-module-lexer` skips the re-export): ```js Object.keys(t).forEach(function(e){if(e===`default`||e===`__esModule`)return}); ``` After: ```js Object.keys(t).forEach(function(e){if(e==="default"||e==="__esModule")return}); ``` AI disclosure: drafted with Claude Code, reviewed manually.
This commit is contained in:
1 parent
ea6b4d66cd
commit
a069b1c631
4 files changed
+77
-4
No files matched your search
@@ -12,6 +12,52 @@ use oxc_syntax::{
|
||||
|
||||
use crate::{Codegen, Context, Operator, r#gen::GenExpr};
|
||||
|
||||
/// Force plain string quotes for `"default"` and `"__esModule"` when they
|
||||
/// appear as an operand of an equality comparison during minification. This
|
||||
/// keeps `cjs-module-lexer` (used by Node for CJS/ESM interop) able to
|
||||
/// recognise the re-export pattern emitted by Babel / TypeScript / Rollup:
|
||||
///
|
||||
/// ```js
|
||||
/// Object.keys(mod).forEach(function (key) {
|
||||
/// if (key === "default" || key === "__esModule") return;
|
||||
/// ...
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
/// Without this, `print_string_literal`'s tie-breaker in
|
||||
/// `calculate_quote_maybe_backtick` prefers backtick on equal cost, and the
|
||||
/// lexer's heuristic parser does not match `key === \`default\``.
|
||||
///
|
||||
/// Companion to `try_print_require_call` and `try_print_cjs_define_property_call`
|
||||
/// in `gen.rs`, which preserve plain quotes at the other two syntactic positions
|
||||
/// `cjs-module-lexer` recognises.
|
||||
///
|
||||
/// Returns `true` if printed.
|
||||
#[inline]
|
||||
fn try_print_cjs_lexer_equality_string(
|
||||
operator: BinaryishOperator,
|
||||
operand: &Expression,
|
||||
p: &mut Codegen,
|
||||
) -> bool {
|
||||
if !p.options.minify {
|
||||
return false;
|
||||
}
|
||||
let BinaryishOperator::Binary(op) = operator else {
|
||||
return false;
|
||||
};
|
||||
if !op.is_equality() {
|
||||
return false;
|
||||
}
|
||||
let Expression::StringLiteral(str_lit) = operand else {
|
||||
return false;
|
||||
};
|
||||
if !matches!(str_lit.value.as_str(), "default" | "__esModule") {
|
||||
return false;
|
||||
}
|
||||
p.print_string_literal(str_lit, false);
|
||||
true
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum Binaryish<'a> {
|
||||
Binary(&'a BinaryExpression<'a>),
|
||||
@@ -124,7 +170,9 @@ impl<'a> BinaryExpressionVisitor<'a> {
|
||||
};
|
||||
|
||||
let Some(left_binary) = left_binary else {
|
||||
left.gen_expr(p, v.left_precedence, v.ctx);
|
||||
if !try_print_cjs_lexer_equality_string(v.operator, left, p) {
|
||||
left.gen_expr(p, v.left_precedence, v.ctx);
|
||||
}
|
||||
v.visit_right_and_finish(p);
|
||||
break;
|
||||
};
|
||||
@@ -223,7 +271,10 @@ impl<'a> BinaryExpressionVisitor<'a> {
|
||||
p.print_soft_space();
|
||||
self.operator.r#gen(p);
|
||||
p.print_soft_space();
|
||||
self.e.right().gen_expr(p, self.right_precedence, self.ctx);
|
||||
let right = self.e.right();
|
||||
if !try_print_cjs_lexer_equality_string(self.operator, right, p) {
|
||||
right.gen_expr(p, self.right_precedence, self.ctx);
|
||||
}
|
||||
if self.wrap {
|
||||
p.print_ascii_byte(b')');
|
||||
}
|
||||
|
||||
@@ -1517,6 +1517,9 @@ impl GenExpr for CallExpression<'_> {
|
||||
///
|
||||
/// Minify-only: outside minify, `print_string_literal` already uses `self.quote` (never
|
||||
/// backtick), and this path skips the argument comments that `print_arguments` preserves.
|
||||
///
|
||||
/// Companion to `try_print_require_call` (below) and
|
||||
/// `try_print_cjs_lexer_equality_string` in `binary_expr_visitor.rs`.
|
||||
fn try_print_cjs_define_property_call(
|
||||
p: &mut Codegen<'_>,
|
||||
call: &CallExpression<'_>,
|
||||
@@ -1555,6 +1558,9 @@ fn try_print_cjs_define_property_call(
|
||||
///
|
||||
/// Minify-only: outside minify, `print_string_literal` already uses `self.quote` (never
|
||||
/// backtick), and this path skips the argument comments that `print_arguments` preserves.
|
||||
///
|
||||
/// Companion to `try_print_cjs_define_property_call` (above) and
|
||||
/// `try_print_cjs_lexer_equality_string` in `binary_expr_visitor.rs`.
|
||||
fn try_print_require_call(p: &mut Codegen<'_>, call: &CallExpression<'_>) -> bool {
|
||||
if !p.options.minify {
|
||||
return false;
|
||||
|
||||
@@ -668,6 +668,22 @@ fn string() {
|
||||
test_minify("require(foo);", "require(foo);");
|
||||
// Single-quoted require
|
||||
test_minify(r"require('./foo');", r#"require("./foo");"#);
|
||||
|
||||
// `cjs-module-lexer` re-export detection requires `"default"` and
|
||||
// `"__esModule"` to stay as plain string literals in equality comparisons.
|
||||
test_minify(
|
||||
r#"function f(key) { if (key === "default" || key === "__esModule") return; }"#,
|
||||
r#"function f(key){if(key==="default"||key==="__esModule")return}"#,
|
||||
);
|
||||
test_minify(r#"a = x !== "default""#, r#"a=x!=="default";"#);
|
||||
test_minify(r#"a = x == "__esModule""#, r#"a=x=="__esModule";"#);
|
||||
test_minify(r#"a = x != "default""#, r#"a=x!="default";"#);
|
||||
// Magic string on the left side of an equality is also preserved.
|
||||
test_minify(r#"a = "default" === x"#, r#"a="default"===x;"#);
|
||||
// Other strings in equality comparisons are unaffected.
|
||||
test_minify(r#"a = x === "foo""#, "a=x===`foo`;");
|
||||
// The two magic strings are unaffected when not in equality comparisons.
|
||||
test_minify(r#"a = "default""#, "a=`default`;");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -21,7 +21,7 @@ Original | minified | minified | gzip | gzip | Iterations | Fi
|
||||
|
||||
3.20 MB | 1.00 MB | 1.01 MB | 322.61 kB | 331.56 kB | 2 | echarts.js
|
||||
|
||||
6.69 MB | 2.22 MB | 2.31 MB | 458.45 kB | 488.28 kB | 4 | antd.js
|
||||
6.69 MB | 2.22 MB | 2.31 MB | 458.46 kB | 488.28 kB | 4 | antd.js
|
||||
|
||||
10.95 MB | 3.33 MB | 3.49 MB | 853.31 kB | 915.50 kB | 4 | typescript.js
|
||||
10.95 MB | 3.33 MB | 3.49 MB | 853.35 kB | 915.50 kB | 4 | typescript.js
|
||||
|
||||
Reference in new issue
Block a user