jsmdcui 0.8.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,27 +1,276 @@
1
- # This is completely optional
2
- - I still recommend using the methods in the root README.md
3
- - Bun's Android build is currently not supported
1
+ # Bun Single-File Executable Bootstrap
4
2
 
5
- # Usage
6
- - First run `bun ./packAssets.sh` to bundle the assets into `assets.tar`
7
- - Then run the build script like this
3
+ Build a Bun application and its runtime assets into one executable while
4
+ keeping the regular main module free of Bun-specific asset imports.
8
5
 
9
- ```shell
10
- bun build --compile --bytecode --minify ./entry.mjs --outfile=binname
11
- ```
6
+ You can copy and adapt this build setup for your own project by changing its
7
+ main-module path and asset list as described below.
12
8
 
13
- - You'll get a binname executable
9
+ - Bundle an asset tree alongside the application code in one executable.
10
+ - Keep the regular main module available to the Node.js ESM execution path.
11
+ - Read embedded resources first with an optional external-file fallback.
12
+ - List, extract, or bypass embedded assets with built-in CLI flags.
13
+ - Build for the current platform or pass a Bun cross-compilation target.
14
14
 
15
- # Single Executable Intro
15
+ > This workflow is optional; the standard methods in the root README remain
16
+ > available. Bun's Android build is currently not supported.
16
17
 
