buninu 0.3.1 → 0.4.0
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.
- package/CHANGELOG.md +71 -0
- package/README.md +313 -37
- package/apps/cmdlist +6 -1
- package/apps/jsgotty/gotty.js +65 -76
- package/apps/jsgotty/package.json +6 -1
- package/apps/jsgotty/single-exe/README.md +358 -54
- package/apps/jsgotty/single-exe/assetsHelper.js +293 -12
- package/apps/jsgotty/single-exe/assetsLoader.mjs +1 -1
- package/apps/jsgotty/single-exe/assetsPacker.js +218 -0
- package/apps/jsgotty/single-exe/compiled.js +379 -19
- package/apps/jsgotty/single-exe/entry.mjs +16 -2
- package/apps/jsgotty/zmodem-node.js +14 -12
- package/apps/jsmdcui/CHANGELOG.md +56 -0
- package/apps/jsmdcui/package.json +12 -2
- package/apps/jsmdcui/single-exe/README.md +298 -375
- package/apps/jsmdcui/single-exe/assetsHelper.js +198 -18
- package/apps/jsmdcui/single-exe/assetsLoader.mjs +1 -1
- package/apps/jsmdcui/single-exe/assetsPacker.js +218 -0
- package/apps/jsmdcui/single-exe/compiled.js +291 -5
- package/apps/jsmdcui/single-exe/entry.mjs +7 -0
- package/apps/jsmdcui/src/index.js +11 -8
- package/apps/jsmdcui/tests/jmigt.js +16 -0
- package/bin/bunmsh.bat +3 -0
- package/bin/bunmsh.sh +5 -0
- package/bin/init.js +37 -2
- package/bin/install.js +541 -0
- package/package.json +3 -2
|
@@ -1,473 +1,396 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Single-file executable bootstrap
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Bundles a project's runtime assets into one `bun build --compile` binary, so
|
|
4
|
+
the executable carries its own `demos/`, `runtime/`, templates and so on.
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
main-module path and asset list as described below.
|
|
6
|
+
Assets are declared once, in `package.json`:
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
- Build for the current platform or pass a Bun cross-compilation target.
|
|
14
|
-
|
|
15
|
-
> This workflow is optional; the standard methods in the root README remain
|
|
16
|
-
> available. Bun's Android build is currently not supported.
|
|
17
|
-
|
|
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
|
|
8
|
+
```json
|
|
9
|
+
{
|
|
10
|
+
"assets": ["demos", "runtime", "README.md", "src/cui/server.mjs"]
|
|
11
|
+
}
|
|
25
12
|
```
|
|
26
13
|
|
|
27
|
-
|
|
28
|
-
|
|
14
|
+
Paths are relative to the project root. Folders are packed recursively; only
|
|
15
|
+
regular files are stored, so symlinks and empty folders leave no trace.
|
|
29
16
|
|
|
30
|
-
|
|
31
|
-
bun ./src/index.js --build-for <target>
|
|
32
|
-
```
|
|
17
|
+
## The two back ends
|
|
33
18
|
|
|
34
|
-
|
|
35
|
-
|
|
19
|
+
The default packs everything into a gzipped `assets.tar` that is embedded as a
|
|
20
|
+
single file and unpacked into memory at startup. Setting `ASSETS_BUNFS=1` at
|
|
21
|
+
build time switches to `bun build --compile --asset`, which stores the files
|
|
22
|
+
individually inside the binary's virtual filesystem instead.
|
|
36
23
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
bun ./src/index.js --build-for <target> --sourcemap
|
|
40
|
-
```
|
|
24
|
+
Reading code never sees the difference — `assetsHelper` answers the same keys
|
|
25
|
+
either way. The trade is binary size against startup cost:
|
|
41
26
|
|
|
42
|
-
|
|
43
|
-
need quotes. `stringifyNonPrimitiveDefineValues()` converts a bare value with
|
|
44
|
-
`JSON.stringify()` before it reaches Bun. jsmdcui uses it for
|
|
45
|
-
`global.MDCUI_MAIN`.
|
|
46
|
-
|
|
47
|
-
`global.MDCUI_MAIN` is a jsmdcui application convention, not a name reserved by
|
|
48
|
-
the single-executable helpers. An adopting project can use
|
|
49
|
-
`global.MY_EMBEDDED_APP`, or any other build expression, as long as it updates
|
|
50
|
-
its own call sites and passes the same name to
|
|
51
|
-
`stringifyNonPrimitiveDefineValues()`.
|
|
52
|
-
|
|
53
|
-
The jsmdcui switch defines below are presence-based: `=0` and `=false` still
|
|
54
|
-
enable them. Omit a switch define to disable it; examples consistently use
|
|
55
|
-
`=1`.
|
|
56
|
-
|
|
57
|
-
- `MDCUI_DEFAULT_EDIT=1`: editable-text default.
|
|
58
|
-
- `MDCUI_DEFAULT_DEMO=1`: no-argument `testapp.md` TUI.
|
|
59
|
-
- `MDCUI_DEFAULT_DEMO_WUI=1`: no-argument `testapp.md` WUI.
|
|
60
|
-
- `MDCUI_OVERWRITE_DEMO=1`: overwrite modifier; it does not select a demo.
|
|
61
|
-
- `global.MDCUI_MAIN=<path>.md`: embed a custom Markdown app and its generated
|
|
62
|
-
front, RPC, back, HTML, and server modules.
|
|
63
|
-
- `global.MDCUI_MAIN_BASE`: generated internally from `MDCUI_MAIN`; do not pass
|
|
64
|
-
it manually.
|
|
65
|
-
|
|
66
|
-
Put defines after `--build-exe` (or after the `--build-for` target), and choose
|
|
67
|
-
at most one `MDCUI_DEFAULT_*` mode.
|
|
68
|
-
|
|
69
|
-
| Build defines | No-argument launch | Switch UI |
|
|
27
|
+
| | tar (default) | `ASSETS_BUNFS=1` |
|
|
70
28
|
| --- | --- | --- |
|
|
71
|
-
|
|
|
72
|
-
|
|
|
73
|
-
|
|
|
74
|
-
|
|
|
75
|
-
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
29
|
+
| Stored as | one gzipped archive | individual files, uncompressed |
|
|
30
|
+
| Size, for jsgotty's 3.45 MB of assets | **0.70 MB** | 3.45 MB |
|
|
31
|
+
| Work before `main` runs | decode the archive, copy every file into memory | **none** |
|
|
32
|
+
| Startup cost, same assets | ~16 ms | **0 ms** |
|
|
33
|
+
| Resident memory | whole payload, always | only what is read |
|
|
34
|
+
| Maturity | in use | experimental |
|
|
35
|
+
|
|
36
|
+
Text compresses well, so the tar usually wins on size by a wide margin — the
|
|
37
|
+
numbers above are mostly one 1.6 MB bundle and its source map. What it costs is
|
|
38
|
+
paid on every run, whether or not the program touches a single asset. Reach for
|
|
39
|
+
`ASSETS_BUNFS=1` when that startup work, or holding the whole payload in memory,
|
|
40
|
+
matters more than the extra megabytes on disk.
|
|
41
|
+
|
|
42
|
+
## Adopting it in another project
|
|
43
|
+
|
|
44
|
+
Copy `single-exe/` to the project root. Nothing inside it hard-codes its own
|
|
45
|
+
name or depth, so it can be renamed or moved — but the importing code has to
|
|
46
|
+
follow, so staying at the root is the easy path. Three edits point it at the
|
|
47
|
+
project around it.
|
|
48
|
+
|
|
49
|
+
1. **`assetsPacker.js`, the first two lines.** They are the only place the
|
|
50
|
+
asset root is written down, and a static import specifier cannot be
|
|
51
|
+
computed, so both must be literal:
|
|
52
|
+
|
|
53
|
+
```js
|
|
54
|
+
import pkg from "../package.json" with { type: "json" };
|
|
55
|
+
export const ASSETS_ROOT = "..";
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
2. **`entry.mjs`, the import of the program.** It names what to start, and
|
|
59
|
+
entry.mjs is the compiled entry point in both back ends:
|
|
60
|
+
|
|
61
|
+
```js
|
|
62
|
+
await import("../src/index.js");
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Keep the `await`.** It reads like a formality — nothing here needs the
|
|
66
|
+
module object — but together with the `await globalThis.assetsLoaderPromise`
|
|
67
|
+
line above it, it is the only top-level `await` the compiled program writes
|
|
68
|
+
anywhere, and `--bytecode` reads exactly that to decide the program starts
|
|
69
|
+
asynchronously. Turn both into `.then(…)` and the build produces an
|
|
70
|
+
executable that exits 0 without running a line.
|
|
71
|
+
[Why](#the-await-in-entrymjs-is-load-bearing) explains the trap.
|
|
72
|
+
|
|
73
|
+
3. **`package.json`.** Add the `assets` array shown above.
|
|
74
|
+
|
|
75
|
+
Everything else — `compiled.js`, `assetsHelper.js`, `assetsLoader.mjs` — is
|
|
76
|
+
project independent, with one exception: `buildHtmlBundleImageMap` defaults its
|
|
77
|
+
attribute name to jsmdcui's `data-mdcui-src`, so pass your own.
|
|
78
|
+
|
|
79
|
+
Node's own entry path must not import `assetsLoader.mjs` or `entry.mjs`; those
|
|
80
|
+
are Bun only. `assetsHelper.js` and `compiled.js` run under Node 20.11+.
|
|
81
|
+
|
|
82
|
+
## Reading assets
|
|
83
|
+
|
|
84
|
+
Import from `assetsHelper.js`. Keys are the same package-relative paths that
|
|
85
|
+
appear in `package.json`.
|
|
86
|
+
|
|
87
|
+
| Function | Returns |
|
|
88
|
+
| --- | --- |
|
|
89
|
+
| `readInternalAssetText(path)` | string, or `null` when not embedded |
|
|
90
|
+
| `readInternalAssetBytes(path)` | `Uint8Array`, or `null` |
|
|
91
|
+
| `readAssetText(path)` | string, falling back to the file on disk |
|
|
92
|
+
| `readAssetBytes(path)` | `Uint8Array`, same fallback |
|
|
93
|
+
| `hasInternalAssets()` | whether this package has anything embedded |
|
|
94
|
+
| `listInternalAssetPaths(prefix?)` | every embedded path under `prefix` |
|
|
95
|
+
| `listInternalAssetDirs(prefix?)` | the immediate child names under `prefix` |
|
|
96
|
+
| `assetPath(...parts)` | joins parts into a key |
|
|
97
|
+
| `getAssetKey(path)` | the key as stored, namespace included |
|
|
98
|
+
| `assetDiskPath(path)` | where the disk fallback reads that key from |
|
|
99
|
+
| `SELF` | this package's namespace, `assets/<name>@<version>` |
|
|
100
|
+
|
|
101
|
+
The read functions are synchronous for embedded assets on both back ends.
|
|
102
|
+
`readAssetText` / `readAssetBytes` are async only because of the disk
|
|
103
|
+
fallback, which is what makes the same code work from a source checkout.
|
|
104
|
+
|
|
105
|
+
**Only the read functions fall back to disk.** The listing functions report
|
|
106
|
+
what is embedded and nothing else, so from a source checkout they return an
|
|
107
|
+
empty array. Branch on `hasInternalAssets()` and read the directory yourself:
|
|
85
108
|
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
109
|
+
```js
|
|
110
|
+
import { readdirSync } from "node:fs";
|
|
111
|
+
import { join } from "node:path";
|
|
112
|
+
import { readAssetText, hasInternalAssets, listInternalAssetDirs } from "../single-exe/assetsHelper.js";
|
|
113
|
+
import { REPO_ROOT } from "../single-exe/compiled.js";
|
|
114
|
+
|
|
115
|
+
// Embedded or on disk, this one already works either way.
|
|
116
|
+
const page = await readAssetText("templates/page.html");
|
|
117
|
+
|
|
118
|
+
// Listing does not, so pick the source.
|
|
119
|
+
const demos = hasInternalAssets()
|
|
120
|
+
? listInternalAssetDirs("demos")
|
|
121
|
+
: readdirSync(join(REPO_ROOT, "demos"));
|
|
90
122
|
```
|
|
91
123
|
|
|
92
|
-
|
|
93
|
-
UI must repeat `--demo` or `--demo-中文工具`. The main basename must end in
|
|
94
|
-
lowercase `.md`; its demo name allows Unicode letters/numbers (including
|
|
95
|
-
Chinese), dots, underscores, and hyphens, but no whitespace.
|
|
124
|
+
## Building
|
|
96
125
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
demos use embedded modules after the asset is written. A differing byte length
|
|
100
|
-
warns and selects filesystem companion modules.
|
|
126
|
+
`compiled.js` exports the build entry points. Wire `buildEarlyExit` into the
|
|
127
|
+
CLI and it handles `--build-exe` and `--build-for <target>`:
|
|
101
128
|
|
|
102
|
-
|
|
129
|
+
```js
|
|
130
|
+
import { buildEarlyExit, IS_COMPILED, REPO_ROOT } from "./single-exe/compiled.js";
|
|
103
131
|
|
|
104
|
-
|
|
105
|
-
bun ./packAssets.sh
|
|
106
|
-
bun build --format=esm --compile --minify --bytecode ./entry.mjs --outfile=mdcui
|
|
132
|
+
await buildEarlyExit(process.argv, "myapp");
|
|
107
133
|
```
|
|
108
134
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
`entry.mjs` and `packAssets.sh` contain jsmdcui paths, so another project must
|
|
113
|
-
complete the following integration steps.
|
|
114
|
-
|
|
115
|
-
The adopting project needs Bun for packing and compiling, a `tar` command for
|
|
116
|
-
`packAssets.sh`, and an ES-module setup. Keep `"type": "module"` in
|
|
117
|
-
`package.json`, or rename the copied `.js` modules to `.mjs` and update their
|
|
118
|
-
imports. The supported uncompiled Node path requires Node.js 20.11 or newer
|
|
119
|
-
because `compiled.js` uses `import.meta.dirname`.
|
|
120
|
-
|
|
121
|
-
`assetsHelper.js` and `compiled.js` are Node-compatible under those conditions.
|
|
122
|
-
`assetsLoader.mjs` and `entry.mjs` are Bun-only and must not be imported by the
|
|
123
|
-
regular Node entry path.
|
|
124
|
-
|
|
125
|
-
### 1. Copy the bootstrap directory
|
|
126
|
-
|
|
127
|
-
Copy the entire `single-exe/` directory to the root of the other project and
|
|
128
|
-
keep its name and relative position unless you also update the paths in
|
|
129
|
-
`compiled.js` and `packAssets.sh`. A typical layout is:
|
|
130
|
-
|
|
131
|
-
```text
|
|
132
|
-
my-project/
|
|
133
|
-
├── single-exe/
|
|
134
|
-
│ ├── assetsHelper.js
|
|
135
|
-
│ ├── assetsLoader.mjs
|
|
136
|
-
│ ├── compiled.js
|
|
137
|
-
│ ├── entry.mjs
|
|
138
|
-
│ └── packAssets.sh
|
|
139
|
-
└── src/
|
|
140
|
-
└── index.js
|
|
141
|
-
```
|
|
135
|
+
`REPO_ROOT` is the project root in a checkout and the executable's own folder
|
|
136
|
+
once compiled, which is where `--assets-extract` writes. `IS_COMPILED` tells
|
|
137
|
+
the two apart.
|
|
142
138
|
|
|
143
|
-
|
|
144
|
-
the first packing step.
|
|
139
|
+
### Chaining file/folder assets across dependencies
|
|
145
140
|
|
|
146
|
-
|
|
141
|
+
A build first bundles once to write a metafile, then treats every
|
|
142
|
+
`assetsPacker.js` appearing in its `inputs` as a participant. Each package
|
|
143
|
+
vendors its own `single-exe/`, and its `assetsHelper` imports its
|
|
144
|
+
`assetsPacker`, so a packer lands in that graph exactly when the package is
|
|
145
|
+
reachable from the entry point. There is no registration step, and anything
|
|
146
|
+
tree-shaken away brings nothing with it.
|
|
147
147
|
|
|
148
|
-
|
|
149
|
-
|
|
148
|
+
The packers then run one after another, each adding its files under its own
|
|
149
|
+
`assets/<name>@<version>/` namespace — appending into the shared `assets.tar`
|
|
150
|
+
by default, or copying into the shared `build/assets` tree under
|
|
151
|
+
`ASSETS_BUNFS=1`. Only once every packer has finished does the real compile
|
|
152
|
+
run, so the archive is complete and, by default, compressed by then.
|
|
150
153
|
|
|
151
|
-
|
|
152
|
-
|
|
154
|
+
That namespace is why `ASSETS_BUNFS=1` stages every package into one fixed
|
|
155
|
+
`build/assets`. `--asset` keeps only the **basename** of the path it is given
|
|
156
|
+
and preserves everything below it, so the folder's own name becomes the root
|
|
157
|
+
inside the binary:
|
|
153
158
|
|
|
154
|
-
import "./assetsLoader.mjs";
|
|
155
|
-
await globalThis.assetsLoaderPromise;
|
|
156
|
-
await import("../src/index.js");
|
|
157
159
|
```
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
> `import "../src/index.js"`, because static dependencies are evaluated before
|
|
161
|
-
> the asset Promise is awaited.
|
|
162
|
-
|
|
163
|
-
Only the Bun single-file executable build should use this bootstrap entry.
|
|
164
|
-
Continue to run the regular main module directly when using Node:
|
|
165
|
-
|
|
166
|
-
```shell
|
|
167
|
-
node ./src/index.js
|
|
160
|
+
--asset ./build/assets with build/assets/jsgotty@1.1.6/static/index.html
|
|
161
|
+
-> assets/jsgotty@1.1.6/static/index.html
|
|
168
162
|
```
|
|
169
163
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
listed runtime resource required by the compiled program. Paths stored in the
|
|
175
|
-
archive become the lookup keys used by `assetsHelper.js`; use keys such as
|
|
176
|
-
`public/app.css`, without a leading `./`. The normal `--build-exe` and
|
|
177
|
-
`--build-for` flows invoke this script automatically, so you do not need to run
|
|
178
|
-
`tar` yourself.
|
|
179
|
-
|
|
180
|
-
For example:
|
|
181
|
-
|
|
182
|
-
```sh
|
|
183
|
-
#!/bin/sh
|
|
184
|
-
|
|
185
|
-
script_dir=$(dirname "$0")
|
|
186
|
-
cd "$script_dir/.." || exit 1
|
|
187
|
-
|
|
188
|
-
tar -cvf single-exe/assets.tar public templates README.md
|
|
189
|
-
```
|
|
164
|
+
Pass `./build/assets/jsgotty@1.1.6` instead and `build/assets/` is gone: the
|
|
165
|
+
files land at `jsgotty@1.1.6/...` and every package gets its own root. One
|
|
166
|
+
folder named `assets`, everyone copying into it, is what makes the keys come
|
|
167
|
+
out the same as the ones the tar back end writes.
|
|
190
168
|
|
|
191
|
-
|
|
192
|
-
unless the adopting project actually contains and needs them.
|
|
169
|
+
`assetsPacker.js` also runs standalone; `--help` documents its flags.
|
|
193
170
|
|
|
194
|
-
|
|
171
|
+
## Why the loader hands over a Promise
|
|
195
172
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
173
|
+
`entry.mjs` imports `assetsLoader.mjs`, awaits `globalThis.assetsLoaderPromise`,
|
|
174
|
+
and only then imports the main program. The indirection is deliberate: the
|
|
175
|
+
loader carries a Bun-only import,
|
|
199
176
|
|
|
200
177
|
```js
|
|
201
|
-
import {
|
|
202
|
-
readAssetBytes,
|
|
203
|
-
readAssetText,
|
|
204
|
-
} from "../single-exe/assetsHelper.js";
|
|
205
|
-
|
|
206
|
-
// Embedded asset first; external REPO_ROOT/templates/page.html fallback.
|
|
207
|
-
const source = await readAssetText("templates/page.html");
|
|
208
|
-
|
|
209
|
-
// The same fallback behavior, returned as Uint8Array.
|
|
210
|
-
const image = await readAssetBytes("images/photo.png");
|
|
178
|
+
import assets from "./assets.tar" with { type: "file" };
|
|
211
179
|
```
|
|
212
180
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
resource should be returned as embedded bytes without the fallback.
|
|
217
|
-
Use `listInternalAssetPaths()` to list embedded asset paths and
|
|
218
|
-
`listInternalAssetDirs()` to list embedded directories.
|
|
181
|
+
and if the main program imported the loader, that dependency would land in its
|
|
182
|
+
module graph. Node could then no longer load the program at all — not even to
|
|
183
|
+
fall back to reading the assets from disk.
|
|
219
184
|
|
|
220
|
-
|
|
185
|
+
Keeping the loader behind `entry.mjs` means the main program never mentions it,
|
|
186
|
+
so Node 20.11+ runs the same file directly against on-disk assets while Bun
|
|
187
|
+
runs it through the bootstrap. The Promise is that boundary, not a workaround
|
|
188
|
+
for missing top-level await.
|
|
221
189
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
the path returned by `homepage.files`; do not hard-code either platform prefix.
|
|
190
|
+
`ASSETS_BUNFS=1` sidesteps the question: there is no tar to load, so the build
|
|
191
|
+
writes `.bunfs-entry.mjs` next to `entry.mjs` — the same file with the loader
|
|
192
|
+
import removed — compiles that, and deletes it afterwards. A copy rather than a
|
|
193
|
+
stub, because entry.mjs may do more than import the program: a CommonJS main
|
|
194
|
+
that starts itself from `require.main === module` needs the exported starter
|
|
195
|
+
called instead, and that call has to survive.
|
|
229
196
|
|
|
230
|
-
|
|
231
|
-
attribute. Its name belongs to the adopting project; it is not fixed by the
|
|
232
|
-
helper. This example chooses `data-original-image`. Bun rewrites `src` but
|
|
233
|
-
leaves the custom attribute intact:
|
|
234
|
-
|
|
235
|
-
```html
|
|
236
|
-
<!-- Input -->
|
|
237
|
-
<img src="./images/photo.jpg" data-original-image="./images/photo.jpg">
|
|
197
|
+
## The `await` in entry.mjs is load-bearing
|
|
238
198
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
199
|
+
`--bytecode` decides whether a program starts asynchronously by looking for a
|
|
200
|
+
top-level `await` **written in some file's own source**. The bundler moves a
|
|
201
|
+
module into a lazy init function whenever something dynamically imports it, and
|
|
202
|
+
a module moved that way takes its top-level `await` with it — what stays behind
|
|
203
|
+
is an `await init_x()` the bundler wrote itself, which does not count. If no
|
|
204
|
+
file wrote one, the executable is evaluated on the synchronous path, its
|
|
205
|
+
suspended startup is dropped, and it exits 0 having run nothing. No error, at
|
|
206
|
+
any `--minify` setting, however small the program.
|
|
242
207
|
|
|
243
|
-
|
|
244
|
-
terminal runtime can load:
|
|
208
|
+
`entry.mjs` is immune because it writes two of its own:
|
|
245
209
|
|
|
246
210
|
```js
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
export { homepage };
|
|
211
|
+
await globalThis.assetsLoaderPromise;
|
|
212
|
+
await import("../src/index.js");
|
|
250
213
|
```
|
|
251
214
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
215
|
+
One is enough, and every other `await` in the graph — all of
|
|
216
|
+
`assetsLoader.mjs`'s, for one — sits inside a function and cannot help. Turn
|
|
217
|
+
both of these into `.then(…)` and the flag is gone. It is also why bunfs builds
|
|
218
|
+
compile a copy of this file rather than handing the main program to `bun build`
|
|
219
|
+
directly: a main program that reaches top-level await only through its imports
|
|
220
|
+
writes none of its own.
|
|
255
221
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
canonicalHtmlBundleImageHref,
|
|
260
|
-
} from "../single-exe/assetsHelper.js";
|
|
261
|
-
import { IS_COMPILED } from "../single-exe/compiled.js";
|
|
222
|
+
Upstream: `src/bundler/linker_context/postProcessJSChunk.rs` skips exactly the
|
|
223
|
+
modules the bundler moved, still true on oven-sh/bun `main` at 8eb5b6e2b5
|
|
224
|
+
(2026-08-22). Until that changes, treat both `await`s as API.
|
|
262
225
|
|
|
263
|
-
|
|
264
|
-
const imageSourceAttribute = "data-original-image";
|
|
226
|
+
## Environment variables
|
|
265
227
|
|
|
266
|
-
|
|
267
|
-
|
|
228
|
+
| Variable | When | Effect |
|
|
229
|
+
| --- | --- | --- |
|
|
230
|
+
| `ASSETS_BUNFS=1` | build | use `--asset` instead of the tar |
|
|
231
|
+
| `ASSETS_NO_GZIP=1` | build | leave `assets.tar` uncompressed |
|
|
232
|
+
| `ASSETS_DEBUG=1` | run | print how long unpacking took |
|
|
268
233
|
|
|
269
|
-
|
|
270
|
-
.then(module => buildHtmlBundleImageMap(
|
|
271
|
-
module.homepage,
|
|
272
|
-
imageSourceAttribute,
|
|
273
|
-
));
|
|
234
|
+
## Runtime flags
|
|
274
235
|
|
|
275
|
-
|
|
276
|
-
|
|
236
|
+
| Flag | Effect |
|
|
237
|
+
| --- | --- |
|
|
238
|
+
| `--assets-list` | print every embedded path and exit |
|
|
239
|
+
| `--assets-extract` | write the assets next to the executable and exit |
|
|
240
|
+
| `--assets-external` | ignore embedded assets and read from disk |
|
|
277
241
|
|
|
278
|
-
|
|
279
|
-
const key = canonicalHtmlBundleImageHref(originalImageHref);
|
|
280
|
-
const bundledPath = images?.get(key);
|
|
281
|
-
const bytes = bundledPath ? await Bun.file(bundledPath).bytes() : null;
|
|
282
|
-
```
|
|
242
|
+
These are handled by `assetsLoader.mjs`, so they exist in tar builds only.
|
|
283
243
|
|
|
284
|
-
|
|
285
|
-
to match the adopting application, replace it with a direct static import, or
|
|
286
|
-
use another compiled-only condition. The helper does not inspect or require
|
|
287
|
-
`global.MDCUI_MAIN`; that is jsmdcui's own convention. When using a Bun
|
|
288
|
-
`--define`, use the same expression at the call site, for example
|
|
289
|
-
`--define global.MY_EMBEDDED_APP=...`.
|
|
244
|
+
## Advanced usage
|
|
290
245
|
|
|
291
|
-
|
|
292
|
-
once and returns a `Map<string, string>` from canonical original image
|
|
293
|
-
references to Bun compiled virtual paths on either platform. The second
|
|
294
|
-
argument is the custom attribute name. It defaults to `"data-mdcui-src"` only
|
|
295
|
-
for jsmdcui's convenience:
|
|
296
|
-
|
|
297
|
-
```js
|
|
298
|
-
const images = await buildHtmlBundleImageMap(homepage);
|
|
299
|
-
```
|
|
246
|
+
### Forwarding arguments to `bun build`
|
|
300
247
|
|
|
301
|
-
|
|
302
|
-
|
|
248
|
+
Anything after `--build-exe`, or after the target given to `--build-for`, is
|
|
249
|
+
passed straight through to `bun build`:
|
|
303
250
|
|
|
304
|
-
```
|
|
305
|
-
|
|
251
|
+
```shell
|
|
252
|
+
bun ./src/index.js --build-exe --sourcemap
|
|
253
|
+
bun ./src/index.js --build-for bun-linux-x64 --sourcemap
|
|
306
254
|
```
|
|
307
255
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
homepage,
|
|
311
|
-
"data-original-image",
|
|
312
|
-
);
|
|
313
|
-
```
|
|
256
|
+
Order matters. The flags have to come after the build switch, or Bun consumes
|
|
257
|
+
them before the program ever starts:
|
|
314
258
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
259
|
+
```shell
|
|
260
|
+
# Wrong: Bun parses this define before any of your code runs
|
|
261
|
+
bun --define MY_APP=../app.md ./src/index.js --build-exe
|
|
318
262
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
`image/*`, and `htmlBundleImageAssetPath()` returns only the corresponding
|
|
323
|
-
Bun compiled virtual path.
|
|
263
|
+
# Right
|
|
264
|
+
bun ./src/index.js --build-exe --define MY_APP=../app.md
|
|
265
|
+
```
|
|
324
266
|
|
|
325
|
-
###
|
|
267
|
+
### `--define` with a string value
|
|
326
268
|
|
|
327
|
-
|
|
328
|
-
|
|
269
|
+
**Bun expects `--define` values to be JSON literals, so a string has to arrive
|
|
270
|
+
already quoted.** A bare path is read as an identifier and the build fails or
|
|
271
|
+
inlines something unintended. Quoting it through a shell is awkward, so
|
|
272
|
+
`compiled.js` exports a helper that does it:
|
|
329
273
|
|
|
330
274
|
```js
|
|
331
|
-
|
|
332
|
-
await compiledHelper?.buildEarlyExit?.(process.argv, "my-bin");
|
|
333
|
-
```
|
|
334
|
-
|
|
335
|
-
The second argument is the output filename; it defaults to `single.exe` when
|
|
336
|
-
omitted. This enables:
|
|
275
|
+
import { buildEarlyExit, stringifyNonPrimitiveDefineValues } from "./single-exe/compiled.js";
|
|
337
276
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
bun ./src/index.js --build-for <target>
|
|
277
|
+
stringifyNonPrimitiveDefineValues(process.argv, "MY_APP");
|
|
278
|
+
await buildEarlyExit(process.argv, "my-bin");
|
|
341
279
|
```
|
|
342
280
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
Alternatively, build manually from inside `single-exe/`:
|
|
281
|
+
Call it before `buildEarlyExit`, once per define name you want treated as a
|
|
282
|
+
string. It rewrites the value in place:
|
|
347
283
|
|
|
348
|
-
```
|
|
349
|
-
|
|
350
|
-
|
|
284
|
+
```js
|
|
285
|
+
["--define", "MY_APP=../app.md"] // what the user typed
|
|
286
|
+
["--define", 'MY_APP="../app.md"'] // what Bun receives
|
|
351
287
|
```
|
|
352
288
|
|
|
353
|
-
|
|
289
|
+
`--define=MY_APP=../app.md` works the same way. Numbers, booleans, `null` and
|
|
290
|
+
`undefined` are left alone, so only genuine strings get quoted.
|
|
354
291
|
|
|
355
|
-
|
|
356
|
-
strings to be quoted literals. To accept a bare value such as a path, call the below
|
|
357
|
-
helper to normalize it before `buildEarlyExit()`:
|
|
292
|
+
### Images from an imported HTML bundle
|
|
358
293
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
294
|
+
A compiled binary exposes an imported HTML entry as a `homepage` object:
|
|
295
|
+
`homepage.index` is the compiled HTML and `homepage.files` lists the
|
|
296
|
+
content-hashed assets by their path inside the binary. `assetsHelper` can map
|
|
297
|
+
the *original* image references back to those paths, without copying any image
|
|
298
|
+
bytes into memory.
|
|
364
299
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
```
|
|
300
|
+
Bun rewrites `src` during the build, so keep the original reference in a custom
|
|
301
|
+
attribute of your own choosing:
|
|
368
302
|
|
|
369
|
-
```
|
|
370
|
-
|
|
371
|
-
|
|
303
|
+
```html
|
|
304
|
+
<!-- source -->
|
|
305
|
+
<img src="./images/photo.jpg" data-original-image="./images/photo.jpg">
|
|
372
306
|
|
|
373
|
-
|
|
374
|
-
|
|
307
|
+
<!-- compiled -->
|
|
308
|
+
<img src="/photo-abcd1234.jpg" data-original-image="./images/photo.jpg">
|
|
375
309
|
```
|
|
376
310
|
|
|
377
|
-
|
|
311
|
+
Then build the map once, in a compiled binary only:
|
|
378
312
|
|
|
379
313
|
```js
|
|
380
|
-
|
|
381
|
-
|
|
314
|
+
import { buildHtmlBundleImageMap, canonicalHtmlBundleImageHref } from "../single-exe/assetsHelper.js";
|
|
315
|
+
import { IS_COMPILED } from "../single-exe/compiled.js";
|
|
382
316
|
|
|
383
|
-
|
|
317
|
+
const images = IS_COMPILED
|
|
318
|
+
? await buildHtmlBundleImageMap(homepage, "data-original-image")
|
|
319
|
+
: null;
|
|
384
320
|
|
|
385
|
-
|
|
386
|
-
|
|
321
|
+
const path = images?.get(canonicalHtmlBundleImageHref(originalHref));
|
|
322
|
+
const bytes = path ? await Bun.file(path).bytes() : null;
|
|
387
323
|
```
|
|
388
324
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
325
|
+
`buildHtmlBundleImageMap` returns a `Map` from canonical original reference to
|
|
326
|
+
the path inside the binary; it decodes HTML entities and percent encoding so a
|
|
327
|
+
reference written either way still matches. Reading stays lazy — the bytes are
|
|
328
|
+
only touched when you use the returned path.
|
|
392
329
|
|
|
330
|
+
Lower level, if you already hold a rewritten public path such as
|
|
331
|
+
`/photo-abcd1234.jpg`: `findHtmlBundleAsset(homepage, publicPath, options)`
|
|
332
|
+
finds its `homepage.files` entry, `findHtmlBundleImageAsset()` restricts that
|
|
333
|
+
to `image/*`, and `htmlBundleImageAssetPath()` returns just the path.
|
|
393
334
|
|
|
394
|
-
|
|
335
|
+
Never hard-code `/$bunfs/root/` or `B:/~BUN/`; always use the path that
|
|
336
|
+
`homepage.files` reports.
|
|
395
337
|
|
|
396
|
-
|
|
338
|
+
## jsmdcui specifics
|
|
397
339
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
./my-bin
|
|
401
|
-
```
|
|
340
|
+
The rest of this file is generic. These names belong to jsmdcui itself and are
|
|
341
|
+
not reserved by the bootstrap — an adopting project picks its own.
|
|
402
342
|
|
|
403
|
-
|
|
404
|
-
|
|
343
|
+
Embedding a Markdown app has its own flags, and they are what you normally
|
|
344
|
+
want:
|
|
405
345
|
|
|
406
346
|
```shell
|
|
407
|
-
|
|
347
|
+
bun ./src/index.js --build-md-exe ./中文工具.md
|
|
348
|
+
bun ./src/index.js --build-md-for bun-linux-x64 ./中文工具.md
|
|
408
349
|
```
|
|
409
350
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
### Why `assetsLoaderPromise` is used
|
|
415
|
-
|
|
416
|
-
A more tightly coupled ESM design could export the assets with top-level
|
|
417
|
-
`await`, then make the main module import `assetsLoader.mjs` directly. This
|
|
418
|
-
project deliberately does not do that because `assetsLoader.mjs` contains
|
|
419
|
-
Bun's compiled-file import:
|
|
351
|
+
Each expands into the plain build switch plus the define, so the low-level form
|
|
352
|
+
below is only needed when you want to combine it with other defines or drive
|
|
353
|
+
the build yourself:
|
|
420
354
|
|
|
421
|
-
```
|
|
422
|
-
|
|
355
|
+
```shell
|
|
356
|
+
--build-md-exe ./app.md -> --build-exe --define global.MDCUI_MAIN=./app.md
|
|
357
|
+
--build-md-for <p> ./app.md -> --build-for <p> --define global.MDCUI_MAIN=./app.md
|
|
423
358
|
```
|
|
424
359
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
360
|
+
`global.MDCUI_MAIN` embeds a custom Markdown app together with its generated
|
|
361
|
+
front, RPC, back, HTML and server modules. It is a string define, so it goes
|
|
362
|
+
through `stringifyNonPrimitiveDefineValues(process.argv, "global.MDCUI_MAIN")`.
|
|
363
|
+
`global.MDCUI_MAIN_BASE` is derived from it during the build; never pass it by
|
|
364
|
+
hand.
|
|
428
365
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
does not import the loader, inspect the Promise, or contain Bun bootstrap code.
|
|
432
|
-
Under the supported Node.js 20.11+ ESM path, Node runs that clean main module
|
|
433
|
-
directly with external files. The Promise is therefore an intentional
|
|
434
|
-
Node-compatibility boundary inside the Bun-only bootstrap, not a failure to use
|
|
435
|
-
a more modern dependency or top-level-`await` design.
|
|
366
|
+
The switch defines are presence-based — `=0` and `=false` still enable them, so
|
|
367
|
+
omit a switch to disable it. Choose at most one `MDCUI_DEFAULT_*` mode.
|
|
436
368
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
That keeps the main program bootable even if asset loading reports errors.
|
|
446
|
-
|
|
447
|
-
## Assets Loading
|
|
448
|
-
|
|
449
|
-
- Bundled assets are loaded sequentially with `await file.bytes()`
|
|
450
|
-
- Load failures are collected and printed to `stderr`
|
|
451
|
-
- Asset loading never rejects the bootstrap promise
|
|
452
|
-
- When loading succeeds, the archive is available through `globalThis.internalAssets`
|
|
369
|
+
| Build define | No-argument launch | Switch UI at runtime |
|
|
370
|
+
| --- | --- | --- |
|
|
371
|
+
| `MDCUI_DEFAULT_EDIT=1` | text editor | `./mdcui --tui app.md` |
|
|
372
|
+
| `MDCUI_DEFAULT_DEMO=1` | `testapp.md` TUI | `./mdcui --wui --demo` |
|
|
373
|
+
| `MDCUI_DEFAULT_DEMO_WUI=1` | `testapp.md` WUI | `./mdcui --tui --demo` |
|
|
374
|
+
| `global.MDCUI_MAIN=../中文工具.md` | custom TUI | `./mdcui --wui --demo-中文工具` |
|
|
375
|
+
| the above plus `MDCUI_DEFAULT_DEMO_WUI=1` | custom WUI | `./mdcui --tui --demo-中文工具` |
|
|
453
376
|
|
|
454
|
-
|
|
455
|
-
keeps the resulting bytes in `globalThis.internalAssets`. Large asset archives
|
|
456
|
-
can therefore increase startup and ongoing RAM usage. For workloads where that
|
|
457
|
-
matters, I also wrote an experimental Linux-only zero-copy hack:
|
|
458
|
-
[bun-assets-zerocopy](https://github.com/jjtseng93/bun-assets-zerocopy).
|
|
377
|
+
`MDCUI_OVERWRITE_DEMO=1` is a modifier, not a mode; it does not select a demo.
|
|
459
378
|
|
|
460
|
-
|
|
379
|
+
```shell
|
|
380
|
+
# custom TUI-default executable
|
|
381
|
+
bun ./src/index.js --build-md-exe ../中文工具.md
|
|
461
382
|
|
|
462
|
-
-
|
|
463
|
-
|
|
464
|
-
|
|
383
|
+
# custom WUI-default executable, where the second define needs the long form
|
|
384
|
+
bun ./src/index.js --build-md-exe ../中文工具.md \
|
|
385
|
+
--define MDCUI_DEFAULT_DEMO_WUI=1
|
|
386
|
+
```
|
|
465
387
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
388
|
+
Explicit runtime arguments suppress the automatic demo/WUI selection, so
|
|
389
|
+
switching UI has to repeat `--demo` or `--demo-中文工具`. The main basename must
|
|
390
|
+
end in lowercase `.md`; the demo name it derives allows Unicode letters and
|
|
391
|
+
numbers, dots, underscores and hyphens, but no whitespace.
|
|
469
392
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
393
|
+
The custom demo uses the embedded TUI front/RPC, or the embedded WUI server,
|
|
394
|
+
when the local Markdown byte length matches the embedded asset. A missing or
|
|
395
|
+
overwritten demo uses the embedded modules after the asset is written. A
|
|
396
|
+
differing byte length warns and falls back to the filesystem companions.
|