feat(cli): Use registry templates to create new apps

This commit is contained in:
Edoardo Marangoni
2024-05-15 19:24:05 +02:00
parent c65781da80
commit fc6471e46c
9 changed files with 1141 additions and 990 deletions

193
Cargo.lock generated
View File

@@ -17,6 +17,17 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "aes"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
dependencies = [
"cfg-if 1.0.0",
"cipher",
"cpufeatures",
]
[[package]]
name = "ahash"
version = "0.7.8"
@@ -450,6 +461,27 @@ dependencies = [
"serde",
]
[[package]]
name = "bzip2"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8"
dependencies = [
"bzip2-sys",
"libc",
]
[[package]]
name = "bzip2-sys"
version = "0.1.11+1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc"
dependencies = [
"cc",
"libc",
"pkg-config",
]
[[package]]
name = "camino"
version = "1.1.6"
@@ -654,6 +686,15 @@ dependencies = [
"bitflags 1.3.2",
]
[[package]]
name = "cmake"
version = "0.1.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130"
dependencies = [
"cc",
]
[[package]]
name = "colorchoice"
version = "1.0.1"
@@ -921,6 +962,21 @@ dependencies = [
"build_const",
]
[[package]]
name = "crc"
version = "3.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636"
dependencies = [
"crc-catalog",
]
[[package]]
name = "crc-catalog"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
[[package]]
name = "crc32fast"
version = "1.4.0"
@@ -1254,6 +1310,12 @@ version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2"
[[package]]
name = "deflate64"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83ace6c86376be0b6cdcf3fb41882e81d94b31587573d1cfa9d01cd06bba210d"
[[package]]
name = "deranged"
version = "0.3.11"
@@ -1407,6 +1469,17 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "displaydoc"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.61",
]
[[package]]
name = "distance"
version = "0.4.0"
@@ -1695,6 +1768,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae"
dependencies = [
"crc32fast",
"libz-ng-sys",
"miniz_oxide",
]
@@ -2708,6 +2782,16 @@ dependencies = [
"vcpkg",
]
[[package]]
name = "libz-ng-sys"
version = "1.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6409efc61b12687963e602df8ecf70e8ddacf95bc6576bcf16e3ac6328083c5"
dependencies = [
"cmake",
"libc",
]
[[package]]
name = "linked-hash-map"
version = "0.5.6"
@@ -2830,7 +2914,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aba8ecb0450dfabce4ad72085eed0a75dffe8f21f7ada05638564ea9db2d7fb1"
dependencies = [
"byteorder",
"crc",
"crc 1.8.1",
]
[[package]]
name = "lzma-rs"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e"
dependencies = [
"byteorder",
"crc 3.2.1",
]
[[package]]
@@ -4669,6 +4763,12 @@ dependencies = [
"libc",
]
[[package]]
name = "simd-adler32"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
[[package]]
name = "simdutf8"
version = "0.1.4"
@@ -5707,6 +5807,12 @@ dependencies = [
"static_assertions",
]
[[package]]
name = "typed-arena"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a"
[[package]]
name = "typenum"
version = "1.17.0"
@@ -6573,6 +6679,7 @@ dependencies = [
"wasmer-wasix",
"wasmer-wast",
"webc",
"zip",
]
[[package]]
@@ -6882,7 +6989,7 @@ dependencies = [
"indicatif",
"lazy_static",
"log 0.4.21",
"lzma-rs",
"lzma-rs 0.2.0",
"minisign",
"pretty_assertions",
"rand",
@@ -7665,3 +7772,85 @@ name = "zeroize"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d"
dependencies = [
"zeroize_derive",
]
[[package]]
name = "zeroize_derive"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.61",
]
[[package]]
name = "zip"
version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c700ea425e148de30c29c580c1f9508b93ca57ad31c9f4e96b83c194c37a7a8f"
dependencies = [
"aes",
"arbitrary",
"bzip2",
"constant_time_eq",
"crc32fast",
"crossbeam-utils 0.8.19",
"deflate64",
"displaydoc",
"flate2",
"hmac",
"indexmap 2.2.6",
"lzma-rs 0.3.0",
"pbkdf2",
"rand",
"sha1",
"thiserror",
"time 0.3.36",
"zeroize",
"zopfli",
"zstd",
]
[[package]]
name = "zopfli"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c1f48f3508a3a3f2faee01629564400bc12260f6214a056d06a3aaaa6ef0736"
dependencies = [
"crc32fast",
"log 0.4.21",
"simd-adler32",
"typed-arena",
]
[[package]]
name = "zstd"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d789b1514203a1120ad2429eae43a7bd32b90976a7bb8a05f7ec02fa88cc23a"
dependencies = [
"zstd-safe",
]
[[package]]
name = "zstd-safe"
version = "7.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cd99b45c6bc03a018c8b8a86025678c87e55526064e38f9df301989dce7ec0a"
dependencies = [
"zstd-sys",
]
[[package]]
name = "zstd-sys"
version = "2.0.10+zstd.1.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c253a4914af5bafc8fa8c86ee400827e83cf6ec01195ec1f1ed8441bf00d65aa"
dependencies = [
"cc",
"pkg-config",
]

View File

