Skip to content

bundler: require a bundled .node file from the chunk that holds it - #47483

Open
robobun wants to merge 1 commit into
mainfrom
robobun/ec0f35a0/asset-require-from-its-chunk
Open

robobun wants to merge 1 commit into
mainfrom
robobun/ec0f35a0/asset-require-from-its-chunk

Conversation

@robobun

@robobun robobun commented Oct 30, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • A bundled .node addon of a --splitting build does not load when its chunk and the runtime module's chunk are in different directories: error: Cannot find module '../resvgjs.linux-x64-gnu-83v4kzhy.node' from '<out>/entry-<hash>.js'. The build exits 0.
  • The linker prints the addon's path for its own chunk (src/bundler/Chunk.rs:699). The runtime module's __require (src/bundler/ParseTask.rs:425) resolves it from that module's chunk, so main can also load the wrong addon.
  • No issue reports it, and --external <package> avoids it.

Fix

  • For Bun, the addon calls import.meta.require(<asset>), as a split require() does (bundler: with --splitting --target bun, require() of an ES module is a chunk boundary #40519) and as 1.1.42 did.
  • For Node.js, a chunk without the runtime module makes the require for its addon: __require("node:module").createRequire(import.meta.url)(<asset>).
  • Verified: test/bundler/bundler_splitting.test.ts (33 new cases, 23 fail on main), plus the loader, compile and plugin suites.
  • Self-reviewed: 23 concerns raised, 19 addressed in full. Not done: a draft PR of the per-chunk branch (it needs a ruling first), a Bun.ModuleGraph test (experimental API), a layout fuzzer (follow-up).

Background

  • The runtime module holds the bundler's helpers, in one chunk of a split build.
  • Considered the addon's path relative to the runtime module's chunk: no new call, but the request still resolves from another chunk.

Downsides

  • Per .node file: Bun +10 B. Node.js +46 B and +210k instructions per load, in a chunk without the runtime module.
  • 14 of 208 checked builds stop loading an addon: 10 redirect __require with --define, 4 moved the .node files.
  • User-written require calls stay broken (repro in Notes). Question: take this now, or wait for a require per chunk?
Notes

History. Each version builds and runs its own output (linux-x64, node v26.3.0). The shape is one entry point and an import() page, with --chunk-naming 'chunks/[name]-[hash].[ext]'.

addon request target 1.1.42 1.1.43 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 main this PR
require("./addon.node") bun found not found not found found found not found not found not found found
require("./addon.node") node not found not found not found found found not found not found not found found
import addon from "./addon.node" bun found not found not found found found not found not found not found found
import addon from "./addon.node" node not found not found not found found found not found not found not found found

Trigger. All of these must hold:

  • The build splits, and the addon is bundled, not external.
  • The addon is printed in a chunk other than the one that holds the runtime module. That needs a second chunk that uses a runtime helper.
  • The path of the addon from its own chunk names another file from the directory of the runtime module's chunk.

Examples are entry points in two directories with the default names, a chunks/ directory with entry points at the root, [dir] in --chunk-naming, and [hash] in --entry-naming.

What main does.

  • @resvg/resvg-js 2.4.1, @napi-rs/canvas 0.1.91 and @swc/core 1.3.38, each with entry points in two directories, for both targets: main's build exits 0 and the output throws for 6 of 6. This PR runs 6 of 6. The errors of the last two name the package or npm: Cannot find native binding. npm has a bug related to optional dependencies and Bindings not found.
  • The request can name a file outside --outdir. From out/a/entry.js, main prints __require("../a-required-<hash>.node") and resolves it from out/.
  • With [dir] in --asset-naming and in --chunk-naming, a/ and b/ can each hold an addon of one name. On main, b/entry.js loads the addon of a/ and exits 0. The case AddonOfTheSameNameLoadsFromItsChunk shows it.

The rule, and what it decides. In ESM output, a request that the linker writes for a file that it emits resolves from the chunk that holds the request. __require is a helper of the bundle. #40519 applied this rule to a split require(). For a bundled .node file the rule decides three cases. A maintainer can want each one the other way:

  • --define of import.meta.require or import.meta.url. On main the addon request follows the definition, because __require does. Now Bun's addon calls the real import.meta.require, and for Node.js a chunk without the runtime module uses its real import.meta.url. These are the 10 --define rows of Downsides.
  • A plugin that answers for the node:module import of the runtime module. On main the addon request goes through the plugin's createRequire. Now, for Node.js, a chunk without the runtime module asks __require for node:module and makes its own require from it. Open PR bundler: do not offer the runtime module's imports to onResolve plugins #43111 stops the offer of that import to plugins.
  • require = x in the program. On main, x gets each addon path. Now Bun's addon does not call x, and for Node.js a chunk without the runtime module calls x("node:module"). Open PR bundler: give a file that assigns require its own require variable #46640 gives a file that assigns require a variable of its own. The loader of @node-rs/xxhash 1.7.6 assigns require: for Bun, a split build of such a loader now loads (AddonLoadsWhenItsLoaderAssignsRequire).

An earlier revision kept main's call when a define or a plugin changed the declaration of __require. It cost 61 source lines and no such build is known, so it is not here.

One import that nothing reads. For Bun, an addon in a module without a wrapper (an import addon from "./x.node" that the linker does not wrap) keeps its import of __require. Without that use, the chunk of the runtime module and the order of the chunks change for other entry points (#44495). The case AddonInOneEntryPointKeepsTheOrderOfTheOthers holds this. An addon in a module with a wrapper (__commonJS, __esm) drops the use, as a split require() does.

More behaviour changes.

  • A program that catches the failed require ran without its addon on main. It now loads the addon.
  • In the same-name layout above, two entry points now load two files where main mapped one.
  • Bun.ModuleGraph (experimental), for a Node.js build that runs on Bun: Bun's createRequire takes the graph of its caller, and the new call runs when the addon first loads. If that first use is in a callback of a graph that was disposed, it throws ERR_INVALID_STATE, and main's build loads. A build for Bun is not changed: import.meta.require takes the graph of its module, as __require does.

What is not changed.

The require that a user writes (not fixed). No issue tracks it. The repro:

mkdir -p rq/src/a rq/src/b && cd rq
printf 'import { shared } from "../shared.js";\nconst lib = require("../lib.js");\nconsole.log("a", shared, lib.name);\n' > src/a/entry.js
sed 's/"a"/"b"/' src/a/entry.js > src/b/entry.js
echo 'export const shared = "shared";' > src/shared.js
echo 'module.exports = { name: "lib" };' > src/lib.js
bun build ./src/a/entry.js ./src/b/entry.js --splitting --outdir out --target bun --format esm --external ./src/lib.js
cp src/lib.js out/lib.js
bun out/a/entry.js
  • It prints error: Cannot find module '../lib.js' from '<dir>/out/entry-jjdkv1j6.js'. Without --splitting it prints a shared lib. --target node with Node.js fails in the same way.
  • require(name), a require that the program passes to a function, require.resolve("../lib.js") and require.resolve(name) fail in the same way.
  • --target bun printed import.meta.require("../lib.js") in 1.1.42 and ran. It fails from 1.1.43 (transpiler: dont inline import.meta.require #16222). --target node fails on every version from 1.1.42 to 1.4.4.
  • The branch robobun/ec0f35a0/chunk-own-require has one design: a chunk in another directory than the runtime module gets a require of its own. Its measurements are in scratch/chunk-own-require/RESULTS.txt there. That design replaces the call that this PR prints for Node.js. It needs a maintainer's decision first, because it changes which require object a chunk has.

Considered, in more words.

  • The path relative to the runtime module's chunk, with __require kept. By a rewrite of main's output (not built), 196 of the 208 rows below print what main prints or are fixed, against 194 for this PR. It prints no new call and keeps the 10 --define rows. It does not fix a relative --public-path, and it leaves 8 rows for Bun as on main: the second build with --compile --aot. It makes the addon's path the one path in a chunk that is not relative to that chunk (src/bundler/Chunk.rs:699-711), the call still resolves from another chunk, and a require per chunk has to remove it again.
  • For Node.js, main's call kept for a chunk in the directory of the runtime module's chunk. It saves the 46 B there, and needs the output directory of each chunk before the chunks are printed.

Measurements (release builds of f7607a3 and of this change on it, linux-x64, node v26.3.0).

  • Tests: 33 new cases. On main 23 fail. The 10 that pass on main are 2 builds without code splitting, 1 build with the addon beside the runtime module, 2 standalone executables (their paths are absolute), the 2 chunk-order cases, and 3 cases that hold the limits of the change. With the Loader::Napi check removed from the printer path, those 3 fail. With the one of the link-time path removed, 1 of them fails. No other case of the file fails for these two edits.
  • 208 build-and-run rows for both targets, plain and --minify. Node.js: 100 print what main prints or are fixed, 8 differ. Bun: 94, and 6 differ. The 14 rows that differ are the 10 --define rows and the 4 moved-file rows of Downsides.
  • 512 cells of request forms by placement: 24 differ, all from MODULE_NOT_FOUND to a loaded addon. 0 cells of a build that does not split differ.
  • 60 builds of 3 inputs: the 52 builds without a .node file in a split build are byte-identical.
  • bun build --compile --splitting in 5 modes (plain, --bytecode --format esm, --aot, --no-split-require, and the bytecode mode with --no-split-require): each executable prints what main's prints.
  • Output bytes for each .node file. Bun: +10 B (+18 B minified), and -61 B once when a module with a wrapper was the one user of __require. Node.js, in a chunk without the runtime module: +46 B.
  • Instructions for each load (the count for 501 addons minus the count for 1, divided by 500, minimum of 7 runs). Node.js, in a chunk without the runtime module: 316k to 525k for a required addon, and 209k to 338k for an imported one with process.dlopen replaced. Bun: 113.7k to 114.7k for a required addon and 35.5k to 36.6k for an imported one, with noise of about 1k.
  • Binary: text +1,024 B (138,087,639 to 138,088,663).
  • valgrind, strace, perf and bloaty are not installed here. Instructions are counted with perf_event_open.

[human-review] gate passed · iteration 0 · 6 files touched

fails on main (without fix)
ASAN without fix: 23 failed, 4 skipped
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/bundler/bundler_splitting.test.ts
bun test v1.4.4 (367d939d9)

test/bundler/bundler_splitting.test.ts:
(pass) bundler > splitting/DynamicImportCSSFile [696.67ms]
(pass) bundler > splitting/DynamicImportMultipleCSSImports [468.70ms]
(pass) bundler > splitting/StaticAndDynamicCSSImports [474.44ms]
(pass) bundler > splitting/NestedDynamicImportWithCSS [465.39ms]
(pass) bundler > splitting/SharedCSSBetweenChunks [489.74ms]
(pass) bundler > splitting/DynamicImportChainWithCSS [522.81ms]
(pass) bundler > splitting/ConditionalDynamicImportWithCSS [484.81ms]
(pass) bundler > splitting/MultipleEntryPointsWithSharedCSS [665.50ms]
(pass) bundler > splitting/DynamicImportWithOnlyCSSNoJS [478.44ms]
(pass) bundler > splitting/CircularDynamicImportsWithCSS [456.15ms]
(pass) bundler > splitting/DeadDynamicImportTargetGetsNoChunk-cli [483.59ms]
(pass) bundler > splitting/DeadDynamicImportTargetGetsNoChunk-api [342.50ms]
(pass) bundler > splitting/DeadDynamicImportTargetNoSplittingReference [462.09ms]
(pass) bundler > splitting/DeadDynamicImportTarg
... (truncated)

release without fix: 1 failed, 4 skipped
bun test v1.4.4-canary.1 (45ce1bf95)

test/bundler/bundler_splitting.test.ts:
(pass) bundler > splitting/DynamicImportCSSFile [68.44ms]
(pass) bundler > splitting/DynamicImportMultipleCSSImports [23.47ms]
(pass) bundler > splitting/StaticAndDynamicCSSImports [19.87ms]
(pass) bundler > splitting/NestedDynamicImportWithCSS [18.06ms]
(pass) bundler > splitting/SharedCSSBetweenChunks [17.73ms]
(pass) bundler > splitting/DynamicImportChainWithCSS [17.32ms]
(pass) bundler > splitting/ConditionalDynamicImportWithCSS [16.27ms]
(pass) bundler > splitting/MultipleEntryPointsWithSharedCSS [25.52ms]
(pass) bundler > splitting/DynamicImportWithOnlyCSSNoJS [15.14ms]
(pass) bundler > splitting/CircularDynamicImportsWithCSS [17.12ms]
(pass) bundler > splitting/DeadDynamicImportTargetGetsNoChunk-cli [14.70ms]
(pass) bundler > splitting/DeadDynamicImportTargetGetsNoChunk-api [15.63ms]
(pass) bundler > splitting/DeadDynamicImportTargetNoSplittingReference [18.03ms]
(pass) bundler > splitting/DeadDynamicImportTargetKeptWithoutTreeShaking [13.20ms]
(pass) bundler > splitting/LiveDynamicImportTargetStillGetsChunk [14.93ms]
(pass) bundler > splitting/DynamicImportChainLiveness [17.08ms]
(
... (truncated)
passes on PR (with fix)
ASAN with fix: 4 skipped
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/bundler/bundler_splitting.test.ts
bun test v1.4.4 (367d939d9)

test/bundler/bundler_splitting.test.ts:
(pass) bundler > splitting/DynamicImportCSSFile [948.70ms]
(pass) bundler > splitting/DynamicImportMultipleCSSImports [422.38ms]
(pass) bundler > splitting/StaticAndDynamicCSSImports [511.92ms]
(pass) bundler > splitting/NestedDynamicImportWithCSS [551.20ms]
(pass) bundler > splitting/SharedCSSBetweenChunks [583.79ms]
(pass) bundler > splitting/DynamicImportChainWithCSS [608.28ms]
(pass) bundler > splitting/ConditionalDynamicImportWithCSS [563.87ms]
(pass) bundler > splitting/MultipleEntryPointsWithSharedCSS [833.15ms]
(pass) bundler > splitting/DynamicImportWithOnlyCSSNoJS [529.04ms]
(pass) bundler > splitting/CircularDynamicImportsWithCSS [543.73ms]
(pass) bundler > splitting/DeadDynamicImportTargetGetsNoChunk-cli [544.60ms]
(pass) bundler > splitting/DeadDynamicImportTargetGetsNoChunk-api [372.06ms]
(pass) bundler > splitting/DeadDynamicImportTargetNoSplittingReference [502.59ms]
(pass) bundler > splitting/DeadDynamicImportTarg
... (truncated)

release with fix: 4 skipped
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 1110ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/118] build.rs build_script_build
[2/118] rustc bun_safety 
[3/118] rustc bun_zlib_sys 
[4/118] rustc bun_perf 
[5/118] rustc bun_output 
[6/118] rustc bun_boringssl_sys 
[7/118] rustc bun_brotli 
[8/118] rustc bun_picohttp 
[9/118] rustc bun_zstd 
[10/118] rustc bun_ptr 
[11/118] rustc bun_base64 
[12/118] rustc bun_cares_sys 
[13/118] rustc bun_errno 
[14/118] rustc bun_clap 
[15/118] rustc bun_tcc_sys 
[16/118] rustc bun_lsquic_sys 
[17/118] rustc bun_valkey 
[18/118] rustc bun_text_diff 
[19/118] rustc bun_zlib 
[20/118] rustc bun_paths 
[21/118] rustc bun_collections 
[22/118] rustc bun_url 
[23/118] rustc bun_shell_parser 
[24/118] rustc bun_semver 
[25/118] rustc bun_analytics 
[26/118] rustc bun_tar 
[27/118] rustc bun_http_types 
[28/118] rustc bun_which 
[29/118] rustc bun_boringssl 
[30/118] rustc bun_node_path 
[31/118] rustc bun_spawn_sys 
[32/118] rustc bun_dns 
[33/118] rustc bun_archive 
[34/118] rustc bun_sha_hmac 
[35/118] rustc bun_core 
[36/118] rustc bun_threading 
[37/118] rustc bu
... (truncated)
diff hotspot
src/bundler/LinkerContext.rs                       |  12 +
 src/bundler/ParseTask.rs                           |   2 +
 .../linker_context/generateCodeForFileInChunkJS.rs |   2 +
 .../linker_context/generateCodeForLazyExport.rs    |  34 +-
 src/js_printer/lib.rs                              |  10 +
 test/bundler/bundler_splitting.test.ts             | 453 +++++++++++++++++++++
 6 files changed, 511 insertions(+), 2 deletions(-)

gate history · 1 passed · 0 rejected · iteration 0

evidence per changed file
file                                                      reads  edits  tests
src/bundler/LinkerContext.rs                                  5      6    116
src/bundler/ParseTask.rs                                      4      4    114
…/bundler/linker_context/generateCodeForFileInChunkJS.rs      0      0    114
src/bundler/linker_context/generateCodeForLazyExport.rs       3      4    115
src/js_printer/lib.rs                                         0      0    116
test/bundler/bundler_splitting.test.ts                        8      7    114

In a split build the runtime module declares `__require` in one chunk, and
`__require` resolves a request from that chunk. The linker prints the path
of a bundled `.node` file for the chunk that holds the file. When the two
chunks are in different directories, the addon does not load, or another
addon of the same name loads.

For Bun, the file now calls `import.meta.require`, as a split `require()`
does. For Node.js, a chunk without the runtime module makes the `require`
for its addon: `__require("node:module").createRequire(import.meta.url)`.
@coderabbitai

coderabbitai Bot commented Oct 30, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 15 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 10 included reviews currently available.

Learn how review limits work.

Review configuration:

Run configuration
Commits

Reviewing files that changed from the base of the PR and between f7607a3 and f43878d.

Files selected for processing: 6
  • src/bundler/LinkerContext.rs
  • src/bundler/ParseTask.rs
  • src/bundler/linker_context/generateCodeForFileInChunkJS.rs
  • src/bundler/linker_context/generateCodeForLazyExport.rs
  • src/js_printer/lib.rs
  • test/bundler/bundler_splitting.test.ts
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Oct 30, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 10:32 AM PT - Oct 30th, 2026

❌ @robobun, your commit f43878d has 1 failures in Build #135824 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 47483

That installs a local version of the PR into your bun-47483 executable, so you can run:

bun-47483 --bun

@robobun

robobun commented Oct 30, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: ready for review. PR: #47483

How I reproduced it (main at f7607a3, linux-x64, Node.js v26.3.0):

mkdir -p repro/src/a repro/src/b && cd repro && bun add @resvg/resvg-js@2.4.1
echo 'export const tag = "ran";' > src/tag.js
printf 'import { tag } from "../tag.js";\nimport { Resvg } from "@resvg/resvg-js";\nconst r = new Resvg(`<svg xmlns="http://www.w3.org/2000/svg" width="40" height="30"/>`);\nconsole.log(tag, r.width, "by", r.height);\n' > src/a/entry.js
printf 'import { tag } from "../tag.js";\nconsole.log(tag, require("./other.cjs").name);\n' > src/b/entry.js
echo 'module.exports = { name: "other" };' > src/b/other.cjs
bun build ./src/a/entry.js ./src/b/entry.js --splitting --format esm --target bun --outdir out
bun out/a/entry.js
--target bun, run with Bun --target node, run with Node.js
main error: Cannot find module '../resvgjs.linux-x64-gnu-83v4kzhy.node' from '<dir>/out/entry-wk3gkj6a.js' Error: Cannot find module '../resvgjs.linux-x64-gnu-83v4kzhy.node'
this PR ran 40 by 30 ran 40 by 30

The same build without --splitting prints ran 40 by 30 on main.

CI (build 135824, finished): 183 of 188 jobs passed. The new cases pass on every lane: Linux (glibc, musl, ASAN), Windows x64, Windows arm64, macOS arm64 and macOS x64. The 5 failed jobs have one red test, test/aot/engine/ir-6.test.ts. This PR does not touch it, and the same failure is in build 135385, the final build of #47393, which is the tip of main.

For a maintainer: one review thread is open. It asks whether a --define of import.meta.require or import.meta.url keeps main's call for the addon (thread). The description has one more question: take this now, or wait for a require per chunk.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beyond the inline findings, I also checked that non-splitting builds and CommonJS output are untouched: makes_require_for_asset is false whenever the chunk holds the runtime module (always true without splitting), the Bun rewrite in generate_code_for_lazy_export is gated on code_splitting and skipped for CJS (where calls_runtime_require is already false), and the printer flag is inert when require_ref is None (CJS format and the runtime-source print path, which passes false).

Extended reasoning...

The change threads a per-chunk flag through the linker into the printer so a bundled .node addon is required relative to the chunk that holds it, with a separate import.meta.require rewrite for Bun targets. It touches no security-sensitive surface. Two inline findings (a publicPath regression and the loss of --define import.meta.require on Bun targets) already signal that a human needs to weigh the behavior change; the note above records the paths confirmed unaffected.

Comment thread src/bundler/LinkerContext.rs Resolved
Comment on lines +433 to +441
call.target = Expr::init(
E::Dot {
target: Expr::init(E::ImportMeta {}, loc),
name: b"require".as_slice().into(),
name_loc: loc,
..Default::default()
},
loc,
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Users who build a split Bun-target bundle with --define import.meta.require=... (or import.meta.url) lose their define for every bundled .node addon after merging. The target injected at src/bundler/linker_context/generateCodeForLazyExport.rs:433-441 is a fresh import.meta.require AST node built in the linker, after the parser has already applied defines, so it prints literally; on the base branch the call went through the runtime's __require, whose own import.meta.require the define had rewritten. Fix: make the injected addon call honour the build's defines, e.g. keep __require when import.meta.require is defined or run the define substitution over the injected expression. [also at: src/js_printer/lib.rs:3560 - Users who build a split Node.js or Bun bundle with a .node file and --define import.meta.url=... (or import.meta.require=...) get an addon that no longer loads after this merges.]

Why this was flagged

A user runs bun build --splitting --target bun --define import.meta.require=require entry.js so the output also runs on Node, and entry.js requires a .node addon that lands in the same directory as the runtime chunk. On the base branch the addon call prints __require(<path>) and the runtime module's __require is import.meta.require, which the parser-time define turned into require, so the addon loads. After this change requires_asset_from_import_meta (generateCodeForLazyExport.rs:589-593) is true and lines 433-441 replace the call target with E::Dot { target: E::ImportMeta, name: "require" }; defines are applied in the parser, not here, so the printer at src/js_printer/lib.rs:3278 emits the literal import.meta.require, which is undefined when the output runs on Node, and the addon fails to load. The PR description lists 10 of 208 checked builds that stop loading an addon because they redirect __require with --define; no guard in the diff keeps the old call when such a define is present.

Verification: Defines on import.meta.<x> are applied at parse time (src/js_parser/visit/visit_expr.rs:1358-1361). After this change, src/bundler/linker_context/generateCodeForLazyExport.rs:428-441 replaces the ERequireCallTarget with a freshly built E::Dot { target: E::ImportMeta, name: "require" } after parsing is done. The printer prints E::ImportMeta literally (src/js_printer/lib.rs:3273-3278), so the user's define no longer reaches it.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is true, and it is deliberate. The description states it under Downsides (10 of the 208 checked builds) and under "The rule, and what it decides" in the notes.

Main already prints a literal import.meta.require for the other request that the linker writes for a file that it emits: a split require() (#40519, src/js_printer/lib.rs:2934). A --define does not reach that call. On main at f7607a3:

$ printf 'const tool = () => require("./tool.js");\nconsole.log(tool().name);\n' > main.js
$ printf 'export const name = "tool";\n' > tool.js
$ bun build ./main.js --splitting --format esm --target bun --define 'import.meta.require=globalThis.hostRequire' --outdir out
$ grep -n 'tool-' out/main.js
8:  var tool = () => import.meta.require("./tool-h5n7x3et.js");

So on main, a build that replaces import.meta.require loads its chunks with the real one and its addons with the replacement. With this change, both use the real one.

The example in the comment does not run on main. --define import.meta.require=require prints var __require = require;. Node.js throws ReferenceError: require is not defined in ES module scope, and Bun throws ReferenceError: require is not defined. With this change, Bun finds the addon.

An earlier revision of this branch kept main's call when a define or a plugin changed the declaration of __require. I removed it: it was 61 lines, and no build that needs it is known. If a maintainer wants a define of import.meta.require or import.meta.url to keep main's call for the addon, I can add a check for those two keys.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant