diff --git a/.freebuff/project-id b/.freebuff/project-id new file mode 100644 index 0000000..8cc32e9 --- /dev/null +++ b/.freebuff/project-id @@ -0,0 +1 @@ +1db4284c-fc55-41be-a7dd-cde2036997da diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..067db70 --- /dev/null +++ b/.github/workflows/publish.yml @@ -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 }} diff --git a/README.md b/README.md index 0529a89..57899d7 100644 --- a/README.md +++ b/README.md @@ -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. +## 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 OmniEmu2.0 is a full Electron app used to validate Gelectron compatibility: diff --git a/benchmark/package.json b/benchmark/package.json index ac2fc1b..3e1d33c 100644 --- a/benchmark/package.json +++ b/benchmark/package.json @@ -2,5 +2,8 @@ "name": "gelectron-benchmark", "version": "1.0.0", "description": "Simple web benchmark for comparing Electron vs Gelectron", - "main": "main.js" + "main": "main.js", + "dependencies": { + "electron": "^43.4.0" + } } diff --git a/benchmark/results/benchmark_20260728_222020.json b/benchmark/results/benchmark_20260728_222020.json deleted file mode 100644 index d8f3f4c..0000000 --- a/benchmark/results/benchmark_20260728_222020.json +++ /dev/null @@ -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] - } -} diff --git a/benchmark/results/benchmark_20260814_165256.json b/benchmark/results/benchmark_20260814_165256.json new file mode 100644 index 0000000..908e0c8 --- /dev/null +++ b/benchmark/results/benchmark_20260814_165256.json @@ -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] + } +} diff --git a/crates/gelectron-app/src/main.rs b/crates/gelectron-app/src/main.rs index 4f2b154..edfe3ee 100644 --- a/crates/gelectron-app/src/main.rs +++ b/crates/gelectron-app/src/main.rs @@ -11,6 +11,13 @@ use std::sync::mpsc; use std::sync::atomic::{AtomicBool, Ordering}; use std::thread; 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_loop::{ControlFlow, EventLoopBuilder}; use tao::window::{Fullscreen, WindowBuilder, WindowId}; @@ -236,7 +243,7 @@ struct NotificationOpts { struct WindowPair { #[allow(dead_code)] window: tao::window::Window, - webview: WebView, + webview: Option, } struct AppState { @@ -548,7 +555,7 @@ require('{}'); } 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(); if st.node_exited.load(Ordering::SeqCst) { @@ -559,7 +566,7 @@ require('{}'); } match event { - Event::NewEvents(StartCause::Poll) => { + Event::NewEvents(StartCause::Poll | StartCause::ResumeTimeReached { .. }) => { // Drain async responses from background threads (dialogs, clipboard, etc.) while let Ok((request_id, result)) = response_rx.try_recv() { st.send_to_node(&ToNode::Response { @@ -650,22 +657,24 @@ window.__gelectron_run_main(`{}`); } 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(); match event { - Event::NewEvents(StartCause::Poll) => { + Event::NewEvents(StartCause::Poll | StartCause::ResumeTimeReached { .. }) => { // Drain async responses from background threads (dialogs, clipboard, etc.) 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!( "window.__gelectron_response('{}', {});", request_id, 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 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}) } }; - 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()) - ); + )); } } @@ -719,7 +728,7 @@ window.__gelectron_run_main(`{}`); let js = serde_json::to_string(&serde_json::json!({ "__from_node": true, "type": "window-closed", "id": id, })).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.window_wids.remove(&window_id); @@ -733,7 +742,7 @@ window.__gelectron_run_main(`{}`); let js = serde_json::to_string(&serde_json::json!({ "__from_node": true, "type": "window-focus", "id": id, })).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, + to_rust_tx: Option<&Arc>>, +) -> wry::Result { + 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::(&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::(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( event_loop_target: &tao::event_loop::EventLoopWindowTarget<()>, state: &Rc>, @@ -783,7 +833,7 @@ fn create_initial_webview_window( Ok(webview) => { let wid = window.id(); 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); log::info!("Initial WebView window created (running compat layer)"); } @@ -1459,58 +1509,32 @@ fn handle_to_rust( Ok(window) => { let url = options.url.unwrap_or_else(|| "about:blank".into()); 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()); - match 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::(&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::(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); + // Build the WebView immediately (about:blank). Init scripts + // and the ipc message handler are registered at build time + // and survive subsequent in-place navigations (LoadUrl/LoadFile). + let webview = match create_webview(&window, &url, &init, id, ipc_tx, to_rust_tx) { + Ok(wv) => Some(wv), + Err(e) => { + log::error!("WebView error: {}", e); + None } - 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), } @@ -1518,10 +1542,12 @@ fn handle_to_rust( ToRust::LoadUrl { id, url } => { log::info!("Loading url in window {}: {}", id, url); if let Some(pair) = st.windows.get(&id) { - let _ = pair.webview.evaluate_script(&format!( - "window.location.replace({});", - serde_json::to_string(&url).unwrap() - )); + if let Some(webview) = &pair.webview { + let _ = webview.evaluate_script(&format!( + "window.location.replace({});", + serde_json::to_string(&url).unwrap() + )); + } } } ToRust::LoadFile { id, path } => { @@ -1529,44 +1555,17 @@ fn handle_to_rust( .map(|u| u.to_string()) .unwrap_or_else(|_| "about:blank".into()); log::info!("Loading file in window {}: {}", id, url); - let init = st.bundle_js.clone().unwrap_or_else(|| preload_script()); - if let Some(pair) = st.windows.get_mut(&id) { - let window = &pair.window; - let ipc_tx_clone = ipc_tx.clone(); - let to_rust_tx_clone = to_rust_tx.cloned(); - let wid_for_ipc = id; - match 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::(&body) { - if let Some(ref tx) = to_rust_tx_clone { - if let Ok(cmd) = serde_json::from_value::(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), + // Navigate in-place instead of rebuilding the WebView. Rebuilding on + // macOS creates a stray 500x500 NSWindow artifact when the target + // window is still hidden (the vanilla app loads its splash this way). + // The WKUserScript init script re-runs on navigation, so window.gelectron + // (and any bundle) is re-established on the new document. + if let Some(pair) = st.windows.get(&id) { + if let Some(webview) = &pair.webview { + let _ = webview.evaluate_script(&format!( + "window.location.replace({});", + serde_json::to_string(&url).unwrap() + )); } } } @@ -1584,12 +1583,12 @@ fn handle_to_rust( if let Some(pair) = st.windows.get(&id) { let msg = serde_json::json!({"__from_node":true,"channel":channel,"data":data}); 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 } => { 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 => { diff --git a/npm/darwin-arm64/package.json b/npm/darwin-arm64/package.json index d372727..d5d498d 100644 --- a/npm/darwin-arm64/package.json +++ b/npm/darwin-arm64/package.json @@ -1,16 +1,11 @@ { "name": "gelectron-darwin-arm64", "version": "0.1.0", - "description": "Gelectron native binary for macOS ARM64", + "description": "Gelectron native addon for macOS ARM64", "main": "gelectron_core.darwin-arm64.node", - "files": [ - "gelectron_core.darwin-arm64.node" - ], - "os": [ - "darwin" - ], - "cpu": [ - "arm64" - ], - "license": "MIT" -} \ No newline at end of file + "files": ["gelectron_core.darwin-arm64.node"], + "os": ["darwin"], + "cpu": ["arm64"], + "license": "MIT", + "author": "mileswa1q22" +} diff --git a/npm/darwin-x64/package.json b/npm/darwin-x64/package.json new file mode 100644 index 0000000..b5ec52e --- /dev/null +++ b/npm/darwin-x64/package.json @@ -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" +} diff --git a/npm/linux-arm64-gnu/package.json b/npm/linux-arm64-gnu/package.json new file mode 100644 index 0000000..f9eb16c --- /dev/null +++ b/npm/linux-arm64-gnu/package.json @@ -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" +} diff --git a/npm/linux-x64-gnu/package.json b/npm/linux-x64-gnu/package.json new file mode 100644 index 0000000..5ce1ec9 --- /dev/null +++ b/npm/linux-x64-gnu/package.json @@ -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" +} diff --git a/npm/win32-arm64-msvc/package.json b/npm/win32-arm64-msvc/package.json new file mode 100644 index 0000000..cfa9bc0 --- /dev/null +++ b/npm/win32-arm64-msvc/package.json @@ -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" +} diff --git a/npm/win32-x64-msvc/package.json b/npm/win32-x64-msvc/package.json new file mode 100644 index 0000000..6c0ff7c --- /dev/null +++ b/npm/win32-x64-msvc/package.json @@ -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" +} diff --git a/package-lock.json b/package-lock.json index 2835e36..b116f46 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1875,6 +1875,19 @@ "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": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", diff --git a/scripts/build-npm.sh b/scripts/build-npm.sh new file mode 100755 index 0000000..26f5828 --- /dev/null +++ b/scripts/build-npm.sh @@ -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 "═══════════════════════════════════════════════════" diff --git a/scripts/publish-npm.sh b/scripts/publish-npm.sh new file mode 100755 index 0000000..e9838eb --- /dev/null +++ b/scripts/publish-npm.sh @@ -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 "═══════════════════════════════════════════════════"