buninu 0.3.0 → 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.
Files changed (142) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/README.md +334 -37
  3. package/apps/bunmsh/CHANGELOG.md +262 -0
  4. package/apps/bunmsh/LICENSE +21 -0
  5. package/apps/bunmsh/LICENSE-MKSH +295 -0
  6. package/apps/bunmsh/PORTING.md +192 -0
  7. package/apps/bunmsh/README.md +801 -0
  8. package/apps/bunmsh/bunmsh +7 -0
  9. package/apps/bunmsh/bunshell.md +61 -0
  10. package/apps/bunmsh/help/README.md +1945 -0
  11. package/apps/bunmsh/help/alias.md +22 -0
  12. package/apps/bunmsh/help/basename.md +38 -0
  13. package/apps/bunmsh/help/builtin.md +25 -0
  14. package/apps/bunmsh/help/bun.md +27 -0
  15. package/apps/bunmsh/help/bunmsh.md +47 -0
  16. package/apps/bunmsh/help/cat.md +31 -0
  17. package/apps/bunmsh/help/catfancy.md +35 -0
  18. package/apps/bunmsh/help/cd.md +22 -0
  19. package/apps/bunmsh/help/chmod.md +27 -0
  20. package/apps/bunmsh/help/clear.md +21 -0
  21. package/apps/bunmsh/help/colon.md +22 -0
  22. package/apps/bunmsh/help/command.md +27 -0
  23. package/apps/bunmsh/help/cp.md +28 -0
  24. package/apps/bunmsh/help/cut.md +25 -0
  25. package/apps/bunmsh/help/date.md +25 -0
  26. package/apps/bunmsh/help/dirname.md +25 -0
  27. package/apps/bunmsh/help/echo.md +25 -0
  28. package/apps/bunmsh/help/env.md +27 -0
  29. package/apps/bunmsh/help/eval.md +22 -0
  30. package/apps/bunmsh/help/exec.md +22 -0
  31. package/apps/bunmsh/help/exit.md +21 -0
  32. package/apps/bunmsh/help/export.md +22 -0
  33. package/apps/bunmsh/help/false.md +22 -0
  34. package/apps/bunmsh/help/find.md +33 -0
  35. package/apps/bunmsh/help/getopts.md +28 -0
  36. package/apps/bunmsh/help/grep.md +34 -0
  37. package/apps/bunmsh/help/head.md +27 -0
  38. package/apps/bunmsh/help/kill.md +27 -0
  39. package/apps/bunmsh/help/ln.md +28 -0
  40. package/apps/bunmsh/help/ls.md +37 -0
  41. package/apps/bunmsh/help/lsbun.md +29 -0
  42. package/apps/bunmsh/help/lsfancy.md +37 -0
  43. package/apps/bunmsh/help/md5sum.md +21 -0
  44. package/apps/bunmsh/help/mkdir.md +27 -0
  45. package/apps/bunmsh/help/mktemp.md +26 -0
  46. package/apps/bunmsh/help/mv.md +26 -0
  47. package/apps/bunmsh/help/print.md +30 -0
  48. package/apps/bunmsh/help/printf.md +25 -0
  49. package/apps/bunmsh/help/pwd.md +21 -0
  50. package/apps/bunmsh/help/read.md +27 -0
  51. package/apps/bunmsh/help/readonly.md +27 -0
  52. package/apps/bunmsh/help/realpath.md +21 -0
  53. package/apps/bunmsh/help/rm.md +33 -0
  54. package/apps/bunmsh/help/rmdir.md +27 -0
  55. package/apps/bunmsh/help/sed.md +29 -0
  56. package/apps/bunmsh/help/seq.md +29 -0
  57. package/apps/bunmsh/help/serve.md +50 -0
  58. package/apps/bunmsh/help/set.md +26 -0
  59. package/apps/bunmsh/help/sha256sum.md +21 -0
  60. package/apps/bunmsh/help/shift.md +23 -0
  61. package/apps/bunmsh/help/sleep.md +25 -0
  62. package/apps/bunmsh/help/sort.md +29 -0
  63. package/apps/bunmsh/help/source.md +23 -0
  64. package/apps/bunmsh/help/tab.md +30 -0
  65. package/apps/bunmsh/help/tac.md +29 -0
  66. package/apps/bunmsh/help/tail.md +27 -0
  67. package/apps/bunmsh/help/tee.md +25 -0
  68. package/apps/bunmsh/help/test.md +30 -0
  69. package/apps/bunmsh/help/time.md +29 -0
  70. package/apps/bunmsh/help/touch.md +26 -0
  71. package/apps/bunmsh/help/tr.md +26 -0
  72. package/apps/bunmsh/help/true.md +22 -0
  73. package/apps/bunmsh/help/type.md +21 -0
  74. package/apps/bunmsh/help/umask.md +22 -0
  75. package/apps/bunmsh/help/unalias.md +28 -0
  76. package/apps/bunmsh/help/uname.md +31 -0
  77. package/apps/bunmsh/help/unset.md +28 -0
  78. package/apps/bunmsh/help/wc.md +27 -0
  79. package/apps/bunmsh/help/whence.md +26 -0
  80. package/apps/bunmsh/help/which.md +41 -0
  81. package/apps/bunmsh/help/yes.md +23 -0
  82. package/apps/bunmsh/package.json +26 -0
  83. package/apps/bunmsh/serve.js +468 -0
  84. package/apps/bunmsh/single-exe/README.md +402 -0
  85. package/apps/bunmsh/single-exe/assetsHelper.js +371 -0
  86. package/apps/bunmsh/single-exe/assetsLoader.mjs +85 -0
  87. package/apps/bunmsh/single-exe/assetsPacker.js +218 -0
  88. package/apps/bunmsh/single-exe/compiled.js +527 -0
  89. package/apps/bunmsh/single-exe/entry.mjs +17 -0
  90. package/apps/bunmsh/src/cat-fancy.js +117 -0
  91. package/apps/bunmsh/src/cat-operands.js +31 -0
  92. package/apps/bunmsh/src/completion.js +247 -0
  93. package/apps/bunmsh/src/environment.js +61 -0
  94. package/apps/bunmsh/src/fancy-ls.js +186 -0
  95. package/apps/bunmsh/src/find.js +119 -0
  96. package/apps/bunmsh/src/history.js +198 -0
  97. package/apps/bunmsh/src/main.js +776 -0
  98. package/apps/bunmsh/src/mouse.js +103 -0
  99. package/apps/bunmsh/src/shell.js +3557 -0
  100. package/apps/bunmsh/test/catfancy.test.js +102 -0
  101. package/apps/bunmsh/test/reference-source.sh +1 -0
  102. package/apps/bunmsh/test/reference.test.js +677 -0
  103. package/apps/bunmsh/test/serve/README.md +3 -0
  104. package/apps/bunmsh/test/serve/bad.json +1 -0
  105. package/apps/bunmsh/test/serve/data.json +1 -0
  106. package/apps/bunmsh/test/serve/data.json5 +1 -0
  107. package/apps/bunmsh/test/serve/data.jsonc +2 -0
  108. package/apps/bunmsh/test/serve/data.jsonl +2 -0
  109. package/apps/bunmsh/test/serve/data.ndjson +1 -0
  110. package/apps/bunmsh/test/serve/data.toml +3 -0
  111. package/apps/bunmsh/test/serve/data.ts +15 -0
  112. package/apps/bunmsh/test/serve/data.xml +1 -0
  113. package/apps/bunmsh/test/serve/data.yaml +3 -0
  114. package/apps/bunmsh/test/serve/data.yml +1 -0
  115. package/apps/bunmsh/test/serve.test.js +241 -0
  116. package/apps/bunmsh/test/shell.test.js +1778 -0
  117. package/apps/bunmsh/test/t.sh +12 -0
  118. package/apps/cmdlist +6 -1
  119. package/apps/jsgotty/gotty.js +65 -76
  120. package/apps/jsgotty/package.json +6 -1
  121. package/apps/jsgotty/single-exe/README.md +358 -54
  122. package/apps/jsgotty/single-exe/assetsHelper.js +293 -12
  123. package/apps/jsgotty/single-exe/assetsLoader.mjs +1 -1
  124. package/apps/jsgotty/single-exe/assetsPacker.js +218 -0
  125. package/apps/jsgotty/single-exe/compiled.js +379 -19
  126. package/apps/jsgotty/single-exe/entry.mjs +16 -2
  127. package/apps/jsgotty/zmodem-node.js +14 -12
  128. package/apps/jsmdcui/CHANGELOG.md +56 -0
  129. package/apps/jsmdcui/package.json +12 -2
  130. package/apps/jsmdcui/single-exe/README.md +298 -375
  131. package/apps/jsmdcui/single-exe/assetsHelper.js +198 -18
  132. package/apps/jsmdcui/single-exe/assetsLoader.mjs +1 -1
  133. package/apps/jsmdcui/single-exe/assetsPacker.js +218 -0
  134. package/apps/jsmdcui/single-exe/compiled.js +291 -5
  135. package/apps/jsmdcui/single-exe/entry.mjs +7 -0
  136. package/apps/jsmdcui/src/index.js +11 -8
  137. package/apps/jsmdcui/tests/jmigt.js +16 -0
  138. package/bin/bunmsh.bat +3 -0
  139. package/bin/bunmsh.sh +5 -0
  140. package/bin/init.js +48 -5
  141. package/bin/install.js +541 -0
  142. package/package.json +4 -2
