Skip to content
Merged
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
13 changes: 13 additions & 0 deletions docs/bundler/executables.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -758,6 +758,19 @@ const wasmModule = await WebAssembly.instantiate(wasmBytes);
const fontData = await file(fontPath).bytes();
```

### Embed text as a string

Use the `text` loader when you want the contents of a file as a string. `.txt` files use it by default. For other extensions, add `with { type: "text" }` or pass `--loader .md:text`.

```ts index.ts icon="/icons/typescript.svg"
import notes from "./notes.txt";
import readme from "./README.md" with { type: "text" };

console.log(notes); // the contents of notes.txt
```

In a compiled executable the text is stored once, in the form the JavaScript engine uses for strings (one byte per character for ASCII text, UTF-16 otherwise). The runtime hands the string back without a copy, so a large text import costs its size in the binary and no extra copy on the heap. A text import is a module, not a file: `Bun.embeddedFiles` does not list it. Use `with { type: "file" }` when you need a file path instead.

### Embed SQLite databases

To embed a SQLite database into the compiled executable, set `type: "sqlite"` in the import attribute and the `embed` attribute to `"true"`.
Expand Down
8 changes: 8 additions & 0 deletions src/bun_core/string/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,14 @@ impl String {
// without copying and never frees it.
unsafe { BunString__createStaticExternal(bytes.as_ptr(), bytes.len(), is_latin1) }
}
/// UTF-16 form of [`Self::create_static_external`]: `units` must be
/// 2-byte aligned and live for the rest of the process.
Comment thread
robobun marked this conversation as resolved.
pub fn create_static_external_utf16(units: &[u16]) -> Self {
debug_assert!(!units.is_empty());
// SAFETY: the C++ side takes the length in code units and stores
// ptr/len without copying or freeing.
unsafe { BunString__createStaticExternal(units.as_ptr().cast::<u8>(), units.len(), false) }
}
/// `bun.String.createFormat` — formats `args` into a temporary buffer and
/// copies the result into a fresh WTF-backed string.
pub fn create_format(args: core::fmt::Arguments<'_>) -> Self {
Expand Down
161 changes: 86 additions & 75 deletions src/bundler/ParseTask.rs
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,56 @@ pub mod parse_worker {
pub(crate) content_hash: u64,
}

/// Returns the unique key the printer replaces with the asset's final path.
fn register_embedded_asset<'b>(
bump: &'b Bump,
source: &Source,
unique_key_prefix: u64,
unique_key_for_additional_file: &mut FileLoaderHash,
) -> &'b [u8] {
use core::fmt::Write as _;
let mut buf = bun_alloc::ArenaString::new_in(bump);
write!(
&mut buf,
"{}",
crate::chunk::UniqueKey {
prefix: unique_key_prefix,
kind: crate::chunk::QueryKind::Asset,
index: source.index.0,
},
)
.expect("unreachable");
let unique_key = buf.into_bump_str().as_bytes();
*unique_key_for_additional_file = FileLoaderHash {
key: ast::StoreStr::new(unique_key),
content_hash: ContentHasher::run(&source.contents),
};
unique_key
}

/// `require("<unique key>")`. Unlike `import.meta.require`, the call target
/// prints per output format, so `--bytecode` (CommonJS) can compile it.
Comment thread
robobun marked this conversation as resolved.
fn require_embedded_asset(unique_key: &[u8]) -> Expr {
let import_path = Expr::init(
E::String {
data: unique_key.into(),
..Default::default()
},
Loc { start: 0 },
);
Expr::init(
E::Call {
target: Expr {
data: ast::ExprData::ERequireCallTarget,
loc: Loc { start: 0 },
},
args: bun_ast::ExprNodeList::from_arena_slice(&[import_path]),
..Default::default()
},
Loc { start: 0 },
)
}

// ───────────────────────────────────────────────────────────────────────────
// CSS Symbol bridge — `bun_ast::Symbol` ↔ `bun_ast::Symbol`
//
Expand Down Expand Up @@ -886,13 +936,26 @@ pub mod parse_worker {
return result;
}
Loader::Text => {
let root = Expr::init(
E::String {
data: source.contents().into(),
..Default::default()
},
Loc { start: 0 },
);
// A standalone executable embeds the text as a string body the
// runtime aliases without a copy (`encode_text_module`), so the
// module becomes `export default require("<bunfs path>")`.
// Browser chunks cannot reach the embedded graph.
Comment thread
robobun marked this conversation as resolved.
let root = if topts.compile_mode.is_executable() && topts.target.is_bun() {
require_embedded_asset(register_embedded_asset(
bump,
source,
unique_key_prefix,
unique_key_for_additional_file,
))
} else {
Expr::init(
E::String {
data: source.contents().into(),
..Default::default()
},
Loc { start: 0 },
)
};
let mut ast = JSAst::init(
js_parser::new_lazy_export_ast(
bump,
Expand Down Expand Up @@ -967,29 +1030,15 @@ pub mod parse_worker {
return Err(crate::Error::ParserError);
}

let path_to_use: &[u8] = 'brk: {
// Implements embedded sqlite
if loader == Loader::SqliteEmbedded {
let mut buf = bun_alloc::ArenaString::new_in(bump);
write!(
&mut buf,
"{}",
crate::chunk::UniqueKey {
prefix: unique_key_prefix,
kind: crate::chunk::QueryKind::Asset,
index: source.index.0,
},
)
.expect("unreachable");
let embedded_path = buf.into_bump_str().as_bytes();
*unique_key_for_additional_file = FileLoaderHash {
key: ast::StoreStr::new(embedded_path),
content_hash: ContentHasher::run(&source.contents),
};
break 'brk embedded_path;
}

break 'brk source.path.text;
let path_to_use: &[u8] = if loader == Loader::SqliteEmbedded {
register_embedded_asset(
bump,
source,
unique_key_prefix,
unique_key_for_additional_file,
)
} else {
source.path.text
};

// This injects the following code:
Expand Down Expand Up @@ -1088,50 +1137,16 @@ pub mod parse_worker {
return Err(crate::Error::ParserError);
}

let mut buf = bun_alloc::ArenaString::new_in(bump);
write!(
&mut buf,
"{}",
crate::chunk::UniqueKey {
prefix: unique_key_prefix,
kind: crate::chunk::QueryKind::Asset,
index: source.index.0,
},
)
.expect("unreachable");
let unique_key = buf.into_bump_str().as_bytes();
// This injects the following code:
//
// require(unique_key)
//
let import_path = Expr::init(
E::String {
data: unique_key.into(),
..Default::default()
},
Loc { start: 0 },
);

let require_args = bump.alloc_slice_fill_default::<Expr>(1);
require_args[0] = import_path;

let root = Expr::init(
E::Call {
target: Expr {
data: ast::ExprData::ERequireCallTarget,
loc: Loc { start: 0 },
},
// SAFETY: bump-owned slice; never grown via this Vec.
args: unsafe { bun_ast::ExprNodeList::from_bump_slice(require_args) },
..Default::default()
},
Loc { start: 0 },
);

*unique_key_for_additional_file = FileLoaderHash {
key: ast::StoreStr::new(unique_key),
content_hash: ContentHasher::run(&source.contents),
};
let root = require_embedded_asset(register_embedded_asset(
bump,
source,
unique_key_prefix,
unique_key_for_additional_file,
));
return Ok(JSAst::init(
js_parser::new_lazy_export_ast(
bump,
Expand Down Expand Up @@ -2677,11 +2692,7 @@ pub mod parse_worker {
package_name: task.package_name,

// Hash the files in here so that we do it in parallel.
content_hash_for_additional_file: if loader.should_copy_for_bundling() {
unique_key_for_additional_file.content_hash
} else {
0
},
content_hash_for_additional_file: unique_key_for_additional_file.content_hash,
})
}

Expand Down
57 changes: 35 additions & 22 deletions src/bundler/bundle_v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4191,28 +4191,36 @@ pub mod bv2_impl {
let index = reachable_source.get() as usize;
let key: &[u8] = &unique_key_for_additional_files[index];
if !key.is_empty() {
let mut template: options::PathTemplate =
if self.graph.html_imports.server_source_indices.len() != 0
&& self.transpiler.options.asset_naming.is_empty()
{
options::PathTemplate::ASSET_WITH_TARGET.into()
} else {
options::PathTemplate::ASSET.into()
};

let loader = loaders[index];
let target = targets[index];
// SAFETY: see `self_ptr` note above — `transpiler_for_target` needs
// `&mut self` only to pick between two stored `*mut Transpiler`s; it
// never touches `graph.input_files`.
let asset_naming = unsafe {
&(*self_ptr)
.transpiler_for_target(target)
.options
.asset_naming
let mut template: options::PathTemplate = if loader == Loader::Text {
// Text modules ignore `--asset-naming`: without `[hash]`
// two same-named files would share one path.
Comment thread
robobun marked this conversation as resolved.
options::PathTemplate::ASSET.into()
} else {
let mut template: options::PathTemplate =
if self.graph.html_imports.server_source_indices.len() != 0
&& self.transpiler.options.asset_naming.is_empty()
{
options::PathTemplate::ASSET_WITH_TARGET.into()
} else {
options::PathTemplate::ASSET.into()
};

// SAFETY: see `self_ptr` note above — `transpiler_for_target` needs
// `&mut self` only to pick between two stored `*mut Transpiler`s; it
// never touches `graph.input_files`.
let asset_naming = unsafe {
&(*self_ptr)
.transpiler_for_target(target)
.options
.asset_naming
};
if !asset_naming.is_empty() {
template.data.clone_from(asset_naming);
}
template
};
if !asset_naming.is_empty() {
template.data.clone_from(asset_naming);
}

let source = &mut sources[index];

Expand Down Expand Up @@ -4253,8 +4261,6 @@ pub mod bv2_impl {
v.into_boxed_slice()
};

let loader = loaders[index];

// Hand the existing `source.contents` buffer to the
// OutputFile — no copy: move the contents
// out instead of `to_vec()`-cloning,
Expand Down Expand Up @@ -7097,6 +7103,13 @@ pub mod bv2_impl {
.input_files
.items_content_hash_for_additional_file_mut()[result_source_index] =
result.content_hash_for_additional_file;
if !result.unique_key_for_additional_file.is_empty()
&& result.loader == Loader::Text
{
// `process_resolve_queue` only counts `should_copy_for_bundling()`
// loaders, and a zero count skips `process_files_to_copy`.
Comment thread
robobun marked this conversation as resolved.
this.graph.estimated_file_loader_count += 1;
}
if !result.unique_key_for_additional_file.is_empty()
&& result.loader.should_copy_for_bundling()
{
Expand Down
22 changes: 16 additions & 6 deletions src/bundler/linker_context/generateCodeForLazyExport.rs
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,12 @@ pub(crate) fn generate_code_for_lazy_export(
loc: stmt.loc,
};

// `require(<asset>)` prints as the runtime's `__require` outside CommonJS
// output, so the part that holds the call must import it.
Comment thread
robobun marked this conversation as resolved.
let calls_runtime_require = matches!(expr.data, ExprData::ECall(ref c)
if matches!(c.target.data, ExprData::ERequireCallTarget))
&& this.options.output_format != crate::options::OutputFormat::Cjs;

match exports_kind {
bun_ast::ExportsKind::Cjs => {
part.stmts.slice_mut()[0] = Stmt::assign(
Expand All @@ -395,12 +401,7 @@ pub(crate) fn generate_code_for_lazy_export(
Index::init(source_index),
)?;

// If this is a .napi addon and it's not node, we need to generate a require() call to the runtime
if matches!(expr.data, ExprData::ECall(ref c)
if matches!(c.target.data, ExprData::ERequireCallTarget))
// if it's commonjs, use require()
&& this.options.output_format != crate::options::OutputFormat::Cjs
{
if calls_runtime_require {
this.graph.generate_runtime_symbol_import_and_use(
source_index,
Index::part(1u32),
Expand Down Expand Up @@ -507,6 +508,15 @@ pub(crate) fn generate_code_for_lazy_export(
)));
let parts = this.graph.ast.items_parts_mut()[source_index as usize].as_mut_slice();
parts[generated.1 as usize].stmts = bun_ast::StoreSlice::new_mut(new_stmts);

if calls_runtime_require {
this.graph.generate_runtime_symbol_import_and_use(
source_index,
Index::part(generated.1),
b"__require",
1,
)?;
}
}
}
}
Expand Down
19 changes: 19 additions & 0 deletions src/jsc/bindings/ModuleLoader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,11 @@ JSValue fetchBuiltinModuleWithoutResolution(
RELEASE_AND_RETURN(scope, jsNumber(-1));
}

// A text file embedded by `bun build --compile`: the string is `module.exports`.
case SyntheticModuleType::ExportDefaultObject: {
return JSC::JSValue::decode(res->result.value.jsvalue_for_export);
}

default: {
if (tag & SyntheticModuleType::InternalModuleRegistryFlag) {
constexpr auto mask = (SyntheticModuleType::InternalModuleRegistryFlag - 1);
Expand Down Expand Up @@ -1065,6 +1070,20 @@ static JSValue fetchESMSourceCode(
BUN_FOREACH_LAZY_ESM_NATIVE_MODULE(LAZY_CASE)
#undef LAZY_CASE

// A text file embedded by `bun build --compile`: the string is the default export.
case SyntheticModuleType::ExportDefaultObject: {
JSC::JSValue value = JSC::JSValue::decode(res->result.value.jsvalue_for_export);
if (!value) {
RELEASE_AND_RETURN(scope, reject(JSC::createSyntaxError(globalObject, "Failed to parse Object"_s)));
}
auto function = generateJSValueExportDefaultObjectSourceCode(globalObject, value);
auto source = JSC::SourceCode(
JSC::SyntheticSourceProvider::create(WTF::move(function),
JSC::SourceOrigin(), WTF::move(moduleKey)));
JSC::ensureStillAliveHere(value);
RELEASE_AND_RETURN(scope, rejectOrResolve(JSSourceCode::create(vm, WTF::move(source))));
}

// CommonJS modules from src/js/*
default: {
if (tag & SyntheticModuleType::InternalModuleRegistryFlag) {
Expand Down
Loading
Loading