electron-cli 0.3.0-alpha.4 → 0.3.0-alpha.6
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.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/README.md +8 -3
- package/package.json +1 -1
- package/src/cli.rs +71 -0
- package/src/commands/make.rs +32 -9
- package/src/commands/mod.rs +1 -0
- package/src/commands/package.rs +290 -18
- package/src/commands/plan.rs +14 -1
- package/src/commands/publish.rs +432 -0
- package/src/main.rs +1 -0
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
package/README.md
CHANGED
|
@@ -18,6 +18,7 @@ electron-cli init my-app
|
|
|
18
18
|
electron-cli start
|
|
19
19
|
electron-cli package
|
|
20
20
|
electron-cli make
|
|
21
|
+
electron-cli publish
|
|
21
22
|
electron-cli inspect --json
|
|
22
23
|
electron-cli doctor --json
|
|
23
24
|
electron-cli plan --json
|
|
@@ -25,12 +26,13 @@ electron-cli init my-app --dry-run --json
|
|
|
25
26
|
electron-cli start --dry-run --json
|
|
26
27
|
electron-cli package --dry-run --json
|
|
27
28
|
electron-cli make --dry-run --json
|
|
29
|
+
electron-cli publish --dry-run --json
|
|
28
30
|
```
|
|
29
31
|
|
|
30
32
|
Planned commands:
|
|
31
33
|
|
|
32
34
|
```sh
|
|
33
|
-
electron-cli publish
|
|
35
|
+
electron-cli publish --publisher github
|
|
34
36
|
```
|
|
35
37
|
|
|
36
38
|
The default `init` template is `minimal`, a built-in starter written by this project. Non-native template names are still passed to `create-electron-app` as an escape hatch while this project grows.
|
|
@@ -39,10 +41,11 @@ The Rust-native flow currently owns:
|
|
|
39
41
|
|
|
40
42
|
- `init --template minimal`: writes a local Electron starter without Electron Forge.
|
|
41
43
|
- `start`: launches the installed Electron runtime directly.
|
|
42
|
-
- `package`: copies the installed Electron runtime
|
|
44
|
+
- `package`: copies the installed Electron runtime, app files, and installed production dependency closure into a local app bundle for the current platform and architecture.
|
|
43
45
|
- `make`: runs `package` and writes a ZIP distributable under `out/make/zip/<platform>/<arch>/`.
|
|
46
|
+
- `publish`: runs `make` and publishes the distributable to a local directory with a manifest.
|
|
44
47
|
|
|
45
|
-
|
|
48
|
+
Remote publishers such as GitHub Releases are not implemented yet. Platform-specific makers, app metadata, signing, and notarization are also still TODO.
|
|
46
49
|
|
|
47
50
|
## Install
|
|
48
51
|
|
|
@@ -77,6 +80,7 @@ cargo run -- init my-app
|
|
|
77
80
|
cargo run -- start --dry-run
|
|
78
81
|
cargo run -- package --dry-run
|
|
79
82
|
cargo run -- make --dry-run
|
|
83
|
+
cargo run -- publish --dry-run
|
|
80
84
|
```
|
|
81
85
|
|
|
82
86
|
## Design Goals
|
|
@@ -101,6 +105,7 @@ The inspection and planning commands support `--json` so agents and scripts can
|
|
|
101
105
|
`start --dry-run --json` shows the Electron executable that will be launched.
|
|
102
106
|
`package --dry-run --json` shows the runtime and app file copy plan.
|
|
103
107
|
`make --dry-run --json` shows the package prerequisite and ZIP artifact path.
|
|
108
|
+
`publish --dry-run --json` shows the make prerequisite, destination artifact, and manifest path.
|
|
104
109
|
|
|
105
110
|
```sh
|
|
106
111
|
electron-cli plan --json
|
package/package.json
CHANGED
package/src/cli.rs
CHANGED
|
@@ -28,6 +28,8 @@ pub enum Commands {
|
|
|
28
28
|
Package(PackageArgs),
|
|
29
29
|
/// Recommend next commands and risks from the project snapshot.
|
|
30
30
|
Plan(CommandArgs),
|
|
31
|
+
/// Publish made artifacts to a distribution target.
|
|
32
|
+
Publish(PublishArgs),
|
|
31
33
|
/// Launch the current Electron app without Electron Forge.
|
|
32
34
|
Start(StartArgs),
|
|
33
35
|
}
|
|
@@ -183,6 +185,61 @@ pub struct MakeArgs {
|
|
|
183
185
|
pub json: bool,
|
|
184
186
|
}
|
|
185
187
|
|
|
188
|
+
#[derive(Debug, Clone, Args)]
|
|
189
|
+
pub struct PublishArgs {
|
|
190
|
+
/// Project directory to publish from.
|
|
191
|
+
#[arg(long, default_value = ".", value_name = "PATH")]
|
|
192
|
+
pub cwd: PathBuf,
|
|
193
|
+
|
|
194
|
+
/// Output directory used for package, make, and publish artifacts.
|
|
195
|
+
#[arg(long, default_value = "out", value_name = "PATH")]
|
|
196
|
+
pub out_dir: PathBuf,
|
|
197
|
+
|
|
198
|
+
/// Override the application name.
|
|
199
|
+
#[arg(long)]
|
|
200
|
+
pub name: Option<String>,
|
|
201
|
+
|
|
202
|
+
/// Target platform label. Defaults to the current platform.
|
|
203
|
+
#[arg(long)]
|
|
204
|
+
pub platform: Option<String>,
|
|
205
|
+
|
|
206
|
+
/// Target architecture label. Defaults to the current architecture.
|
|
207
|
+
#[arg(long)]
|
|
208
|
+
pub arch: Option<String>,
|
|
209
|
+
|
|
210
|
+
/// Maker target whose artifact should be published.
|
|
211
|
+
#[arg(long, value_enum, default_value_t = MakeTarget::Zip)]
|
|
212
|
+
pub target: MakeTarget,
|
|
213
|
+
|
|
214
|
+
/// Publisher target to use.
|
|
215
|
+
#[arg(long, value_enum, default_value_t = PublishTarget::Local)]
|
|
216
|
+
pub publisher: PublishTarget,
|
|
217
|
+
|
|
218
|
+
/// Destination for local published artifacts.
|
|
219
|
+
#[arg(long, default_value = "out/publish/local", value_name = "PATH")]
|
|
220
|
+
pub to: PathBuf,
|
|
221
|
+
|
|
222
|
+
/// Release channel label written into the publish manifest.
|
|
223
|
+
#[arg(long, default_value = "default")]
|
|
224
|
+
pub channel: String,
|
|
225
|
+
|
|
226
|
+
/// Reuse an existing make artifact instead of running package and make first.
|
|
227
|
+
#[arg(long)]
|
|
228
|
+
pub skip_make: bool,
|
|
229
|
+
|
|
230
|
+
/// Overwrite existing publish artifacts.
|
|
231
|
+
#[arg(long)]
|
|
232
|
+
pub force: bool,
|
|
233
|
+
|
|
234
|
+
/// Print the publish plan without creating files.
|
|
235
|
+
#[arg(long)]
|
|
236
|
+
pub dry_run: bool,
|
|
237
|
+
|
|
238
|
+
/// Emit machine-readable JSON.
|
|
239
|
+
#[arg(long)]
|
|
240
|
+
pub json: bool,
|
|
241
|
+
}
|
|
242
|
+
|
|
186
243
|
#[derive(Debug, Clone, Copy, ValueEnum)]
|
|
187
244
|
#[value(rename_all = "lower")]
|
|
188
245
|
pub enum PackageManager {
|
|
@@ -216,3 +273,17 @@ impl MakeTarget {
|
|
|
216
273
|
}
|
|
217
274
|
}
|
|
218
275
|
}
|
|
276
|
+
|
|
277
|
+
#[derive(Debug, Clone, Copy, ValueEnum)]
|
|
278
|
+
#[value(rename_all = "lower")]
|
|
279
|
+
pub enum PublishTarget {
|
|
280
|
+
Local,
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
impl PublishTarget {
|
|
284
|
+
pub fn as_str(self) -> &'static str {
|
|
285
|
+
match self {
|
|
286
|
+
PublishTarget::Local => "local",
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
package/src/commands/make.rs
CHANGED
|
@@ -17,7 +17,7 @@ use crate::{
|
|
|
17
17
|
};
|
|
18
18
|
|
|
19
19
|
#[derive(Debug, Serialize)]
|
|
20
|
-
struct MakeReport {
|
|
20
|
+
pub(crate) struct MakeReport {
|
|
21
21
|
package: PackageReport,
|
|
22
22
|
target: String,
|
|
23
23
|
skip_package: bool,
|
|
@@ -44,17 +44,12 @@ pub fn run(args: MakeArgs) -> Result<()> {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
execute_make(&mut report, &args)?;
|
|
47
|
-
report.
|
|
48
|
-
report.artifact_size = Some(
|
|
49
|
-
fs::metadata(report.artifact.as_str())
|
|
50
|
-
.with_context(|| format!("Could not stat {}", report.artifact))?
|
|
51
|
-
.len(),
|
|
52
|
-
);
|
|
47
|
+
report.mark_made()?;
|
|
53
48
|
|
|
54
49
|
print_report(&report, args.json)
|
|
55
50
|
}
|
|
56
51
|
|
|
57
|
-
fn build_report(args: &MakeArgs) -> Result<MakeReport> {
|
|
52
|
+
pub(crate) fn build_report(args: &MakeArgs) -> Result<MakeReport> {
|
|
58
53
|
let package_args = PackageArgs {
|
|
59
54
|
cwd: args.cwd.clone(),
|
|
60
55
|
out_dir: args.out_dir.clone(),
|
|
@@ -107,7 +102,7 @@ fn build_report(args: &MakeArgs) -> Result<MakeReport> {
|
|
|
107
102
|
})
|
|
108
103
|
}
|
|
109
104
|
|
|
110
|
-
fn execute_make(report: &mut MakeReport, args: &MakeArgs) -> Result<()> {
|
|
105
|
+
pub(crate) fn execute_make(report: &mut MakeReport, args: &MakeArgs) -> Result<()> {
|
|
111
106
|
if !args.skip_package {
|
|
112
107
|
package::execute_package(&report.package, args.force)?;
|
|
113
108
|
report.package.mark_packaged();
|
|
@@ -298,6 +293,34 @@ impl MakeStatus {
|
|
|
298
293
|
}
|
|
299
294
|
}
|
|
300
295
|
|
|
296
|
+
impl MakeReport {
|
|
297
|
+
pub(crate) fn mark_made(&mut self) -> Result<()> {
|
|
298
|
+
self.status = MakeStatus::Made;
|
|
299
|
+
self.artifact_size = Some(
|
|
300
|
+
fs::metadata(self.artifact.as_str())
|
|
301
|
+
.with_context(|| format!("Could not stat {}", self.artifact))?
|
|
302
|
+
.len(),
|
|
303
|
+
);
|
|
304
|
+
Ok(())
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
pub(crate) fn package(&self) -> &PackageReport {
|
|
308
|
+
&self.package
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
pub(crate) fn target(&self) -> &str {
|
|
312
|
+
&self.target
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
pub(crate) fn artifact(&self) -> &Utf8PathBuf {
|
|
316
|
+
&self.artifact
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
pub(crate) fn warnings(&self) -> &[String] {
|
|
320
|
+
&self.warnings
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
301
324
|
#[cfg(test)]
|
|
302
325
|
mod tests {
|
|
303
326
|
use super::*;
|
package/src/commands/mod.rs
CHANGED
package/src/commands/package.rs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
use std::{
|
|
2
|
+
collections::{BTreeMap, BTreeSet, VecDeque},
|
|
2
3
|
fs,
|
|
3
4
|
path::{Path, PathBuf},
|
|
4
5
|
};
|
|
@@ -6,6 +7,7 @@ use std::{
|
|
|
6
7
|
use anyhow::{bail, Context, Result};
|
|
7
8
|
use camino::Utf8PathBuf;
|
|
8
9
|
use serde::Serialize;
|
|
10
|
+
use serde_json::Value;
|
|
9
11
|
|
|
10
12
|
use crate::{cli::PackageArgs, output, project::ProjectSnapshot};
|
|
11
13
|
|
|
@@ -87,13 +89,6 @@ pub(crate) fn build_report(snapshot: ProjectSnapshot, args: &PackageArgs) -> Res
|
|
|
87
89
|
warnings.push("No package.json main field found.".to_string());
|
|
88
90
|
}
|
|
89
91
|
|
|
90
|
-
if has_runtime_dependencies(&snapshot) {
|
|
91
|
-
warnings.push(
|
|
92
|
-
"Packaging production node_modules is not implemented yet; this project declares runtime dependencies."
|
|
93
|
-
.to_string(),
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
92
|
if !electron_source.exists() {
|
|
98
93
|
warnings.push(format!(
|
|
99
94
|
"Electron runtime was not found at {}.",
|
|
@@ -115,11 +110,19 @@ pub(crate) fn build_report(snapshot: ProjectSnapshot, args: &PackageArgs) -> Res
|
|
|
115
110
|
));
|
|
116
111
|
}
|
|
117
112
|
|
|
113
|
+
warnings.extend(runtime_dependency_warnings(root, &snapshot));
|
|
114
|
+
|
|
118
115
|
let create_dirs = vec![package_root.clone(), app_resources_dir.clone()];
|
|
119
|
-
let copy_steps = [
|
|
116
|
+
let mut copy_steps = vec![
|
|
120
117
|
(electron_source, bundle_dir.clone()),
|
|
121
118
|
(root.to_path_buf(), app_resources_dir.join("app")),
|
|
122
119
|
];
|
|
120
|
+
if has_runtime_dependencies(&snapshot) {
|
|
121
|
+
copy_steps.push((
|
|
122
|
+
root.join("node_modules"),
|
|
123
|
+
app_resources_dir.join("app/node_modules"),
|
|
124
|
+
));
|
|
125
|
+
}
|
|
123
126
|
|
|
124
127
|
Ok(PackageReport {
|
|
125
128
|
project: snapshot,
|
|
@@ -159,12 +162,6 @@ pub(crate) fn execute_package(report: &PackageReport, force: bool) -> Result<()>
|
|
|
159
162
|
bail!("No electron dependency found. Install Electron before packaging the app.");
|
|
160
163
|
}
|
|
161
164
|
|
|
162
|
-
if has_runtime_dependencies(&report.project) {
|
|
163
|
-
bail!(
|
|
164
|
-
"Packaging production node_modules is not implemented yet. Remove runtime dependencies or wait for dependency pruning support."
|
|
165
|
-
);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
165
|
if report.platform != current_platform() {
|
|
169
166
|
bail!(
|
|
170
167
|
"Cross-platform packaging is not implemented yet. Requested {}, host is {}.",
|
|
@@ -223,6 +220,11 @@ pub(crate) fn execute_package(report: &PackageReport, force: bool) -> Result<()>
|
|
|
223
220
|
&app_dir,
|
|
224
221
|
Path::new(report.output_dir.as_str()),
|
|
225
222
|
)?;
|
|
223
|
+
copy_runtime_dependencies(
|
|
224
|
+
Path::new(report.project.root.as_str()),
|
|
225
|
+
&app_dir,
|
|
226
|
+
&report.project,
|
|
227
|
+
)?;
|
|
226
228
|
|
|
227
229
|
Ok(())
|
|
228
230
|
}
|
|
@@ -300,6 +302,180 @@ fn copy_project_files(source: &Path, destination: &Path, output_dir: &Path) -> R
|
|
|
300
302
|
Ok(())
|
|
301
303
|
}
|
|
302
304
|
|
|
305
|
+
#[derive(Debug)]
|
|
306
|
+
struct DependencyRequest {
|
|
307
|
+
name: String,
|
|
308
|
+
requested_by: Option<PathBuf>,
|
|
309
|
+
optional: bool,
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
fn copy_runtime_dependencies(
|
|
313
|
+
root: &Path,
|
|
314
|
+
app_dir: &Path,
|
|
315
|
+
snapshot: &ProjectSnapshot,
|
|
316
|
+
) -> Result<()> {
|
|
317
|
+
if !has_runtime_dependencies(snapshot) {
|
|
318
|
+
return Ok(());
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
let root_node_modules = root.join("node_modules");
|
|
322
|
+
let app_node_modules = app_dir.join("node_modules");
|
|
323
|
+
let mut queue = VecDeque::new();
|
|
324
|
+
let mut copied_paths = BTreeSet::new();
|
|
325
|
+
|
|
326
|
+
for name in snapshot.dependencies.keys() {
|
|
327
|
+
queue.push_back(DependencyRequest {
|
|
328
|
+
name: name.clone(),
|
|
329
|
+
requested_by: None,
|
|
330
|
+
optional: false,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
for name in snapshot.optional_dependencies.keys() {
|
|
335
|
+
queue.push_back(DependencyRequest {
|
|
336
|
+
name: name.clone(),
|
|
337
|
+
requested_by: None,
|
|
338
|
+
optional: true,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
while let Some(request) = queue.pop_front() {
|
|
343
|
+
let Some(package_dir) = resolve_dependency_dir(
|
|
344
|
+
&root_node_modules,
|
|
345
|
+
request.requested_by.as_deref(),
|
|
346
|
+
&request.name,
|
|
347
|
+
) else {
|
|
348
|
+
if request.optional {
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
bail!(
|
|
353
|
+
"Runtime dependency '{}' is not installed. Run your package manager install first.",
|
|
354
|
+
request.name
|
|
355
|
+
);
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
let canonical_package_dir = package_dir
|
|
359
|
+
.canonicalize()
|
|
360
|
+
.with_context(|| format!("Could not resolve {}", package_dir.display()))?;
|
|
361
|
+
let canonical_root_node_modules = root_node_modules
|
|
362
|
+
.canonicalize()
|
|
363
|
+
.with_context(|| format!("Could not resolve {}", root_node_modules.display()))?;
|
|
364
|
+
if !copied_paths.insert(canonical_package_dir.clone()) {
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
let relative_path = canonical_package_dir
|
|
369
|
+
.strip_prefix(&canonical_root_node_modules)
|
|
370
|
+
.with_context(|| {
|
|
371
|
+
format!(
|
|
372
|
+
"Could not make dependency {} relative to {}",
|
|
373
|
+
canonical_package_dir.display(),
|
|
374
|
+
canonical_root_node_modules.display()
|
|
375
|
+
)
|
|
376
|
+
})?;
|
|
377
|
+
let destination = app_node_modules.join(relative_path);
|
|
378
|
+
copy_recursively(&canonical_package_dir, &destination).with_context(|| {
|
|
379
|
+
format!(
|
|
380
|
+
"Could not copy runtime dependency {} to {}",
|
|
381
|
+
canonical_package_dir.display(),
|
|
382
|
+
destination.display()
|
|
383
|
+
)
|
|
384
|
+
})?;
|
|
385
|
+
|
|
386
|
+
let package_json = read_dependency_package_json(&canonical_package_dir)?;
|
|
387
|
+
for name in string_map(package_json.get("dependencies")).keys() {
|
|
388
|
+
queue.push_back(DependencyRequest {
|
|
389
|
+
name: name.clone(),
|
|
390
|
+
requested_by: Some(canonical_package_dir.clone()),
|
|
391
|
+
optional: false,
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
for name in string_map(package_json.get("optionalDependencies")).keys() {
|
|
395
|
+
queue.push_back(DependencyRequest {
|
|
396
|
+
name: name.clone(),
|
|
397
|
+
requested_by: Some(canonical_package_dir.clone()),
|
|
398
|
+
optional: true,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
Ok(())
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
fn runtime_dependency_warnings(root: &Path, snapshot: &ProjectSnapshot) -> Vec<String> {
|
|
407
|
+
let mut warnings = Vec::new();
|
|
408
|
+
let root_node_modules = root.join("node_modules");
|
|
409
|
+
|
|
410
|
+
for name in snapshot.dependencies.keys() {
|
|
411
|
+
if resolve_dependency_dir(&root_node_modules, None, name).is_none() {
|
|
412
|
+
warnings.push(format!(
|
|
413
|
+
"Runtime dependency is not installed and packaging will fail: {name}."
|
|
414
|
+
));
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
for name in snapshot.optional_dependencies.keys() {
|
|
419
|
+
if resolve_dependency_dir(&root_node_modules, None, name).is_none() {
|
|
420
|
+
warnings.push(format!(
|
|
421
|
+
"Optional runtime dependency is not installed and will be skipped: {name}."
|
|
422
|
+
));
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
warnings
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
fn resolve_dependency_dir(
|
|
430
|
+
root_node_modules: &Path,
|
|
431
|
+
requested_by: Option<&Path>,
|
|
432
|
+
name: &str,
|
|
433
|
+
) -> Option<PathBuf> {
|
|
434
|
+
let relative_path = dependency_relative_path(name);
|
|
435
|
+
|
|
436
|
+
if let Some(requested_by) = requested_by {
|
|
437
|
+
let nested = requested_by.join("node_modules").join(&relative_path);
|
|
438
|
+
if nested.exists() {
|
|
439
|
+
return Some(nested);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
let hoisted = root_node_modules.join(relative_path);
|
|
444
|
+
hoisted.exists().then_some(hoisted)
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
fn dependency_relative_path(name: &str) -> PathBuf {
|
|
448
|
+
let mut path = PathBuf::new();
|
|
449
|
+
for part in name.split('/') {
|
|
450
|
+
if !part.is_empty() {
|
|
451
|
+
path.push(part);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
path
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
fn read_dependency_package_json(package_dir: &Path) -> Result<Value> {
|
|
458
|
+
let package_json_path = package_dir.join("package.json");
|
|
459
|
+
let raw = fs::read_to_string(&package_json_path)
|
|
460
|
+
.with_context(|| format!("Could not read {}", package_json_path.display()))?;
|
|
461
|
+
serde_json::from_str::<Value>(&raw)
|
|
462
|
+
.with_context(|| format!("Could not parse {}", package_json_path.display()))
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
fn string_map(value: Option<&Value>) -> BTreeMap<String, String> {
|
|
466
|
+
value
|
|
467
|
+
.and_then(Value::as_object)
|
|
468
|
+
.map(|object| {
|
|
469
|
+
object
|
|
470
|
+
.iter()
|
|
471
|
+
.filter_map(|(key, value)| {
|
|
472
|
+
value.as_str().map(|value| (key.clone(), value.to_string()))
|
|
473
|
+
})
|
|
474
|
+
.collect()
|
|
475
|
+
})
|
|
476
|
+
.unwrap_or_default()
|
|
477
|
+
}
|
|
478
|
+
|
|
303
479
|
fn copy_recursively(source: &Path, destination: &Path) -> Result<()> {
|
|
304
480
|
if source.is_dir() {
|
|
305
481
|
fs::create_dir_all(destination)
|
|
@@ -622,7 +798,55 @@ mod tests {
|
|
|
622
798
|
}
|
|
623
799
|
|
|
624
800
|
#[test]
|
|
625
|
-
fn
|
|
801
|
+
fn packages_runtime_dependency_closure_from_node_modules() {
|
|
802
|
+
let root = unique_temp_dir("runtime-deps");
|
|
803
|
+
fs::write(
|
|
804
|
+
root.join("package.json"),
|
|
805
|
+
r#"{"name":"starter-app","version":"0.1.0","main":"src/main.js","dependencies":{"dep-a":"1.0.0"},"devDependencies":{"electron":"30.0.0","dev-only":"1.0.0"}}"#,
|
|
806
|
+
)
|
|
807
|
+
.expect("package.json should be written");
|
|
808
|
+
write_app_file(&root);
|
|
809
|
+
write_fake_electron_dist(&root);
|
|
810
|
+
write_dependency_package(
|
|
811
|
+
&root,
|
|
812
|
+
"dep-a",
|
|
813
|
+
r#"{"name":"dep-a","version":"1.0.0","dependencies":{"dep-b":"1.0.0"}}"#,
|
|
814
|
+
);
|
|
815
|
+
write_dependency_package(&root, "dep-b", r#"{"name":"dep-b","version":"1.0.0"}"#);
|
|
816
|
+
write_dependency_package(
|
|
817
|
+
&root,
|
|
818
|
+
"dev-only",
|
|
819
|
+
r#"{"name":"dev-only","version":"1.0.0"}"#,
|
|
820
|
+
);
|
|
821
|
+
|
|
822
|
+
let args = PackageArgs {
|
|
823
|
+
cwd: root.clone(),
|
|
824
|
+
out_dir: PathBuf::from("out"),
|
|
825
|
+
name: None,
|
|
826
|
+
platform: None,
|
|
827
|
+
arch: None,
|
|
828
|
+
force: false,
|
|
829
|
+
dry_run: false,
|
|
830
|
+
json: false,
|
|
831
|
+
};
|
|
832
|
+
let snapshot = crate::project::inspect(&root).expect("project should inspect");
|
|
833
|
+
let report = build_report(snapshot, &args).expect("report should build");
|
|
834
|
+
|
|
835
|
+
assert!(report.warnings.is_empty());
|
|
836
|
+
execute_package(&report, false).expect("package should succeed");
|
|
837
|
+
|
|
838
|
+
let app_node_modules = Path::new(report.app_resources_dir.as_str())
|
|
839
|
+
.join("app")
|
|
840
|
+
.join("node_modules");
|
|
841
|
+
assert!(app_node_modules.join("dep-a/package.json").exists());
|
|
842
|
+
assert!(app_node_modules.join("dep-b/package.json").exists());
|
|
843
|
+
assert!(!app_node_modules.join("dev-only").exists());
|
|
844
|
+
|
|
845
|
+
let _ = fs::remove_dir_all(root);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
#[test]
|
|
849
|
+
fn missing_required_runtime_dependency_fails() {
|
|
626
850
|
let root = unique_temp_dir("runtime-deps");
|
|
627
851
|
fs::write(
|
|
628
852
|
root.join("package.json"),
|
|
@@ -645,18 +869,55 @@ mod tests {
|
|
|
645
869
|
let snapshot = crate::project::inspect(&root).expect("project should inspect");
|
|
646
870
|
let report = build_report(snapshot, &args).expect("report should build");
|
|
647
871
|
|
|
648
|
-
assert!(report
|
|
649
|
-
.
|
|
650
|
-
|
|
872
|
+
assert!(report.warnings.contains(
|
|
873
|
+
&"Runtime dependency is not installed and packaging will fail: left-pad.".to_string()
|
|
874
|
+
));
|
|
651
875
|
assert!(execute_package(&report, false).is_err());
|
|
652
876
|
|
|
653
877
|
let _ = fs::remove_dir_all(root);
|
|
654
878
|
}
|
|
655
879
|
|
|
880
|
+
#[test]
|
|
881
|
+
fn missing_optional_runtime_dependency_is_skipped() {
|
|
882
|
+
let root = unique_temp_dir("optional-runtime-deps");
|
|
883
|
+
fs::write(
|
|
884
|
+
root.join("package.json"),
|
|
885
|
+
r#"{"name":"starter-app","version":"0.1.0","main":"src/main.js","optionalDependencies":{"optional-native":"1.0.0"},"devDependencies":{"electron":"30.0.0"}}"#,
|
|
886
|
+
)
|
|
887
|
+
.expect("package.json should be written");
|
|
888
|
+
write_app_file(&root);
|
|
889
|
+
write_fake_electron_dist(&root);
|
|
890
|
+
|
|
891
|
+
let args = PackageArgs {
|
|
892
|
+
cwd: root.clone(),
|
|
893
|
+
out_dir: PathBuf::from("out"),
|
|
894
|
+
name: None,
|
|
895
|
+
platform: None,
|
|
896
|
+
arch: None,
|
|
897
|
+
force: false,
|
|
898
|
+
dry_run: false,
|
|
899
|
+
json: false,
|
|
900
|
+
};
|
|
901
|
+
let snapshot = crate::project::inspect(&root).expect("project should inspect");
|
|
902
|
+
let report = build_report(snapshot, &args).expect("report should build");
|
|
903
|
+
|
|
904
|
+
assert!(report.warnings.contains(
|
|
905
|
+
&"Optional runtime dependency is not installed and will be skipped: optional-native."
|
|
906
|
+
.to_string()
|
|
907
|
+
));
|
|
908
|
+
execute_package(&report, false).expect("optional dependency should be skipped");
|
|
909
|
+
|
|
910
|
+
let _ = fs::remove_dir_all(root);
|
|
911
|
+
}
|
|
912
|
+
|
|
656
913
|
#[test]
|
|
657
914
|
fn cleans_scoped_package_names_for_bundle_paths() {
|
|
658
915
|
assert_eq!(clean_app_name("@scope/app"), "scope-app");
|
|
659
916
|
assert_eq!(sanitize_artifact_name("Starter App"), "starter-app");
|
|
917
|
+
assert_eq!(
|
|
918
|
+
dependency_relative_path("@scope/app"),
|
|
919
|
+
PathBuf::from("@scope/app")
|
|
920
|
+
);
|
|
660
921
|
}
|
|
661
922
|
|
|
662
923
|
fn write_package_json(root: &Path) {
|
|
@@ -673,6 +934,17 @@ mod tests {
|
|
|
673
934
|
.expect("main file should be written");
|
|
674
935
|
}
|
|
675
936
|
|
|
937
|
+
fn write_dependency_package(root: &Path, name: &str, package_json: &str) {
|
|
938
|
+
let package_dir = root
|
|
939
|
+
.join("node_modules")
|
|
940
|
+
.join(dependency_relative_path(name));
|
|
941
|
+
fs::create_dir_all(&package_dir).expect("dependency package dir should be created");
|
|
942
|
+
fs::write(package_dir.join("package.json"), package_json)
|
|
943
|
+
.expect("dependency package.json should be written");
|
|
944
|
+
fs::write(package_dir.join("index.js"), "module.exports = true;")
|
|
945
|
+
.expect("dependency index should be written");
|
|
946
|
+
}
|
|
947
|
+
|
|
676
948
|
fn write_fake_electron_dist(root: &Path) {
|
|
677
949
|
let dist = root.join("node_modules/electron/dist");
|
|
678
950
|
if current_platform() == "darwin" {
|
package/src/commands/plan.rs
CHANGED
|
@@ -112,6 +112,12 @@ fn build_report(snapshot: &project::ProjectSnapshot) -> PlanReport {
|
|
|
112
112
|
recommended_commands.insert("make".to_string(), run_script(snapshot, script));
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
if matches!(project_type, ProjectType::Electron) && snapshot.main.is_some() {
|
|
116
|
+
recommended_commands.insert("publish".to_string(), "electron-cli publish".to_string());
|
|
117
|
+
} else if let Some(script) = first_script(snapshot, &["publish", "release"]) {
|
|
118
|
+
recommended_commands.insert("publish".to_string(), run_script(snapshot, script));
|
|
119
|
+
}
|
|
120
|
+
|
|
115
121
|
recommended_commands.insert(
|
|
116
122
|
"diagnostics".to_string(),
|
|
117
123
|
"electron-cli doctor --json".to_string(),
|
|
@@ -142,7 +148,7 @@ fn build_report(snapshot: &project::ProjectSnapshot) -> PlanReport {
|
|
|
142
148
|
if matches!(project_type, ProjectType::ElectronForge) {
|
|
143
149
|
notes.push("Electron Forge was detected; its scripts remain the safest path for Forge-managed apps today.".to_string());
|
|
144
150
|
} else if snapshot.electron_dependency.is_some() {
|
|
145
|
-
notes.push("Electron was detected without Forge; electron-cli can start and
|
|
151
|
+
notes.push("Electron was detected without Forge; electron-cli can start, package, make, and publish local artifacts directly.".to_string());
|
|
146
152
|
} else {
|
|
147
153
|
notes.push("This does not currently look like an Electron app.".to_string());
|
|
148
154
|
}
|
|
@@ -276,5 +282,12 @@ mod tests {
|
|
|
276
282
|
report.recommended_commands.get("make").map(String::as_str),
|
|
277
283
|
Some("electron-cli make")
|
|
278
284
|
);
|
|
285
|
+
assert_eq!(
|
|
286
|
+
report
|
|
287
|
+
.recommended_commands
|
|
288
|
+
.get("publish")
|
|
289
|
+
.map(String::as_str),
|
|
290
|
+
Some("electron-cli publish")
|
|
291
|
+
);
|
|
279
292
|
}
|
|
280
293
|
}
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
use std::{
|
|
2
|
+
fs,
|
|
3
|
+
path::{Path, PathBuf},
|
|
4
|
+
time::{SystemTime, UNIX_EPOCH},
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
use anyhow::{bail, Context, Result};
|
|
8
|
+
use camino::Utf8PathBuf;
|
|
9
|
+
use serde::Serialize;
|
|
10
|
+
|
|
11
|
+
use crate::{
|
|
12
|
+
cli::{MakeArgs, PublishArgs},
|
|
13
|
+
commands::make::{self, MakeReport},
|
|
14
|
+
output,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
#[derive(Debug, Serialize)]
|
|
18
|
+
struct PublishReport {
|
|
19
|
+
make: MakeReport,
|
|
20
|
+
publisher: String,
|
|
21
|
+
channel: String,
|
|
22
|
+
destination_dir: Utf8PathBuf,
|
|
23
|
+
destination_artifact: Utf8PathBuf,
|
|
24
|
+
manifest: Utf8PathBuf,
|
|
25
|
+
skip_make: bool,
|
|
26
|
+
dry_run: bool,
|
|
27
|
+
status: PublishStatus,
|
|
28
|
+
published_at_unix_seconds: Option<u64>,
|
|
29
|
+
warnings: Vec<String>,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
#[derive(Debug, Serialize)]
|
|
33
|
+
#[serde(rename_all = "kebab-case")]
|
|
34
|
+
enum PublishStatus {
|
|
35
|
+
Planned,
|
|
36
|
+
Published,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
#[derive(Debug, Serialize)]
|
|
40
|
+
struct PublishManifest {
|
|
41
|
+
schema_version: u8,
|
|
42
|
+
publisher: String,
|
|
43
|
+
channel: String,
|
|
44
|
+
app_name: String,
|
|
45
|
+
package_name: Option<String>,
|
|
46
|
+
package_version: Option<String>,
|
|
47
|
+
platform: String,
|
|
48
|
+
arch: String,
|
|
49
|
+
target: String,
|
|
50
|
+
published_at_unix_seconds: u64,
|
|
51
|
+
artifacts: Vec<PublishedArtifact>,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
#[derive(Debug, Serialize)]
|
|
55
|
+
struct PublishedArtifact {
|
|
56
|
+
file: String,
|
|
57
|
+
path: Utf8PathBuf,
|
|
58
|
+
size: u64,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
pub fn run(args: PublishArgs) -> Result<()> {
|
|
62
|
+
let mut report = build_report(&args)?;
|
|
63
|
+
|
|
64
|
+
if args.dry_run {
|
|
65
|
+
return print_report(&report, args.json);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
execute_publish(&mut report, &args)?;
|
|
69
|
+
report.status = PublishStatus::Published;
|
|
70
|
+
|
|
71
|
+
print_report(&report, args.json)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
fn build_report(args: &PublishArgs) -> Result<PublishReport> {
|
|
75
|
+
let make_args = MakeArgs {
|
|
76
|
+
cwd: args.cwd.clone(),
|
|
77
|
+
out_dir: args.out_dir.clone(),
|
|
78
|
+
name: args.name.clone(),
|
|
79
|
+
platform: args.platform.clone(),
|
|
80
|
+
arch: args.arch.clone(),
|
|
81
|
+
target: args.target,
|
|
82
|
+
skip_package: false,
|
|
83
|
+
force: args.force,
|
|
84
|
+
dry_run: false,
|
|
85
|
+
json: false,
|
|
86
|
+
};
|
|
87
|
+
let make = make::build_report(&make_args)?;
|
|
88
|
+
let root = Path::new(make.package().project().root.as_str());
|
|
89
|
+
let publish_root = resolve_destination(root, &args.to);
|
|
90
|
+
let destination_dir = publish_root
|
|
91
|
+
.join(&args.channel)
|
|
92
|
+
.join(make.package().platform())
|
|
93
|
+
.join(make.package().arch());
|
|
94
|
+
let artifact_name = make
|
|
95
|
+
.artifact()
|
|
96
|
+
.file_name()
|
|
97
|
+
.context("Make artifact path has no file name")?;
|
|
98
|
+
let destination_artifact = destination_dir.join(artifact_name);
|
|
99
|
+
let manifest = destination_dir.join("manifest.json");
|
|
100
|
+
|
|
101
|
+
let mut warnings = make.warnings().to_vec();
|
|
102
|
+
if args.skip_make && !Path::new(make.artifact().as_str()).exists() {
|
|
103
|
+
warnings.push(format!(
|
|
104
|
+
"Make artifact does not exist: {}.",
|
|
105
|
+
make.artifact()
|
|
106
|
+
));
|
|
107
|
+
}
|
|
108
|
+
if destination_artifact.exists() && !args.force {
|
|
109
|
+
warnings.push(format!(
|
|
110
|
+
"Publish artifact already exists: {}. Use --force to overwrite it.",
|
|
111
|
+
destination_artifact.display()
|
|
112
|
+
));
|
|
113
|
+
}
|
|
114
|
+
if manifest.exists() && !args.force {
|
|
115
|
+
warnings.push(format!(
|
|
116
|
+
"Publish manifest already exists: {}. Use --force to overwrite it.",
|
|
117
|
+
manifest.display()
|
|
118
|
+
));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
Ok(PublishReport {
|
|
122
|
+
make,
|
|
123
|
+
publisher: args.publisher.as_str().to_string(),
|
|
124
|
+
channel: args.channel.clone(),
|
|
125
|
+
destination_dir: utf8_path(destination_dir)?,
|
|
126
|
+
destination_artifact: utf8_path(destination_artifact)?,
|
|
127
|
+
manifest: utf8_path(manifest)?,
|
|
128
|
+
skip_make: args.skip_make,
|
|
129
|
+
dry_run: args.dry_run,
|
|
130
|
+
status: PublishStatus::Planned,
|
|
131
|
+
published_at_unix_seconds: None,
|
|
132
|
+
warnings,
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
fn execute_publish(report: &mut PublishReport, args: &PublishArgs) -> Result<()> {
|
|
137
|
+
if !args.skip_make {
|
|
138
|
+
let make_args = MakeArgs {
|
|
139
|
+
cwd: args.cwd.clone(),
|
|
140
|
+
out_dir: args.out_dir.clone(),
|
|
141
|
+
name: args.name.clone(),
|
|
142
|
+
platform: args.platform.clone(),
|
|
143
|
+
arch: args.arch.clone(),
|
|
144
|
+
target: args.target,
|
|
145
|
+
skip_package: false,
|
|
146
|
+
force: args.force,
|
|
147
|
+
dry_run: false,
|
|
148
|
+
json: false,
|
|
149
|
+
};
|
|
150
|
+
make::execute_make(&mut report.make, &make_args)?;
|
|
151
|
+
report.make.mark_made()?;
|
|
152
|
+
} else if !Path::new(report.make.artifact().as_str()).exists() {
|
|
153
|
+
bail!(
|
|
154
|
+
"Make artifact does not exist: {}. Run without --skip-make or run electron-cli make first.",
|
|
155
|
+
report.make.artifact()
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
let destination_artifact = Path::new(report.destination_artifact.as_str());
|
|
160
|
+
let manifest = Path::new(report.manifest.as_str());
|
|
161
|
+
|
|
162
|
+
for path in [destination_artifact, manifest] {
|
|
163
|
+
if path.exists() {
|
|
164
|
+
if args.force {
|
|
165
|
+
fs::remove_file(path)
|
|
166
|
+
.with_context(|| format!("Could not remove {}", path.display()))?;
|
|
167
|
+
} else {
|
|
168
|
+
bail!(
|
|
169
|
+
"Publish output already exists: {}. Use --force to overwrite it.",
|
|
170
|
+
path.display()
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
fs::create_dir_all(report.destination_dir.as_str())
|
|
177
|
+
.with_context(|| format!("Could not create {}", report.destination_dir))?;
|
|
178
|
+
fs::copy(report.make.artifact().as_str(), destination_artifact).with_context(|| {
|
|
179
|
+
format!(
|
|
180
|
+
"Could not publish {} to {}",
|
|
181
|
+
report.make.artifact(),
|
|
182
|
+
destination_artifact.display()
|
|
183
|
+
)
|
|
184
|
+
})?;
|
|
185
|
+
|
|
186
|
+
let published_at_unix_seconds = now_unix_seconds()?;
|
|
187
|
+
report.published_at_unix_seconds = Some(published_at_unix_seconds);
|
|
188
|
+
let manifest_json =
|
|
189
|
+
serde_json::to_string_pretty(&build_manifest(report, published_at_unix_seconds)?)?;
|
|
190
|
+
fs::write(manifest, format!("{manifest_json}\n"))
|
|
191
|
+
.with_context(|| format!("Could not write {}", manifest.display()))?;
|
|
192
|
+
|
|
193
|
+
Ok(())
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
fn build_manifest(
|
|
197
|
+
report: &PublishReport,
|
|
198
|
+
published_at_unix_seconds: u64,
|
|
199
|
+
) -> Result<PublishManifest> {
|
|
200
|
+
let destination_artifact = Path::new(report.destination_artifact.as_str());
|
|
201
|
+
let artifact_size = fs::metadata(destination_artifact)
|
|
202
|
+
.with_context(|| format!("Could not stat {}", destination_artifact.display()))?
|
|
203
|
+
.len();
|
|
204
|
+
let artifact_file = destination_artifact
|
|
205
|
+
.file_name()
|
|
206
|
+
.and_then(|name| name.to_str())
|
|
207
|
+
.context("Published artifact path has no UTF-8 file name")?
|
|
208
|
+
.to_string();
|
|
209
|
+
|
|
210
|
+
Ok(PublishManifest {
|
|
211
|
+
schema_version: 1,
|
|
212
|
+
publisher: report.publisher.clone(),
|
|
213
|
+
channel: report.channel.clone(),
|
|
214
|
+
app_name: report.make.package().app_name().to_string(),
|
|
215
|
+
package_name: report.make.package().project().name.clone(),
|
|
216
|
+
package_version: report.make.package().project().version.clone(),
|
|
217
|
+
platform: report.make.package().platform().to_string(),
|
|
218
|
+
arch: report.make.package().arch().to_string(),
|
|
219
|
+
target: report.make.target().to_string(),
|
|
220
|
+
published_at_unix_seconds,
|
|
221
|
+
artifacts: vec![PublishedArtifact {
|
|
222
|
+
file: artifact_file,
|
|
223
|
+
path: report.destination_artifact.clone(),
|
|
224
|
+
size: artifact_size,
|
|
225
|
+
}],
|
|
226
|
+
})
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
fn print_report(report: &PublishReport, json: bool) -> Result<()> {
|
|
230
|
+
if json {
|
|
231
|
+
return output::json(report);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
println!("electron-cli publish");
|
|
235
|
+
println!();
|
|
236
|
+
println!("Project");
|
|
237
|
+
println!(" root: {}", report.make.package().project().root);
|
|
238
|
+
match report.make.package().project().package_label() {
|
|
239
|
+
Some(label) => println!(" package: {label}"),
|
|
240
|
+
None => println!(" package: not found"),
|
|
241
|
+
}
|
|
242
|
+
println!(" app name: {}", report.make.package().app_name());
|
|
243
|
+
println!(
|
|
244
|
+
" target: {} {} {}",
|
|
245
|
+
report.make.target(),
|
|
246
|
+
report.make.package().platform(),
|
|
247
|
+
report.make.package().arch()
|
|
248
|
+
);
|
|
249
|
+
println!(" publisher: {}", report.publisher);
|
|
250
|
+
println!(" channel: {}", report.channel);
|
|
251
|
+
println!(" status: {}", report.status.as_str());
|
|
252
|
+
|
|
253
|
+
println!();
|
|
254
|
+
println!("Publish");
|
|
255
|
+
println!(" artifact: {}", report.destination_artifact);
|
|
256
|
+
println!(" manifest: {}", report.manifest);
|
|
257
|
+
|
|
258
|
+
if !report.warnings.is_empty() {
|
|
259
|
+
println!();
|
|
260
|
+
println!("Warnings");
|
|
261
|
+
for warning in &report.warnings {
|
|
262
|
+
println!(" {warning}");
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
Ok(())
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
fn resolve_destination(root: &Path, destination: &Path) -> PathBuf {
|
|
270
|
+
if destination.is_absolute() {
|
|
271
|
+
destination.to_path_buf()
|
|
272
|
+
} else {
|
|
273
|
+
root.join(destination)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
fn now_unix_seconds() -> Result<u64> {
|
|
278
|
+
Ok(SystemTime::now()
|
|
279
|
+
.duration_since(UNIX_EPOCH)
|
|
280
|
+
.context("System clock is before the Unix epoch")?
|
|
281
|
+
.as_secs())
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
fn utf8_path(path: PathBuf) -> Result<Utf8PathBuf> {
|
|
285
|
+
Utf8PathBuf::from_path_buf(path).map_err(|path| {
|
|
286
|
+
anyhow::anyhow!(
|
|
287
|
+
"Path contains invalid UTF-8 and cannot be represented in JSON: {}",
|
|
288
|
+
path.display()
|
|
289
|
+
)
|
|
290
|
+
})
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
impl PublishStatus {
|
|
294
|
+
fn as_str(&self) -> &'static str {
|
|
295
|
+
match self {
|
|
296
|
+
PublishStatus::Planned => "planned",
|
|
297
|
+
PublishStatus::Published => "published",
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
#[cfg(test)]
|
|
303
|
+
mod tests {
|
|
304
|
+
use super::*;
|
|
305
|
+
|
|
306
|
+
#[test]
|
|
307
|
+
fn builds_local_publish_report() {
|
|
308
|
+
let root = unique_temp_dir("plan");
|
|
309
|
+
write_package_json(&root);
|
|
310
|
+
write_app_file(&root);
|
|
311
|
+
write_fake_electron_dist(&root);
|
|
312
|
+
|
|
313
|
+
let args = publish_args(root.clone(), true);
|
|
314
|
+
let report = build_report(&args).expect("report should build");
|
|
315
|
+
|
|
316
|
+
assert_eq!(report.publisher, "local");
|
|
317
|
+
assert_eq!(report.channel, "default");
|
|
318
|
+
assert!(Path::new(report.destination_artifact.as_str()).ends_with(
|
|
319
|
+
PathBuf::from("out")
|
|
320
|
+
.join("publish")
|
|
321
|
+
.join("local")
|
|
322
|
+
.join("default")
|
|
323
|
+
.join(report.make.package().platform())
|
|
324
|
+
.join(report.make.package().arch())
|
|
325
|
+
.join(format!(
|
|
326
|
+
"starter-app-{}-{}.zip",
|
|
327
|
+
report.make.package().platform(),
|
|
328
|
+
report.make.package().arch()
|
|
329
|
+
))
|
|
330
|
+
));
|
|
331
|
+
|
|
332
|
+
let _ = fs::remove_dir_all(root);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
#[test]
|
|
336
|
+
fn publishes_make_artifact_to_local_directory() {
|
|
337
|
+
let root = unique_temp_dir("execute");
|
|
338
|
+
write_package_json(&root);
|
|
339
|
+
write_app_file(&root);
|
|
340
|
+
write_fake_electron_dist(&root);
|
|
341
|
+
|
|
342
|
+
let args = publish_args(root.clone(), false);
|
|
343
|
+
let mut report = build_report(&args).expect("report should build");
|
|
344
|
+
|
|
345
|
+
execute_publish(&mut report, &args).expect("publish should succeed");
|
|
346
|
+
|
|
347
|
+
assert!(Path::new(report.destination_artifact.as_str()).exists());
|
|
348
|
+
assert!(Path::new(report.manifest.as_str()).exists());
|
|
349
|
+
let manifest =
|
|
350
|
+
fs::read_to_string(report.manifest.as_str()).expect("manifest should be readable");
|
|
351
|
+
assert!(manifest.contains("\"publisher\": \"local\""));
|
|
352
|
+
assert!(manifest.contains("\"app_name\": \"starter-app\""));
|
|
353
|
+
|
|
354
|
+
let _ = fs::remove_dir_all(root);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
#[test]
|
|
358
|
+
fn skip_make_requires_existing_artifact() {
|
|
359
|
+
let root = unique_temp_dir("skip-make");
|
|
360
|
+
write_package_json(&root);
|
|
361
|
+
write_app_file(&root);
|
|
362
|
+
write_fake_electron_dist(&root);
|
|
363
|
+
|
|
364
|
+
let mut args = publish_args(root.clone(), false);
|
|
365
|
+
args.skip_make = true;
|
|
366
|
+
let mut report = build_report(&args).expect("report should build");
|
|
367
|
+
|
|
368
|
+
assert!(execute_publish(&mut report, &args).is_err());
|
|
369
|
+
|
|
370
|
+
let _ = fs::remove_dir_all(root);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
fn publish_args(root: PathBuf, dry_run: bool) -> PublishArgs {
|
|
374
|
+
PublishArgs {
|
|
375
|
+
cwd: root,
|
|
376
|
+
out_dir: PathBuf::from("out"),
|
|
377
|
+
name: None,
|
|
378
|
+
platform: None,
|
|
379
|
+
arch: None,
|
|
380
|
+
target: crate::cli::MakeTarget::Zip,
|
|
381
|
+
publisher: crate::cli::PublishTarget::Local,
|
|
382
|
+
to: PathBuf::from("out/publish/local"),
|
|
383
|
+
channel: "default".to_string(),
|
|
384
|
+
skip_make: false,
|
|
385
|
+
force: false,
|
|
386
|
+
dry_run,
|
|
387
|
+
json: true,
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
fn write_package_json(root: &Path) {
|
|
392
|
+
fs::write(
|
|
393
|
+
root.join("package.json"),
|
|
394
|
+
r#"{"name":"starter-app","version":"0.1.0","main":"src/main.js","devDependencies":{"electron":"30.0.0"}}"#,
|
|
395
|
+
)
|
|
396
|
+
.expect("package.json should be written");
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
fn write_app_file(root: &Path) {
|
|
400
|
+
fs::create_dir_all(root.join("src")).expect("src should be created");
|
|
401
|
+
fs::write(root.join("src/main.js"), "console.log('hello');")
|
|
402
|
+
.expect("main file should be written");
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
fn write_fake_electron_dist(root: &Path) {
|
|
406
|
+
let dist = root.join("node_modules/electron/dist");
|
|
407
|
+
if cfg!(target_os = "macos") {
|
|
408
|
+
let app = dist.join("Electron.app/Contents/MacOS");
|
|
409
|
+
fs::create_dir_all(&app).expect("fake macOS electron app should be created");
|
|
410
|
+
fs::write(app.join("Electron"), "").expect("fake macOS binary should be written");
|
|
411
|
+
} else if cfg!(target_os = "windows") {
|
|
412
|
+
fs::create_dir_all(&dist).expect("fake electron dist should be created");
|
|
413
|
+
fs::write(dist.join("electron.exe"), "").expect("fake exe should be written");
|
|
414
|
+
} else {
|
|
415
|
+
fs::create_dir_all(&dist).expect("fake electron dist should be created");
|
|
416
|
+
fs::write(dist.join("electron"), "").expect("fake binary should be written");
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
fn unique_temp_dir(label: &str) -> PathBuf {
|
|
421
|
+
let nanos = std::time::SystemTime::now()
|
|
422
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
423
|
+
.expect("clock should be after epoch")
|
|
424
|
+
.as_nanos();
|
|
425
|
+
let path = std::env::temp_dir().join(format!(
|
|
426
|
+
"electron-cli-publish-{label}-{}-{nanos}",
|
|
427
|
+
std::process::id()
|
|
428
|
+
));
|
|
429
|
+
fs::create_dir_all(&path).expect("temp dir should be created");
|
|
430
|
+
path
|
|
431
|
+
}
|
|
432
|
+
}
|
package/src/main.rs
CHANGED
|
@@ -24,6 +24,7 @@ fn run() -> Result<()> {
|
|
|
24
24
|
Commands::Make(args) => commands::make::run(args),
|
|
25
25
|
Commands::Package(args) => commands::package::run(args),
|
|
26
26
|
Commands::Plan(args) => commands::plan::run(args),
|
|
27
|
+
Commands::Publish(args) => commands::publish::run(args),
|
|
27
28
|
Commands::Start(args) => commands::start::run(args),
|
|
28
29
|
}
|
|
29
30
|
}
|