jsmdcui 0.11.2 → 0.12.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 +47 -0
- package/README.md +36 -1
- package/package.json +1 -1
- package/runmd.mjs +14 -2
- package/runtime/help/help.md +2 -1
- package/single-exe/README.md +114 -2
- package/single-exe/assetsHelper.js +89 -0
- package/src/cui/kitty-images.mjs +10 -3
- package/src/cui/kitty-mode.mjs +6 -0
- package/src/cui/server.mjs +1 -0
- package/src/index.js +16 -3
- package/src/lua/engine.js +1 -1
- package/src/plugins/js-bridge.js +1 -1
- package/src/plugins/manager.js +1 -1
- package/src/runtime/registry.js +1 -1
- package/tests/assets-helper.test.js +116 -0
- package/tests/kitty-images.test.js +22 -0
- package/tests/kitty-mode.test.js +14 -0
- package/tests/wui-responsive-images.test.js +2 -0
- package/single-exe/README.md-rpc.js +0 -623
- package/single-exe/README.md-server.js +0 -127
- package/single-exe/README.md.back.js +0 -2
- package/single-exe/README.md.front.js +0 -27
- package/single-exe/README.md.html +0 -325
- package/src/runtime/assets.js +0 -90
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
|
|
3
|
-
import { evalBack } from "./README.md-rpc.js";
|
|
4
|
-
|
|
5
|
-
const csl = console.log
|
|
6
|
-
const mda = Bun.markdown.ansi
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
import homepage from "./README.md.html"
|
|
10
|
-
import * as backmod from "./README.md.back.js";
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
/*
|
|
14
|
-
// Wait until Bun fixes dynamic html import bundle
|
|
15
|
-
|
|
16
|
-
process.env.PUBLIC_MDPATH ||= "README.md"
|
|
17
|
-
|
|
18
|
-
const { default: homepage } =
|
|
19
|
-
await import(
|
|
20
|
-
"./" +
|
|
21
|
-
process.env.PUBLIC_MDPATH +
|
|
22
|
-
".html"
|
|
23
|
-
) ;
|
|
24
|
-
|
|
25
|
-
const backmod =
|
|
26
|
-
await import(
|
|
27
|
-
'./' +
|
|
28
|
-
process.env.PUBLIC_MDPATH +
|
|
29
|
-
".back.js"
|
|
30
|
-
) ;
|
|
31
|
-
|
|
32
|
-
*/
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
function json(data, init = {}) {
|
|
36
|
-
return Response.json(data, {
|
|
37
|
-
headers: {
|
|
38
|
-
"access-control-allow-origin": "*",
|
|
39
|
-
...init.headers,
|
|
40
|
-
},
|
|
41
|
-
...init,
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
export function main()
|
|
47
|
-
{
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const basePath = "/" + crypto.randomUUID()
|
|
52
|
-
const pathname = basePath + "/"
|
|
53
|
-
const serverOptions = {
|
|
54
|
-
port: Number(process.env.PORT || 3000),
|
|
55
|
-
development: {
|
|
56
|
-
hmr: true,
|
|
57
|
-
console: true,
|
|
58
|
-
},
|
|
59
|
-
routes: {
|
|
60
|
-
[basePath]: (req) => Response.redirect(new URL(pathname, req.url), 308),
|
|
61
|
-
[pathname]: homepage,
|
|
62
|
-
|
|
63
|
-
[pathname + "rpc"]: async (req) => {
|
|
64
|
-
csl("\n"+mda("## "+req.method+" "+req.url))
|
|
65
|
-
|
|
66
|
-
if (req.method === "OPTIONS") {
|
|
67
|
-
return new Response(null, {
|
|
68
|
-
headers: {
|
|
69
|
-
"access-control-allow-origin": "*",
|
|
70
|
-
"access-control-allow-methods": "POST,OPTIONS",
|
|
71
|
-
"access-control-allow-headers": "content-type",
|
|
72
|
-
},
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (req.method !== "POST") {
|
|
77
|
-
csl('illegal method')
|
|
78
|
-
return new Response("Method Not Allowed", { status: 405 });
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
try {
|
|
82
|
-
const packet = await req.json();
|
|
83
|
-
|
|
84
|
-
csl('req json:',packet)
|
|
85
|
-
|
|
86
|
-
const result = await evalBack(backmod, packet);
|
|
87
|
-
|
|
88
|
-
return json(result ?? null);
|
|
89
|
-
} catch (error) {
|
|
90
|
-
return json(
|
|
91
|
-
{
|
|
92
|
-
ok: false,
|
|
93
|
-
error: error?.stack || String(error),
|
|
94
|
-
},
|
|
95
|
-
{ status: 400 },
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
},
|
|
99
|
-
},
|
|
100
|
-
|
|
101
|
-
fetch() {
|
|
102
|
-
return new Response("Not Found", { status: 404 });
|
|
103
|
-
},
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
let server;
|
|
107
|
-
try {
|
|
108
|
-
server = Bun.serve(serverOptions);
|
|
109
|
-
} catch (error) {
|
|
110
|
-
const addressInUse = error?.code === "EADDRINUSE"
|
|
111
|
-
|| error?.cause?.code === "EADDRINUSE"
|
|
112
|
-
|| /EADDRINUSE|address already in use/i.test(String(error?.message || error));
|
|
113
|
-
if (serverOptions.port !== 3000 || !addressInUse) throw error;
|
|
114
|
-
server = Bun.serve({ ...serverOptions, port: 0 });
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
console.error(mda("- Bun RPC server listening on"));
|
|
118
|
-
console.log(`http://localhost:${server.port}${pathname}`);
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
return server
|
|
123
|
-
|
|
124
|
-
} // main
|
|
125
|
-
|
|
126
|
-
if(import.meta.main)
|
|
127
|
-
main()
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
|
|
3
|
-
import { rpc as wuiRpcClient } from "./README.md-rpc.js";
|
|
4
|
-
let rpc = null
|
|
5
|
-
if(globalThis.process)
|
|
6
|
-
{
|
|
7
|
-
rpc = await import(
|
|
8
|
-
"./" +
|
|
9
|
-
(global.MDCUI_MAIN_BASE||"README.md") +
|
|
10
|
-
".back.js"
|
|
11
|
-
) ;
|
|
12
|
-
}
|
|
13
|
-
else
|
|
14
|
-
rpc = wuiRpcClient ;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if (typeof window !== "undefined")
|
|
19
|
-
{
|
|
20
|
-
// Browser
|
|
21
|
-
setTimeout( () => {
|
|
22
|
-
import("./README.md.front.js").then(mod=>{
|
|
23
|
-
Object.assign(window,mod);
|
|
24
|
-
})
|
|
25
|
-
}, 0 ) ;
|
|
26
|
-
}
|
|
27
|
-
|
|
@@ -1,325 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="zh-TW">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
-
<title>README.md</title>
|
|
7
|
-
<style>
|
|
8
|
-
img {
|
|
9
|
-
max-width: 100%;
|
|
10
|
-
height: auto;
|
|
11
|
-
}
|
|
12
|
-
</style>
|
|
13
|
-
</head>
|
|
14
|
-
<body>
|
|
15
|
-
<section>
|
|
16
|
-
<h1 id="bun-single-file-executable-bootstrap">Bun Single-File Executable Bootstrap</h1>
|
|
17
|
-
<p>Build a Bun application and its runtime assets into one executable while
|
|
18
|
-
keeping the regular main module free of Bun-specific asset imports.</p>
|
|
19
|
-
<p>You can copy and adapt this build setup for your own project by changing its
|
|
20
|
-
main-module path and asset list as described below.</p>
|
|
21
|
-
<ul>
|
|
22
|
-
<li>Bundle an asset tree alongside the application code in one executable.</li>
|
|
23
|
-
<li>Keep the regular main module available to the Node.js ESM execution path.</li>
|
|
24
|
-
<li>Read embedded resources first with an optional external-file fallback.</li>
|
|
25
|
-
<li>List, extract, or bypass embedded assets with built-in CLI flags.</li>
|
|
26
|
-
<li>Build for the current platform or pass a Bun cross-compilation target.</li>
|
|
27
|
-
</ul>
|
|
28
|
-
<blockquote>
|
|
29
|
-
<p>This workflow is optional; the standard methods in the root README remain
|
|
30
|
-
available. Bun's Android build is currently not supported.</p>
|
|
31
|
-
</blockquote>
|
|
32
|
-
<section>
|
|
33
|
-
<h2 id="quick-start-for-jsmdcui">Quick start for jsmdcui</h2>
|
|
34
|
-
<p>From the repository root, build for the current platform:</p>
|
|
35
|
-
<pre><code class="language-shell">bun ./src/index.js --build-exe
|
|
36
|
-
./mdcui --version
|
|
37
|
-
</code></pre>
|
|
38
|
-
<p><code>--build-exe</code> runs <code>packAssets.sh</code> automatically and writes the <code>mdcui</code>
|
|
39
|
-
executable to the current directory. To pass a Bun compilation target, use:</p>
|
|
40
|
-
<pre><code class="language-shell">bun ./src/index.js --build-for <target>
|
|
41
|
-
</code></pre>
|
|
42
|
-
<p>Any additional arguments after <code>--build-exe</code>, or after the target passed to
|
|
43
|
-
<code>--build-for</code>, are forwarded to the <code>bun build</code> command:</p>
|
|
44
|
-
<pre><code class="language-shell">bun ./src/index.js --build-exe --sourcemap
|
|
45
|
-
bun ./src/index.js --build-for <target> --sourcemap
|
|
46
|
-
</code></pre>
|
|
47
|
-
<p>Bun expects <code>--define</code> values to be JSON-style literals, so strings normally
|
|
48
|
-
need quotes. <code>stringifyNonPrimitiveDefineValues()</code> converts a bare value with
|
|
49
|
-
<code>JSON.stringify()</code> before it reaches Bun. jsmdcui uses it for
|
|
50
|
-
<code>global.MDCUI_MAIN</code>.</p>
|
|
51
|
-
<p>The switch defines below are presence-based: <code>=0</code> and <code>=false</code> still enable
|
|
52
|
-
them. Omit a switch define to disable it; examples consistently use <code>=1</code>.</p>
|
|
53
|
-
<ul>
|
|
54
|
-
<li><code>MDCUI_DEFAULT_EDIT=1</code>: editable-text default.</li>
|
|
55
|
-
<li><code>MDCUI_DEFAULT_DEMO=1</code>: no-argument <code>testapp.md</code> TUI.</li>
|
|
56
|
-
<li><code>MDCUI_DEFAULT_DEMO_WUI=1</code>: no-argument <code>testapp.md</code> WUI.</li>
|
|
57
|
-
<li><code>MDCUI_OVERWRITE_DEMO=1</code>: overwrite modifier; it does not select a demo.</li>
|
|
58
|
-
<li><code>global.MDCUI_MAIN=<path>.md</code>: embed a custom Markdown app and its generated
|
|
59
|
-
front, RPC, back, HTML, and server modules.</li>
|
|
60
|
-
<li><code>global.MDCUI_MAIN_BASE</code>: generated internally from <code>MDCUI_MAIN</code>; do not pass
|
|
61
|
-
it manually.</li>
|
|
62
|
-
</ul>
|
|
63
|
-
<p>Put defines after <code>--build-exe</code> (or after the <code>--build-for</code> target), and choose
|
|
64
|
-
at most one <code>MDCUI_DEFAULT_*</code> mode.</p>
|
|
65
|
-
<table>
|
|
66
|
-
<thead>
|
|
67
|
-
<tr><th>Build defines</th><th>No-argument launch</th><th>Switch UI</th></tr>
|
|
68
|
-
</thead>
|
|
69
|
-
<tbody>
|
|
70
|
-
<tr><td><code>MDCUI_DEFAULT_EDIT=1</code></td><td>text editor</td><td><code>./mdcui --tui app.md</code></td></tr>
|
|
71
|
-
<tr><td><code>MDCUI_DEFAULT_DEMO=1</code></td><td><code>testapp.md</code> TUI</td><td><code>./mdcui --wui --demo</code></td></tr>
|
|
72
|
-
<tr><td><code>MDCUI_DEFAULT_DEMO_WUI=1</code></td><td><code>testapp.md</code> WUI</td><td><code>./mdcui --tui --demo</code></td></tr>
|
|
73
|
-
<tr><td><code>global.MDCUI_MAIN=../中文工具.md</code></td><td>custom TUI</td><td><code>./mdcui --wui --demo-中文工具</code></td></tr>
|
|
74
|
-
<tr><td>MAIN plus <code>MDCUI_DEFAULT_DEMO_WUI=1</code></td><td>custom WUI</td><td><code>./mdcui --tui --demo-中文工具</code></td></tr>
|
|
75
|
-
</tbody>
|
|
76
|
-
</table>
|
|
77
|
-
<p>For a custom TUI-default executable:</p>
|
|
78
|
-
<pre><code class="language-shell">bun ./src/index.js --build-exe \
|
|
79
|
-
--define global.MDCUI_MAIN=../中文工具.md
|
|
80
|
-
</code></pre>
|
|
81
|
-
<p>For a custom WUI-default executable:</p>
|
|
82
|
-
<pre><code class="language-shell">bun ./src/index.js --build-exe \
|
|
83
|
-
--define global.MDCUI_MAIN=../中文工具.md \
|
|
84
|
-
--define MDCUI_DEFAULT_DEMO_WUI=1
|
|
85
|
-
</code></pre>
|
|
86
|
-
<p>Explicit runtime arguments suppress automatic demo/WUI selection, so switching
|
|
87
|
-
UI must repeat <code>--demo</code> or <code>--demo-中文工具</code>. The main basename must end in
|
|
88
|
-
lowercase <code>.md</code>; its demo name allows Unicode letters/numbers (including
|
|
89
|
-
Chinese), dots, underscores, and hyphens, but no whitespace.</p>
|
|
90
|
-
<p>The custom demo uses embedded TUI front/RPC or the embedded WUI server when its
|
|
91
|
-
local Markdown byte length matches the embedded asset. Missing or overwritten
|
|
92
|
-
demos use embedded modules after the asset is written. A differing byte length
|
|
93
|
-
warns and selects filesystem companion modules.</p>
|
|
94
|
-
<p>To perform the same steps manually, run these commands from <code>single-exe/</code>:</p>
|
|
95
|
-
<pre><code class="language-shell">bun ./packAssets.sh
|
|
96
|
-
bun build --format=esm --compile --minify --bytecode ./entry.mjs --outfile=mdcui
|
|
97
|
-
</code></pre>
|
|
98
|
-
</section>
|
|
99
|
-
<section>
|
|
100
|
-
<h2 id="adapting-this-folder-to-another-project">Adapting this folder to another project</h2>
|
|
101
|
-
<p>This folder is a reusable starting point, not a drop-in package. The current
|
|
102
|
-
<code>entry.mjs</code> and <code>packAssets.sh</code> contain jsmdcui paths, so another project must
|
|
103
|
-
complete the following integration steps.</p>
|
|
104
|
-
<p>The adopting project needs Bun for packing and compiling, a <code>tar</code> command for
|
|
105
|
-
<code>packAssets.sh</code>, and an ES-module setup. Keep <code>"type": "module"</code> in
|
|
106
|
-
<code>package.json</code>, or rename the copied <code>.js</code> modules to <code>.mjs</code> and update their
|
|
107
|
-
imports. The supported uncompiled Node path requires Node.js 20.11 or newer
|
|
108
|
-
because <code>compiled.js</code> uses <code>import.meta.dirname</code>.</p>
|
|
109
|
-
<p><code>assetsHelper.js</code> and <code>compiled.js</code> are Node-compatible under those conditions.
|
|
110
|
-
<code>assetsLoader.mjs</code> and <code>entry.mjs</code> are Bun-only and must not be imported by the
|
|
111
|
-
regular Node entry path.</p>
|
|
112
|
-
<section>
|
|
113
|
-
<h3 id="1-copy-the-bootstrap-directory">1. Copy the bootstrap directory</h3>
|
|
114
|
-
<p>Copy the entire <code>single-exe/</code> directory to the root of the other project and
|
|
115
|
-
keep its name and relative position unless you also update the paths in
|
|
116
|
-
<code>compiled.js</code> and <code>packAssets.sh</code>. A typical layout is:</p>
|
|
117
|
-
<textarea data-mdcui-tag="text" data-mdcui-language="text" class="language-text" rows="9" cols="24" wrap="soft" style="box-sizing:border-box;max-width:100%;width:100%;resize:vertical;overflow-y:hidden">my-project/
|
|
118
|
-
├── single-exe/
|
|
119
|
-
│ ├── assetsHelper.js
|
|
120
|
-
│ ├── assetsLoader.mjs
|
|
121
|
-
│ ├── compiled.js
|
|
122
|
-
│ ├── entry.mjs
|
|
123
|
-
│ └── packAssets.sh
|
|
124
|
-
└── src/
|
|
125
|
-
└── index.js</textarea>
|
|
126
|
-
<p><code>assets.tar</code> is generated by <code>packAssets.sh</code>; it does not need to exist before
|
|
127
|
-
the first packing step.</p>
|
|
128
|
-
</section>
|
|
129
|
-
<section>
|
|
130
|
-
<h3 id="2-point-entrymjs-at-the-main-program">2. Point <code>entry.mjs</code> at the main program</h3>
|
|
131
|
-
<p>Keep the asset loader as the first import and change the path passed to the
|
|
132
|
-
final <code>await import()</code> so it points to the other project's real entry module:</p>
|
|
133
|
-
<pre><code class="language-js">#!/usr/bin/env bun
|
|
134
|
-
|
|
135
|
-
import "./assetsLoader.mjs";
|
|
136
|
-
await globalThis.assetsLoaderPromise;
|
|
137
|
-
await import("../src/index.js");
|
|
138
|
-
</code></pre>
|
|
139
|
-
<blockquote>
|
|
140
|
-
<p><strong>Important:</strong> Keep the final import dynamic. Do not replace it with a static
|
|
141
|
-
<code>import "../src/index.js"</code>, because static dependencies are evaluated before
|
|
142
|
-
the asset Promise is awaited.</p>
|
|
143
|
-
</blockquote>
|
|
144
|
-
<p>Only the Bun single-file executable build should use this bootstrap entry.
|
|
145
|
-
Continue to run the regular main module directly when using Node:</p>
|
|
146
|
-
<pre><code class="language-shell">node ./src/index.js
|
|
147
|
-
</code></pre>
|
|
148
|
-
</section>
|
|
149
|
-
<section>
|
|
150
|
-
<h3 id="3-choose-which-files-to-embed">3. Choose which files to embed</h3>
|
|
151
|
-
<p>Edit the resource list in <code>packAssets.sh</code>; the script runs <code>tar</code> for you. It
|
|
152
|
-
changes to the project root, writes <code>single-exe/assets.tar</code>, and packs every
|
|
153
|
-
listed runtime resource required by the compiled program. Paths stored in the
|
|
154
|
-
archive become the lookup keys used by <code>assetsHelper.js</code>; use keys such as
|
|
155
|
-
<code>public/app.css</code>, without a leading <code>./</code>. The normal <code>--build-exe</code> and
|
|
156
|
-
<code>--build-for</code> flows invoke this script automatically, so you do not need to run
|
|
157
|
-
<code>tar</code> yourself.</p>
|
|
158
|
-
<p>For example:</p>
|
|
159
|
-
<pre><code class="language-sh">#!/bin/sh
|
|
160
|
-
|
|
161
|
-
script_dir=$(dirname "$0")
|
|
162
|
-
cd "$script_dir/.." || exit 1
|
|
163
|
-
|
|
164
|
-
tar -cvf single-exe/assets.tar public templates README.md
|
|
165
|
-
</code></pre>
|
|
166
|
-
<p>Do not retain jsmdcui's <code>demos</code>, <code>runtime</code>, <code>src/cui</code>, or <code>testapp.md</code> entries
|
|
167
|
-
unless the adopting project actually contains and needs them.</p>
|
|
168
|
-
</section>
|
|
169
|
-
<section>
|
|
170
|
-
<h3 id="4-read-embedded-assets-with-an-external-fallback">4. Read embedded assets with an external fallback</h3>
|
|
171
|
-
<p>Import the Node-compatible helpers from <code>assetsHelper.js</code>. They return <code>null</code>
|
|
172
|
-
when the embedded store is unavailable or a path is not present:</p>
|
|
173
|
-
<pre><code class="language-js">import { readFile } from "node:fs/promises";
|
|
174
|
-
import { resolve } from "node:path";
|
|
175
|
-
import { readInternalAssetText } from "../single-exe/assetsHelper.js";
|
|
176
|
-
import { REPO_ROOT } from "../single-exe/compiled.js";
|
|
177
|
-
|
|
178
|
-
async function readResourceText(path) {
|
|
179
|
-
return readInternalAssetText(path) ??
|
|
180
|
-
await readFile(resolve(REPO_ROOT, path), "utf8");
|
|
181
|
-
}
|
|
182
|
-
</code></pre>
|
|
183
|
-
<p>In the source tree, <code>REPO_ROOT</code> is the project root. In a compiled executable,
|
|
184
|
-
it is the executable's directory, which is also where <code>--assets-extract</code>
|
|
185
|
-
places the external resource tree. Apply the same embedded-first fallback to
|
|
186
|
-
every file that must work in both modes. Use <code>readInternalAssetBytes()</code> when
|
|
187
|
-
a resource should be returned as bytes instead of decoded text.
|
|
188
|
-
Use <code>listInternalAssetPaths()</code> to list embedded asset paths and
|
|
189
|
-
<code>listInternalAssetDirs()</code> to list embedded directories.</p>
|
|
190
|
-
</section>
|
|
191
|
-
<section>
|
|
192
|
-
<h3 id="5-add-the-optional-build-commands-to-the-regular-cli">5. Add the optional build commands to the regular CLI</h3>
|
|
193
|
-
<p>Before normal argument parsing, call <code>buildEarlyExit</code> from the regular main
|
|
194
|
-
module:</p>
|
|
195
|
-
<pre><code class="language-js">const compiledHelper = await import("../single-exe/compiled.js").catch(() => null);
|
|
196
|
-
await compiledHelper?.buildEarlyExit?.(process.argv, "my-bin");
|
|
197
|
-
</code></pre>
|
|
198
|
-
<p>The second argument is the output filename; it defaults to <code>single.exe</code> when
|
|
199
|
-
omitted. This enables:</p>
|
|
200
|
-
<pre><code class="language-shell">bun ./src/index.js --build-exe
|
|
201
|
-
bun ./src/index.js --build-for <target>
|
|
202
|
-
</code></pre>
|
|
203
|
-
<p>Both commands run <code>packAssets.sh</code> before <code>bun build</code>. The second form passes
|
|
204
|
-
<code><target></code> through as Bun's <code>--target=<target></code> value.</p>
|
|
205
|
-
<p><code>buildEarlyExit()</code> forwards build arguments to Bun, whose define parser expects
|
|
206
|
-
strings to be quoted literals. To accept a bare value such as a path, call the
|
|
207
|
-
helper before <code>buildEarlyExit()</code>:</p>
|
|
208
|
-
<pre><code class="language-js">const compiledHelper = await import("../single-exe/compiled.js").catch(() => null);
|
|
209
|
-
compiledHelper?.stringifyNonPrimitiveDefineValues?.(
|
|
210
|
-
process.argv,
|
|
211
|
-
"MY_STRING_DEFINE",
|
|
212
|
-
);
|
|
213
|
-
await compiledHelper?.buildEarlyExit?.(process.argv, "my-bin");
|
|
214
|
-
</code></pre>
|
|
215
|
-
<pre><code class="language-shell"># Wrong: Bun parses the define before the helper runs.
|
|
216
|
-
bun --define MY_STRING_DEFINE=../app.md ./src/index.js --build-exe
|
|
217
|
-
|
|
218
|
-
# Correct
|
|
219
|
-
bun ./src/index.js --build-exe --define MY_STRING_DEFINE=../app.md
|
|
220
|
-
</code></pre>
|
|
221
|
-
<p>Before normalization:</p>
|
|
222
|
-
<pre><code class="language-js">["--define", "MY_STRING_DEFINE=../app.md"]
|
|
223
|
-
</code></pre>
|
|
224
|
-
<p>After normalization:</p>
|
|
225
|
-
<pre><code class="language-js">["--define", 'MY_STRING_DEFINE="../app.md"']
|
|
226
|
-
</code></pre>
|
|
227
|
-
<p>The inline form works too:
|
|
228
|
-
<code>--define=MY_STRING_DEFINE=../app.md</code> becomes
|
|
229
|
-
<code>--define=MY_STRING_DEFINE="../app.md"</code>.</p>
|
|
230
|
-
<p>Alternatively, build manually from inside <code>single-exe/</code>:</p>
|
|
231
|
-
<pre><code class="language-shell">bun ./packAssets.sh
|
|
232
|
-
bun build --format=esm --compile --minify --bytecode ./entry.mjs --outfile=my-bin
|
|
233
|
-
</code></pre>
|
|
234
|
-
</section>
|
|
235
|
-
<section>
|
|
236
|
-
<h3 id="6-verify-both-execution-paths">6. Verify both execution paths</h3>
|
|
237
|
-
<p>Verify the compiled asset archive and then run the program normally:</p>
|
|
238
|
-
<pre><code class="language-shell">./my-bin --assets-list
|
|
239
|
-
./my-bin
|
|
240
|
-
</code></pre>
|
|
241
|
-
<p>Also run the uncompiled main module with Node.js 20.11 or newer to verify that
|
|
242
|
-
all required files have a working external fallback:</p>
|
|
243
|
-
<pre><code class="language-shell">node ./src/index.js
|
|
244
|
-
</code></pre>
|
|
245
|
-
<p>Use <code>./my-bin --assets-extract</code> to write the packed tree beside the executable.
|
|
246
|
-
After extraction, <code>./my-bin --assets-external</code> skips the embedded store and is
|
|
247
|
-
useful for testing the external-resource path.</p>
|
|
248
|
-
</section>
|
|
249
|
-
<section>
|
|
250
|
-
<h3 id="why-assetsloaderpromise-is-used">Why <code>assetsLoaderPromise</code> is used</h3>
|
|
251
|
-
<p>A more tightly coupled ESM design could export the assets with top-level
|
|
252
|
-
<code>await</code>, then make the main module import <code>assetsLoader.mjs</code> directly. This
|
|
253
|
-
project deliberately does not do that because <code>assetsLoader.mjs</code> contains
|
|
254
|
-
Bun's compiled-file import:</p>
|
|
255
|
-
<pre><code class="language-js">import assets from "./assets.tar" with { type: "file" };
|
|
256
|
-
</code></pre>
|
|
257
|
-
<p>If the regular main module imported <code>assetsLoader.mjs</code>, that Bun-specific
|
|
258
|
-
<code>type: "file"</code> dependency would enter the main module graph. Node would then be
|
|
259
|
-
unable to load the program before it could select an external-file fallback.</p>
|
|
260
|
-
<p>Instead, only Bun's <code>entry.mjs</code> imports the Bun-specific loader, awaits its
|
|
261
|
-
Promise, and then dynamically imports the regular main module. The main module
|
|
262
|
-
does not import the loader, inspect the Promise, or contain Bun bootstrap code.
|
|
263
|
-
Under the supported Node.js 20.11+ ESM path, Node runs that clean main module
|
|
264
|
-
directly with external files. The Promise is therefore an intentional
|
|
265
|
-
Node-compatibility boundary inside the Bun-only bootstrap, not a failure to use
|
|
266
|
-
a more modern dependency or top-level-<code>await</code> design.</p>
|
|
267
|
-
</section>
|
|
268
|
-
</section>
|
|
269
|
-
<section>
|
|
270
|
-
<h2 id="entry-flow">Entry Flow</h2>
|
|
271
|
-
<ul>
|
|
272
|
-
<li><code>entry.mjs</code> imports <code>assetsLoader.mjs</code> first</li>
|
|
273
|
-
<li><code>assetsLoader.mjs</code> loads <code>assets.tar</code> with <code>Bun.Archive</code> and mounts it as <code>globalThis.internalAssets</code></li>
|
|
274
|
-
<li><code>assetsLoaderPromise</code> is exposed on <code>globalThis</code></li>
|
|
275
|
-
<li><code>entry.mjs</code> awaits <code>assetsLoaderPromise</code></li>
|
|
276
|
-
<li><code>entry.mjs</code> dynamically imports the main program after the assets are ready</li>
|
|
277
|
-
</ul>
|
|
278
|
-
<p>That keeps the main program bootable even if asset loading reports errors.</p>
|
|
279
|
-
</section>
|
|
280
|
-
<section>
|
|
281
|
-
<h2 id="assets-loading">Assets Loading</h2>
|
|
282
|
-
<ul>
|
|
283
|
-
<li>Bundled assets are loaded sequentially with <code>await file.bytes()</code></li>
|
|
284
|
-
<li>Load failures are collected and printed to <code>stderr</code></li>
|
|
285
|
-
<li>Asset loading never rejects the bootstrap promise</li>
|
|
286
|
-
<li>When loading succeeds, the archive is available through <code>globalThis.internalAssets</code></li>
|
|
287
|
-
</ul>
|
|
288
|
-
<p>This loader is not zero-copy: it materializes every bundled file in memory and
|
|
289
|
-
keeps the resulting bytes in <code>globalThis.internalAssets</code>. Large asset archives
|
|
290
|
-
can therefore increase startup and ongoing RAM usage. For workloads where that
|
|
291
|
-
matters, I also wrote an experimental Linux-only zero-copy hack:
|
|
292
|
-
<a href="https://github.com/jjtseng93/bun-assets-zerocopy">bun-assets-zerocopy</a>.</p>
|
|
293
|
-
</section>
|
|
294
|
-
<section>
|
|
295
|
-
<h2 id="cli-flags">CLI Flags</h2>
|
|
296
|
-
<ul>
|
|
297
|
-
<li>
|
|
298
|
-
<p><code>--assets-list</code></p>
|
|
299
|
-
<ul>
|
|
300
|
-
<li>Lists all entries inside bundled <code>assets.tar</code></li>
|
|
301
|
-
<li>Exits early before the main program starts</li>
|
|
302
|
-
</ul>
|
|
303
|
-
</li>
|
|
304
|
-
<li>
|
|
305
|
-
<p><code>--assets-extract</code></p>
|
|
306
|
-
<ul>
|
|
307
|
-
<li>Extracts bundled assets to the same directory as the executable</li>
|
|
308
|
-
<li>Exits early before the main program starts</li>
|
|
309
|
-
</ul>
|
|
310
|
-
</li>
|
|
311
|
-
<li>
|
|
312
|
-
<p><code>--assets-external</code></p>
|
|
313
|
-
<ul>
|
|
314
|
-
<li>Skips loading bundled assets into <code>globalThis.internalAssets</code></li>
|
|
315
|
-
<li>Forces the main program and runtime helpers to use the external file tree</li>
|
|
316
|
-
<li>Keeps the bootstrap alive while leaving <code>internalAssets</code> falsy</li>
|
|
317
|
-
</ul>
|
|
318
|
-
</li>
|
|
319
|
-
</ul>
|
|
320
|
-
</section>
|
|
321
|
-
</section>
|
|
322
|
-
|
|
323
|
-
<script type="module" src="./README.md.front.js"></script>
|
|
324
|
-
</body>
|
|
325
|
-
</html>
|
package/src/runtime/assets.js
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
const textDecoder = new TextDecoder();
|
|
2
|
-
const textEncoder = new TextEncoder();
|
|
3
|
-
|
|
4
|
-
export function hasInternalAssets() {
|
|
5
|
-
return Boolean(getAssetStore());
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export function assetPath(...parts) {
|
|
9
|
-
return parts
|
|
10
|
-
.flatMap((part) => String(part).split(/[\\/]+/))
|
|
11
|
-
.filter(Boolean)
|
|
12
|
-
.join("/");
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function listInternalAssetPaths(prefix = "") {
|
|
16
|
-
const store = getAssetStore();
|
|
17
|
-
if (!store) return [];
|
|
18
|
-
|
|
19
|
-
const normalizedPrefix = assetPath(prefix);
|
|
20
|
-
const entries = iterateAssetKeys(store);
|
|
21
|
-
if (!normalizedPrefix) {
|
|
22
|
-
return entries.sort();
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const base = `${normalizedPrefix}/`;
|
|
26
|
-
return entries.filter((path) => path === normalizedPrefix || path.startsWith(base)).sort();
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function listInternalAssetDirs(prefix = "") {
|
|
30
|
-
const normalizedPrefix = assetPath(prefix);
|
|
31
|
-
const base = normalizedPrefix ? `${normalizedPrefix}/` : "";
|
|
32
|
-
const dirs = new Set();
|
|
33
|
-
|
|
34
|
-
for (const path of listInternalAssetPaths(prefix)) {
|
|
35
|
-
const rest = normalizedPrefix ? path.slice(base.length) : path;
|
|
36
|
-
const [dir] = rest.split("/");
|
|
37
|
-
if (dir) dirs.add(dir);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return [...dirs].sort();
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function getInternalAsset(path) {
|
|
44
|
-
const store = getAssetStore();
|
|
45
|
-
if (!store) return null;
|
|
46
|
-
const key = assetPath(path);
|
|
47
|
-
if (store instanceof Map) return store.get(key) ?? null;
|
|
48
|
-
return store[key] ?? store[path] ?? null;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function readInternalAssetBytes(path) {
|
|
52
|
-
const value = getInternalAsset(path);
|
|
53
|
-
if (value == null) return null;
|
|
54
|
-
if (value instanceof Uint8Array) return value;
|
|
55
|
-
if (ArrayBuffer.isView(value)) {
|
|
56
|
-
return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
57
|
-
}
|
|
58
|
-
if (value instanceof ArrayBuffer) return new Uint8Array(value);
|
|
59
|
-
if (typeof value === "string") return textEncoder.encode(value);
|
|
60
|
-
return textEncoder.encode(String(value));
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function readInternalAssetText(path) {
|
|
64
|
-
const bytes = readInternalAssetBytes(path);
|
|
65
|
-
if (!bytes) return null;
|
|
66
|
-
return textDecoder.decode(bytes);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function internalAssetSource(path) {
|
|
70
|
-
return {
|
|
71
|
-
name: path.split("/").pop() ?? path,
|
|
72
|
-
path,
|
|
73
|
-
async text() {
|
|
74
|
-
return readInternalAssetText(path) ?? "";
|
|
75
|
-
},
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function getAssetStore() {
|
|
80
|
-
const store = globalThis.internalAssets;
|
|
81
|
-
if (!store) return null;
|
|
82
|
-
if (store instanceof Map) return store;
|
|
83
|
-
if (typeof store === "object") return store;
|
|
84
|
-
return null;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function iterateAssetKeys(store) {
|
|
88
|
-
if (store instanceof Map) return [...store.keys()].map(String);
|
|
89
|
-
return Object.keys(store);
|
|
90
|
-
}
|