bunmicro 1.0.0 → 1.0.3

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/src/lua/engine.js CHANGED
@@ -1,21 +1,55 @@
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 wasmBytes = readInternalAssetBytes("node_modules/wasmoon/dist/glue.wasm");
34
+ if (wasmBytes) {
35
+ const wasmPath = join(tmpdir(), "bunmicro-wasmoon-glue.wasm");
36
+ try {
37
+ const existing = await Bun.file(wasmPath).bytes().catch(() => null);
38
+ if (!existing || existing.byteLength !== wasmBytes.byteLength) {
39
+ await Bun.write(wasmPath, wasmBytes);
40
+ }
41
+ return wasmPath;
42
+ } catch (error) {
43
+ console.error("# failed to stage bundled wasmoon wasm");
44
+ console.error(error);
45
+ }
18
46
  }
47
+
48
+ const fallbackPath = isCompiledBinary(process.argv)
49
+ ? join(resolveCompiledBaseDir({ argv: process.argv }), "node_modules", "wasmoon", "dist", "glue.wasm")
50
+ : join(SOURCE_ROOT, "node_modules", "wasmoon", "dist", "glue.wasm");
51
+
52
+ return existsSync(fallbackPath) ? fallbackPath : undefined;
19
53
  }
20
54
 
21
55
  class WasmoonEngine {
@@ -256,6 +256,9 @@ export function osc52Clipboard(stdout) {
256
256
  }
257
257
 
258
258
  export async function probeOSC52(ttyIn, ttyOut, timeoutMs) {
259
+ return true;
260
+ // Almost no terminal reliably supports probing OSC 52, so treat it as
261
+ // available and let the actual write path fail or work on its own.
259
262
  if (process.env.TMUX) return true;
260
263
  return new Promise((resolve) => {
261
264
  let done = false;
@@ -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
+ }