Skip to main content

fission_cli/
lib.rs

1use anyhow::Result;
2use clap::Parser;
3use std::path::Path;
4
5mod cli;
6
7#[cfg(test)]
8use fission_command_core::{read_project_config, Target};
9
10use cli::{Cli, Command, ServerCommand, SiteCommand};
11
12pub fn run<I, S>(args: I) -> Result<()>
13where
14    I: IntoIterator<Item = S>,
15    S: Into<std::ffi::OsString> + Clone,
16{
17    let mut argv: Vec<std::ffi::OsString> = args.into_iter().map(Into::into).collect();
18    if let Some(bin) = argv.first() {
19        if let Some(name) = Path::new(bin).file_name().and_then(|value| value.to_str()) {
20            if name == "cargo-fission" {
21                argv[0] = std::ffi::OsString::from("fission");
22                if argv.get(1).and_then(|value| value.to_str()) == Some("fission") {
23                    argv.remove(1);
24                }
25            }
26        }
27    }
28    let cli = Cli::parse_from(argv);
29    match cli.command {
30        Command::Init {
31            path,
32            name,
33            app_id,
34            local_path,
35        } => fission_command_core::init_project(&path, name, app_id, local_path),
36        Command::AddTarget {
37            targets,
38            project_dir,
39        } => fission_command_core::add_targets(&project_dir, &targets),
40        Command::AddCapability {
41            capabilities,
42            project_dir,
43        } => fission_command_core::add_capabilities(&project_dir, &capabilities),
44        Command::Doctor {
45            targets,
46            project_dir,
47            strict,
48        } => fission_command_run::doctor::run_doctor(&project_dir, &targets, strict),
49        Command::Devices { project_dir, json } => {
50            fission_command_run::list_devices(&project_dir, json)
51        }
52        Command::Run {
53            target,
54            device,
55            project_dir,
56            detach,
57            release,
58            host,
59            port,
60            no_open,
61            headless,
62        } => fission_command_run::run_app(fission_command_run::RunOptions {
63            project_dir,
64            target,
65            device,
66            detach,
67            release,
68            host,
69            port,
70            no_open,
71            headless,
72        }),
73        Command::Build {
74            target,
75            project_dir,
76            release,
77        } => fission_command_run::build_app(fission_command_run::BuildOptions {
78            project_dir,
79            target,
80            release,
81        }),
82        Command::Test {
83            target,
84            project_dir,
85            headless,
86        } => fission_command_run::test_app(fission_command_run::TestOptions {
87            project_dir,
88            target,
89            headless,
90        }),
91        Command::Site { command } => match command {
92            SiteCommand::Build {
93                project_dir,
94                release,
95            } => fission_command_site::build(&project_dir, release),
96            SiteCommand::Check {
97                project_dir,
98                release,
99            } => fission_command_site::check(&project_dir, release),
100            SiteCommand::Serve {
101                project_dir,
102                host,
103                port,
104                release,
105                no_open,
106            } => fission_command_site::serve(&project_dir, release, host, port, !no_open),
107            SiteCommand::Routes { project_dir } => fission_command_site::routes(&project_dir),
108        },
109        Command::Server { command } => match command {
110            ServerCommand::Build {
111                project_dir,
112                release,
113            } => fission_command_server::build(&project_dir, release),
114            ServerCommand::Check {
115                project_dir,
116                release,
117            } => fission_command_server::check(&project_dir, release),
118            ServerCommand::Serve {
119                project_dir,
120                host,
121                port,
122                release,
123            } => fission_command_server::serve(&project_dir, release, host, port),
124            ServerCommand::Routes { project_dir } => fission_command_server::routes(&project_dir),
125            ServerCommand::Artifacts {
126                project_dir,
127                release,
128                no_compile,
129            } => fission_command_server::artifacts(&project_dir, release, !no_compile),
130        },
131        Command::Package {
132            target,
133            format,
134            project_dir,
135            release,
136            json,
137        } => fission_command_package::package(fission_command_package::PackageOptions {
138            project_dir,
139            target,
140            format,
141            release,
142            json,
143        }),
144        Command::Distribute {
145            action,
146            provider,
147            artifact,
148            site,
149            deploy,
150            track,
151            dry_run,
152            yes,
153            project_dir,
154            json,
155        } => fission_command_package::distribute(fission_command_package::DistributeOptions {
156            project_dir,
157            provider,
158            action: action.unwrap_or(fission_command_package::DistributeAction::Publish),
159            artifact,
160            site,
161            deploy,
162            track,
163            dry_run,
164            yes,
165            json,
166        }),
167        Command::Publish {
168            provider,
169            artifact,
170            site,
171            deploy,
172            track,
173            dry_run,
174            yes,
175            project_dir,
176            json,
177        } => fission_command_package::distribute(fission_command_package::DistributeOptions {
178            project_dir,
179            provider,
180            action: fission_command_package::DistributeAction::Publish,
181            artifact,
182            site,
183            deploy,
184            track,
185            dry_run,
186            yes,
187            json,
188        }),
189        Command::Readiness {
190            kind,
191            target,
192            format,
193            provider,
194            artifact,
195            site,
196            track,
197            project_dir,
198            json,
199        } => fission_command_package::readiness(fission_command_package::ReadinessOptions {
200            project_dir,
201            kind,
202            target,
203            format,
204            provider,
205            artifact,
206            site,
207            track,
208            json,
209        }),
210        Command::ReleaseConfig { command } => fission_command_release::release_config(command),
211        Command::ReleaseContent { command } => fission_command_release::release_content(command),
212        Command::Beta { command } => fission_command_release::beta(command),
213        Command::Signing { command } => fission_command_release::signing(command),
214        Command::Reviews { command } => fission_command_release::reviews(command),
215        Command::ReleaseWorkflow { command } => fission_command_release::release_workflow(command),
216        Command::Auth { command } => fission_command_release::auth(command),
217        Command::Logs {
218            target,
219            device,
220            project_dir,
221            follow,
222        } => fission_command_run::attach_logs(fission_command_run::LogOptions {
223            project_dir,
224            target,
225            device,
226            follow,
227        }),
228        Command::Ui {
229            project_dir,
230            screenshot,
231            exit_after_render,
232            width,
233            height,
234        } => fission_command_ui::run_ui(fission_command_ui::UiOptions {
235            project_dir,
236            screenshot,
237            exit_after_render,
238            width,
239            height,
240        }),
241        Command::ServeWeb {
242            project_dir,
243            host,
244            port,
245            open,
246        } => fission_command_run::serve_web(fission_command_run::ServeWebOptions {
247            project_dir,
248            host,
249            port,
250            open,
251        }),
252    }
253}
254
255#[cfg(test)]
256mod tests {
257    use super::*;
258    use std::{fs, path::PathBuf};
259
260    fn unique_dir(name: &str) -> PathBuf {
261        let dir =
262            std::env::temp_dir().join(format!("cargo-fission-{}-{}", name, std::process::id()));
263        let _ = fs::remove_dir_all(&dir);
264        dir
265    }
266
267    #[test]
268    fn init_creates_project_files() {
269        let dir = unique_dir("init");
270        run([
271            "fission",
272            "init",
273            dir.to_str().unwrap(),
274            "--name",
275            "hello-fission",
276        ])
277        .unwrap();
278
279        assert!(dir.join("Cargo.toml").exists());
280        assert!(dir.join("src/main.rs").exists());
281        assert!(dir.join("src/lib.rs").exists());
282        assert!(dir.join("src/app.rs").exists());
283        assert!(dir.join("assets/app-icon.png").exists());
284        assert!(dir.join("fission.toml").exists());
285        assert!(dir.join("platforms/windows/README.md").exists());
286        assert!(dir.join("platforms/macos/README.md").exists());
287        assert!(dir.join("platforms/linux/README.md").exists());
288        let readme = std::fs::read_to_string(dir.join("README.md")).unwrap();
289        assert!(readme.contains("fission devices --project-dir ."));
290        assert!(readme.contains("fission run --project-dir ."));
291        assert!(readme.contains("fission logs --target <target>"));
292        assert!(readme.contains("fission build --target <target>"));
293        assert!(readme.contains("fission test --target <target>"));
294        let manifest = std::fs::read_to_string(dir.join("Cargo.toml")).unwrap();
295        assert!(manifest.contains("default-features = false"));
296        assert!(manifest.contains("features = [\"desktop\"]"));
297    }
298
299    #[test]
300    fn add_target_updates_manifest_and_scaffold() {
301        let dir = unique_dir("targets");
302        run(["fission", "init", dir.to_str().unwrap()]).unwrap();
303        run([
304            "fission",
305            "add-target",
306            "web",
307            "ios",
308            "android",
309            "--project-dir",
310            dir.to_str().unwrap(),
311        ])
312        .unwrap();
313
314        let project = read_project_config(&dir).unwrap();
315        assert!(project.targets.contains(&Target::Web));
316        assert!(project.targets.contains(&Target::Ios));
317        assert!(project.targets.contains(&Target::Android));
318        let manifest = std::fs::read_to_string(dir.join("Cargo.toml")).unwrap();
319        assert!(manifest.contains("default-features = false"));
320        assert!(manifest.contains("features = [\"desktop\", \"web\", \"android\", \"ios\"]"));
321        assert!(dir.join("platforms/web/README.md").exists());
322        assert!(dir.join("platforms/web/index.html").exists());
323        assert!(dir.join("platforms/web/bootstrap.mjs").exists());
324        assert!(dir.join("platforms/web/build-wasm.sh").exists());
325        assert!(dir.join("platforms/web/run-browser.sh").exists());
326        assert!(dir.join("platforms/web/test-browser.sh").exists());
327        assert!(dir.join("platforms/ios/README.md").exists());
328        assert!(dir.join("platforms/ios/Info.plist").exists());
329        assert!(dir.join("platforms/ios/LaunchScreen.storyboard").exists());
330        assert!(dir.join("platforms/ios/package-sim.sh").exists());
331        assert!(dir.join("platforms/ios/run-sim.sh").exists());
332        assert!(dir.join("platforms/ios/test-sim.sh").exists());
333        assert!(dir.join("platforms/android/README.md").exists());
334        assert!(dir.join("platforms/android/AndroidManifest.xml").exists());
335        assert!(dir.join("platforms/android/res/values/colors.xml").exists());
336        assert!(dir.join("platforms/android/res/values/styles.xml").exists());
337        assert!(dir
338            .join("platforms/android/res/drawable/fission_splash_background.xml")
339            .exists());
340        assert!(dir.join("platforms/android/package-apk.sh").exists());
341        assert!(dir.join("platforms/android/run-emulator.sh").exists());
342        assert!(dir.join("platforms/android/test-emulator.sh").exists());
343        let android_manifest =
344            std::fs::read_to_string(dir.join("platforms/android/AndroidManifest.xml")).unwrap();
345        assert!(android_manifest.contains("android:icon=\"@drawable/app_icon\""));
346        assert!(android_manifest.contains("android:targetSdkVersion=\"35\""));
347        assert!(android_manifest.contains("android:theme=\"@style/FissionLaunchTheme\""));
348        let android_styles =
349            std::fs::read_to_string(dir.join("platforms/android/res/values/styles.xml")).unwrap();
350        assert!(android_styles.contains("android:windowBackground"));
351        assert!(android_styles.contains("android:windowSplashScreenAnimatedIcon"));
352        let android_package_script =
353            std::fs::read_to_string(dir.join("platforms/android/package-apk.sh")).unwrap();
354        assert!(android_package_script.contains("detect_android_toolchain"));
355        assert!(android_package_script
356            .contains("darwin-aarch64 darwin-x86_64 linux-x86_64 windows-x86_64"));
357        assert!(android_package_script.contains(
358            "ANDROID_MIN_API_LEVEL=\"${ANDROID_MIN_API_LEVEL:-${ANDROID_API_LEVEL:-24}}\""
359        ));
360        assert!(android_package_script.contains("ANDROID_TARGET_API_LEVEL="));
361        assert!(
362            android_package_script.contains("aarch64-linux-android${ANDROID_MIN_API_LEVEL}-clang")
363        );
364        assert!(android_package_script.contains("BUILD_MANIFEST"));
365        assert!(android_package_script.contains("android:targetSdkVersion=\"{target_api}\""));
366        assert!(android_package_script.contains("cp -R \"$SCRIPT_DIR/res/.\""));
367        assert!(android_package_script.contains("fission_splash_image.png"));
368        assert!(android_package_script.contains("APP_ICONS"));
369        assert!(android_package_script.contains("res/drawable-nodpi/app_icon.*"));
370        let android_run_script =
371            std::fs::read_to_string(dir.join("platforms/android/run-emulator.sh")).unwrap();
372        assert!(android_run_script.contains("ANDROID_EMULATOR_API_LEVEL"));
373        assert!(android_run_script.contains("fission doctor android"));
374        assert!(android_run_script.contains("wait_for_android_boot()"));
375        assert!(android_run_script.contains("cmd package list packages"));
376        assert!(android_run_script.contains("ADB_INSTALL_FLAGS:---no-streaming -r"));
377        assert!(!android_run_script.contains("wait_for_android_boot() {\n  wait_for_android_boot"));
378        assert!(!android_run_script.contains("  wait_for_android_boot\n  wait_for_android_boot"));
379        assert!(
380            std::fs::read_to_string(dir.join("platforms/android/README.md"))
381                .unwrap()
382                .contains("fission run --target android")
383        );
384        let android_test_script =
385            std::fs::read_to_string(dir.join("platforms/android/test-emulator.sh")).unwrap();
386        assert!(android_test_script.contains("/health"));
387        let ios_package_script =
388            std::fs::read_to_string(dir.join("platforms/ios/package-sim.sh")).unwrap();
389        assert!(ios_package_script.contains("TARGET=\"${IOS_SIM_TARGET:-aarch64-apple-ios-sim}\""));
390        assert!(ios_package_script.contains("PROFILE=\"${IOS_SIM_PROFILE:-debug}\""));
391        assert!(ios_package_script.contains("BUNDLE_ID=\"${IOS_BUNDLE_ID:-com.example."));
392        assert!(ios_package_script.contains("DISPLAY_NAME=\"${IOS_DISPLAY_NAME:-"));
393        assert!(ios_package_script.contains("EXECUTABLE_NAME=\"${IOS_EXECUTABLE_NAME:-"));
394        assert!(ios_package_script.contains("plistlib.load"));
395        assert!(ios_package_script.contains("PkgInfo"));
396        assert!(ios_package_script.contains("PLATFORM_APP_ICONS"));
397        assert!(ios_package_script.contains("AppIcon.png"));
398        assert!(ios_package_script.contains("LaunchScreen.storyboard"));
399        assert!(ios_package_script.contains("ibtool"));
400        assert!(ios_package_script.contains("LaunchScreen.storyboardc"));
401        assert!(ios_package_script.contains("SplashImage.png"));
402        let ios_plist = std::fs::read_to_string(dir.join("platforms/ios/Info.plist")).unwrap();
403        assert!(ios_plist.contains("UILaunchStoryboardName"));
404        let ios_run_script = std::fs::read_to_string(dir.join("platforms/ios/run-sim.sh")).unwrap();
405        assert!(ios_run_script.contains("BUNDLE_ID=\"${IOS_BUNDLE_ID:-com.example."));
406        assert!(ios_run_script.contains("IOS_SIM_UNINSTALL_BEFORE_INSTALL"));
407        assert!(ios_run_script.contains(
408            "xcrun simctl launch --terminate-running-process \"$DEVICE_ID\" \"$BUNDLE_ID\""
409        ));
410        assert!(std::fs::read_to_string(dir.join("platforms/ios/README.md"))
411            .unwrap()
412            .contains("fission run --target ios"));
413        assert!(
414            std::fs::read_to_string(dir.join("platforms/ios/test-sim.sh"))
415                .unwrap()
416                .contains("/health")
417        );
418        assert!(
419            std::fs::read_to_string(dir.join("platforms/web/index.html"))
420                .unwrap()
421                .contains("../../assets/app-icon.png")
422        );
423        let web_index = std::fs::read_to_string(dir.join("platforms/web/index.html")).unwrap();
424        assert!(web_index.contains("id=\"fission-web-mount\""));
425        assert!(web_index.contains("height: 100vh"));
426        assert!(web_index.contains("outline: none"));
427        assert!(web_index.contains("touch-action: none"));
428        assert!(!web_index.contains("Generated by"));
429        let web_test_script =
430            std::fs::read_to_string(dir.join("platforms/web/test-browser.sh")).unwrap();
431        assert!(web_test_script.contains("--remote-debugging-port=\"$CDP_PORT\""));
432        assert!(web_test_script.contains("/json/list"));
433        assert!(std::fs::read_to_string(dir.join("platforms/web/README.md"))
434            .unwrap()
435            .contains("fission run --target web"));
436    }
437
438    #[test]
439    fn custom_icon_config_is_preserved_and_applied_to_mobile_scaffolds() {
440        let dir = unique_dir("custom-icons");
441        run(["fission", "init", dir.to_str().unwrap()]).unwrap();
442        fs::copy(
443            dir.join("assets/app-icon.png"),
444            dir.join("assets/shared-icon.png"),
445        )
446        .unwrap();
447        fs::copy(
448            dir.join("assets/app-icon.png"),
449            dir.join("assets/android-icon.png"),
450        )
451        .unwrap();
452        fs::copy(
453            dir.join("assets/app-icon.png"),
454            dir.join("assets/ios-icon.png"),
455        )
456        .unwrap();
457        let mut manifest = fs::read_to_string(dir.join("fission.toml")).unwrap();
458        manifest.push_str(
459            r##"
460
461[package.icons]
462mode = "mixed"
463source = "assets/shared-icon.png"
464safe_zone = 0.72
465allow_upscale = false
466
467[package.icons.android]
468source = "assets/android-icon.png"
469
470[package.icons.ios]
471source = "assets/ios-icon.png"
472"##,
473        );
474        fs::write(dir.join("fission.toml"), manifest).unwrap();
475
476        run([
477            "fission",
478            "add-target",
479            "ios",
480            "android",
481            "--project-dir",
482            dir.to_str().unwrap(),
483        ])
484        .unwrap();
485
486        let manifest = fs::read_to_string(dir.join("fission.toml")).unwrap();
487        assert!(manifest.contains("[package.icons]"));
488        assert!(manifest.contains("source = \"assets/shared-icon.png\""));
489        assert!(manifest.contains("[package.icons.android]"));
490        assert!(manifest.contains("[package.icons.ios]"));
491        assert!(dir
492            .join("platforms/android/res/drawable-nodpi/app_icon.png")
493            .exists());
494        assert!(dir.join("platforms/ios/AppIcon.png").exists());
495        let android_manifest =
496            fs::read_to_string(dir.join("platforms/android/AndroidManifest.xml")).unwrap();
497        assert!(android_manifest.contains("android:icon=\"@drawable/app_icon\""));
498        let android_package =
499            fs::read_to_string(dir.join("platforms/android/package-apk.sh")).unwrap();
500        assert!(android_package.contains("APP_ICONS"));
501        let ios_package = fs::read_to_string(dir.join("platforms/ios/package-sim.sh")).unwrap();
502        assert!(ios_package.contains("PLATFORM_APP_ICONS"));
503    }
504
505    #[test]
506    fn custom_splash_config_updates_mobile_platform_files() {
507        let dir = unique_dir("custom-splash");
508        run(["fission", "init", dir.to_str().unwrap()]).unwrap();
509        fs::copy(
510            dir.join("assets/app-icon.png"),
511            dir.join("assets/custom-splash.png"),
512        )
513        .unwrap();
514        let mut manifest = fs::read_to_string(dir.join("fission.toml")).unwrap();
515        manifest.push_str(
516            r##"
517
518[app.splash]
519background_color = "#123456"
520image = "assets/custom-splash.png"
521resize_mode = "cover"
522android_animation_duration_ms = 650
523"##,
524        );
525        fs::write(dir.join("fission.toml"), manifest).unwrap();
526
527        run([
528            "fission",
529            "add-target",
530            "ios",
531            "android",
532            "--project-dir",
533            dir.to_str().unwrap(),
534        ])
535        .unwrap();
536
537        let android_colors =
538            fs::read_to_string(dir.join("platforms/android/res/values/colors.xml")).unwrap();
539        assert!(android_colors.contains("#123456"));
540        let android_styles =
541            fs::read_to_string(dir.join("platforms/android/res/values/styles.xml")).unwrap();
542        assert!(android_styles.contains("650"));
543        assert!(dir
544            .join("platforms/android/res/drawable-nodpi/fission_splash_image.png")
545            .exists());
546        let storyboard =
547            fs::read_to_string(dir.join("platforms/ios/LaunchScreen.storyboard")).unwrap();
548        assert!(storyboard.contains("scaleAspectFill"));
549        assert!(storyboard.contains("red=\"0.070588\""));
550        assert!(dir.join("platforms/ios/SplashImage.png").exists());
551    }
552
553    #[test]
554    fn add_capability_updates_project_and_platform_config() {
555        let dir = unique_dir("capability");
556        run(["fission", "init", dir.to_str().unwrap()]).unwrap();
557        run([
558            "fission",
559            "add-target",
560            "ios",
561            "android",
562            "--project-dir",
563            dir.to_str().unwrap(),
564        ])
565        .unwrap();
566        run([
567            "fission",
568            "add-capability",
569            "nfc",
570            "biometric",
571            "bluetooth",
572            "barcode-scanner",
573            "camera",
574            "geolocation",
575            "haptics",
576            "microphone",
577            "volume-control",
578            "wifi",
579            "--project-dir",
580            dir.to_str().unwrap(),
581        ])
582        .unwrap();
583
584        let project = read_project_config(&dir).unwrap();
585        assert!(project
586            .capabilities
587            .contains(&fission_command_core::PlatformCapability::Nfc));
588        assert!(project
589            .capabilities
590            .contains(&fission_command_core::PlatformCapability::Biometric));
591        assert!(project
592            .capabilities
593            .contains(&fission_command_core::PlatformCapability::Bluetooth));
594        assert!(project
595            .capabilities
596            .contains(&fission_command_core::PlatformCapability::BarcodeScanner));
597        assert!(project
598            .capabilities
599            .contains(&fission_command_core::PlatformCapability::Camera));
600        assert!(project
601            .capabilities
602            .contains(&fission_command_core::PlatformCapability::Geolocation));
603        assert!(project
604            .capabilities
605            .contains(&fission_command_core::PlatformCapability::Haptics));
606        assert!(project
607            .capabilities
608            .contains(&fission_command_core::PlatformCapability::Microphone));
609        assert!(project
610            .capabilities
611            .contains(&fission_command_core::PlatformCapability::VolumeControl));
612        assert!(project
613            .capabilities
614            .contains(&fission_command_core::PlatformCapability::Wifi));
615
616        let android_manifest =
617            std::fs::read_to_string(dir.join("platforms/android/AndroidManifest.xml")).unwrap();
618        assert!(android_manifest.contains("android.permission.NFC"));
619        assert!(android_manifest.contains("android.hardware.nfc"));
620        assert!(android_manifest.contains("android.permission.USE_BIOMETRIC"));
621        assert!(android_manifest.contains("android.permission.BLUETOOTH_SCAN"));
622        assert!(android_manifest.contains("android.permission.BLUETOOTH_CONNECT"));
623        assert!(android_manifest.contains("android.hardware.bluetooth_le"));
624        assert!(android_manifest.contains("android.permission.CAMERA"));
625        assert!(android_manifest.contains("android.hardware.camera.flash"));
626        assert!(android_manifest.contains("android.permission.ACCESS_FINE_LOCATION"));
627        assert!(android_manifest.contains("android.permission.VIBRATE"));
628        assert!(android_manifest.contains("android.permission.RECORD_AUDIO"));
629        assert!(android_manifest.contains("android.permission.MODIFY_AUDIO_SETTINGS"));
630        assert!(android_manifest.contains("android.permission.NEARBY_WIFI_DEVICES"));
631        assert!(android_manifest.contains("android.permission.ACCESS_WIFI_STATE"));
632        assert!(dir
633            .join("platforms/android/java/rs/fission/runtime/FissionAndroidCapabilities.java")
634            .exists());
635
636        let ios_info = std::fs::read_to_string(dir.join("platforms/ios/Info.plist")).unwrap();
637        assert!(ios_info.contains("NFCReaderUsageDescription"));
638        assert!(ios_info.contains("NSFaceIDUsageDescription"));
639        assert!(ios_info.contains("NSBluetoothAlwaysUsageDescription"));
640        assert!(ios_info.contains("NSCameraUsageDescription"));
641        assert!(ios_info.contains("NSLocationWhenInUseUsageDescription"));
642        assert!(ios_info.contains("NSMicrophoneUsageDescription"));
643        let ios_entitlements =
644            std::fs::read_to_string(dir.join("platforms/ios/Entitlements.plist")).unwrap();
645        assert!(ios_entitlements.contains("com.apple.developer.nfc.readersession.formats"));
646        assert!(ios_entitlements.contains("com.apple.developer.networking.wifi-info"));
647    }
648
649    #[test]
650    fn init_existing_project_preserves_user_files_and_detects_targets() {
651        let dir = unique_dir("existing");
652        fs::create_dir_all(dir.join("src")).unwrap();
653        fs::create_dir_all(dir.join("platforms/web")).unwrap();
654        fs::write(
655            dir.join("Cargo.toml"),
656            "[package]\nname = \"existing-web\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
657        )
658        .unwrap();
659        fs::write(dir.join("src/main.rs"), "fn main() {}\n").unwrap();
660        fs::write(dir.join("src/lib.rs"), "pub fn existing() {}\n").unwrap();
661        fs::write(dir.join("README.md"), "# keep me\n").unwrap();
662        fs::write(
663            dir.join("platforms/web/index.html"),
664            "<!doctype html><title>keep me</title>\n",
665        )
666        .unwrap();
667
668        run(["fission", "init", dir.to_str().unwrap()]).unwrap();
669
670        assert_eq!(
671            fs::read_to_string(dir.join("README.md")).unwrap(),
672            "# keep me\n"
673        );
674        assert_eq!(
675            fs::read_to_string(dir.join("src/main.rs")).unwrap(),
676            "fn main() {}\n"
677        );
678        assert_eq!(
679            fs::read_to_string(dir.join("src/lib.rs")).unwrap(),
680            "pub fn existing() {}\n"
681        );
682        assert_eq!(
683            fs::read_to_string(dir.join("platforms/web/index.html")).unwrap(),
684            "<!doctype html><title>keep me</title>\n"
685        );
686
687        let project = read_project_config(&dir).unwrap();
688        assert_eq!(project.app.name, "existing-web");
689        assert!(project.targets.contains(&Target::Web));
690        assert!(project.targets.contains(&Target::Macos));
691        assert!(project.targets.contains(&Target::Linux));
692        assert!(project.targets.contains(&Target::Windows));
693        assert!(dir.join("platforms/web/README.md").exists());
694        assert!(dir.join("platforms/web/bootstrap.mjs").exists());
695        assert!(dir.join("assets/app-icon.png").exists());
696    }
697
698    #[test]
699    fn init_existing_project_is_idempotent() {
700        let dir = unique_dir("idempotent");
701        run(["fission", "init", dir.to_str().unwrap(), "--name", "idem"]).unwrap();
702        let manifest = fs::read_to_string(dir.join("fission.toml")).unwrap();
703        let main = fs::read_to_string(dir.join("src/main.rs")).unwrap();
704
705        run(["fission", "init", dir.to_str().unwrap()]).unwrap();
706
707        assert_eq!(
708            fs::read_to_string(dir.join("fission.toml")).unwrap(),
709            manifest
710        );
711        assert_eq!(fs::read_to_string(dir.join("src/main.rs")).unwrap(), main);
712    }
713
714    #[test]
715    fn add_target_preserves_existing_target_files() {
716        let dir = unique_dir("preserve-target");
717        run([
718            "fission",
719            "init",
720            dir.to_str().unwrap(),
721            "--name",
722            "preserve-target",
723        ])
724        .unwrap();
725        fs::create_dir_all(dir.join("platforms/web")).unwrap();
726        fs::write(
727            dir.join("platforms/web/index.html"),
728            "<!doctype html><title>custom</title>\n",
729        )
730        .unwrap();
731        fs::write(dir.join("README.md"), "# custom readme\n").unwrap();
732
733        run([
734            "fission",
735            "add-target",
736            "web",
737            "--project-dir",
738            dir.to_str().unwrap(),
739        ])
740        .unwrap();
741
742        assert_eq!(
743            fs::read_to_string(dir.join("platforms/web/index.html")).unwrap(),
744            "<!doctype html><title>custom</title>\n"
745        );
746        assert_eq!(
747            fs::read_to_string(dir.join("README.md")).unwrap(),
748            "# custom readme\n"
749        );
750        assert!(dir.join("platforms/web/README.md").exists());
751        assert!(dir.join("platforms/web/bootstrap.mjs").exists());
752        let project = read_project_config(&dir).unwrap();
753        assert!(project.targets.contains(&Target::Web));
754    }
755
756    #[test]
757    fn cargo_fission_alias_accepts_prefixed_subcommand() {
758        let dir = unique_dir("cargo-fission");
759        run([
760            "cargo-fission",
761            "fission",
762            "init",
763            dir.to_str().unwrap(),
764            "--name",
765            "cargo-fission-demo",
766        ])
767        .unwrap();
768
769        assert!(dir.join("Cargo.toml").exists());
770        assert!(dir.join("fission.toml").exists());
771    }
772
773    #[test]
774    fn doctor_command_runs_in_non_strict_mode() {
775        let dir = unique_dir("doctor");
776        run(["fission", "init", dir.to_str().unwrap()]).unwrap();
777        run([
778            "fission",
779            "doctor",
780            "web",
781            "--project-dir",
782            dir.to_str().unwrap(),
783        ])
784        .unwrap();
785    }
786}