@ionyx-apps/cli 0.4.8 → 0.4.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/bin/ionyx.exe +0 -0
  2. package/ionyx-cli/src/templates/angular/src-ionyx/Cargo.toml +30 -0
  3. package/ionyx-cli/src/templates/angular/src-ionyx/build.rs +5 -0
  4. package/ionyx-cli/src/templates/angular/src-ionyx/main.rs +48 -0
  5. package/ionyx-cli/src/templates/angular/src-ionyx/src/lib.rs +267 -0
  6. package/ionyx-cli/src/templates/basic/ionyx.config.json +3 -0
  7. package/ionyx-cli/src/templates/basic/package.json +6 -1
  8. package/ionyx-cli/src/templates/basic/src-ionyx/Cargo.toml +14 -12
  9. package/ionyx-cli/src/templates/basic/src-ionyx/main.rs +46 -3
  10. package/ionyx-cli/src/templates/leptos/package.json +6 -1
  11. package/ionyx-cli/src/templates/leptos/src-ionyx/Cargo.toml +14 -8
  12. package/ionyx-cli/src/templates/leptos/src-ionyx/main.rs +46 -3
  13. package/ionyx-cli/src/templates/mod.rs +1 -5
  14. package/ionyx-cli/src/templates/react/ionyx.config.json +4 -1
  15. package/ionyx-cli/src/templates/react/package.json +6 -1
  16. package/ionyx-cli/src/templates/react/src-ionyx/Cargo.toml +14 -12
  17. package/ionyx-cli/src/templates/react/src-ionyx/main.rs +46 -3
  18. package/ionyx-cli/src/templates/src-ionyx/Cargo.toml +15 -5
  19. package/ionyx-cli/src/templates/src-ionyx/bridge.rs +380 -0
  20. package/ionyx-cli/src/templates/src-ionyx/fusion.rs +47 -0
  21. package/ionyx-cli/src/templates/src-ionyx/ionyx.js +100 -0
  22. package/ionyx-cli/src/templates/src-ionyx/ipc_bridge.rs +206 -0
  23. package/ionyx-cli/src/templates/src-ionyx/lib.rs +204 -114
  24. package/ionyx-cli/src/templates/src-ionyx/main.rs +268 -3
  25. package/ionyx-cli/src/templates/src-ionyx/protocol.rs +98 -0
  26. package/ionyx-cli/src/templates/src-ionyx/window.rs +13 -0
  27. package/ionyx-cli/src/templates/svelte/package.json +6 -1
  28. package/ionyx-cli/src/templates/svelte/src-ionyx/Cargo.toml +14 -12
  29. package/ionyx-cli/src/templates/svelte/src-ionyx/main.rs +46 -3
  30. package/ionyx-cli/src/templates/vanilla/ionyx.config.json +3 -0
  31. package/ionyx-cli/src/templates/vanilla/package.json +6 -1
  32. package/ionyx-cli/src/templates/vanilla/src-ionyx/Cargo.toml +14 -12
  33. package/ionyx-cli/src/templates/vanilla/src-ionyx/main.rs +46 -3
  34. package/ionyx-cli/src/templates/vue/ionyx.config.json +4 -1
  35. package/ionyx-cli/src/templates/vue/package.json +6 -1
  36. package/ionyx-cli/src/templates/vue/src-ionyx/Cargo.toml +14 -12
  37. package/ionyx-cli/src/templates/vue/src-ionyx/main.rs +46 -3
  38. package/package.json +1 -1
