electron-cli 0.3.0-alpha.1 → 0.3.0-alpha.11

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.
@@ -0,0 +1,287 @@
1
+ use std::{
2
+ path::{Path, PathBuf},
3
+ process::Command,
4
+ };
5
+
6
+ use anyhow::{bail, Context, Result};
7
+ use camino::Utf8PathBuf;
8
+ use serde::Serialize;
9
+
10
+ use crate::{cli::StartArgs, output, project::ProjectSnapshot};
11
+
12
+ #[derive(Debug, Serialize)]
13
+ struct StartReport {
14
+ project: ProjectSnapshot,
15
+ electron_path: Option<Utf8PathBuf>,
16
+ command: Vec<String>,
17
+ command_display: String,
18
+ passthrough_args: Vec<String>,
19
+ dry_run: bool,
20
+ status: StartStatus,
21
+ exit_code: Option<i32>,
22
+ warnings: Vec<String>,
23
+ }
24
+
25
+ #[derive(Debug, Serialize)]
26
+ #[serde(rename_all = "kebab-case")]
27
+ enum StartStatus {
28
+ Planned,
29
+ Exited,
30
+ }
31
+
32
+ pub fn run(args: StartArgs) -> Result<()> {
33
+ let snapshot = crate::project::inspect(&args.cwd)?;
34
+ let mut report = build_report(snapshot, &args)?;
35
+
36
+ if args.dry_run {
37
+ return print_report(&report, args.json);
38
+ }
39
+
40
+ let exit_code = execute_start(&report)?;
41
+ report.status = StartStatus::Exited;
42
+ report.exit_code = exit_code;
43
+
44
+ if args.json {
45
+ output::json(&report)?;
46
+ }
47
+
48
+ if let Some(code) = exit_code {
49
+ bail!("Electron exited with status code {code}");
50
+ } else {
51
+ Ok(())
52
+ }
53
+ }
54
+
55
+ fn build_report(snapshot: ProjectSnapshot, args: &StartArgs) -> Result<StartReport> {
56
+ let root = Path::new(snapshot.root.as_str());
57
+ let electron_path = find_electron_executable(root);
58
+ let mut warnings = Vec::new();
59
+
60
+ if snapshot.package_json.is_none() {
61
+ warnings.push("No package.json found.".to_string());
62
+ }
63
+
64
+ if snapshot.electron_dependency.is_none() {
65
+ warnings.push("No electron dependency is declared in package.json.".to_string());
66
+ }
67
+
68
+ if electron_path.is_none() {
69
+ warnings.push(
70
+ "Electron executable was not found under node_modules/electron/dist.".to_string(),
71
+ );
72
+ }
73
+
74
+ let mut command = Vec::new();
75
+ if let Some(path) = &electron_path {
76
+ command.push(path_arg(path));
77
+ command.push(snapshot.root.to_string());
78
+ command.extend(args.electron_args.clone());
79
+ }
80
+
81
+ let command_display = if command.is_empty() {
82
+ "electron executable not found".to_string()
83
+ } else {
84
+ display_command(&command)
85
+ };
86
+
87
+ Ok(StartReport {
88
+ project: snapshot,
89
+ electron_path: electron_path.map(utf8_path).transpose()?,
90
+ command,
91
+ command_display,
92
+ passthrough_args: args.electron_args.clone(),
93
+ dry_run: args.dry_run,
94
+ status: StartStatus::Planned,
95
+ exit_code: None,
96
+ warnings,
97
+ })
98
+ }
99
+
100
+ fn execute_start(report: &StartReport) -> Result<Option<i32>> {
101
+ if report.project.package_json.is_none() {
102
+ bail!("No package.json found. Run electron-cli start inside an Electron project.");
103
+ }
104
+
105
+ if report.project.electron_dependency.is_none() {
106
+ bail!("No electron dependency found. Install Electron before starting the app.");
107
+ }
108
+
109
+ let (program, args) = report
110
+ .command
111
+ .split_first()
112
+ .context("Electron executable was not found. Run your package manager install first.")?;
113
+
114
+ let status = Command::new(program)
115
+ .args(args)
116
+ .current_dir(report.project.root.as_str())
117
+ .status()
118
+ .with_context(|| format!("Could not execute {}", report.command_display))?;
119
+
120
+ if status.success() {
121
+ Ok(None)
122
+ } else {
123
+ Ok(status.code())
124
+ }
125
+ }
126
+
127
+ fn print_report(report: &StartReport, json: bool) -> Result<()> {
128
+ if json {
129
+ return output::json(report);
130
+ }
131
+
132
+ println!("electron-cli start");
133
+ println!();
134
+ println!("Project");
135
+ println!(" root: {}", report.project.root);
136
+ match report.project.package_label() {
137
+ Some(label) => println!(" package: {label}"),
138
+ None => println!(" package: not found"),
139
+ }
140
+
141
+ println!();
142
+ println!("Command");
143
+ println!(" {}", report.command_display);
144
+
145
+ if !report.warnings.is_empty() {
146
+ println!();
147
+ println!("Warnings");
148
+ for warning in &report.warnings {
149
+ println!(" {warning}");
150
+ }
151
+ }
152
+
153
+ Ok(())
154
+ }
155
+
156
+ fn find_electron_executable(root: &Path) -> Option<PathBuf> {
157
+ let dist = root.join("node_modules/electron/dist");
158
+ let candidates = electron_executable_candidates(&dist);
159
+
160
+ candidates.into_iter().find(|path| path.exists())
161
+ }
162
+
163
+ fn electron_executable_candidates(dist: &Path) -> Vec<PathBuf> {
164
+ if cfg!(target_os = "macos") {
165
+ vec![
166
+ dist.join("Electron.app/Contents/MacOS/Electron"),
167
+ dist.join("electron"),
168
+ ]
169
+ } else if cfg!(target_os = "windows") {
170
+ vec![dist.join("electron.exe")]
171
+ } else {
172
+ vec![dist.join("electron")]
173
+ }
174
+ }
175
+
176
+ fn display_command(command: &[String]) -> String {
177
+ command
178
+ .iter()
179
+ .map(|arg| shell_quote(arg))
180
+ .collect::<Vec<_>>()
181
+ .join(" ")
182
+ }
183
+
184
+ fn shell_quote(value: &str) -> String {
185
+ if value
186
+ .chars()
187
+ .all(|char| char.is_ascii_alphanumeric() || matches!(char, '.' | '/' | '-' | '_' | '@'))
188
+ {
189
+ value.to_string()
190
+ } else {
191
+ format!("'{}'", value.replace('\'', "'\\''"))
192
+ }
193
+ }
194
+
195
+ fn path_arg(path: &Path) -> String {
196
+ path.to_string_lossy().to_string()
197
+ }
198
+
199
+ fn utf8_path(path: PathBuf) -> Result<Utf8PathBuf> {
200
+ Utf8PathBuf::from_path_buf(path).map_err(|path| {
201
+ anyhow::anyhow!(
202
+ "Path contains invalid UTF-8 and cannot be represented in JSON: {}",
203
+ path.display()
204
+ )
205
+ })
206
+ }
207
+
208
+ #[cfg(test)]
209
+ mod tests {
210
+ use super::*;
211
+ use std::fs;
212
+
213
+ #[test]
214
+ fn plans_start_command_when_electron_dist_exists() {
215
+ let root = unique_temp_dir();
216
+ write_package_json(&root);
217
+ write_fake_electron(&root);
218
+
219
+ let args = StartArgs {
220
+ cwd: root.clone(),
221
+ dry_run: true,
222
+ json: true,
223
+ electron_args: vec!["--trace-warnings".to_string()],
224
+ };
225
+ let snapshot = crate::project::inspect(&root).expect("project should inspect");
226
+ let report = build_report(snapshot, &args).expect("report should build");
227
+
228
+ assert!(report.electron_path.is_some());
229
+ assert!(report.command_display.contains("--trace-warnings"));
230
+ assert!(report.warnings.is_empty());
231
+
232
+ let _ = fs::remove_dir_all(root);
233
+ }
234
+
235
+ #[test]
236
+ fn reports_missing_electron_dist_as_warning() {
237
+ let root = unique_temp_dir();
238
+ write_package_json(&root);
239
+
240
+ let args = StartArgs {
241
+ cwd: root.clone(),
242
+ dry_run: true,
243
+ json: true,
244
+ electron_args: Vec::new(),
245
+ };
246
+ let snapshot = crate::project::inspect(&root).expect("project should inspect");
247
+ let report = build_report(snapshot, &args).expect("report should build");
248
+
249
+ assert!(report.electron_path.is_none());
250
+ assert!(report.warnings.contains(
251
+ &"Electron executable was not found under node_modules/electron/dist.".to_string()
252
+ ));
253
+
254
+ let _ = fs::remove_dir_all(root);
255
+ }
256
+
257
+ fn write_package_json(root: &Path) {
258
+ fs::write(
259
+ root.join("package.json"),
260
+ r#"{"name":"starter","version":"0.1.0","main":"src/main.js","devDependencies":{"electron":"30.0.0"}}"#,
261
+ )
262
+ .expect("package.json should be written");
263
+ }
264
+
265
+ fn write_fake_electron(root: &Path) {
266
+ let path = electron_executable_candidates(&root.join("node_modules/electron/dist"))
267
+ .into_iter()
268
+ .next()
269
+ .expect("candidate should exist");
270
+ fs::create_dir_all(path.parent().expect("candidate should have parent"))
271
+ .expect("electron parent should be created");
272
+ fs::write(path, "").expect("electron executable should be written");
273
+ }
274
+
275
+ fn unique_temp_dir() -> PathBuf {
276
+ let nanos = std::time::SystemTime::now()
277
+ .duration_since(std::time::UNIX_EPOCH)
278
+ .expect("clock should be after epoch")
279
+ .as_nanos();
280
+ let path = std::env::temp_dir().join(format!(
281
+ "electron-cli-start-test-{}-{nanos}",
282
+ std::process::id()
283
+ ));
284
+ fs::create_dir_all(&path).expect("temp dir should be created");
285
+ path
286
+ }
287
+ }
package/src/main.rs CHANGED
@@ -19,7 +19,12 @@ fn run() -> Result<()> {
19
19
 
20
20
  match cli.command {
21
21
  Commands::Doctor(args) => commands::doctor::run(args),
22
+ Commands::Init(args) => commands::init::run(args),
22
23
  Commands::Inspect(args) => commands::inspect::run(args),
24
+ Commands::Make(args) => commands::make::run(args),
25
+ Commands::Package(args) => commands::package::run(args),
23
26
  Commands::Plan(args) => commands::plan::run(args),
27
+ Commands::Publish(args) => commands::publish::run(args),
28
+ Commands::Start(args) => commands::start::run(args),
24
29
  }
25
30
  }