@@ -1,473 +1,396 @@
1
- # Bun Single-File Executable Bootstrap
1
+ # Single-file executable bootstrap
2
2
 
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.
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
- 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.
6
+ Assets are declared once, in `package.json`:
8
7
 
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
-
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
- `--build-exe` runs `packAssets.sh` automatically and writes the `mdcui`
28
- executable to the current directory. To pass a Bun compilation target, use:
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
- ```shell
31
- bun ./src/index.js --build-for <target>
32
- ```
17
+ ## The two back ends
33
18
 
34
- Any additional arguments after `--build-exe`, or after the target passed to
35
- `--build-for`, are forwarded to the `bun build` command:
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
- ```shell
38
- bun ./src/index.js --build-exe --sourcemap
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
- Bun expects `--define` values to be JSON-style literals, so strings normally
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
- | `MDCUI_DEFAULT_EDIT=1` | text editor | `./mdcui --tui app.md` |
72
- | `MDCUI_DEFAULT_DEMO=1` | `testapp.md` TUI | `./mdcui --wui --demo` |
73
- | `MDCUI_DEFAULT_DEMO_WUI=1` | `testapp.md` WUI | `./mdcui --tui --demo` |
74
- | `global.MDCUI_MAIN=../中文工具.md` | custom TUI | `./mdcui --wui --demo-中文工具` |
75
- | MAIN plus `MDCUI_DEFAULT_DEMO_WUI=1` | custom WUI | `./mdcui --tui --demo-中文工具` |
76
-
77
- For a custom TUI-default executable:
78
-
79
- ```shell
80
- bun ./src/index.js --build-exe \
81
- --define global.MDCUI_MAIN=../中文工具.md
82
- ```
83
-
84
- For a custom WUI-default executable:
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
- ```shell
87
- bun ./src/index.js --build-exe \
88
- --define global.MDCUI_MAIN=../中文工具.md \
89
- --define MDCUI_DEFAULT_DEMO_WUI=1
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
- Explicit runtime arguments suppress automatic demo/WUI selection, so switching
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
- The custom demo uses embedded TUI front/RPC or the embedded WUI server when its
98
- local Markdown byte length matches the embedded asset. Missing or overwritten
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
- To perform the same steps manually, run these commands from `single-exe/`:
129
+ ```js
130
+ import { buildEarlyExit, IS_COMPILED, REPO_ROOT } from "./single-exe/compiled.js";
103
131
 
104
- ```shell
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
- ## Adapting this folder to another project
110
-
111
- This folder is a reusable starting point, not a drop-in package. The current
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
- `assets.tar` is generated by `packAssets.sh`; it does not need to exist before
144
- the first packing step.
139
+ ### Chaining file/folder assets across dependencies
145
140
 