@@ -0,0 +1,206 @@
1
+ use anyhow::Result;
2
+ use serde::{Deserialize, Serialize};
3
+ use serde_json::{json, Value};
4
+ use std::collections::HashMap;
5
+ use std::sync::Arc;
6
+ use tokio::sync::RwLock;
7
+ use tracing::{error, info};
8
+
9
+ use crate::config::Config;
10
+
11
+ #[derive(Debug, Clone, Serialize, Deserialize)]
12
+ pub struct IpcBridgeRequest {
13
+ pub source_app: String,
14
+ pub target_app: String,
15
+ pub command: String,
16
+ pub payload: Value,
17
+ pub id: String,
18
+ }
19
+
20
+ #[derive(Debug, Clone, Serialize, Deserialize)]
21
+ pub struct IpcBridgeResponse {
22
+ pub id: String,
23
+ pub success: bool,
24
+ pub data: Option<Value>,
25
+ pub error: Option<String>,
26
+ pub source_app: String,
27
+ pub target_app: String,
28
+ }
29
+
30
+ #[derive(Debug, Clone, Serialize, Deserialize)]
31
+ pub struct AppInfo {
32
+ pub name: String,
33
+ pub version: String,
34
+ pub pid: u32,
35
+ pub capabilities: Vec<String>,
36
+ }
37
+
38
+ pub struct IpcBridgeManager {
39
+ apps: Arc<RwLock<HashMap<String, AppInfo>>>,
40
+ config: Config,
41
+ }
42
+
43
+ impl IpcBridgeManager {
44
+ pub fn new(config: Config, _app_name: String) -> Self {
45
+ let apps = Arc::new(RwLock::new(HashMap::new()));
46
+
47
+ Self { apps, config }
48
+ }
49
+
50
+ pub async fn register_app(&self, app_info: AppInfo) -> Result<()> {
51
+ info!("Registering app: {} (PID: {})", app_info.name, app_info.pid);
52
+
53
+ let mut apps = self.apps.write().await;
54
+ apps.insert(app_info.name.clone(), app_info);
55
+
56
+ info!("App registered successfully. Total apps: {}", apps.len());
57
+ Ok(())
58
+ }
59
+
60
+ pub async fn unregister_app(&self, app_name: &str) -> Result<()> {
61
+ info!("Unregistering app: {}", app_name);
62
+
63
+ let mut apps = self.apps.write().await;
64
+ apps.remove(app_name);
65
+
66
+ info!("App unregistered successfully. Total apps: {}", apps.len());
67
+ Ok(())
68
+ }
69
+
70
+ pub async fn get_registered_apps(&self) -> Result<Vec<AppInfo>> {
71
+ let apps = self.apps.read().await;
72
+ Ok(apps.values().cloned().collect())
73
+ }
74
+
75
+ pub async fn send_to_app(&self, request: IpcBridgeRequest) -> Result<IpcBridgeResponse> {
76
+ info!(
77
+ "Sending IPC request from {} to {}: {}",
78
+ request.source_app, request.target_app, request.command
79
+ );
80
+
81
+ let source_app = request.source_app.clone();
82
+ let target_app = request.target_app.clone();
83
+
84
+ // Check if target app exists
85
+ let apps = self.apps.read().await;
86
+ if !apps.contains_key(&target_app) {
87
+ return Err(anyhow::anyhow!("Target app '{}' not found", target_app));
88
+ }
89
+
90
+ // Check if source app has permission to send to target app
91
+ if !self
92
+ .check_permissions(&source_app, &target_app, &request.command)
93
+ .await?
94
+ {
95
+ return Err(anyhow::anyhow!(
96
+ "Permission denied for command '{}' from '{}' to '{}'",
97
+ request.command,
98
+ source_app,
99
+ target_app
100
+ ));
101
+ }
102
+
103
+ // Process the request
104
+ let response = match self.process_bridge_request(&request).await {
105
+ Ok(data) => IpcBridgeResponse {
106
+ id: request.id,
107
+ success: true,
108
+ data: Some(data),
109
+ error: None,
110
+ source_app,
111
+ target_app,
112
+ },
113
+ Err(e) => IpcBridgeResponse {
114
+ id: request.id,
115
+ success: false,
116
+ data: None,
117
+ error: Some(e.to_string()),
118
+ source_app,
119
+ target_app,
120
+ },
121
+ };
122
+
123
+ info!(
124
+ "IPC request processed: {} -> {} ({})",
125
+ response.source_app, response.target_app, response.success
126
+ );
127
+
128
+ Ok(response)
129
+ }
130
+
131
+ pub async fn send_to_app_direct(&self, request: IpcBridgeRequest) -> Result<IpcBridgeResponse> {
132
+ self.process_bridge_request(&request)
133
+ .await
134
+ .map(|data| IpcBridgeResponse {
135
+ id: request.id,
136
+ success: true,
137
+ data: Some(data),
138
+ error: None,
139
+ source_app: request.source_app,
140
+ target_app: request.target_app,
141
+ })
142
+ }
143
+
144
+ async fn check_permissions(
145
+ &self,
146
+ _source_app: &str,
147
+ _target_app: &str,
148
+ _command: &str,
149
+ ) -> Result<bool> {
150
+ if self.config.permissions.bridge {
151
+ Ok(true)
152
+ } else {
153
+ error!("Bridge permission is disabled in config");
154
+ Ok(false)
155
+ }
156
+ }
157
+
158
+ async fn process_bridge_request(&self, request: &IpcBridgeRequest) -> Result<Value> {
159
+ match request.command.as_str() {
160
+ "app.getInfo" => {
161
+ let apps = self.apps.read().await;
162
+ if let Some(app_info) = apps.get(&request.target_app) {
163
+ Ok(json!({
164
+ "name": app_info.name,
165
+ "version": app_info.version,
166
+ "pid": app_info.pid,
167
+ "capabilities": app_info.capabilities
168
+ }))
169
+ } else {
170
+ Err(anyhow::anyhow!("App '{}' not found", request.target_app))
171
+ }
172
+ }
173
+ "app.list" => {
174
+ let apps = self.apps.read().await;
175
+ let app_list: Vec<Value> = apps
176
+ .values()
177
+ .map(|app| {
178
+ json!({
179
+ "name": app.name,
180
+ "version": app.version,
181
+ "pid": app.pid,
182
+ "capabilities": app.capabilities
183
+ })
184
+ })
185
+ .collect();
186
+ Ok(json!(app_list))
187
+ }
188
+ "system.broadcast" => {
189
+ // Broadcast message to all registered apps
190
+ info!(
191
+ "Broadcasting message from {}: {:?}",
192
+ request.source_app, request.payload
193
+ );
194
+ Ok(json!({
195
+ "broadcast_sent": true,
196
+ "message": "Message broadcasted to all apps",
197
+ "recipients": self.apps.read().await.len()
198
+ }))
199
+ }
200
+ _ => Err(anyhow::anyhow!(
201
+ "Unknown bridge command: {}",
202
+ request.command
203
+ )),
204
+ }
205
+ }
206
+ }
@@ -1,136 +1,226 @@
1
1
  use anyhow::Result;
