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..6ac6a1970b6f 100644 --- a/test/bundler/bundler_compile.test.ts +++ b/test/bundler/bundler_compile.test.ts @@ -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.