17
- This folder contains the Bun single-exe bootstrap used by the project.
18
+ ## Quick start for jsmdcui
19
+
20
+ From the repository root, build for the current platform:
21
+
22
+ ```shell
23
+ bun ./src/index.js --build-exe
24
+ ./mdcui --version
25
+ ```
26
+
27
+ `--build-exe` runs `packAssets.sh` automatically and writes the `mdcui`
28
+ executable to the current directory. To pass a Bun compilation target, use:
29
+
30
+ ```shell
31
+ bun ./src/index.js --build-for <target>
32
+ ```
33
+
34
+ Any additional arguments after `--build-exe`, or after the target passed to
35
+ `--build-for`, are forwarded to the `bun build` command:
36
+
37
+ ```shell
38
+ bun ./src/index.js --build-exe --sourcemap
39
+ bun ./src/index.js --build-for <target> --sourcemap
40
+ ```
41
+
42
+ The following presence-based build constants select distribution defaults:
43
+
44
+ - `MDCUI_DEFAULT_EDIT`: open files as editable text by default.
45
+ - `MDCUI_DEFAULT_DEMO`: add `--demo` when launched without arguments.
46
+ - `MDCUI_DEFAULT_DEMO_WUI`: add `--wui` when launched without arguments.
47
+
48
+ Choose only one of these three default-mode constants for each build.
49
+ `MDCUI_OVERWRITE_DEMO` is an optional modifier for a demo or WUI build.
50
+
51
+ For example, build a default text editor with:
52
+
53
+ ```shell
54
+ bun ./src/index.js --build-exe \
55
+ --define MDCUI_DEFAULT_EDIT=true
56
+ ```
57
+
58
+ To build a bundled terminal demo that replaces its local copy:
59
+
60
+ ```shell
61
+ bun ./src/index.js --build-exe \
62
+ --define MDCUI_DEFAULT_DEMO=true \
63
+ --define MDCUI_OVERWRITE_DEMO=true
64
+ ```
65
+
66
+ `MDCUI_DEFAULT_DEMO=true` makes an executable launched without arguments behave
67
+ as if `--demo` was passed. `MDCUI_OVERWRITE_DEMO=true` makes every selected demo
68
+ replace an existing local copy; it does not select a demo by itself.
69
+
70
+ For a browser UI that automatically adds `--wui` when launched without
71
+ arguments, use `MDCUI_DEFAULT_DEMO_WUI` instead:
72
+
73
+ ```shell
74
+ bun ./src/index.js --build-exe \
75
+ --define MDCUI_DEFAULT_DEMO_WUI=true \
76
+ --define MDCUI_OVERWRITE_DEMO=true
77
+ ```
78
+
79
+ To perform the same steps manually, run these commands from `single-exe/`:
80
+
81
+ ```shell
82
+ bun ./packAssets.sh
83
+ bun build --format=esm --compile --minify --bytecode ./entry.mjs --outfile=mdcui
84
+ ```
85
+
86
+ ## Adapting this folder to another project
87
+
88
+ This folder is a reusable starting point, not a drop-in package. The current
89
+ `entry.mjs` and `packAssets.sh` contain jsmdcui paths, so another project must
90
+ complete the following integration steps.
91
+
92
+ The adopting project needs Bun for packing and compiling, a `tar` command for
93
+ `packAssets.sh`, and an ES-module setup. Keep `"type": "module"` in
94
+ `package.json`, or rename the copied `.js` modules to `.mjs` and update their
95
+ imports. The supported uncompiled Node path requires Node.js 20.11 or newer
96
+ because `compiled.js` uses `import.meta.dirname`.
97
+
98
+ `assetsHelper.js` and `compiled.js` are Node-compatible under those conditions.
99
+ `assetsLoader.mjs` and `entry.mjs` are Bun-only and must not be imported by the
100
+ regular Node entry path.
101
+
102
+ ### 1. Copy the bootstrap directory
103
+
104
+ Copy the entire `single-exe/` directory to the root of the other project and
105
+ keep its name and relative position unless you also update the paths in
106
+ `compiled.js` and `packAssets.sh`. A typical layout is:
107
+
108
+ ```text
109
+ my-project/
110
+ ├── single-exe/
111
+ │ ├── assetsHelper.js
112
+ │ ├── assetsLoader.mjs
113
+ │ ├── compiled.js
114
+ │ ├── entry.mjs
115
+ │ └── packAssets.sh
116
+ └── src/
117
+ └── index.js
118
+ ```
119
+
120
+ `assets.tar` is generated by `packAssets.sh`; it does not need to exist before
121
+ the first packing step.
122
+
123
+ ### 2. Point `entry.mjs` at the main program
124
+
125
+ Keep the asset loader as the first import and change the path passed to the
126
+ final `await import()` so it points to the other project's real entry module:
127
+
128
+ ```js
129
+ #!/usr/bin/env bun
130
+
131
+ import "./assetsLoader.mjs";
132
+ await globalThis.assetsLoaderPromise;
133
+ await import("../src/index.js");
134
+ ```
135
+
136
+ > **Important:** Keep the final import dynamic. Do not replace it with a static
137
+ > `import "../src/index.js"`, because static dependencies are evaluated before
138
+ > the asset Promise is awaited.
139
+
140
+ Only the Bun single-file executable build should use this bootstrap entry.
141
+ Continue to run the regular main module directly when using Node:
142
+
143
+ ```shell
144
+ node ./src/index.js
145
+ ```
146
+
147
+ ### 3. Choose which files to embed
148
+
149
+ Edit the resource list in `packAssets.sh`; the script runs `tar` for you. It
150
+ changes to the project root, writes `single-exe/assets.tar`, and packs every
151
+ listed runtime resource required by the compiled program. Paths stored in the
152
+ archive become the lookup keys used by `assetsHelper.js`; use keys such as
153
+ `public/app.css`, without a leading `./`. The normal `--build-exe` and
154
+ `--build-for` flows invoke this script automatically, so you do not need to run
155
+ `tar` yourself.
156
+
157
+ For example:
158
+
159
+ ```sh
160
+ #!/bin/sh
161
+
162
+ script_dir=$(dirname "$0")
163
+ cd "$script_dir/.." || exit 1
164
+
165
+ tar -cvf single-exe/assets.tar public templates README.md
166
+ ```
167
+
168
+ Do not retain jsmdcui's `demos`, `runtime`, `src/cui`, or `testapp.md` entries
169
+ unless the adopting project actually contains and needs them.
170
+
171
+ ### 4. Read embedded assets with an external fallback
172
+
173
+ Import the Node-compatible helpers from `assetsHelper.js`. They return `null`
174
+ when the embedded store is unavailable or a path is not present:
175
+
176
+ ```js
177
+ import { readFile } from "node:fs/promises";
178
+ import { resolve } from "node:path";
179
+ import { readInternalAssetText } from "../single-exe/assetsHelper.js";
180
+ import { REPO_ROOT } from "../single-exe/compiled.js";
181
+
182
+ async function readResourceText(path) {
183
+ return readInternalAssetText(path) ??
184
+ await readFile(resolve(REPO_ROOT, path), "utf8");
185
+ }
186
+ ```
187
+
188
+ In the source tree, `REPO_ROOT` is the project root. In a compiled executable,
189
+ it is the executable's directory, which is also where `--assets-extract`
190
+ places the external resource tree. Apply the same embedded-first fallback to
191
+ every file that must work in both modes. Use `readInternalAssetBytes()` when
192
+ a resource should be returned as bytes instead of decoded text.
193
+ Use `listInternalAssetPaths()` to list embedded asset paths and
194
+ `listInternalAssetDirs()` to list embedded directories.
195
+
196
+ ### 5. Add the optional build commands to the regular CLI
197
+
198
+ Before normal argument parsing, call `buildEarlyExit` from the regular main
199
+ module:
200
+
201
+ ```js
202
+ const compiledHelper = await import("../single-exe/compiled.js").catch(() => null);
203
+ await compiledHelper?.buildEarlyExit?.(process.argv, "my-bin");
204
+ ```
205
+
206
+ The second argument is the output filename; it defaults to `single.exe` when
207
+ omitted. This enables:
208
+
209
+ ```shell
210
+ bun ./src/index.js --build-exe
211
+ bun ./src/index.js --build-for <target>
212
+ ```
213
+
214
+ Both commands run `packAssets.sh` before `bun build`. The second form passes
215
+ `<target>` through as Bun's `--target=<target>` value.
216
+
217
+ Alternatively, build manually from inside `single-exe/`:
218
+
219
+ ```shell
220
+ bun ./packAssets.sh
221
+ bun build --format=esm --compile --minify --bytecode ./entry.mjs --outfile=my-bin
222
+ ```
223
+
224
+ ### 6. Verify both execution paths
225
+
226
+ Verify the compiled asset archive and then run the program normally:
227
+
228
+ ```shell
229
+ ./my-bin --assets-list
230
+ ./my-bin
231
+ ```
232
+
233
+ Also run the uncompiled main module with Node.js 20.11 or newer to verify that
234
+ all required files have a working external fallback:
235
+
236
+ ```shell
237
+ node ./src/index.js
238
+ ```
239
+
240
+ Use `./my-bin --assets-extract` to write the packed tree beside the executable.
241
+ After extraction, `./my-bin --assets-external` skips the embedded store and is
242
+ useful for testing the external-resource path.
243
+
244
+ ### Why `assetsLoaderPromise` is used
245
+
246
+ A more tightly coupled ESM design could export the assets with top-level
247
+ `await`, then make the main module import `assetsLoader.mjs` directly. This
248
+ project deliberately does not do that because `assetsLoader.mjs` contains
249
+ Bun's compiled-file import:
250
+
251
+ ```js
252
+ import assets from "./assets.tar" with { type: "file" };
253
+ ```
254
+
255
+ If the regular main module imported `assetsLoader.mjs`, that Bun-specific
256
+ `type: "file"` dependency would enter the main module graph. Node would then be
257
+ unable to load the program before it could select an external-file fallback.
258
+
259
+ Instead, only Bun's `entry.mjs` imports the Bun-specific loader, awaits its
260
+ Promise, and then dynamically imports the regular main module. The main module
261
+ does not import the loader, inspect the Promise, or contain Bun bootstrap code.
262
+ Under the supported Node.js 20.11+ ESM path, Node runs that clean main module
263
+ directly with external files. The Promise is therefore an intentional
264
+ Node-compatibility boundary inside the Bun-only bootstrap, not a failure to use
265
+ a more modern dependency or top-level-`await` design.
18
266
 