package/src/project.rs CHANGED
@@ -15,6 +15,8 @@ pub struct ProjectSnapshot {
15
15
  pub package_json: Option<Utf8PathBuf>,
16
16
  pub name: Option<String>,
17
17
  pub version: Option<String>,
18
+ pub repository: Option<String>,
19
+ pub license: Option<String>,
18
20
  pub main: Option<String>,
19
21
  pub package_manager: Option<String>,
20
22
  pub scripts: BTreeMap<String, String>,
@@ -128,6 +130,12 @@ pub fn inspect(cwd: &Path) -> Result<ProjectSnapshot> {
128
130
  .and_then(|package| package.get("version"))
129
131
  .and_then(Value::as_str)
130
132
  .map(ToOwned::to_owned),
133
+ repository: package_json.as_ref().and_then(repository_value),
134
+ license: package_json
135
+ .as_ref()
136
+ .and_then(|package| package.get("license"))
137
+ .and_then(Value::as_str)
138
+ .map(ToOwned::to_owned),
131
139
  main: package_json
132
140
  .as_ref()
133
141
  .and_then(|package| package.get("main"))
@@ -145,6 +153,17 @@ pub fn inspect(cwd: &Path) -> Result<ProjectSnapshot> {
145
153
  })
146
154
  }
147
155
 