@@ -12,7 +12,8 @@ use wasmer_config::package::PackageIdent;
use crate::{
types::{
self, CreateNamespaceVars, DeployApp, DeployAppConnection, DeployAppVersion,
DeployAppVersionConnection, DnsDomain, GetCurrentUserWithAppsVars, GetDeployAppAndVersion,
DeployAppVersionConnection, DnsDomain, GetAppTemplateFromSlugVariables,
GetAppTemplatesQueryVariables, GetCurrentUserWithAppsVars, GetDeployAppAndVersion,
GetDeployAppVersionsVars, GetNamespaceAppsVars, GetSignedUrlForPackageUploadVariables, Log,
LogStream, PackageVersionConnection, PublishDeployAppVars, PushPackageReleasePayload,
SignedUrl, TagPackageReleasePayload, UpsertDomainFromZoneFileVars,
@@ -55,6 +56,36 @@ pub async fn fetch_webc_package(
webc::compat::Container::from_bytes(data).context("failed to parse webc package")
}
/// Fetch app templates.
pub async fn fetch_app_template_from_slug(
client: &WasmerClient,
slug: String,
) -> Result<Option<types::AppTemplate>, anyhow::Error> {
client
.run_graphql_strict(types::GetAppTemplateFromSlug::build(
GetAppTemplateFromSlugVariables { slug },
))
.await
.map(|v| v.get_app_template)
}
/// Fetch app templates.
pub async fn fetch_app_templates(
client: &WasmerClient,
category_slug: String,
first: i32,
) -> Result<Option<types::AppTemplateConnection>, anyhow::Error> {
client
.run_graphql_strict(types::GetAppTemplatesQuery::build(
GetAppTemplatesQueryVariables {
category_slug,
first,
},
))
.await
.map(|r| r.get_app_templates)
}
/// Get a signed URL to upload packages.
pub async fn get_signed_url_for_package_upload(
client: &WasmerClient,

View File

@@ -126,6 +126,64 @@ mod queries {
pub package: Package,
}
#[derive(cynic::QueryVariables, Debug)]
pub struct GetAppTemplateFromSlugVariables {
pub slug: String,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Query", variables = "GetAppTemplateFromSlugVariables")]
pub struct GetAppTemplateFromSlug {
#[arguments(slug: $slug)]
pub get_app_template: Option<AppTemplate>,
}
#[derive(cynic::QueryVariables, Debug)]
pub struct GetAppTemplatesQueryVariables {
pub category_slug: String,
pub first: i32,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Query", variables = "GetAppTemplatesQueryVariables")]
pub struct GetAppTemplatesQuery {
#[arguments(categorySlug: $category_slug, first: $first)]
pub get_app_templates: Option<AppTemplateConnection>,
}
#[derive(cynic::QueryFragment, Debug)]
pub struct AppTemplateConnection {
pub edges: Vec<Option<AppTemplateEdge>>,
pub page_info: PageInfo,
}
#[derive(cynic::QueryFragment, Debug)]
pub struct AppTemplateEdge {
pub node: Option<AppTemplate>,
pub cursor: String,
}
#[derive(cynic::QueryFragment, Debug)]
pub struct AppTemplate {
pub demo_url: String,
pub language: String,
pub name: String,
pub framework: String,
pub created_at: DateTime,
pub description: String,
pub id: cynic::Id,
pub is_public: bool,
pub repo_license: String,
pub readme: String,
pub repo_url: String,
pub slug: String,
pub updated_at: DateTime,
pub use_cases: Jsonstring,
}
#[derive(cynic::Scalar, Debug, Clone)]
#[cynic(graphql_type = "JSONString")]
pub struct Jsonstring(pub String);
#[derive(cynic::QueryVariables, Debug)]
pub struct GetPackageReleaseVars {
pub hash: String,

View File

@@ -185,7 +185,7 @@ tldextract = "0.6.0"
hex = "0.4.3"
flate2 = "1.0.25"
cargo_metadata = "0.15.2"
tar = "0.4.38"
tar = "0.4.40"
bytes = "1"
thiserror = "1.0.37"
log = "0.4.17"
@@ -229,6 +229,7 @@ tun-tap = { version = "0.1.3", features = ["tokio"], optional = true }
clap_complete = "4.5.2"
clap_mangen = "0.2.20"
zip = "1.2.3"
# NOTE: Must use different features for clap because the "color" feature does not
# work on wasi due to the anstream dependency not compiling.

View File

@@ -3,23 +3,17 @@
use crate::{
commands::AsyncCliCommand,
opts::{ApiOpts, ItemFormatOpts, WasmerEnv},
utils::{
load_package_manifest,
package_wizard::{CreateMode, PackageType, PackageWizard},
},
utils::load_package_manifest,
};
use anyhow::Context;
use colored::Colorize;
use dialoguer::{theme::ColorfulTheme, Confirm, Select};
use is_terminal::IsTerminal;
use std::{collections::HashMap, env, path::PathBuf, str::FromStr};
use wasmer_api::{types::UserWithNamespaces, WasmerClient};
use wasmer_config::{
app::AppConfigV1,
package::{NamedPackageIdent, PackageSource, Tag},
};
use std::{collections::HashMap, env, io::Cursor, path::PathBuf, str::FromStr};
use wasmer_api::{types::AppTemplate, WasmerClient};
use wasmer_config::{app::AppConfigV1, package::PackageSource};
use super::deploy::CmdAppDeploy;
use super::{deploy::CmdAppDeploy, util::login_user};
async fn write_app_config(app_config: &AppConfigV1, dir: Option<PathBuf>) -> anyhow::Result<()> {
let raw_app_config = app_config.clone().to_yaml()?;
@@ -41,8 +35,30 @@ async fn write_app_config(app_config: &AppConfigV1, dir: Option<PathBuf>) -> any
/// Create a new Edge app.
#[derive(clap::Parser, Debug)]
pub struct CmdAppCreate {
#[clap(name = "type", short = 't', long)]
pub template: Option<AppType>,
/// A reference to the template to use.
///
/// It can be either an URL to a github repository - like
/// `https://github.com/wasmer-examples/php-wasmer-starter` - or the name of a template that
/// will be searched for in the selected registry, like `astro-starter`.
#[clap(
long,
conflicts_with = "package",
conflicts_with = "use_local_manifest"
)]
pub template: Option<String>,
/// Name of the package to use.
#[clap(
long,
conflicts_with = "template",
conflicts_with = "use_local_manifest"
)]
pub package: Option<String>,
/// Whether or not to search (and use) a local manifest.
#[clap(long, conflicts_with = "template", conflicts_with = "package")]
pub use_local_manifest: bool,
/// Whether or not to deploy the application once it is created.
///
/// If selected, this might entail the step of publishing the package related to the
@@ -90,17 +106,13 @@ pub struct CmdAppCreate {
#[allow(missing_docs)]
pub fmt: ItemFormatOpts,
/// Name of the package to use.
#[clap(long, short = 'p')]
pub package: Option<String>,
/// Whether or not to search (and use) a local manifest.
#[clap(long)]
pub use_local_manifest: bool,
/// Name to use when creating a new package from a template.
#[clap(long)]
pub new_package_name: Option<String>,
/// Don't print any message.
#[clap(long)]
pub quiet: bool,
}
impl CmdAppCreate {
@@ -145,7 +157,7 @@ impl CmdAppCreate {
)
}
async fn get_owner(&self) -> anyhow::Result<String> {
async fn get_owner(&self, client: &WasmerClient) -> anyhow::Result<String> {
if let Some(owner) = &self.owner {
return Ok(owner.clone());
}
@@ -156,21 +168,12 @@ impl CmdAppCreate {
}
if !self.offline {
match self.api.client() {
Ok(client) => {
let user =
wasmer_api::query::current_user_with_namespaces(&client, None).await?;
let user = wasmer_api::query::current_user_with_namespaces(&client, None).await?;
crate::utils::prompts::prompt_for_namespace(
"Who should own this app?",
None,
Some(&user),
)
}
Err(e) => anyhow::bail!(
"Can't determine user info: {e}. Please, user `wasmer login` before deploying an
app or use the --owner <owner> flag to specify the owner of the app to deploy."
),
}
} else {
anyhow::bail!(
"Please, user `wasmer login` before deploying an app or use the --owner <owner>
@@ -213,10 +216,9 @@ impl CmdAppCreate {
};
if self.use_local_manifest || ask_confirmation()? {
let app_config =
self.get_app_config(owner, app_name, manifest_path.to_string_lossy().as_ref());
let app_config = self.get_app_config(owner, app_name, ".");
write_app_config(&app_config, self.app_dir_path.clone()).await?;
self.try_deploy(owner).await?;
self.try_deploy(owner, app_name).await?;
return Ok(true);
}
@@ -231,7 +233,7 @@ impl CmdAppCreate {
if let Some(pkg) = &self.package {
let app_config = self.get_app_config(owner, app_name, pkg);
write_app_config(&app_config, self.app_dir_path.clone()).await?;
self.try_deploy(owner).await?;
self.try_deploy(owner, app_name).await?;
return Ok(true);
} else if !self.non_interactive {
let theme = ColorfulTheme::default();
@@ -241,7 +243,7 @@ impl CmdAppCreate {
let app_config = self.get_app_config(owner, app_name, &package_name);
write_app_config(&app_config, self.app_dir_path.clone()).await?;
self.try_deploy(owner).await?;
self.try_deploy(owner, app_name).await?;
return Ok(true);
} else {
eprintln!(
@@ -253,109 +255,202 @@ impl CmdAppCreate {
Ok(false)
}
async fn create_from_template(&self, owner: &str, app_name: &str) -> anyhow::Result<bool> {
let template = match self.template {
Some(t) => t,
None => {
if !self.non_interactive {
// A utility function used to fetch the URL of the template to use.
async fn get_template_url(&self, client: &WasmerClient) -> anyhow::Result<url::Url> {
let mut url = if let Some(template) = &self.template {
if let Ok(url) = url::Url::parse(&template) {
url
} else if let Some(template) =
wasmer_api::query::fetch_app_template_from_slug(client, template.clone()).await?
{
url::Url::parse(&template.repo_url)?
} else {
anyhow::bail!("Template '{}' not found in the registry", template)
}
} else {
if self.non_interactive {
anyhow::bail!("No template selected")
}
let templates: Vec<AppTemplate> =
wasmer_api::query::fetch_app_templates(client, String::new(), 20)
.await?
.ok_or(anyhow::anyhow!("No template received from the backend"))?
.edges
.into_iter()
.filter(|v| v.is_some())
.map(|v| v.unwrap())
.filter(|v| v.node.is_some())
.map(|v| v.node.unwrap())
.collect();
let theme = ColorfulTheme::default();
let index = dialoguer::Select::with_theme(&theme)
.with_prompt("App type")
.default(0)
.items(&[
"Static website",
"HTTP server",
"Browser shell",
"JS Worker (WinterJS)",
"Python Application",
])
.interact()?;
match index {
0 => AppType::StaticWebsite,
1 => AppType::HttpServer,
2 => AppType::BrowserShell,
3 => AppType::JsWorker,
4 => AppType::PyApplication,
x => panic!("unhandled app type index '{x}'"),
}
let items = templates
.iter()
.map(|t| {
format!(
"{}{}{}\n",
t.name.bold(),
if t.language.is_empty() {
String::new()
} else {
return Ok(false);
}
}
};
let allow_local_package = match template {
AppType::HttpServer => true,
AppType::StaticWebsite => true,
AppType::BrowserShell => false,
AppType::JsWorker => true,
AppType::PyApplication => true,
};
let app_dir_path = match &self.app_dir_path {
Some(dir) => dir.clone(),
None => std::env::current_dir()?,
};
let local_package = if allow_local_package {
match crate::utils::load_package_manifest(&app_dir_path) {
Ok(Some(p)) => Some(p),
Ok(None) => None,
Err(err) => {
eprintln!(
"{warning}: could not load package manifest: {err}",
warning = "Warning".yellow(),
);
None
}
}
} else {
None
};
let user = if self.offline {
None
} else if let Ok(client) = &self.api.client() {
let u = wasmer_api::query::current_user_with_namespaces(
client,
Some(wasmer_api::types::GrapheneRole::Admin),
)
.await?;
Some(u)
} else {
None
};
let creator = AppCreator {
app_name: String::from(app_name),
new_package_name: self.new_package_name.clone(),
package: self.package.clone(),
template,
interactive: !self.non_interactive,
app_dir_path,
owner: String::from(owner),
api: if self.offline {
None
} else {
self.api.client().ok()
format!(" {}", t.language.dimmed())
},
user,
local_package,
format!(
"\n {} {}",
"demo url:".bold().dimmed(),
t.demo_url.dimmed()
)
)
})
.collect::<Vec<_>>();
// items.sort();
let dialog = dialoguer::Select::with_theme(&theme)
.with_prompt(format!("Select a template ({} available)", items.len()))
.items(&items)
.max_length(3)
.clear(true)
.report(false)
.default(0);
let selection = dialog.interact()?;
let selected_template = templates
.get(selection)
.ok_or(anyhow::anyhow!("Invalid selection!"))?;
if !self.quiet {
eprintln!(
"{} {} {} {} ({} {})",
"".green().bold(),
"Selected template".bold(),
"·".dimmed(),
selected_template.name.green().bold(),
"demo url".dimmed().bold(),
selected_template.demo_url.dimmed()
)
}
url::Url::parse(&selected_template.repo_url)?
};
match template {
AppType::HttpServer
| AppType::StaticWebsite
| AppType::JsWorker
| AppType::PyApplication => creator.build_app().await?,
AppType::BrowserShell => creator.build_browser_shell_app().await?,
if url.path().contains("archive/refs/heads") {
return Ok(url);
} else if url.path().contains("/zipball/") {
return Ok(url);
} else {
let old_path = url.path();
url.set_path(&format!("{old_path}/zipball/main"));
return Ok(url);
}
}
async fn create_from_template(
&self,
client: &WasmerClient,
owner: &str,
app_name: &str,
) -> anyhow::Result<bool> {
let url = self.get_template_url(client).await?;
tracing::info!("Downloading template from url {url}");
let output_path = if let Some(path) = &self.app_dir_path {
path.clone()
} else {
PathBuf::from(".").canonicalize()?
};
self.try_deploy(owner).await?;
let pb = indicatif::ProgressBar::new_spinner();
pb.enable_steady_tick(std::time::Duration::from_millis(500));
pb.set_style(
indicatif::ProgressStyle::with_template("{spinner:.magenta} {msg}")
.unwrap()
.tick_strings(&["", "", "", "", "", ""]),
);
pb.set_message("Downloading package..");
let response = reqwest::get(url).await?;
let bytes = response.bytes().await?;
pb.set_message("Unpacking the template..");
let cursor = Cursor::new(bytes);
let mut archive = zip::ZipArchive::new(cursor)?;
// Extract the files to the output path
for entry in 0..archive.len() {
let mut entry = archive
.by_index(entry)
.context(format!("Getting the archive entry #{entry}"))?;
let path = entry.mangled_name();
let path: PathBuf = {
let mut components = path.components();
components.next();
components.collect()
};
if path.to_str().unwrap_or_default().contains(".github") {
continue;
}
let path = output_path.join(path);
if let Some(parent) = path.parent() {
if !parent.exists() {
std::fs::create_dir_all(parent)?;
}
}
if !path.exists() {
// AsyncRead not implemented for entry..
if entry.is_file() {
let mut outfile = std::fs::File::create(&path)?;
std::io::copy(&mut entry, &mut outfile)?;
} else {
std::fs::create_dir(path)?;
}
}
}
pb.finish();
let app_yaml_path = output_path.join(AppConfigV1::CANONICAL_FILE_NAME);
if app_yaml_path.exists() && app_yaml_path.is_file() {
let contents = tokio::fs::read_to_string(&app_yaml_path).await?;
let contents = format!("{contents}\nname: {app_name}");
let mut app_config = AppConfigV1::parse_yaml(&contents)?;
app_config.owner = Some(owner.to_string());
let raw_app = serde_yaml::to_string(&app_config)?;
tokio::fs::write(&app_yaml_path, raw_app).await?;
}
let build_md_path = output_path.join("BUILD.md");
if build_md_path.exists() {
let contents = tokio::fs::read_to_string(build_md_path).await?;
eprintln!(
"{}: {}
{}",
"NOTE".bold(),
"The selected template has a `BUILD.md` file.
This means there are likely additional build
steps that you need to perform before deploying
the app:\n"
.bold(),
contents
);
} else {
self.try_deploy(owner, app_name).await?;
}
Ok(true)
}
async fn try_deploy(&self, owner: &str) -> anyhow::Result<()> {
async fn try_deploy(&self, owner: &str, app_name: &str) -> anyhow::Result<()> {
let interactive = !self.non_interactive;
let theme = dialoguer::theme::ColorfulTheme::default();
@@ -380,8 +475,11 @@ impl CmdAppCreate {
no_default: false,
no_persist_id: false,
owner: Some(String::from(owner)),
app_name: None,
app_name: Some(app_name.into()),
bump: false,
template: self.template.clone(),
package: self.package.clone(),
use_local_manifest: self.use_local_manifest,
};
cmd_deploy.run_async().await?;
}
@@ -395,15 +493,24 @@ impl AsyncCliCommand for CmdAppCreate {
type Output = ();
async fn run_async(self) -> Result<Self::Output, anyhow::Error> {
let client = login_user(
&self.api,
&self.env,
!self.non_interactive,
"retrieve informations about the owner of the app",
)
.await?;
// Get the future owner of the app.
let owner = self.get_owner().await?;
let owner = self.get_owner(&client).await?;
// Get the name of the app.
let app_name = self.get_app_name().await?;
if !self.create_from_local_manifest(&owner, &app_name).await? {
if self.template.is_some() {
self.create_from_template(&owner, &app_name).await?;
self.create_from_template(&client, &owner, &app_name)
.await?;
} else if self.package.is_some() {
self.create_from_package(&owner, &app_name).await?;
} else if !self.non_interactive {
@@ -414,7 +521,10 @@ impl AsyncCliCommand for CmdAppCreate {
.default(0)
.interact()?;
match choice {
0 => self.create_from_template(&owner, &app_name).await?,
0 => {
self.create_from_template(&client, &owner, &app_name)
.await?
}
1 => self.create_from_package(&owner, &app_name).await?,
x => panic!("unhandled selection {x}"),
};
@@ -427,270 +537,6 @@ impl AsyncCliCommand for CmdAppCreate {
}
}
/// App type.
#[derive(clap::ValueEnum, Clone, Copy, Debug)]
pub enum AppType {
/// A HTTP server.
#[clap(name = "http")]
HttpServer,
/// A static website.
#[clap(name = "static-website")]
StaticWebsite,
/// Wraps another package to run in the browser.
#[clap(name = "browser-shell")]
BrowserShell,
/// Winter-js based JS-Worker
#[clap(name = "js-worker")]
JsWorker,
/// Python worker
#[clap(name = "py-application")]
PyApplication,
}
struct AppCreator {
package: Option<String>,
new_package_name: Option<String>,
app_name: String,
template: AppType,
interactive: bool,
app_dir_path: PathBuf,
owner: String,
api: Option<WasmerClient>,
user: Option<UserWithNamespaces>,
local_package: Option<(PathBuf, wasmer_config::package::Manifest)>,
}
impl AppCreator {
async fn build_browser_shell_app(self) -> Result<(), anyhow::Error> {
const WASM_BROWSER_CONTAINER_PACKAGE: &str = "wasmer/wasmer-sh";
const WASM_BROWSER_CONTAINER_VERSION: &str = "0.2";
eprintln!("A browser web shell wraps another package and runs it in the browser");
eprintln!("Select the package to wrap.");
let (inner_pkg, _inner_pkg_api) = crate::utils::prompt_for_package(
"Package",
None,
Some(crate::utils::PackageCheckMode::MustExist),
self.api.as_ref(),
)
.await?;
let app_name = self.app_name;
eprintln!("What should be the name of the package?");
let default_name = format!(
"{}-{}-webshell",
self.owner,
inner_pkg.to_string().replace('/', "-")
);
let outer_pkg_name =
crate::utils::prompts::prompt_for_ident("Package name", Some(&default_name))?;
let outer_pkg_full_name = format!("{}/{}", self.owner, outer_pkg_name);
// Build the package.
let public_dir = self.app_dir_path.join("public");
if !public_dir.exists() {
std::fs::create_dir_all(&public_dir)?;
}
let init = serde_json::json!({
"init": format!("{}/{}", inner_pkg.namespace.as_ref().unwrap(), inner_pkg.name),
"prompt": inner_pkg.name,
"no_welcome": true,
"connect": format!("wss://{app_name}.wasmer.app/.well-known/edge-vpn"),
});
let init_path = public_dir.join("init.json");
std::fs::write(&init_path, init.to_string())
.with_context(|| format!("Failed to write to '{}'", init_path.display()))?;
let package = wasmer_config::package::PackageBuilder::new(
outer_pkg_full_name,
"0.1.0".parse().unwrap(),
format!("{} web shell", inner_pkg.name),
)
.rename_commands_to_raw_command_name(false)
.build()?;
let manifest = wasmer_config::package::ManifestBuilder::new(package)
.with_dependency(
WASM_BROWSER_CONTAINER_PACKAGE,
WASM_BROWSER_CONTAINER_VERSION.to_string().parse().unwrap(),
)
.map_fs("public", PathBuf::from("public"))
.build()?;
let manifest_path = self.app_dir_path.join("wasmer.toml");
let raw = manifest.to_string()?;
eprintln!(
"Writing wasmer.toml package to '{}'",
manifest_path.display()
);
std::fs::write(&manifest_path, raw)?;
let app_config = AppConfigV1 {
name: app_name,
app_id: None,
owner: Some(self.owner.clone()),
package: PackageSource::Path(".".into()),
domains: None,
env: Default::default(),
cli_args: None,
capabilities: None,
scheduled_tasks: None,
volumes: None,
health_checks: None,
debug: Some(false),
scaling: None,
extra: Default::default(),
};
write_app_config(&app_config, Some(self.app_dir_path.clone())).await?;
Ok(())
}
async fn build_app(self) -> Result<(), anyhow::Error> {
let package_opt: Option<NamedPackageIdent> = if let Some(package) = self.package {
Some(NamedPackageIdent::from_str(&package)?)
} else if let Some((_, local)) = self.local_package.as_ref() {
let pkg = match &local.package {
Some(pkg) => pkg.clone(),
None => anyhow::bail!(
"Error while building app: template manifest has no package field!"
),
};
if let (Some(name), Some(version)) = (pkg.name, pkg.version) {
let full = format!("{}@{}", name, version);
let mut pkg_ident = NamedPackageIdent::from_str(&name).with_context(|| {
format!("local package manifest has invalid name: '{full}'")
})?;
// Pin the version.
pkg_ident.tag = Some(Tag::from_str(&version.to_string()).unwrap());
if self.interactive {
eprintln!("Found local package: '{}'", full.green());
let msg = format!("Use package '{pkg_ident}'");
let theme = dialoguer::theme::ColorfulTheme::default();
let should_use = Confirm::with_theme(&theme)
.with_prompt(&msg)
.interact_opt()?
.unwrap_or_default();
if should_use {
Some(pkg_ident)
} else {
None
}
} else {
Some(pkg_ident)
}
} else {
None
}
} else {
None
};
let (package, _api_pkg, _local_package) = if let Some(pkg) = package_opt {
if let Some(api) = &self.api {
let p2 = wasmer_api::query::get_package(
api,
format!("{}/{}", pkg.namespace.as_ref().unwrap(), pkg.name),
)
.await?;
(
PackageSource::Ident(wasmer_config::package::PackageIdent::Named(pkg)),
p2,
self.local_package,
)
} else {
(
PackageSource::Ident(wasmer_config::package::PackageIdent::Named(pkg)),
None,
self.local_package,
)
}
} else {
let ty = match self.template {
AppType::HttpServer => None,
AppType::StaticWebsite => Some(PackageType::StaticWebsite),
AppType::BrowserShell => None,
AppType::JsWorker => Some(PackageType::JsWorker),
AppType::PyApplication => Some(PackageType::PyApplication),
};
let create_mode = match ty {
Some(PackageType::StaticWebsite)
| Some(PackageType::JsWorker)
| Some(PackageType::PyApplication) => CreateMode::Create,
// Only static website creation is currently supported.
_ => CreateMode::SelectExisting,
};
let w = PackageWizard {
path: self.app_dir_path.clone(),
name: self.new_package_name.clone(),
type_: ty,
create_mode,
namespace: Some(self.owner.clone()),
namespace_default: self.user.as_ref().map(|u| u.username.clone()),
user: self.user.clone(),
};
let output = w.run(self.api.as_ref()).await?;
(
PackageSource::Path(".".into()),
output.api,
output
.local_path
.and_then(move |x| Some((x, output.local_manifest?))),
)
};
let name = self.app_name;
let cli_args = match self.template {
AppType::PyApplication => Some(vec!["/src/main.py".to_string()]),
AppType::JsWorker => Some(vec!["/src/index.js".to_string()]),
_ => None,
};
// TODO: check if name already exists.
let app_config = AppConfigV1 {
name,
app_id: None,
owner: Some(self.owner.clone()),
package,
domains: None,
env: Default::default(),
// CLI args are only set for JS and Py workers for now.
cli_args,
// TODO: allow setting the description.
// description: Some("".to_string()),
capabilities: None,
scheduled_tasks: None,
volumes: None,
health_checks: None,
debug: Some(false),
scaling: None,
extra: Default::default(),
};
write_app_config(&app_config, Some(self.app_dir_path.clone())).await?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -700,6 +546,7 @@ mod tests {
let dir = tempfile::tempdir().unwrap();
let cmd = CmdAppCreate {
quiet: true,
template: Some(AppType::StaticWebsite),
deploy_app: false,
no_validate: false,
@@ -735,6 +582,7 @@ debug: false
let dir = tempfile::tempdir().unwrap();
let cmd = CmdAppCreate {
quiet: true,
template: Some(AppType::HttpServer),
deploy_app: false,
no_validate: false,
@@ -769,6 +617,7 @@ debug: false
let dir = tempfile::tempdir().unwrap();
let cmd = CmdAppCreate {
quiet: true,
template: Some(AppType::JsWorker),
deploy_app: false,
no_validate: false,
@@ -806,6 +655,7 @@ debug: false
let dir = tempfile::tempdir().unwrap();
let cmd = CmdAppCreate {
quiet: true,
template: Some(AppType::PyApplication),
deploy_app: false,
no_validate: false,

View File

@@ -76,7 +76,7 @@ pub struct CmdAppDeploy {
/// If specified via this flag, the app_name will be overridden. Otherwise, the `app.yaml` is
/// inspected and, if there is no `name` field in the spec file, if running interactive the
/// user will be prompted to insert an app name, otherwise the deployment will fail.
#[clap(long)]
#[clap(long, name = "name")]
pub app_name: Option<String>,
/// Whether or not to automatically bump the package version if publishing.
@@ -89,6 +89,31 @@ pub struct CmdAppDeploy {
/// operation.
#[clap(long)]
pub quiet: bool,
// - App creation -
/// A reference to the template to use when creating an app to deploy.
///
/// It can be either an URL to a github repository - like
/// `https://github.com/wasmer-examples/php-wasmer-starter` - or the name of a template that
/// will be searched for in the selected registry, like `astro-starter`.
#[clap(
long,
conflicts_with = "package",
conflicts_with = "use_local_manifest"
)]
pub template: Option<String>,
/// Name of the package to use when creating an app to deploy.
#[clap(
long,
conflicts_with = "template",
conflicts_with = "use_local_manifest"
)]
pub package: Option<String>,
/// Whether or not to search (and use) a local manifest when creating an app to deploy.
#[clap(long, conflicts_with = "template", conflicts_with = "package")]
pub use_local_manifest: bool,
}
impl CmdAppDeploy {
@@ -172,7 +197,7 @@ impl CmdAppDeploy {
eprintln!("It seems you are trying to create a new app!");
let create_cmd = CmdAppCreate {
template: None,
quiet: self.quiet,
deploy_app: false,
no_validate: false,
non_interactive: false,
@@ -185,9 +210,10 @@ impl CmdAppDeploy {
fmt: ItemFormatOpts {
format: self.fmt.format,
},
package: None,
package: self.package.clone(),
template: self.template.clone(),
app_dir_path: None,
use_local_manifest: false,
use_local_manifest: self.use_local_manifest,
new_package_name: None,
};

View File

@@ -10,11 +10,8 @@ use std::{
};
use anyhow::{bail, Context as _, Result};
use dialoguer::theme::ColorfulTheme;
use once_cell::sync::Lazy;
use regex::Regex;
use wasmer_api::WasmerClient;
use wasmer_config::package::NamedPackageIdent;
use wasmer_wasix::runners::MappedDirectory;
fn retrieve_alias_pathbuf(alias: &str, real_dir: &str) -> Result<MappedDirectory> {
@@ -111,79 +108,79 @@ pub fn load_package_manifest(
Ok(Some((file_path, manifest)))
}
/// Ask a user for a package name.
///
/// Will continue looping until the user provides a valid name.
pub fn prompt_for_package_name(
message: &str,
default: Option<&str>,
) -> Result<NamedPackageIdent, anyhow::Error> {
loop {
let theme = ColorfulTheme::default();
let raw: String = dialoguer::Input::with_theme(&theme)
.with_prompt(message)
.with_initial_text(default.unwrap_or_default())
.interact_text()
.context("could not read user input")?;
///// Ask a user for a package name.
/////
///// Will continue looping until the user provides a valid name.
//pub fn prompt_for_package_name(
// message: &str,
// default: Option<&str>,
//) -> Result<NamedPackageIdent, anyhow::Error> {
// loop {
// let theme = ColorfulTheme::default();
// let raw: String = dialoguer::Input::with_theme(&theme)
// .with_prompt(message)
// .with_initial_text(default.unwrap_or_default())
// .interact_text()
// .context("could not read user input")?;
//
// match raw.parse::<NamedPackageIdent>() {
// Ok(p) => break Ok(p),
// Err(err) => {
// eprintln!("invalid package name: {err}");
// }
// }
// }
//}
match raw.parse::<NamedPackageIdent>() {
Ok(p) => break Ok(p),
Err(err) => {
eprintln!("invalid package name: {err}");
}
}
}
}
/// Defines how to check for a package.
pub enum PackageCheckMode {
/// The package must exist in the registry.
MustExist,
/// The package must NOT exist in the registry.
#[allow(dead_code)]
MustNotExist,
}
/// Ask for a package name.
///
/// Will continue looping until the user provides a valid name.
///
/// If an API is provided, will check if the package exists.
pub async fn prompt_for_package(
message: &str,
default: Option<&str>,
check: Option<PackageCheckMode>,
client: Option<&WasmerClient>,
) -> Result<(NamedPackageIdent, Option<wasmer_api::types::Package>), anyhow::Error> {
loop {
let name = prompt_for_package_name(message, default)?;
if let Some(check) = &check {
let api = client.expect("Check mode specified, but no API provided");
let pkg = wasmer_api::query::get_package(api, name.to_string())
.await
.context("could not query backend for package")?;
match check {
PackageCheckMode::MustExist => {
if let Some(pkg) = pkg {
break Ok((name, Some(pkg)));
} else {
eprintln!("Package '{name}' does not exist");
}
}
PackageCheckMode::MustNotExist => {
if pkg.is_none() {
break Ok((name, None));
} else {
eprintln!("Package '{name}' already exists");
}
}
}
}
}
}
// /// Defines how to check for a package.
// pub enum PackageCheckMode {
// /// The package must exist in the registry.
// MustExist,
// /// The package must NOT exist in the registry.
// #[allow(dead_code)]
// MustNotExist,
// }
//
// /// Ask for a package name.
// ///
// /// Will continue looping until the user provides a valid name.
// ///
// /// If an API is provided, will check if the package exists.
// pub async fn prompt_for_package(
// message: &str,
// default: Option<&str>,
// check: Option<PackageCheckMode>,
// client: Option<&WasmerClient>,
// ) -> Result<(NamedPackageIdent, Option<wasmer_api::types::Package>), anyhow::Error> {
// loop {
// let name = prompt_for_package_name(message, default)?;
//
// if let Some(check) = &check {
// let api = client.expect("Check mode specified, but no API provided");
//
// let pkg = wasmer_api::query::get_package(api, name.to_string())
// .await
// .context("could not query backend for package")?;
//
// match check {
// PackageCheckMode::MustExist => {
// if let Some(pkg) = pkg {
// break Ok((name, Some(pkg)));
// } else {
// eprintln!("Package '{name}' does not exist");
// }
// }
// PackageCheckMode::MustNotExist => {
// if pkg.is_none() {
// break Ok((name, None));
// } else {
// eprintln!("Package '{name}' already exists");
// }
// }
// }
// }
// }
// }
// /// Republish the package described by the [`wasmer_config::package::Manifest`] given as argument and return a
// /// [`Result<wasmer_config::package::Manifest>`].

View File

@@ -1,426 +1,426 @@
use std::path::{Path, PathBuf};
use anyhow::Context;
use dialoguer::{theme::ColorfulTheme, Select};
use wasmer_api::{types::UserWithNamespaces, WasmerClient};
use super::prompts::PackageCheckMode;
const WASM_STATIC_SERVER_PACKAGE: &str = "wasmer/static-web-server";
const WASM_STATIC_SERVER_VERSION: &str = "1";
const WASMER_WINTER_JS_PACKAGE: &str = "wasmer/winterjs";
const WASMER_WINTER_JS_VERSION: &str = "*";
const WASM_PYTHON_PACKAGE: &str = "wasmer/python";
const WASM_PYTHON_VERSION: &str = "3.12.6";
const SAMPLE_INDEX_HTML: &str = include_str!("./templates/static-site/index.html");
const SAMPLE_JS_WORKER: &str = include_str!("./templates/js-worker/index.js");
const SAMPLE_PY_APPLICATION: &str = include_str!("./templates/py-application/main.py");
#[derive(clap::ValueEnum, Clone, Copy, Debug)]
pub enum PackageType {
#[clap(name = "regular")]
Regular,
/// A static website.
#[clap(name = "static-website")]
StaticWebsite,
/// A js-worker
#[clap(name = "js-worker")]
JsWorker,
/// A py-worker
#[clap(name = "py-application")]
PyApplication,
}
#[derive(Clone, Copy, Debug)]
pub enum CreateMode {
Create,
SelectExisting,
#[allow(dead_code)]
CreateOrSelect,
}
fn prompt_for_package_type() -> Result<PackageType, anyhow::Error> {
let theme = ColorfulTheme::default();
Select::with_theme(&theme)
.with_prompt("What type of package do you want to create?")
.items(&["Basic pacakge", "Static website"])
.interact()
.map(|idx| match idx {
0 => PackageType::Regular,
1 => PackageType::StaticWebsite,
_ => unreachable!(),
})
.map_err(anyhow::Error::from)
}
#[derive(Debug)]
pub struct PackageWizard {
pub path: PathBuf,
pub type_: Option<PackageType>,
pub create_mode: CreateMode,
/// Namespace to use.
pub namespace: Option<String>,
/// Default namespace to use.
/// Will still show a prompt, with this as the default value.
/// Ignored if [`Self::namespace`] is set.
pub namespace_default: Option<String>,
/// Pre-configured package name.
pub name: Option<String>,
pub user: Option<UserWithNamespaces>,
}
pub struct PackageWizardOutput {
pub api: Option<wasmer_api::types::Package>,
pub local_path: Option<PathBuf>,
pub local_manifest: Option<wasmer_config::package::Manifest>,
}
impl PackageWizard {
fn build_new_package(&self) -> Result<PackageWizardOutput, anyhow::Error> {
let ty = match self.type_ {
Some(t) => t,
None => prompt_for_package_type()?,
};
if !self.path.is_dir() {
std::fs::create_dir_all(&self.path).with_context(|| {
format!("Failed to create directory: '{}'", self.path.display())
})?;
}
let manifest = match ty {
PackageType::Regular => todo!(),
PackageType::StaticWebsite => initialize_static_site(&self.path)?,
PackageType::JsWorker => initialize_js_worker(&self.path)?,
PackageType::PyApplication => initialize_py_worker(&self.path)?,
};
let manifest_path = self.path.join("wasmer.toml");
let manifest_raw = manifest
.to_string()
.context("could not serialize package manifest")?;
std::fs::write(manifest_path, manifest_raw)
.with_context(|| format!("Failed to write manifest to '{}'", self.path.display()))?;
Ok(PackageWizardOutput {
api: None,
local_path: Some(self.path.clone()),
local_manifest: Some(manifest),
})
}
async fn prompt_existing_package(
&self,
api: Option<&WasmerClient>,
) -> Result<PackageWizardOutput, anyhow::Error> {
// Existing package
let check = if api.is_some() {
Some(PackageCheckMode::MustExist)
} else {
None
};
eprintln!("Enter the name of an existing package:");
let (_ident, api) = super::prompts::prompt_for_package("Package", None, check, api).await?;
Ok(PackageWizardOutput {
api,
local_path: None,
local_manifest: None,
})
}
pub async fn run(
self,
api: Option<&WasmerClient>,
) -> Result<PackageWizardOutput, anyhow::Error> {
match self.create_mode {
CreateMode::Create => self.build_new_package(),
CreateMode::SelectExisting => self.prompt_existing_package(api).await,
CreateMode::CreateOrSelect => {
let theme = ColorfulTheme::default();
let index = Select::with_theme(&theme)
.with_prompt("What package do you want to use?")
.items(&["Create new package", "Use existing package"])
.default(0)
.interact()?;
match index {
0 => self.build_new_package(),
1 => self.prompt_existing_package(api).await,
other => {
unreachable!("Unexpected index: {other}");
}
}
}
}
}
}
fn initialize_static_site(path: &Path) -> Result<wasmer_config::package::Manifest, anyhow::Error> {
let pubdir_name = "public";
let pubdir = path.join(pubdir_name);
if !pubdir.is_dir() {
std::fs::create_dir_all(&pubdir)
.with_context(|| format!("Failed to create directory: '{}'", pubdir.display()))?;
}
let index = pubdir.join("index.html");
let static_html = SAMPLE_INDEX_HTML.replace("{{title}}", "My static website");
if !index.is_file() {
std::fs::write(&index, static_html.as_str())
.with_context(|| "Could not write index.html file".to_string())?;
} else {
// The index.js file already exists, so we can ask the user if they want to overwrite it
let theme = dialoguer::theme::ColorfulTheme::default();
let should_overwrite = dialoguer::Confirm::with_theme(&theme)
.with_prompt("index.html already exists. Do you want to overwrite it?")
.interact()
.unwrap();
if should_overwrite {
std::fs::write(&index, static_html.as_str())
.with_context(|| "Could not write index.html file".to_string())?;
}
}
let raw_static_site_toml = format!(
r#"
[dependencies]
"{}" = "{}"
[fs]
public = "{}"
"#,
WASM_STATIC_SERVER_PACKAGE, WASM_STATIC_SERVER_VERSION, pubdir_name
);
let manifest = wasmer_config::package::Manifest::parse(raw_static_site_toml.as_str())
.map_err(|e| anyhow::anyhow!("Could not parse js worker manifest: {}", e))?;
Ok(manifest)
}
fn initialize_js_worker(path: &Path) -> Result<wasmer_config::package::Manifest, anyhow::Error> {
let srcdir_name = "src";
let srcdir = path.join(srcdir_name);
if !srcdir.is_dir() {
std::fs::create_dir_all(&srcdir)
.with_context(|| format!("Failed to create directory: '{}'", srcdir.display()))?;
}
let index_js = srcdir.join("index.js");
let sample_js = SAMPLE_JS_WORKER.replace("{{package}}", "My JS worker");
if !index_js.is_file() {
std::fs::write(&index_js, sample_js.as_str())
.with_context(|| "Could not write index.js file".to_string())?;
}
// get the remote repository if it exists
// Todo: add this to the manifest
// let remote_repo_url = Command::new("git")
// .arg("remote")
// .arg("get-url")
// .arg("origin")
// .output()
// .map_or("".to_string(), |f| String::from_utf8(f.stdout).unwrap());
let raw_js_worker_toml = format!(
r#"
[dependencies]
"{winterjs_pkg}" = "{winterjs_version}"
[fs]
"/src" = "./src"
[[command]]
name = "script"
module = "{winterjs_pkg}:winterjs"
runner = "https://webc.org/runner/wasi"
[command.annotations.wasi]
main-args = ["/src/index.js"]
env = ["JS_PATH=/src/index.js"]
"#,
winterjs_pkg = WASMER_WINTER_JS_PACKAGE,
winterjs_version = WASMER_WINTER_JS_VERSION,
);
let manifest = wasmer_config::package::Manifest::parse(raw_js_worker_toml.as_str())
.map_err(|e| anyhow::anyhow!("Could not parse js worker manifest: {}", e))?;
Ok(manifest)
}
fn initialize_py_worker(path: &Path) -> Result<wasmer_config::package::Manifest, anyhow::Error> {
let appdir_name = "src";
let appdir = path.join(appdir_name);
if !appdir.is_dir() {
std::fs::create_dir_all(&appdir)
.with_context(|| format!("Failed to create directory: '{}'", appdir.display()))?;
}
let main_py = appdir.join("main.py");
let sample_main = SAMPLE_PY_APPLICATION.replace("{{package}}", "My Python Worker");
if !main_py.is_file() {
std::fs::write(&main_py, sample_main.as_str())
.with_context(|| "Could not write main.py file".to_string())?;
}
// Todo: add this to the manifest
// let remote_repo_url = Command::new("git")
// .arg("remote")
// .arg("get-url")
// .arg("origin")
// .output()
// .map_or("".to_string(), |f| String::from_utf8(f.stdout).unwrap());
let raw_py_worker_toml = format!(
r#"
[dependencies]
"{}" = "{}"
[fs]
"/src" = "./src"
# "/.env" = "./.env/" # Bundle the virtualenv
[[command]]
name = "script"
module = "{}:python" # The "python" atom from "wasmer/python"
runner = "wasi"
[command.annotations.wasi]
main-args = ["/src/main.py"]
# env = ["PYTHON_PATH=/app/.env:/etc/python3.12/site-packages"] # Make our virtualenv accessible
"#,
WASM_PYTHON_PACKAGE, WASM_PYTHON_VERSION, WASM_PYTHON_PACKAGE
);
let manifest = wasmer_config::package::Manifest::parse(raw_py_worker_toml.as_str())
.map_err(|e| anyhow::anyhow!("Could not parse py worker manifest: {}", e))?;
Ok(manifest)
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_package_wizard_create_static_site() {
let dir = tempfile::tempdir().unwrap();
PackageWizard {
path: dir.path().to_owned(),
type_: Some(PackageType::StaticWebsite),
create_mode: CreateMode::Create,
namespace: None,
namespace_default: None,
name: None,
user: None,
}
.run(None)
.await
.unwrap();
let manifest = std::fs::read_to_string(dir.path().join("wasmer.toml")).unwrap();
pretty_assertions::assert_eq!(
manifest,
r#"[dependencies]
"wasmer/static-web-server" = "^1"
[fs]
public = "public"
"#,
);
assert!(dir.path().join("public").join("index.html").is_file());
}
#[tokio::test]
async fn test_package_wizard_create_js_worker() {
let dir = tempfile::tempdir().unwrap();
PackageWizard {
path: dir.path().to_owned(),
type_: Some(PackageType::JsWorker),
create_mode: CreateMode::Create,
namespace: None,
namespace_default: None,
name: None,
user: None,
}
.run(None)
.await
.unwrap();
let manifest = std::fs::read_to_string(dir.path().join("wasmer.toml")).unwrap();
pretty_assertions::assert_eq!(
manifest,
r#"[dependencies]
"wasmer/winterjs" = "*"
[fs]
"/src" = "./src"
[[command]]
name = "script"
module = "wasmer/winterjs:winterjs"
runner = "https://webc.org/runner/wasi"
[command.annotations.wasi]
env = ["JS_PATH=/src/index.js"]
main-args = ["/src/index.js"]
"#,
);
assert!(dir.path().join("src").join("index.js").is_file());
}
#[tokio::test]
async fn test_package_wizard_create_py_worker() {
let dir = tempfile::tempdir().unwrap();
PackageWizard {
path: dir.path().to_owned(),
type_: Some(PackageType::PyApplication),
create_mode: CreateMode::Create,
namespace: None,
namespace_default: None,
name: None,
user: None,
}
.run(None)
.await
.unwrap();
let manifest = std::fs::read_to_string(dir.path().join("wasmer.toml")).unwrap();
pretty_assertions::assert_eq!(
manifest,
r#"[dependencies]
"wasmer/python" = "^3.12.6"
[fs]
"/src" = "./src"
[[command]]
name = "script"
module = "wasmer/python:python"
runner = "wasi"
[command.annotations.wasi]
main-args = ["/src/main.py"]
"#,
);
assert!(dir.path().join("src").join("main.py").is_file());
}
}
// use std::path::{Path, PathBuf};
//
// use anyhow::Context;
// use dialoguer::{theme::ColorfulTheme, Select};
// use wasmer_api::{types::UserWithNamespaces, WasmerClient};
//
// use super::prompts::PackageCheckMode;
//
// const WASM_STATIC_SERVER_PACKAGE: &str = "wasmer/static-web-server";
// const WASM_STATIC_SERVER_VERSION: &str = "1";
//
// const WASMER_WINTER_JS_PACKAGE: &str = "wasmer/winterjs";
// const WASMER_WINTER_JS_VERSION: &str = "*";
//
// const WASM_PYTHON_PACKAGE: &str = "wasmer/python";
// const WASM_PYTHON_VERSION: &str = "3.12.6";
//
// const SAMPLE_INDEX_HTML: &str = include_str!("./templates/static-site/index.html");
// const SAMPLE_JS_WORKER: &str = include_str!("./templates/js-worker/index.js");
// const SAMPLE_PY_APPLICATION: &str = include_str!("./templates/py-application/main.py");
//
// #[derive(clap::ValueEnum, Clone, Copy, Debug)]
// pub enum PackageType {
// #[clap(name = "regular")]
// Regular,
// /// A static website.
// #[clap(name = "static-website")]
// StaticWebsite,
// /// A js-worker
// #[clap(name = "js-worker")]
// JsWorker,
// /// A py-worker
// #[clap(name = "py-application")]
// PyApplication,
// }
//
// #[derive(Clone, Copy, Debug)]
// pub enum CreateMode {
// Create,
// SelectExisting,
// #[allow(dead_code)]
// CreateOrSelect,
// }
//
// fn prompt_for_package_type() -> Result<PackageType, anyhow::Error> {
// let theme = ColorfulTheme::default();
// Select::with_theme(&theme)
// .with_prompt("What type of package do you want to create?")
// .items(&["Basic pacakge", "Static website"])
// .interact()
// .map(|idx| match idx {
// 0 => PackageType::Regular,
// 1 => PackageType::StaticWebsite,
// _ => unreachable!(),
// })
// .map_err(anyhow::Error::from)
// }
//
// #[derive(Debug)]
// pub struct PackageWizard {
// pub path: PathBuf,
// pub type_: Option<PackageType>,
//
// pub create_mode: CreateMode,
//
// /// Namespace to use.
// pub namespace: Option<String>,
// /// Default namespace to use.
// /// Will still show a prompt, with this as the default value.
// /// Ignored if [`Self::namespace`] is set.
// pub namespace_default: Option<String>,
//
// /// Pre-configured package name.
// pub name: Option<String>,
//
// pub user: Option<UserWithNamespaces>,
// }
//
// pub struct PackageWizardOutput {
// pub api: Option<wasmer_api::types::Package>,
// pub local_path: Option<PathBuf>,
// pub local_manifest: Option<wasmer_config::package::Manifest>,
// }
//
// impl PackageWizard {
// fn build_new_package(&self) -> Result<PackageWizardOutput, anyhow::Error> {
// let ty = match self.type_ {
// Some(t) => t,
// None => prompt_for_package_type()?,
// };
//
// if !self.path.is_dir() {
// std::fs::create_dir_all(&self.path).with_context(|| {
// format!("Failed to create directory: '{}'", self.path.display())
// })?;
// }
//
// let manifest = match ty {
// PackageType::Regular => todo!(),
// PackageType::StaticWebsite => initialize_static_site(&self.path)?,
// PackageType::JsWorker => initialize_js_worker(&self.path)?,
// PackageType::PyApplication => initialize_py_worker(&self.path)?,
// };
//
// let manifest_path = self.path.join("wasmer.toml");
// let manifest_raw = manifest
// .to_string()
// .context("could not serialize package manifest")?;
// std::fs::write(manifest_path, manifest_raw)
// .with_context(|| format!("Failed to write manifest to '{}'", self.path.display()))?;
//
// Ok(PackageWizardOutput {
// api: None,
// local_path: Some(self.path.clone()),
// local_manifest: Some(manifest),
// })
// }
//
// async fn prompt_existing_package(
// &self,
// api: Option<&WasmerClient>,
// ) -> Result<PackageWizardOutput, anyhow::Error> {
// // Existing package
// let check = if api.is_some() {
// Some(PackageCheckMode::MustExist)
// } else {
// None
// };
//
// eprintln!("Enter the name of an existing package:");
// let (_ident, api) = super::prompts::prompt_for_package("Package", None, check, api).await?;
// Ok(PackageWizardOutput {
// api,
// local_path: None,
// local_manifest: None,
// })
// }
//
// pub async fn run(
// self,
// api: Option<&WasmerClient>,
// ) -> Result<PackageWizardOutput, anyhow::Error> {
// match self.create_mode {
// CreateMode::Create => self.build_new_package(),
// CreateMode::SelectExisting => self.prompt_existing_package(api).await,
// CreateMode::CreateOrSelect => {
// let theme = ColorfulTheme::default();
// let index = Select::with_theme(&theme)
// .with_prompt("What package do you want to use?")
// .items(&["Create new package", "Use existing package"])
// .default(0)
// .interact()?;
//
// match index {
// 0 => self.build_new_package(),
// 1 => self.prompt_existing_package(api).await,
// other => {
// unreachable!("Unexpected index: {other}");
// }
// }
// }
// }
// }
// }
//
// fn initialize_static_site(path: &Path) -> Result<wasmer_config::package::Manifest, anyhow::Error> {
// let pubdir_name = "public";
// let pubdir = path.join(pubdir_name);
// if !pubdir.is_dir() {
// std::fs::create_dir_all(&pubdir)
// .with_context(|| format!("Failed to create directory: '{}'", pubdir.display()))?;
// }
// let index = pubdir.join("index.html");
//
// let static_html = SAMPLE_INDEX_HTML.replace("{{title}}", "My static website");
//
// if !index.is_file() {
// std::fs::write(&index, static_html.as_str())
// .with_context(|| "Could not write index.html file".to_string())?;
// } else {
// // The index.js file already exists, so we can ask the user if they want to overwrite it
// let theme = dialoguer::theme::ColorfulTheme::default();
// let should_overwrite = dialoguer::Confirm::with_theme(&theme)
// .with_prompt("index.html already exists. Do you want to overwrite it?")
// .interact()
// .unwrap();
// if should_overwrite {
// std::fs::write(&index, static_html.as_str())
// .with_context(|| "Could not write index.html file".to_string())?;
// }
// }
//
// let raw_static_site_toml = format!(
// r#"
// [dependencies]
// "{}" = "{}"
//
// [fs]
// public = "{}"
// "#,
// WASM_STATIC_SERVER_PACKAGE, WASM_STATIC_SERVER_VERSION, pubdir_name
// );
//
// let manifest = wasmer_config::package::Manifest::parse(raw_static_site_toml.as_str())
// .map_err(|e| anyhow::anyhow!("Could not parse js worker manifest: {}", e))?;
//
// Ok(manifest)
// }
//
// fn initialize_js_worker(path: &Path) -> Result<wasmer_config::package::Manifest, anyhow::Error> {
// let srcdir_name = "src";
// let srcdir = path.join(srcdir_name);
// if !srcdir.is_dir() {
// std::fs::create_dir_all(&srcdir)
// .with_context(|| format!("Failed to create directory: '{}'", srcdir.display()))?;
// }
//
// let index_js = srcdir.join("index.js");
//
// let sample_js = SAMPLE_JS_WORKER.replace("{{package}}", "My JS worker");
//
// if !index_js.is_file() {
// std::fs::write(&index_js, sample_js.as_str())
// .with_context(|| "Could not write index.js file".to_string())?;
// }
//
// // get the remote repository if it exists
// // Todo: add this to the manifest
// // let remote_repo_url = Command::new("git")
// // .arg("remote")
// // .arg("get-url")
// // .arg("origin")
// // .output()
// // .map_or("".to_string(), |f| String::from_utf8(f.stdout).unwrap());
//
// let raw_js_worker_toml = format!(
// r#"
// [dependencies]
// "{winterjs_pkg}" = "{winterjs_version}"
//
// [fs]
// "/src" = "./src"
//
// [[command]]
// name = "script"
// module = "{winterjs_pkg}:winterjs"
// runner = "https://webc.org/runner/wasi"
//
// [command.annotations.wasi]
// main-args = ["/src/index.js"]
// env = ["JS_PATH=/src/index.js"]
// "#,
// winterjs_pkg = WASMER_WINTER_JS_PACKAGE,
// winterjs_version = WASMER_WINTER_JS_VERSION,
// );
//
// let manifest = wasmer_config::package::Manifest::parse(raw_js_worker_toml.as_str())
// .map_err(|e| anyhow::anyhow!("Could not parse js worker manifest: {}", e))?;
//
// Ok(manifest)
// }
//
// fn initialize_py_worker(path: &Path) -> Result<wasmer_config::package::Manifest, anyhow::Error> {
// let appdir_name = "src";
// let appdir = path.join(appdir_name);
// if !appdir.is_dir() {
// std::fs::create_dir_all(&appdir)
// .with_context(|| format!("Failed to create directory: '{}'", appdir.display()))?;
// }
// let main_py = appdir.join("main.py");
//
// let sample_main = SAMPLE_PY_APPLICATION.replace("{{package}}", "My Python Worker");
//
// if !main_py.is_file() {
// std::fs::write(&main_py, sample_main.as_str())
// .with_context(|| "Could not write main.py file".to_string())?;
// }
//
// // Todo: add this to the manifest
// // let remote_repo_url = Command::new("git")
// // .arg("remote")
// // .arg("get-url")
// // .arg("origin")
// // .output()
// // .map_or("".to_string(), |f| String::from_utf8(f.stdout).unwrap());
//
// let raw_py_worker_toml = format!(
// r#"
// [dependencies]
// "{}" = "{}"
//
// [fs]
// "/src" = "./src"
// # "/.env" = "./.env/" # Bundle the virtualenv
//
// [[command]]
// name = "script"
// module = "{}:python" # The "python" atom from "wasmer/python"
// runner = "wasi"
//
// [command.annotations.wasi]
// main-args = ["/src/main.py"]
// # env = ["PYTHON_PATH=/app/.env:/etc/python3.12/site-packages"] # Make our virtualenv accessible
// "#,
// WASM_PYTHON_PACKAGE, WASM_PYTHON_VERSION, WASM_PYTHON_PACKAGE
// );
//
// let manifest = wasmer_config::package::Manifest::parse(raw_py_worker_toml.as_str())
// .map_err(|e| anyhow::anyhow!("Could not parse py worker manifest: {}", e))?;
//
// Ok(manifest)
// }
// #[cfg(test)]
// mod tests {
// use super::*;
//
// #[tokio::test]
// async fn test_package_wizard_create_static_site() {
// let dir = tempfile::tempdir().unwrap();
//
// PackageWizard {
// path: dir.path().to_owned(),
// type_: Some(PackageType::StaticWebsite),
// create_mode: CreateMode::Create,
// namespace: None,
// namespace_default: None,
// name: None,
// user: None,
// }
// .run(None)
// .await
// .unwrap();
//
// let manifest = std::fs::read_to_string(dir.path().join("wasmer.toml")).unwrap();
// pretty_assertions::assert_eq!(
// manifest,
// r#"[dependencies]
// "wasmer/static-web-server" = "^1"
//
// [fs]
// public = "public"
// "#,
// );
//
// assert!(dir.path().join("public").join("index.html").is_file());
// }
//
// #[tokio::test]
// async fn test_package_wizard_create_js_worker() {
// let dir = tempfile::tempdir().unwrap();
//
// PackageWizard {
// path: dir.path().to_owned(),
// type_: Some(PackageType::JsWorker),
// create_mode: CreateMode::Create,
// namespace: None,
// namespace_default: None,
// name: None,
// user: None,
// }
// .run(None)
// .await
// .unwrap();
// let manifest = std::fs::read_to_string(dir.path().join("wasmer.toml")).unwrap();
//
// pretty_assertions::assert_eq!(
// manifest,
// r#"[dependencies]
// "wasmer/winterjs" = "*"
//
// [fs]
// "/src" = "./src"
//
// [[command]]
// name = "script"
// module = "wasmer/winterjs:winterjs"
// runner = "https://webc.org/runner/wasi"
//
// [command.annotations.wasi]
// env = ["JS_PATH=/src/index.js"]
// main-args = ["/src/index.js"]
// "#,
// );
//
// assert!(dir.path().join("src").join("index.js").is_file());
// }
//
// #[tokio::test]
// async fn test_package_wizard_create_py_worker() {
// let dir = tempfile::tempdir().unwrap();
//
// PackageWizard {
// path: dir.path().to_owned(),
// type_: Some(PackageType::PyApplication),
// create_mode: CreateMode::Create,
// namespace: None,
// namespace_default: None,
// name: None,
// user: None,
// }
// .run(None)
// .await
// .unwrap();
// let manifest = std::fs::read_to_string(dir.path().join("wasmer.toml")).unwrap();
//
// pretty_assertions::assert_eq!(
// manifest,
// r#"[dependencies]
// "wasmer/python" = "^3.12.6"
//
// [fs]
// "/src" = "./src"
//
// [[command]]
// name = "script"
// module = "wasmer/python:python"
// runner = "wasi"
//
// [command.annotations.wasi]
// main-args = ["/src/main.py"]
// "#,
// );
//
// assert!(dir.path().join("src").join("main.py").is_file());
// }
// }

View File

@@ -2,7 +2,6 @@ use anyhow::Context;
use colored::Colorize;
use dialoguer::{theme::ColorfulTheme, Select};
use wasmer_api::WasmerClient;
use wasmer_config::package::NamedPackageIdent;
pub fn prompt_for_ident(message: &str, default: Option<&str>) -> Result<String, anyhow::Error> {
loop {
@@ -23,38 +22,38 @@ pub fn prompt_for_ident(message: &str, default: Option<&str>) -> Result<String,
}
}
/// Ask a user for a package name.
///
/// Will continue looping until the user provides a valid name.
pub fn prompt_for_package_ident(
message: &str,
default: Option<&str>,
) -> Result<NamedPackageIdent, anyhow::Error> {
loop {
let theme = ColorfulTheme::default();
let raw: String = dialoguer::Input::with_theme(&theme)
.with_prompt(message)
.with_initial_text(default.unwrap_or_default())
.interact_text()
.context("could not read user input")?;
// /// Ask a user for a package name.
// ///
// /// Will continue looping until the user provides a valid name.
// pub fn prompt_for_package_ident(
// message: &str,
// default: Option<&str>,
// ) -> Result<NamedPackageIdent, anyhow::Error> {
// loop {
// let theme = ColorfulTheme::default();
// let raw: String = dialoguer::Input::with_theme(&theme)
// .with_prompt(message)
// .with_initial_text(default.unwrap_or_default())
// .interact_text()
// .context("could not read user input")?;
//
// match raw.parse::<NamedPackageIdent>() {
// Ok(p) => break Ok(p),
// Err(err) => {
// eprintln!("invalid package name: {err}");
// }
// }
// }
// }
match raw.parse::<NamedPackageIdent>() {
Ok(p) => break Ok(p),
Err(err) => {
eprintln!("invalid package name: {err}");
}
}
}
}
/// Defines how to check for a package.
pub enum PackageCheckMode {
/// The package must exist in the registry.
MustExist,
/// The package must NOT exist in the registry.
#[allow(dead_code)]
MustNotExist,
}
// /// Defines how to check for a package.
// pub enum PackageCheckMode {
// /// The package must exist in the registry.
// MustExist,
// /// The package must NOT exist in the registry.
// #[allow(dead_code)]
// MustNotExist,
// }
/// Ask a user for a package version.
///
@@ -80,51 +79,51 @@ pub fn prompt_for_package_version(
}
}
/// Ask for a package name.
///
/// Will continue looping until the user provides a valid name.
///
/// If an API is provided, will check if the package exists.
pub async fn prompt_for_package(
message: &str,
default: Option<&str>,
check: Option<PackageCheckMode>,
client: Option<&WasmerClient>,
) -> Result<(NamedPackageIdent, Option<wasmer_api::types::Package>), anyhow::Error> {
loop {
let ident = prompt_for_package_ident(message, default)?;
if let Some(check) = &check {
let api = client.expect("Check mode specified, but no API provided");
let pkg = wasmer_api::query::get_package(api, ident.to_string())
.await
.context("could not query backend for package")?;
match check {
PackageCheckMode::MustExist => {
if let Some(pkg) = pkg {
let mut ident = ident;
if let Some(v) = &pkg.last_version {
ident.tag =
Some(wasmer_config::package::Tag::VersionReq(v.version.parse()?));
}
break Ok((ident, Some(pkg)));
} else {
eprintln!("Package '{ident}' does not exist");
}
}
PackageCheckMode::MustNotExist => {
if pkg.is_none() {
break Ok((ident, None));
} else {
eprintln!("Package '{ident}' already exists");
}
}
}
}
}
}
// /// Ask for a package name.
// ///
// /// Will continue looping until the user provides a valid name.
// ///
// /// If an API is provided, will check if the package exists.
// pub async fn prompt_for_package(
// message: &str,
// default: Option<&str>,
// check: Option<PackageCheckMode>,
// client: Option<&WasmerClient>,
// ) -> Result<(NamedPackageIdent, Option<wasmer_api::types::Package>), anyhow::Error> {
// loop {
// let ident = prompt_for_package_ident(message, default)?;
//
// if let Some(check) = &check {
// let api = client.expect("Check mode specified, but no API provided");
//
// let pkg = wasmer_api::query::get_package(api, ident.to_string())
// .await
// .context("could not query backend for package")?;
//
// match check {
// PackageCheckMode::MustExist => {
// if let Some(pkg) = pkg {
// let mut ident = ident;
// if let Some(v) = &pkg.last_version {
// ident.tag =
// Some(wasmer_config::package::Tag::VersionReq(v.version.parse()?));
// }
// break Ok((ident, Some(pkg)));
// } else {
// eprintln!("Package '{ident}' does not exist");
// }
// }
// PackageCheckMode::MustNotExist => {
// if pkg.is_none() {
// break Ok((ident, None));
// } else {
// eprintln!("Package '{ident}' already exists");
// }
// }
// }
// }
// }
// }
/// Prompt for a namespace.
///