2
- use serde::{Deserialize, Serialize};
2
+ use serde_json::json;
3
+ use std::env;
4
+ use std::path::PathBuf;
3
5
  use std::sync::{Arc, Mutex};
4
- use wry::webview::WebView;
5
- use tracing::{info, error, debug};
6
- use crate::security::SecurityManager;
7
-
8
- #[derive(Debug, Clone, Serialize, Deserialize)]
9
- pub struct IpcRequest {
10
- pub id: String,
11
- pub command: String,
12
- pub payload: serde_json::Value,
6
+ use tao::event_loop::{ControlFlow, EventLoop, EventLoopBuilder, EventLoopProxy};
7
+ use tracing::{error, info};
8
+ use wry::{WebContext, WebViewBuilder};
9
+
10
+ mod bridge;
11
+ mod config;
12
+ mod ipc_bridge;
13
+ mod protocol;
14
+ mod security;
15
+ mod window;
16
+ mod fusion;
17
+
18
+ use fusion::FusionManager;
19
+
20
+ use bridge::IpcBridge;
21
+ use config::Config;
22
+
23
+ // IPC Dispatcher for resolving frontend promises
24
+ #[derive(Debug, Clone)]
25
+ pub struct IpcDispatcher {
26
+ proxy: EventLoopProxy<String>,
13
27
  }
14
28
 
15
- #[derive(Debug, Clone, Serialize, Deserialize)]
16
- pub struct IpcResponse {
17
- pub id: String,
18
- pub success: bool,
19
- pub data: Option<serde_json::Value>,
20
- pub error: Option<String>,
21
- }
22
-
23
- pub struct IpcBridge {
24
- config: crate::config::Config,
25
- security: SecurityManager,
26
- }
27
-
28
- impl IpcBridge {
29
- pub fn new(config: crate::config::Config) -> Self {
30
- Self {
31
- config,
32
- security: SecurityManager::new(config.clone()),
33
- }
29
+ impl IpcDispatcher {
30
+ pub fn new(proxy: EventLoopProxy<String>) -> Self {
31
+ Self { proxy }
34
32
  }
35
33
 
36
- pub async fn handle_request(
34
+ pub fn resolve_promise(
37
35
  &self,
38
- request: String,
39
- webview: &Arc<Mutex<Option<WebView>>>,
40
- proxy: &tao::event_loop::EventLoopProxy<UserEvent>,
41
- ) -> Result<()> {
42
- info!("📨 Processing IPC request: {}", request);
43
-
44
- let ipc_request: IpcRequest = serde_json::from_str(&request)
45
- .map_err(|e| anyhow::anyhow!("Failed to parse IPC request: {}", e))?;
46
-
47
- // Security check
48
- if !self.security.is_command_allowed(&ipc_request.command) {
49
- return Err(anyhow::anyhow!("Command
50
-
51
- not allowed", ipc_request.command));
52
- }
53
-
54
- // Process command
55
- let response = match self.process_command(&ipc_request).await {
56
- Ok(data) => IpcResponse {
57
- id: ipc_request.id,
58
- success: true,
59
- data: Some(data),
60
- error: None,
61
- },
62
- Err(e) => IpcResponse {
63
- id: ipc_request.id,
64
- success: false,
65
- data: None,
66
- error: Some(e.to_string()),
67
- },
68
- };
36
+ request_id: &str,
37
+ success: bool,
38
+ data: Option<serde_json::Value>,
39
+ error: Option<String>,
40
+ ) {
41
+ let response = json!({
42
+ "id": request_id,
43
+ "success": success,
44
+ "data": data,
45
+ "error": error
46
+ });
69
47
 
70
- // Send response back to frontend
71
- let response_json = serde_json::to_string(&response)?;
72
48
  let js_code = format!(
73
- "if (window.ionyx && window.ionyx.resolveResponse) {{ window.ionyx.resolveResponse(
74
-
75
- , {}); }}",
76
- response.id,
77
- response_json
49
+ "if (window.ionyx && window.ionyx.resolveResponse) {{ window.ionyx.resolveResponse('{}', {}); }}",
50
+ request_id,
51
+ response
78
52
  );
79
53
 
80
- // Send JavaScript code to main thread
81
- proxy.send_event(UserEvent::JavaScript(js_code));
82
-
83
- info!("✅ IPC response sent for: {}", ipc_request.command);
84
- Ok(())
85
- }
86
-
87
- async fn process_command(&self, request: &IpcRequest) -> Result<serde_json::Value> {
88
- match request.command.as_str() {
89
- "app.getVersion" => self.handle_get_version().await,
90
- "app.getConfig" => self.handle_get_config().await,
91
- "fs.exists" => self.handle_fs_exists(&request.payload).await,
92
- "os.info" => self.handle_os_info().await,
93
- _ => Err(anyhow::anyhow!("Unknown command: {}", request.command)),
54
+ if let Err(e) = self.proxy.send_event(js_code) {
55
+ error!("Failed to send IPC response to event loop: {}", e);
94
56
  }
95
57
  }
58
+ }
59
+
60
+ pub struct Builder;
96
61
 
97
- async fn handle_get_version(&self) -> Result<serde_json::Value> {
98
- Ok(serde_json::json!({
99
- "name": "my-ionyx-app",
100
- "version": "1.0.0",
101
- "platform": std::env::consts::OS,
102
- "arch": std::env::consts::ARCH
103
- }))
62
+ impl Default for Builder {
63
+ fn default() -> Self {
64
+ Self::new()
104
65
  }
66
+ }
105
67
 
106
- async fn handle_get_config(&self) -> Result<serde_json::Value> {
107
- Ok(serde_json::json!({
108
- "window": self.config.window,
109
- "permissions": self.config.ionyx.permissions,
110
- "security": self.config.ionyx.security
111
- }))
68
+ impl Builder {
69
+ pub fn new() -> Self {
70
+ Self
112
71
  }
113
72
 
114
- async fn handle_fs_exists(&self, payload: &serde_json::Value) -> Result<serde_json::Value> {
115
- let path = payload["path"]
116
- .as_str()
117
- .ok_or_else(|| "default")
118
- .to_string();
73
+ pub fn run(self) -> Result<()> {
74
+ let rt = tokio::runtime::Runtime::new()?;
75
+ rt.block_on(async {
76
+ self.run_async().await
77
+ })
78
+ }
119
79
 
120
- let path_buf = PathBuf::from(&path);
121
- let exists = path_buf.exists();
122
-
123
- self.security.check_file_access(&path_buf)?;
80
+ async fn run_async(self) -> Result<()> {
81
+ // Initialize logging
82
+ tracing_subscriber::fmt::init();
83
+
84
+ info!("Starting Ionyx Framework Application");
85
+
86
+ // Load configuration
87
+ let config = Config::load().await?;
88
+ info!("Loaded configuration: {}", config.app.name);
89
+
90
+ // Create event loop
91
+ let event_loop: EventLoop<String> = EventLoopBuilder::with_user_event().build();
92
+ let _web_context = WebContext::new(Some(Default::default()));
93
+
94
+ // Create window
95
+ let window = config
96
+ .window
97
+ .apply_to_builder(tao::window::WindowBuilder::new())
98
+ .build(&event_loop)?;
99
+
100
+ // Get the current working directory
101
+ let current_dir = env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
102
+ let dist_dir = if current_dir.ends_with("dist") {
103
+ // If we're already in the dist directory, use current directory
104
+ current_dir.clone()
105
+ } else {
106
+ // Otherwise, look for dist subdirectory
107
+ current_dir.join("dist")
108
+ };
124
109
 
125
- Ok(serde_json::json!({ "exists": exists }))
126
- }
110
+ info!("Current working directory: {:?}", current_dir);
111
+ info!("Dist directory: {:?}", dist_dir);
112
+
113
+ // Create Fusion Manager
114
+ let fusion_manager = Arc::new(FusionManager::new());
115
+ // Initialize with basic app info (Inspired by NW.js context)
116
+ fusion_manager.set("app_name", serde_json::Value::String(config.app.name.clone()));
117
+ fusion_manager.set("app_version", serde_json::Value::String(config.app.version.clone()));
118
+ fusion_manager.set("os", serde_json::Value::String(os_info::get().os_type().to_string()));
119
+
120
+ // Create IPC bridge
121
+ let ipc_bridge = std::sync::Arc::new(IpcBridge::new(
122
+ config.clone(),
123
+ IpcDispatcher::new(event_loop.create_proxy()),
124
+ fusion_manager.clone(),
125
+ ));
126
+
127
+ // Import Windows-specific trait for WebView extensions
128
+ #[cfg(target_os = "windows")]
129
+ use wry::WebViewBuilderExtWindows;
130
+
131
+ // Determine webview URL based on build mode
132
+ let webview = if cfg!(debug_assertions) {
133
+ // Development mode: use IONYX_URL from environment
134
+ let dev_url = std::env::var("IONYX_URL")
135
+ .unwrap_or_else(|_| {
136
+ format!("http://127.0.0.1:{}", config.development.port)
137
+ });
138
+
139
+ info!("Development mode: Connecting to {}", dev_url);
140
+
141
+ let builder = WebViewBuilder::new()
142
+ .with_initialization_script(include_str!("ionyx.js"))
143
+ .with_initialization_script(&fusion_manager.generate_sync_script())
144
+ .with_ipc_handler(move |request| {
145
+ let bridge = ipc_bridge.clone();
146
+ let request_body = request.body().clone();
147
+ tokio::spawn(async move {
148
+ if let Err(e) = bridge.handle_request_with_response(request_body).await {
149
+ error!("IPC request failed: {}", e);
150
+ }
151
+ });
152
+ })
153
+ .with_navigation_handler(|url| {
154
+ // Allow localhost and file:// protocols in debug mode
155
+ url.starts_with("http://localhost:") ||
156
+ url.starts_with("https://localhost:") ||
157
+ url.starts_with("http://127.0.0.1:") ||
158
+ url.starts_with("https://127.0.0.1:") ||
159
+ url.starts_with("file://") ||
160
+ url.starts_with("ionyx://")
161
+ })
162
+ .with_url(&dev_url)
163
+ .with_devtools(true);
164
+
165
+ #[cfg(target_os = "windows")]
166
+ let builder = builder.with_additional_browser_args("--enable-unsafe-webgpu --enable-features=WebGPU");
167
+
168
+ builder
169
+ } else {
170
+ // Release mode: serve embedded assets from binary
171
+ info!("Release mode: Loading embedded assets from binary");
172
+
173
+ let builder = WebViewBuilder::new()
174
+ .with_initialization_script(include_str!("ionyx.js"))
175
+ .with_initialization_script(&fusion_manager.generate_sync_script())
176
+ .with_ipc_handler(move |request| {
177
+ let bridge = ipc_bridge.clone();
178
+ let request_body = request.body().clone();
179
+ tokio::spawn(async move {
180
+ if let Err(e) = bridge.handle_request_with_response(request_body).await {
181
+ error!("IPC request failed: {}", e);
182
+ }
183
+ });
184
+ })
185
+ .with_custom_protocol("ionyx".to_string(), move |_, request| {
186
+ let handler = crate::protocol::CustomProtocolHandler::new();
187
+ handler.handle_request(request)
188
+ })
189
+ .with_url("ionyx://index.html");
190
+
191
+ #[cfg(target_os = "windows")]
192
+ let builder = builder.with_additional_browser_args("--enable-unsafe-webgpu --enable-features=WebGPU");
193
+
194
+ builder
195
+ };
127
196
 
128
- async fn handle_os_info(&self) -> Result<serde_json::Value> {
129
- Ok(serde_json::json!({
130
- "platform": std::env::consts::OS,
131
- "arch": std::env::consts::ARCH,
132
- "hostname": gethostname::get().unwrap_or_else(|_| "unknown".to_string()),
133
- "version": os_info::get()
134
- }))
197
+ let webview = Arc::new(Mutex::new(webview.build(&window)?));
198
+
199
+ // Run event loop
200
+ event_loop.run(move |event, _, control_flow| {
201
+ *control_flow = ControlFlow::Wait;
202
+
203
+ match event {
204
+ tao::event::Event::UserEvent(js_code) => {
205
+ // Execute JavaScript code from IPC dispatcher
206
+ info!("Executing JavaScript code: {}", js_code);
207
+ if let Ok(webview_guard) = webview.lock() {
208
+ if let Err(e) = webview_guard.evaluate_script(&js_code) {
209
+ error!("Failed to execute JavaScript code: {}", e);
210
+ } else {
211
+ info!("JavaScript code executed successfully");
212
+ }
213
+ }
214
+ }
215
+ tao::event::Event::WindowEvent {
216
+ event: tao::event::WindowEvent::CloseRequested,
217
+ ..
218
+ } => {
219
+ info!("Window close requested");
220
+ *control_flow = ControlFlow::Exit;
221
+ }
222
+ _ => (),
223
+ }
224
+ });
135
225
  }
136
- }
226
+ }