Skip to content
Open
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
12 changes: 10 additions & 2 deletions src/js_parser/fold.rs
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,12 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
if identifier_opts.is_call_target() && name == b"require" {
p.ignore_usage(p.module_ref);
return Some(p.value_for_require(name_loc));
Comment thread
robobun marked this conversation as resolved.
Resolved
} else if !p.commonjs_named_exports_deoptimized && name == b"exports" {
} else if !p.commonjs_named_exports_deoptimized
&& !p.has_user_declared_module
// The rewrite prints through `exports_ref`, which is the user's binding here.
&& !p.has_user_declared_exports
&& name == b"exports"
{
Comment thread
robobun marked this conversation as resolved.
Resolved
if identifier_opts.assign_target() != js_ast::AssignTarget::None {
p.commonjs_module_exports_assigned_deoptimized = true;
}
Expand Down Expand Up @@ -474,7 +479,10 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O

if p.should_unwrap_common_js_to_esm() {
if !p.is_control_flow_dead && id.ref_.eql(p.exports_ref) {
if !p.commonjs_named_exports_deoptimized {
if !p.commonjs_named_exports_deoptimized
&& !p.has_user_declared_module
&& !p.has_user_declared_exports
Comment thread
robobun marked this conversation as resolved.
Resolved
{
if identifier_opts.is_delete_target() {
p.deoptimize_common_js_named_exports();
return None;
Expand Down
18 changes: 18 additions & 0 deletions src/js_parser/p.rs
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,13 @@ pub struct P<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> {
/// A module-scope `var` has the name of a top-level function, which module code rejects.
pub(crate) has_top_level_function_merged_with_var: bool,

/// A module-scope `var module = ...` replaces the CommonJS wrapper value.
pub(crate) has_user_declared_module: bool,
/// A module-scope `var exports = ...` replaces the CommonJS wrapper value.
pub(crate) has_user_declared_exports: bool,
/// A module-scope `var exports = module.exports`, which keeps the wrapper value if `module` is the wrapper's.
pub(crate) has_var_exports_from_module_exports: bool,

pub(crate) is_file_considered_to_have_esm_exports: bool,

pub(crate) has_called_runtime: bool,
Expand Down Expand Up @@ -3343,6 +3350,14 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
self.declare_common_js_symbol(js_ast::symbol::Kind::Hoisted, b"exports")?;
self.module_ref =
self.declare_common_js_symbol(js_ast::symbol::Kind::Hoisted, b"module")?;
if self.has_var_exports_from_module_exports
&& self
.module_scope()
.get_member_with_hash(b"module", Scope::get_member_hash(b"module"))
.is_some_and(|member| !member.ref_.eql(self.module_ref))
{
self.has_user_declared_exports = true;
}

self.require_ref =
self.declare_common_js_symbol(js_ast::symbol::Kind::Unbound, b"require")?;
Expand Down Expand Up @@ -9732,6 +9747,9 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
hoisted_ref_for_sloppy_mode_block_fn: Default::default(),
has_with_scope: false,
has_top_level_function_merged_with_var: false,
has_user_declared_module: false,
has_user_declared_exports: false,
has_var_exports_from_module_exports: false,
is_file_considered_to_have_esm_exports: false,
has_called_runtime: false,
symbol_uses,
Expand Down
2 changes: 2 additions & 0 deletions src/js_parser/parse/parse_entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1740,6 +1740,8 @@ impl<'a> Parser<'a> {
&& !p.has_top_level_return
&& !p.has_with_scope
&& !p.has_top_level_function_merged_with_var
&& !p.has_user_declared_module
Comment thread
robobun marked this conversation as resolved.
Resolved
&& !p.has_user_declared_exports
&& p.symbols.as_slice()[p.module_ref.inner_index() as usize].use_count_estimate == 1
&& p.symbols.as_slice()[p.exports_ref.inner_index() as usize].use_count_estimate == 0
{
Expand Down
108 changes: 108 additions & 0 deletions src/js_parser/parse/parse_stmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
p.lexer.next()?;
let decls = p.parse_and_declare_decls(js_ast::symbol::Kind::Hoisted, opts)?;
p.lexer.expect_or_insert_semicolon()?;
if !opts.is_typescript_declare {
p.note_var_shadowing_module_or_exports(decls.slice(), false);
}
Ok(p.s(
S::Local {
kind: js_ast::s::Kind::KVar,
Expand All @@ -161,6 +164,101 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
))
}

/// `var exports;` and `var exports = module.exports;` keep the wrapper value and do not count.
fn note_var_shadowing_module_or_exports(&mut self, decls: &[G::Decl], is_loop_target: bool) {
for decl in decls {
if decl.value.is_none() && !is_loop_target {
continue;
}
match decl.binding.data {
js_ast::b::B::BIdentifier(id) => {
let name = self.load_name_from_ref(id.r#ref);
if name != b"module" && name != b"exports" {
continue;
}
if name == b"exports"
&& !is_loop_target
&& decl
.value
.is_some_and(|value| self.is_module_dot_exports_at_parse(value))
{
self.has_var_exports_from_module_exports |=
self.var_hoists_to_module_scope();
continue;
}
Comment thread
robobun marked this conversation as resolved.
Resolved
if !self.var_hoists_to_module_scope() {
return;
}
if name == b"module" {
self.has_user_declared_module = true;
Comment thread
robobun marked this conversation as resolved.
Resolved
} else {
self.has_user_declared_exports = true;
}
}
js_ast::b::B::BArray(_) | js_ast::b::B::BObject(_) => {
let (module, exports) = self.binding_names_module_or_exports(&decl.binding);
if (module || exports) && self.var_hoists_to_module_scope() {
self.has_user_declared_module |= module;
self.has_user_declared_exports |= exports;
}
}
js_ast::b::B::BMissing(_) => {}
}
}
}

/// (`module`, `exports`) found anywhere in a destructuring pattern.
fn binding_names_module_or_exports(&self, binding: &js_ast::Binding) -> (bool, bool) {
match binding.data {
js_ast::b::B::BIdentifier(id) => {
let name = self.load_name_from_ref(id.r#ref);
(name == b"module", name == b"exports")
}
js_ast::b::B::BArray(array) => {
array.items().iter().fold((false, false), |acc, item| {
let (m, e) = self.binding_names_module_or_exports(&item.binding);
(acc.0 || m, acc.1 || e)
})
}
js_ast::b::B::BObject(object) => {
object
.properties()
.iter()
.fold((false, false), |acc, prop| {
let (m, e) = self.binding_names_module_or_exports(&prop.value);
(acc.0 || m, acc.1 || e)
})
}
js_ast::b::B::BMissing(_) => (false, false),
}
}

fn var_hoists_to_module_scope(&self) -> bool {
let mut scope = self.current_scope_ref();
while !scope.kind_stops_hoisting() {
scope = scope.parent.unwrap();
}
scope == self.module_scope_ref()
}

/// `module.exports` or `module.exports = ...` before the visit pass resolves names.
fn is_module_dot_exports_at_parse(&self, mut expr: Expr) -> bool {
if let Some(bin) = expr.data.e_binary()
&& bin.op == js_ast::OpCode::BinAssign
{
expr = bin.left;
}
let Some(dot) = expr.data.e_dot() else {
return false;
};
dot.name == b"exports"
&& dot
.target
.data
.e_identifier()
.is_some_and(|id| self.load_name_from_ref(id.ref_) == b"module")
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}
Comment thread
robobun marked this conversation as resolved.
Resolved

#[inline]
fn t_const(
p: &mut Self,
Expand Down Expand Up @@ -658,6 +756,9 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
}

p.forbid_initializers(decls_ptr.slice(), "of", false)?;
if is_var {
p.note_var_shadowing_module_or_exports(decls_ptr.slice(), true);
}
p.lexer.next()?;
let value = p.parse_expr(Level::Comma)?;
p.lexer.expect(T::TCloseParen)?;
Expand All @@ -677,6 +778,9 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
// Detect for-in loops
if p.lexer.token == T::TIn {
p.forbid_initializers(decls_ptr.slice(), "in", is_var)?;
if is_var {
p.note_var_shadowing_module_or_exports(decls_ptr.slice(), true);
}
p.lexer.next()?;
let value = p.parse_expr(Level::Lowest)?;
p.lexer.expect(T::TCloseParen)?;
Expand All @@ -692,6 +796,10 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
));
}

if is_var {
p.note_var_shadowing_module_or_exports(decls_ptr.slice(), false);
}

// Only require "const" statement initializers when we know we're a normal for loop
if let Some(init_stmt) = &init_ {
match &init_stmt.data {
Expand Down
5 changes: 2 additions & 3 deletions src/js_parser/visit/visit_expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,9 +258,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
),
),
}
} else if p.exports_ref.eql(e_.ref_) {
// Assigning to `exports` in a CommonJS module must be tracked to undo the
// `module.exports` -> `exports` optimization.
} else if p.exports_ref.eql(e_.ref_) || p.module_ref.eql(e_.ref_) {
// An assignment to `exports` or `module` undoes the `module.exports` -> `exports` rewrite.
p.commonjs_module_exports_assigned_deoptimized = true;
}

Expand Down
Loading
Loading