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
14 changes: 14 additions & 0 deletions docs/bundler/executables.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -888,6 +888,20 @@ export default {

This is honestly a workaround, and we expect to improve this in the future with a more direct API.

### Detecting standalone mode at runtime

Use `Bun.isStandaloneExecutable` to check whether the current process is running from a compiled binary:

```ts index.ts icon="/icons/typescript.svg"
if (Bun.isStandaloneExecutable) {
// Running from `bun build --compile` output
} else {
// Running via `bun <file>` or as a library
}
```

Unlike `Bun.embeddedFiles.length > 0`, this check does not allocate `Blob` objects for each embedded file, so it is safe to call at startup in binaries that embed large assets.

### Listing embedded files

`Bun.embeddedFiles` gives you access to all embedded files as `Blob` objects:
Expand Down
16 changes: 16 additions & 0 deletions packages/bun-types/bun.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4148,6 +4148,22 @@ declare module "bun" {
*/
const embeddedFiles: ReadonlyArray<Blob>;

/**
* `true` when the current process is a standalone executable produced by
* `bun build --compile`, `false` otherwise.
*
* Unlike checking `Bun.embeddedFiles.length > 0`, reading this property does
* not materialize embedded files as `Blob` objects.
*
* @example
* ```ts
* if (Bun.isStandaloneExecutable) {
* console.log("Running from a compiled binary");
* }
* ```
*/
const isStandaloneExecutable: boolean;

/**
* `Blob` that leverages the fastest system calls available to operate on files.
*
Expand Down
1 change: 1 addition & 0 deletions src/jsc/bindings/BunObject+exports.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
macro(enableANSIColors) \
macro(hash) \
macro(inspect) \
macro(isStandaloneExecutable) \
macro(origin) \
macro(s3) \
macro(semver) \
Expand Down
1 change: 1 addition & 0 deletions src/jsc/bindings/BunObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -970,6 +970,7 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj
inflateSync BunObject_callback_inflateSync DontDelete|Function 1
inspect BunObject_lazyPropCb_wrap_inspect DontDelete|PropertyCallback
isMainThread constructIsMainThread ReadOnly|DontDelete|PropertyCallback
isStandaloneExecutable BunObject_lazyPropCb_wrap_isStandaloneExecutable ReadOnly|DontDelete|PropertyCallback
jest BunObject_callback_jest DontEnum|DontDelete|Function 1
listen BunObject_callback_listen DontDelete|Function 1
udpSocket BunObject_callback_udpSocket DontDelete|Function 1
Expand Down
5 changes: 5 additions & 0 deletions src/runtime/api/BunObject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,7 @@ pub mod bun_object {
BunObject_lazyPropCb_cwd => super::get_cwd,
BunObject_lazyPropCb_embeddedFiles => super::get_embedded_files,
BunObject_lazyPropCb_enableANSIColors => super::enable_ansi_colors,
BunObject_lazyPropCb_isStandaloneExecutable => super::get_is_standalone_executable,
BunObject_lazyPropCb_hash => super::get_hash_object,
BunObject_lazyPropCb_inspect => super::get_inspect,
BunObject_lazyPropCb_origin => super::get_origin,
Expand Down Expand Up @@ -1989,6 +1990,10 @@ pub(crate) fn get_terminal_constructor(global_this: &JSGlobalObject, _: &JSObjec
crate::api::bun_terminal_body::js::get_constructor(global_this)
}

pub(crate) fn get_is_standalone_executable(global_this: &JSGlobalObject, _: &JSObject) -> JSValue {
JSValue::js_boolean(global_this.bun_vm().standalone_module_graph.is_some())
}

pub(crate) fn get_embedded_files(global_this: &JSGlobalObject, _: &JSObject) -> JsResult<JSValue> {
use crate::webcore::blob::{Blob, BlobExt as _};
use bun_standalone_graph::{File as GraphFile, Graph as StandaloneModuleGraph};
Expand Down
54 changes: 54 additions & 0 deletions test/bundler/bundler_compile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,60 @@ describe("bundler", () => {
outfile: "dist/out",
run: { stdout: "Hello, world!", setCwd: true },
});
itBundled("compile/Bun.isStandaloneExecutable", {
compile: true,
assetNaming: "[name].[ext]",
files: {
"/entry.ts": /* js */ `
import { heapStats } from "bun:jsc";
import "./asset.file";

const blobCount = () => heapStats().objectTypeCounts.Blob ?? 0;

// Reading isStandaloneExecutable must not materialize embedded files as Blobs.
Bun.gc(true);
const baseline = blobCount();
if (Bun.isStandaloneExecutable !== true) {
throw new Error("expected Bun.isStandaloneExecutable === true, got " + Bun.isStandaloneExecutable);
}
const afterRead = blobCount();
if (afterRead !== baseline) {
throw new Error("reading Bun.isStandaloneExecutable changed Blob count (" + baseline + " -> " + afterRead + ")");
}

// Accessing embeddedFiles allocates a Blob per embedded asset; if it did not,
// the afterRead === baseline check above would be vacuous.
const files = Bun.embeddedFiles;
if (files.length !== 1) throw new Error("expected 1 embedded file, got " + files.length);
const afterEmbedded = blobCount();
if (afterEmbedded <= baseline) {
throw new Error("expected Blob count to increase after reading Bun.embeddedFiles (" + baseline + " -> " + afterEmbedded + ")");
}
console.log("ok", JSON.stringify({ baseline, afterRead, afterEmbedded }));
`,
"/asset.file": "abcd",
},
outfile: "dist/out",
run: { stdout: /^ok \{"baseline":\d+,"afterRead":\d+,"afterEmbedded":\d+\}$/ },
});
test("Bun.isStandaloneExecutable is false when not compiled", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`console.log(JSON.stringify({ value: Bun.isStandaloneExecutable, type: typeof Bun.isStandaloneExecutable }))`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect({ stdout: stdout.trim(), stderr, exitCode }).toEqual({
stdout: `{"value":false,"type":"boolean"}`,
stderr: expect.not.stringContaining("error"),
exitCode: 0,
});
});
itBundled("compile/ResolveEmbeddedFileOutfile", {
compile: true,
// TODO: this shouldn't be necessary, or we should add a map aliasing files.
Expand Down
Loading