Skip to content

bundler: bundle template-literal require()/import() via glob - #35699

Closed
robobun wants to merge 15 commits into
mainfrom
farm/8ec791a1/bundler-glob-require
Closed

robobun wants to merge 15 commits into
mainfrom
farm/8ec791a1/bundler-glob-require

Conversation

@robobun

@robobun robobun commented Jul 25, 2026 •

Copy link
Copy Markdown
Collaborator

What does this PR do?

Fixes #13672.

When bun build (including --compile) encounters a require() or import() whose argument is a template literal or string concatenation with a relative prefix, it now globs the matching files at build time and bundles them into a lookup map, matching esbuild's behaviour.

Before

// require(`./engines/${f}`) was left as an unbundled runtime call:
var __require = import.meta.require;
const e = __require(`./engines/${f}`);

In a --compile'd binary this resolved relative to /$bunfs/root and failed:

error: Cannot find module './engines/boa.js' from '/$bunfs/root/esvu'

After

var __glob = (map, fallback) => (path) => {
  var fn = map[path];
  if (fn) return fn();
  if (fallback) return fallback(path);
  throw new Error("Module not found in bundle: " + path);
};
const e = __glob({
  "./engines/boa.js": () => require_boa(),
  "./engines/v8.js": () => require_v8(),
}, __require)(`./engines/${f}`);

The matching files are bundled and the lookup succeeds regardless of where the binary runs. On a map miss (for example "./engines/" + "boa" with no extension), __glob falls back to the runtime require / import() so those cases resolve exactly as they did before this change.

How does it work?

When the parser visits require(expr) / import(expr) and expr is not a plain string:

  1. Flatten expr into alternating literal / wildcard segments. Supports untagged template literals and "prefix" + x chains (the left operand of + must itself be analysable so there is a literal prefix to anchor the glob).
  2. Bail unless the first literal starts with ./ or ../ and names a real path component (so a bare ./ + x or ../ + x does not glob-bundle an entire directory tree) and there is at least one wildcard.
  3. Build a glob pattern: a wildcard immediately after / becomes **/* (recurses into subdirectories); a mid-segment wildcard becomes * (current directory only). Literal glob metacharacters (*, ?, [, {) in a template segment bail to the runtime path.
  4. Walk the filesystem from the source file's directory. If nothing matches (or any I/O error), fall through unchanged so allowUnresolved still applies and runtime-created files can still be loaded.
  5. For each match, create a normal ImportKind::Require / ImportKind::Dynamic import record (using the relative specifier, so --external patterns and onResolve plugins still match) and emit __glob({ "./rel/path": () => <require>, ... }, <fallback>)(expr). For import(), the surrounding { with: { type } } options and the .then().catch() / awaited-in-try error-handling flag are threaded through to both the bundled entries and the fallback.

__glob is a new runtime helper in src/runtime.js.

Why is this the correct fix?

The reported bug is not a resolver quirk: the ./engines/*.js files were never bundled because the specifier was dynamic, so there was nothing for the standalone resolver to find. Making the standalone resolver search the build-machine path would only paper over it on the machine that built the binary. Bundling the matches is what actually makes the executable self-contained, and it is the established behaviour in esbuild that users coming from that ecosystem expect. The runtime fallback on map miss keeps everything that worked before working.

Notes

The glob walk runs in the parser using a direct readdir. Each matched file's import record is the relative specifier, so the bundler's resolver, --external matching, and onResolve plugins still see every file. The pattern itself is not exposed to plugins (same as esbuild), and the directory listing is not registered with the resolver's DirEntry cache, so a file added to a globbed directory during --watch does not trigger a rebuild until one of the already-tracked files changes. Moving the walk to bundle_v2::resolve_import_records behind DirEntryAccessor would close that gap; it is a larger refactor left for a follow-up.

Tested by

test/bundler/bundler_glob.test.ts (new, 13 cases): template-literal require, string-concat require, template-literal import(), extensionless-key fallback for require and import(), import(..., { with: { type } }), zero-match fallthrough, bare-package ignored, * vs **/* wildcard placement, ../ prefix, literal-* fallthrough, and the original #13672 readdir + require(\./engines/${f}`)` scenario run as a compiled standalone executable.

9 of the 13 fail on main (the negative cases that assert the glob does not fire, and the extensionless fallback cases that exercise pre-existing runtime resolution, pass on both). test/bundler/bundler_allow_unresolved.test.ts, bundler_cjs.test.ts, and bundler_edgecase.test.ts still pass unchanged.


