Skip to content
Open
Show file tree
Hide file tree
Changes from 11 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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 19 additions & 0 deletions docs/bundler/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1478,6 +1478,25 @@ interface BuildMetafile {
}
```

## Glob imports

A `require()` or `import()` whose argument is a relative path built from a template literal or string concatenation is bundled like in [esbuild](https://esbuild.github.io/api/#glob). The static parts become a glob pattern, every file that matches is bundled, and the call becomes a lookup in a map from specifier to module:

```ts title="index.ts" icon="/icons/typescript.svg"
const lang = process.env.LANG ?? "en";
const messages = require(`./locales/${lang}.json`); // bundles every ./locales/**/*.json
```

The rules:

- The path must start with `./` or `../` and name a directory or file prefix before the first `${}` or `+`. A bare `require("./" + name)` is left alone.
Comment thread
robobun marked this conversation as resolved.
Resolved
- Static text that contains a glob metacharacter (`*`, `?`, `[`, `{`, `!`, or `\`), or a `.`, `..`, or empty path segment past the leading `./` or `../`, leaves the call as a runtime call.
- A placeholder right after a `/` matches across directories (`**/*`). Anywhere else it matches one path segment (`*`).
- A `const` whose initializer is such a template or concatenation is followed, so ``const file = `./bin/${arch}/addon.node`; require(file)`` is bundled too.
- When the path ends in a placeholder, each file can also be reached without its extension: `require("./src/" + name)` finds `./src/cat.js` for `"cat"`.
- A specifier that is not in the map falls back to the runtime `require()` or `import()`. With `allowUnresolved: []` (or `--reject-unresolved`) the map is a closed set and a miss throws `MODULE_NOT_FOUND`.
- A pattern that matches no files, or more than 1024 files, leaves the call as a runtime call.

## Outputs

The `Bun.build` function returns a `Promise<BuildOutput>`, defined as:
Expand Down
8 changes: 5 additions & 3 deletions docs/snippets/cli/build.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,14 @@ bun build <entry points>
</ParamField>

<ParamField path="--allow-unresolved" type="string" default="*">
Allow unresolved dynamic import()/require() specifiers matching these glob patterns. Pass <code>''</code> to allow
opaque specifiers
Allow unresolved dynamic import()/require() specifiers matching these glob patterns. A pattern is matched against the
specifier's static parts (a template literal, a string concatenation, or a <code>const</code> bound to one) with
<code>*</code> in place of each dynamic part. Pass <code>''</code> to allow opaque specifiers
</ParamField>

<ParamField path="--reject-unresolved" type="boolean">
Fail the build on any dynamic import()/require() specifier that cannot be resolved at build time
Fail the build on any dynamic import()/require() specifier that cannot be resolved at build time. A relative template
or concatenation that matches files on disk is bundled as a closed glob map and passes
</ParamField>

<ParamField path="--packages" type="string" default="bundle">
Expand Down
9 changes: 8 additions & 1 deletion packages/bun-types/bun.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3254,10 +3254,17 @@ declare module "bun" {
* - `["*"]` (default) — allow all dynamic specifiers
* - `[]` — fail the build on any dynamic specifier
* - `["./locales/*.json", ...]` — allow only specifiers whose static
* template parts match one of these glob patterns
* parts match one of these glob patterns
*
* The static parts come from a template literal, a string concatenation,
* or a `const` bound to one of those; each dynamic part becomes `*`.
* Add `""` to the list to allow fully opaque specifiers like `import(fn())`.
*
* A relative template or concatenation that matches files on disk is
* bundled as a glob map and always passes. The map keeps a runtime
* fallback for specifiers it does not contain only when this option
* allows the shape; with `[]` a miss throws `MODULE_NOT_FOUND`.
*
* @default ["*"]
*/
allowUnresolved?: string[];
Expand Down
46 changes: 26 additions & 20 deletions src/ast/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,10 +173,11 @@ pub struct Imports {
pub(crate) __promiseAll: Ref,
pub(crate) __MEMO_CACHE_SENTINEL: Ref,
pub(crate) __EARLY_RETURN_SENTINEL: Ref,
pub(crate) __glob: Ref,
}

impl Imports {
pub const ALL: [&'static [u8]; 27] = [
pub const ALL: [&'static [u8]; 28] = [
b"__name",
b"__require",
b"__export",
Expand Down Expand Up @@ -204,12 +205,13 @@ impl Imports {
b"__promiseAll",
b"__MEMO_CACHE_SENTINEL",
b"__EARLY_RETURN_SENTINEL",
b"__glob",
];

/// Rust stable cannot sort in `const`; precomputed here and verified by
/// the test in `tests` below.
#[cfg_attr(not(test), allow(dead_code))]
const ALL_SORTED: [&'static [u8]; 27] = [
const ALL_SORTED: [&'static [u8]; 28] = [
b"$$typeof",
b"__EARLY_RETURN_SENTINEL",
b"__MEMO_CACHE_SENTINEL",
Expand All @@ -220,6 +222,7 @@ impl Imports {
b"__export",
b"__exportDefault",
b"__exportValue",
b"__glob",
b"__jsonParse",
b"__legacyDecorateClassTS",
b"__legacyDecorateParamTS",
Expand All @@ -241,34 +244,35 @@ impl Imports {

/// When generating the list of runtime imports, we sort it for determinism.
/// This is a lookup table so we don't need to resort the strings each time
pub const ALL_SORTED_INDEX: [usize; 27] = [
15, // __name
24, // __require
pub const ALL_SORTED_INDEX: [usize; 28] = [
16, // __name
25, // __require
7, // __export
23, // __reExport
24, // __reExport
9, // __exportValue
8, // __exportDefault
14, // __merge
11, // __legacyDecorateClassTS
12, // __legacyDecorateParamTS
13, // __legacyMetadataTS
22, // __publicField
18, // __privateIn
17, // __privateGet
16, // __privateAdd
20, // __privateSet
19, // __privateMethod
15, // __merge
12, // __legacyDecorateClassTS
13, // __legacyDecorateParamTS
14, // __legacyMetadataTS
23, // __publicField
19, // __privateIn
18, // __privateGet
17, // __privateAdd
21, // __privateSet
20, // __privateMethod
6, // __decoratorStart
5, // __decoratorMetadata
25, // __runInitializers
26, // __runInitializers
4, // __decorateElement
0, // $$typeof
26, // __using
27, // __using
3, // __callDispose
10, // __jsonParse
21, // __promiseAll
11, // __jsonParse
22, // __promiseAll
2, // __MEMO_CACHE_SENTINEL
1, // __EARLY_RETURN_SENTINEL
10, // __glob
];

pub const NAME: &'static [u8] = b"bun:wrap";
Expand Down Expand Up @@ -304,6 +308,7 @@ impl Imports {
24 => self.__promiseAll,
25 => self.__MEMO_CACHE_SENTINEL,
26 => self.__EARLY_RETURN_SENTINEL,
27 => self.__glob,
_ => return None,
};
r.to_nullable()
Expand Down Expand Up @@ -339,6 +344,7 @@ impl Imports {
24 => Some(&mut self.__promiseAll),
25 => Some(&mut self.__MEMO_CACHE_SENTINEL),
26 => Some(&mut self.__EARLY_RETURN_SENTINEL),
27 => Some(&mut self.__glob),
_ => None,
}
}
Expand Down
1 change: 1 addition & 0 deletions src/bundler/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ bun_collections.workspace = true
bun_css.workspace = true
bun_dotenv.workspace = true
bun_event_loop.workspace = true
bun_glob.workspace = true
bun_http_types.workspace = true
bun_parsers.workspace = true
bun_io.workspace = true
Expand Down
1 change: 1 addition & 0 deletions src/bundler/ParseTask.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2445,6 +2445,7 @@ pub mod parse_worker {
// a worker-owned `Transpiler` that outlives the parse.
// SAFETY: ARENA — `topts` outlives `opts` (worker-owned for the bundle pass).
opts.allow_unresolved = unsafe { bun_collections::detach_ref(&topts.allow_unresolved) };
opts.glob_resolver = Some(crate::options::parser_glob_resolver);
// `Transpiler.macro_context` is `Option<bun_ast::Macro::MacroContext>`
// (same nominal type as `ParserOptions.macro_context`'s pointee). Reborrow
// through the raw `*mut Transpiler` so the `&mut MacroContext` is disjoint
Expand Down
48 changes: 48 additions & 0 deletions src/bundler/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,54 @@ pub(crate) fn validate_path(
// `&transpiler.options.allow_unresolved` straight through.
pub use bun_js_parser::options::AllowUnresolved;

/// `glob_resolver` for the parser's template-literal `require()` / `import()`
/// support: every file under `source_dir` matching `pattern`, each as the
/// `./`- or `../`-prefixed POSIX-separated relative specifier the runtime
/// string would have to equal. Hidden entries are skipped (`dot: false`).
Comment thread
robobun marked this conversation as resolved.
Resolved
pub fn parser_glob_resolver(source_dir: &[u8], pattern: &[u8]) -> Vec<Box<[u8]>> {
let mut out: Vec<Box<[u8]>> = Vec::new();
if !(pattern.starts_with(b"./") || pattern.starts_with(b"../")) {
return out;
}

let walker = match bun_glob::BunGlobWalker::init_with_cwd(
pattern, source_dir, /* dot */ false, /* absolute */ true,
/* follow_symlinks */ true, /* error_on_broken_symlinks */ false,
/* only_files */ true, None,
Comment thread
robobun marked this conversation as resolved.
Resolved
) {
Ok(Ok(w)) => w,
_ => return out,
};
let mut walker = Box::new(walker);
let mut iter = bun_glob::walk::Iterator::new(&mut *walker);
if iter.init().map(|m| m.is_err()).unwrap_or(true) {
return out;
}
loop {
match iter.next() {
Ok(Ok(Some(abs))) => {
// `Loose` emits `/` separators, so the key matches the
// string the source builds at runtime on every platform.
Comment thread
robobun marked this conversation as resolved.
Resolved
let rel = bun_paths::resolve_path::relative_platform::<
bun_paths::resolve_path::platform::Loose,
false,
>(source_dir, &abs);
let mut key: Vec<u8> = Vec::with_capacity(rel.len() + 2);
if !(rel.starts_with(b"./") || rel.starts_with(b"../")) {
key.extend_from_slice(b"./");
}
key.extend_from_slice(rel);
out.push(key.into_boxed_slice());
}
Ok(Ok(None)) => break,
// A mid-walk failure would otherwise emit a partial map whose
// keys can never cover the runtime specifier.
Comment thread
robobun marked this conversation as resolved.
Resolved
Ok(Err(_)) | Err(_) => return Vec::new(),
}
}
out
}

// Canonical defs live in `bun_resolver::options` (lower tier; resolver is the
// runtime consumer of `.patterns`/`.abs_paths`/`.node_modules`). Re-export so
// `BundleOptions.external` and `Resolver.opts.external` are the SAME nominal
Expand Down
1 change: 1 addition & 0 deletions src/bundler/transpiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1580,6 +1580,7 @@ impl<'a> Transpiler<'a> {
macro_context: None,
warn_about_unbundled_modules: !target.is_bun(),
allow_unresolved: &p_opts::AllowUnresolved::DEFAULT,
glob_resolver: None,
module_type: to_parser_module_type(this_parse.module_type),
output_format: p_opts::Format::Esm,
transform_only: self.options.transform_only,
Expand Down
Loading
Loading