Skip to content
Open
17 changes: 17 additions & 0 deletions docs/bundler/loaders.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import my_toml from "./my_file" with { type: "toml" };
const { default: my_toml } = await import("./my_file", { with: { type: "toml" } });
```

The same file imported with different `type` attributes is a different module each time. An import that the bundler leaves in the output, such as an external package or any import under `--no-bundle`, keeps its `with { ... }` clause as written for every target.

## Built-in loaders

### `js`
Expand Down Expand Up @@ -313,6 +315,21 @@ export default "Hello, world!";

---

### `bytes`

**Bytes loader.** Not the default for any extension. Select it with the `type` import attribute.

Bun reads the file and returns its bytes as a `Uint8Array`.

```js
import bytes from "./model.bin" with { type: "bytes" };
console.log(bytes instanceof Uint8Array); // => true
```

When the file is referenced during a build, Bun inlines the contents into the bundle as base64 and decodes it when the module runs. A standalone executable (`bun build --compile`) embeds the raw bytes instead.

---

### `napi`

**Native addon loader.** Default for `.node`.
Expand Down
11 changes: 11 additions & 0 deletions docs/runtime/file-types.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,17 @@ export default "Hello, world!";

</CodeGroup>

### `bytes`

**Bytes loader**. Not the default for any extension. Select it with the `type` import attribute.

Bun reads the file and returns its bytes as a `Uint8Array`.

```ts
import bytes from "./model.bin" with { type: "bytes" };
console.log(bytes instanceof Uint8Array); // => true
```

### `md`

**Markdown loader**. Default for `.md` and `.markdown`.
Expand Down
3 changes: 2 additions & 1 deletion packages/bun-native-bundler-plugin-api/bundler_plugin.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@ typedef enum {
BUN_LOADER_HTML = 17,
BUN_LOADER_YAML = 18,
BUN_LOADER_XML = 21,
BUN_LOADER_BYTES = 22,
} BunLoader;

const BunLoader BUN_LOADER_MAX = BUN_LOADER_XML;
const BunLoader BUN_LOADER_MAX = BUN_LOADER_BYTES;

typedef struct BunLogOptions {
size_t __struct_size;
Expand Down
1 change: 1 addition & 0 deletions packages/bun-types/bun.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6004,6 +6004,7 @@ declare module "bun" {
| "napi"
| "wasm"
| "text"
| "bytes"
| "css"
| "html";

Expand Down
39 changes: 35 additions & 4 deletions src/ast/e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2460,20 +2460,51 @@
self.import_record_index == u32::MAX
}

pub fn import_record_loader(&self) -> Option<crate::Loader> {
/// The `with` (or `assert`) object literal of the `import()` options.
fn import_attributes_object(&self) -> Option<StoreRef<Object>> {
let crate::ExprData::EObject(obj) = &self.options.data else {
return None;
};
let with = Object::get(obj, b"with").or_else(|| Object::get(obj, b"assert"))?;
let crate::ExprData::EObject(with_obj) = &with.data else {
let crate::ExprData::EObject(with_obj) = with.data else {
return None;
};
let str_ = Object::get(with_obj, b"type")?.data.as_e_string()?;
Some(with_obj)
}

/// The `with: { key: "value" }` entries of an `import()` call, in source order.
pub fn import_record_attributes<'b>(
&self,
bump: &'b Bump,
) -> Option<&'b [crate::ImportAttribute]> {
let with_obj = self.import_attributes_object()?;
let mut attributes: Vec<crate::ImportAttribute> = Vec::new();
for prop in with_obj.properties.slice().iter() {
if prop.kind != G::PropertyKind::Normal
|| prop.flags.contains(crate::flags::Property::IsComputed)
{
return None;
}
let mut key_str = prop.key?.data.as_e_string()?;
let mut value_str = prop.value?.data.as_e_string()?;
attributes.push(crate::ImportAttribute {
// SAFETY: `bump` is the parser arena, which outlives the import
// record that stores these slices.
key: unsafe { bun_ptr::detach_lifetime(key_str.slice(bump)) },
value: unsafe { bun_ptr::detach_lifetime(value_str.slice(bump)) },

Check failure on line 2494 in src/ast/e.rs

View workflow job for this annotation

GitHub Actions / cargo clippy

unsafe block missing a safety comment
});
}
Some(bump.alloc_slice_copy(&attributes))
}

pub fn import_record_loader(&self) -> Option<crate::Loader> {
let with_obj = self.import_attributes_object()?;
let str_ = Object::get(&with_obj, b"type")?.data.as_e_string()?;

if !str_.is_utf16 {
if let Some(loader) = crate::Loader::from_string(&str_.data) {
if loader == crate::Loader::Sqlite {
let Some(embed) = Object::get(with_obj, b"embed") else {
let Some(embed) = Object::get(&with_obj, b"embed") else {
return Some(loader);
};
let Some(embed_str) = embed.data.as_e_string() else {
Expand Down
43 changes: 43 additions & 0 deletions src/ast/import_record.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ pub struct ImportRecord {
pub tag: Tag,
pub loader: Option<Loader>,

/// The `with { ... }` clause as written; empty when the import has none.
pub attributes: &'static [ImportAttribute],

pub source_index: Index,

/// The original import specifier as written in source code (e.g., "./foo.js").
Expand Down Expand Up @@ -98,6 +101,46 @@ bitflags::bitflags! {

pub type List<'a> = bun_alloc::ArenaVec<'a, ImportRecord>;

/// One `key: "value"` entry of an import attributes clause (UTF-8).
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct ImportAttribute {
pub key: &'static [u8],
pub value: &'static [u8],
}

impl ImportRecord {
/// The value of the import's `type` attribute, if it has one.
pub fn type_attribute(&self) -> Option<&'static [u8]> {
self.attributes
.iter()
.find(|attr| attr.key == b"type")
.map(|attr| attr.value)
}

/// The module-graph key: the path alone, or the path plus the attributes sorted by key.
pub fn module_graph_key<'k>(
path_text: &'k [u8],
attributes: &[ImportAttribute],
buf: &'k mut Vec<u8>,
) -> &'k [u8] {
if attributes.is_empty() {
return path_text;
}
use std::io::Write as _;
let mut sorted: Vec<&ImportAttribute> = attributes.iter().collect();
sorted.sort_by_key(|attr| attr.key);
buf.clear();
buf.extend_from_slice(path_text);
for attr in sorted {
write!(buf, "\0{}:", attr.key.len()).expect("unreachable");
buf.extend_from_slice(attr.key);
write!(buf, "\0{}:", attr.value.len()).expect("unreachable");
buf.extend_from_slice(attr.value);
}
buf
}
}

#[repr(u8)]
#[derive(Copy, Clone, Eq, PartialEq, Debug, Default)]
pub enum Tag {
Expand Down
4 changes: 3 additions & 1 deletion src/ast/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2887,7 +2887,9 @@ pub use ast_result::{
Ast, CommonJSNamedExport, CommonJSNamedExports, ConstValuesMap, NamedExports, NamedImports,
TopLevelSymbolToParts, TsEnumsMap,
};
pub use import_record::{Flags as ImportRecordFlags, ImportRecord, Tag as ImportRecordTag};
pub use import_record::{
Flags as ImportRecordFlags, ImportAttribute, ImportRecord, Tag as ImportRecordTag,
};
pub use loader::{Loader, LoaderHashTable, SideEffects};
pub use target::Target;
pub mod transpiler_cache;
Expand Down
15 changes: 12 additions & 3 deletions src/ast/loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ pub enum Loader {
Json5 = 19,
Md = 20,
Xml = 21,
/// `with { type: "bytes" }`: the file's bytes as a `Uint8Array` default export.
Bytes = 22,
}

// Crosses FFI as `uint8_t default_loader` / `uint8_t loader` in
Expand All @@ -63,7 +65,7 @@ bun_core::assert_ffi_discr!(
Jsx = 0, Js = 1, Ts = 2, Tsx = 3, Css = 4, File = 5, Json = 6,
Jsonc = 7, Toml = 8, Wasm = 9, Napi = 10, Base64 = 11, Dataurl = 12,
Text = 13, Bunsh = 14, Sqlite = 15, SqliteEmbedded = 16, Html = 17,
Yaml = 18, Json5 = 19, Md = 20, Xml = 21,
Yaml = 18, Json5 = 19, Md = 20, Xml = 21, Bytes = 22,
);

// E0658: inherent assoc types are nightly-only; lifted to module scope.
Expand All @@ -88,6 +90,8 @@ bun_core::comptime_string_map! {
b"json5" => Loader::Json5,
b"xml" => Loader::Xml,
b"wasm" => Loader::Wasm,
// JSC's reserved `type` for WebAssembly modules; loads like a plain `.wasm` import.
b"webassembly" => Loader::Wasm,
b"napi" => Loader::Napi,
b"node" => Loader::Napi,
b"dataurl" => Loader::Dataurl,
Expand All @@ -100,6 +104,7 @@ bun_core::comptime_string_map! {
b"html" => Loader::Html,
b"md" => Loader::Md,
b"markdown" => Loader::Md,
b"bytes" => Loader::Bytes,
};
}

Expand Down Expand Up @@ -127,7 +132,10 @@ impl Loader {
}

pub fn handles_empty_file(self) -> bool {
matches!(self, Loader::Wasm | Loader::File | Loader::Text)
matches!(
self,
Loader::Wasm | Loader::File | Loader::Text | Loader::Bytes
)
}

// `to_mime_type` / `from_mime_type` stay in bun_http_types as extension
Expand Down Expand Up @@ -242,7 +250,8 @@ impl Loader {
| Loader::Json5
| Loader::Xml
| Loader::File
| Loader::Md => SideEffects::NoSideEffectsPureData,
| Loader::Md
| Loader::Bytes => SideEffects::NoSideEffectsPureData,
_ => SideEffects::HasSideEffects,
}
}
Expand Down
14 changes: 10 additions & 4 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) __toBytes: 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"__toBytes",
];

/// 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 @@ -236,12 +238,13 @@ impl Imports {
b"__reExport",
b"__require",
b"__runInitializers",
b"__toBytes",
b"__using",
];

/// 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] = [
pub const ALL_SORTED_INDEX: [usize; 28] = [
15, // __name
24, // __require
7, // __export
Expand All @@ -263,12 +266,13 @@ impl Imports {
25, // __runInitializers
4, // __decorateElement
0, // $$typeof
26, // __using
27, // __using
3, // __callDispose
10, // __jsonParse
21, // __promiseAll
2, // __MEMO_CACHE_SENTINEL
1, // __EARLY_RETURN_SENTINEL
26, // __toBytes
];

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.__toBytes,
_ => 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.__toBytes),
_ => None,
}
}
Expand Down
1 change: 1 addition & 0 deletions src/bundler/AstBuilder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ impl<'a, 'bump> AstBuilder<'a, 'bump> {
range: Range::default(),
tag: Default::default(),
loader: None,
attributes: &[],
source_index: Default::default(),
original_path: b"",
flags: Default::default(),
Expand Down
1 change: 1 addition & 0 deletions src/bundler/HTMLScanner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ impl<'a> HTMLScanner<'a> {
range: Range::NONE,
tag: ImportRecordTag::default(),
loader: None,
attributes: &[],
source_index: AstIndex::default(),
original_path: b"",
flags: ImportRecordFlags::default(),
Expand Down
1 change: 1 addition & 0 deletions src/bundler/LinkerContext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3101,6 +3101,7 @@ impl<'a> LinkerContext<'a> {
| Loader::Base64
| Loader::Dataurl
| Loader::Text
| Loader::Bytes
| Loader::Bunsh => {}
}
}
Expand Down
Loading
Loading