146
- ### 2. Point `entry.mjs` at the main program
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
- Keep the asset loader as the first import and change the path passed to the
149
- final `await import()` so it points to the other project's real entry module:
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
- ```js
152
- #!/usr/bin/env bun
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
- > **Important:** Keep the final import dynamic. Do not replace it with a static
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
- ### 3. Choose which files to embed
171
-
172
- Edit the resource list in `packAssets.sh`; the script runs `tar` for you. It
173
- changes to the project root, writes `single-exe/assets.tar`, and packs every
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
- Do not retain jsmdcui's `demos`, `runtime`, `src/cui`, or `testapp.md` entries
192
- unless the adopting project actually contains and needs them.
169
+ `assetsPacker.js` also runs standalone; `--help` documents its flags.
193
170
 
194
- ### 4. Read embedded assets with an external fallback
171
+ ## Why the loader hands over a Promise
195
172
 
196
- Import `readAssetText()` or `readAssetBytes()` from `assetsHelper.js`. They
197
- first read the embedded asset and automatically fall back to the same path
198
- under `REPO_ROOT` when the asset is not embedded:
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
- In the source tree, `REPO_ROOT` is the project root. In a compiled executable,
214
- it is the executable's directory, which is also where `--assets-extract`
215
- places the external resource tree. Use `readInternalAssetBytes()` when a
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
- #### Reuse images from a compiled HTML bundle
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
- Bun exposes an imported HTML entry as a `homepage` object in a compiled
223
- executable. Its `index` points to the compiled HTML and its `files` array lists
224
- the content-hashed assets under Bun's compiled virtual filesystem: paths
225
- normally begin with `/$bunfs/root/` on POSIX systems and `B:/~BUN/` on
226
- Windows. The helpers can turn image references preserved in that HTML into a
227
- lazy path map without copying the image bytes into `internalAssets`. Always use
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
- Before compiling, preserve each original image reference in a custom
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
- <!-- Compiled HTML -->
240
- <img src="/photo-abcd1234.jpg" data-original-image="./images/photo.jpg">
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
- Export the imported HTML bundle from a module that both the web server and
244
- terminal runtime can load:
208
+ `entry.mjs` is immune because it writes two of its own:
245
209
 
246
210
  ```js