no test proof · iteration 5 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/bundler/bundler_glob.test.ts

@coderabbitai

coderabbitai Bot commented Jul 25, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

Compile-time glob bundling

Layer / File(s) Summary
Runtime glob import contract
src/ast/runtime.rs, src/runtime.js
Adds the __glob runtime import slot, indexed lookup support, and a resolver that selects bundled factories or invokes a fallback.
Glob analysis and expansion
src/js_parser/Cargo.toml, src/js_parser/p.rs
Analyzes dynamic import and require specifiers, walks matching files with bun_glob, and generates bundled lookup maps with runtime fallbacks.
Require visitor integration
src/js_parser/visit/visit_expr.rs
Rewrites supported non-string require() arguments through glob handling.
Bundler validation and snapshots
test/bundler/bundler_glob.test.ts, test/bundler/bundler_promiseall_deadcode.test.ts, test/regression/issue/cyclic-imports-async-bundler.test.js, test/bundler/html-import-manifest.test.ts
Tests bundled matches, fallback behavior, wildcard rules, import options, CLI and symlink execution, and updated generated snapshots.

Possibly related PRs

  • oven-sh/bun#35676: Implements related compile-time globbed dynamic require()/import() bundling and __glob runtime wiring.

Suggested reviewers: jarred-sumner

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes address #13672 by bundling dynamic relative imports/requires for compiled binaries and adding the needed runtime fallback.
Out of Scope Changes check ✅ Passed The extra snapshot updates appear to be fallout from the bundler/runtime output changes, not unrelated feature work.
Title check ✅ Passed The title clearly and concisely summarizes the main change: bundling dynamic template-literal require/import calls via globbing.
Description check ✅ Passed The description matches the template and includes both the change summary and verification details, with sufficient implementation context.

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

@robobun

robobun commented Jul 25, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 4:21 AM PT - Jul 26th, 2026

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

  • 📦 Binary size — 12 over 0.50 MB
  • targetthis build canary: main #79916
    sizeΔ
    ❌ bun-darwin-aarch6458.13 MB57.58 MB+564.9 KB
    ❌ bun-darwin-x6463.48 MB62.95 MB+544.5 KB
    ❌ bun-linux-aarch6470.98 MB70.42 MB+576.0 KB
    ❌ bun-linux-x6472.48 MB71.95 MB+544.0 KB
    ❌ bun-linux-aarch64-musl64.88 MB64.32 MB+576.0 KB
    ❌ bun-linux-x64-musl66.99 MB66.45 MB+560.0 KB
    ❌ bun-linux-aarch64-android78.47 MB77.97 MB+512.0 KB
    ❌ bun-linux-x64-android80.64 MB80.10 MB+545.2 KB
    ❌ bun-freebsd-x6483.10 MB82.56 MB+560.0 KB
    ❌ bun-freebsd-aarch6484.85 MB84.31 MB+560.0 KB
    ❌ bun-windows-x6480.27 MB79.70 MB+586.0 KB
    ❌ bun-windows-aarch6470.87 MB70.34 MB+547.0 KB

    Add [skip size check] to the commit message if this increase is intentional.


🧪   To try this PR locally:

bunx bun-pr 35699

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

bun-35699 --bun

@github-actions

Copy link
Copy Markdown
Contributor

Found 3 issues this PR may fix:

  1. Support Run-time import paths / template literals in Bun.build() #14877 - Explicitly requests esbuild-style __glob({...}) transformation for template literal imports like import(`./engines/${engine}.js`), which is exactly what this PR implements
  2. Importing dependency using dynamic require() calls does not work #6004 - Dynamic require() with template literals/string concatenation (e.g. selenium-webdriver's path patterns) fails after bundling; reporter references esbuild's glob support as the desired fix
  3. using dynamic import causes __require to be emitted when bundling #12615 - Dynamic import() causes __require shim to be emitted with "Dynamic require not supported" error; glob-based resolution of analyzable dynamic imports replaces this broken output

If this is helpful, copy the block below into the PR description to auto-close these issues on merge.

Fixes #14877
Fixes #6004
Fixes #12615