19
267
  ## Entry Flow
20
268
 
21
269
  - `entry.mjs` imports `assetsLoader.mjs` first
22
270
  - `assetsLoader.mjs` loads `assets.tar` with `Bun.Archive` and mounts it as `globalThis.internalAssets`
23
271
  - `assetsLoaderPromise` is exposed on `globalThis`
24
- - The main program waits for `assetsLoaderPromise` if it exists
272
+ - `entry.mjs` awaits `assetsLoaderPromise`
273
+ - `entry.mjs` dynamically imports the main program after the assets are ready
25
274
 
26
275
  That keeps the main program bootable even if asset loading reports errors.
27
276
 
@@ -32,6 +281,12 @@ That keeps the main program bootable even if asset loading reports errors.
32
281
  - Asset loading never rejects the bootstrap promise
33
282
  - When loading succeeds, the archive is available through `globalThis.internalAssets`
34
283
 
284
+ This loader is not zero-copy: it materializes every bundled file in memory and
285
+ keeps the resulting bytes in `globalThis.internalAssets`. Large asset archives
286
+ can therefore increase startup and ongoing RAM usage. For workloads where that
287
+ matters, I also wrote an experimental Linux-only zero-copy hack:
288
+ [bun-assets-zerocopy](https://github.com/jjtseng93/bun-assets-zerocopy).
289
+
35
290
  ## CLI Flags
36
291
 
37
292
  - `--assets-list`
@@ -46,35 +301,3 @@ That keeps the main program bootable even if asset loading reports errors.
46
301
  - Skips loading bundled assets into `globalThis.internalAssets`
47
302
  - Forces the main program and runtime helpers to use the external file tree
48
303
  - Keeps the bootstrap alive while leaving `internalAssets` falsy
49
-
50
- ## Adapting this folder
51
-
52
- This single-exe folder is intended for reuse by other projects. Usually you
53
- only need to edit these project-specific files first:
54
-
55
- - `entry.mjs`
56
- - `packAssets.sh`
57
-
58
- Then the main program can import `buildEarlyExit` from `compiled.js` and call
59
- it before normal CLI parsing. `--build-exe` and `--build-for <target>` will run
60
- `packAssets.sh` first, so `assets.tar` is generated as part of the build flow.
61
-
62
- ```js
63
- const compiledHelper = await import("./single-exe/compiled.js").catch(() => null);
64
- await compiledHelper?.buildEarlyExit?.(process.argv, "my-bin");
65
- ```
66
-
67
- - `--build-exe` builds with no Bun target.
68
- - `--build-for <target>` passes `<target>` to `bun build --target=<target>`.
69
- - The second argument is the output filename. If omitted, it defaults to `single.exe`.
70
-
71
- The main program should also switch its repo/resource root when running as a
72
- compiled binary. Use `IS_COMPILED` and `REPO_ROOT` from
73
- `compiled.js`; when compiled, treat the executable directory as the repo root
74
- used for external fallback files such as `static/`, `README.md`, or other files
75
- packed by `packAssets.sh`.
76
-
77
- ```js
78
- const compiledHelper = await import("./single-exe/compiled.js").catch(() => null);
79
- const repoRoot = compiledHelper?.REPO_ROOT
80
- ```
@@ -12,9 +12,12 @@ const SINGLE_EXE_DIR = resolve(REPO_ROOT, "single-exe");
12
12
  const SINGLE_EXE_ENTRY = resolve(SINGLE_EXE_DIR, "entry.mjs");
13
13
 
14
14
 
15
- export function isCompiledBinary() {
16
- const argv = process.argv
17
- return Boolean(argv?.[1]?.startsWith?.("/$bunfs/"));
15
+ export function isCompiledBinary(argv = process.argv) {
16
+ const entry = argv?.[1];
17
+ return Boolean(
18
+ entry?.startsWith?.("/$bunfs/") ||
19
+ entry?.startsWith?.("B:/~BUN")
20
+ );
18
21
  }
19
22
 
20
23
  export function getExeDirname() {
@@ -35,10 +38,11 @@ export function getDirnameFromUrl(importMetaUrl) {
35
38
  return dirname(fileURLToPath(importMetaUrl));
36
39
  }
37
40
 
38
- export async function buildExecutable(target = "",build_outfile="single.exe") {
41
+ export async function buildExecutable(target = "",build_outfile="single.exe", bunArgs = []) {
39
42
 
40
43
  const outfile = resolve(process.cwd(), build_outfile);
41
44
  const normalizedTarget = String(target || "").trim();
45
+ const extraBunArgs = Array.from(bunArgs ?? [], String);
42
46
  if(!globalThis.Bun || IS_COMPILED)
43
47
  {
44
48
  console.log("Build exe can only be run by Bun in the source tree");
@@ -68,6 +72,7 @@ export async function buildExecutable(target = "",build_outfile="single.exe") {
68
72
  `--outfile=${outfile}`,
69
73
  `--metafile-md=${outfile}.meta.md`,
70
74
  ...(normalizedTarget ? [`--target=${normalizedTarget}`] : []),
75
+ ...extraBunArgs,
71
76
  ],
72
77
  },
73
78
  ];
@@ -142,8 +147,8 @@ export async function buildEarlyExit(argv,build_outfile) {
142
147
  console.error("Missing target value for --build-for");
143
148
  process.exit(2);
144
149
  }
145
- process.exit(await buildExe(target,build_outfile));
150
+ process.exit(await buildExe(target, build_outfile, argv.slice(buildForIndex + 2)));
146
151
  }
147
152
 
148
- process.exit(await buildExe(null,build_outfile));
153
+ process.exit(await buildExe(null, build_outfile, argv.slice(buildExeIndex + 1)));
149
154
  }
@@ -3,7 +3,8 @@
3
3
  // 1. Injects assets to global.internalAssets
4
4
  // as { "./path/in/tar":file.bytes() }
5
5
  // 2. Sets global.assetsLoaderPromise
6
- import "./assetsLoader.mjs"
7
-
8
- import "../src/index.js"
6
+ // 3. Starts the main program only after the assets are ready
7
+ import "./assetsLoader.mjs";
9
8
 
9
+ await globalThis.assetsLoaderPromise;
10
+ await import("../src/index.js");
@@ -4,4 +4,4 @@ sd=$(dirname "$0")
4
4
 
5
5
  cd "$sd"/..
6
6
 
7
- tar -cvf single-exe/assets.tar demos runtime README.md CHANGELOG.md src/cui/server.mjs src/cui/rpc.mjs testapp.md
7
+ tar -cvf single-exe/assets.tar demos runtime README.md CHANGELOG.md cdp-maze.js src/cui/server.mjs src/cui/rpc.mjs testapp.md
@@ -1,7 +1,7 @@
1
1
  import { appendFileSync, writeFileSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
3
 
4
- export const KITTY_DEBUG_LOG = resolve(import.meta.dir, "../..", "kitty-placement.log");
4
+ export const KITTY_DEBUG_LOG = resolve(import.meta.dirname, "../..", "kitty-placement.log");
5
5
  export const KITTY_DEBUG_ENABLED = /^(1|true|yes|on)$/i.test(
6
6
  String(process.env.JSMDCUI_KITTY_DEBUG ?? ""),
7
7
  );