use std::fs;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use clap::Args;
use crate::{print_field, print_finish, print_header};
#[derive(Args)]
pub(crate) struct InitArgs {
#[arg(default_value = ".")]
pub(crate) path: PathBuf,
#[arg(long)]
pub(crate) force: bool,
}
pub(crate) fn execute(args: &InitArgs) -> Result<()> {
let root = args
.path
.canonicalize()
.unwrap_or_else(|_| args.path.clone());
fs::create_dir_all(&root)
.with_context(|| format!("failed to create app directory {}", root.display()))?;
let files = generated_files(&root)?;
let conflicts: Vec<&Path> = files
.iter()
.map(|(path, _)| path.as_path())
.filter(|path| path.exists())
.collect();
if !conflicts.is_empty() && !args.force {
let names = conflicts
.iter()
.map(|path| {
path.strip_prefix(&root)
.unwrap_or(path)
.display()
.to_string()
})
.collect::<Vec<_>>()
.join(", ");
return Err(anyhow::anyhow!(
"desktop init would overwrite existing files: {names}; help: choose an empty directory or rerun with --force"
));
}
print_header("WebUI Desktop Init");
for (path, content) in files {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("failed to create {}", parent.display()))?;
}
fs::write(&path, content).with_context(|| format!("failed to write {}", path.display()))?;
print_field(
"Created",
&path.strip_prefix(&root).unwrap_or(&path).display(),
);
}
print_finish("Desktop app scaffold complete");
Ok(())
}
fn generated_files(root: &Path) -> Result<Vec<(PathBuf, String)>> {
Ok(vec![
(root.join("package.json"), PACKAGE_JSON.to_string()),
(root.join("webui-desktop.json"), DESKTOP_CONFIG.to_string()),
(root.join("src/index.html"), INDEX_HTML.to_string()),
(root.join("desktop/Cargo.toml"), desktop_cargo_toml()?),
(root.join("desktop/src/main.rs"), DESKTOP_MAIN.to_string()),
])
}
const PACKAGE_JSON: &str = r#"{
"name": "webui-desktop-app",
"version": "0.1.0",
"private": true,
"scripts": {
"build": "webui build ./src --out ./dist"
}
}
"#;
const DESKTOP_CONFIG: &str = r#"{
"appId": "com.example.webui.desktop",
"appName": "WebUI Desktop App",
"title": "WebUI Desktop App"
}
"#;
const INDEX_HTML: &str = r#"<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>WebUI Desktop App</title>
</head>
<body>
<main>
<h1>WebUI Desktop App</h1>
<p>Edit <code>src/index.html</code> to get started.</p>
</main>
</body>
</html>
"#;
fn desktop_cargo_toml() -> Result<String> {
let workspace = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
let crate_dir = workspace.join("crates/webui-desktop");
let mut text = String::from(
r#"[package]
name = "webui-desktop-app"
version = "0.1.0"
edition = "2021"
publish = false
[[bin]]
name = "webui-desktop-app"
path = "src/main.rs"
[features]
default = []
source = ["webui-desktop/source", "dep:serde", "dep:serde_json"]
[workspace]
[profile.release]
lto = "thin"
codegen-units = 1
panic = "abort"
strip = true
[dependencies]
"#,
);
text.push_str("webui-desktop = { package = \"microsoft-webui-desktop\", ");
if crate_dir.is_dir() {
let crate_dir = crate_dir
.canonicalize()
.with_context(|| format!("failed to locate SDK at {}", crate_dir.display()))?;
text.push_str("path = ");
text.push_str(&serde_json::to_string(&crate_dir.to_string_lossy())?);
} else {
text.push_str("version = \"");
text.push_str(env!("CARGO_PKG_VERSION"));
text.push('"');
}
text.push_str(", default-features = false, features = [\"native\"] }\n");
text.push_str("serde = { version = \"1.0\", features = [\"derive\"], optional = true }\n");
text.push_str("serde_json = { version = \"1.0\", optional = true }\n");
Ok(text)
}
const DESKTOP_MAIN: &str = r#"// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
use std::process::ExitCode;
use webui_desktop::DesktopError;
#[cfg(feature = "source")]
use webui_desktop::{BuildOptions, DesktopSourceConfig, WindowOptions};
use webui_desktop::{DesktopApp, DesktopAppBuilder, Result};
#[cfg(feature = "source")]
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct DesktopSettings {
app_id: String,
#[serde(flatten)]
window: WindowOptions,
}
fn main() -> ExitCode {
match run() {
Ok(()) => ExitCode::SUCCESS,
Err(error) => {
eprintln!("{}", error.chain_message());
if let Some(help) = error.hint() {
eprintln!("help: {help}");
}
ExitCode::FAILURE
}
}
}
fn run() -> Result<()> {
let app = match webui_desktop::find_packaged_resources_dir() {
Some(resources) => DesktopApp::from_bundle(resources)?,
None => source_app()?,
};
webui_desktop::run_frame(app.build()?)
}
#[cfg(feature = "source")]
fn source_app() -> Result<DesktopAppBuilder> {
let settings: DesktopSettings =
serde_json::from_str(include_str!("../../webui-desktop.json")).map_err(|source| {
DesktopError::Serialization {
context: "parsing webui-desktop.json; help: correct its appId and window settings"
.to_string(),
source,
}
})?;
let app_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join("src");
let options = BuildOptions {
app_dir,
..Default::default()
};
Ok(DesktopApp::from_source(DesktopSourceConfig::new(options))
.app_id(settings.app_id)
.window(settings.window))
}
#[cfg(not(feature = "source"))]
fn source_app() -> Result<DesktopAppBuilder> {
Err(DesktopError::PackagedResourcesNotFound)
}
"#;
#[cfg(test)]
#[allow(clippy::disallowed_methods)]
mod tests {
use super::*;
use std::process::Command;
use tempfile::TempDir;
#[test]
fn creates_scaffold_and_runner() {
let temp = TempDir::new().unwrap();
execute(&InitArgs {
path: temp.path().join("app"),
force: false,
})
.unwrap();
assert!(temp.path().join("app/package.json").is_file());
assert!(!fs::read_to_string(temp.path().join("app/package.json"))
.unwrap()
.contains("webuiDesktop"));
assert!(temp.path().join("app/webui-desktop.json").is_file());
assert!(temp.path().join("app/src/index.html").is_file());
assert!(temp.path().join("app/desktop/src/main.rs").is_file());
assert!(
fs::read_to_string(temp.path().join("app/desktop/src/main.rs"))
.unwrap()
.contains("include_str!(\"../../webui-desktop.json\")")
);
}
#[test]
fn refuses_existing_generated_files_without_force() {
let temp = TempDir::new().unwrap();
fs::create_dir_all(temp.path().join("app")).unwrap();
fs::write(temp.path().join("app/package.json"), "{}").unwrap();
let error = execute(&InitArgs {
path: temp.path().join("app"),
force: false,
})
.unwrap_err()
.to_string();
assert!(error.contains("--force"));
}
#[test]
fn scaffold_is_runtime_only_with_an_explicit_release_profile() {
let manifest = desktop_cargo_toml().unwrap();
assert!(manifest.contains("default = []"));
assert!(manifest
.contains("source = [\"webui-desktop/source\", \"dep:serde\", \"dep:serde_json\"]"));
assert!(manifest.contains("default-features = false, features = [\"native\"]"));
assert!(manifest.contains("[workspace]"));
assert!(manifest.contains("[profile.release]"));
assert!(manifest.contains("lto = \"thin\""));
assert!(manifest.contains("codegen-units = 1"));
assert!(manifest.contains("panic = \"abort\""));
assert!(manifest.contains("strip = true"));
assert!(!manifest.contains("microsoft-webui-desktop-runner"));
assert!(!manifest.contains("package = \"microsoft-webui\""));
assert!(!manifest.contains("anyhow"));
assert!(DESKTOP_MAIN.contains("#[cfg(not(feature = \"source\"))]"));
assert!(DESKTOP_MAIN.contains("DesktopError::PackagedResourcesNotFound"));
assert!(DESKTOP_MAIN.contains("DesktopApp::from_bundle(resources)?"));
assert!(DESKTOP_MAIN.contains("run_frame(app.build()?)"));
assert!(!DESKTOP_MAIN.contains("webui_desktop_runner"));
}
fn assert_standalone_metadata(root: &Path) {
execute(&InitArgs {
path: root.to_path_buf(),
force: false,
})
.unwrap();
let manifest = root.join("desktop/Cargo.toml");
let output = Command::new(env!("CARGO"))
.args([
"metadata",
"--offline",
"--no-deps",
"--format-version",
"1",
])
.arg("--manifest-path")
.arg(&manifest)
.current_dir(root)
.output()
.unwrap();
assert!(
output.status.success(),
"cargo metadata failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let metadata: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(
Path::new(metadata["workspace_root"].as_str().unwrap())
.canonicalize()
.unwrap(),
root.join("desktop").canonicalize().unwrap()
);
let package = &metadata["packages"][0];
let dependencies = package["dependencies"].as_array().unwrap();
assert_eq!(dependencies.len(), 3);
let desktop = dependencies
.iter()
.find(|dependency| dependency["name"] == "microsoft-webui-desktop")
.unwrap();
assert_eq!(desktop["uses_default_features"], false);
assert_eq!(desktop["features"][0], "native");
assert!(package["features"]["default"]
.as_array()
.unwrap()
.is_empty());
assert!(package["features"]["source"]
.as_array()
.unwrap()
.contains(&serde_json::Value::from("webui-desktop/source")));
}
#[test]
fn scaffold_is_standalone_outside_a_workspace() {
let temp = TempDir::new().unwrap();
assert_standalone_metadata(&temp.path().join("app"));
}
#[test]
fn scaffold_is_standalone_inside_a_workspace_without_editing_it() {
let temp = TempDir::new().unwrap();
let enclosing = temp.path().join("Cargo.toml");
let original = b"[workspace]\nmembers = []\nresolver = \"2\"\n";
fs::write(&enclosing, original).unwrap();
assert_standalone_metadata(&temp.path().join("app"));
assert_eq!(fs::read(&enclosing).unwrap(), original);
}
}