electron-cli 0.2.8 → 0.3.0-alpha.1

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,235 @@
1
+ use std::collections::BTreeMap;
2
+
3
+ use anyhow::Result;
4
+ use serde::Serialize;
5
+
6
+ use crate::{cli::CommandArgs, output, project};
7
+
8
+ #[derive(Debug, Serialize)]
9
+ struct PlanReport {
10
+ project_type: ProjectType,
11
+ recommended_commands: BTreeMap<String, String>,
12
+ missing: Vec<String>,
13
+ risks: Vec<String>,
14
+ notes: Vec<String>,
15
+ }
16
+
17
+ #[derive(Debug, Serialize)]
18
+ #[serde(rename_all = "kebab-case")]
19
+ enum ProjectType {
20
+ ElectronForge,
21
+ Electron,
22
+ JavaScript,
23
+ Unknown,
24
+ }
25
+
26
+ pub fn run(args: CommandArgs) -> Result<()> {
27
+ let snapshot = project::inspect(&args.cwd)?;
28
+ let report = build_report(&snapshot);
29
+
30
+ if args.json {
31
+ output::json(&report)
32
+ } else {
33
+ println!("electron-cli plan");
34
+ println!();
35
+ println!("Project");
36
+ println!(" root: {}", snapshot.root);
37
+ match snapshot.package_label() {
38
+ Some(label) => println!(" package: {label}"),
39
+ None => println!(" package: not found"),
40
+ }
41
+ println!(" type: {}", report.project_type.as_str());
42
+
43
+ if !report.recommended_commands.is_empty() {
44
+ println!();
45
+ println!("Recommended Commands");
46
+ for (name, command) in &report.recommended_commands {
47
+ println!(" {name}: {command}");
48
+ }
49
+ }
50
+
51
+ if !report.missing.is_empty() {
52
+ println!();
53
+ println!("Missing");
54
+ for item in &report.missing {
55
+ println!(" {item}");
56
+ }
57
+ }
58
+
59
+ if !report.risks.is_empty() {
60
+ println!();
61
+ println!("Risks");
62
+ for risk in &report.risks {
63
+ println!(" {risk}");
64
+ }
65
+ }
66
+
67
+ if !report.notes.is_empty() {
68
+ println!();
69
+ println!("Notes");
70
+ for note in &report.notes {
71
+ println!(" {note}");
72
+ }
73
+ }
74
+
75
+ Ok(())
76
+ }
77
+ }
78
+
79
+ fn build_report(snapshot: &project::ProjectSnapshot) -> PlanReport {
80
+ let project_type = detect_project_type(snapshot);
81
+ let mut recommended_commands = BTreeMap::new();
82
+ let mut missing = Vec::new();
83
+ let mut risks = Vec::new();
84
+ let mut notes = Vec::new();
85
+
86
+ if let Some(script) = first_script(snapshot, &["start", "dev"]) {
87
+ recommended_commands.insert("dev".to_string(), run_script(snapshot, script));
88
+ } else if snapshot.electron_dependency.is_some() && snapshot.main.is_some() {
89
+ recommended_commands.insert("dev".to_string(), package_exec(snapshot, "electron ."));
90
+ missing.push(
91
+ "Add a start or dev script so humans and agents have a stable entrypoint.".to_string(),
92
+ );
93
+ } else {
94
+ missing.push("No start or dev script was found.".to_string());
95
+ }
96
+
97
+ if let Some(script) = first_script(snapshot, &["package", "pack"]) {
98
+ recommended_commands.insert("package".to_string(), run_script(snapshot, script));
99
+ } else if matches!(project_type, ProjectType::ElectronForge) {
100
+ missing.push(
101
+ "No package script was found even though Electron Forge is declared.".to_string(),
102
+ );
103
+ }
104
+
105
+ if let Some(script) = first_script(snapshot, &["make", "dist"]) {
106
+ recommended_commands.insert("make".to_string(), run_script(snapshot, script));
107
+ }
108
+
109
+ recommended_commands.insert(
110
+ "diagnostics".to_string(),
111
+ "electron-cli doctor --json".to_string(),
112
+ );
113
+ recommended_commands.insert(
114
+ "inspect".to_string(),
115
+ "electron-cli inspect --json".to_string(),
116
+ );
117
+
118
+ if snapshot.package_json.is_none() {
119
+ risks.push(
120
+ "No package.json was found, so Electron project detection is limited.".to_string(),
121
+ );
122
+ }
123
+
124
+ if snapshot.electron_dependency.is_none() {
125
+ risks.push("Electron is not declared in package dependencies.".to_string());
126
+ }
127
+
128
+ if snapshot.electron_dependency.is_some() && snapshot.main.is_none() {
129
+ risks.push("Electron is declared, but package.json has no main process entry.".to_string());
130
+ }
131
+
132
+ if snapshot.has_javascript_dependencies() && snapshot.package_manager.is_none() {
133
+ risks.push("JavaScript dependencies are declared, but no lockfile was found.".to_string());
134
+ }
135
+
136
+ if matches!(project_type, ProjectType::ElectronForge) {
137
+ notes.push("Electron Forge was detected; wrapping Forge commands is the safest workflow path today.".to_string());
138
+ } else if snapshot.electron_dependency.is_some() {
139
+ notes.push("Electron was detected without Forge; prefer inspection before choosing a packaging path.".to_string());
140
+ } else {
141
+ notes.push("This does not currently look like an Electron app.".to_string());
142
+ }
143
+
144
+ PlanReport {
145
+ project_type,
146
+ recommended_commands,
147
+ missing,
148
+ risks,
149
+ notes,
150
+ }
151
+ }
152
+
153
+ fn detect_project_type(snapshot: &project::ProjectSnapshot) -> ProjectType {
154
+ if !snapshot.forge_dependencies.is_empty()
155
+ || snapshot
156
+ .scripts
157
+ .values()
158
+ .any(|script| script.contains("electron-forge"))
159
+ {
160
+ ProjectType::ElectronForge
161
+ } else if snapshot.electron_dependency.is_some() {
162
+ ProjectType::Electron
163
+ } else if snapshot.package_json.is_some() {
164
+ ProjectType::JavaScript
165
+ } else {
166
+ ProjectType::Unknown
167
+ }
168
+ }
169
+
170
+ fn first_script<'a>(snapshot: &project::ProjectSnapshot, names: &'a [&'a str]) -> Option<&'a str> {
171
+ names
172
+ .iter()
173
+ .copied()
174
+ .find(|name| snapshot.scripts.contains_key(*name))
175
+ }
176
+
177
+ fn run_script(snapshot: &project::ProjectSnapshot, script: &str) -> String {
178
+ match snapshot.package_manager.as_deref() {
179
+ Some("bun") => format!("bun run {script}"),
180
+ Some("pnpm") => format!("pnpm run {script}"),
181
+ Some("yarn") => format!("yarn run {script}"),
182
+ Some("npm") | None => format!("npm run {script}"),
183
+ Some(package_manager) => format!("{package_manager} run {script}"),
184
+ }
185
+ }
186
+
187
+ fn package_exec(snapshot: &project::ProjectSnapshot, command: &str) -> String {
188
+ match snapshot.package_manager.as_deref() {
189
+ Some("bun") => format!("bunx {command}"),
190
+ Some("pnpm") => format!("pnpm exec {command}"),
191
+ Some("yarn") => format!("yarn {command}"),
192
+ Some("npm") | None => format!("npx {command}"),
193
+ Some(package_manager) => format!("{package_manager} exec {command}"),
194
+ }
195
+ }
196
+
197
+ impl ProjectType {
198
+ fn as_str(&self) -> &'static str {
199
+ match self {
200
+ ProjectType::ElectronForge => "electron-forge",
201
+ ProjectType::Electron => "electron",
202
+ ProjectType::JavaScript => "javascript",
203
+ ProjectType::Unknown => "unknown",
204
+ }
205
+ }
206
+ }
207
+
208
+ #[cfg(test)]
209
+ mod tests {
210
+ use std::path::Path;
211
+
212
+ use super::*;
213
+
214
+ #[test]
215
+ fn plans_for_electron_forge_fixture() {
216
+ let fixture = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/electron-forge");
217
+ let snapshot = project::inspect(&fixture).expect("fixture should inspect");
218
+
219
+ let report = build_report(&snapshot);
220
+
221
+ assert!(matches!(report.project_type, ProjectType::ElectronForge));
222
+ assert_eq!(
223
+ report.recommended_commands.get("dev").map(String::as_str),
224
+ Some("npm run start")
225
+ );
226
+ assert_eq!(
227
+ report
228
+ .recommended_commands
229
+ .get("package")
230
+ .map(String::as_str),
231
+ Some("npm run package")
232
+ );
233
+ assert!(report.risks.is_empty());
234
+ }
235
+ }
package/src/main.rs ADDED
@@ -0,0 +1,25 @@
1
+ mod cli;
2
+ mod commands;
3
+ mod output;
4
+ mod project;
5
+
6
+ use anyhow::Result;
7
+ use clap::Parser;
8
+ use cli::{Cli, Commands};
9
+
10
+ fn main() {
11
+ if let Err(error) = run() {
12
+ eprintln!("error: {error:#}");
13
+ std::process::exit(1);
14
+ }
15
+ }
16
+
17
+ fn run() -> Result<()> {
18
+ let cli = Cli::parse();
19
+
20
+ match cli.command {
21
+ Commands::Doctor(args) => commands::doctor::run(args),
22
+ Commands::Inspect(args) => commands::inspect::run(args),
23
+ Commands::Plan(args) => commands::plan::run(args),
24
+ }
25
+ }
package/src/output.rs ADDED
@@ -0,0 +1,7 @@
1
+ use anyhow::Result;
2
+ use serde::Serialize;
3
+
4
+ pub fn json(value: &impl Serialize) -> Result<()> {
5
+ println!("{}", serde_json::to_string_pretty(value)?);
6
+ Ok(())
7
+ }
package/src/project.rs ADDED
@@ -0,0 +1,320 @@
1
+ use std::{
2
+ collections::BTreeMap,
3
+ fs,
4
+ path::{Path, PathBuf},
5
+ };
6
+
7
+ use anyhow::{Context, Result};
8
+ use camino::Utf8PathBuf;
9
+ use serde::Serialize;
10
+ use serde_json::Value;
11
+
12
+ #[derive(Debug, Serialize)]
13
+ pub struct ProjectSnapshot {
14
+ pub root: Utf8PathBuf,
15
+ pub package_json: Option<Utf8PathBuf>,
16
+ pub name: Option<String>,
17
+ pub version: Option<String>,
18
+ pub main: Option<String>,
19
+ pub package_manager: Option<String>,
20
+ pub scripts: BTreeMap<String, String>,
21
+ pub dependencies: BTreeMap<String, String>,
22
+ pub dev_dependencies: BTreeMap<String, String>,
23
+ pub optional_dependencies: BTreeMap<String, String>,
24
+ pub peer_dependencies: BTreeMap<String, String>,
25
+ pub electron_dependency: Option<String>,
26
+ pub forge_dependencies: BTreeMap<String, String>,
27
+ pub signals: Vec<String>,
28
+ }
29
+
30
+ impl ProjectSnapshot {
31
+ pub fn package_label(&self) -> Option<String> {
32
+ match (&self.name, &self.version) {
33
+ (Some(name), Some(version)) => Some(format!("{name}@{version}")),
34
+ (Some(name), None) => Some(name.clone()),
35
+ (None, Some(version)) => Some(format!("version {version}")),
36
+ (None, None) => None,
37
+ }
38
+ }
39
+
40
+ pub fn has_javascript_dependencies(&self) -> bool {
41
+ !self.dependencies.is_empty()
42
+ || !self.dev_dependencies.is_empty()
43
+ || !self.optional_dependencies.is_empty()
44
+ || !self.peer_dependencies.is_empty()
45
+ }
46
+ }
47
+
48
+ pub fn inspect(cwd: &Path) -> Result<ProjectSnapshot> {
49
+ let cwd = cwd
50
+ .canonicalize()
51
+ .with_context(|| format!("Could not resolve {}", cwd.display()))?;
52
+
53
+ let package_json_path = find_upwards(&cwd, "package.json");
54
+ let root = package_json_path
55
+ .as_ref()
56
+ .and_then(|path| path.parent().map(Path::to_path_buf))
57
+ .unwrap_or(cwd);
58
+
59
+ let package_json = match &package_json_path {
60
+ Some(path) => {
61
+ let raw = fs::read_to_string(path)
62
+ .with_context(|| format!("Could not read {}", path.display()))?;
63
+ Some(
64
+ serde_json::from_str::<Value>(&raw)
65
+ .with_context(|| format!("Could not parse {}", path.display()))?,
66
+ )
67
+ }
68
+ None => None,
69
+ };
70
+
71
+ let scripts = package_json
72
+ .as_ref()
73
+ .map(|package| string_map(package.get("scripts")))
74
+ .unwrap_or_default();
75
+
76
+ let dependencies = package_json
77
+ .as_ref()
78
+ .map(|package| string_map(package.get("dependencies")))
79
+ .unwrap_or_default();
80
+
81
+ let dev_dependencies = package_json
82
+ .as_ref()
83
+ .map(|package| string_map(package.get("devDependencies")))
84
+ .unwrap_or_default();
85
+
86
+ let optional_dependencies = package_json
87
+ .as_ref()
88
+ .map(|package| string_map(package.get("optionalDependencies")))
89
+ .unwrap_or_default();
90
+
91
+ let peer_dependencies = package_json
92
+ .as_ref()
93
+ .map(|package| string_map(package.get("peerDependencies")))
94
+ .unwrap_or_default();
95
+
96
+ let all_dependencies = merge_dependencies([
97
+ &dependencies,
98
+ &dev_dependencies,
99
+ &optional_dependencies,
100
+ &peer_dependencies,
101
+ ]);
102
+
103
+ let electron_dependency = all_dependencies.get("electron").cloned();
104
+ let forge_dependencies = all_dependencies
105
+ .iter()
106
+ .filter(|(name, _)| name.starts_with("@electron-forge/"))
107
+ .map(|(name, version)| (name.clone(), version.clone()))
108
+ .collect::<BTreeMap<_, _>>();
109
+
110
+ let package_manager = detect_package_manager(&root);
111
+ let signals = build_signals(
112
+ &scripts,
113
+ &all_dependencies,
114
+ electron_dependency.as_ref(),
115
+ &forge_dependencies,
116
+ );
117
+
118
+ Ok(ProjectSnapshot {
119
+ root: utf8_path(root)?,
120
+ package_json: package_json_path.map(utf8_path).transpose()?,
121
+ name: package_json
122
+ .as_ref()
123
+ .and_then(|package| package.get("name"))
124
+ .and_then(Value::as_str)
125
+ .map(ToOwned::to_owned),
126
+ version: package_json
127
+ .as_ref()
128
+ .and_then(|package| package.get("version"))
129
+ .and_then(Value::as_str)
130
+ .map(ToOwned::to_owned),
131
+ main: package_json
132
+ .as_ref()
133
+ .and_then(|package| package.get("main"))
134
+ .and_then(Value::as_str)
135
+ .map(ToOwned::to_owned),
136
+ package_manager,
137
+ scripts,
138
+ dependencies,
139
+ dev_dependencies,
140
+ optional_dependencies,
141
+ peer_dependencies,
142
+ electron_dependency,
143
+ forge_dependencies,
144
+ signals,
145
+ })
146
+ }
147
+
148
+ fn string_map(value: Option<&Value>) -> BTreeMap<String, String> {
149
+ value
150
+ .and_then(Value::as_object)
151
+ .map(|object| {
152
+ object
153
+ .iter()
154
+ .filter_map(|(key, value)| {
155
+ value.as_str().map(|value| (key.clone(), value.to_string()))
156
+ })
157
+ .collect()
158
+ })
159
+ .unwrap_or_default()
160
+ }
161
+
162
+ fn merge_dependencies<'a>(
163
+ groups: impl IntoIterator<Item = &'a BTreeMap<String, String>>,
164
+ ) -> BTreeMap<String, String> {
165
+ let mut merged = BTreeMap::new();
166
+
167
+ for group in groups {
168
+ for (name, version) in group {
169
+ merged.insert(name.clone(), version.clone());
170
+ }
171
+ }
172
+
173
+ merged
174
+ }
175
+
176
+ fn detect_package_manager(root: &Path) -> Option<String> {
177
+ [
178
+ ("package-lock.json", "npm"),
179
+ ("npm-shrinkwrap.json", "npm"),
180
+ ("pnpm-lock.yaml", "pnpm"),
181
+ ("yarn.lock", "yarn"),
182
+ ("bun.lock", "bun"),
183
+ ("bun.lockb", "bun"),
184
+ ]
185
+ .iter()
186
+ .find_map(|(file, manager)| root.join(file).exists().then(|| manager.to_string()))
187
+ }
188
+
189
+ fn build_signals(
190
+ scripts: &BTreeMap<String, String>,
191
+ dependencies: &BTreeMap<String, String>,
192
+ electron_dependency: Option<&String>,
193
+ forge_dependencies: &BTreeMap<String, String>,
194
+ ) -> Vec<String> {
195
+ let mut signals = Vec::new();
196
+
197
+ if electron_dependency.is_some() {
198
+ signals.push("electron dependency declared".to_string());
199
+ }
200
+
201
+ if !forge_dependencies.is_empty() {
202
+ signals.push("electron forge dependency declared".to_string());
203
+ }
204
+
205
+ if dependencies.contains_key("vite") || dependencies.contains_key("@vitejs/plugin-react") {
206
+ signals.push("vite tooling detected".to_string());
207
+ }
208
+
209
+ if dependencies.contains_key("typescript") {
210
+ signals.push("typescript tooling detected".to_string());
211
+ }
212
+
213
+ if scripts
214
+ .values()
215
+ .any(|script| script.contains("electron") || script.contains("electron-forge"))
216
+ {
217
+ signals.push("electron command found in package scripts".to_string());
218
+ }
219
+
220
+ signals
221
+ }
222
+
223
+ fn find_upwards(start: &Path, file_name: &str) -> Option<PathBuf> {
224
+ let mut current = Some(start);
225
+
226
+ while let Some(path) = current {
227
+ let candidate = path.join(file_name);
228
+ if candidate.exists() {
229
+ return Some(candidate);
230
+ }
231
+
232
+ current = path.parent();
233
+ }
234
+
235
+ None
236
+ }
237
+
238
+ fn utf8_path(path: PathBuf) -> Result<Utf8PathBuf> {
239
+ Utf8PathBuf::from_path_buf(path).map_err(|path| {
240
+ anyhow::anyhow!(
241
+ "Path contains invalid UTF-8 and cannot be represented in JSON: {}",
242
+ path.display()
243
+ )
244
+ })
245
+ }
246
+
247
+ #[cfg(test)]
248
+ mod tests {
249
+ use super::*;
250
+
251
+ #[test]
252
+ fn maps_string_values_only() {
253
+ let value = serde_json::json!({
254
+ "electron": "^30.0.0",
255
+ "bad": false
256
+ });
257
+
258
+ let map = string_map(Some(&value));
259
+
260
+ assert_eq!(map.get("electron"), Some(&"^30.0.0".to_string()));
261
+ assert!(!map.contains_key("bad"));
262
+ }
263
+
264
+ #[test]
265
+ fn builds_electron_signals() {
266
+ let mut scripts = BTreeMap::new();
267
+ scripts.insert("start".to_string(), "electron-forge start".to_string());
268
+
269
+ let mut dependencies = BTreeMap::new();
270
+ dependencies.insert("electron".to_string(), "^30.0.0".to_string());
271
+ dependencies.insert("@electron-forge/cli".to_string(), "^7.0.0".to_string());
272
+ dependencies.insert("typescript".to_string(), "^5.0.0".to_string());
273
+
274
+ let forge = dependencies
275
+ .iter()
276
+ .filter(|(name, _)| name.starts_with("@electron-forge/"))
277
+ .map(|(name, version)| (name.clone(), version.clone()))
278
+ .collect::<BTreeMap<_, _>>();
279
+
280
+ let signals = build_signals(
281
+ &scripts,
282
+ &dependencies,
283
+ dependencies.get("electron"),
284
+ &forge,
285
+ );
286
+
287
+ assert!(signals.contains(&"electron dependency declared".to_string()));
288
+ assert!(signals.contains(&"electron forge dependency declared".to_string()));
289
+ assert!(signals.contains(&"typescript tooling detected".to_string()));
290
+ assert!(signals.contains(&"electron command found in package scripts".to_string()));
291
+ }
292
+
293
+ #[test]
294
+ fn inspects_electron_forge_fixture_from_nested_directory() {
295
+ let fixture = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/electron-forge");
296
+ let nested = fixture.join("src");
297
+
298
+ let snapshot = inspect(&nested).expect("fixture should inspect");
299
+
300
+ assert_eq!(snapshot.name.as_deref(), Some("fixture-electron-forge-app"));
301
+ assert_eq!(snapshot.version.as_deref(), Some("0.1.0"));
302
+ assert_eq!(snapshot.main.as_deref(), Some("src/main.ts"));
303
+ assert_eq!(snapshot.package_manager.as_deref(), Some("npm"));
304
+ assert_eq!(snapshot.electron_dependency.as_deref(), Some("^31.0.0"));
305
+ assert_eq!(
306
+ snapshot
307
+ .forge_dependencies
308
+ .get("@electron-forge/cli")
309
+ .map(String::as_str),
310
+ Some("^7.0.0")
311
+ );
312
+ assert!(snapshot.has_javascript_dependencies());
313
+ assert!(snapshot
314
+ .signals
315
+ .contains(&"electron forge dependency declared".to_string()));
316
+ assert!(snapshot
317
+ .signals
318
+ .contains(&"electron command found in package scripts".to_string()));
319
+ }
320
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "fixture-electron-forge-app",
3
+ "version": "0.1.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "fixture-electron-forge-app",
9
+ "version": "0.1.0",
10
+ "dependencies": {
11
+ "electron-squirrel-startup": "^1.0.1"
12
+ },
13
+ "devDependencies": {
14
+ "@electron-forge/cli": "^7.0.0",
15
+ "@electron-forge/maker-squirrel": "^7.0.0",
16
+ "@electron-forge/plugin-vite": "^7.0.0",
17
+ "electron": "^31.0.0",
18
+ "typescript": "^5.5.0",
19
+ "vite": "^5.0.0"
20
+ }
21
+ }
22
+ }
23
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "fixture-electron-forge-app",
3
+ "version": "0.1.0",
4
+ "main": "src/main.ts",
5
+ "scripts": {
6
+ "start": "electron-forge start",
7
+ "package": "electron-forge package",
8
+ "make": "electron-forge make"
9
+ },
10
+ "dependencies": {
11
+ "electron-squirrel-startup": "^1.0.1"
12
+ },
13
+ "devDependencies": {
14
+ "@electron-forge/cli": "^7.0.0",
15
+ "@electron-forge/maker-squirrel": "^7.0.0",
16
+ "@electron-forge/plugin-vite": "^7.0.0",
17
+ "electron": "^31.0.0",
18
+ "typescript": "^5.5.0",
19
+ "vite": "^5.0.0"
20
+ }
21
+ }
@@ -0,0 +1,8 @@
1
+ import { app, BrowserWindow } from "electron";
2
+
3
+ function createWindow() {
4
+ const window = new BrowserWindow({ width: 800, height: 600 });
5
+ window.loadURL("about:blank");
6
+ }
7
+
8
+ app.whenReady().then(createWindow);
package/.babelrc DELETED
@@ -1,14 +0,0 @@
1
- {
2
- "presets": ["stage-2","es2015"],
3
- "plugins": [
4
- "transform-runtime",
5
- "syntax-async-functions",
6
- "transform-async-to-generator",
7
- "transform-object-rest-spread"
8
- ],
9
- "env": {
10
- "test": {
11
- "plugins": [ "istanbul" ]
12
- }
13
- }
14
- }
package/.eslintrc DELETED
@@ -1,7 +0,0 @@
1
- {
2
- "extends": ["airbnb-base","standard"],
3
- "rules": {
4
- "complexity": [1, { "max": 5 }],
5
- "semi": "error"
6
- }
7
- }