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
17 changes: 17 additions & 0 deletions src/js_parser/p.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,13 @@ pub struct P<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> {
/// When visiting e_call, if the target ref matches this, we replace the call with
/// a boolean based on whether the feature flag is enabled.
pub(crate) bundler_feature_flag_ref: Ref,

/// Local binding of `createRequire` imported from "module"/"node:module"
/// (only set when bundling).
Comment thread
robobun marked this conversation as resolved.
Resolved
pub(crate) create_require_ref: Ref,
/// `const` bindings initialized with `createRequire(import.meta.url)`.
/// e_call bundles string-literal calls on these like `require()`.
Comment thread
robobun marked this conversation as resolved.
Resolved
pub(crate) create_require_target_refs: RefMap,
/// Set to true when visiting an if/ternary condition. feature() calls are only valid in this context.
pub(crate) in_branch_condition: bool,

Expand Down Expand Up @@ -3867,6 +3874,14 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
let alias: &'a [u8] = item.alias.slice();
self.check_for_non_bmp_code_point(item.alias_loc, alias);

if self.options.bundle
&& !self.is_source_runtime()
&& alias == b"createRequire"
&& (path.text == b"module" || path.text == b"node:module")
{
self.create_require_ref = r#ref;
}

// ensure every e_import_identifier holds the namespace
if self.options.features.hot_module_reloading {
let symbol = &mut self.symbols[r#ref.inner_index() as usize];
Expand Down Expand Up @@ -8766,6 +8781,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
response_ref: Ref::NONE,
bun_app_namespace_ref: Ref::NONE,
bundler_feature_flag_ref: Ref::NONE,
create_require_ref: Ref::NONE,
create_require_target_refs: Default::default(),
in_branch_condition: false,
has_classic_runtime_warned: false,
macro_call_count: 0,
Expand Down
34 changes: 34 additions & 0 deletions src/js_parser/visit/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,24 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
self.react_compiler_candidate_name = Some(id.r#ref);
self.react_compiler_in_react_hoc = in_hoc;
}
// The visit may inline `import.meta.url` to a string (CJS output,
// Bake), so check the argument shape first; callee refs only
// resolve during the visit, so that check happens after.
Comment thread
robobun marked this conversation as resolved.
Resolved
let arg_was_import_meta_url = was_const
&& self.create_require_ref.is_valid()
&& matches!(decl.binding.data, BData::BIdentifier(_))
&& match val.data.e_call() {
Some(call) if call.args.len_u32() == 1 => {
match &call.args.slice()[0].data {
ExprData::EDot(dot) => {
matches!(dot.target.data, ExprData::EImportMeta(_))
&& (dot.name == b"url" || dot.name == b"filename")
}
_ => false,
}
}
_ => false,
};
self.visit_expr_in_out(
&mut val,
ExprIn {
Expand Down Expand Up @@ -352,6 +370,22 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
}
}

// `const` only, so the binding can never be reassigned.
if arg_was_import_meta_url
&& let BData::BIdentifier(id) = decl.binding.data
&& let Some(value) = decl.value
&& let Some(call) = value.data.e_call()
{
let target_ref = match &call.target.data {
ExprData::EIdentifier(ident) => Some(ident.ref_),
ExprData::EImportIdentifier(ident) => Some(ident.ref_),
_ => None,
};
if target_ref.is_some_and(|r| r.eql(self.create_require_ref)) {
self.create_require_target_refs.insert(id.r#ref, ());
}
}

if self.should_unwrap_common_js_to_esm() {
if prev_require_to_convert_count < self.imports_to_convert_from_require.len() {
if let BData::BIdentifier(id) = decl.binding.data {
Expand Down
23 changes: 23 additions & 0 deletions src/js_parser/visit/visit_expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2007,6 +2007,29 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
}
}

// A call through a `const req = createRequire(import.meta.url)` binding
// resolves like `require()`: bundle string-literal arguments the same
// way and leave dynamic arguments as runtime calls on the binding.
Comment thread
robobun marked this conversation as resolved.
Resolved
if !p.create_require_target_refs.is_empty() && e_.args.len_u32() == 1 {
let target_ref = match &e_.target.data {
Data::EIdentifier(ident) => Some(ident.ref_),
_ => None,
};
if let Some(target_ref) = target_ref
&& p.create_require_target_refs.contains_key(&target_ref)
&& matches!(e_.args.slice()[0].data, Data::EString(..))
{
let first = e_.args.slice()[0];
let state = TransposeState {
is_require_immediately_assigned_to_decl: in_.is_immediately_assigned_to_decl,
..Default::default()
};
*e = p.transpose_require(first, &state);
p.ignore_usage(target_ref);
return;
}
}

if matches!(e_.target.data, Data::ERequireCallTarget) {
e_.can_be_unwrapped_if_unused = E::CallUnwrap::Never;

Expand Down
66 changes: 66 additions & 0 deletions test/bundler/bundler_edgecase.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2858,6 +2858,72 @@ describe("bundler", () => {
expect(out).not.toContain("require_foo\u2014bar");
},
});
// https://github.com/oven-sh/bun/issues/36564
// `const req = createRequire(import.meta.url)` resolves relative to the
// current module, so calls with a static string must be bundled like
// require(). The entry lives in /src while the output is /out.js, so an
// unbundled runtime lookup would fail to find the files.
itBundled("edgecase/CreateRequireImportMetaUrl", {
files: {
"/src/entry.js": /* js */ `
import { createRequire } from "module";
const require = createRequire(import.meta.url);
const data = require("./data.json");
const util = require("./util.cjs");
console.log(JSON.stringify(data), util.add(1, 2));
`,
"/src/data.json": `{ "hello": "world" }`,
"/src/util.cjs": /* js */ `
module.exports.add = (a, b) => a + b;
`,
},
target: "bun",
run: { stdout: `{"hello":"world"} 3` },
});
itBundled("edgecase/CreateRequireImportMetaUrlCjsFormat", {
// With CJS output format the visit pass inlines import.meta.url to a
// string, which must not defeat the createRequire detection.
files: {
"/src/entry.js": /* js */ `
import { createRequire } from "module";
const require2 = createRequire(import.meta.url);
console.log(require2("./data.json").value);
`,
"/src/data.json": `{ "value": "cjs-ok" }`,
},
target: "node",
format: "cjs",
run: { stdout: "cjs-ok" },
});
itBundled("edgecase/CreateRequireNodeModuleAliased", {
files: {
"/src/entry.js": /* js */ `
import { createRequire as cr } from "node:module";
const myRequire = cr(import.meta.filename);
console.log(myRequire("./data.json").value);
`,
"/src/data.json": `{ "value": 42 }`,
},
target: "node",
run: { stdout: "42" },
});
itBundled("edgecase/CreateRequireDynamicArgumentUntouched", {
files: {
// A non-string argument keeps calling the binding at runtime; the target
// file sits next to the output so the runtime lookup still resolves.
"/src/entry.js": /* js */ `
import { createRequire } from "module";
const require2 = createRequire(import.meta.url);
const name = "./dy" + "namic.cjs";
console.log(require2(name).x);
`,
"/dynamic.cjs": /* js */ `
module.exports.x = "dyn";
`,
},
target: "bun",
run: { stdout: "dyn" },
});
});

for (const backend of ["api", "cli"] as const) {
Expand Down
Loading