From eb66e04a854a534543f15289b7851531a661cdee Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Mon, 22 Jun 2026 05:32:12 +0000 Subject: [PATCH 1/4] Add Bun.isStandaloneExecutable A zero-cost boolean indicating whether the current process is a `bun build --compile` standalone executable. The existing way to detect this (`Bun.embeddedFiles.length > 0`) materializes every embedded file as a heap-backed Blob, which is wasteful for binaries embedding large assets that only need a yes/no answer. The other workaround (`Bun.main.startsWith('/$bunfs/')`) couples user code to an internal path format. --- docs/bundler/executables.mdx | 14 +++++++++ packages/bun-types/bun.d.ts | 16 ++++++++++ src/jsc/bindings/BunObject+exports.h | 1 + src/jsc/bindings/BunObject.cpp | 1 + src/runtime/api/BunObject.rs | 5 ++++ test/bundler/bundler_compile.test.ts | 45 ++++++++++++++++++++++++++++ 6 files changed, 82 insertions(+) diff --git a/docs/bundler/executables.mdx b/docs/bundler/executables.mdx index 6f93d3dfbc6a..a7f9650764b6 100644 --- a/docs/bundler/executables.mdx +++ b/docs/bundler/executables.mdx @@ -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 ` 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: diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index d25acfdb9420..62140a7d5387 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -4148,6 +4148,22 @@ declare module "bun" { */ const embeddedFiles: ReadonlyArray; + /** + * `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. * diff --git a/src/jsc/bindings/BunObject+exports.h b/src/jsc/bindings/BunObject+exports.h index c8c703df57af..47a1f8d280db 100644 --- a/src/jsc/bindings/BunObject+exports.h +++ b/src/jsc/bindings/BunObject+exports.h @@ -35,6 +35,7 @@ macro(enableANSIColors) \ macro(hash) \ macro(inspect) \ + macro(isStandaloneExecutable) \ macro(origin) \ macro(s3) \ macro(semver) \ diff --git a/src/jsc/bindings/BunObject.cpp b/src/jsc/bindings/BunObject.cpp index 23554a77f449..f9cebdd276b5 100644 --- a/src/jsc/bindings/BunObject.cpp +++ b/src/jsc/bindings/BunObject.cpp @@ -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 diff --git a/src/runtime/api/BunObject.rs b/src/runtime/api/BunObject.rs index 1a5d3e29fa6e..1b12337eb620 100644 --- a/src/runtime/api/BunObject.rs +++ b/src/runtime/api/BunObject.rs @@ -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, @@ -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 { use crate::webcore::blob::{Blob, BlobExt as _}; use bun_standalone_graph::{File as GraphFile, Graph as StandaloneModuleGraph}; diff --git a/test/bundler/bundler_compile.test.ts b/test/bundler/bundler_compile.test.ts index 5955895e4561..70c4e39427be 100644 --- a/test/bundler/bundler_compile.test.ts +++ b/test/bundler/bundler_compile.test.ts @@ -443,6 +443,51 @@ 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"; + + if (Bun.isStandaloneExecutable !== true) { + throw new Error("expected Bun.isStandaloneExecutable === true, got " + Bun.isStandaloneExecutable); + } + + // Reading isStandaloneExecutable must not materialize embedded files as Blobs. + Bun.gc(true); + const before = heapStats().objectTypeCounts.Blob ?? 0; + + // Accessing embeddedFiles allocates a Blob per embedded asset. + const files = Bun.embeddedFiles; + if (files.length !== 1) throw new Error("expected 1 embedded file, got " + files.length); + const after = heapStats().objectTypeCounts.Blob ?? 0; + + if (after <= before) { + throw new Error("expected Blob count to increase after reading Bun.embeddedFiles (before=" + before + " after=" + after + ")"); + } + console.log("before=" + before, "after=" + after); + `, + "/asset.file": "abcd", + }, + outfile: "dist/out", + run: { stdout: /^before=0 after=[1-9]\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. From bc123c3035f85aa91ac4e1298989148983a1946a Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2026 05:34:40 +0000 Subject: [PATCH 2/4] [autofix.ci] apply automated fixes --- test/bundler/bundler_compile.test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/bundler/bundler_compile.test.ts b/test/bundler/bundler_compile.test.ts index 70c4e39427be..f8bf9532972f 100644 --- a/test/bundler/bundler_compile.test.ts +++ b/test/bundler/bundler_compile.test.ts @@ -476,7 +476,11 @@ describe("bundler", () => { }); 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 }))`], + cmd: [ + bunExe(), + "-e", + `console.log(JSON.stringify({ value: Bun.isStandaloneExecutable, type: typeof Bun.isStandaloneExecutable }))`, + ], env: bunEnv, stdout: "pipe", stderr: "pipe", From 4baa8c5ab0bab59209fdaf566b2d6edca68ab4e8 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Mon, 22 Jun 2026 05:44:13 +0000 Subject: [PATCH 3/4] test: don't pin baseline Blob count to 0 Measure the Blob count before and after reading isStandaloneExecutable and assert it is unchanged, rather than asserting it is exactly 0. This keeps the assertion strong (reading the property allocates no Blobs) without coupling to unrelated runtime allocation changes. --- test/bundler/bundler_compile.test.ts | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/test/bundler/bundler_compile.test.ts b/test/bundler/bundler_compile.test.ts index f8bf9532972f..6ac6a1970b6f 100644 --- a/test/bundler/bundler_compile.test.ts +++ b/test/bundler/bundler_compile.test.ts @@ -451,28 +451,33 @@ describe("bundler", () => { import { heapStats } from "bun:jsc"; import "./asset.file"; - if (Bun.isStandaloneExecutable !== true) { - throw new Error("expected Bun.isStandaloneExecutable === true, got " + Bun.isStandaloneExecutable); - } + const blobCount = () => heapStats().objectTypeCounts.Blob ?? 0; // Reading isStandaloneExecutable must not materialize embedded files as Blobs. Bun.gc(true); - const before = heapStats().objectTypeCounts.Blob ?? 0; + 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. + // 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 after = heapStats().objectTypeCounts.Blob ?? 0; - - if (after <= before) { - throw new Error("expected Blob count to increase after reading Bun.embeddedFiles (before=" + before + " after=" + after + ")"); + const afterEmbedded = blobCount(); + if (afterEmbedded <= baseline) { + throw new Error("expected Blob count to increase after reading Bun.embeddedFiles (" + baseline + " -> " + afterEmbedded + ")"); } - console.log("before=" + before, "after=" + after); + console.log("ok", JSON.stringify({ baseline, afterRead, afterEmbedded })); `, "/asset.file": "abcd", }, outfile: "dist/out", - run: { stdout: /^before=0 after=[1-9]\d*$/ }, + run: { stdout: /^ok \{"baseline":\d+,"afterRead":\d+,"afterEmbedded":\d+\}$/ }, }); test("Bun.isStandaloneExecutable is false when not compiled", async () => { await using proc = Bun.spawn({ From 13d2c5fae212855feaf238eba9771667b04b6060 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Mon, 22 Jun 2026 06:16:03 +0000 Subject: [PATCH 4/4] ci: retrigger