use std::path::Path;
use anyhow::{bail, Context};
use is_terminal::IsTerminal;
use wasmer_api::backend::gql::UserWithNamespaces;
use wasmer_deploy_schema::schema::{DeploymentV1, WorkloadV1};
use crate::{cmd::AsyncCliCommand, ApiOpts, ItemFormatOpts};
const WASM_STATIC_SERVER_PACKAGE: &str = "sharrattj/static-web-server";
const WASM_STATIC_SERVER_VERSION: &str = "1";
#[derive(clap::Parser, Debug)]
pub struct CmdInitStaticSite {
#[clap(flatten)]
api: ApiOpts,
#[clap(long)]
name: Option<String>,
#[clap(long)]
path: Option<std::path::PathBuf>,
public_path: Option<String>,
#[clap(long)]
publish: bool,
}
impl CmdInitStaticSite {
async fn exec(self) -> Result<(), anyhow::Error> {
let interactive = std::io::stdin().is_terminal();
let client = self.api.client()?;
let user = wasmer_api::backend::current_user(&client).await?;
let path = if let Some(path) = &self.path {
if !path.is_dir() {
anyhow::bail!("Specified path is not a directory: '{}'", path.display());
}
path.clone()
} else {
let current = std::env::current_dir()?;
eprintln!("Initializing in current directory: {}", current.display());
current
};
let manifest = self.build_package(&path, interactive, &user).await?;
let (namespace, name) = manifest
.package
.name
.split_once('/')
.context("could not determine namespace")?;
let _depl = self.build_deployment(&path, namespace, name).await?;
let should_publish = if self.publish {
true
} else if interactive {
dialoguer::Confirm::new()
.with_prompt("Would you like to publish the new package and app?")
.default(false)
.interact()?
} else {
false
};
if should_publish {
let cmd = crate::cmd::publish::CmdAppPublish {
api: self.api.clone(),
no_validate: false,
path: Some(path),
fmt: ItemFormatOpts {
format: Default::default(),
},
publish_package: true,
non_interactive: true,
};
cmd.run_async().await?;
eprintln!("Deployment complete");
eprintln!("To use a custom domain, create a CNAME DNS record pointing to: {name}.{namespace}.wasmer.io");
}
Ok(())
}
async fn build_deployment(
&self,
path: &Path,
namespace: &str,
name: &str,
) -> Result<DeploymentV1, anyhow::Error> {
let deploy_yaml_path = path.join("deploy.yaml");
let full_name = format!("{namespace}/{name}");
let (deploy_config, _is_new_file) = if deploy_yaml_path.try_exists()? {
let config = std::fs::read_to_string(&deploy_yaml_path).with_context(|| {
format!(
"Could not read config file: '{}'",
deploy_yaml_path.display()
)
})?;
let config: DeploymentV1 = serde_yaml::from_str(&config).with_context(|| {
format!(
"Could not parse existing deploy.yaml file: '{}'",
deploy_yaml_path.display()
)
})?;
(config, false)
} else {
eprintln!("No deploy.yaml found. Configuring new app...");
let conf = DeploymentV1 {
name: full_name,
workload: WorkloadV1 {
name: None,
runner: wasmer_deploy_schema::schema::WorkloadRunnerV1::WebProxy(
wasmer_deploy_schema::schema::RunnerWebProxyV1 {
source: wasmer_deploy_schema::schema::WorkloadRunnerWasmSourceV1::Webc(
wasmer_deploy_schema::schema::WebcSourceV1 {
package:
wasmer_deploy_schema::schema::WebcPackageIdentifierV1 {
repository: "https://wapm.dev".parse().unwrap(),
namespace: namespace.to_string(),
name: name.to_string(),
tag: None,
},
auth_token: None,
},
),
},
),
capabilities: Default::default(),
},
};
eprintln!("Writing Deploy config: '{}'", deploy_yaml_path.display());
std::fs::write(&deploy_yaml_path, serde_yaml::to_string(&conf)?).with_context(
|| {
format!(
"Could not write deploy.yaml file: '{}'",
deploy_yaml_path.display()
)
},
)?;
(conf, true)
};
Ok(deploy_config)
}
async fn build_package(
&self,
path: &Path,
interactive: bool,
user: &UserWithNamespaces,
) -> Result<wasmer_toml::Manifest, anyhow::Error> {
let manifest_path = path.join("wasmer.toml");
let (manifest, _is_new_file) = if manifest_path.is_file() {
eprintln!("Using existing package definition in wasmer.toml...");
let contents = std::fs::read_to_string(&manifest_path)?;
let manifest = wasmer_toml::Manifest::parse(&contents).with_context(|| {
format!(
"Could not parse existing wasmer.toml file: '{}'",
manifest_path.display()
)
})?;
eprintln!("Existing wasmer.toml found. To publish a new version, increment the version in `wasmer.toml` and run `wasmer publish`");
(manifest, false)
} else {
eprintln!("No wasmer.toml found. Initializing package...");
let name = if let Some(name) = &self.name {
name.to_string()
} else {
let auto_name = path.file_name().map(|x| x.to_string_lossy().to_string());
if interactive {
let auto_name = auto_name.unwrap_or_else(|| "my-app".to_string());
let username = &user.username;
let name: String = dialoguer::Input::new()
.with_prompt("App name (namespace/name)")
.with_initial_text(format!("{username}/{auto_name}"))
.interact_text()?;
if name.trim().is_empty() {
anyhow::bail!("App name cannot be empty.");
}
name.trim().to_string()
} else {
auto_name.with_context(|| {
"Could not guess app name. Please provide a name with --name."
})?
}
};
let full_name = if name.contains('/') {
name
} else {
format!("{}/{}", user.username, name.trim())
};
let default_public = path.join("public");
let public_path_name = if let Some(p) = &self.public_path {
let full = path.join(p);
if !full.is_dir() {
bail!("Specified public path is not a directory: '{p}'",);
}
p.clone()
} else if default_public.is_dir() {
"public".to_string()
} else if interactive {
let dirname: String = dialoguer::Input::new()
.with_prompt("Public directory (path to the website files): ")
.with_initial_text("public")
.interact_text()?;
let pubdir = path.join(&dirname);
if !pubdir.is_dir() {
eprintln!("Creating public directory at '{}'...", pubdir.display());
eprintln!("Creating stub index.html...");
std::fs::create_dir_all(&pubdir)?;
let content = SAMPLE_INDEX_HTML
.replace("{title}", &full_name)
.replace("{description}", "A website built with Wasmer.");
std::fs::write(pubdir.join("index.html"), content)?;
}
dirname
} else {
bail!("No public directory found. Please specify one with --public-path.");
};
let public_path = path.join(&public_path_name);
if !public_path.join("index.html").is_file() {
eprintln!(
"WARN: No index.html file found in public directory '{}'",
public_path_name,
);
}
let manifest = wasmer_toml::Manifest {
base_directory_path: path.to_path_buf(),
module: None,
package: wasmer_toml::Package {
name: full_name.clone(),
version: "0.1.0".parse().unwrap(),
description: format!("{full_name} website"),
license: None,
license_file: None,
readme: None,
repository: None,
homepage: None,
wasmer_extra_flags: None,
disable_command_rename: false,
rename_commands_to_raw_command_name: false,
},
dependencies: Some(
vec![(
WASM_STATIC_SERVER_PACKAGE.to_string(),
WASM_STATIC_SERVER_VERSION.to_string(),
)]
.into_iter()
.collect(),
),
fs: Some(
vec![("public".to_string(), public_path_name.into())]
.into_iter()
.collect(),
),
command: None,
};
let raw = manifest.to_string()?;
eprintln!(
"Writing wasmer.toml package to '{}'",
manifest_path.display()
);
std::fs::write(&manifest_path, raw)?;
(manifest, true)
};
Ok(manifest)
}
}
impl AsyncCliCommand for CmdInitStaticSite {
fn run_async(self) -> futures::future::BoxFuture<'static, Result<(), anyhow::Error>> {
Box::pin(self.exec())
}
}
const SAMPLE_INDEX_HTML: &str = r#"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>{title}</title>
</head>
<body>
<header>
<h1>{title}</h1>
<p>{description}</p>
</body>
</html>
"#;