This commit is contained in:
2026-07-26 22:03:25 -05:00
parent e10f66fecf
commit d3de67fadb
9 changed files with 6091 additions and 28 deletions
+7 -3
View File
@@ -5,6 +5,7 @@ crates/*/target/
# Node.js
node_modules/
npm-debug.log*
package-lock.json
# Build artifacts
*.node
@@ -14,6 +15,12 @@ npm-debug.log*
*.d
*.pdb
# Benchmark results
benchmark/result-*.json
# Electron
electron/
# OS
.DS_Store
Thumbs.db
@@ -24,8 +31,5 @@ Thumbs.db
*.swp
*.swo
# Cargo
Cargo.lock
# napi-rs
napi-dist/
Generated
+5572
View File
File diff suppressed because it is too large Load Diff
+219
View File
@@ -0,0 +1,219 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Benchmark</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, system-ui, sans-serif;
background: #0d1117;
color: #e6edf3;
padding: 32px;
}
h1 { font-size: 22px; font-weight: 600; margin-bottom: 4px; }
.subtitle { color: #8b949e; font-size: 13px; margin-bottom: 28px; }
.section { margin-bottom: 24px; }
.section-title { font-size: 13px; font-weight: 600; color: #8b949e; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 10px; }
.card {
background: #161b22;
border: 1px solid #30363d;
border-radius: 8px;
padding: 16px 20px;
}
.row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid #21262d;
}
.row:last-child { border-bottom: none; }
.label { font-size: 14px; color: #c9d1d9; }
.value { font-size: 14px; font-weight: 600; font-variant-numeric: tabular-nums; }
.status { font-size: 14px; color: #8b949e; margin-bottom: 20px; }
.status .dot {
display: inline-block;
width: 8px; height: 8px;
border-radius: 50%;
margin-right: 8px;
vertical-align: middle;
}
.dot.running { background: #d29922; animation: pulse 1s infinite; }
.dot.done { background: #3fb950; }
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.4} }
#results { display: none; }
</style>
</head>
<body>
<h1 id="title">Benchmark</h1>
<p class="subtitle" id="subtitle"></p>
<div class="status"><span class="dot running" id="dot"></span><span id="status-text">Initializing…</span></div>
<div id="results">
<div class="section">
<div class="section-title">Renderer Performance</div>
<div class="card" id="perf-card"></div>
</div>
<div class="section">
<div class="section-title">Memory (Main Process)</div>
<div class="card" id="mem-card"></div>
</div>
</div>
<div id="target" style="position:absolute;left:-9999px"></div>
<script>
try {
var isG = !!(process.versions && process.versions.gelectron);
document.getElementById('title').textContent = (isG ? 'Gelectron' : 'Electron') + ' Benchmark';
document.getElementById('subtitle').textContent = process.platform + ' ' + process.arch + ' | Node ' + process.versions.node;
} catch(e) {}
function setStatus(t, done) {
document.getElementById('status-text').textContent = t;
document.getElementById('dot').className = done ? 'dot done' : 'dot running';
}
function bench(name, fn, iter) {
for (var i = 0; i < Math.min(iter, 50); i++) fn();
var times = [];
var bs = Math.max(1, Math.floor(iter / 10));
for (var b = 0; b < 10; b++) {
var t0 = performance.now();
for (var j = 0; j < bs; j++) fn();
times.push(performance.now() - t0);
}
var total = times.reduce(function(a,b){return a+b},0);
var ops = bs * 10;
return {
timeMs: +(total / 10).toFixed(2),
opsPerSec: +((ops / total) * 1000).toFixed(0)
};
}
function fmtOps(o) {
if (o >= 1e6) return (o/1e6).toFixed(2) + 'M';
if (o >= 1e3) return (o/1e3).toFixed(1) + 'K';
return '' + o;
}
function row(label, val) {
return '<div class="row"><span class="label">' + label + '</span><span class="value">' + val + '</span></div>';
}
(async function() {
var t = document.getElementById('target');
var tests = {};
var s = performance.now();
setStatus('DOM create / append…');
await new Promise(function(r){setTimeout(r,0)});
tests['DOM create/append'] = bench('dc', function() {
var el = document.createElement('div');
el.textContent = 'Hello';
t.appendChild(el);
t.removeChild(el);
}, 10000);
setStatus('DOM querySelector…');
await new Promise(function(r){setTimeout(r,0)});
t.innerHTML = '';
for (var i = 0; i < 1000; i++) {
var el = document.createElement('div');
el.dataset.idx = i;
t.appendChild(el);
}
tests['DOM querySelector'] = bench('dq', function() {
t.querySelector('[data-idx="500"]');
}, 30000);
setStatus('Style recalc…');
await new Promise(function(r){setTimeout(r,0)});
var sty = document.createElement('style');
sty.textContent = '.s{color:red;font-size:14px;padding:8px;margin:4px;border:1px solid #000}';
document.head.appendChild(sty);
t.innerHTML = '';
for (var i = 0; i < 500; i++) {
var el = document.createElement('div');
el.className = 's';
el.textContent = 'Item ' + i;
t.appendChild(el);
}
tests['Style recalc'] = bench('sr', function() {
var items = t.querySelectorAll('.s');
for (var k = 0; k < items.length; k++) {
items[k].classList.toggle('s');
void items[k].offsetHeight;
}
}, 5000);
setStatus('Canvas 2D draw…');
await new Promise(function(r){setTimeout(r,0)});
var canvas = document.createElement('canvas');
canvas.width = 800; canvas.height = 600;
document.body.appendChild(canvas);
var ctx = canvas.getContext('2d');
tests['Canvas 2D draw'] = bench('cv', function() {
ctx.fillStyle = '#4ecdc4';
ctx.fillRect(0, 0, 100, 100);
ctx.strokeStyle = '#ff6b6b';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(200, 200, 50, 0, Math.PI * 2);
ctx.stroke();
ctx.font = '16px sans-serif';
ctx.fillText('Benchmark', 300, 300);
}, 15000);
setStatus('JSON parse…');
await new Promise(function(r){setTimeout(r,0)});
var bigArr = [];
for (var i = 0; i < 500; i++) bigArr.push({id:i,vals:[Math.random(),Math.random()]});
var bigStr = JSON.stringify(bigArr);
tests['JSON parse'] = bench('js', function() { JSON.parse(bigStr); }, 20000);
setStatus('Array sort (10k)…');
await new Promise(function(r){setTimeout(r,0)});
tests['Array sort (10k)'] = bench('as', function() {
var a = [];
for (var i = 0; i < 10000; i++) a.push(Math.random());
a.sort(function(x,y){return x-y});
}, 500);
setStatus('Fibonacci(25)…');
await new Promise(function(r){setTimeout(r,0)});
function fib(n) { return n <= 1 ? n : fib(n-1) + fib(n-2); }
tests['Fibonacci(25)'] = bench('fb', function() { fib(25); }, 30);
setStatus('Collecting memory…', false);
await new Promise(function(r){setTimeout(r,0)});
var mem = {};
try {
var m = process.memoryUsage();
mem = {
RSS: (m.rss/1048576).toFixed(1) + ' MB',
'Heap used': (m.heapUsed/1048576).toFixed(1) + ' MB',
'Heap total': (m.heapTotal/1048576).toFixed(1) + ' MB',
External: (m.external/1048576).toFixed(1) + ' MB'
};
} catch(e) { mem = {Note: 'memory not available'}; }
var elapsed = ((performance.now() - s) / 1000).toFixed(1);
setStatus('Done in ' + elapsed + 's — close window to exit', true);
// Render
var perf = document.getElementById('perf-card');
var h = '';
for (var name in tests) {
h += row(name, fmtOps(tests[name].opsPerSec) + ' ops/s (' + tests[name].timeMs + ' ms)');
}
perf.innerHTML = h;
var mc = document.getElementById('mem-card');
h = '';
for (var k in mem) h += row(k, mem[k]);
mc.innerHTML = h;
document.getElementById('results').style.display = 'block';
})();
</script>
</body>
</html>
+22
View File
@@ -0,0 +1,22 @@
const { app, BrowserWindow } = require('electron');
const path = require('path');
const isGelectron = !!(process.versions.gelectron || process.env.GELECTRON_NATIVE);
app.whenReady().then(() => {
const win = new BrowserWindow({
width: 680,
height: 720,
show: true,
title: (isGelectron ? 'Gelectron' : 'Electron') + ' Benchmark',
webPreferences: {
contextIsolation: false,
nodeIntegration: true,
sandbox: false,
},
});
win.loadFile(path.join(__dirname, 'index.html'));
});
app.on('window-all-closed', () => app.quit());
+6
View File
@@ -0,0 +1,6 @@
{
"name": "gelectron-benchmark",
"version": "1.0.0",
"description": "Simple web benchmark for comparing Electron vs Gelectron",
"main": "main.js"
}
+79
View File
@@ -0,0 +1,79 @@
#!/usr/bin/env bash
set -e
BENCH_DIR="$(cd "$(dirname "$0")" && pwd)"
ELECTRON_BIN="$BENCH_DIR/node_modules/.bin/electron"
GEELECTRON_BIN="$BENCH_DIR/../target/release/gelectron"
usage() {
echo ""
echo " Usage: ./benchmark/run.sh [options]"
echo ""
echo " Options:"
echo " --electron Run with Electron only"
echo " --gelectron Run with Gelectron only"
echo " (no args) Run both back-to-back"
echo ""
}
run_electron() {
if [ -x "$ELECTRON_BIN" ] || command -v electron &>/dev/null; then
local bin="${ELECTRON_BIN:-electron}"
echo ""
echo " ▶ Opening Electron benchmark…"
echo " Close the window when done."
"$bin" "$BENCH_DIR" 2>/dev/null
echo " ✓ Electron done"
else
echo ""
echo " ✗ Electron not found. Install with: npm install electron"
fi
}
run_gelectron() {
if [ -x "$GEELECTRON_BIN" ]; then
echo ""
echo " ▶ Opening Gelectron benchmark…"
echo " Close the window when done."
"$GEELECTRON_BIN" "$BENCH_DIR" 2>/dev/null
echo " ✓ Gelectron done"
elif command -v gelectron &>/dev/null; then
echo ""
echo " ▶ Opening Gelectron benchmark…"
echo " Close the window when done."
gelectron "$BENCH_DIR" 2>/dev/null
echo " ✓ Gelectron done"
else
echo ""
echo " ✗ Gelectron not found. Build with: cargo build --release"
fi
}
MODE="both"
for arg in "$@"; do
case "$arg" in
--electron) MODE="electron" ;;
--gelectron) MODE="gelectron" ;;
-h|--help) usage; exit 0 ;;
*) echo " Unknown option: $arg"; usage; exit 1 ;;
esac
done
echo ""
echo " ── Gelectron Benchmark ──"
if [ "$MODE" = "both" ] || [ "$MODE" = "electron" ]; then
run_electron
fi
if [ "$MODE" = "both" ] || [ "$MODE" = "gelectron" ]; then
run_gelectron
fi
if [ "$MODE" = "both" ]; then
echo ""
echo " ── Done ──"
echo " Both benchmarks ran. Results are shown in each window."
echo " The second window auto-loads a comparison if result files exist."
echo ""
fi
+49 -18
View File
@@ -129,10 +129,14 @@ fn preload_script() -> String {
(function() {
if (window.__gelectron_loaded) return;
window.__gelectron_loaded = true;
window.__ipc_pending = [];
window.gelectron = {
send: function(channel, ...args) {
window.__ipc_pending.push(JSON.stringify({channel: channel, args: args}));
var payload = JSON.stringify({type:'ipc-send', channel: channel, args: args});
if (window.ipc && window.ipc.postMessage) {
window.ipc.postMessage(payload);
} else if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.ipc) {
window.webkit.messageHandlers.ipc.postMessage(payload);
}
},
receive: function(channel, callback) {
window.addEventListener('message', function(e) {
@@ -313,6 +317,7 @@ require('{}');
});
let event_loop = EventLoopBuilder::new().build();
let (ipc_tx, ipc_rx) = mpsc::channel::<ToNode>();
let mut state = AppState::new(node_exited.clone());
state.node_stdin = Some(child_stdin);
let state = Rc::new(RefCell::new(state));
@@ -325,7 +330,6 @@ require('{}');
// If the Node.js child process has exited, shut down
if st.node_exited.load(Ordering::SeqCst) {
log::info!("Node.js process exited, shutting down");
st.windows.clear();
st.window_wids.clear();
*control_flow = ControlFlow::Exit;
@@ -334,6 +338,10 @@ require('{}');
match event {
Event::NewEvents(StartCause::Poll) => {
// Drain IPC messages from webview → Node
while let Ok(msg) = ipc_rx.try_recv() {
st.send_to_node(&msg);
}
while let Ok(msg) = rx.try_recv() {
match msg {
ToRust::CreateWindow { id, options } => {
@@ -354,20 +362,33 @@ require('{}');
Ok(window) => {
let url = options.url.unwrap_or_else(|| "about:blank".into());
log::info!("Creating window {} - '{}'", id, url);
match WebViewBuilder::new()
.with_url(&url)
.with_initialization_script(&preload_script())
.with_devtools(true)
.build(&window)
{
Ok(webview) => {
let wid = window.id();
st.windows.insert(id, WindowPair { window, webview });
st.window_wids.insert(wid, id);
log::info!("Window {} ready", id);
let ipc_tx_clone = ipc_tx.clone();
let wid_for_ipc = id;
match WebViewBuilder::new()
.with_url(&url)
.with_initialization_script(&preload_script())
.with_devtools(true)
.with_ipc_handler(move |req| {
let body = req.body().to_string();
if let Ok(msg) = serde_json::from_str::<serde_json::Value>(&body) {
let msg_type = msg.get("type").and_then(|v| v.as_str()).unwrap_or("");
if msg_type == "ipc-send" {
let channel = msg.get("channel").and_then(|v| v.as_str()).unwrap_or("");
let args = msg.get("args").cloned().unwrap_or(serde_json::Value::Null);
let _ = ipc_tx_clone.send(ToNode::IpcMessage { id: wid_for_ipc, channel: channel.to_string(), data: args });
}
}
Err(e) => log::error!("WebView error: {}", e),
})
.build(&window)
{
Ok(webview) => {
let wid = window.id();
st.windows.insert(id, WindowPair { window, webview });
st.window_wids.insert(wid, id);
log::info!("Window {} ready", id);
}
Err(e) => log::error!("WebView error: {}", e),
}
}
Err(e) => log::error!("Window error: {}", e),
}
@@ -385,15 +406,25 @@ require('{}');
ToRust::LoadFile { id, path } => {
let url = format!("file://{}", std::fs::canonicalize(&path).unwrap_or_default().display());
log::info!("Loading file in window {}: {}", id, url);
// Rebuild the WebView with the correct file URL.
// evaluate_script navigation from about:blank to file:// is
// unreliable on macOS WKWebView, so we recreate the WebView.
if let Some(pair) = st.windows.get_mut(&id) {
let window = &pair.window;
let ipc_tx_clone = ipc_tx.clone();
let wid_for_ipc = id;
match WebViewBuilder::new()
.with_url(&url)
.with_initialization_script(&preload_script())
.with_devtools(true)
.with_ipc_handler(move |req| {
let body = req.body().to_string();
if let Ok(msg) = serde_json::from_str::<serde_json::Value>(&body) {
let msg_type = msg.get("type").and_then(|v| v.as_str()).unwrap_or("");
if msg_type == "ipc-send" {
let channel = msg.get("channel").and_then(|v| v.as_str()).unwrap_or("");
let args = msg.get("args").cloned().unwrap_or(serde_json::Value::Null);
let _ = ipc_tx_clone.send(ToNode::IpcMessage { id: wid_for_ipc, channel: channel.to_string(), data: args });
}
}
})
.build(window)
{
Ok(webview) => {
+131 -2
View File
@@ -7,16 +7,55 @@
"": {
"name": "gelectron",
"version": "0.1.0",
"hasInstallScript": true,
"license": "MIT",
"bin": {
"gelectron": "cli/gelectron.js"
},
"devDependencies": {
"@napi-rs/cli": "^3.0.0"
"@napi-rs/cli": "^3.0.0",
"electron": "^43.2.0"
},
"engines": {
"node": ">=18.0.0"
},
"optionalDependencies": {
"gelectron-darwin-arm64": "0.1.0",
"gelectron-darwin-x64": "0.1.0",
"gelectron-linux-arm64-gnu": "0.1.0",
"gelectron-linux-x64-gnu": "0.1.0",
"gelectron-win32-arm64-msvc": "0.1.0",
"gelectron-win32-x64-msvc": "0.1.0"
}
},
"node_modules/@electron-internal/extract-zip": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@electron-internal/extract-zip/-/extract-zip-1.0.4.tgz",
"integrity": "sha512-Zr1Vs7E9tpCNhZHDAbFVXc2gEVCG9RqPDjrno5+bdgB6LRAuvgyMHJut4NCVyYwtAieapMzc3fiQ3CSTi75ARg==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=22.12.0"
}
},
"node_modules/@electron/get": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@electron/get/-/get-5.0.0.tgz",
"integrity": "sha512-pjoBpru1KdEtcExBnuHAP1cAc/5faoedw0hzJkL3o4/IJp7HNF1+fbrdxT3gMYRX2oJfvnA/WXeCTVQpYYxyJA==",
"dev": true,
"license": "MIT",
"dependencies": {
"debug": "^4.1.1",
"env-paths": "^3.0.0",
"graceful-fs": "^4.2.11",
"progress": "^2.0.3",
"semver": "^7.6.3",
"sumchecker": "^3.0.1"
},
"engines": {
"node": ">=22.12.0"
},
"optionalDependencies": {
"undici": "^7.24.4"
}
},
"node_modules/@emnapi/core": {
@@ -1654,6 +1693,16 @@
"tslib": "^2.4.0"
}
},
"node_modules/@types/node": {
"version": "24.13.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz",
"integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~7.18.0"
}
},
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -1740,6 +1789,25 @@
}
}
},
"node_modules/electron": {
"version": "43.2.0",
"resolved": "https://registry.npmjs.org/electron/-/electron-43.2.0.tgz",
"integrity": "sha512-80zvrgG7ZRXD+tD0IyLvrnN9n+veSxadMRsMaC9wKKP3iUbtC7rGM8+dVuCmOb0Rrwwv8ESW4awnUZh9Hbp1fA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@electron-internal/extract-zip": "^1.0.1",
"@electron/get": "^5.0.0",
"@types/node": "^24.9.0"
},
"bin": {
"electron": "cli.js",
"install-electron": "install.js"
},
"engines": {
"node": ">= 22.12.0"
}
},
"node_modules/emnapi": {
"version": "1.11.3",
"resolved": "https://registry.npmjs.org/emnapi/-/emnapi-1.11.3.tgz",
@@ -1755,6 +1823,19 @@
}
}
},
"node_modules/env-paths": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz",
"integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/es-toolkit": {
"version": "1.50.0",
"resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.50.0.tgz",
@@ -1794,6 +1875,13 @@
"fast-string-width": "^3.0.2"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"dev": true,
"license": "ISC"
},
"node_modules/iconv-lite": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz",
@@ -1872,6 +1960,16 @@
"node": ">=12.20.0"
}
},
"node_modules/progress": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
@@ -1905,6 +2003,19 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/sumchecker": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz",
"integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"debug": "^4.1.0"
},
"engines": {
"node": ">= 8.0"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
@@ -1923,6 +2034,24 @@
"website"
]
},
"node_modules/undici": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz",
"integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">=20.18.1"
}
},
"node_modules/undici-types": {
"version": "7.18.2",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
"dev": true,
"license": "MIT"
},
"node_modules/universal-user-agent": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz",
+6 -5
View File
@@ -51,7 +51,8 @@
"author": "mileswa1q22",
"license": "MIT",
"devDependencies": {
"@napi-rs/cli": "^3.0.0"
"@napi-rs/cli": "^3.0.0",
"electron": "^43.2.0"
},
"engines": {
"node": ">=18.0.0"
@@ -59,9 +60,9 @@
"optionalDependencies": {
"gelectron-darwin-arm64": "0.1.0",
"gelectron-darwin-x64": "0.1.0",
"gelectron-win32-x64-msvc": "0.1.0",
"gelectron-win32-arm64-msvc": "0.1.0",
"gelectron-linux-arm64-gnu": "0.1.0",
"gelectron-linux-x64-gnu": "0.1.0",
"gelectron-linux-arm64-gnu": "0.1.0"
"gelectron-win32-arm64-msvc": "0.1.0",
"gelectron-win32-x64-msvc": "0.1.0"
}
}
}