156
+ fn repository_value(package: &Value) -> Option<String> {
157
+ match package.get("repository") {
158
+ Some(Value::String(repository)) => Some(repository.clone()),
159
+ Some(Value::Object(repository)) => repository
160
+ .get("url")
161
+ .and_then(Value::as_str)
162
+ .map(ToOwned::to_owned),
163
+ _ => None,
164
+ }
165
+ }
166
+
148
167
  fn string_map(value: Option<&Value>) -> BTreeMap<String, String> {
149
168
  value
150
169
  .and_then(Value::as_object)
@@ -261,6 +280,25 @@ mod tests {
261
280
  assert!(!map.contains_key("bad"));
262
281
  }
263
282
 
283
+ #[test]
284
+ fn inspects_repository_url() {
285
+ let root = unique_temp_dir("repository");
286
+ fs::write(
287
+ root.join("package.json"),
288
+ r#"{"name":"repo-app","repository":{"type":"git","url":"git+https://github.com/Ikana/electron-cli.git"}}"#,
289
+ )
290
+ .expect("package.json should be written");
291
+
292
+ let snapshot = inspect(&root).expect("project should inspect");
293
+
294
+ assert_eq!(
295
+ snapshot.repository.as_deref(),
296
+ Some("git+https://github.com/Ikana/electron-cli.git")
297
+ );
298
+
299
+ let _ = fs::remove_dir_all(root);
300
+ }
301
+
264
302
  #[test]
265
303
  fn builds_electron_signals() {
266
304
  let mut scripts = BTreeMap::new();
@@ -317,4 +355,17 @@ mod tests {
317
355
  .signals
318
356
  .contains(&"electron command found in package scripts".to_string()));
319
357
  }
358
+
359
+ fn unique_temp_dir(label: &str) -> PathBuf {
360
+ let nanos = std::time::SystemTime::now()
361
+ .duration_since(std::time::UNIX_EPOCH)
362
+ .expect("clock should be after epoch")
363
+ .as_nanos();
364
+ let path = std::env::temp_dir().join(format!(
365
+ "electron-cli-project-{label}-{}-{nanos}",
366
+ std::process::id()
367
+ ));
368
+ fs::create_dir_all(&path).expect("temp dir should be created");
369
+ path
370
+ }
320
371
  }