🤖 Generated with Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. bundler: bundle template-literal and concatenated require()/import() specifiers via a __glob lookup map #35680 - Also adds a __glob runtime helper to bundle template-literal require()/import() calls by glob-matching files at build time. Both PRs implement the same mechanism (glob pattern extraction, __glob lookup map emission) targeting the same bundler code paths; bundler: bundle template-literal and concatenated require()/import() specifiers via a __glob lookup map #35680 fixes Cannot find module "client.node" using bun build with @tigerbeetle/tigerbeetle-node #9951 (native .node addon loaders) while this PR fixes bun build --compile error: Cannot find module "./engines/boa.js" from "/$bunfs/root/esvu" #13672, but the implementation is functionally identical.
  2. bundler: bundle require("./dir/" + x) by scanning the directory (shelljs) #35676 - Also bundles dynamic require() calls whose argument is a string concatenation or template literal with a relative prefix, also using a __glob lookup map, targeting the same bundler visitor code. bundler: bundle require("./dir/" + x) by scanning the directory (shelljs) #35676 fixes ubable to package into binary or minify shelljs #12302 (shelljs pattern require('./src/' + command)) but the approach and affected files overlap substantially with this PR.

🤖 Generated with Claude Code

Comment thread src/js_parser/p.rs Outdated
Comment thread src/js_parser/p.rs
Comment thread src/runtime.js Outdated
Comment thread src/js_parser/p.rs Outdated
Comment thread src/js_parser/visit/visit_expr.rs
Comment thread src/js_parser/p.rs Outdated
Comment thread src/js_parser/p.rs Outdated
Comment thread src/js_parser/p.rs Outdated
Comment thread src/js_parser/p.rs Outdated
Comment thread src/js_parser/p.rs Outdated
Comment thread src/js_parser/p.rs Outdated
Comment thread src/js_parser/p.rs Outdated
Comment thread src/js_parser/p.rs Outdated
Comment thread src/js_parser/p.rs Outdated
Comment thread src/js_parser/p.rs Outdated
Comment thread src/js_parser/p.rs Outdated
Comment thread src/js_parser/p.rs Outdated
Comment thread src/runtime.js Outdated

@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.

