bunmicro 1.0.2 → 1.0.4

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 CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.0.4] - 2026-06-28
4
+ - Fixed `--build-exe` for npx
5
+
6
+ ## [1.0.3] - 2026-06-27
7
+ - Added hex3 series encodings
8
+ * hex3gz / hex3zst add compressed binary-view encodings
9
+ - Added experimental single-file exe support
10
+ * bundled assets loader
11
+ * internal assets fallback to external file tree when needed
12
+ - Added build helpers for single-exe packaging
13
+ * `--build-exe` runs the asset pack + compile flow
14
+ * `--build-for <target>` runs the same flow with an explicit Bun build target
15
+
3
16
  ## [1.0.2] - 2026-06-24
4
17
  - Added startup profiling flags
5
18
  * -profile / --profile
package/README.md CHANGED
@@ -129,6 +129,11 @@ bun x bunmicro
129
129
  # bun ~/.bun/bin/bunmicro
130
130
  ```
131
131
 
132
+ ## Option 3: Git + Bun
133
+ - git clone https://github.com/jjtseng93/bunmicro.git
134
+ - cd bunmicro
135
+ - bun src/index.js
136
+
132
137
  # Basic features
133
138
  - Inherited from the original Golang micro
134
139
  - Only features listed here are implemented
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bunmicro",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "Bun JavaScript rewrite of the micro editor originally in Golang",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
Binary file
@@ -0,0 +1,49 @@
1
+ # This is completely optional
2
+ - I still recommend using the methods in the root README.md
3
+ - Bun's Android build is currently not supported
4
+
5
+ # Usage
6
+ - First run `bun ./packAssets.sh` to bundle the assets into `assets.tar`
7
+ - Then run the build script like this
8
+
9
+ ```shell
10
+ bun build --compile --bytecode --minify ./entry.mjs --outfile=bmi
11
+ ```
12
+
13
+ - You'll get a bmi executable
14
+
15
+ # Single Executable Intro
16
+
17
+ This folder contains the Bun single-exe bootstrap used by the project.
18
+
19
+ ## Entry Flow
20
+
21
+ - `entry.mjs` imports `assetsLoader.mjs` first
22
+ - `assetsLoader.mjs` loads `assets.tar` with `Bun.Archive` and mounts it as `globalThis.internalAssets`
23
+ - `assetsLoaderPromise` is exposed on `globalThis`
24
+ - `../src/index.js` waits for `assetsLoaderPromise` if it exists
25
+
26
+ That keeps the main program bootable even if asset loading reports errors.
27
+
28
+ ## Assets Loading
29
+
30
+ - Bundled assets are loaded sequentially with `await file.bytes()`
31
+ - Load failures are collected and printed to `stderr`
32
+ - Asset loading never rejects the bootstrap promise
33
+ - When loading succeeds, the archive is available through `globalThis.internalAssets`
34
+
35
+ ## CLI Flags
36
+
37
+ - `--assets-list`
38
+ - Lists all entries inside bundled `assets.tar`
39
+ - Exits early before the main program starts
40
+
41
+ - `--assets-extract`
42
+ - Extracts bundled assets to the same directory as the executable
43
+ - Exits early before the main program starts
44
+
45
+ - `--assets-external`
46
+ - Skips loading bundled assets into `globalThis.internalAssets`
47
+ - Forces `../src/index.js` and runtime helpers to use the external file tree
48
+ - Keeps the bootstrap alive while leaving `internalAssets` falsy
49
+
Binary file
@@ -0,0 +1,85 @@
1
+ import assets from "./assets.tar" with { type: "file" };
2
+ import { resolveCompiledBaseDir, isCompiledBinary } from "../src/runtime/compiled.js";
3
+
4
+ const forceExternalAssets = process.argv.includes("--assets-external");
5
+ const debugAssetsLoader = Boolean(process.env.BUNMICRO_DEBUG);
6
+ if (forceExternalAssets) {
7
+ const flagIndex = process.argv.indexOf("--assets-external");
8
+ if (flagIndex >= 0) process.argv.splice(flagIndex, 1);
9
+ }
10
+
11
+ globalThis.internalAssets = forceExternalAssets ? null : Object.create(null);
12
+ globalThis.assetsLoaderPromise = main(process.argv).catch((error) => {
13
+ console.error("# assets loader failed");
14
+ console.error(error);
15
+ return globalThis.internalAssets;
16
+ });
17
+
18
+ async function main(argv) {
19
+
20
+ if (forceExternalAssets &&
21
+ !argv.includes("--assets-list") &&
22
+ !argv.includes("--assets-extract")) {
23
+ return null;
24
+ }
25
+
26
+ const startedAt = debugAssetsLoader ? Bun.nanoseconds() : 0;
27
+
28
+ const tarball = await Bun.file(assets).bytes();
29
+ const archive = new Bun.Archive(tarball);
30
+
31
+ await cliEarlyExit(archive, argv);
32
+
33
+ const files = await archive.files();
34
+ const entries = [...files.entries()];
35
+ const assetsByPath = Object.create(null);
36
+ const errors = [];
37
+
38
+ for (const [path, file] of entries) {
39
+ try {
40
+ assetsByPath[path] = await file.bytes();
41
+ } catch (reason) {
42
+ errors.push({ path, reason });
43
+ }
44
+ }
45
+
46
+ if (errors.length > 0) {
47
+ console.error(`# Failed to load ${errors.length} bundled asset(s):`);
48
+ for (const { path, reason } of errors) {
49
+ console.error(`- ${path}`);
50
+ if (reason) {
51
+ console.error(reason);
52
+ }
53
+ }
54
+ }
55
+ if (debugAssetsLoader) {
56
+ const elapsedMs = (Bun.nanoseconds() - startedAt) / 1e6;
57
+ console.error(`Loaded assets: ${elapsedMs.toFixed(3)} ms`);
58
+ }
59
+
60
+ globalThis.internalAssets = assetsByPath;
61
+ return assetsByPath;
62
+ }
63
+
64
+ async function cliEarlyExit(archive, argv) {
65
+ if (argv.includes("--assets-list")) {
66
+ const files = await archive.files();
67
+ for (const path of [...files.keys()].sort()) {
68
+ console.log(path);
69
+ }
70
+ process.exit(0);
71
+ }
72
+
73
+ if (argv.includes("--assets-extract")) {
74
+ if (isCompiledBinary(argv))
75
+ {
76
+ const targetDir = resolveCompiledBaseDir({ argv });
77
+ const extracted = await archive.extract(targetDir);
78
+ console.log(`Extracted ${extracted} asset(s) to ${targetDir}`);
79
+ } // if isCompiled
80
+ else
81
+ console.log("Assets extraction should only be used in a Bun compiled single-file executable");
82
+
83
+ process.exit(0);
84
+ }
85
+ }
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env bun
2
+
3
+ // 1. Injects assets to global.internalAssets
4
+ // as { "./path/in/tar":file.bytes() }
5
+ // 2. Sets global.assetsLoaderPromise
6
+ import "./assetsLoader.mjs"
7
+
8
+ import "../src/index.js"
9
+
@@ -0,0 +1,7 @@
1
+ #!/bin/sh
2
+
3
+ sd=$(dirname "$0")
4
+
5
+ cd "$sd"/..
6
+
7
+ tar -cvf single-exe/assets.tar runtime README.md CHANGELOG.md
@@ -4,6 +4,7 @@ import { homedir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  //import { Glob } from "bun";
6
6
  import { defaultAllSettings, OPTION_CHOICES, LOCAL_SETTINGS } from "./defaults.js";
7
+ import { isHex3Encoding } from "../runtime/encodings.js";
7
8
 
8
9
  export class Config {
9
10
  constructor({ configDir = "" } = {}) {
@@ -105,7 +106,7 @@ function normalizeSetting(key, value) {
105
106
  function validateOption(option, value) {
106
107
  if (option === "encoding") {
107
108
  const encoding = String(value || "utf-8");
108
- if (encoding === "hex3") return;
109
+ if (isHex3Encoding(encoding)) return;
109
110
  try { new TextDecoder(encoding); }
110
111
  catch { throw new Error(`Invalid encoding: ${value}`); }
111
112
  }
package/src/index.js CHANGED
@@ -1,5 +1,8 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
+ let mainPromise = globalThis.assetsLoaderPromise ||
4
+ Promise.resolve();
5
+
3
6
  const jsStart = globalThis.Bun ? Bun.nanoseconds() : Date.now() * 1e6;
4
7
  const checkpoints = [
5
8
  { name: "Bun Engine Boot", time: 0 },
@@ -99,6 +102,7 @@ import { Config } from "./config/config.js";
99
102
  import { defaultAllSettings, OPTION_CHOICES, LOCAL_SETTINGS } from "./config/defaults.js";
100
103
  import { cleanConfig } from "./config/clean.js";
101
104
  import { RuntimeRegistry, RTColorscheme, RTHelp } from "./runtime/registry.js";
105
+ import { assetPath, hasInternalAssets, listInternalAssetDirs, readInternalAssetText } from "./runtime/assets.js";
102
106
  import { PluginManager } from "./plugins/manager.js";
103
107
  import { JsPluginManager, buildMicroGlobal, runAction, listActions } from "./plugins/js-bridge.js";
104
108
  import { Colorscheme } from "./config/colorscheme.js";
@@ -113,9 +117,11 @@ import { shellSplit } from "./shell/shell.js";
113
117
  import { styleToAnsi } from "./display/ansi-style.js";
114
118
  import { encodeBinaryToBuffer, decodeBinaryBytes } from "./buffer/fixed3-codec.js";
115
119
  import { writeBackup, removeBackup, applyBackup } from "./buffer/backup.js";
120
+ import { isHex3Encoding } from "./runtime/encodings.js";
116
121
  import { createInterface } from "node:readline/promises";
117
122
 
118
123
  import pkg from "../package.json" with { type: "json" };
124
+ import { isCompiledBinary, resolveCompiledBaseDir } from "./runtime/compiled.js";
119
125
 
120
126
 
121
127
  const __filename = fileURLToPath(import.meta.url);
@@ -159,7 +165,13 @@ Windows
159
165
  }
160
166
 
161
167
  const VERSION = pkg.version;
162
- const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
168
+ const IS_COMPILED = isCompiledBinary(process.argv);
169
+ const REPO_ROOT = IS_COMPILED
170
+ ? resolveCompiledBaseDir({ argv: process.argv })
171
+ : resolve(dirname(fileURLToPath(import.meta.url)), "..");
172
+ const SINGLE_EXE_DIR = resolve(REPO_ROOT, "single-exe");
173
+ const SINGLE_EXE_ENTRY = resolve(SINGLE_EXE_DIR, "entry.mjs");
174
+ const DEFAULT_BUILD_OUTFILE = "bmi";
163
175
  const decoder = new TextDecoder();
164
176
  let _activeTtyStream = null; // set in App.start() for use by the global error handler
165
177
 
@@ -253,13 +265,44 @@ function isHttpUrl(value) {
253
265
  }
254
266
 
255
267
  function decodeTextBytesWithEncoding(bytes, encoding = "utf-8") {
256
- if (normalizeEncodingLabel(encoding) === "hex3") {
257
- return { text: encodeBinaryToBuffer(bytes).toString("latin1"), encoding: "hex3" };
268
+ const normalized = normalizeEncodingLabel(encoding);
269
+ if (normalized === "hex3gz") {
270
+ const decoded = decodeHex3Bytes(Bun.gunzipSync(bytes));
271
+ return { ...decoded, encoding: "hex3gz" };
272
+ }
273
+ if (normalized === "hex3zst") {
274
+ const decoded = decodeHex3Bytes(Bun.zstdDecompressSync(bytes));
275
+ return { ...decoded, encoding: "hex3zst" };
276
+ }
277
+ if (normalized === "hex3") {
278
+ return decodeHex3Bytes(bytes);
258
279
  }
259
- const decoder = new TextDecoder(normalizeEncodingLabel(encoding));
280
+ const decoder = new TextDecoder(normalized);
260
281
  return { text: decoder.decode(bytes), encoding: decoder.encoding };
261
282
  }
262
283
 
284
+ function encodeTextBytesWithEncoding(text, encoding = "utf-8") {
285
+ const normalized = normalizeEncodingLabel(encoding);
286
+ if (normalized === "hex3gz") {
287
+ return Bun.gzipSync(encodeHex3Text(text));
288
+ }
289
+ if (normalized === "hex3zst") {
290
+ return Bun.zstdCompressSync(encodeHex3Text(text));
291
+ }
292
+ if (normalized === "hex3") {
293
+ return encodeHex3Text(text);
294
+ }
295
+ return new TextEncoder().encode(String(text));
296
+ }
297
+
298
+ function decodeHex3Bytes(bytes) {
299
+ return { text: encodeBinaryToBuffer(bytes).toString("latin1"), encoding: "hex3" };
300
+ }
301
+
302
+ function encodeHex3Text(text) {
303
+ return decodeBinaryBytes(Buffer.from(text, "latin1"));
304
+ }
305
+
263
306
  async function readTextFileWithEncoding(path, encoding = "utf-8") {
264
307
  const bytes = new Uint8Array(await Bun.file(path).arrayBuffer());
265
308
  return decodeTextBytesWithEncoding(bytes, encoding);
@@ -272,7 +315,7 @@ async function fetchTextWithEncoding(url, encoding = "utf-8") {
272
315
 
273
316
  function normalizeEncodingLabel(encoding = "utf-8") {
274
317
  const s = String(encoding || "utf-8");
275
- if (s === "hex3") return "hex3";
318
+ if (isHex3Encoding(s)) return s.toLowerCase();
276
319
  return new TextDecoder(s).encoding;
277
320
  }
278
321
 
@@ -602,6 +645,8 @@ function parseArgs(argv) {
602
645
  cat: false,
603
646
  docs: false,
604
647
  changelog: false,
648
+ buildExe: false,
649
+ buildFor: "",
605
650
  configDir: "",
606
651
  debug: false,
607
652
  profile: false,
@@ -626,8 +671,16 @@ function parseArgs(argv) {
626
671
  else if (arg === "--hex3") {
627
672
  flags.settings.set("encoding", "hex3");
628
673
  }
674
+ else if (arg === "--hex3gz") {
675
+ flags.settings.set("encoding", "hex3gz");
676
+ }
677
+ else if (arg === "--hex3zst") {
678
+ flags.settings.set("encoding", "hex3zst");
679
+ }
629
680
  else if (arg === "--docs" || arg === "--readme") flags.docs = true;
630
681
  else if (arg === "--changelog") flags.changelog = true;
682
+ else if (arg === "--build-exe") flags.buildExe = true;
683
+ else if (arg === "--build-for") flags.buildFor = argv[++i] ?? "";
631
684
  else if (arg === "-debug") flags.debug = true;
632
685
  else if (arg === "-profile" || arg === "--profile") flags.profile = true;
633
686
  else if (arg === "-config-dir") flags.configDir = argv[++i] ?? "";
@@ -682,8 +735,9 @@ function usage() {
682
735
  " Syntax-highlight file(s) and write to stdout, then exit (.md uses Bun.markdown.ansi)\n",
683
736
  "--xxd, --hexdump",
684
737
  " Hex3 dump file(s) and write to stdout (same as --cat -encoding hex3)\n",
685
- "--hex3",
686
- " Set -encoding hex3 for this session\n",
738
+ "--hex3, --hex3gz, --hex3zst",
739
+ " Set -encoding hex3, hex3gz, or hex3zst for this session",
740
+ " hex3 shows raw bytes; gz/zst variants compress the same hex3 view\n",
687
741
  "-help, -h, --help",
688
742
  " Show this help & exit",
689
743
  "-version, -V, --version",
@@ -701,6 +755,53 @@ function usage() {
701
755
  ].join("\n");
702
756
  }
703
757
 
758
+ async function buildExecutable(target = "") {
759
+ const outfile = resolve(process.cwd(), DEFAULT_BUILD_OUTFILE);
760
+ const normalizedTarget = String(target || "").trim();
761
+
762
+ const steps = [
763
+ {
764
+ label: "Pack assets",
765
+ cwd: SINGLE_EXE_DIR,
766
+ cmd: "bun",
767
+ args: ["./packAssets.sh"],
768
+ },
769
+ {
770
+ label: "Compile executable",
771
+ cwd: process.cwd(),
772
+ cmd: "bun",
773
+ args: [
774
+ "build",
775
+ "--compile",
776
+ "--bytecode",
777
+ "--minify",
778
+ SINGLE_EXE_ENTRY,
779
+ `--outfile=${DEFAULT_BUILD_OUTFILE}`,
780
+ ...(normalizedTarget ? [`--target=${normalizedTarget}`] : []),
781
+ ],
782
+ },
783
+ ];
784
+
785
+ for (const step of steps) {
786
+ console.log('');
787
+ console.log(Bun.markdown.ansi('## '+step.label));
788
+
789
+ const result = child_process.spawnSync(step.cmd, step.args, {
790
+ cwd: step.cwd,
791
+ stdio: "inherit",
792
+ env: process.env,
793
+ });
794
+
795
+ console.log("");
796
+ console.log(Bun.markdown.ansi(
797
+ '- Status: '+result.status+' for '+step.label
798
+ ));
799
+
800
+ }
801
+
802
+ console.log(`Built executable: ${outfile}`);
803
+ }
804
+
704
805
  function parseInput(args) {
705
806
  const files = [];
706
807
  const command = {
@@ -1283,9 +1384,9 @@ class BufferModel {
1283
1384
  if (this._backupRevision === backupRevision) this._backupRequested = false;
1284
1385
  this._forceKeepBackup = true;
1285
1386
  }
1286
- if (this.encoding === "hex3") {
1387
+ if (isHex3Encoding(this.encoding)) {
1287
1388
  try {
1288
- await Bun.write(targetPath, decodeBinaryBytes(Buffer.from(text, "latin1")));
1389
+ await Bun.write(targetPath, encodeTextBytesWithEncoding(text, this.encoding));
1289
1390
  } finally {
1290
1391
  this._forceKeepBackup = false;
1291
1392
  }
@@ -4143,7 +4244,7 @@ class App {
4143
4244
  if (!force && this.buffer?.readonly) { this.message = "Can't save under readonly mode"; return; }
4144
4245
  try {
4145
4246
  const enc = normalizeEncodingLabel(this.buffer?.encoding);
4146
- if (enc !== "utf-8" && enc !== "hex3") {
4247
+ if (enc !== "utf-8" && !isHex3Encoding(enc)) {
4147
4248
  this.openYNPrompt("Save in UTF-8?(y,n)", async (answer) => {
4148
4249
  if (answer === "y") await this.saveUtf8();
4149
4250
  });
@@ -4560,7 +4661,8 @@ class App {
4560
4661
  const saveArgs = [...cmdArgs];
4561
4662
  const saveForce = saveArgs[0] === "-f" && (saveArgs.shift(), true);
4562
4663
  if (!saveForce && buf?.readonly) { this.message = "Can't save under readonly mode"; break; }
4563
- if (saveArgs.length > 0 && normalizeEncodingLabel(buf?.encoding) !== "utf-8" && normalizeEncodingLabel(buf?.encoding) !== "hex3") {
4664
+ const bufferEncoding = normalizeEncodingLabel(buf?.encoding);
4665
+ if (saveArgs.length > 0 && bufferEncoding !== "utf-8" && !isHex3Encoding(bufferEncoding)) {
4564
4666
  const target = resolve(expandHome(saveArgs[0]));
4565
4667
  this.openYNPrompt("Save in UTF-8?(y,n)", async (answer) => {
4566
4668
  if (answer === "y") {
@@ -5435,14 +5537,14 @@ const COMMAND_NAMES = [
5435
5537
  ];
5436
5538
 
5437
5539
  const SUPPORTED_ENCODING_LABELS = [
5438
- "hex3",
5540
+ "hex3", "hex3gz", "hex3zst",
5439
5541
  "utf-8", "utf-16le", "utf-16be",
5440
5542
  "windows-1252", "iso-8859-1", "latin1",
5441
5543
  "big5", "gbk", "gb18030",
5442
5544
  "shift_jis", "sjis", "euc-jp", "iso-2022-jp",
5443
5545
  "euc-kr", "ks_c_5601-1987",
5444
5546
  ].filter((encoding) => {
5445
- if (encoding === "hex3") return true;
5547
+ if (isHex3Encoding(encoding)) return true;
5446
5548
  try { new TextDecoder(encoding); return true; }
5447
5549
  catch { return false; }
5448
5550
  });
@@ -6703,18 +6805,38 @@ async function loadBuffers(files, command) {
6703
6805
  }
6704
6806
 
6705
6807
  async function printReadmeDocs() {
6706
- const readme = await Bun.file(join(REPO_ROOT, "README.md")).text();
6808
+ const readme = readInternalAssetText("README.md") ?? await Bun.file(join(REPO_ROOT, "README.md")).text();
6707
6809
  process.stdout.write(Bun.markdown.ansi(readme, { hyperlinks: true }));
6708
6810
  }
6709
6811
 
6710
6812
  async function printChangelogDocs() {
6711
- const changelog = await Bun.file(join(REPO_ROOT, "CHANGELOG.md")).text();
6813
+ const changelog = readInternalAssetText("CHANGELOG.md") ?? await Bun.file(join(REPO_ROOT, "CHANGELOG.md")).text();
6712
6814
  process.stdout.write(Bun.markdown.ansi(changelog, { hyperlinks: true }));
6713
6815
  }
6714
6816
 
6715
6817
  async function main() {
6716
6818
  addCheckpoint("Argument Parsing");
6717
6819
  const { flags, files: rawFiles } = parseArgs(process.argv.slice(2));
6820
+ if (flags.buildExe && flags.buildFor) {
6821
+ console.error("--build-exe and --build-for are separate paths; use only one");
6822
+ process.exit(1);
6823
+ }
6824
+ if (flags.buildExe) {
6825
+ if (IS_COMPILED) {
6826
+ console.error("--build-exe is only available in the source tree");
6827
+ process.exit(1);
6828
+ }
6829
+ await buildExecutable();
6830
+ return;
6831
+ }
6832
+ if (flags.buildFor) {
6833
+ if (IS_COMPILED) {
6834
+ console.error("--build-for is only available in the source tree");
6835
+ process.exit(1);
6836
+ }
6837
+ await buildExecutable(flags.buildFor);
6838
+ return;
6839
+ }
6718
6840
  if (flags.help) {
6719
6841
  console.log(usage());
6720
6842
  return;
@@ -6783,17 +6905,30 @@ async function main() {
6783
6905
 
6784
6906
  if (flags.plugin === "list") {
6785
6907
  const luaList = plugins.list();
6786
- const jsDirs = [
6787
- { dir: join(REPO_ROOT, "runtime", "jsplugins"), builtin: true },
6788
- { dir: join(config.configDir, "jsplug"), builtin: false },
6789
- ];
6790
6908
  const jsItems = [];
6791
- for (const { dir, builtin } of jsDirs) {
6792
- if (!existsSync(dir)) continue;
6793
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
6909
+ const builtinJsPluginNames = hasInternalAssets()
6910
+ ? listInternalAssetDirs(assetPath("runtime", "jsplugins"))
6911
+ : [];
6912
+ if (builtinJsPluginNames.length > 0) {
6913
+ for (const name of builtinJsPluginNames) {
6914
+ jsItems.push({ name, builtin: true });
6915
+ }
6916
+ } else {
6917
+ const builtinJsDir = join(REPO_ROOT, "runtime", "jsplugins");
6918
+ if (existsSync(builtinJsDir)) {
6919
+ for (const entry of readdirSync(builtinJsDir, { withFileTypes: true })) {
6920
+ if (!entry.isDirectory()) continue;
6921
+ if (existsSync(join(builtinJsDir, entry.name, `${entry.name}.js`)))
6922
+ jsItems.push({ name: entry.name, builtin: true });
6923
+ }
6924
+ }
6925
+ }
6926
+ const userJsDir = join(config.configDir, "jsplug");
6927
+ if (existsSync(userJsDir)) {
6928
+ for (const entry of readdirSync(userJsDir, { withFileTypes: true })) {
6794
6929
  if (!entry.isDirectory()) continue;
6795
- if (existsSync(join(dir, entry.name, `${entry.name}.js`)))
6796
- jsItems.push({ name: entry.name, builtin });
6930
+ if (existsSync(join(userJsDir, entry.name, `${entry.name}.js`)))
6931
+ jsItems.push({ name: entry.name, builtin: false });
6797
6932
  }
6798
6933
  }
6799
6934
  const fmtTag = (p) => p.builtin ? " *(built-in)*" : "";
@@ -7380,6 +7515,10 @@ async function catFiles(files, colorscheme, syntaxDefinitions, encoding = DEFAUL
7380
7515
  }
7381
7516
  }
7382
7517
 
7518
+
7519
+ mainPromise.then(r=>{
7520
+
7521
+
7383
7522
  main().catch((error) => {
7384
7523
  try {
7385
7524
  (_activeTtyStream ?? process.stdin).setRawMode?.(false);
@@ -7389,3 +7528,6 @@ main().catch((error) => {
7389
7528
  process.exit(1);
7390
7529
  }
7391
7530
  });
7531
+
7532
+
7533
+ })
package/src/lua/engine.js CHANGED
@@ -1,21 +1,56 @@
1
+ import { existsSync } from "node:fs";
2
+ import { readInternalAssetBytes } from "../runtime/assets.js";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+ import { fileURLToPath } from "node:url";
6
+ import { isCompiledBinary, resolveCompiledBaseDir } from "../runtime/compiled.js";
7
+
8
+ const SOURCE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
9
+
1
10
  export async function createLuaEngine() {
2
- const wasmoon = await tryImport("wasmoon");
11
+ let wasmoon = null;
12
+ try {
13
+ wasmoon = await import("wasmoon");
14
+ } catch (error) {
15
+ if (
16
+ error?.code !== "ERR_MODULE_NOT_FOUND" &&
17
+ !/Cannot find package/.test(String(error?.message))
18
+ ) {
19
+ throw error;
20
+ }
21
+ }
3
22
  if (!wasmoon?.LuaFactory) {
4
23
  throw new Error("Lua support requires the WASM runtime `wasmoon`. Install it with `bun add wasmoon`.");
5
24
  }
6
25
 
7
- const factory = new wasmoon.LuaFactory();
26
+ const wasmLocation = await resolveLuaWasmLocation();
27
+ const factory = new wasmoon.LuaFactory(wasmLocation);
8
28
  const lua = await factory.createEngine();
9
29
  return new WasmoonEngine(lua);
10
30
  }
11
31
 
12
- async function tryImport(name) {
13
- try {
14
- return await import(name);
15
- } catch (error) {
16
- if (error?.code === "ERR_MODULE_NOT_FOUND" || /Cannot find package/.test(String(error?.message))) return null;
17
- throw error;
32
+ async function resolveLuaWasmLocation() {
33
+ const wasmAssetPath = "runtime/wasmoon_glue.wasm";
34
+ const wasmBytes = readInternalAssetBytes(wasmAssetPath);
35
+ if (wasmBytes) {
36
+ const wasmPath = join(tmpdir(), "bunmicro-wasmoon-glue.wasm");
37
+ try {
38
+ const existing = await Bun.file(wasmPath).bytes().catch(() => null);
39
+ if (!existing || existing.byteLength !== wasmBytes.byteLength) {
40
+ await Bun.write(wasmPath, wasmBytes);
41
+ }
42
+ return wasmPath;
43
+ } catch (error) {
44
+ console.error("# failed to stage bundled wasmoon wasm");
45
+ console.error(error);
46
+ }
18
47
  }
48
+
49
+ const fallbackPath = isCompiledBinary(process.argv)
50
+ ? join(resolveCompiledBaseDir({ argv: process.argv }), wasmAssetPath)
51
+ : join(SOURCE_ROOT, wasmAssetPath);
52
+
53
+ return existsSync(fallbackPath) ? fallbackPath : undefined;
19
54
  }
20
55
 
21
56
  class WasmoonEngine {
@@ -1,15 +1,50 @@
1
- import { existsSync } from "node:fs";
1
+ import { existsSync, mkdirSync } from "node:fs";
2
2
  import { readdir } from "node:fs/promises";
3
- import { join, basename, extname } from "node:path";
3
+ import { dirname, join, basename, extname } from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ import { tmpdir } from "node:os";
6
+ import { assetPath, hasInternalAssets, listInternalAssetDirs, listInternalAssetPaths, readInternalAssetBytes } from "../runtime/assets.js";
4
7
  import { newMessage, newMessageAtLine, MTError, MTWarning, MTInfo } from "../buffer/message.js";
5
8
  import { Loc } from "../buffer/loc.js";
6
9
 
7
10
  // ── Action registry ──────────────────────────────────────────────────────────
8
11
 
9
12
  const ACTIONS = new Map();
13
+ const INTERNAL_JSPLUGIN_STAGE_ROOT = join(tmpdir(), "bunmicro-jsplugins");
14
+ let internalJsPluginStagePromise = null;
10
15
 
11
16
  function reg(name, fn) { ACTIONS.set(name, fn); }
12
17
 
18
+ function stageInternalJsPlugins() {
19
+ if (!internalJsPluginStagePromise) {
20
+ internalJsPluginStagePromise = _stageInternalJsPlugins().catch((error) => {
21
+ console.error("# failed to stage internal JS plugins");
22
+ console.error(error);
23
+ return null;
24
+ });
25
+ }
26
+ return internalJsPluginStagePromise;
27
+ }
28
+
29
+ async function _stageInternalJsPlugins() {
30
+ const prefix = assetPath("runtime", "jsplugins");
31
+ const paths = listInternalAssetPaths(prefix);
32
+ if (paths.length === 0) return null;
33
+
34
+ mkdirSync(INTERNAL_JSPLUGIN_STAGE_ROOT, { recursive: true });
35
+ await Bun.write(join(INTERNAL_JSPLUGIN_STAGE_ROOT, "package.json"), JSON.stringify({ type: "module" }));
36
+
37
+ for (const assetPathName of paths) {
38
+ const bytes = readInternalAssetBytes(assetPathName);
39
+ if (!bytes) continue;
40
+ const stagedPath = join(INTERNAL_JSPLUGIN_STAGE_ROOT, ...assetPathName.split("/"));
41
+ mkdirSync(dirname(stagedPath), { recursive: true });
42
+ await Bun.write(stagedPath, bytes);
43
+ }
44
+
45
+ return INTERNAL_JSPLUGIN_STAGE_ROOT;
46
+ }
47
+
13
48
  function _actIndentStr(buf) {
14
49
  if (buf?.Settings?.tabstospaces) return " ".repeat(buf?.Settings?.tabsize ?? 4);
15
50
  return "\t";
@@ -550,6 +585,10 @@ export class JsPluginManager {
550
585
  // Scan and load all JS plugins from given directories
551
586
  async loadFrom(dirs) {
552
587
  for (const { dir, builtin } of dirs) {
588
+ if (builtin && hasInternalAssets()) {
589
+ const loadedFromAssets = await this._loadFromInternalAssets(dir, builtin);
590
+ if (loadedFromAssets) continue;
591
+ }
553
592
  if (!existsSync(dir)) continue;
554
593
  const entries = await readdir(dir, { withFileTypes: true });
555
594
  for (const entry of entries) {
@@ -562,6 +601,30 @@ export class JsPluginManager {
562
601
  }
563
602
  }
564
603
 
604
+ async _loadFromInternalAssets(dir, builtin) {
605
+ const prefix = assetPath("runtime", "jsplugins");
606
+ const stageRoot = await stageInternalJsPlugins();
607
+ if (!stageRoot) return false;
608
+
609
+ const pluginNames = listInternalAssetDirs(prefix);
610
+ if (pluginNames.length === 0) return false;
611
+
612
+ let loadedAny = false;
613
+ for (const pluginName of pluginNames) {
614
+ const stagedMainPath = join(stageRoot, prefix, pluginName, `${pluginName}.js`);
615
+ if (!existsSync(stagedMainPath)) continue;
616
+ try {
617
+ await import(pathToFileURL(stagedMainPath).href);
618
+ this._loaded.push({ path: assetPath(prefix, pluginName, `${pluginName}.js`), name: pluginName, builtin, loaded: true });
619
+ loadedAny = true;
620
+ } catch (e) {
621
+ this._loaded.push({ path: assetPath(prefix, pluginName, `${pluginName}.js`), name: pluginName, builtin, loaded: false, error: e.message });
622
+ console.error(`[jsplugin] failed to load ${pluginName}: ${e.message}`);
623
+ }
624
+ }
625
+ return loadedAny;
626
+ }
627
+
565
628
  async _loadFile(path, name, builtin) {
566
629
  try {
567
630
  await import(path);
@@ -4,6 +4,7 @@ import { basename, dirname, extname, join, sep } from "node:path";
4
4
  import { fetchHttp, downloadFile } from "../platform/commands.js";
5
5
  import { extractAndStrip } from "../platform/archive.js";
6
6
  import { createLuaEngine } from "../lua/engine.js";
7
+ import { assetPath, hasInternalAssets, internalAssetSource, listInternalAssetDirs, listInternalAssetPaths, readInternalAssetText } from "../runtime/assets.js";
7
8
  import { execCommand, runBackgroundShell, runCommand } from "../shell/shell.js";
8
9
  import { Loc } from "../buffer/loc.js";
9
10
  import { BTDefault, BTHelp, BTInfo, BTLog, BTRaw, BTScratch, byteOffset, BufferCore } from "../buffer/buffer.js";
@@ -107,10 +108,24 @@ export class PluginManager {
107
108
  }
108
109
 
109
110
  async scanBuiltinPlugins() {
111
+ const userNames = new Set(this.plugins.map((plugin) => plugin.name));
112
+ const internalPrefix = assetPath("runtime", "plugins");
113
+
114
+ if (hasInternalAssets()) {
115
+ const entries = listInternalAssetDirs(internalPrefix);
116
+ if (entries.length > 0) {
117
+ for (const entryName of entries) {
118
+ if (userNames.has(entryName)) continue;
119
+ const plugin = await scanPluginDirectoryFromAssets(assetPath(internalPrefix, entryName), entryName, true);
120
+ if (plugin) this.plugins.push(plugin);
121
+ }
122
+ return;
123
+ }
124
+ }
125
+
110
126
  const plugdir = join(this.repoRoot, "runtime", "plugins");
111
127
  if (!existsSync(plugdir)) return;
112
128
  const entries = await readdir(plugdir, { withFileTypes: true });
113
- const userNames = new Set(this.plugins.map((plugin) => plugin.name));
114
129
  for (const entry of entries) {
115
130
  if (!entry.isDirectory() || userNames.has(entry.name)) continue;
116
131
  const plugin = await scanPluginDirectory(join(plugdir, entry.name), entry.name, true);
@@ -412,6 +427,16 @@ export class PluginManager {
412
427
  addPluginRuntimeFile(plugin, kind, relpath) {
413
428
  const owner = this.plugins.find((p) => p.name === plugin);
414
429
  if (!owner) return;
430
+ const internalPath = assetPath("runtime", "plugins", owner.dirName, relpath);
431
+ const internalText = readInternalAssetText(internalPath);
432
+ if (internalText != null) {
433
+ this.runtime.files[kind].push({
434
+ name: basename(relpath, extname(relpath)),
435
+ path: internalPath,
436
+ text: async () => internalText,
437
+ });
438
+ return;
439
+ }
415
440
  const path = join(this.repoRoot, "runtime", "plugins", owner.dirName, relpath);
416
441
  this.runtime.files[kind].push({ name: basename(path, extname(path)), path, text: async () => await readFile(path, "utf8") });
417
442
  }
@@ -589,6 +614,14 @@ async function scanPluginDirectory(dir, defaultName, builtin) {
589
614
  }
590
615
 
591
616
  function fileSource(path) {
617
+ const internalText = readInternalAssetText(path);
618
+ if (internalText != null) {
619
+ return {
620
+ name: basename(path),
621
+ path,
622
+ text: async () => internalText,
623
+ };
624
+ }
592
625
  return {
593
626
  name: basename(path),
594
627
  path,
@@ -596,6 +629,28 @@ function fileSource(path) {
596
629
  };
597
630
  }
598
631
 
632
+ async function scanPluginDirectoryFromAssets(prefix, defaultName, builtin) {
633
+ const entries = listInternalAssetPaths(prefix);
634
+ const srcs = [];
635
+ let info = null;
636
+ let name = defaultName;
637
+ const base = `${assetPath(prefix)}/`;
638
+
639
+ for (const fullPath of entries) {
640
+ const rel = fullPath.slice(base.length);
641
+ if (!rel || rel.includes("/")) continue;
642
+ if (rel.endsWith(".lua")) {
643
+ srcs.push(internalAssetSource(fullPath));
644
+ } else if (rel.endsWith(".json")) {
645
+ const parsed = JSON.parse(readInternalAssetText(fullPath) ?? "null");
646
+ info = Array.isArray(parsed) ? parsed[0] : parsed;
647
+ if (info?.Name) name = info.Name;
648
+ }
649
+ }
650
+ if (!VALID_PLUGIN_NAME.test(name) || srcs.length === 0) return null;
651
+ return new Plugin({ name, dirName: basename(prefix), srcs, info, builtin });
652
+ }
653
+
599
654
  function humanizeBytes(bytes) {
600
655
  const units = ["B", "KB", "MB", "GB", "TB"];
601
656
  let value = Number(bytes);
@@ -0,0 +1,90 @@
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
+ }
@@ -0,0 +1,25 @@
1
+ import { basename, dirname, resolve } from "node:path";
2
+ import { readFileSync } from "node:fs";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ export function isCompiledBinary(argv = process.argv) {
6
+ return Boolean(argv?.[1]?.startsWith?.("/$bunfs/"));
7
+ }
8
+
9
+ export function resolveCompiledBaseDir({ argv = process.argv, execPath = process.execPath } = {}) {
10
+ const bn = basename(execPath);
11
+ if (bn.startsWith("ld") ||
12
+ bn.startsWith("libld") ||
13
+ bn.startsWith("linker") ) {
14
+ const realArgv = readFileSync("/proc/self/cmdline", "utf8").match(/[^\0]+/g);
15
+ return dirname(realArgv?.[1] ?? execPath);
16
+ }
17
+ return dirname(execPath) || process.cwd();
18
+ }
19
+
20
+ export function resolveRepoRoot(importMetaUrl, options = {}) {
21
+ if (isCompiledBinary(options.argv)) {
22
+ return resolveCompiledBaseDir(options);
23
+ }
24
+ return resolve(dirname(fileURLToPath(importMetaUrl)), "..");
25
+ }
@@ -0,0 +1,5 @@
1
+ const HEX3_ENCODINGS = new Set(["hex3", "hex3gz", "hex3zst"]);
2
+
3
+ export function isHex3Encoding(encoding) {
4
+ return HEX3_ENCODINGS.has(String(encoding || "utf-8").toLowerCase());
5
+ }
@@ -1,6 +1,7 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { readdir, readFile } from "node:fs/promises";
3
3
  import { basename, extname, join } from "node:path";
4
+ import { assetPath, hasInternalAssets, listInternalAssetPaths, readInternalAssetBytes } from "./assets.js";
4
5
 
5
6
  export const RTColorscheme = 0;
6
7
  export const RTSyntax = 1;
@@ -29,6 +30,10 @@ export class RuntimeRegistry {
29
30
 
30
31
  async addRuntimeKind(kind, dir, extension, user) {
31
32
  if (user) await this.addDirectory(kind, join(this.configDir, dir), extension, true);
33
+ const internalPrefix = assetPath("runtime", dir);
34
+ if (hasInternalAssets() && await this.addInternalDirectory(kind, internalPrefix, extension, false)) {
35
+ return;
36
+ }
32
37
  await this.addDirectory(kind, join(this.repoRoot, "runtime", dir), extension, false);
33
38
  }
34
39
 
@@ -47,6 +52,31 @@ export class RuntimeRegistry {
47
52
  }
48
53
  }
49
54
 
55
+ async addInternalDirectory(kind, prefix, extension, real) {
56
+ const entries = listInternalAssetPaths(prefix);
57
+ if (entries.length === 0) return false;
58
+
59
+ const base = `${assetPath(prefix)}/`;
60
+ let added = false;
61
+ for (const path of entries) {
62
+ const rel = path.slice(base.length);
63
+ if (!rel || rel.includes("/")) continue;
64
+ if (!rel.endsWith(extension)) continue;
65
+
66
+ const data = readInternalAssetBytes(path);
67
+ if (!data) continue;
68
+ const file = new MemoryRuntimeFile(path, data);
69
+ if (!real && this.realFiles[kind].some((f) => f.name === file.name)) {
70
+ this.fallbackFiles[kind].push(file);
71
+ continue;
72
+ }
73
+ this.files[kind].push(file);
74
+ if (real) this.realFiles[kind].push(file);
75
+ added = true;
76
+ }
77
+ return added;
78
+ }
79
+
50
80
  addMemoryFile(kind, name, data) {
51
81
  this.files[kind].push(new MemoryRuntimeFile(name, data));
52
82
  }
@@ -85,14 +115,20 @@ class MemoryRuntimeFile {
85
115
  this.name = basename(name, extname(name));
86
116
  this.path = name;
87
117
  this.real = false;
88
- this._data = String(data);
118
+ this._data = data;
89
119
  }
90
120
 
91
121
  async data() {
92
- return new TextEncoder().encode(this._data);
122
+ if (this._data instanceof Uint8Array) return this._data;
123
+ if (ArrayBuffer.isView(this._data)) {
124
+ return new Uint8Array(this._data.buffer, this._data.byteOffset, this._data.byteLength);
125
+ }
126
+ if (this._data instanceof ArrayBuffer) return new Uint8Array(this._data);
127
+ return new TextEncoder().encode(String(this._data));
93
128
  }
94
129
 
95
130
  async text() {
96
- return this._data;
131
+ if (typeof this._data === "string") return this._data;
132
+ return new TextDecoder().decode(await this.data());
97
133
  }
98
134
  }
@@ -0,0 +1,95 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import {
3
+ decodeBinaryStrict,
4
+ encodeBinary,
5
+ } from "../src/buffer/fixed3-codec.js";
6
+
7
+ const SIZES = [1024, 64 * 1024, 1024 * 1024];
8
+ const ITERATIONS = {
9
+ 1024: 20_000,
10
+ [64 * 1024]: 2_000,
11
+ [1024 * 1024]: 200,
12
+ };
13
+
14
+ function makeRandom(size) {
15
+ return randomBytes(size);
16
+ }
17
+
18
+ function makeAscii(size) {
19
+ const buf = Buffer.allocUnsafe(size);
20
+ for (let i = 0; i < size; i++) {
21
+ buf[i] = 0x20 + (i % 95);
22
+ }
23
+ return buf;
24
+ }
25
+
26
+ function mbps(bytes, ns) {
27
+ return (bytes / (1024 * 1024)) / (Number(ns) / 1e9);
28
+ }
29
+
30
+ function bench(name, fn, iters, payloadSize) {
31
+ const warmup = Math.min(200, Math.max(20, Math.floor(iters / 20)));
32
+ for (let i = 0; i < warmup; i++) fn();
33
+
34
+ const start = process.hrtime.bigint();
35
+ let sink = 0;
36
+ for (let i = 0; i < iters; i++) {
37
+ sink ^= fn();
38
+ }
39
+ const elapsed = process.hrtime.bigint() - start;
40
+
41
+ return {
42
+ name,
43
+ iters,
44
+ ms: Number(elapsed) / 1e6,
45
+ mbps: mbps(payloadSize * iters, elapsed),
46
+ sink,
47
+ };
48
+ }
49
+
50
+ function runCase(label, input) {
51
+ const fixedEncoded = encodeBinary(input);
52
+ const base64Encoded = input.toString("base64");
53
+
54
+ if (!decodeBinaryStrict(fixedEncoded).equals(input)) {
55
+ throw new Error(`hex3 roundtrip failed for ${label}`);
56
+ }
57
+ if (!Buffer.from(base64Encoded, "base64").equals(input)) {
58
+ throw new Error(`base64 roundtrip failed for ${label}`);
59
+ }
60
+
61
+ const iters = ITERATIONS[input.length] ?? Math.max(50, Math.floor(100 * 1024 * 1024 / input.length));
62
+
63
+ const results = [
64
+ bench(
65
+ "hex3 encode+decode",
66
+ () => {
67
+ const encoded = encodeBinary(input);
68
+ return decodeBinaryStrict(encoded).length;
69
+ },
70
+ iters,
71
+ input.length,
72
+ ),
73
+ bench(
74
+ "base64 encode+decode",
75
+ () => {
76
+ const encoded = input.toString("base64");
77
+ return Buffer.from(encoded, "base64").length;
78
+ },
79
+ iters,
80
+ input.length,
81
+ ),
82
+ ];
83
+
84
+ console.log(`\n${label} (${input.length.toLocaleString()} bytes, ${iters.toLocaleString()} iterations)`);
85
+ for (const r of results) {
86
+ console.log(
87
+ `${r.name.padEnd(22)} ${r.ms.toFixed(1).padStart(10)} ms ${r.mbps.toFixed(1).padStart(8)} MiB/s sink=${r.sink}`,
88
+ );
89
+ }
90
+ }
91
+
92
+ for (const size of SIZES) {
93
+ runCase("random", makeRandom(size));
94
+ runCase("ascii", makeAscii(size));
95
+ }