mirror of
https://github.com/mileswolfallen2/gelectron.git
synced 2026-09-08 04:43:14 +00:00
update
This commit is contained in:
+1
-1
@@ -1,5 +1,5 @@
|
||||
[workspace]
|
||||
members = ["crates/gelectron-core"]
|
||||
members = ["crates/gelectron-core", "crates/gelectron-app"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.dependencies]
|
||||
|
||||
+5
-5
@@ -113,8 +113,8 @@ if (process.env.GELECTRON_LOG) {
|
||||
console.log(`[gelectron] native addon: ${nativeAddonPath || 'not found (using build target)'}`);
|
||||
}
|
||||
|
||||
const rustBin = path.join(nativeDir, 'target', 'release', 'gelectron');
|
||||
const rustBinDebug = path.join(nativeDir, 'target', 'debug', 'gelectron');
|
||||
const rustBin = path.join(__dirname, '..', 'target', 'release', 'gelectron');
|
||||
const rustBinDebug = path.join(__dirname, '..', 'target', 'debug', 'gelectron');
|
||||
|
||||
let executable;
|
||||
if (fs.existsSync(rustBin)) {
|
||||
@@ -124,15 +124,15 @@ if (fs.existsSync(rustBin)) {
|
||||
}
|
||||
|
||||
if (executable) {
|
||||
const child = spawn(executable, args.slice(1), {
|
||||
console.log(`[gelectron] Using native binary: ${executable}`);
|
||||
const child = spawn(executable, args, {
|
||||
env,
|
||||
stdio: 'inherit',
|
||||
cwd: process.cwd(),
|
||||
});
|
||||
child.on('exit', (code) => process.exit(code || 0));
|
||||
} else {
|
||||
console.log(`[gelectron] Native binary not found. Running main script directly via Node.js.`);
|
||||
console.log(`[gelectron] Build with: cargo build --release -p gelectron-core`);
|
||||
console.log(`[gelectron] Native binary not found. Run: cargo build --release -p gelectron`);
|
||||
console.log(`[gelectron] Falling back to Node.js runtime...\n`);
|
||||
|
||||
require('../src/electron/runtime.js').run(mainScript, env);
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "gelectron"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Gelectron - Firefox-engine alternative to Electron"
|
||||
license = "MIT"
|
||||
authors = ["mileswa1q22"]
|
||||
|
||||
[[bin]]
|
||||
name = "gelectron"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
log = { workspace = true }
|
||||
env_logger = { workspace = true }
|
||||
wry = "0.47"
|
||||
tao = "0.30"
|
||||
@@ -0,0 +1,468 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::process::{Child, Command, Stdio};
|
||||
use std::rc::Rc;
|
||||
use std::sync::mpsc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::thread;
|
||||
use std::sync::Arc;
|
||||
|
||||
use tao::event::{Event, StartCause, WindowEvent};
|
||||
use tao::event_loop::{ControlFlow, EventLoopBuilder};
|
||||
use tao::window::{Fullscreen, WindowBuilder, WindowId};
|
||||
use wry::{WebView, WebViewBuilder};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
enum ToRust {
|
||||
#[serde(rename = "create-window")]
|
||||
CreateWindow { id: u32, options: WindowOpts },
|
||||
#[serde(rename = "load-url")]
|
||||
LoadUrl { id: u32, url: String },
|
||||
#[serde(rename = "load-file")]
|
||||
LoadFile { id: u32, path: String },
|
||||
#[serde(rename = "destroy-window")]
|
||||
DestroyWindow { id: u32 },
|
||||
#[serde(rename = "set-title")]
|
||||
SetTitle { id: u32, title: String },
|
||||
#[serde(rename = "set-size")]
|
||||
SetSize { id: u32, width: u32, height: u32 },
|
||||
#[serde(rename = "show")]
|
||||
Show { id: u32 },
|
||||
#[serde(rename = "hide")]
|
||||
Hide { id: u32 },
|
||||
#[serde(rename = "focus")]
|
||||
Focus { id: u32 },
|
||||
#[serde(rename = "minimize")]
|
||||
Minimize { id: u32 },
|
||||
#[serde(rename = "maximize")]
|
||||
Maximize { id: u32 },
|
||||
#[serde(rename = "close")]
|
||||
Close { id: u32 },
|
||||
#[serde(rename = "ipc-message")]
|
||||
IpcMessage {
|
||||
id: u32,
|
||||
channel: String,
|
||||
data: serde_json::Value,
|
||||
},
|
||||
#[serde(rename = "eval-js")]
|
||||
EvalJs { id: u32, script: String },
|
||||
#[serde(rename = "quit")]
|
||||
Quit,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
enum ToNode {
|
||||
#[serde(rename = "window-closed")]
|
||||
WindowClosed { id: u32 },
|
||||
#[serde(rename = "window-focus")]
|
||||
WindowFocus { id: u32 },
|
||||
#[serde(rename = "ipc-message")]
|
||||
IpcMessage {
|
||||
id: u32,
|
||||
channel: String,
|
||||
data: serde_json::Value,
|
||||
},
|
||||
#[serde(rename = "ready")]
|
||||
Ready,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
|
||||
struct WindowOpts {
|
||||
#[serde(default)]
|
||||
width: Option<u32>,
|
||||
#[serde(default)]
|
||||
height: Option<u32>,
|
||||
#[serde(default)]
|
||||
title: Option<String>,
|
||||
#[serde(default)]
|
||||
url: Option<String>,
|
||||
#[serde(default)]
|
||||
show: Option<bool>,
|
||||
#[serde(default)]
|
||||
resizable: Option<bool>,
|
||||
#[serde(default)]
|
||||
always_on_top: Option<bool>,
|
||||
#[serde(default)]
|
||||
fullscreen: Option<bool>,
|
||||
}
|
||||
|
||||
struct WindowPair {
|
||||
#[allow(dead_code)]
|
||||
window: tao::window::Window,
|
||||
webview: WebView,
|
||||
}
|
||||
|
||||
struct AppState {
|
||||
windows: HashMap<u32, WindowPair>,
|
||||
window_wids: HashMap<WindowId, u32>,
|
||||
node_stdin: Option<std::process::ChildStdin>,
|
||||
node_exited: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
fn new(node_exited: Arc<AtomicBool>) -> Self {
|
||||
Self {
|
||||
windows: HashMap::new(),
|
||||
window_wids: HashMap::new(),
|
||||
node_stdin: None,
|
||||
node_exited,
|
||||
}
|
||||
}
|
||||
|
||||
fn send_to_node(&mut self, msg: &ToNode) {
|
||||
let json = serde_json::to_string(msg).unwrap();
|
||||
if let Some(ref mut stdin) = self.node_stdin {
|
||||
if writeln!(stdin, "{}", json).is_err() || stdin.flush().is_err() {
|
||||
// Node.js process has likely exited; mark it so the event loop can shut down
|
||||
self.node_exited.store(true, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn preload_script() -> String {
|
||||
r#"
|
||||
(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}));
|
||||
},
|
||||
receive: function(channel, callback) {
|
||||
window.addEventListener('message', function(e) {
|
||||
if (e.source === window && typeof e.data === 'string') {
|
||||
try {
|
||||
var msg = JSON.parse(e.data);
|
||||
if (msg.__from_node && msg.channel === channel) {
|
||||
callback(msg.data);
|
||||
}
|
||||
} catch(err) {}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
})();
|
||||
"#
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
if args.len() < 2 {
|
||||
eprintln!("Usage: gelectron <path-to-app>");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
let app_path = std::path::PathBuf::from(&args[1]);
|
||||
if !app_path.exists() {
|
||||
eprintln!("Error: path not found: {}", app_path.display());
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
// Canonicalize so relative paths (e.g. "demo/") become absolute.
|
||||
// Node.js -e resolves require() relative to [eval], not cwd.
|
||||
let app_path = std::fs::canonicalize(&app_path).unwrap_or(app_path);
|
||||
|
||||
let main_script = if app_path.is_dir() {
|
||||
let pkg_path = app_path.join("package.json");
|
||||
if pkg_path.exists() {
|
||||
let pkg: serde_json::Value =
|
||||
serde_json::from_str(&std::fs::read_to_string(&pkg_path).unwrap_or_default())
|
||||
.unwrap_or_default();
|
||||
let main = pkg
|
||||
.get("main")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("index.js");
|
||||
app_path.join(main)
|
||||
} else {
|
||||
app_path.join("index.js")
|
||||
}
|
||||
} else {
|
||||
app_path.clone()
|
||||
};
|
||||
|
||||
if !main_script.exists() {
|
||||
eprintln!("Error: main script not found: {}", main_script.display());
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
let gelectron_dir = std::env::current_exe()
|
||||
.ok()
|
||||
.and_then(|p| std::fs::canonicalize(p).ok())
|
||||
.and_then(|p| p.parent().map(|p| p.to_path_buf()))
|
||||
.unwrap_or_default();
|
||||
|
||||
// From target/release/ or target/debug/, go up to project root
|
||||
let project_root = if gelectron_dir.ends_with("release") || gelectron_dir.ends_with("debug") {
|
||||
gelectron_dir.parent()
|
||||
.and_then(|p| p.parent())
|
||||
.map(|p| p.to_path_buf())
|
||||
.unwrap_or_else(|| gelectron_dir.clone())
|
||||
} else {
|
||||
gelectron_dir.clone()
|
||||
};
|
||||
|
||||
let compat_dir = if project_root.join("src/electron/index.js").exists() {
|
||||
project_root.join("src/electron")
|
||||
} else if project_root.join("crates/gelectron-app/src/main.rs").exists() {
|
||||
project_root.join("src/electron")
|
||||
} else {
|
||||
// npm installed: try to find in node_modules
|
||||
project_root.join("node_modules/gelectron/src/electron")
|
||||
};
|
||||
|
||||
log::info!("App: {}", app_path.display());
|
||||
log::info!("Script: {}", main_script.display());
|
||||
log::info!("Compat: {}", compat_dir.display());
|
||||
|
||||
let node_path = which_node().unwrap_or_else(|| {
|
||||
eprintln!("Error: node not found in PATH");
|
||||
std::process::exit(1);
|
||||
});
|
||||
|
||||
let setup_script = format!(
|
||||
r#"
|
||||
process.env.GELECTRON_NATIVE = '1';
|
||||
process.env.GELECTRON_MAIN_SCRIPT = '{}';
|
||||
process.env.GELECTRON_APP_PATH = '{}';
|
||||
|
||||
const Module = require('module');
|
||||
const path = require('path');
|
||||
const compatPath = '{}';
|
||||
|
||||
// Purge any cached 'electron' module from the real npm package
|
||||
Object.keys(Module._cache).forEach(function(key) {{
|
||||
var normalized = key.replace(/\\/g, '/');
|
||||
if (normalized.endsWith('/electron') || normalized.endsWith('/electron/index.js') || normalized.endsWith('/electron/index.cjs')) {{
|
||||
delete Module._cache[key];
|
||||
}}
|
||||
}});
|
||||
|
||||
const origResolve = Module._resolveFilename;
|
||||
Module._resolveFilename = function(request, parent, isMain, options) {{
|
||||
if (request === 'electron' || request === 'electron/main' || request === 'electron/common') return path.join(compatPath, 'index.js');
|
||||
if (request === 'electron/renderer') return path.join(compatPath, 'ipc-renderer.js');
|
||||
return origResolve.call(this, request, parent, isMain, options);
|
||||
}};
|
||||
|
||||
// Also patch _resolveRequest for Node >= 22
|
||||
if (typeof Module._resolveRequest === 'function') {{
|
||||
var origResolveRequest = Module._resolveRequest;
|
||||
Module._resolveRequest = function(request, parent, isMain, options) {{
|
||||
if (request === 'electron' || request === 'electron/main' || request === 'electron/common') return path.join(compatPath, 'index.js');
|
||||
if (request === 'electron/renderer') return path.join(compatPath, 'ipc-renderer.js');
|
||||
return origResolveRequest.call(this, request, parent, isMain, options);
|
||||
}};
|
||||
}}
|
||||
|
||||
// Pre-load the gelectron shim so require('electron') hits cache
|
||||
require(path.join(compatPath, 'index.js'));
|
||||
|
||||
require('{}');
|
||||
"#,
|
||||
main_script.display().to_string().replace('\\', "\\\\").replace('\'', "\\'"),
|
||||
app_path.display().to_string().replace('\\', "\\\\").replace('\'', "\\'"),
|
||||
compat_dir.display().to_string().replace('\\', "\\\\").replace('\'', "\\'"),
|
||||
main_script.display().to_string().replace('\\', "\\\\").replace('\'', "\\'"),
|
||||
);
|
||||
|
||||
let mut child: Child = Command::new(&node_path)
|
||||
.arg("-e")
|
||||
.arg(&setup_script)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::inherit())
|
||||
.spawn()
|
||||
.expect("Failed to spawn Node.js");
|
||||
|
||||
let child_stdin = child.stdin.take().unwrap();
|
||||
let child_stdout = child.stdout.take().unwrap();
|
||||
|
||||
let (tx, rx) = mpsc::channel::<ToRust>();
|
||||
let node_exited = Arc::new(AtomicBool::new(false));
|
||||
let node_exited_clone = node_exited.clone();
|
||||
|
||||
thread::spawn(move || {
|
||||
let reader = BufReader::new(child_stdout);
|
||||
for line in reader.lines() {
|
||||
match line {
|
||||
Ok(line) => {
|
||||
let trimmed = line.trim().to_string();
|
||||
if trimmed.is_empty() || !trimmed.starts_with('{') {
|
||||
continue;
|
||||
}
|
||||
if let Ok(msg) = serde_json::from_str::<ToRust>(&trimmed) {
|
||||
if tx.send(msg).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
// Node.js process stdout closed – the child has exited
|
||||
node_exited_clone.store(true, Ordering::SeqCst);
|
||||
});
|
||||
|
||||
let event_loop = EventLoopBuilder::new().build();
|
||||
let mut state = AppState::new(node_exited.clone());
|
||||
state.node_stdin = Some(child_stdin);
|
||||
let state = Rc::new(RefCell::new(state));
|
||||
|
||||
state.borrow_mut().send_to_node(&ToNode::Ready);
|
||||
|
||||
event_loop.run(move |event, event_loop_target, control_flow| {
|
||||
*control_flow = ControlFlow::Poll;
|
||||
let mut st = state.borrow_mut();
|
||||
|
||||
// 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;
|
||||
return;
|
||||
}
|
||||
|
||||
match event {
|
||||
Event::NewEvents(StartCause::Poll) => {
|
||||
while let Ok(msg) = rx.try_recv() {
|
||||
match msg {
|
||||
ToRust::CreateWindow { id, options } => {
|
||||
let mut wb = WindowBuilder::new()
|
||||
.with_title(options.title.clone().unwrap_or_else(|| "Gelectron".into()))
|
||||
.with_inner_size(tao::dpi::LogicalSize::new(
|
||||
options.width.unwrap_or(800) as f64,
|
||||
options.height.unwrap_or(600) as f64,
|
||||
));
|
||||
if let Some(r) = options.resizable { wb = wb.with_resizable(r); }
|
||||
if let Some(a) = options.always_on_top { wb = wb.with_always_on_top(a); }
|
||||
if let Some(true) = options.fullscreen {
|
||||
wb = wb.with_fullscreen(Some(Fullscreen::Borderless(None)));
|
||||
}
|
||||
wb = wb.with_visible(options.show.unwrap_or(true));
|
||||
|
||||
match wb.build(event_loop_target) {
|
||||
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);
|
||||
}
|
||||
Err(e) => log::error!("WebView error: {}", e),
|
||||
}
|
||||
}
|
||||
Err(e) => log::error!("Window error: {}", e),
|
||||
}
|
||||
}
|
||||
ToRust::LoadUrl { id, url } => {
|
||||
log::info!("Loading url in window {}: {}", id, url);
|
||||
if let Some(pair) = st.windows.get(&id) {
|
||||
let wv = &pair.webview;
|
||||
let _ = wv.evaluate_script(&format!(
|
||||
"window.location.replace({});",
|
||||
serde_json::to_string(&url).unwrap()
|
||||
));
|
||||
}
|
||||
}
|
||||
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;
|
||||
match WebViewBuilder::new()
|
||||
.with_url(&url)
|
||||
.with_initialization_script(&preload_script())
|
||||
.with_devtools(true)
|
||||
.build(window)
|
||||
{
|
||||
Ok(webview) => {
|
||||
pair.webview = webview;
|
||||
log::info!("WebView rebuilt for window {}", id);
|
||||
}
|
||||
Err(e) => log::error!("WebView rebuild error: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
ToRust::DestroyWindow { id } => {
|
||||
st.windows.remove(&id);
|
||||
st.window_wids.retain(|_, v| *v != id);
|
||||
if st.windows.is_empty() { *control_flow = ControlFlow::Exit; }
|
||||
}
|
||||
ToRust::Close { id } => {
|
||||
st.windows.remove(&id);
|
||||
st.window_wids.retain(|_, v| *v != id);
|
||||
if st.windows.is_empty() { *control_flow = ControlFlow::Exit; }
|
||||
}
|
||||
ToRust::IpcMessage { id, channel, data } => {
|
||||
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);
|
||||
}
|
||||
}
|
||||
ToRust::EvalJs { id, script } => {
|
||||
if let Some(pair) = st.windows.get(&id) {
|
||||
let _ = pair.webview.evaluate_script(&script);
|
||||
}
|
||||
}
|
||||
ToRust::Quit => {
|
||||
st.windows.clear();
|
||||
*control_flow = ControlFlow::Exit;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::WindowEvent { event: WindowEvent::CloseRequested, window_id, .. } => {
|
||||
if let Some(&id) = st.window_wids.get(&window_id) {
|
||||
st.send_to_node(&ToNode::WindowClosed { id });
|
||||
st.windows.remove(&id);
|
||||
st.window_wids.remove(&window_id);
|
||||
log::info!("Window {} closed", id);
|
||||
if st.windows.is_empty() { *control_flow = ControlFlow::Exit; }
|
||||
}
|
||||
}
|
||||
Event::WindowEvent { event: WindowEvent::Focused(true), window_id, .. } => {
|
||||
if let Some(&id) = st.window_wids.get(&window_id) {
|
||||
st.send_to_node(&ToNode::WindowFocus { id });
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn which_node() -> Option<String> {
|
||||
Command::new("which")
|
||||
.arg("node")
|
||||
.output()
|
||||
.ok()
|
||||
.and_then(|o| if o.status.success() { String::from_utf8(o.stdout).ok().map(|s| s.trim().to_string()) } else { None })
|
||||
.or_else(|| {
|
||||
for p in &["/usr/local/bin/node", "/opt/homebrew/bin/node", "/usr/bin/node"] {
|
||||
if std::path::Path::new(p).exists() { return Some(p.to_string()); }
|
||||
}
|
||||
None
|
||||
})
|
||||
}
|
||||
+295
@@ -0,0 +1,295 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Gelectron Demo</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
background: #0f0f1a;
|
||||
color: #e0e0e0;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
header {
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||||
border-bottom: 1px solid #2a2a4a;
|
||||
padding: 24px 32px;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(90deg, #7b68ee, #00d4ff);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
header p {
|
||||
margin-top: 6px;
|
||||
color: #8888aa;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
padding: 32px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 24px;
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #1a1a2e;
|
||||
border: 1px solid #2a2a4a;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #7b68ee;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.card p, .card li {
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
color: #b0b0cc;
|
||||
}
|
||||
|
||||
.card ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.card ul li::before {
|
||||
content: '\2713';
|
||||
color: #00d4ff;
|
||||
margin-right: 8px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.counter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.counter button {
|
||||
background: #2a2a4a;
|
||||
color: #e0e0e0;
|
||||
border: 1px solid #3a3a6a;
|
||||
border-radius: 8px;
|
||||
padding: 10px 20px;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, transform 0.1s;
|
||||
}
|
||||
|
||||
.counter button:hover { background: #3a3a6a; }
|
||||
.counter button:active { transform: scale(0.95); }
|
||||
|
||||
.counter .value {
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
color: #00d4ff;
|
||||
min-width: 60px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.canvas-wrap {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
canvas {
|
||||
background: #12121f;
|
||||
border: 1px solid #2a2a4a;
|
||||
border-radius: 8px;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.color-bar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.color-bar span {
|
||||
flex: 1;
|
||||
height: 32px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
#clock {
|
||||
font-size: 32px;
|
||||
font-weight: 300;
|
||||
color: #00d4ff;
|
||||
font-variant-numeric: tabular-nums;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.env-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.env-table td {
|
||||
padding: 6px 0;
|
||||
font-size: 13px;
|
||||
border-bottom: 1px solid #1f1f35;
|
||||
}
|
||||
|
||||
.env-table td:first-child {
|
||||
color: #7b68ee;
|
||||
font-weight: 600;
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.env-table td:last-child {
|
||||
color: #b0b0cc;
|
||||
font-family: 'SF Mono', Menlo, monospace;
|
||||
}
|
||||
|
||||
footer {
|
||||
padding: 16px 32px;
|
||||
border-top: 1px solid #2a2a4a;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: #555577;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>Gelectron Demo</h1>
|
||||
<p>HTML + CSS + JavaScript rendering via Gecko / Servo engine</p>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div class="card">
|
||||
<h2>Interactive Counter</h2>
|
||||
<p>Proof that JavaScript runs and DOM updates work.</p>
|
||||
<div class="counter">
|
||||
<button onclick="dec()">\u2212</button>
|
||||
<div class="value" id="count">0</div>
|
||||
<button onclick="inc()">+</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Live Clock</h2>
|
||||
<p>requestAnimationFrame-driven rendering.</p>
|
||||
<div id="clock">--:--:--</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Canvas 2D</h2>
|
||||
<p>Animated canvas with moving shapes.</p>
|
||||
<div class="canvas-wrap">
|
||||
<canvas id="scene" width="400" height="200"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Runtime Info</h2>
|
||||
<p>Environment details from the gelectron shim.</p>
|
||||
<table class="env-table">
|
||||
<tr><td>Platform</td><td id="env-platform"></td></tr>
|
||||
<tr><td>Arch</td><td id="env-arch"></td></tr>
|
||||
<tr><td>Node.js</td><td id="env-node"></td></tr>
|
||||
<tr><td>User Agent</td><td id="env-ua"></td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="card" style="grid-column: 1 / -1;">
|
||||
<h2>CSS Features</h2>
|
||||
<p>Grid, gradients, border-radius, transitions, flexbox, backdrop styling.</p>
|
||||
<div class="color-bar">
|
||||
<span style="background: #7b68ee;"></span>
|
||||
<span style="background: #00d4ff;"></span>
|
||||
<span style="background: #ff6b9d;"></span>
|
||||
<span style="background: #ffd93d;"></span>
|
||||
<span style="background: #6bcb77;"></span>
|
||||
<span style="background: #ee5a24;"></span>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer>Gelectron v0.1.0 — Electron API compatibility layer powered by Gecko/Servo</footer>
|
||||
|
||||
<script>
|
||||
// Counter
|
||||
var count = 0;
|
||||
var countEl = document.getElementById('count');
|
||||
function inc() { countEl.textContent = ++count; }
|
||||
function dec() { countEl.textContent = --count; }
|
||||
|
||||
// Live clock
|
||||
function tick() {
|
||||
var now = new Date();
|
||||
var h = String(now.getHours()).padStart(2, '0');
|
||||
var m = String(now.getMinutes()).padStart(2, '0');
|
||||
var s = String(now.getSeconds()).padStart(2, '0');
|
||||
document.getElementById('clock').textContent = h + ':' + m + ':' + s;
|
||||
requestAnimationFrame(tick);
|
||||
}
|
||||
tick();
|
||||
|
||||
// Canvas animation
|
||||
var canvas = document.getElementById('scene');
|
||||
var ctx = canvas.getContext('2d');
|
||||
var t = 0;
|
||||
function drawFrame() {
|
||||
t += 0.02;
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// bouncing ball
|
||||
var bx = 50 + Math.sin(t) * 150;
|
||||
var by = 100 + Math.cos(t * 1.3) * 60;
|
||||
ctx.beginPath();
|
||||
ctx.arc(bx, by, 18, 0, Math.PI * 2);
|
||||
ctx.fillStyle = '#7b68ee';
|
||||
ctx.fill();
|
||||
|
||||
// second ball
|
||||
var bx2 = 200 + Math.cos(t * 0.7) * 120;
|
||||
var by2 = 80 + Math.sin(t * 1.1) * 70;
|
||||
ctx.beginPath();
|
||||
ctx.arc(bx2, by2, 14, 0, Math.PI * 2);
|
||||
ctx.fillStyle = '#00d4ff';
|
||||
ctx.fill();
|
||||
|
||||
// trailing line
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, 100 + Math.sin(t) * 40);
|
||||
for (var x = 0; x < canvas.width; x += 4) {
|
||||
ctx.lineTo(x, 100 + Math.sin(t + x * 0.03) * 40);
|
||||
}
|
||||
ctx.strokeStyle = 'rgba(123,104,238,0.3)';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.stroke();
|
||||
|
||||
requestAnimationFrame(drawFrame);
|
||||
}
|
||||
drawFrame();
|
||||
|
||||
// Runtime info
|
||||
document.getElementById('env-platform').textContent = navigator.platform || 'unknown';
|
||||
document.getElementById('env-arch').textContent = 'unknown (browser sandbox)';
|
||||
document.getElementById('env-node').textContent = 'N/A in renderer';
|
||||
document.getElementById('env-ua').textContent = navigator.userAgent;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,44 @@
|
||||
const { app, BrowserWindow } = require('electron');
|
||||
const path = require('path');
|
||||
|
||||
let mainWindow = null;
|
||||
|
||||
const gotLock = app.requestSingleInstanceLock();
|
||||
if (!gotLock) {
|
||||
app.quit();
|
||||
} else {
|
||||
app.on('second-instance', () => {
|
||||
if (mainWindow) {
|
||||
if (mainWindow.isMinimized()) mainWindow.restore();
|
||||
mainWindow.show();
|
||||
mainWindow.focus();
|
||||
}
|
||||
});
|
||||
|
||||
app.whenReady().then(() => {
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 900,
|
||||
height: 680,
|
||||
title: 'Gelectron Demo',
|
||||
backgroundColor: '#1a1a2e',
|
||||
webPreferences: {
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
},
|
||||
});
|
||||
|
||||
mainWindow.loadFile(path.join(__dirname, 'index.html'));
|
||||
|
||||
mainWindow.once('ready-to-show', () => {
|
||||
mainWindow.show();
|
||||
});
|
||||
|
||||
mainWindow.on('closed', () => {
|
||||
mainWindow = null;
|
||||
});
|
||||
});
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
app.quit();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "gelectron-demo",
|
||||
"version": "1.0.0",
|
||||
"description": "Minimal demo for gelectron — HTML, CSS, JavaScript rendering",
|
||||
"main": "main.js"
|
||||
}
|
||||
@@ -6,7 +6,11 @@
|
||||
"files": [
|
||||
"gelectron_core.darwin-arm64.node"
|
||||
],
|
||||
"os": ["darwin"],
|
||||
"cpu": ["arm64"],
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
+6
-1
@@ -57,6 +57,11 @@
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"gelectron-darwin-arm64": "0.1.0"
|
||||
"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-x64-gnu": "0.1.0",
|
||||
"gelectron-linux-arm64-gnu": "0.1.0"
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,15 @@ class App extends EventEmitter {
|
||||
} : null;
|
||||
|
||||
this._argv = process.argv.slice();
|
||||
|
||||
// Emit 'ready' automatically on the next tick, matching real Electron
|
||||
// where the app becomes ready once the process has initialised.
|
||||
process.nextTick(() => {
|
||||
if (!this._ready) {
|
||||
this._ready = true;
|
||||
this.emit('ready');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get commandLine() {
|
||||
|
||||
@@ -3,7 +3,15 @@
|
||||
/**
|
||||
* Gelectron - autoUpdater module (Electron compatible)
|
||||
* Stub implementation for electron-updater compatibility.
|
||||
* electron-updater uses require("electron").autoUpdater as its native backend.
|
||||
*
|
||||
* electron-updater's MacUpdater / NsisUpdater / AppImageUpdater all
|
||||
* access `require("electron").autoUpdater` and expect it to be an
|
||||
* EventEmitter with the native Electron autoUpdater API surface:
|
||||
* .on("error", …), .on("update-downloaded", …)
|
||||
* .setFeedURL(), .getFeedURL()
|
||||
* .checkForUpdates()
|
||||
* .quitAndInstall()
|
||||
* .removeListener()
|
||||
*/
|
||||
|
||||
const { EventEmitter } = require('events');
|
||||
@@ -13,6 +21,7 @@ class AutoUpdater extends EventEmitter {
|
||||
super();
|
||||
this._isUpdateAvailable = false;
|
||||
this._updateInfo = null;
|
||||
this._feedURL = null;
|
||||
this.autoDownload = true;
|
||||
this.autoInstallOnAppQuit = false;
|
||||
this.autoRunAppAfterInstall = true;
|
||||
@@ -20,10 +29,12 @@ class AutoUpdater extends EventEmitter {
|
||||
}
|
||||
|
||||
getFeedURL() {
|
||||
return null;
|
||||
return this._feedURL;
|
||||
}
|
||||
|
||||
setFeedURL() {}
|
||||
setFeedURL(options) {
|
||||
this._feedURL = options;
|
||||
}
|
||||
|
||||
async checkForUpdates() {
|
||||
return {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
const { EventEmitter } = require('events');
|
||||
const path = require('path');
|
||||
const { bridge, isNative } = require('./native-bridge');
|
||||
|
||||
class WebContents extends EventEmitter {
|
||||
constructor(id) {
|
||||
@@ -52,22 +53,41 @@ class WebContents extends EventEmitter {
|
||||
this._url = targetUrl;
|
||||
this._isLoading = true;
|
||||
this.emit('did-start-loading');
|
||||
if (isNative) {
|
||||
bridge.loadUrl(this.id, targetUrl);
|
||||
}
|
||||
setTimeout(() => {
|
||||
this._isLoading = false;
|
||||
this.emit('did-stop-loading');
|
||||
this.emit('did-finish-load');
|
||||
this.emit('dom-ready');
|
||||
}, 100);
|
||||
}, isNative ? 500 : 100);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
loadFile(filePath) {
|
||||
return this.loadURL(`file://${path.resolve(filePath)}`);
|
||||
const url = `file://${path.resolve(filePath)}`;
|
||||
this._url = url;
|
||||
this._isLoading = true;
|
||||
this.emit('did-start-loading');
|
||||
if (isNative) {
|
||||
bridge.loadFile(this.id, filePath);
|
||||
}
|
||||
setTimeout(() => {
|
||||
this._isLoading = false;
|
||||
this.emit('did-stop-loading');
|
||||
this.emit('did-finish-load');
|
||||
this.emit('dom-ready');
|
||||
}, isNative ? 500 : 100);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
reload() {
|
||||
this._isLoading = true;
|
||||
this.emit('did-start-loading');
|
||||
if (isNative && this._url) {
|
||||
bridge.loadUrl(this.id, this._url);
|
||||
}
|
||||
setTimeout(() => {
|
||||
this._isLoading = false;
|
||||
this.emit('did-stop-loading');
|
||||
@@ -82,6 +102,9 @@ class WebContents extends EventEmitter {
|
||||
goForward() {}
|
||||
|
||||
executeJavaScript(code, userGesture = true) {
|
||||
if (isNative) {
|
||||
bridge.evalJs(this.id, code);
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
@@ -89,7 +112,11 @@ class WebContents extends EventEmitter {
|
||||
insertJS(code, hasUserGesture = true) { return this.executeJavaScript(code, hasUserGesture); }
|
||||
|
||||
send(channel, ...args) {
|
||||
console.log(`[gelectron] webContents.send('${channel}')`);
|
||||
if (isNative) {
|
||||
bridge.sendToRenderer(this.id, channel, ...args);
|
||||
} else {
|
||||
console.log(`[gelectron] webContents.send('${channel}')`);
|
||||
}
|
||||
}
|
||||
|
||||
sendInputEvent() {}
|
||||
@@ -178,6 +205,18 @@ class BrowserWindow extends EventEmitter {
|
||||
|
||||
BrowserWindow._windows.set(this.id, this);
|
||||
|
||||
if (isNative) {
|
||||
bridge.createWindow(this.id, {
|
||||
width: this._options.width,
|
||||
height: this._options.height,
|
||||
title: this._options.title,
|
||||
show: this._options.show,
|
||||
resizable: this._options.resizable,
|
||||
alwaysOnTop: this._options.alwaysOnTop,
|
||||
fullscreen: this._options.fullscreen,
|
||||
});
|
||||
}
|
||||
|
||||
if (this._options.show) {
|
||||
process.nextTick(() => {
|
||||
if (!this._isDestroyed) {
|
||||
@@ -209,23 +248,28 @@ class BrowserWindow extends EventEmitter {
|
||||
|
||||
loadURL(targetUrl) {
|
||||
this._url = targetUrl;
|
||||
if (isNative) {
|
||||
bridge.loadUrl(this.id, targetUrl);
|
||||
}
|
||||
return this.webContents.loadURL(targetUrl);
|
||||
}
|
||||
|
||||
loadFile(filePath) {
|
||||
return this.loadURL(`file://${path.resolve(filePath)}`);
|
||||
return this.webContents.loadFile(filePath);
|
||||
}
|
||||
|
||||
show() {
|
||||
if (this._isDestroyed) return;
|
||||
this._isVisible = true;
|
||||
this._isMinimized = false;
|
||||
if (isNative) bridge.showWindow(this.id);
|
||||
this.emit('show');
|
||||
}
|
||||
|
||||
hide() {
|
||||
if (this._isDestroyed) return;
|
||||
this._isVisible = false;
|
||||
if (isNative) bridge.hideWindow(this.id);
|
||||
this.emit('hide');
|
||||
}
|
||||
|
||||
@@ -238,15 +282,16 @@ class BrowserWindow extends EventEmitter {
|
||||
destroy() {
|
||||
if (this._isDestroyed) return;
|
||||
this._isDestroyed = true;
|
||||
if (isNative) bridge.destroyWindow(this.id);
|
||||
BrowserWindow._windows.delete(this.id);
|
||||
this.emit('closed');
|
||||
}
|
||||
|
||||
focus() { if (!this._isDestroyed) this.emit('focus'); }
|
||||
focus() { if (!this._isDestroyed) { if (isNative) bridge.focusWindow(this.id); this.emit('focus'); } }
|
||||
blur() {}
|
||||
|
||||
minimize() { if (!this._isDestroyed) { this._isMinimized = true; this.emit('minimize'); } }
|
||||
maximize() { if (!this._isDestroyed) { this._isMaximized = true; this.emit('maximize'); } }
|
||||
minimize() { if (!this._isDestroyed) { this._isMinimized = true; if (isNative) bridge.minimizeWindow(this.id); this.emit('minimize'); } }
|
||||
maximize() { if (!this._isDestroyed) { this._isMaximized = true; if (isNative) bridge.maximizeWindow(this.id); this.emit('maximize'); } }
|
||||
unmaximize() { if (!this._isDestroyed) { this._isMaximized = false; this.emit('unmaximize'); } }
|
||||
restore() { if (!this._isDestroyed) { this._isMinimized = false; this._isMaximized = false; this.emit('restore'); } }
|
||||
|
||||
@@ -282,7 +327,7 @@ class BrowserWindow extends EventEmitter {
|
||||
setClosable(v) { this._options.closable = v; }
|
||||
isClosable() { return this._options.closable; }
|
||||
|
||||
setTitle(title) { this._options.title = title; this.webContents._title = title; }
|
||||
setTitle(title) { this._options.title = title; this.webContents._title = title; if (isNative) bridge.setTitle(this.id, title); }
|
||||
getTitle() { return this._options.title; }
|
||||
|
||||
setSkipTaskbar() {}
|
||||
|
||||
@@ -18,6 +18,14 @@ const safeStorage = require('./safe-storage');
|
||||
const contextBridge = require('./context-bridge');
|
||||
const webContents = require('./web-contents');
|
||||
const { autoUpdater, AutoUpdater } = require('./auto-updater');
|
||||
const { bridge, isNative } = require('./native-bridge');
|
||||
|
||||
if (isNative) {
|
||||
bridge.on('ipc-message', (windowId, channel, data) => {
|
||||
const event = { sender: { id: windowId }, channel };
|
||||
ipcMain._emit(channel, event, data);
|
||||
});
|
||||
}
|
||||
|
||||
// Session stub (electron-updater calls session.fromPartition)
|
||||
const sessionStub = {
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Gelectron Native Bridge - IPC between Node.js and the Rust binary.
|
||||
* When GELECTRON_NATIVE=1, communicates via stdin/stdout JSON lines.
|
||||
*/
|
||||
|
||||
const readline = require('readline');
|
||||
const { EventEmitter } = require('events');
|
||||
|
||||
const isNative = process.env.GELECTRON_NATIVE === '1';
|
||||
|
||||
class NativeBridge extends EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
this._ready = false;
|
||||
this._readyCallbacks = [];
|
||||
this._windowListeners = new Map();
|
||||
|
||||
if (isNative) {
|
||||
this._setupStdio();
|
||||
}
|
||||
}
|
||||
|
||||
_setupStdio() {
|
||||
const rl = readline.createInterface({ input: process.stdin, terminal: false });
|
||||
|
||||
rl.on('line', (line) => {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || !trimmed.startsWith('{')) return;
|
||||
try {
|
||||
const msg = JSON.parse(trimmed);
|
||||
this._handleMessage(msg);
|
||||
} catch (e) {
|
||||
// Not IPC, ignore
|
||||
}
|
||||
});
|
||||
|
||||
process.stdout.on('error', () => {});
|
||||
}
|
||||
|
||||
_handleMessage(msg) {
|
||||
switch (msg.type) {
|
||||
case 'ready':
|
||||
this._ready = true;
|
||||
for (const cb of this._readyCallbacks) cb();
|
||||
this._readyCallbacks = [];
|
||||
this.emit('ready');
|
||||
break;
|
||||
case 'window-closed':
|
||||
this.emit('window-closed', msg.id);
|
||||
break;
|
||||
case 'window-focus':
|
||||
this.emit('window-focus', msg.id);
|
||||
break;
|
||||
case 'ipc-message':
|
||||
this.emit('ipc-message', msg.id, msg.channel, msg.data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_send(msg) {
|
||||
if (!isNative) return;
|
||||
try {
|
||||
process.stdout.write(JSON.stringify(msg) + '\n');
|
||||
} catch (e) {
|
||||
// stdout may be closed
|
||||
}
|
||||
}
|
||||
|
||||
onReady(cb) {
|
||||
if (this._ready) {
|
||||
cb();
|
||||
} else {
|
||||
this._readyCallbacks.push(cb);
|
||||
}
|
||||
}
|
||||
|
||||
createWindow(id, options) {
|
||||
this._send({ type: 'create-window', id, options });
|
||||
}
|
||||
|
||||
loadUrl(id, url) {
|
||||
this._send({ type: 'load-url', id, url });
|
||||
}
|
||||
|
||||
loadFile(id, filePath) {
|
||||
this._send({ type: 'load-file', id, path: filePath });
|
||||
}
|
||||
|
||||
destroyWindow(id) {
|
||||
this._send({ type: 'destroy-window', id });
|
||||
}
|
||||
|
||||
setTitle(id, title) {
|
||||
this._send({ type: 'set-title', id, title });
|
||||
}
|
||||
|
||||
setSize(id, width, height) {
|
||||
this._send({ type: 'set-size', id, width, height });
|
||||
}
|
||||
|
||||
showWindow(id) {
|
||||
this._send({ type: 'show', id });
|
||||
}
|
||||
|
||||
hideWindow(id) {
|
||||
this._send({ type: 'hide', id });
|
||||
}
|
||||
|
||||
focusWindow(id) {
|
||||
this._send({ type: 'focus', id });
|
||||
}
|
||||
|
||||
minimizeWindow(id) {
|
||||
this._send({ type: 'minimize', id });
|
||||
}
|
||||
|
||||
maximizeWindow(id) {
|
||||
this._send({ type: 'maximize', id });
|
||||
}
|
||||
|
||||
closeWindow(id) {
|
||||
this._send({ type: 'close', id });
|
||||
}
|
||||
|
||||
sendToRenderer(id, channel, ...data) {
|
||||
this._send({ type: 'ipc-message', id, channel, data: data.length === 1 ? data[0] : data });
|
||||
}
|
||||
|
||||
evalJs(id, script) {
|
||||
this._send({ type: 'eval-js', id, script });
|
||||
}
|
||||
|
||||
quit() {
|
||||
this._send({ type: 'quit' });
|
||||
}
|
||||
}
|
||||
|
||||
const bridge = new NativeBridge();
|
||||
|
||||
module.exports = { bridge, isNative };
|
||||
+37
-8
@@ -14,24 +14,53 @@ function run(mainScript, env) {
|
||||
|
||||
// Patch require so that 'electron' resolves to gelectron's compat layer
|
||||
const electronCompatPath = path.join(__dirname, 'index.js');
|
||||
const ipcRendererPath = path.join(__dirname, 'ipc-renderer.js');
|
||||
|
||||
// Purge any cached 'electron' module from the real npm package so that
|
||||
// our _resolveFilename patch takes absolute priority. The real 'electron'
|
||||
// npm package (devDependency in the target app) exports a *path string*
|
||||
// to the Electron binary – it does NOT have .autoUpdater, .app, etc.
|
||||
for (const key of Object.keys(Module._cache)) {
|
||||
const normalized = key.replace(/\\/g, '/');
|
||||
if (
|
||||
normalized.endsWith('/electron') ||
|
||||
normalized.endsWith('/electron/index.js') ||
|
||||
normalized.endsWith('/electron/index.cjs')
|
||||
) {
|
||||
delete Module._cache[key];
|
||||
}
|
||||
}
|
||||
|
||||
const originalResolveFilename = Module._resolveFilename;
|
||||
Module._resolveFilename = function (request, parent, isMain, options) {
|
||||
if (request === 'electron') {
|
||||
return electronCompatPath;
|
||||
}
|
||||
if (request === 'electron/main') {
|
||||
if (request === 'electron' || request === 'electron/main' || request === 'electron/common') {
|
||||
return electronCompatPath;
|
||||
}
|
||||
if (request === 'electron/renderer') {
|
||||
return path.join(__dirname, 'ipc-renderer.js');
|
||||
}
|
||||
if (request === 'electron/common') {
|
||||
return electronCompatPath;
|
||||
return ipcRendererPath;
|
||||
}
|
||||
return originalResolveFilename.call(this, request, parent, isMain, options);
|
||||
};
|
||||
|
||||
// Also monkey-patch Module._resolveRequest for Node ≥ 22 where it may
|
||||
// be used internally instead of _resolveFilename.
|
||||
if (typeof Module._resolveRequest === 'function') {
|
||||
const originalResolveRequest = Module._resolveRequest;
|
||||
Module._resolveRequest = function (request, parent, isMain, options) {
|
||||
if (request === 'electron' || request === 'electron/main' || request === 'electron/common') {
|
||||
return electronCompatPath;
|
||||
}
|
||||
if (request === 'electron/renderer') {
|
||||
return ipcRendererPath;
|
||||
}
|
||||
return originalResolveRequest.call(this, request, parent, isMain, options);
|
||||
};
|
||||
}
|
||||
|
||||
// Pre-load the gelectron shim into the module cache so that every
|
||||
// subsequent require('electron') hits cache immediately.
|
||||
require(electronCompatPath);
|
||||
|
||||
// Require the app's main script
|
||||
try {
|
||||
require(mainScript);
|
||||
|
||||
Reference in New Issue
Block a user