Skip to content
Merged
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
4 changes: 2 additions & 2 deletions docs/bundler/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1415,11 +1415,11 @@ In each case the member access compiles to a direct reference to `object`, the `

Assignments (`z.x = 1`), optional chains, and non-literal computed keys (`z[key]`) are left as property accesses; `z["object"]` is treated like `z.object`. `export default someImport` is followed only when it ends at a namespace; a default that snapshots a `let` export keeps snapshot semantics.

The same applies to the default import of a CommonJS module whose `exports.x = ...` assignments the bundler lifted to ES module exports, such as `react`, `react-dom` and `scheduler`. The default import of a CommonJS module is its `module.exports`, which is that module's namespace, so `import React from "react"; React.useState()` compiles to a direct call of the lifted `useState` binding. The namespace object is created only when `React` itself is used as a value, and it lists the exports in assignment order, like `module.exports` does. `React.default`, and `ns.default` on `import * as ns`, is the lifted `default` export when the module has one, and otherwise the namespace itself, as `module.exports` is in Node. A module that sets `exports.__esModule` keeps its CommonJS wrapper when the importer is not an ES module by type (`.mjs`, `.mts`, or `"type": "module"`), because the default import then depends on that flag at run time.
The same applies to the default import of a CommonJS module whose `exports.x = ...` assignments the bundler lifted to ES module exports, such as `react` and `scheduler`. The default import of a CommonJS module is its `module.exports`, which is that module's namespace, so `import React from "react"; React.useState()` compiles to a direct call of the lifted `useState` binding. The namespace object is created only when `React` itself is used as a value, and it lists the exports in assignment order, like `module.exports` does. A write through it, `React.useLayoutEffect = React.useEffect`, assigns the lifted binding, so every importer sees the new value, the same as a write to `module.exports`. `React.default`, and `ns.default` on `import * as ns`, is the lifted `default` export when the module has one, and otherwise the namespace itself, as `module.exports` is in Node. A module that sets `exports.__esModule` keeps its CommonJS wrapper when the importer is not an ES module by type (`.mjs`, `.mts`, or `"type": "module"`), because the default import then depends on that flag at run time.

### deprecatedNamespaceObjectSetters

Default `true`. When a namespace object does have to be created, each property currently gets a getter and a setter; the setter accepts `ns.foo = value` without throwing (reads still return the module's binding). Set this to `false` to emit getter-only namespace objects, which is what a future Bun release will do unconditionally.
Default `true`. When a namespace object does have to be created, each property currently gets a getter and a setter; the setter accepts `ns.foo = value` without throwing (reads still return the module's binding). Set this to `false` to emit getter-only namespace objects, which is what a future Bun release will do unconditionally. The namespace of a lifted CommonJS module is not affected: it stands in for `module.exports`, so its setters assign the lifted bindings either way.

<Tabs>
<Tab title="JavaScript">
Expand Down
5 changes: 5 additions & 0 deletions src/bundler/LinkerGraph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,9 @@ pub mod js_meta {
pub cjs_export_copies: CjsExportCopies,
pub wrapper_part_index: Index,
pub dynamic_import_referenced_aliases: DynamicImportReferencedAliases,
/// The parameter of the setters on a lifted CommonJS module's namespace
/// object (`set: (value) => $foo = value`). `Ref::NONE` for other files.
pub lifted_setter_param: Ref,
pub flags: Flags,
}

Expand All @@ -179,6 +182,7 @@ pub mod js_meta {
cjs_export_copies: AstAlloc::vec(),
wrapper_part_index: Index::default(),
dynamic_import_referenced_aliases: DynamicImportReferencedAliases::default(),
lifted_setter_param: Ref::NONE,
flags: Flags::default(),
}
}
Expand All @@ -195,6 +199,7 @@ pub mod js_meta {
cjs_export_copies: CjsExportCopies,
wrapper_part_index: Index,
dynamic_import_referenced_aliases: DynamicImportReferencedAliases,
lifted_setter_param: Ref,
flags: Flags,
}
}
Expand Down
122 changes: 109 additions & 13 deletions src/bundler/linker_context/doStep5.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,11 @@ impl LinkerContext<'_> {
.map(|s| bun_alloc::AstAlloc::vec_from_slice(*s).into_boxed_slice()),
);

// SAFETY: read of this task's own row; step 4 wrote it, nothing in
// step 5 does.
let lifted_setter_param: Ref =
unsafe { *meta.lifted_setter_param.cast::<Ref>().add(id as usize) };

// Export creation uses "sortedAndFilteredExportAliases" so this must
// come second after we fill in that array
c.create_exports_for_file(
Expand All @@ -236,6 +241,7 @@ impl LinkerContext<'_> {
imports_to_bind,
export_aliases,
re_exports_count,
lifted_setter_param,
// Per-row mutable SoA cells (own `id` only — disjoint across tasks).
row_mut!(meta.flags, js_meta::Flags, id),
row_mut!(ast.flags, AstFlags, id),
Expand Down Expand Up @@ -420,6 +426,7 @@ impl LinkerContext<'_> {
imports_to_bind: &[RefImportData],
export_aliases: &[&[u8]],
re_exports_count: usize,
lifted_setter_param: Ref,
meta_flags: &mut js_meta::Flags,
ast_flags: &mut AstFlags,
ast_parts: &mut bun_ast::PartList,
Expand All @@ -441,6 +448,17 @@ impl LinkerContext<'_> {
// 1 property per export
let mut properties =
bun_alloc::ArenaVec::<G::Property>::with_capacity_in(export_aliases.len(), arena);
// A lifted CommonJS module's namespace stands in for `module.exports`:
// writes through it assign the lifted bindings, so every local export
// also gets a setter.
let mut setter_properties = bun_alloc::ArenaVec::<G::Property>::with_capacity_in(
if lifted_setter_param.is_valid() {
export_aliases.len()
} else {
0
},
arena,
);

let mut ns_export_symbol_uses = PartSymbolUseMap::default();
ns_export_symbol_uses
Expand Down Expand Up @@ -550,6 +568,59 @@ impl LinkerContext<'_> {
)),
..Default::default()
});
if lifted_setter_param.is_valid()
&& exp_data.source_index.get() == id
&& self
.graph
.symbols
.get_const(exp_data.import_ref)
.is_some_and(|symbol| symbol.kind != bun_ast::symbol::Kind::Import)
{
let body: &mut [Stmt] = arena.alloc_slice_fill_with(1, |_| {
Stmt::allocate(
arena,
S::Return {
value: Some(Expr::assign(
Expr::init_identifier(exp_data.import_ref, loc),
Expr::init_identifier(lifted_setter_param, loc),
)),
},
loc,
)
});
let args: &mut [G::Arg] = arena.alloc_slice_fill_with(1, |_| G::Arg {
binding: Binding::alloc(
arena,
bun_ast::b::Identifier {
r#ref: lifted_setter_param,
},
loc,
),
..Default::default()
});
setter_properties.push(G::Property {
key: Some(Expr::allocate(
arena,
// SAFETY: as for the getter key above.
E::String::init(unsafe { bun_ptr::detach_lifetime(alias) }),
loc,
)),
value: Some(Expr::allocate(
arena,
E::Arrow {
args: bun_ast::StoreSlice::new_mut(args),
prefer_expr: true,
body: G::FnBody {
stmts: bun_ast::StoreSlice::new_mut(body),
loc,
},
..Default::default()
},
loc,
)),
..Default::default()
});
}
ns_export_symbol_uses
.put_assume_capacity(exp_data.import_ref, SymbolUse { count_estimate: 1 });

Expand Down Expand Up @@ -608,33 +679,58 @@ impl LinkerContext<'_> {
.expect("unreachable");
}

// "__export(exports, { foo: () => foo })"
// "__export(exports, { foo: () => foo })", or for a lifted CommonJS module
// "__exportCjs(exports, { foo: () => foo }, { foo: (value) => foo = value })"
let mut export_ref = Ref::NONE;
if !properties.is_empty() {
export_ref = self.runtime_function(self.export_runtime_function());
let is_lifted = lifted_setter_param.is_valid();
export_ref = self.runtime_function(if is_lifted {
b"__exportCjs"
} else {
self.export_runtime_function()
});
// `bumpalo::Vec` → `Vec` via the global heap;
// `G::PropertyList` is `Vec<Property>` and currently has no
// arena-backed `move_from_list`, so re-own.
let mut owned_props: Vec<G::Property> = Vec::with_capacity(properties.len());
owned_props.extend(properties.drain(..));
let mut args: Vec<Expr> = Vec::with_capacity(3);
args.push(Expr::init_identifier(exports_ref, loc));
args.push(Expr::allocate(
arena,
E::Object {
properties: G::PropertyList::move_from_list(owned_props),
..Default::default()
},
loc,
));
if is_lifted {
let mut owned_setters: Vec<G::Property> =
Vec::with_capacity(setter_properties.len());
owned_setters.extend(setter_properties.drain(..));
args.push(Expr::allocate(
arena,
E::Object {
properties: G::PropertyList::move_from_list(owned_setters),
..Default::default()
},
loc,
));
declared_symbols
.append(DeclaredSymbol {
ref_: lifted_setter_param,
is_top_level: true,
})
.expect("unreachable");
}
emit_export_stmt!(Stmt::allocate(
arena,
S::SExpr {
value: Expr::allocate(
arena,
E::Call {
target: Expr::init_identifier(export_ref, loc),
args: bun_ast::ExprNodeList::from_slice(&[
Expr::init_identifier(exports_ref, loc),
Expr::allocate(
arena,
E::Object {
properties: G::PropertyList::move_from_list(owned_props),
..Default::default()
},
loc,
),
]),
args: bun_ast::ExprNodeList::from_owned_slice(args.into_boxed_slice()),
..Default::default()
},
loc,
Expand Down
38 changes: 32 additions & 6 deletions src/bundler/linker_context/scanImportsAndExports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ pub(crate) fn scan_imports_and_exports(
ast.dynamic_import_aliases;
let dyn_ref_aliases: *mut [js_meta::DynamicImportReferencedAliases] =
meta.dynamic_import_referenced_aliases;
let lifted_setter_params: *mut [Ref] = meta.lifted_setter_param;

{
// Step 1: Figure out what modules must be CommonJS
Expand Down Expand Up @@ -556,6 +557,22 @@ pub(crate) fn scan_imports_and_exports(
col!(flags)[source_index] = flag;
}

// The namespace object of a lifted CommonJS module stands in for
// `module.exports`, so its properties get setters that assign the
// lifted bindings. Step 5 runs in parallel and cannot create the
// setters' parameter symbol, so create it here.
if export_kind != ExportsKind::Cjs
&& flag.wrap != WrapKind::Cjs
&& col_ref!(ast_flags_list)[source_index]
.contains(AstFlags::COMMONJS_LIFTED_TO_ESM)
{
col!(lifted_setter_params)[source_index] = this.graph.generate_new_symbol(
source_index_.get(),
SymbolKind::Other,
b"value",
);
}

let wrapped_ref = col_ref!(wrapper_refs)[source_index];

// Create the wrapper part for wrapped files. This is needed by a later step.
Expand Down Expand Up @@ -627,6 +644,7 @@ pub(crate) fn scan_imports_and_exports(
// const needs_export_symbol_from_runtime: []const bool = this.graph.meta.items().needs_export_symbol_from_runtime;

let mut runtime_export_symbol_ref: Ref = Ref::NONE;
let mut runtime_export_cjs_symbol_ref: Ref = Ref::NONE;
let mut ident_scratch: Vec<u8> = Vec::new();
let module_preload = this.module_preload();
this.entry_point_part_indices = vec![u32::MAX; col_ref!(import_records_list).len()];
Expand Down Expand Up @@ -790,17 +808,25 @@ pub(crate) fn scan_imports_and_exports(
// previous step. The previous step can't do this because it's running in
// parallel and can't safely mutate the "importsToBind" map of another file.
if flag.needs_export_symbol_from_runtime {
if !runtime_export_symbol_ref.is_valid() {
runtime_export_symbol_ref =
this.runtime_function(this.export_runtime_function());
}
let export_symbol_ref = if col_ref!(lifted_setter_params)[id].is_valid() {
if !runtime_export_cjs_symbol_ref.is_valid() {
runtime_export_cjs_symbol_ref = this.runtime_function(b"__exportCjs");
}
runtime_export_cjs_symbol_ref
} else {
if !runtime_export_symbol_ref.is_valid() {
runtime_export_symbol_ref =
this.runtime_function(this.export_runtime_function());
}
runtime_export_symbol_ref
};

debug_assert!(runtime_export_symbol_ref.is_valid());
debug_assert!(export_symbol_ref.is_valid());

this.graph.generate_symbol_import_and_use(
source_index,
bun_ast::NAMESPACE_EXPORT_PART_INDEX,
runtime_export_symbol_ref,
export_symbol_ref,
1,
Index::RUNTIME,
)?;
Expand Down
13 changes: 13 additions & 0 deletions src/runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -340,3 +340,16 @@ export var __promiseAll = args => Promise.all(args);
// React Compiler memo-cache slot sentinels.
export var __MEMO_CACHE_SENTINEL = /* @__PURE__ */ Symbol.for("react.memo_cache_sentinel");
export var __EARLY_RETURN_SENTINEL = /* @__PURE__ */ Symbol.for("react.early_return_sentinel");

// The namespace of a CommonJS module whose `exports.x = ...` were lifted to
// bindings stands in for `module.exports`: a write through it assigns the
// binding, so every importer sees it.
export var __exportCjs = /* @__PURE__ */ (target, getters, setters) => {
for (var name in getters)
__defProp(target, name, {
get: getters[name],
set: setters[name],
enumerable: true,
configurable: true,
});
};
Loading
Loading