Add CI publish workflow

This commit is contained in:
2026-08-30 22:09:54 -05:00
parent 48a994db09
commit f09c02ec30
16 changed files with 680 additions and 146 deletions
+1
View File
@@ -0,0 +1 @@
1db4284c-fc55-41be-a7dd-cde2036997da
+129
View File
@@ -0,0 +1,129 @@
name: Publish to npm
on:
push:
tags:
- 'v*'
jobs:
build:
name: Build ${{ matrix.target }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
- os: ubuntu-latest
target: aarch64-unknown-linux-gnu
- os: macos-latest
target: aarch64-apple-darwin
- os: macos-latest
target: x86_64-apple-darwin
- os: windows-latest
target: x86_64-pc-windows-msvc
- os: windows-latest
target: aarch64-pc-windows-msvc
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 18
registry-url: 'https://registry.npmjs.org'
- name: Install dependencies
run: npm ci
- name: Build N-API addon for ${{ matrix.target }}
run: napi build --platform --release --target ${{ matrix.target }} --package gelectron-core
- name: Rename .node file with platform-specific name
shell: bash
run: |
case "${{ matrix.target }}" in
x86_64-apple-darwin) name="gelectron_core.darwin-x64.node" ;;
aarch64-apple-darwin) name="gelectron_core.darwin-arm64.node" ;;
x86_64-pc-windows-msvc) name="gelectron_core.win32-x64-msvc.node" ;;
aarch64-pc-windows-msvc) name="gelectron_core.win32-arm64-msvc.node" ;;
x86_64-unknown-linux-gnu) name="gelectron_core.linux-x64-gnu.node" ;;
aarch64-unknown-linux-gnu) name="gelectron_core.linux-arm64-gnu.node" ;;
esac
find crates/gelectron-core -name "*.node" -exec mv {} "$name" \;
ls -la *.node
- name: Upload .node artifact
uses: actions/upload-artifact@v4
with:
name: bindings-${{ matrix.target }}
path: gelectron_core.*.node
publish:
name: Publish to npm
needs: build
runs-on: ubuntu-latest
environment: npm-publish
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 18
registry-url: 'https://registry.npmjs.org'
- name: Download all platform packages
uses: actions/download-artifact@v4
with:
path: artifacts
pattern: bindings-*
merge-multiple: true
- name: Install dependencies
run: npm ci
- name: Assemble platform packages
shell: bash
run: |
for node in artifacts/*.node; do
base="$(basename "$node")"
case "$base" in
*.darwin-x64.node) dir="npm/darwin-x64" ;;
*.darwin-arm64.node) dir="npm/darwin-arm64" ;;
*.win32-x64-msvc.node) dir="npm/win32-x64-msvc" ;;
*.win32-arm64-msvc.node) dir="npm/win32-arm64-msvc" ;;
*.linux-x64-gnu.node) dir="npm/linux-x64-gnu" ;;
*.linux-arm64-gnu.node) dir="npm/linux-arm64-gnu" ;;
esac
cp "$node" "$dir/$base"
done
echo "Assembled platform packages:"
ls -la npm/*/
- name: Publish platform packages
shell: bash
run: |
for dir in npm/*/; do
cd "$dir"
npm publish --access public
cd ../../
done
- name: Publish main package
run: |
npm run prepublishOnly
npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+169
View File
@@ -316,6 +316,175 @@ cargo build --release -p gelectron-core
The N-API addon compiles to a `.node` file that can be loaded directly into Node.js. The N-API addon compiles to a `.node` file that can be loaded directly into Node.js.
## Publishing to npm
Gelectron uses [napi-rs](https://napi.rs/) to produce platform-specific native addons. The main `gelectron` npm package ships platform-specific optional packages so that `npm install gelectron` automatically pulls the right binary for the user's OS.
### Prerequisites
- Rust 1.75+ (`rustup.rs`)
- Node.js 18+
- npm
- An [npm account](https://www.npmjs.com/signup) with publish access
- Each target platform needs to be built on that platform (or via CI)
### Step 1: Build the native addon for your platform
```bash
# Build the N-API addon (produces crates/gelectron-core/*.node)
npm run build
# Or build with debug symbols for development
npm run build:debug
```
This compiles the Rust N-API addon (`gelectron-core`) into a `.node` file that Node.js can load.
### Step 2: Create platform-specific npm packages
For each platform you want to support, create a directory under `npm/` with a `package.json`:
```bash
# Example for macOS ARM64
mkdir -p npm/darwin-arm64
cat > npm/darwin-arm64/package.json << 'EOF'
{
"name": "gelectron-darwin-arm64",
"version": "0.1.0",
"description": "Gelectron native addon for macOS ARM64",
"main": "index.darwin-arm64.node",
"files": ["index.darwin-arm64.node"],
"os": ["darwin"],
"cpu": ["arm64"],
"license": "MIT"
}
EOF
# Copy the built .node file
cp crates/gelectron-core/gelectron_core.darwin-arm64.node npm/darwin-arm64/
```
Repeat for each platform:
| Directory | os | cpu |
|---|---|---|
| `npm/darwin-arm64/` | `darwin` | `arm64` |
| `npm/darwin-x64/` | `darwin` | `x64` |
| `npm/win32-x64-msvc/` | `win32` | `x64` |
| `npm/win32-arm64-msvc/` | `win32` | `arm64` |
| `npm/linux-x64-gnu/` | `linux` | `x64` |
| `npm/linux-arm64-gnu/` | `linux` | `arm64` |
### Step 3: Publish platform packages first
Each platform package must be published before the main package:
```bash
# Publish each platform package
npm publish npm/darwin-arm64 --access public
npm publish npm/darwin-x64 --access public
npm publish npm/win32-x64-msvc --access public
# ... etc for each platform
```
### Step 4: Prepare and publish the main package
```bash
# Run prepublish hook (generates napi artifacts metadata)
npm run prepublishOnly
# Publish the main package
npm publish --access public
```
### Using napi-rs CLI (recommended)
The `@napi-rs/cli` handles cross-compilation and artifact management:
```bash
# Install napi-rs CLI globally (if not already installed)
npm install -g @napi-rs/cli
# Build for all configured targets
napi build --platform --release
# Generate artifact metadata for npm publishing
napi prepublish -t npm
# Create a GitHub release with platform binaries
napi artifacts
```
### CI/CD Publishing (recommended)
For multi-platform publishing, use GitHub Actions to build on each OS:
```yaml
# .github/workflows/publish.yml
name: Publish to npm
on:
push:
tags: ['v*']
jobs:
build:
strategy:
matrix:
include:
- os: macos-latest
target: aarch64-apple-darwin
- os: macos-latest
target: x86_64-apple-darwin
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
- os: windows-latest
target: x86_64-pc-windows-msvc
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- uses: actions/setup-node@v4
with:
node-version: 18
- run: npm ci
- run: napi build --platform --release --target ${{ matrix.target }}
- run: napi prepublish -t npm
- uses: actions/upload-artifact@v4
with:
name: bindings-${{ matrix.target }}
path: npm/
publish:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
- run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
```
### Quick publish (single platform)
If you only need to publish for your current platform:
```bash
# Build
npm run build
# Preview what will be published
npm pack --dry-run
# Publish
npm run prepublishOnly
npm publish --access public
```
> **Tip:** Use `npm pack` to create a tarball locally and inspect it before publishing. Run `npm pack` and then `tar -tzf gelectron-0.1.0.tgz` to verify the contents.
## Testing with OmniEmu2.0 ## Testing with OmniEmu2.0
OmniEmu2.0 is a full Electron app used to validate Gelectron compatibility: OmniEmu2.0 is a full Electron app used to validate Gelectron compatibility:
+4 -1
View File
@@ -2,5 +2,8 @@
"name": "gelectron-benchmark", "name": "gelectron-benchmark",
"version": "1.0.0", "version": "1.0.0",
"description": "Simple web benchmark for comparing Electron vs Gelectron", "description": "Simple web benchmark for comparing Electron vs Gelectron",
"main": "main.js" "main": "main.js",
"dependencies": {
"electron": "^43.4.0"
}
} }
@@ -1,28 +0,0 @@
{
"timestamp": "2026-07-29T03:22:15Z",
"runs": 10,
"platform": "Darwin arm64",
"electron": {
"version": "v43.2.0",
"avg_startup_s": 5.671,
"min_startup_s": 5.668,
"max_startup_s": 5.674,
"avg_memory_mb": 585.9,
"min_memory_mb": 583.3,
"max_memory_mb": 587.4,
"runtime_size_mb": 296,
"raw_times": [5.670,5.672,5.674,5.672,5.671,5.668,5.670,5.668,5.673,5.672],
"raw_memory": [583.7,586.5,583.3,587.1,586.5,587.4,586.5,587.1,584.1,586.3]
},
"gelectron": {
"avg_startup_s": 5.601,
"min_startup_s": 5.598,
"max_startup_s": 5.604,
"avg_memory_mb": 131.4,
"min_memory_mb": 131.0,
"max_memory_mb": 131.7,
"runtime_size_mb": 3,
"raw_times": [5.600,5.601,5.602,5.602,5.598,5.600,5.601,5.602,5.602,5.604],
"raw_memory": [131.6,131.7,131.3,131.5,131.3,131.7,131.0,131.5,131.3,131.5]
}
}
@@ -0,0 +1,28 @@
{
"timestamp": "2026-08-14T21:54:50Z",
"runs": 10,
"platform": "Darwin arm64",
"electron": {
"version": "v43.2.0",
"avg_startup_s": 5.670,
"min_startup_s": 5.651,
"max_startup_s": 5.676,
"avg_memory_mb": 586.3,
"min_memory_mb": 574.0,
"max_memory_mb": 590.7,
"runtime_size_mb": 0,
"raw_times": [5.651,5.676,5.663,5.673,5.675,5.672,5.672,5.672,5.670,5.673],
"raw_memory": [584.1,590.5,574.0,589.4,590.7,589.5,589.1,586.9,588.9,580.3]
},
"gelectron": {
"avg_startup_s": 5.603,
"min_startup_s": 5.601,
"max_startup_s": 5.606,
"avg_memory_mb": 142.6,
"min_memory_mb": 142.3,
"max_memory_mb": 142.9,
"runtime_size_mb": 3,
"raw_times": [5.606,5.601,5.604,5.604,5.605,5.601,5.604,5.604,5.603,5.601],
"raw_memory": [142.8,142.8,142.5,142.5,142.9,142.7,142.6,142.3,142.8,142.5]
}
}
+104 -105
View File
@@ -11,6 +11,13 @@ use std::sync::mpsc;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::thread; use std::thread;
use std::sync::Arc; use std::sync::Arc;
use std::time::{Duration, Instant};
// Heartbeat interval for the event loop. We avoid ControlFlow::Poll (which
// busy-spins at 100% CPU when idle) and instead wake the loop on a fixed
// cadence to drain IPC channels, while still responding immediately to real
// window events.
const POLL_INTERVAL: Duration = Duration::from_millis(16);
use tao::event::{Event, StartCause, WindowEvent}; use tao::event::{Event, StartCause, WindowEvent};
use tao::event_loop::{ControlFlow, EventLoopBuilder}; use tao::event_loop::{ControlFlow, EventLoopBuilder};
use tao::window::{Fullscreen, WindowBuilder, WindowId}; use tao::window::{Fullscreen, WindowBuilder, WindowId};
@@ -236,7 +243,7 @@ struct NotificationOpts {
struct WindowPair { struct WindowPair {
#[allow(dead_code)] #[allow(dead_code)]
window: tao::window::Window, window: tao::window::Window,
webview: WebView, webview: Option<WebView>,
} }
struct AppState { struct AppState {
@@ -548,7 +555,7 @@ require('{}');
} }
event_loop.run(move |event, event_loop_target, control_flow| { event_loop.run(move |event, event_loop_target, control_flow| {
*control_flow = ControlFlow::Poll; *control_flow = ControlFlow::WaitUntil(Instant::now() + POLL_INTERVAL);
let mut st = state.borrow_mut(); let mut st = state.borrow_mut();
if st.node_exited.load(Ordering::SeqCst) { if st.node_exited.load(Ordering::SeqCst) {
@@ -559,7 +566,7 @@ require('{}');
} }
match event { match event {
Event::NewEvents(StartCause::Poll) => { Event::NewEvents(StartCause::Poll | StartCause::ResumeTimeReached { .. }) => {
// Drain async responses from background threads (dialogs, clipboard, etc.) // Drain async responses from background threads (dialogs, clipboard, etc.)
while let Ok((request_id, result)) = response_rx.try_recv() { while let Ok((request_id, result)) = response_rx.try_recv() {
st.send_to_node(&ToNode::Response { st.send_to_node(&ToNode::Response {
@@ -650,22 +657,24 @@ window.__gelectron_run_main(`{}`);
} }
event_loop.run(move |event, event_loop_target, control_flow| { event_loop.run(move |event, event_loop_target, control_flow| {
*control_flow = ControlFlow::Poll; *control_flow = ControlFlow::WaitUntil(Instant::now() + POLL_INTERVAL);
let mut st = state.borrow_mut(); let mut st = state.borrow_mut();
match event { match event {
Event::NewEvents(StartCause::Poll) => { Event::NewEvents(StartCause::Poll | StartCause::ResumeTimeReached { .. }) => {
// Drain async responses from background threads (dialogs, clipboard, etc.) // Drain async responses from background threads (dialogs, clipboard, etc.)
while let Ok((request_id, result)) = response_rx.try_recv() { while let Ok((request_id, result)) = response_rx.try_recv() {
if let Some(pair) = st.windows.get(&1u32) { if let Some(pair) = st.windows.get(&1u32) {
if let Some(webview) = &pair.webview {
let js = format!( let js = format!(
"window.__gelectron_response('{}', {});", "window.__gelectron_response('{}', {});",
request_id, request_id,
serde_json::to_string(&result).unwrap_or_default() serde_json::to_string(&result).unwrap_or_default()
); );
let _ = pair.webview.evaluate_script(&js); let _ = webview.evaluate_script(&js);
} }
} }
}
// Drain IPC messages from webview → forward back into the same webview // Drain IPC messages from webview → forward back into the same webview
while let Ok(msg) = ipc_rx.try_recv() { while let Ok(msg) = ipc_rx.try_recv() {
@@ -690,9 +699,9 @@ window.__gelectron_run_main(`{}`);
serde_json::json!({"__from_node":true,"type":"notification-event","id":id,"event":event,"action_index":action_index,"action":action,"reply":reply}) serde_json::json!({"__from_node":true,"type":"notification-event","id":id,"event":event,"action_index":action_index,"action":action,"reply":reply})
} }
}; };
let _ = pair.webview.evaluate_script( let _ = pair.webview.as_ref().map(|wv| wv.evaluate_script(
&format!("window.postMessage({},'*');", serde_json::to_string(&js_msg).unwrap()) &format!("window.postMessage({},'*');", serde_json::to_string(&js_msg).unwrap())
); ));
} }
} }
@@ -719,7 +728,7 @@ window.__gelectron_run_main(`{}`);
let js = serde_json::to_string(&serde_json::json!({ let js = serde_json::to_string(&serde_json::json!({
"__from_node": true, "type": "window-closed", "id": id, "__from_node": true, "type": "window-closed", "id": id,
})).unwrap(); })).unwrap();
let _ = pair.webview.evaluate_script(&format!("window.postMessage({},'*');", js)); let _ = pair.webview.as_ref().map(|wv| wv.evaluate_script(&format!("window.postMessage({},'*');", js)));
} }
st.windows.remove(&id); st.windows.remove(&id);
st.window_wids.remove(&window_id); st.window_wids.remove(&window_id);
@@ -733,7 +742,7 @@ window.__gelectron_run_main(`{}`);
let js = serde_json::to_string(&serde_json::json!({ let js = serde_json::to_string(&serde_json::json!({
"__from_node": true, "type": "window-focus", "id": id, "__from_node": true, "type": "window-focus", "id": id,
})).unwrap(); })).unwrap();
let _ = pair.webview.evaluate_script(&format!("window.postMessage({},'*');", js)); let _ = pair.webview.as_ref().map(|wv| wv.evaluate_script(&format!("window.postMessage({},'*');", js)));
} }
} }
} }
@@ -742,6 +751,47 @@ window.__gelectron_run_main(`{}`);
}); });
} }
fn create_webview(
window: &tao::window::Window,
url: &str,
init: &str,
id: u32,
ipc_tx: &mpsc::Sender<ToNode>,
to_rust_tx: Option<&Arc<mpsc::Sender<ToRust>>>,
) -> wry::Result<WebView> {
let ipc_tx_clone = ipc_tx.clone();
let to_rust_tx_clone = to_rust_tx.cloned();
let wid_for_ipc = id;
WebViewBuilder::new()
.with_url(url)
.with_initialization_script(init)
.with_devtools(false)
.with_ipc_handler(move |req| {
let body = req.body().to_string();
if let Ok(val) = serde_json::from_str::<serde_json::Value>(&body) {
// Try parsing as a ToRust command (load-file, load-url, etc.)
if let Some(ref tx) = to_rust_tx_clone {
if let Ok(cmd) = serde_json::from_value::<ToRust>(val.clone()) {
let _ = tx.send(cmd);
return;
}
}
// Fall back to ipc-send handling
let msg_type = val.get("type").and_then(|v| v.as_str()).unwrap_or("");
if msg_type == "ipc-send" {
let channel = val.get("channel").and_then(|v| v.as_str()).unwrap_or("");
let args = val.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 });
} else if msg_type == "quit" {
if let Some(ref tx) = to_rust_tx_clone {
let _ = tx.send(ToRust::Quit);
}
}
}
})
.build(window)
}
fn create_initial_webview_window( fn create_initial_webview_window(
event_loop_target: &tao::event_loop::EventLoopWindowTarget<()>, event_loop_target: &tao::event_loop::EventLoopWindowTarget<()>,
state: &Rc<RefCell<AppState>>, state: &Rc<RefCell<AppState>>,
@@ -783,7 +833,7 @@ fn create_initial_webview_window(
Ok(webview) => { Ok(webview) => {
let wid = window.id(); let wid = window.id();
let mut st = state.borrow_mut(); let mut st = state.borrow_mut();
st.windows.insert(window_id, WindowPair { window, webview }); st.windows.insert(window_id, WindowPair { window, webview: Some(webview) });
st.window_wids.insert(wid, window_id); st.window_wids.insert(wid, window_id);
log::info!("Initial WebView window created (running compat layer)"); log::info!("Initial WebView window created (running compat layer)");
} }
@@ -1459,58 +1509,32 @@ fn handle_to_rust(
Ok(window) => { Ok(window) => {
let url = options.url.unwrap_or_else(|| "about:blank".into()); let url = options.url.unwrap_or_else(|| "about:blank".into());
log::info!("Creating window {} - '{}'", id, url); log::info!("Creating window {} - '{}'", id, url);
let ipc_tx_clone = ipc_tx.clone();
let to_rust_tx_clone = to_rust_tx.cloned();
let wid_for_ipc = id;
let init = st.bundle_js.clone().unwrap_or_else(|| preload_script()); let init = st.bundle_js.clone().unwrap_or_else(|| preload_script());
match WebViewBuilder::new() // Build the WebView immediately (about:blank). Init scripts
.with_url(&url) // and the ipc message handler are registered at build time
.with_initialization_script(&init) // and survive subsequent in-place navigations (LoadUrl/LoadFile).
.with_devtools(false) let webview = match create_webview(&window, &url, &init, id, ipc_tx, to_rust_tx) {
.with_ipc_handler(move |req| { Ok(wv) => Some(wv),
let body = req.body().to_string(); Err(e) => {
if let Ok(val) = serde_json::from_str::<serde_json::Value>(&body) { log::error!("WebView error: {}", e);
// Try parsing as a ToRust command (load-file, load-url, etc.) None
if let Some(ref tx) = to_rust_tx_clone {
if let Ok(cmd) = serde_json::from_value::<ToRust>(val.clone()) {
let _ = tx.send(cmd);
return;
}
}
// Fall back to ipc-send handling
let msg_type = val.get("type").and_then(|v| v.as_str()).unwrap_or("");
if msg_type == "ipc-send" {
let channel = val.get("channel").and_then(|v| v.as_str()).unwrap_or("");
let args = val.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 });
} else if msg_type == "quit" {
if let Some(ref tx) = to_rust_tx_clone {
let _ = tx.send(ToRust::Quit);
}
}
}
})
.build(&window)
{
Ok(webview) => {
let wid = window.id();
if let Some(icon) = options
.icon
.as_deref()
.and_then(decode_base64_icon)
{
st.app_icon = Some(icon.clone());
apply_dock_icon(&icon);
apply_window_icon(&window, &icon);
} else if let Some(icon) = st.app_icon.clone() {
apply_window_icon(&window, &icon);
}
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), };
let wid = window.id();
if let Some(icon) = options
.icon
.as_deref()
.and_then(decode_base64_icon)
{
st.app_icon = Some(icon.clone());
apply_dock_icon(&icon);
apply_window_icon(&window, &icon);
} else if let Some(icon) = st.app_icon.clone() {
apply_window_icon(&window, &icon);
} }
st.windows.insert(id, WindowPair { window, webview });
st.window_wids.insert(wid, id);
log::info!("Window {} ready", id);
} }
Err(e) => log::error!("Window error: {}", e), Err(e) => log::error!("Window error: {}", e),
} }
@@ -1518,10 +1542,12 @@ fn handle_to_rust(
ToRust::LoadUrl { id, url } => { ToRust::LoadUrl { id, url } => {
log::info!("Loading url in window {}: {}", id, url); log::info!("Loading url in window {}: {}", id, url);
if let Some(pair) = st.windows.get(&id) { if let Some(pair) = st.windows.get(&id) {
let _ = pair.webview.evaluate_script(&format!( if let Some(webview) = &pair.webview {
"window.location.replace({});", let _ = webview.evaluate_script(&format!(
serde_json::to_string(&url).unwrap() "window.location.replace({});",
)); serde_json::to_string(&url).unwrap()
));
}
} }
} }
ToRust::LoadFile { id, path } => { ToRust::LoadFile { id, path } => {
@@ -1529,44 +1555,17 @@ fn handle_to_rust(
.map(|u| u.to_string()) .map(|u| u.to_string())
.unwrap_or_else(|_| "about:blank".into()); .unwrap_or_else(|_| "about:blank".into());
log::info!("Loading file in window {}: {}", id, url); log::info!("Loading file in window {}: {}", id, url);
let init = st.bundle_js.clone().unwrap_or_else(|| preload_script()); // Navigate in-place instead of rebuilding the WebView. Rebuilding on
if let Some(pair) = st.windows.get_mut(&id) { // macOS creates a stray 500x500 NSWindow artifact when the target
let window = &pair.window; // window is still hidden (the vanilla app loads its splash this way).
let ipc_tx_clone = ipc_tx.clone(); // The WKUserScript init script re-runs on navigation, so window.gelectron
let to_rust_tx_clone = to_rust_tx.cloned(); // (and any bundle) is re-established on the new document.
let wid_for_ipc = id; if let Some(pair) = st.windows.get(&id) {
match WebViewBuilder::new() if let Some(webview) = &pair.webview {
.with_url(&url) let _ = webview.evaluate_script(&format!(
.with_initialization_script(&init) "window.location.replace({});",
.with_devtools(false) serde_json::to_string(&url).unwrap()
.with_ipc_handler(move |req| { ));
let body = req.body().to_string();
if let Ok(val) = serde_json::from_str::<serde_json::Value>(&body) {
if let Some(ref tx) = to_rust_tx_clone {
if let Ok(cmd) = serde_json::from_value::<ToRust>(val.clone()) {
let _ = tx.send(cmd);
return;
}
}
let msg_type = val.get("type").and_then(|v| v.as_str()).unwrap_or("");
if msg_type == "ipc-send" {
let channel = val.get("channel").and_then(|v| v.as_str()).unwrap_or("");
let args = val.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 });
} else if msg_type == "quit" {
if let Some(ref tx) = to_rust_tx_clone {
let _ = tx.send(ToRust::Quit);
}
}
}
})
.build(window)
{
Ok(webview) => {
pair.webview = webview;
log::info!("WebView rebuilt for window {}", id);
}
Err(e) => log::error!("WebView rebuild error: {}", e),
} }
} }
} }
@@ -1584,12 +1583,12 @@ fn handle_to_rust(
if let Some(pair) = st.windows.get(&id) { if let Some(pair) = st.windows.get(&id) {
let msg = serde_json::json!({"__from_node":true,"channel":channel,"data":data}); let msg = serde_json::json!({"__from_node":true,"channel":channel,"data":data});
let js = format!("window.postMessage({},'*');", serde_json::to_string(&msg).unwrap()); let js = format!("window.postMessage({},'*');", serde_json::to_string(&msg).unwrap());
let _ = pair.webview.evaluate_script(&js); let _ = pair.webview.as_ref().map(|wv| wv.evaluate_script(&js));
} }
} }
ToRust::EvalJs { id, script } => { ToRust::EvalJs { id, script } => {
if let Some(pair) = st.windows.get(&id) { if let Some(pair) = st.windows.get(&id) {
let _ = pair.webview.evaluate_script(&script); let _ = pair.webview.as_ref().map(|wv| wv.evaluate_script(&script));
} }
} }
ToRust::Quit => { ToRust::Quit => {
+7 -12
View File
@@ -1,16 +1,11 @@
{ {
"name": "gelectron-darwin-arm64", "name": "gelectron-darwin-arm64",
"version": "0.1.0", "version": "0.1.0",
"description": "Gelectron native binary for macOS ARM64", "description": "Gelectron native addon for macOS ARM64",
"main": "gelectron_core.darwin-arm64.node", "main": "gelectron_core.darwin-arm64.node",
"files": [ "files": ["gelectron_core.darwin-arm64.node"],
"gelectron_core.darwin-arm64.node" "os": ["darwin"],
], "cpu": ["arm64"],
"os": [ "license": "MIT",
"darwin" "author": "mileswa1q22"
], }
"cpu": [
"arm64"
],
"license": "MIT"
}
+11
View File
@@ -0,0 +1,11 @@
{
"name": "gelectron-darwin-x64",
"version": "0.1.0",
"description": "Gelectron native addon for macOS x64",
"main": "gelectron_core.darwin-x64.node",
"files": ["gelectron_core.darwin-x64.node"],
"os": ["darwin"],
"cpu": ["x64"],
"license": "MIT",
"author": "mileswa1q22"
}
+11
View File
@@ -0,0 +1,11 @@
{
"name": "gelectron-linux-arm64-gnu",
"version": "0.1.0",
"description": "Gelectron native addon for Linux ARM64 (GNU)",
"main": "gelectron_core.linux-arm64-gnu.node",
"files": ["gelectron_core.linux-arm64-gnu.node"],
"os": ["linux"],
"cpu": ["arm64"],
"license": "MIT",
"author": "mileswa1q22"
}
+11
View File
@@ -0,0 +1,11 @@
{
"name": "gelectron-linux-x64-gnu",
"version": "0.1.0",
"description": "Gelectron native addon for Linux x64 (GNU)",
"main": "gelectron_core.linux-x64-gnu.node",
"files": ["gelectron_core.linux-x64-gnu.node"],
"os": ["linux"],
"cpu": ["x64"],
"license": "MIT",
"author": "mileswa1q22"
}
+11
View File
@@ -0,0 +1,11 @@
{
"name": "gelectron-win32-arm64-msvc",
"version": "0.1.0",
"description": "Gelectron native addon for Windows ARM64",
"main": "gelectron_core.win32-arm64-msvc.node",
"files": ["gelectron_core.win32-arm64-msvc.node"],
"os": ["win32"],
"cpu": ["arm64"],
"license": "MIT",
"author": "mileswa1q22"
}
+11
View File
@@ -0,0 +1,11 @@
{
"name": "gelectron-win32-x64-msvc",
"version": "0.1.0",
"description": "Gelectron native addon for Windows x64",
"main": "gelectron_core.win32-x64-msvc.node",
"files": ["gelectron_core.win32-x64-msvc.node"],
"os": ["win32"],
"cpu": ["x64"],
"license": "MIT",
"author": "mileswa1q22"
}
+13
View File
@@ -1875,6 +1875,19 @@
"fast-string-width": "^3.0.2" "fast-string-width": "^3.0.2"
} }
}, },
"node_modules/gelectron-darwin-arm64": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/gelectron-darwin-arm64/-/gelectron-darwin-arm64-0.1.0.tgz",
"integrity": "sha512-K0bmA9pfSNlA7CLPD+BOBNdvb0Cb8qMWcYry9BYuP3DC0gcL8Gfq1iM2h3CHNKJB/YEZdBA0uCgTd3+0SMvQ3w==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/graceful-fs": { "node_modules/graceful-fs": {
"version": "4.2.11", "version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+102
View File
@@ -0,0 +1,102 @@
#!/usr/bin/env bash
#
# build-npm.sh
#
# Builds the gelectron-core N-API addon for every supported platform target
# and copies the resulting .node files into the corresponding npm/ package
# folders.
#
# Each target is attempted. If the toolchain or linker isn't available,
# it's skipped with a warning. To build more targets:
# - Linux: brew install mingw-w64 (for Windows), or use Docker/cross
# - Run this script on each target OS for best results
#
set -eo pipefail
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$REPO_DIR"
HOST_TARGET="$(rustc -vV | grep '^host:' | awk '{print $2}')"
HAS_RUSTUP=false
command -v rustup &>/dev/null && HAS_RUSTUP=true
NAPI="$REPO_DIR/node_modules/.bin/napi"
if [ ! -x "$NAPI" ]; then
echo "Error: napi not found. Run 'npm install' first."
exit 1
fi
# Each line: target_triple|npm_folder|node_filename
TARGETS=(
"aarch64-apple-darwin|npm/darwin-arm64|gelectron_core.darwin-arm64.node"
"x86_64-apple-darwin|npm/darwin-x64|gelectron_core.darwin-x64.node"
"x86_64-pc-windows-msvc|npm/win32-x64-msvc|gelectron_core.win32-x64-msvc.node"
"aarch64-pc-windows-msvc|npm/win32-arm64-msvc|gelectron_core.win32-arm64-msvc.node"
"x86_64-unknown-linux-gnu|npm/linux-x64-gnu|gelectron_core.linux-x64-gnu.node"
"aarch64-unknown-linux-gnu|npm/linux-arm64-gnu|gelectron_core.linux-arm64-gnu.node"
)
built=0
skipped=0
echo "═══════════════════════════════════════════════════"
echo " Gelectron NPM Builder"
echo "═══════════════════════════════════════════════════"
echo " Host target: $HOST_TARGET"
echo " rustup: $HAS_RUSTUP"
echo "═══════════════════════════════════════════════════"
echo ""
for entry in "${TARGETS[@]}"; do
target="$(echo "$entry" | cut -d'|' -f1)"
dest_dir="$(echo "$entry" | cut -d'|' -f2)"
node_name="$(echo "$entry" | cut -d'|' -f3)"
echo "── $target ──"
# Add the Rust target
if $HAS_RUSTUP; then
if ! rustup target add "$target" 2>/dev/null; then
echo " ⚠ Could not install Rust target — skipping"
skipped=$((skipped + 1))
echo ""
continue
fi
elif [ "$target" != "$HOST_TARGET" ]; then
echo " ⚠ No rustup and not host target — skipping"
echo " Install rustup: brew install rustup && rustup-init"
skipped=$((skipped + 1))
echo ""
continue
fi
# Build
echo " Building..."
if $NAPI build --platform --release --target "$target" --package gelectron-core 2>/dev/null; then
node_file="crates/gelectron-core/${node_name}"
if [ ! -f "$node_file" ]; then
node_file=$(ls crates/gelectron-core/gelectron_core.*.node 2>/dev/null | head -1 || true)
fi
if [ -n "$node_file" ] && [ -f "$node_file" ]; then
mkdir -p "$dest_dir"
cp "$node_file" "$dest_dir/$node_name"
size=$(ls -lh "$dest_dir/$node_name" | awk '{print $5}')
echo " ✓ Built ($size) → $dest_dir/$node_name"
built=$((built + 1))
else
echo " ✗ Build succeeded but .node file not found"
skipped=$((skipped + 1))
fi
else
echo " ✗ Build failed (missing linker or toolchain)"
skipped=$((skipped + 1))
fi
echo ""
done
echo "═══════════════════════════════════════════════════"
echo " Done: $built built, $skipped skipped"
echo "═══════════════════════════════════════════════════"
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env bash
#
# publish-npm.sh
#
# Publishes all platform packages and the main gelectron package to npm.
# Run this after build-npm.sh has placed .node files in the npm/ folders.
#
set -eo pipefail
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$REPO_DIR"
PLATFORMS=(
"npm/darwin-arm64"
"npm/darwin-x64"
"npm/win32-x64-msvc"
"npm/win32-arm64-msvc"
"npm/linux-x64-gnu"
"npm/linux-arm64-gnu"
)
echo "═══════════════════════════════════════════════════"
echo " Gelectron NPM Publisher"
echo "═══════════════════════════════════════════════════"
echo ""
# Check if logged in
if ! npm whoami &>/dev/null; then
echo "Not logged in to npm. Run 'npm login' first."
exit 1
fi
published=0
skipped=0
for dir in "${PLATFORMS[@]}"; do
pkg_name=$(node -e "console.log(require('./$dir/package.json').name)")
has_node=false
# Check if the .node file exists
for f in "$dir"/*.node; do
if [ -f "$f" ]; then
has_node=true
break
fi
done
if $has_node; then
echo "Publishing $pkg_name..."
(cd "$dir" && npm publish --access public)
published=$((published + 1))
else
echo "Skipping $pkg_name (no .node file)"
skipped=$((skipped + 1))
fi
done
echo ""
echo "Publishing main gelectron package..."
npm run prepublishOnly
npm publish --access public
published=$((published + 1))
echo ""
echo "═══════════════════════════════════════════════════"
echo " Done: $published published, $skipped skipped"
echo "═══════════════════════════════════════════════════"