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
2 changes: 2 additions & 0 deletions docs/bundler/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,8 @@ Each `import()` of a bundled JavaScript module also becomes its own chunk. Tree

With `target: "bun"`, a `require()` of a bundled ES module becomes a chunk of its own as well. The call is emitted as `import.meta.require("./chunk-<hash>.js")` and stays synchronous — Bun evaluates the chunk when the call runs — so a `require()` inside a function that never runs keeps its module out of the startup working set entirely. Tree shaking treats these chunks like `import()` chunks. `require()` of a CommonJS module is unaffected and keeps returning `module.exports`. A `require()` cycle between such chunks follows Bun's runtime `require()` of an ES module: a module required again while its chunk is still being loaded sees the partially initialized CommonJS placeholder (`{}`) rather than the live bindings the in-chunk wrapper provides. For other targets — and with `splitRequire: false` (`--no-split-require`) — the required module is inlined into the calling chunk behind a lazy wrapper instead, because the call must return synchronously.

Either way, `require()` of an ES module returns what it returns in Node.js and in Bun's runtime: a copy of the module namespace object with `__esModule: true` added, or, when the module exports the name `module.exports` (`export { EventEmitter as "module.exports" }`), the value of that export. The same holds for the bundle itself: with `format: "cjs"`, an entry point that exports that name assigns the value to the bundle's `module.exports`, so code that loads the output with `require()` gets it too. A polyfill that `target: "browser"` substitutes for a Node.js builtin exports that name when the builtin's `module.exports` is more than its set of named exports, so `require("events")` is the `EventEmitter` class, `require("assert")` is callable, and `require("path")` is the same object as `import path from "path"`.

#### Tree-shaking `import()` and `require()` results

When the result of a string-literal `import()` or `require()` of an ES module is only ever used to read specific exports, the exports nobody reads are dropped from that module — and whatever only they depended on tree-shakes with them. This applies with and without `splitting`; the module is still loaded lazily, exactly where the call is written.
Expand Down
3 changes: 3 additions & 0 deletions src/ast/import_record.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ bitflags::bitflags! {
/// `IS_UNUSED` for its own reasons (an unused TypeScript import, a macro
/// import), and those records must never be resolved.
const IS_BARREL_DEFERRED = 1 << 18;

/// `require()` of an ES module that exports `module.exports`: `__toCommonJS(exports_foo, 1)`.
const REQUIRE_MODULE_EXPORTS_EXPORT = 1 << 30;
}
}

Expand Down
13 changes: 9 additions & 4 deletions src/bundler/LinkerContext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4182,10 +4182,15 @@ impl<'a> LinkerContext<'a> {
// Its own chunk: the name is a binding of the loaded module.
record.source_index.is_valid()
&& !self.is_external_dynamic_import(record, source_index)
// `require()` returns this export, not the namespace.
&& !(record.kind == ImportKind::Require
&& self.graph.meta.items_resolved_exports()[record.source_index.get() as usize]
.contains(b"module.exports"))
&& !self.require_returns_module_exports_export(record)
}

/// `require()` of an ES module that exports `module.exports` returns that export, not the namespace.
pub(crate) fn require_returns_module_exports_export(&self, record: &ImportRecord) -> bool {
record.kind == ImportKind::Require
&& record.source_index.is_valid()
&& self.graph.meta.items_resolved_exports()[record.source_index.get() as usize]
.contains(b"module.exports")
}
Comment thread
robobun marked this conversation as resolved.
Resolved

/// Whether the export an item of such a record matched can stand in for it.
Expand Down
33 changes: 33 additions & 0 deletions src/bundler/linker_context/postProcessJSChunk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1233,6 +1233,39 @@ pub(crate) fn generate_entry_point_tail_js(
_ => {}
}

// "module.exports = __toCommonJS(exports, 1);" once the body ran, so an external `require()` gets that export too.
if flags.wrap != crate::WrapKind::Cjs
&& flags.force_include_exports_for_entry_point
&& c.graph.meta.items_resolved_exports()[source_index as usize]
.contains(b"module.exports")
{
stmts.push(Stmt::assign(
Expr::init(
E::Dot {
target: Expr::init_identifier(
c.unbound_module_ref,
bun_ast::Loc::EMPTY,
),
name: b"exports".into(),
name_loc: bun_ast::Loc::EMPTY,
..Default::default()
},
bun_ast::Loc::EMPTY,
),
Expr::init(
E::Call {
target: Expr::init_identifier(to_common_js_ref, bun_ast::Loc::EMPTY),
args: bun_ast::ExprNodeList::from_slice(&[
Expr::init_identifier(ast.exports_ref, bun_ast::Loc::EMPTY),
Expr::init(E::Number::new(1.0), bun_ast::Loc::EMPTY),
]),
..Default::default()
},
bun_ast::Loc::EMPTY,
),
));
}