247
- import homepage from "./index.html";
248
-
249
- export { homepage };
211
+ await globalThis.assetsLoaderPromise;
212
+ await import("../src/index.js");
250
213
  ```
251
214
 
252
- Build and cache the map only in a compiled executable. Guard the import first:
253
- a source-tree launch does not necessarily have the generated server or HTML
254
- module.
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
- ```js
257
- import {
258
- buildHtmlBundleImageMap,
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
- let imageMapPromise = null;
264
- const imageSourceAttribute = "data-original-image";
226
+ ## Environment variables
265
227
 
266
- function getBundledImageMap() {
267
- if (!IS_COMPILED || !global.MY_EMBEDDED_APP) return null;
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
- imageMapPromise ??= import(global.MY_EMBEDDED_APP + "-server.js")
270
- .then(module => buildHtmlBundleImageMap(
271
- module.homepage,
272
- imageSourceAttribute,
273
- ));
234
+ ## Runtime flags
274
235
 
275
- return imageMapPromise;
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
- const images = await getBundledImageMap();
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
- `global.MY_EMBEDDED_APP` is only an example build-time module selector. Name it
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
- `buildHtmlBundleImageMap(homepage, sourceAttribute)` reads the compiled HTML
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
- Adopting projects can choose any valid HTML attribute name and pass it
302
- explicitly:
248
+ Anything after `--build-exe`, or after the target given to `--build-for`, is
249
+ passed straight through to `bun build`:
303
250
 
304
- ```html
305
- <img src="./photo.jpg" data-original-image="./photo.jpg">
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
- ```js
309
- const images = await buildHtmlBundleImageMap(
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
- The helper decodes HTML attribute entities and percent encoding so the HTML
316
- reference can match the equivalent original caller reference. Image bytes
317
- remain lazy and are read only when the caller uses the returned path.
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
- For lower-level lookup, `findHtmlBundleAsset(homepage, publicPath, options)`
320
- matches a rewritten public path such as `/photo-abcd1234.jpg` to its
321
- `homepage.files` entry. `findHtmlBundleImageAsset()` restricts the lookup to
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
- ### 5. Add the optional build commands to the regular CLI
267
+ ### `--define` with a string value
326
268
 
327
- Before normal argument parsing, call `buildEarlyExit` from the regular main
328
- module:
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
- const compiledHelper = await import("../single-exe/compiled.js").catch(() => null);
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
- ```shell
339
- bun ./src/index.js --build-exe
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
- Both commands run `packAssets.sh` before `bun build`. The second form passes
344
- `<target>` through as Bun's `--target=<target>` value.
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
- ```shell
349
- bun ./packAssets.sh
350
- bun build --format=esm --compile --minify --bytecode ./entry.mjs --outfile=my-bin
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
- #### Passing --define to bun build
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
- `buildEarlyExit()` forwards build arguments to Bun, whose define parser expects
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
- ```js
360
- import {
361
- buildEarlyExit,
362
- stringifyNonPrimitiveDefineValues,
363
- } from "../single-exe/compiled.js";
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
- stringifyNonPrimitiveDefineValues(process.argv, "MY_STRING_DEFINE");
366
- await buildEarlyExit(process.argv, "my-bin");
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
- ```shell
370
- # Wrong: Bun parses the define before the helper runs.
371
- bun --define MY_STRING_DEFINE=../app.md ./src/index.js --build-exe
303
+ ```html
304
+ <!-- source -->
305
+ <img src="./images/photo.jpg" data-original-image="./images/photo.jpg">
372
306
 
373
- # Correct
374
- bun ./src/index.js --build-exe --define MY_STRING_DEFINE=../app.md
307
+ <!-- compiled -->
308
+ <img src="/photo-abcd1234.jpg" data-original-image="./images/photo.jpg">
375
309
  ```
376
310
 
377
- Before normalization:
311
+ Then build the map once, in a compiled binary only:
378
312
 
379
313
  ```js
380
- ["--define", "MY_STRING_DEFINE=../app.md"]
381
- ```
314
+ import { buildHtmlBundleImageMap, canonicalHtmlBundleImageHref } from "../single-exe/assetsHelper.js";
315
+ import { IS_COMPILED } from "../single-exe/compiled.js";
382
316
 
383
- After normalization:
317
+ const images = IS_COMPILED
318
+ ? await buildHtmlBundleImageMap(homepage, "data-original-image")
319
+ : null;
384
320
 
385
- ```js
386
- ["--define", 'MY_STRING_DEFINE="../app.md"']
321
+ const path = images?.get(canonicalHtmlBundleImageHref(originalHref));
322
+ const bytes = path ? await Bun.file(path).bytes() : null;
387
323
  ```
388
324
 
389
- The inline form works too:
390
- `--define=MY_STRING_DEFINE=../app.md` becomes
391
- `--define=MY_STRING_DEFINE="../app.md"`.
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
- ### 6. Verify both execution paths
335
+ Never hard-code `/$bunfs/root/` or `B:/~BUN/`; always use the path that
336
+ `homepage.files` reports.
395
337
 
396
- Verify the compiled asset archive and then run the program normally:
338
+ ## jsmdcui specifics
397
339
 
398
- ```shell
399
- ./my-bin --assets-list
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
- Also run the uncompiled main module with Node.js 20.11 or newer to verify that
404
- all required files have a working external fallback:
343
+ Embedding a Markdown app has its own flags, and they are what you normally
344
+ want:
405
345
 
406
346
  ```shell
407
- node ./src/index.js
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
- Use `./my-bin --assets-extract` to write the packed tree beside the executable.
411
- After extraction, `./my-bin --assets-external` skips the embedded store and is
412
- useful for testing the external-resource path.
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
- ```js
422
- import assets from "./assets.tar" with { type: "file" };
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
- If the regular main module imported `assetsLoader.mjs`, that Bun-specific
426
- `type: "file"` dependency would enter the main module graph. Node would then be
427
- unable to load the program before it could select an external-file fallback.
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
- Instead, only Bun's `entry.mjs` imports the Bun-specific loader, awaits its
430
- Promise, and then dynamically imports the regular main module. The main module
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
- ## Entry Flow
438
-
439
- - `entry.mjs` imports `assetsLoader.mjs` first
440
- - `assetsLoader.mjs` loads `assets.tar` with `Bun.Archive` and mounts it as `globalThis.internalAssets`
441
- - `assetsLoaderPromise` is exposed on `globalThis`
442
- - `entry.mjs` awaits `assetsLoaderPromise`
443
- - `entry.mjs` dynamically imports the main program after the assets are ready
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
- This loader is not zero-copy: it materializes every bundled file in memory and
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
- ## CLI Flags
379
+ ```shell
380
+ # custom TUI-default executable
381
+ bun ./src/index.js --build-md-exe ../中文工具.md
461
382
 
462
- - `--assets-list`
463
- - Lists all entries inside bundled `assets.tar`
464
- - Exits early before the main program starts
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
- - `--assets-extract`
467
- - Extracts bundled assets to the same directory as the executable
468
- - Exits early before the main program starts
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
- - `--assets-external`
471
- - Skips loading bundled assets into `globalThis.internalAssets`
472
- - Forces the main program and runtime helpers to use the external file tree
473
- - Keeps the bootstrap alive while leaving `internalAssets` falsy
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.