Skip to content

Commit f2c563a

Browse files
authored
Merge branch 'main' into feat/bun-abort-basic
2 parents 3d52b42 + f25ea59 commit f2c563a

File tree

14 files changed

+363
-69
lines changed

14 files changed

+363
-69
lines changed

docs/bundler/executables.mdx

Lines changed: 157 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -413,34 +413,141 @@ cd /home/me/Desktop
413413

414414
## Embed assets & files
415415

416-
Standalone executables support embedding files.
416+
Standalone executables support embedding files directly into the binary. This lets you ship a single executable that contains images, JSON configs, templates, or any other assets your application needs.
417417

418-
To embed files into an executable with `bun build --compile`, import the file in your code.
418+
### How it works
419+
420+
Use the `with { type: "file" }` [import attribute](https://github.com/tc39/proposal-import-attributes) to embed a file:
421+
422+
```ts index.ts icon="/icons/typescript.svg"
423+
import icon from "./icon.png" with { type: "file" };
424+
425+
console.log(icon);
426+
// During development: "./icon.png"
427+
// After compilation: "$bunfs/icon-a1b2c3d4.png" (internal path)
428+
```
429+
430+
The import returns a **path string** that points to the embedded file. At build time, Bun:
431+
432+
1. Reads the file contents
433+
2. Embeds the data into the executable
434+
3. Replaces the import with an internal path (prefixed with `$bunfs/`)
435+
436+
You can then read this embedded file using `Bun.file()` or Node.js `fs` APIs.
437+
438+
### Reading embedded files with Bun.file()
439+
440+
`Bun.file()` is the recommended way to read embedded files:
419441

420442
```ts index.ts icon="/icons/typescript.svg"
421-
// this becomes an internal file path
422443
import icon from "./icon.png" with { type: "file" };
423444
import { file } from "bun";
424445

446+
// Get file contents as different types
447+
const bytes = await file(icon).arrayBuffer(); // ArrayBuffer
448+
const text = await file(icon).text(); // string (for text files)
449+
const blob = file(icon); // Blob
450+
451+
// Stream the file in a response
425452
export default {
426453
fetch(req) {
427-
// Embedded files can be streamed from Response objects
428-
return new Response(file(icon));
454+
return new Response(file(icon), {
455+
headers: { "Content-Type": "image/png" },
456+
});
429457
},
430458
};
431459
```
432460

433-
Embedded files can be read using `Bun.file`'s functions or the Node.js `fs.readFile` function (in `"node:fs"`).
461+
### Reading embedded files with Node.js fs
434462

435-
For example, to read the contents of the embedded file:
463+
Embedded files work seamlessly with Node.js file system APIs:
436464

437465
```ts index.ts icon="/icons/typescript.svg"
438466
import icon from "./icon.png" with { type: "file" };
467+
import config from "./config.json" with { type: "file" };
468+
import { readFileSync, promises as fs } from "node:fs";
469+
470+
// Synchronous read
471+
const iconBuffer = readFileSync(icon);
472+
473+
// Async read
474+
const configData = await fs.readFile(config, "utf-8");
475+
const parsed = JSON.parse(configData);
476+
477+
// Check file stats
478+
const stats = await fs.stat(icon);
479+
console.log(`Icon size: ${stats.size} bytes`);
480+
```
481+
482+
### Practical examples
483+
484+
#### Embedding a JSON config file
485+
486+
```ts index.ts icon="/icons/typescript.svg"
487+
import configPath from "./default-config.json" with { type: "file" };
488+
import { file } from "bun";
489+
490+
// Load the embedded default configuration
491+
const defaultConfig = await file(configPath).json();
492+
493+
// Merge with user config if it exists
494+
const userConfig = await file("./user-config.json")
495+
.json()
496+
.catch(() => ({}));
497+
const config = { ...defaultConfig, ...userConfig };
498+
```
499+
500+
#### Serving static assets in an HTTP server
501+
502+
Use `static` routes in `Bun.serve()` for efficient static file serving:
503+
504+
```ts server.ts icon="/icons/typescript.svg"
505+
import favicon from "./favicon.ico" with { type: "file" };
506+
import logo from "./logo.png" with { type: "file" };
507+
import styles from "./styles.css" with { type: "file" };
508+
import { file, serve } from "bun";
509+
510+
serve({
511+
static: {
512+
"/favicon.ico": file(favicon),
513+
"/logo.png": file(logo),
514+
"/styles.css": file(styles),
515+
},
516+
fetch(req) {
517+
return new Response("Not found", { status: 404 });
518+
},
519+
});
520+
```
521+
522+
Bun automatically handles Content-Type headers and caching for static routes.
523+
524+
#### Embedding templates
525+
526+
```ts index.ts icon="/icons/typescript.svg"
527+
import templatePath from "./email-template.html" with { type: "file" };
528+
import { file } from "bun";
529+
530+
async function sendWelcomeEmail(user: { name: string; email: string }) {
531+
const template = await file(templatePath).text();
532+
const html = template.replace("{{name}}", user.name).replace("{{email}}", user.email);
533+
534+
// Send email with the rendered template...
535+
}
536+
```
537+
538+
#### Embedding binary files
539+
540+
```ts index.ts icon="/icons/typescript.svg"
541+
import wasmPath from "./processor.wasm" with { type: "file" };
542+
import fontPath from "./font.ttf" with { type: "file" };
439543
import { file } from "bun";
440544

441-
const bytes = await file(icon).arrayBuffer();
442-
// await fs.promises.readFile(icon)
443-
// fs.readFileSync(icon)
545+
// Load a WebAssembly module
546+
const wasmBytes = await file(wasmPath).arrayBuffer();
547+
const wasmModule = await WebAssembly.instantiate(wasmBytes);
548+
549+
// Read binary font data
550+
const fontData = await file(fontPath).bytes();
444551
```
445552

446553
### Embed SQLite databases
@@ -507,22 +614,57 @@ This is honestly a workaround, and we expect to improve this in the future with
507614

508615
### Listing embedded files
509616

510-
To get a list of all embedded files, use `Bun.embeddedFiles`:
617+
`Bun.embeddedFiles` gives you access to all embedded files as `Blob` objects:
511618

512619
```ts index.ts icon="/icons/typescript.svg"
513620
import "./icon.png" with { type: "file" };
621+
import "./data.json" with { type: "file" };
622+
import "./template.html" with { type: "file" };
514623
import { embeddedFiles } from "bun";
515624

516-
console.log(embeddedFiles[0].name); // `icon-${hash}.png`
625+
// List all embedded files
626+
for (const blob of embeddedFiles) {
627+
console.log(`${blob.name} - ${blob.size} bytes`);
628+
}
629+
// Output:
630+
// icon-a1b2c3d4.png - 4096 bytes
631+
// data-e5f6g7h8.json - 256 bytes
632+
// template-i9j0k1l2.html - 1024 bytes
517633
```
518634

519-
`Bun.embeddedFiles` returns an array of `Blob` objects which you can use to get the size, contents, and other properties of the files.
635+
Each item in `Bun.embeddedFiles` is a `Blob` with a `name` property:
520636

521637
```ts
522-
embeddedFiles: Blob[]
638+
embeddedFiles: ReadonlyArray<Blob>;
523639
```
524640

525-
The list of embedded files excludes bundled source code like `.ts` and `.js` files.
641+
This is useful for dynamically serving all embedded assets using `static` routes:
642+
643+
```ts server.ts icon="/icons/typescript.svg"
644+
import "./public/favicon.ico" with { type: "file" };
645+
import "./public/logo.png" with { type: "file" };
646+
import "./public/styles.css" with { type: "file" };
647+
import { embeddedFiles, serve } from "bun";
648+
649+
// Build static routes from all embedded files
650+
const staticRoutes: Record<string, Blob> = {};
651+
for (const blob of embeddedFiles) {
652+
// Remove hash from filename: "icon-a1b2c3d4.png" -> "icon.png"
653+
const name = blob.name.replace(/-[a-f0-9]+\./, ".");
654+
staticRoutes[`/${name}`] = blob;
655+
}
656+
657+
serve({
658+
static: staticRoutes,
659+
fetch(req) {
660+
return new Response("Not found", { status: 404 });
661+
},
662+
});
663+
```
664+
665+
<Note>
666+
`Bun.embeddedFiles` excludes bundled source code (`.ts`, `.js`, etc.) to help protect your application's source.
667+
</Note>
526668

527669
#### Content hash
528670

docs/runtime/http/server.mdx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,15 +193,17 @@ This is the maximum amount of time a connection is allowed to be idle before the
193193
Thus far, the examples on this page have used the explicit `Bun.serve` API. Bun also supports an alternate syntax.
194194

195195
```ts server.ts
196-
import { type Serve } from "bun";
196+
import type { Serve } from "bun";
197197

198198
export default {
199199
fetch(req) {
200200
return new Response("Bun!");
201201
},
202-
} satisfies Serve;
202+
} satisfies Serve.Options<undefined>;
203203
```
204204

205+
The type parameter `<undefined>` represents WebSocket data — if you add a `websocket` handler with custom data attached via `server.upgrade(req, { data: ... })`, replace `undefined` with your data type.
206+
205207
Instead of passing the server options into `Bun.serve`, `export default` it. This file can be executed as-is; when Bun sees a file with a `default` export containing a `fetch` handler, it passes it into `Bun.serve` under the hood.
206208

207209
---

packages/bun-types/s3.d.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,24 @@ declare module "bun" {
281281
*/
282282
type?: string;
283283

284+
/**
285+
* The Content-Disposition header value.
286+
* Controls how the file is presented when downloaded.
287+
*
288+
* @example
289+
* // Setting attachment disposition with filename
290+
* const file = s3.file("report.pdf", {
291+
* contentDisposition: "attachment; filename=\"quarterly-report.pdf\""
292+
* });
293+
*
294+
* @example
295+
* // Setting inline disposition
296+
* await s3.write("image.png", imageData, {
297+
* contentDisposition: "inline"
298+
* });
299+
*/
300+
contentDisposition?: string | undefined;
301+
284302
/**
285303
* By default, Amazon S3 uses the STANDARD Storage Class to store newly created objects.
286304
*

src/ast/SideEffects.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ pub const SideEffects = enum(u1) {
277277
}
278278
}
279279

280-
properties_slice[end] = prop_;
280+
properties_slice[end] = prop;
281281
end += 1;
282282
}
283283

src/bun.js/webcore/Blob.zig

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -968,6 +968,7 @@ fn writeFileWithEmptySourceToDestination(ctx: *jsc.JSGlobalObject, destination_b
968968
s3.path(),
969969
"",
970970
destination_blob.contentTypeOrMimeType(),
971+
aws_options.content_disposition,
971972
aws_options.acl,
972973
proxy_url,
973974
aws_options.storage_class,
@@ -1116,6 +1117,7 @@ pub fn writeFileWithSourceDestination(ctx: *jsc.JSGlobalObject, source_blob: *Bl
11161117
aws_options.acl,
11171118
aws_options.storage_class,
11181119
destination_blob.contentTypeOrMimeType(),
1120+
aws_options.content_disposition,
11191121
proxy_url,
11201122
null,
11211123
undefined,
@@ -1154,6 +1156,7 @@ pub fn writeFileWithSourceDestination(ctx: *jsc.JSGlobalObject, source_blob: *Bl
11541156
s3.path(),
11551157
bytes.slice(),
11561158
destination_blob.contentTypeOrMimeType(),
1159+
aws_options.content_disposition,
11571160
aws_options.acl,
11581161
proxy_url,
11591162
aws_options.storage_class,
@@ -1183,6 +1186,7 @@ pub fn writeFileWithSourceDestination(ctx: *jsc.JSGlobalObject, source_blob: *Bl
11831186
aws_options.acl,
11841187
aws_options.storage_class,
11851188
destination_blob.contentTypeOrMimeType(),
1189+
aws_options.content_disposition,
11861190
proxy_url,
11871191
null,
11881192
undefined,
@@ -1387,6 +1391,7 @@ pub fn writeFileInternal(globalThis: *jsc.JSGlobalObject, path_or_blob_: *PathOr
13871391
aws_options.acl,
13881392
aws_options.storage_class,
13891393
destination_blob.contentTypeOrMimeType(),
1394+
aws_options.content_disposition,
13901395
proxy_url,
13911396
null,
13921397
undefined,
@@ -1447,6 +1452,7 @@ pub fn writeFileInternal(globalThis: *jsc.JSGlobalObject, path_or_blob_: *PathOr
14471452
aws_options.acl,
14481453
aws_options.storage_class,
14491454
destination_blob.contentTypeOrMimeType(),
1455+
aws_options.content_disposition,
14501456
proxy_url,
14511457
null,
14521458
undefined,
@@ -2402,6 +2408,7 @@ pub fn pipeReadableStreamToBlob(this: *Blob, globalThis: *jsc.JSGlobalObject, re
24022408
aws_options.acl,
24032409
aws_options.storage_class,
24042410
this.contentTypeOrMimeType(),
2411+
aws_options.content_disposition,
24052412
proxy_url,
24062413
null,
24072414
undefined,
@@ -2629,13 +2636,22 @@ pub fn getWriter(
26292636
}
26302637
}
26312638
}
2639+
var content_disposition_str: ?ZigString.Slice = null;
2640+
defer if (content_disposition_str) |cd| cd.deinit();
2641+
if (try options.getTruthy(globalThis, "contentDisposition")) |content_disposition| {
2642+
if (!content_disposition.isString()) {
2643+
return globalThis.throwInvalidArgumentType("write", "options.contentDisposition", "string");
2644+
}
2645+
content_disposition_str = try content_disposition.toSlice(globalThis, bun.default_allocator);
2646+
}
26322647
const credentialsWithOptions = try s3.getCredentialsWithOptions(options, globalThis);
26332648
return try S3.writableStream(
26342649
credentialsWithOptions.credentials.dupe(),
26352650
path,
26362651
globalThis,
26372652
credentialsWithOptions.options,
26382653
this.contentTypeOrMimeType(),
2654+
if (content_disposition_str) |cd| cd.slice() else null,
26392655
proxy_url,
26402656
credentialsWithOptions.storage_class,
26412657
);
@@ -2647,6 +2663,7 @@ pub fn getWriter(
26472663
globalThis,
26482664
.{},
26492665
this.contentTypeOrMimeType(),
2666+
null,
26502667
proxy_url,
26512668
null,
26522669
);

src/bun.js/webcore/fetch.zig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1301,6 +1301,7 @@ pub fn Bun__fetch_(
13011301
credentialsWithOptions.acl,
13021302
credentialsWithOptions.storage_class,
13031303
if (headers) |h| (h.getContentType()) else null,
1304+
if (headers) |h| h.getContentDisposition() else null,
13041305
proxy_url,
13051306
@ptrCast(&Wrapper.resolve),
13061307
s3_stream,

src/http/Headers.zig

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -75,20 +75,12 @@ pub fn deinit(this: *Headers) void {
7575
this.entries.deinit(this.allocator);
7676
this.buf.clearAndFree(this.allocator);
7777
}
78-
pub fn getContentType(this: *const Headers) ?[]const u8 {
79-
if (this.entries.len == 0 or this.buf.items.len == 0) {
80-
return null;
81-
}
82-
const header_entries = this.entries.slice();
83-
const header_names = header_entries.items(.name);
84-
const header_values = header_entries.items(.value);
8578

86-
for (header_names, 0..header_names.len) |name, i| {
87-
if (bun.strings.eqlCaseInsensitiveASCII(this.asStr(name), "content-type", true)) {
88-
return this.asStr(header_values[i]);
89-
}
90-
}
91-
return null;
79+
pub fn getContentDisposition(this: *const Headers) ?[]const u8 {
80+
return this.get("content-disposition");
81+
}
82+
pub fn getContentType(this: *const Headers) ?[]const u8 {
83+
return this.get("content-type");
9284
}
9385
pub fn asStr(this: *const Headers, ptr: api.StringPointer) []const u8 {
9486
return if (ptr.offset + ptr.length <= this.buf.items.len)

0 commit comments

Comments
 (0)