Additional findings (outside current diff — PR may have been updated during review):

  • 🟡 entry.js:1-3 — This looks like a scratch file used to manually test dynamic import() while developing the feature — it self-imports and logs, and nothing in src/, test/, or the build references it. Please git rm entry.js before merge so it doesn't land at the repo root.

    Extended reasoning...

    What the issue is

    The PR adds a new file entry.js at the repository root:

    export const id = "entry";
    const v = await import("./entry.js");
    console.log("loaded", v.id);

    This is a scratch reproduction script — it self-imports via a dynamic import() and logs the result, which is exactly the kind of one-off you'd write to eyeball what the new glob-bundling logic does with a dynamic specifier. It was staged as part of commit bcb96434 ("glob-bundle: fall back to runtime require/import on map miss") and left in.

    Why it doesn't belong

    • Nothing references it. Grep across the repo for entry.js hits only bundler test fixtures that use /entry.js as a virtual path inside itBundled({ files: { "/entry.js": ... } }) — none of them read the file at the checkout root. It is not listed in any build script, Cargo.toml, or test harness.
    • The repo root is not where fixtures live. Test fixtures go under test/ (specifically test/bundler/ for this feature, per the writing-bundler-tests convention and CLAUDE.md's "Create tests in the right folder"). Bun's repo root contains build config (Cargo.toml, package.json, CMakeLists.txt, etc.), not application entry points.
    • The PR's own tests already cover this shape. test/bundler/bundler_glob.test.ts exercises template-literal import() (glob/ImportTemplateLiteral) and the runtime-fallback path (glob/ExtensionlessFallbackImport), so this file is not standing in for a missing test.

    Impact

    None functionally — the build, the runtime, and the test suite are all unaffected. It is pure repo clutter: a stray top-level .js file that will show up in every fresh clone and in ls at the root. That is why this is filed as a nit rather than a blocking finding.

    Step-by-step proof

    1. git log --oneline -- entry.js shows a single commit, bcb96434, which is one of this PR's two commits — the file did not exist on main.
    2. The file's contents (await import("./entry.js") on itself + console.log) have no side effects reachable from any Bun entry point; they only run if someone explicitly executes bun entry.js from the checkout root.
    3. rg -l 'entry\.js' across the repo returns only test files that construct in-memory /entry.js fixtures via the itBundled/expectBundled harness; none dereference <repo>/entry.js.
    4. Therefore removing it cannot break anything, and keeping it serves no purpose.

    Fix

    git rm entry.js

    and amend/push.

Comment thread test/bundler/bundler_glob.test.ts
Comment thread src/js_parser/p.rs
Comment thread src/js_parser/p.rs
Comment thread src/js_parser/p.rs
Comment thread src/js_parser/p.rs Outdated
Comment thread src/js_parser/p.rs
Comment thread src/js_parser/p.rs
Comment thread src/js_parser/p.rs
Comment thread src/js_parser/p.rs
Comment thread src/js_parser/p.rs
Comment thread src/js_parser/p.rs
@robobun
robobun force-pushed the farm/8ec791a1/bundler-glob-require branch from 0e121de to aaeea8b Compare July 26, 2026 01:15
Comment thread test/bundler/bundler_glob.test.ts Outdated
Comment thread src/js_parser/p.rs
Comment thread src/js_parser/p.rs
Comment thread test/bundler/bundler_glob.test.ts Outdated

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/bundler/bundler_glob.test.ts`:
- Around line 1-4: Add tests in bundler_glob.test.ts covering generated glob
entries with plugin onResolve handling and external resolution, not only normal
resolution. Extend the existing glob test matrix to exercise both behaviors
across the supported alternate modes, verifying the generated import records
honor each contract.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: d335419a-a945-4ff1-bafb-04f5970d7126

📥 Commits

Reviewing files that changed from the base of the PR and between d95053f and fe183a6.

📒 Files selected for processing (3)
  • src/js_parser/p.rs
  • test/bundler/bundler_glob.test.ts
  • test/bundler/html-import-manifest.test.ts

Comment thread test/bundler/bundler_glob.test.ts Outdated

@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.

No bugs found after several rounds of fixes, but this is a new user-facing bundler feature (~350 lines of parser logic, a new __glob runtime helper, and a new bun_glob dep on bun_js_parser) with design choices worth a maintainer's sign-off — notably the parser walking the filesystem directly (with the documented --watch gap) rather than going through the resolver.

What was reviewed:

  • Prefix guard now requires a real path component — bare ./, ../, and stacked ../../ all bail (covered by BareDotSlashFallsThrough / BareDotDotSlashFallsThrough).
  • glob_parts_from_expr has the same stack-overflow guard as the other recursive walkers; rope heads/tails are flattened before reading.
  • TransposeState is threaded so with: { type }, import_record_tag, and HANDLES_IMPORT_ERRORS reach both the bundled entries and the fallback; the fallback's p param is registered in the scope so it renames correctly.
  • ALL_SORTED / ALL_SORTED_INDEX in runtime.rs re-derived and match; snapshot hash/debugId updates are the expected consequence of runtime.js changing.
Extended reasoning...

Overview

Adds esbuild-style glob bundling for require()/import() with template-literal or +-concat arguments. Touches src/js_parser/p.rs (new handle_glob_pattern, glob_parts_from_expr, walk_glob_for_bundle, wired into transpose_import, transpose_require's fallback arm, and the direct require() call arm in visit_expr.rs), src/runtime.js (new __glob helper), src/ast/runtime.rs (registers __glob in the runtime-imports tables), and adds bun_glob as a workspace dep of bun_js_parser. New 425-line bundler_glob.test.ts with 17 cases plus a manual symlink test; three unrelated snapshot files updated for the runtime.js content-hash change.

Security risks

None identified. The glob walk is anchored to the source file's directory, requires a ./ or ../ prefix with a non-dot/slash character, bails on literal glob metacharacters in template segments, and follows symlinks with the walker's built-in cycle protection. This is build-time filesystem enumeration of the user's own project, not runtime input handling.

Level of scrutiny

High. This is new user-facing bundler behavior that changes what gets pulled into every bundle containing a dynamic relative require/import. It runs in the parser hot path (gated on options.bundle), does direct readdir I/O outside the resolver's DirEntry cache (the PR description flags the resulting --watch gap as a follow-up), and encodes several heuristics (* vs **/* placement, dots-and-slashes-only prefix bail, zero-match fallthrough, runtime fallback on map miss). Those are reasonable choices that match esbuild, but they are API-shaped decisions a maintainer should confirm.

Other factors

The PR has been through extensive iteration — earlier rounds caught and fixed dropped import attributes, unflattened rope segments, Windows path-separator handling, the missing transpose_require wiring, an unregistered fallback param symbol, the bare-../ greedy-glob hole, missing stack-overflow guard, follow_symlinks, and undrained subprocess stdout. All threads are resolved and the current diff looks correct. Test coverage is thorough (positive/negative, require/import, ternary, folded constants, onResolve/--external, symlinks, and the original #13672 repro under --compile). Given the scope and the architectural note about moving the walk behind DirEntryAccessor, deferring to a human for final sign-off.

robobun and others added 14 commits July 26, 2026 03:59
When bundling, require(`./dir/${x}`) and import(`./dir/${x}`) now glob
./dir/**/* at build time and emit a lookup map of bundled modules,
matching esbuild's behaviour. Previously these calls were left as
unbundled runtime requires, so in a --compile'd binary they resolved
relative to /$bunfs/root and failed with 'Cannot find module'.

Fixes #13672
- __glob(map, fallback) tries fallback(path) on miss, so extensionless or
  directory-index specifiers ("./engines/" + "boa") still resolve the
  way they did before the matching files were bundled.
- Thread TransposeState into the import() path so {with:{type}} and the
  .then().catch() / awaited-in-try error-handling heuristic are applied
  to the bundled matches and the fallback.
- Record matched files by their relative specifier, not the globbed
  absolute path, so --external patterns and onResolve plugins see them.
…es, glob ternary require branches, register fallback param symbol, trim comments
…le-output snapshots for the __glob runtime helper
@robobun
robobun force-pushed the farm/8ec791a1/bundler-glob-require branch from bb83080 to 4daed74 Compare July 26, 2026 04:06

@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.

No new issues found this pass, but deferring to a human — this is a substantial bundler feature (filesystem globbing inside the parser, a new __glob runtime helper, changed bundling semantics for every dynamic relative require/import) with design tradeoffs the PR itself calls out (the --watch gap, walking via direct readdir rather than the resolver's DirEntryAccessor). Worth a maintainer look at the layering and the fallback semantics before landing.

What was reviewed:

  • Prefix guard now requires a non-./ character, so bare ./, ../, and stacked ../../ all fall through — verified against the pattern-builder and covered by BareDotSlashFallsThrough / BareDotDotSlashFallsThrough.
  • __glob fallback threading: import_options, import_loader/tag, and HANDLES_IMPORT_ERRORS are propagated to both the bundled entries and the runtime fallback; the fallback param is registered in generated so it renames instead of shadowing user p.
  • walk_glob_for_bundle now follows symlinks (cycle-safe via GlobWalker), and relative_platform::<Loose, false> emits POSIX keys on Windows.
  • ALL_SORTED / ALL_SORTED_INDEX reshuffle in runtime.rs checked against the derivation test; snapshot/hash updates are the expected fallout of touching runtime.js.
Extended reasoning...

Overview

This PR adds esbuild-style glob bundling for template-literal / string-concat require() and import() arguments. The core change is ~310 new lines in src/js_parser/p.rs (handle_glob_pattern, glob_parts_from_expr, walk_glob_for_bundle), wired into three call sites (transpose_import, transpose_require's catch-all arm, and the direct require() visitor arm in visit_expr.rs). It adds a new __glob runtime helper to src/runtime.js, registers it in src/ast/runtime.rs (with the sorted-index tables reshuffled), adds bun_glob as a bun_js_parser dependency, and ships a 425-line test file plus snapshot/hash updates in four existing test files that are downstream of the runtime.js change.

Security risks

The parser now walks the filesystem based on user-written string prefixes. The guard requiring a non-./ character in the literal prefix, the bail on literal glob metacharacters, and the ./ / ../ prefix requirement together bound the walk to a named subdirectory of the source file (or a named sibling of an ancestor), which is the same trust boundary the resolver already operates in. Matched files become ordinary import records that go through the resolver / --external / onResolve, so no new bypass of existing gates. I don't see a security concern beyond what static require("./x") already implies.

Level of scrutiny

High. This changes bundler output for a broad class of previously-passthrough inputs, does I/O inside the parser (a layering choice the PR notes as a compromise), and adds user-visible behavior that people will depend on. The __glob map-miss fallback keeps prior behavior working, so the regression surface is bounded, but the design choices — parser-side walk vs. resolver-side, the --watch invalidation gap, whether to warn on zero matches, fallback vs. hard error — are exactly the kind of thing REVIEW.md flags as needing maintainer agreement rather than bot approval.

Other factors

Over prior review passes on this PR I raised (and the author fixed) the dropped import attributes, rope-head flattening, Windows path separators, the missing ternary/transpose_require hook, the un-scoped fallback param, the bare-../ greedy-bundle, symlink following, and a couple of test-hygiene nits — all threads are resolved and reflected in the current diff. Test coverage is thorough (17 itBundled cases plus a manual symlink spawn test and the original #13672 repro under --compile). The remaining hash/debugId snapshot churn is mechanical. Nothing blocking from a correctness standpoint; deferring purely on scope and design sign-off.

@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.

No new issues found, but this is a substantial new bundler feature (~350 LoC in the parser, a new __glob runtime helper, filesystem I/O during parsing) with user-visible design decisions — the glob heuristics, the runtime-fallback shape, and the acknowledged --watch/DirEntry-cache gap — that warrant a maintainer sign-off.

What was reviewed:

  • Prefix guard now requires a non-./ character, closing bare ./ and ../ (and stacked ../../) greedy-bundle cases.
  • glob_parts_from_expr recursion is stack-guarded like the other AST walkers; literal glob metacharacters bail.
  • Symlinks under the wildcard are followed (walker has cycle protection); I/O errors and zero matches fall through unchanged.
  • __glob map keys always carry a .//../ prefix, so plain-object prototype lookups (constructor, __proto__) can't collide.
Extended reasoning...

Overview

This PR adds esbuild-style glob bundling for dynamic require()/import() calls whose argument is a template literal or string concatenation with a relative prefix. It touches: src/js_parser/p.rs (~310 new lines: handle_glob_pattern, glob_parts_from_expr, walk_glob_for_bundle), src/js_parser/visit/visit_expr.rs (one new call site), src/runtime.js (new __glob helper), src/ast/runtime.rs (new runtime-import entry + resorted index tables), src/js_parser/Cargo.toml / Cargo.lock (adds bun_glob dep), a new 425-line test file, and hash/debugId snapshot updates driven by the runtime.js change.

Security risks

Low. The glob walk is anchored under the source file's directory and only fires for .//../-prefixed literals that name at least one real path component, so it cannot be steered by attacker-controlled runtime input (it runs at build time on the build machine's filesystem). The __glob runtime helper does a plain-object property lookup on the computed path, but every key the parser emits — and every value the caller can produce given the required literal prefix — starts with ./ or ../, so it cannot collide with Object.prototype members.

Level of scrutiny

High. This is new user-facing bundler behavior: code that previously fell through to a runtime require() now eagerly bundles every file matching a synthesized glob. That is the intended fix for #13672 and matches esbuild, but it changes output for existing projects and involves several heuristics (when to bail, * vs **/*, symlink following, fallback semantics) plus a documented follow-up (the walk uses a direct readdir rather than the resolver's DirEntry cache, so --watch won't pick up new files in a globbed directory). Those are the kind of API/architecture calls a maintainer should confirm.

Other factors

All earlier review threads (bare-../ guard, stack-overflow guard, symlink following, test assertion style, stdout draining, plugin/external coverage) are resolved and reflected in the current diff. Test coverage is broad (17 itBundled cases + a manual symlink test, including the original #13672 --compile scenario). The ALL_SORTED / ALL_SORTED_INDEX table updates in runtime.rs are consistent and covered by the existing unit test in that file. The snapshot churn is expected given runtime.js changed. Nothing blocking from my side; deferring for maintainer sign-off on the feature design.

@robobun

robobun commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator Author

CI state on ea58737: all test failures are flaky (each passed on retry). The only hard failure is the binary-size check, and it is not from this PR: sibling PRs on current main (for example builds 82126 and 82106) report byte-identical deltas against the stale canary #79916 baseline, so the +~550 KB is drift on main since that canary. Reported separately for triage. The diff itself is ready for review.

@robobun

robobun commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator Author

Closing in favor of #35680, which implements the same build-time glob lookup (a __glob map over the files a template-literal or concatenated require()/import() argument can match) for both require() and import(). The runtime fallback on a map miss, the import() option threading, the esbuild recursion rule (a placeholder right after / becomes **/*), and the test shapes from this PR (ternary argument, **/* recursion, parent directory, bare ./ prefix, onResolve plugin, --external, symlinked file) are now in #35680 under the glob-require/* names in test/bundler/bundler_glob_require.test.ts.

@robobun robobun closed this Aug 30, 2026
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.

bun build --compile error: Cannot find module "./engines/boa.js" from "/$bunfs/root/esvu"

1 participant