electron-cli 0.3.0-alpha.13 → 0.3.0-alpha.15
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 +18 -1
- package/Cargo.toml +2 -1
- package/README.md +22 -6
- package/package.json +1 -1
- package/src/cli.rs +13 -13
- package/src/commands/make.rs +47 -16
- package/src/commands/package.rs +69 -26
- package/src/commands/publish.rs +789 -83
- package/src/forge_config.rs +547 -0
- package/src/main.rs +1 -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
|
+
}
|