// TODO:
// If we are generating CommonJS for node, encode the known export names in
// a form that node can understand them. This relies on the specific behavior
Expand Down
26 changes: 25 additions & 1 deletion src/bundler/linker_context/scanImportsAndExports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,7 @@ pub(crate) fn scan_imports_and_exports(
exports_kind,
named_exports,
ast_flags: ast_flags_list,
sources: input_files,
});
}
export_star_ctx.as_mut().unwrap().add_exports(
Expand Down Expand Up @@ -700,6 +701,8 @@ pub(crate) fn scan_imports_and_exports(
|| !record.source_index.is_valid()
|| col_ref!(flags)[record.source_index.get() as usize].wrap != WrapKind::Esm
|| col_ref!(exports_kind)[record.source_index.get() as usize] != ExportsKind::Esm
// The value is that export, and `typeof` or `if (x)` observes it.
|| this.require_returns_module_exports_export(record)
{
continue;
}
Expand Down Expand Up @@ -1269,10 +1272,18 @@ pub(crate) fn scan_imports_and_exports(
// and subtle set of transpiler interop issues. See for example
// https://github.com/evanw/esbuild/issues/1591.
if kind == ImportKind::Require {
let mut record_flags = ImportRecordFlags::WRAP_WITH_TO_COMMONJS;
if this.require_returns_module_exports_export(
&col_ref!(import_records_list)[id].as_slice()
[import_record_index as usize],
) {
record_flags |=
ImportRecordFlags::REQUIRE_MODULE_EXPORTS_EXPORT;
}
col!(import_records_list)[id].as_mut_slice()
[import_record_index as usize]
.flags
.insert(ImportRecordFlags::WRAP_WITH_TO_COMMONJS);
.insert(record_flags);
to_common_js_uses += 1;
}
}
Expand Down Expand Up @@ -1593,6 +1604,7 @@ struct ExportStarContext<'a> {
imports_to_bind: *mut [RefImportData],
export_star_records: *mut [bun_alloc::AstVec<u32>],
ast_flags: *mut [AstFlags],
sources: *mut [Source<'a>],
}

impl<'a> ExportStarContext<'a> {
Expand Down Expand Up @@ -1647,6 +1659,14 @@ impl<'a> ExportStarContext<'a> {
continue;
}

let is_node_fallback = {
let path = &col_ref!(self.sources)[other_id].path;
path.namespace == b"node"
&& path
.text
.starts_with(crate::bun_node_fallbacks::IMPORT_PATH)
};

// Collect (alias, name) pairs so the
// loop body can mutably borrow `resolved_exports` / `imports_to_bind`.
let exports_len = col_ref!(self.named_exports)[other_id].keys().len();
Expand All @@ -1664,6 +1684,10 @@ impl<'a> ExportStarContext<'a> {
if alias_slice == b"default" && !reexports_default {
continue;
}
// A builtin has no such export: its polyfill names its own `require()` value with it.
if alias_slice == b"module.exports" && !reexports_default && is_node_fallback {
continue;
}

// This export star is shadowed if any file in the stack has a matching real named export
for prev in self.stack.iter() {
Expand Down
8 changes: 8 additions & 0 deletions src/js_printer/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2846,6 +2846,14 @@ pub(crate) mod __gated_printer {
}
self.print_symbol(meta.exports_ref);
if wrap_with_to_cjs {
if record
.flags
.contains(ImportRecordFlags::REQUIRE_MODULE_EXPORTS_EXPORT)
{
self.print(b",");
self.print_space();
self.print(b"1");
}
self.print(b")");
}
}
Expand Down
11 changes: 11 additions & 0 deletions src/node-fallbacks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@ When using `bun build --target=browser`, if you attempt to import a Node.js modu

For example, if you import `zlib`, the `node-fallbacks/zlib.js` file will be loaded.

## `require()` shape

The polyfills are ES modules. `require("events")` in bundled CommonJS code must still be what it is in Node.js (the `EventEmitter` class, not a namespace object), so each polyfill whose Node.js `module.exports` is not just its set of named exports names that value with the `require(esm)` interop export:

```js
export default EventEmitter;
export { EventEmitter as "module.exports" };
```

The bundler returns that export from `require()`, as Node.js and Bun's runtime do. Keep it the same value as the default export. The export stays inside the polyfill: `export * from "events"` in user code does not re-export it, because the Node.js builtin has no export with that name.

## Not used by Bun's runtime

These files are _not_ used by Bun's runtime. They are only used for the `bun build --target=browser` command.
Expand Down
3 changes: 2 additions & 1 deletion src/node-fallbacks/assert.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as assert from "./node_modules/assert";
import assert from "./node_modules/assert";

export const {
AssertionError,
Expand All @@ -23,3 +23,4 @@ export const {
throws,
} = assert;
export default assert;
export { assert as "module.exports" };
2 changes: 2 additions & 0 deletions src/node-fallbacks/console.js
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
var console = globalThis.console;
export default console;
export { console as "module.exports" };
1 change: 1 addition & 0 deletions src/node-fallbacks/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,7 @@ export {
getMaxListeners,
EventEmitter as init,
listenerCount,
EventEmitter as "module.exports",
once,
setMaxListeners,
};
1 change: 1 addition & 0 deletions src/node-fallbacks/http.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import http from "stream-http";
export var { request, get, ClientRequest, IncomingMessage, Agent, globalAgent, STATUS_CODES, METHODS } = http;
export default http;
export { http as "module.exports" };
3 changes: 2 additions & 1 deletion src/node-fallbacks/https.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as https from "https-browserify";
import https from "https-browserify";
export var {
Agent,
ClientRequest,
Expand All @@ -19,3 +19,4 @@ export var {
} = https;

export default https;
export { https as "module.exports" };
4 changes: 3 additions & 1 deletion src/node-fallbacks/net.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,10 @@ export function isIP(s) {
return 0;
}

export default {
const net = {
isIPv4,
isIPv6,
isIP,
};
export default net;
export { net as "module.exports" };
2 changes: 1 addition & 1 deletion src/node-fallbacks/path.js
Original file line number Diff line number Diff line change
Expand Up @@ -529,5 +529,5 @@ export const posix = /* @__PURE__ */ (p => ((p.posix = p), p))({
posix: null,
});

export { delimiter, sep };
export { delimiter, posix as "module.exports", sep };
export default posix;
2 changes: 1 addition & 1 deletion src/node-fallbacks/punycode.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export * from "./node_modules/punycode";
export { default } from "./node_modules/punycode";
export { default, default as "module.exports" } from "./node_modules/punycode";
3 changes: 2 additions & 1 deletion src/node-fallbacks/querystring.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import querystring_module from "querystring-es3";
export var { unescapeBuffer, unescape, escape, stringify, encode, parse, decode } = querystring_module;
export * as default from "querystring-es3";
export default querystring_module;
export { querystring_module as "module.exports" };
4 changes: 3 additions & 1 deletion src/node-fallbacks/string_decoder.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
// TODO: This depends on a separate buffer polyfill
export { StringDecoder, StringDecoder as default } from "./node_modules/string_decoder";
import string_decoder from "./node_modules/string_decoder";
export var { StringDecoder } = string_decoder;
export { StringDecoder as default, string_decoder as "module.exports" };
4 changes: 3 additions & 1 deletion src/node-fallbacks/sys.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,7 @@
*
* Imported on usage in `bun build --target=browser`
*/
import util from "util";
export * from "util";
export * as default from "util";
export { util as "module.exports" };
export default util;
5 changes: 3 additions & 2 deletions src/node-fallbacks/tty.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ function WriteStream() {
function ReadStream() {
throw new Error("tty.ReadStream is not implemented for browsers");
}
export { ReadStream, WriteStream, isatty };
export default { ReadStream, WriteStream, isatty };
const tty = { ReadStream, WriteStream, isatty };
export { ReadStream, WriteStream, isatty, tty as "module.exports" };
export default tty;
4 changes: 3 additions & 1 deletion src/node-fallbacks/url.js
Original file line number Diff line number Diff line change
Expand Up @@ -738,12 +738,13 @@ export {
URLSearchParams,
Url as Url,
urlFormat as format,
urlModule as "module.exports",
urlParse as parse,
urlResolve as resolve,
urlResolveObject as resolveObject,
};

export default {
const urlModule = {
parse: urlParse,
resolve: urlResolve,
resolveObject: urlResolveObject,
Expand All @@ -756,3 +757,4 @@ export default {
URL,
URLSearchParams,
};
export default urlModule;
30 changes: 26 additions & 4 deletions src/node-fallbacks/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -675,13 +675,35 @@ export function callbackify(original) {

export const TextEncoder = /* @__PURE__ */ globalThis.TextEncoder;
export const TextDecoder = /* @__PURE__ */ globalThis.TextDecoder;
export default {
TextEncoder,
TextDecoder,
promisify,
const util = {
format,
deprecate,
debuglog,
inspect,
types,
isArray,
isBoolean,
isNull,
isNullOrUndefined,
isNumber,
isString,
isSymbol,
isUndefined,
isRegExp,
isObject,
isDate,
isError,
isFunction,
isPrimitive,
isBuffer,
log,
inherits,
_extend,
promisify,
callbackifyOnRejected,
callbackify,
TextEncoder,
TextDecoder,
};
export default util;
export { util as "module.exports" };
2 changes: 1 addition & 1 deletion src/node-fallbacks/zlib.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export * from "browserify-zlib";
export { default } from "browserify-zlib";
export { default, default as "module.exports" } from "browserify-zlib";
9 changes: 8 additions & 1 deletion src/runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,14 @@ export var __toESM = (mod, isNodeMode, target) => {
// Converts the module from ESM to CommonJS. This clones the input module
// object with the addition of a non-enumerable "__esModule" property set
// to "true", which overwrites any existing export named "__esModule".
export var __toCommonJS = from => {
// With `useModuleExports`, a set "module.exports" export (Node's `require(esm)` interop) is the result instead.
export var __toCommonJS = (from, useModuleExports) => {
if (useModuleExports) {
try {
var moduleExports = from["module.exports"];
} catch {}
if (moduleExports != null) return moduleExports;
}
var entry = (__moduleCache ??= new WeakMap()).get(from),
desc;
if (entry) return entry;
Expand Down
Loading
Loading