@@ -0,0 +1,5 @@
1
+ node_modules
2
+ out
3
+ dist
4
+ .DS_Store
5
+ *.log
@@ -0,0 +1,82 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>electron-cli</title>
7
+ <style>
8
+ :root {
9
+ color-scheme: light dark;
10
+ font-family:
11
+ Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
12
+ "Segoe UI", sans-serif;
13
+ background: #f5f5f2;
14
+ color: #1d2328;
15
+ }
16
+
17
+ body {
18
+ margin: 0;
19
+ min-height: 100vh;
20
+ display: grid;
21
+ place-items: center;
22
+ }
23
+
24
+ main {
25
+ width: min(720px, calc(100vw - 48px));
26
+ }
27
+
28
+ h1 {
29
+ margin: 0 0 12px;
30
+ font-size: 5rem;
31
+ line-height: 0.92;
32
+ letter-spacing: 0;
33
+ }
34
+
35
+ p {
36
+ max-width: 48rem;
37
+ margin: 0 0 24px;
38
+ font-size: 1.05rem;
39
+ line-height: 1.6;
40
+ }
41
+
42
+ dl {
43
+ display: grid;
44
+ grid-template-columns: max-content 1fr;
45
+ gap: 8px 16px;
46
+ margin: 0;
47
+ padding-top: 20px;
48
+ border-top: 1px solid color-mix(in srgb, currentColor 24%, transparent);
49
+ }
50
+
51
+ dt {
52
+ font-weight: 700;
53
+ }
54
+
55
+ dd {
56
+ margin: 0;
57
+ }
58
+
59
+ @media (max-width: 640px) {
60
+ h1 {
61
+ font-size: 3rem;
62
+ }
63
+ }
64
+ </style>
65
+ </head>
66
+ <body>
67
+ <main>
68
+ <h1>electron-cli</h1>
69
+ <p>
70
+ This app was generated by the experimental Rust-native electron-cli
71
+ template.
72
+ </p>
73
+ <dl>
74
+ <dt>Runtime</dt>
75
+ <dd id="runtime">Loading...</dd>
76
+ <dt>Platform</dt>
77
+ <dd id="platform">Loading...</dd>
78
+ </dl>
79
+ </main>
80
+ <script src="./renderer.js"></script>
81
+ </body>
82
+ </html>
@@ -0,0 +1,33 @@
1
+ const { app, BrowserWindow } = require("electron");
2
+ const path = require("node:path");
3
+
4
+ function createWindow() {
5
+ const window = new BrowserWindow({
6
+ width: 960,
7
+ height: 640,
8
+ minWidth: 640,
9
+ minHeight: 420,
10
+ title: "electron-cli",
11
+ webPreferences: {
12
+ preload: path.join(__dirname, "preload.js"),
13
+ },
14
+ });
15
+
16
+ window.loadFile(path.join(__dirname, "index.html"));
17
+ }
18
+
19
+ app.whenReady().then(() => {
20
+ createWindow();
21
+
22
+ app.on("activate", () => {
23
+ if (BrowserWindow.getAllWindows().length === 0) {
24
+ createWindow();
25
+ }
26
+ });
27
+ });
28
+
29
+ app.on("window-all-closed", () => {
30
+ if (process.platform !== "darwin") {
31
+ app.quit();
32
+ }
33
+ });
@@ -0,0 +1,6 @@
1
+ const { contextBridge } = require("electron");
2
+
3
+ contextBridge.exposeInMainWorld("electronCli", {
4
+ platform: process.platform,
5
+ versions: process.versions,
6
+ });
@@ -0,0 +1,5 @@
1
+ const runtime = document.querySelector("#runtime");
2
+ const platform = document.querySelector("#platform");
3
+
4
+ runtime.textContent = `Electron ${window.electronCli.versions.electron}`;
5
+ platform.textContent = window.electronCli.platform;