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: 1 addition & 1 deletion docs/bundler/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ An array of paths corresponding to the entrypoints of your application. Bun gene

A map of file paths to their contents for in-memory bundling: bundle virtual files that don't exist on disk, or override the contents of files that do. This option is only available in the JavaScript API.

You can provide file contents as a `string`, `Blob`, `TypedArray`, or `ArrayBuffer`.
You can provide file contents as a `string`, `Blob`, `TypedArray`, or `ArrayBuffer`. Keys are file paths: a relative key is resolved against the current working directory, like a relative entrypoint, and an import uses the in-memory file when it resolves to that path.

#### Bundle entirely from memory

Expand Down
6 changes: 4 additions & 2 deletions packages/bun-types/bun.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3202,8 +3202,10 @@ declare module "bun" {
* A map of file paths to their contents for in-memory bundling.
*
* Use this to bundle virtual files that don't exist on disk, or override
* the contents of files that do exist on disk. The keys are file paths (which should
* match how they're imported) and the values are the file contents.
* the contents of files that do exist on disk. The keys are file paths and
* the values are the file contents. A relative key is resolved against the
* current working directory, like a relative entrypoint, and an import
* uses the in-memory file when it resolves to that path.
*
* File contents can be provided as:
* - `string` - The source code as a string
Expand Down
229 changes: 90 additions & 139 deletions src/bundler/bundle_v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -869,176 +869,127 @@ pub mod bv2_impl {
}
}

/// Mirrors `JSBundler.FileMap` — virtual in-memory files for the build.
/// The bundler only ever reads `.slice()`, so the moved-down map
/// stores raw bytes.
/// `bun_runtime`'s `from_js` parses JS values via `BlobOrStringOrBuffer`
/// in async (owning-copy) mode and inserts the extracted bytes here.
/// Mirrors `JSBundler.FileMap`: in-memory build files, keyed as [`FileMap::canonical`] spells them.
#[derive(Default)]
pub struct FileMap {
pub map: bun_collections::StringHashMap<Box<[u8]>>,
}
impl FileMap {
pub(crate) fn get(&self, specifier: &[u8]) -> Option<&[u8]> {
pub fn put(&mut self, path: &[u8], contents: Box<[u8]>) -> bun_paths::Result<()> {
let mut buf = bun_paths::path_buffer_pool::get();
let key = Self::canonical(path, &mut **buf)
.ok_or(bun_paths::Error::MaxPathExceeded)?;
self.map.put_assume_capacity(key, contents);
Ok(())
}

/// Canonical key: `/` separators; a non-absolute path is resolved against the cwd.
fn canonical<'b>(path: &[u8], buf: &'b mut [u8]) -> Option<&'b [u8]> {
let mut scratch = bun_paths::path_buffer_pool::get();
let posix = scratch.get_mut(..path.len())?;
posix.copy_from_slice(path);
bun_paths::resolve_path::dangerously_convert_path_to_posix_in_place::<u8>(
posix,
);
if bun_paths::is_absolute(posix) {
let out = buf.get_mut(..posix.len())?;
out.copy_from_slice(posix);
return Some(out);
}
let len = bun_paths::resolve_path::join_abs_string_buf_checked::<
bun_paths::platform::Auto,
>(
bun_resolver::fs::FileSystem::get().top_level_dir,
buf,
&[posix],
)?
.len();
let out = &mut buf[..len];
bun_paths::resolve_path::dangerously_convert_path_to_posix_in_place::<u8>(out);
Some(out)
}

/// Map-owned key and contents for an already canonical path.
fn entry(&self, canonical: &[u8]) -> Option<(&[u8], &[u8])> {
self.map
.get_key_value(canonical)
.map(|(key, contents)| (key.as_ref(), contents.as_ref()))
}

/// `path` stands on its own (entry point or source path), not an import specifier.
fn lookup(&self, path: &[u8]) -> Option<(&[u8], &[u8])> {
if self.map.is_empty() {
return None;
}
#[cfg(not(windows))]
{
self.map.get(specifier).map(|b| b.as_ref())
}
#[cfg(windows)]
{
let mut buf = bun_paths::path_buffer_pool::get();
let normalized =
bun_paths::resolve_path::path_to_posix_buf(specifier, &mut **buf);
self.map.get(normalized).map(|b| b.as_ref())
}
let mut buf = bun_paths::path_buffer_pool::get();
self.entry(Self::canonical(path, &mut **buf)?)
}

pub(crate) fn get(&self, path: &[u8]) -> Option<&[u8]> {
self.lookup(path).map(|(_, contents)| contents)
}

#[inline]
pub fn contains(&self, specifier: &[u8]) -> bool {
if self.map.is_empty() {
return false;
}
#[cfg(not(windows))]
{
self.map.contains_key(specifier)
}
#[cfg(windows)]
{
let mut buf = bun_paths::path_buffer_pool::get();
let normalized =
bun_paths::resolve_path::path_to_posix_buf(specifier, &mut **buf);
self.map.contains_key(normalized)
}
pub fn contains(&self, path: &[u8]) -> bool {
self.lookup(path).is_some()
}
/// Returns a `resolver::Result` for a file in the map, or `None` if
/// not found. Handles direct key matches and relative specifiers
/// joined against `dirname(source_file)` (with Windows
/// drive-letter / separator normalization).
///
/// `arena` is the build's bump arena (`BundleV2::arena()`);
/// the matched key is copied into it so the returned
/// `bun_resolver::Result`'s `Path<'static>` borrows arena memory
/// (lives for the entire build pass) instead of the map's key
/// storage.

/// Resolver hook: a specifier that matches an in-memory file resolves to its map key.
pub(crate) fn resolve(
&self,
arena: &bun_alloc::Arena,
source_file: &[u8],
specifier: &[u8],
) -> Option<bun_resolver::Result> {
if self.map.is_empty() {
if self.map.is_empty() || specifier.is_empty() {
return None;
}

let (key, _) =
if source_file.is_empty() || bun_paths::is_absolute_loose(specifier) {
self.lookup(specifier)?
} else {
self.lookup_import(source_file, specifier)?
};

// SAFETY: ARENA — `arena` is the build-pass bump arena
// (never freed before the `Result` is consumed); detaching the
// borrow lifetime matches the established `Path<'static>`
// convention used throughout `bun_resolver` (PORTING.md
// §Lifetimes: ARENA → `&'bump T`).
let dupe = |key: &[u8]| -> &'static [u8] {
// SAFETY: see ARENA note above — bytes live in the build-pass arena.
unsafe { bun_ptr::detach_lifetime(arena.alloc_slice_copy(key)) }
};

// Direct key match (must use `getKey` to return the map-owned
// key, not the parameter).
#[cfg(not(windows))]
if let Some((key, _)) = self.map.get_key_value(specifier) {
return Some(Self::result_for_key(dupe(key.as_ref())));
}
#[cfg(windows)]
{
let mut buf = bun_paths::path_buffer_pool::get();
let normalized =
bun_paths::resolve_path::path_to_posix_buf(specifier, &mut **buf);
if let Some((key, _)) = self.map.get_key_value(normalized) {
return Some(Self::result_for_key(dupe(key.as_ref())));
}
}

// Also try joining a relative specifier against the importer's
// directory. Relative = not posix-absolute and not Windows
// drive-absolute (e.g. `C:/`).
if !specifier.is_empty() && !bun_paths::is_absolute_loose(specifier) {
// `source_file` may itself be relative (e.g. on Windows
// when the bundler stores paths relative to cwd).
let mut abs_source_buf = bun_paths::path_buffer_pool::get();
let abs_source_file: &[u8] = if bun_paths::is_absolute_loose(source_file) {
source_file
} else {
bun_resolver::fs::FileSystem::instance()
.abs_buf(&[source_file], &mut *abs_source_buf)
};

// Normalize `source_file` to forward slashes (Windows paths
// from the real filesystem may use backslashes).
let mut source_file_buf = bun_paths::path_buffer_pool::get();
let normalized_source_file = bun_paths::resolve_path::path_to_posix_buf::<u8>(
abs_source_file,
&mut **source_file_buf,
);

let mut buf = bun_paths::path_buffer_pool::get();
let source_dir = bun_paths::resolve_path::dirname::<
bun_paths::platform::Posix,
>(normalized_source_file);
// If `dirname` returns empty but the path has a drive
// letter, use the drive root.
let effective_source_dir: &[u8] = if source_dir.is_empty() {
if normalized_source_file.len() >= 3
&& normalized_source_file[1] == b':'
&& normalized_source_file[2] == b'/'
{
&normalized_source_file[0..3] // "C:/"
} else if !normalized_source_file.is_empty()
&& normalized_source_file[0] == b'/'
{
b"/"
} else {
bun_resolver::fs::FileSystem::instance().top_level_dir
}
} else {
source_dir
};
// `.loose` preserves Windows drive letters; normalize
// separators in-place on Windows afterwards.
let joined_len = bun_paths::resolve_path::join_abs_string_buf::<
bun_paths::platform::Loose,
>(
effective_source_dir, &mut **buf, &[specifier]
)
.len();
if cfg!(windows) {
bun_paths::resolve_path::platform_to_posix_in_place::<u8>(
&mut buf[0..joined_len],
);
}
let joined = &buf[0..joined_len];
if let Some((key, _)) = self.map.get_key_value(joined) {
return Some(Self::result_for_key(dupe(key.as_ref())));
}
}

None
}

/// Build a `bun_resolver::Result` for a matched key. `key` must
/// already satisfy `'static` — see [`resolve`], which copies the
/// map-owned key into the build's bump arena before calling here so
/// the resulting `Path<'static>` borrows arena memory rather than
/// forging a `'static` from a map borrow.
#[inline]
fn result_for_key(key: &'static [u8]) -> bun_resolver::Result {
bun_resolver::Result {
let key: &'static [u8] =
unsafe { bun_ptr::detach_lifetime(arena.alloc_slice_copy(key)) };
Some(bun_resolver::Result {
path_pair: bun_resolver::PathPair {
primary: crate::bun_fs::Path::init_with_namespace(key, b"file"),
..Default::default()
},
module_type: crate::options::ModuleType::Unknown,
..Default::default()
}
})
}

/// A relative (or bare) `specifier` imported by `source_file`; an overlong path is not in the map.
fn lookup_import(
&self,
source_file: &[u8],
specifier: &[u8],
) -> Option<(&[u8], &[u8])> {
// `source_file` may be cwd-relative (Windows stores bundler paths that way).
let mut importer_buf = bun_paths::path_buffer_pool::get();
let importer = Self::canonical(source_file, &mut **importer_buf)?;
let source_dir = bun_paths::dirname(importer)?;
// `.loose` preserves drive letters.
let mut buf = bun_paths::path_buffer_pool::get();
let len = bun_paths::resolve_path::join_abs_string_buf_checked::<
bun_paths::platform::Loose,
>(source_dir, &mut **buf, &[specifier])?
.len();
let joined = &mut buf[..len];
bun_paths::resolve_path::dangerously_convert_path_to_posix_in_place::<u8>(
joined,
);
self.entry(joined)
}
}

Expand Down
21 changes: 9 additions & 12 deletions src/runtime/api/JSBundler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ pub mod js_bundler {

/// A map of file paths to their in-memory contents.
/// LAYERING: the data-only struct (`map: StringHashMap<Box<[u8]>>`) and
/// `get`/`contains`/`resolve` live in `bun_bundler::bundle_v2` so the
/// `put`/`get`/`contains`/`resolve` live in `bun_bundler::bundle_v2` so the
/// bundler thread can read it without depending on `bun_runtime`. Only
/// the JS-aware `from_js` constructor lives here.
pub use bun_bundler::bundle_v2::api::JSBundler::FileMap;
Expand Down Expand Up @@ -96,17 +96,14 @@ pub mod js_bundler {
let bytes: Box<[u8]> = blob_or_string.slice().to_vec().into_boxed_slice();
drop(blob_or_string);

// Clone the key since we need to own it.
let mut key = prop.to_owned_slice();

// Normalize backslashes to forward slashes for cross-platform consistency.
// Use dangerouslyConvertPathToPosixInPlace which always converts \ to /
// (uses sep_windows constant, not sep which varies by target).
bun_paths::resolve_path::dangerously_convert_path_to_posix_in_place::<u8>(
key.as_mut_slice(),
);

this.map.put_assume_capacity(&key, bytes);
let key = prop.to_utf8();
if this.put(key.slice(), bytes).is_err() {
return Err(global_this.throw_invalid_arguments(format_args!(
"files: key resolves to a path longer than {} bytes",
bun_paths::MAX_PATH_BYTES
)));
}
drop(key);
}

Ok(this)
Expand Down
Loading
Loading