electron-cli 0.3.0-alpha.2 → 0.3.0-alpha.20
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 +5380 -101
- package/Cargo.toml +17 -1
- package/README.md +103 -12
- package/package.json +2 -1
- package/src/cli.rs +226 -4
- package/src/commands/init.rs +443 -27
- package/src/commands/make.rs +3076 -0
- package/src/commands/mod.rs +4 -0
- package/src/commands/package.rs +3238 -0
- package/src/commands/plan.rs +65 -5
- package/src/commands/publish.rs +1832 -0
- package/src/commands/start.rs +287 -0
- package/src/forge_config.rs +547 -0
- package/src/main.rs +5 -0
- package/src/project.rs +52 -1
- package/templates/minimal/gitignore +5 -0
- package/templates/minimal/src/index.html +82 -0
- package/templates/minimal/src/main.js +33 -0
- package/templates/minimal/src/preload.js +6 -0
- package/templates/minimal/src/renderer.js +5 -0
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
use std::{
|
|
2
|
+
fs,
|
|
3
|
+
path::{Path, PathBuf},
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
use anyhow::{anyhow, Context, Result};
|
|
7
|
+
use serde_json::Value as JsonValue;
|
|
8
|
+
|
|
9
|
+
use crate::project::ProjectSnapshot;
|
|
10
|
+
|
|
11
|
+
#[derive(Clone, Debug, Default)]
|
|
12
|
+
pub(crate) struct ProjectConfig {
|
|
13
|
+
package: Option<JsonValue>,
|
|
14
|
+
forge: Option<JsonValue>,
|
|
15
|
+
electron_cli: Option<JsonValue>,
|
|
16
|
+
warnings: Vec<String>,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
pub(crate) fn read(snapshot: &ProjectSnapshot) -> Result<ProjectConfig> {
|
|
20
|
+
let package = read_package_json(snapshot)?;
|
|
21
|
+
let root = Path::new(snapshot.root.as_str());
|
|
22
|
+
let mut warnings = Vec::new();
|
|
23
|
+
let forge = resolve_forge_config(root, package.as_ref(), &mut warnings);
|
|
24
|
+
let electron_cli = package
|
|
25
|
+
.as_ref()
|
|
26
|
+
.and_then(|package| {
|
|
27
|
+
package
|
|
28
|
+
.get("electronCli")
|
|
29
|
+
.or_else(|| package.get("electron-cli"))
|
|
30
|
+
})
|
|
31
|
+
.cloned();
|
|
32
|
+
|
|
33
|
+
Ok(ProjectConfig {
|
|
34
|
+
package,
|
|
35
|
+
forge,
|
|
36
|
+
electron_cli,
|
|
37
|
+
warnings,
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
fn read_package_json(snapshot: &ProjectSnapshot) -> Result<Option<JsonValue>> {
|
|
42
|
+
let Some(package_json_path) = &snapshot.package_json else {
|
|
43
|
+
return Ok(None);
|
|
44
|
+
};
|
|
45
|
+
let package_json_path = Path::new(package_json_path.as_str());
|
|
46
|
+
let raw = fs::read_to_string(package_json_path)
|
|
47
|
+
.with_context(|| format!("Could not read {}", package_json_path.display()))?;
|
|
48
|
+
serde_json::from_str::<JsonValue>(&raw)
|
|
49
|
+
.with_context(|| format!("Could not parse {}", package_json_path.display()))
|
|
50
|
+
.map(Some)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
fn resolve_forge_config(
|
|
54
|
+
root: &Path,
|
|
55
|
+
package: Option<&JsonValue>,
|
|
56
|
+
warnings: &mut Vec<String>,
|
|
57
|
+
) -> Option<JsonValue> {
|
|
58
|
+
match package
|
|
59
|
+
.and_then(|package| package.get("config"))
|
|
60
|
+
.and_then(|config| config.get("forge"))
|
|
61
|
+
{
|
|
62
|
+
Some(JsonValue::Object(_)) => {
|
|
63
|
+
return package
|
|
64
|
+
.and_then(|package| package.get("config"))
|
|
65
|
+
.and_then(|config| config.get("forge"))
|
|
66
|
+
.cloned()
|
|
67
|
+
}
|
|
68
|
+
Some(JsonValue::String(path)) => {
|
|
69
|
+
return read_forge_config_file(root, Path::new(path), warnings);
|
|
70
|
+
}
|
|
71
|
+
Some(_) => {
|
|
72
|
+
warnings.push(
|
|
73
|
+
"package.json config.forge must be an object or relative config file path."
|
|
74
|
+
.to_string(),
|
|
75
|
+
);
|
|
76
|
+
return None;
|
|
77
|
+
}
|
|
78
|
+
None => {}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
for candidate in [
|
|
82
|
+
"forge.config.js",
|
|
83
|
+
"forge.config.cjs",
|
|
84
|
+
"forge.config.mjs",
|
|
85
|
+
"forge.config.ts",
|
|
86
|
+
] {
|
|
87
|
+
let path = root.join(candidate);
|
|
88
|
+
if path.exists() {
|
|
89
|
+
return read_forge_config_file(root, &PathBuf::from(candidate), warnings);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
None
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
fn read_forge_config_file(
|
|
97
|
+
root: &Path,
|
|
98
|
+
configured_path: &Path,
|
|
99
|
+
warnings: &mut Vec<String>,
|
|
100
|
+
) -> Option<JsonValue> {
|
|
101
|
+
let path = if configured_path.is_absolute() {
|
|
102
|
+
configured_path.to_path_buf()
|
|
103
|
+
} else {
|
|
104
|
+
root.join(configured_path)
|
|
105
|
+
};
|
|
106
|
+
let display = path.display();
|
|
107
|
+
|
|
108
|
+
let raw = match fs::read_to_string(&path) {
|
|
109
|
+
Ok(raw) => raw,
|
|
110
|
+
Err(error) => {
|
|
111
|
+
warnings.push(format!(
|
|
112
|
+
"Could not read Forge config file {display}: {error}."
|
|
113
|
+
));
|
|
114
|
+
return None;
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
match parse_forge_config_file(&raw, &path) {
|
|
119
|
+
Ok(config) => Some(config),
|
|
120
|
+
Err(error) => {
|
|
121
|
+
warnings.push(format!(
|
|
122
|
+
"Could not parse Forge config file {display} without JavaScript execution: {error}."
|
|
123
|
+
));
|
|
124
|
+
None
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
fn parse_forge_config_file(raw: &str, path: &Path) -> Result<JsonValue> {
|
|
130
|
+
if path.extension().and_then(|extension| extension.to_str()) == Some("json") {
|
|
131
|
+
return serde_json::from_str(raw).with_context(|| "Forge JSON config is not valid JSON");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let object_literal = extract_static_config_object(raw)
|
|
135
|
+
.ok_or_else(|| anyhow!("expected a static object export"))?;
|
|
136
|
+
json5::from_str::<JsonValue>(&object_literal)
|
|
137
|
+
.with_context(|| "static Forge config object is not valid JSON5")
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
fn extract_static_config_object(source: &str) -> Option<String> {
|
|
141
|
+
for marker in ["module.exports", "exports.default"] {
|
|
142
|
+
if let Some(object) = extract_assignment_object(source, marker) {
|
|
143
|
+
return Some(object);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if let Some(object) = extract_export_default_object(source) {
|
|
148
|
+
return Some(object);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if let Some(identifier) = export_default_identifier(source)
|
|
152
|
+
.or_else(|| assignment_identifier(source, "module.exports"))
|
|
153
|
+
.or_else(|| assignment_identifier(source, "exports.default"))
|
|
154
|
+
{
|
|
155
|
+
return extract_variable_object(source, &identifier);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
None
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
fn extract_assignment_object(source: &str, marker: &str) -> Option<String> {
|
|
162
|
+
let marker_index = source.find(marker)?;
|
|
163
|
+
let after_marker = &source[marker_index + marker.len()..];
|
|
164
|
+
let equals = after_marker.find('=')?;
|
|
165
|
+
let after_equals_start = marker_index + marker.len() + equals + 1;
|
|
166
|
+
let object_start = find_next_object_start(source, after_equals_start)?;
|
|
167
|
+
extract_balanced_object(source, object_start)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
fn extract_export_default_object(source: &str) -> Option<String> {
|
|
171
|
+
let marker = "export default";
|
|
172
|
+
let marker_index = source.find(marker)?;
|
|
173
|
+
let after_marker = marker_index + marker.len();
|
|
174
|
+
let object_start = find_next_object_start(source, after_marker)?;
|
|
175
|
+
let identifier = read_identifier(source, skip_whitespace(source, after_marker)).0;
|
|
176
|
+
if identifier.is_some() {
|
|
177
|
+
return None;
|
|
178
|
+
}
|
|
179
|
+
extract_balanced_object(source, object_start)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
fn export_default_identifier(source: &str) -> Option<String> {
|
|
183
|
+
let marker = "export default";
|
|
184
|
+
let marker_index = source.find(marker)?;
|
|
185
|
+
let start = skip_whitespace(source, marker_index + marker.len());
|
|
186
|
+
read_identifier(source, start).0
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
fn assignment_identifier(source: &str, marker: &str) -> Option<String> {
|
|
190
|
+
let marker_index = source.find(marker)?;
|
|
191
|
+
let after_marker = &source[marker_index + marker.len()..];
|
|
192
|
+
let equals = after_marker.find('=')?;
|
|
193
|
+
let start = skip_whitespace(source, marker_index + marker.len() + equals + 1);
|
|
194
|
+
read_identifier(source, start).0
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
fn extract_variable_object(source: &str, identifier: &str) -> Option<String> {
|
|
198
|
+
for keyword in ["const", "let", "var"] {
|
|
199
|
+
for (keyword_index, _) in source.match_indices(keyword) {
|
|
200
|
+
if !is_word_boundary(source, keyword_index, keyword.len()) {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
let start = skip_whitespace(source, keyword_index + keyword.len());
|
|
204
|
+
let (name, after_name) = read_identifier(source, start);
|
|
205
|
+
if name.as_deref() != Some(identifier) {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
let rest = &source[after_name..];
|
|
209
|
+
let equals = rest.find('=')?;
|
|
210
|
+
let object_start = find_next_object_start(source, after_name + equals + 1)?;
|
|
211
|
+
return extract_balanced_object(source, object_start);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
None
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
fn find_next_object_start(source: &str, start: usize) -> Option<usize> {
|
|
219
|
+
let bytes = source.as_bytes();
|
|
220
|
+
let mut index = start;
|
|
221
|
+
while index < bytes.len() {
|
|
222
|
+
match bytes[index] {
|
|
223
|
+
b'{' => return Some(index),
|
|
224
|
+
b';' | b'\n' if !source[start..index].trim().is_empty() => return None,
|
|
225
|
+
_ => index += 1,
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
None
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
fn extract_balanced_object(source: &str, object_start: usize) -> Option<String> {
|
|
232
|
+
let bytes = source.as_bytes();
|
|
233
|
+
let mut index = object_start;
|
|
234
|
+
let mut depth = 0usize;
|
|
235
|
+
let mut state = ScanState::Normal;
|
|
236
|
+
|
|
237
|
+
while index < bytes.len() {
|
|
238
|
+
match state {
|
|
239
|
+
ScanState::Normal => match bytes[index] {
|
|
240
|
+
b'{' => {
|
|
241
|
+
depth += 1;
|
|
242
|
+
index += 1;
|
|
243
|
+
}
|
|
244
|
+
b'}' => {
|
|
245
|
+
depth = depth.checked_sub(1)?;
|
|
246
|
+
index += 1;
|
|
247
|
+
if depth == 0 {
|
|
248
|
+
return Some(source[object_start..index].to_string());
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
b'\'' | b'"' | b'`' => {
|
|
252
|
+
state = ScanState::String(bytes[index]);
|
|
253
|
+
index += 1;
|
|
254
|
+
}
|
|
255
|
+
b'/' if bytes.get(index + 1) == Some(&b'/') => {
|
|
256
|
+
state = ScanState::LineComment;
|
|
257
|
+
index += 2;
|
|
258
|
+
}
|
|
259
|
+
b'/' if bytes.get(index + 1) == Some(&b'*') => {
|
|
260
|
+
state = ScanState::BlockComment;
|
|
261
|
+
index += 2;
|
|
262
|
+
}
|
|
263
|
+
_ => index += 1,
|
|
264
|
+
},
|
|
265
|
+
ScanState::String(quote) => {
|
|
266
|
+
if bytes[index] == b'\\' {
|
|
267
|
+
index = (index + 2).min(bytes.len());
|
|
268
|
+
} else if bytes[index] == quote {
|
|
269
|
+
state = ScanState::Normal;
|
|
270
|
+
index += 1;
|
|
271
|
+
} else {
|
|
272
|
+
index += 1;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
ScanState::LineComment => {
|
|
276
|
+
if bytes[index] == b'\n' {
|
|
277
|
+
state = ScanState::Normal;
|
|
278
|
+
}
|
|
279
|
+
index += 1;
|
|
280
|
+
}
|
|
281
|
+
ScanState::BlockComment => {
|
|
282
|
+
if bytes[index] == b'*' && bytes.get(index + 1) == Some(&b'/') {
|
|
283
|
+
state = ScanState::Normal;
|
|
284
|
+
index += 2;
|
|
285
|
+
} else {
|
|
286
|
+
index += 1;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
None
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
fn skip_whitespace(source: &str, start: usize) -> usize {
|
|
296
|
+
let bytes = source.as_bytes();
|
|
297
|
+
let mut index = start;
|
|
298
|
+
while index < bytes.len() && bytes[index].is_ascii_whitespace() {
|
|
299
|
+
index += 1;
|
|
300
|
+
}
|
|
301
|
+
index
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
fn read_identifier(source: &str, start: usize) -> (Option<String>, usize) {
|
|
305
|
+
let bytes = source.as_bytes();
|
|
306
|
+
if bytes.get(start).is_none_or(|byte| !identifier_start(*byte)) {
|
|
307
|
+
return (None, start);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
let mut index = start + 1;
|
|
311
|
+
while index < bytes.len() && identifier_continue(bytes[index]) {
|
|
312
|
+
index += 1;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
(Some(source[start..index].to_string()), index)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
fn is_word_boundary(source: &str, start: usize, len: usize) -> bool {
|
|
319
|
+
let bytes = source.as_bytes();
|
|
320
|
+
let before = start
|
|
321
|
+
.checked_sub(1)
|
|
322
|
+
.and_then(|index| bytes.get(index))
|
|
323
|
+
.copied();
|
|
324
|
+
let after = bytes.get(start + len).copied();
|
|
325
|
+
before.is_none_or(|byte| !identifier_continue(byte))
|
|
326
|
+
&& after.is_none_or(|byte| !identifier_continue(byte))
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
fn identifier_start(byte: u8) -> bool {
|
|
330
|
+
byte.is_ascii_alphabetic() || matches!(byte, b'_' | b'$')
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
fn identifier_continue(byte: u8) -> bool {
|
|
334
|
+
identifier_start(byte) || byte.is_ascii_digit()
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
#[derive(Clone, Copy, Debug)]
|
|
338
|
+
enum ScanState {
|
|
339
|
+
Normal,
|
|
340
|
+
String(u8),
|
|
341
|
+
LineComment,
|
|
342
|
+
BlockComment,
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
impl ProjectConfig {
|
|
346
|
+
pub(crate) fn package(&self) -> Option<&JsonValue> {
|
|
347
|
+
self.package.as_ref()
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
pub(crate) fn forge(&self) -> Option<&JsonValue> {
|
|
351
|
+
self.forge.as_ref()
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
pub(crate) fn electron_cli(&self) -> Option<&JsonValue> {
|
|
355
|
+
self.electron_cli.as_ref()
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
pub(crate) fn warnings(&self) -> &[String] {
|
|
359
|
+
&self.warnings
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
#[cfg(test)]
|
|
364
|
+
mod tests {
|
|
365
|
+
use super::*;
|
|
366
|
+
use camino::Utf8PathBuf;
|
|
367
|
+
|
|
368
|
+
#[test]
|
|
369
|
+
fn parses_commonjs_static_forge_config() {
|
|
370
|
+
let config = parse_forge_config_file(
|
|
371
|
+
r#"
|
|
372
|
+
module.exports = {
|
|
373
|
+
packagerConfig: {
|
|
374
|
+
name: 'Desk Tool',
|
|
375
|
+
},
|
|
376
|
+
makers: [
|
|
377
|
+
{ name: '@electron-forge/maker-zip' },
|
|
378
|
+
],
|
|
379
|
+
};
|
|
380
|
+
"#,
|
|
381
|
+
Path::new("forge.config.js"),
|
|
382
|
+
)
|
|
383
|
+
.expect("config should parse");
|
|
384
|
+
|
|
385
|
+
assert_eq!(
|
|
386
|
+
config
|
|
387
|
+
.get("packagerConfig")
|
|
388
|
+
.and_then(|config| config.get("name"))
|
|
389
|
+
.and_then(JsonValue::as_str),
|
|
390
|
+
Some("Desk Tool")
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
#[test]
|
|
395
|
+
fn parses_typescript_exported_config_identifier() {
|
|
396
|
+
let config = parse_forge_config_file(
|
|
397
|
+
r#"
|
|
398
|
+
import type { ForgeConfig } from '@electron-forge/shared-types';
|
|
399
|
+
|
|
400
|
+
const config: ForgeConfig = {
|
|
401
|
+
publishers: [
|
|
402
|
+
{
|
|
403
|
+
name: '@electron-forge/publisher-github',
|
|
404
|
+
platforms: ['darwin'],
|
|
405
|
+
config: { repository: { owner: 'Ikana', name: 'electron-cli' } },
|
|
406
|
+
},
|
|
407
|
+
],
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
export default config;
|
|
411
|
+
"#,
|
|
412
|
+
Path::new("forge.config.ts"),
|
|
413
|
+
)
|
|
414
|
+
.expect("config should parse");
|
|
415
|
+
|
|
416
|
+
assert_eq!(
|
|
417
|
+
config
|
|
418
|
+
.get("publishers")
|
|
419
|
+
.and_then(JsonValue::as_array)
|
|
420
|
+
.and_then(|publishers| publishers.first())
|
|
421
|
+
.and_then(|publisher| publisher.get("platforms"))
|
|
422
|
+
.and_then(JsonValue::as_array)
|
|
423
|
+
.and_then(|platforms| platforms.first())
|
|
424
|
+
.and_then(JsonValue::as_str),
|
|
425
|
+
Some("darwin")
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
#[test]
|
|
430
|
+
fn reads_config_path_from_package_json() {
|
|
431
|
+
let root = unique_temp_dir("config-path");
|
|
432
|
+
fs::write(
|
|
433
|
+
root.join("package.json"),
|
|
434
|
+
r#"{"name":"app","config":{"forge":"./build/forge.config.js"}}"#,
|
|
435
|
+
)
|
|
436
|
+
.expect("package.json should be written");
|
|
437
|
+
fs::create_dir_all(root.join("build")).expect("build dir should be created");
|
|
438
|
+
fs::write(
|
|
439
|
+
root.join("build/forge.config.js"),
|
|
440
|
+
"module.exports = { makers: [{ name: '@electron-forge/maker-deb' }] };",
|
|
441
|
+
)
|
|
442
|
+
.expect("forge config should be written");
|
|
443
|
+
|
|
444
|
+
let snapshot = snapshot(&root);
|
|
445
|
+
let config = read(&snapshot).expect("config should read");
|
|
446
|
+
|
|
447
|
+
assert_eq!(
|
|
448
|
+
config
|
|
449
|
+
.forge()
|
|
450
|
+
.and_then(|forge| forge.get("makers"))
|
|
451
|
+
.and_then(JsonValue::as_array)
|
|
452
|
+
.and_then(|makers| makers.first())
|
|
453
|
+
.and_then(|maker| maker.get("name"))
|
|
454
|
+
.and_then(JsonValue::as_str),
|
|
455
|
+
Some("@electron-forge/maker-deb")
|
|
456
|
+
);
|
|
457
|
+
assert!(config.warnings().is_empty());
|
|
458
|
+
|
|
459
|
+
let _ = fs::remove_dir_all(root);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
#[test]
|
|
463
|
+
fn discovers_default_forge_config_js() {
|
|
464
|
+
let root = unique_temp_dir("default-config");
|
|
465
|
+
fs::write(root.join("package.json"), r#"{"name":"app"}"#)
|
|
466
|
+
.expect("package.json should be written");
|
|
467
|
+
fs::write(
|
|
468
|
+
root.join("forge.config.js"),
|
|
469
|
+
"module.exports = { packagerConfig: { executableName: 'desk-tool' } };",
|
|
470
|
+
)
|
|
471
|
+
.expect("forge config should be written");
|
|
472
|
+
|
|
473
|
+
let snapshot = snapshot(&root);
|
|
474
|
+
let config = read(&snapshot).expect("config should read");
|
|
475
|
+
|
|
476
|
+
assert_eq!(
|
|
477
|
+
config
|
|
478
|
+
.forge()
|
|
479
|
+
.and_then(|forge| forge.get("packagerConfig"))
|
|
480
|
+
.and_then(|packager| packager.get("executableName"))
|
|
481
|
+
.and_then(JsonValue::as_str),
|
|
482
|
+
Some("desk-tool")
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
let _ = fs::remove_dir_all(root);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
#[test]
|
|
489
|
+
fn warns_when_config_requires_javascript_execution() {
|
|
490
|
+
let root = unique_temp_dir("dynamic-config");
|
|
491
|
+
fs::write(root.join("package.json"), r#"{"name":"app"}"#)
|
|
492
|
+
.expect("package.json should be written");
|
|
493
|
+
fs::write(
|
|
494
|
+
root.join("forge.config.js"),
|
|
495
|
+
"module.exports = buildConfig(process.env.NODE_ENV);",
|
|
496
|
+
)
|
|
497
|
+
.expect("forge config should be written");
|
|
498
|
+
|
|
499
|
+
let snapshot = snapshot(&root);
|
|
500
|
+
let config = read(&snapshot).expect("config should read");
|
|
501
|
+
|
|
502
|
+
assert!(config.forge().is_none());
|
|
503
|
+
assert!(config
|
|
504
|
+
.warnings()
|
|
505
|
+
.iter()
|
|
506
|
+
.any(|warning| warning.contains("without JavaScript execution")));
|
|
507
|
+
|
|
508
|
+
let _ = fs::remove_dir_all(root);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
fn snapshot(root: &Path) -> ProjectSnapshot {
|
|
512
|
+
ProjectSnapshot {
|
|
513
|
+
root: Utf8PathBuf::from_path_buf(root.to_path_buf()).expect("root should be utf-8"),
|
|
514
|
+
package_json: Some(
|
|
515
|
+
Utf8PathBuf::from_path_buf(root.join("package.json"))
|
|
516
|
+
.expect("package path should be utf-8"),
|
|
517
|
+
),
|
|
518
|
+
name: Some("app".to_string()),
|
|
519
|
+
version: None,
|
|
520
|
+
repository: None,
|
|
521
|
+
license: None,
|
|
522
|
+
main: Some("src/main.js".to_string()),
|
|
523
|
+
package_manager: None,
|
|
524
|
+
scripts: Default::default(),
|
|
525
|
+
dependencies: Default::default(),
|
|
526
|
+
dev_dependencies: Default::default(),
|
|
527
|
+
optional_dependencies: Default::default(),
|
|
528
|
+
peer_dependencies: Default::default(),
|
|
529
|
+
electron_dependency: Some("30.0.0".to_string()),
|
|
530
|
+
forge_dependencies: Default::default(),
|
|
531
|
+
signals: Vec::new(),
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
fn unique_temp_dir(label: &str) -> PathBuf {
|
|
536
|
+
let nanos = std::time::SystemTime::now()
|
|
537
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
538
|
+
.expect("clock should be after epoch")
|
|
539
|
+
.as_nanos();
|
|
540
|
+
let path = std::env::temp_dir().join(format!(
|
|
541
|
+
"electron-cli-forge-config-{label}-{}-{nanos}",
|
|
542
|
+
std::process::id()
|
|
543
|
+
));
|
|
544
|
+
fs::create_dir_all(&path).expect("temp dir should be created");
|
|
545
|
+
path
|
|
546
|
+
}
|
|
547
|
+
}
|
package/src/main.rs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
mod cli;
|
|
2
2
|
mod commands;
|
|
3
|
+
mod forge_config;
|
|
3
4
|
mod output;
|
|
4
5
|
mod project;
|
|
5
6
|
|
|
@@ -21,6 +22,10 @@ fn run() -> Result<()> {
|
|
|
21
22
|
Commands::Doctor(args) => commands::doctor::run(args),
|
|
22
23
|
Commands::Init(args) => commands::init::run(args),
|
|
23
24
|
Commands::Inspect(args) => commands::inspect::run(args),
|
|
25
|
+
Commands::Make(args) => commands::make::run(args),
|
|
26
|
+
Commands::Package(args) => commands::package::run(args),
|
|
24
27
|
Commands::Plan(args) => commands::plan::run(args),
|
|
28
|
+
Commands::Publish(args) => commands::publish::run(args),
|
|
29
|
+
Commands::Start(args) => commands::start::run(args),
|
|
25
30
|
}
|
|
26
31
|
}
|
package/src/project.rs
CHANGED
|
@@ -9,12 +9,14 @@ use camino::Utf8PathBuf;
|
|
|
9
9
|
use serde::Serialize;
|
|
10
10
|
use serde_json::Value;
|
|
11
11
|
|
|
12
|
-
#[derive(Debug, Serialize)]
|
|
12
|
+
#[derive(Clone, Debug, Serialize)]
|
|
13
13
|
pub struct ProjectSnapshot {
|
|
14
14
|
pub root: Utf8PathBuf,
|
|
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
|
}
|