wasmer_cli/utils/package_wizard/mod.rs
1// use std::path::{Path, PathBuf};
2//
3// use anyhow::Context;
4// use dialoguer::{theme::ColorfulTheme, Select};
5// use wasmer_backend_api::{types::UserWithNamespaces, WasmerClient};
6//
7// use super::prompts::PackageCheckMode;
8//
9// const WASM_STATIC_SERVER_PACKAGE: &str = "wasmer/static-web-server";
10// const WASM_STATIC_SERVER_VERSION: &str = "1";
11//
12// const WASMER_WINTER_JS_PACKAGE: &str = "wasmer/winterjs";
13// const WASMER_WINTER_JS_VERSION: &str = "*";
14//
15// const WASM_PYTHON_PACKAGE: &str = "wasmer/python";
16// const WASM_PYTHON_VERSION: &str = "3.12.6";
17//
18// const SAMPLE_INDEX_HTML: &str = include_str!("./templates/static-site/index.html");
19// const SAMPLE_JS_WORKER: &str = include_str!("./templates/js-worker/index.js");
20// const SAMPLE_PY_APPLICATION: &str = include_str!("./templates/py-application/main.py");
21//
22// #[derive(clap::ValueEnum, Clone, Copy, Debug)]
23// pub enum PackageType {
24// #[clap(name = "regular")]
25// Regular,
26// /// A static website.
27// #[clap(name = "static-website")]
28// StaticWebsite,
29// /// A js-worker
30// #[clap(name = "js-worker")]
31// JsWorker,
32// /// A py-worker
33// #[clap(name = "py-application")]
34// PyApplication,
35// }
36//
37// #[derive(Clone, Copy, Debug)]
38// pub enum CreateMode {
39// Create,
40// SelectExisting,
41// #[allow(dead_code)]
42// CreateOrSelect,
43// }
44//
45// fn prompt_for_package_type() -> Result<PackageType, anyhow::Error> {
46// let theme = ColorfulTheme::default();
47// Select::with_theme(&theme)
48// .with_prompt("What type of package do you want to create?")
49// .items(&["Basic pacakge", "Static website"])
50// .interact()
51// .map(|idx| match idx {
52// 0 => PackageType::Regular,
53// 1 => PackageType::StaticWebsite,
54// _ => unreachable!(),
55// })
56// .map_err(anyhow::Error::from)
57// }
58//
59// #[derive(Debug)]
60// pub struct PackageWizard {
61// pub path: PathBuf,
62// pub type_: Option<PackageType>,
63//
64// pub create_mode: CreateMode,
65//
66// /// Namespace to use.
67// pub namespace: Option<String>,
68// /// Default namespace to use.
69// /// Will still show a prompt, with this as the default value.
70// /// Ignored if [`Self::namespace`] is set.
71// pub namespace_default: Option<String>,
72//
73// /// Pre-configured package name.
74// pub name: Option<String>,
75//
76// pub user: Option<UserWithNamespaces>,
77// }
78//
79// pub struct PackageWizardOutput {
80// pub api: Option<wasmer_backend_api::types::Package>,
81// pub local_path: Option<PathBuf>,
82// pub local_manifest: Option<wasmer_config::package::Manifest>,
83// }
84//
85// impl PackageWizard {
86// fn build_new_package(&self) -> Result<PackageWizardOutput, anyhow::Error> {
87// let ty = match self.type_ {
88// Some(t) => t,
89// None => prompt_for_package_type()?,
90// };
91//
92// if !self.path.is_dir() {
93// std::fs::create_dir_all(&self.path).with_context(|| {
94// format!("Failed to create directory: '{}'", self.path.display())
95// })?;
96// }
97//
98// let manifest = match ty {
99// PackageType::Regular => todo!(),
100// PackageType::StaticWebsite => initialize_static_site(&self.path)?,
101// PackageType::JsWorker => initialize_js_worker(&self.path)?,
102// PackageType::PyApplication => initialize_py_worker(&self.path)?,
103// };
104//
105// let manifest_path = self.path.join("wasmer.toml");
106// let manifest_raw = manifest
107// .to_string()
108// .context("could not serialize package manifest")?;
109// std::fs::write(manifest_path, manifest_raw)
110// .with_context(|| format!("Failed to write manifest to '{}'", self.path.display()))?;
111//
112// Ok(PackageWizardOutput {
113// api: None,
114// local_path: Some(self.path.clone()),
115// local_manifest: Some(manifest),
116// })
117// }
118//
119// async fn prompt_existing_package(
120// &self,
121// api: Option<&WasmerClient>,
122// ) -> Result<PackageWizardOutput, anyhow::Error> {
123// // Existing package
124// let check = if api.is_some() {
125// Some(PackageCheckMode::MustExist)
126// } else {
127// None
128// };
129//
130// eprintln!("Enter the name of an existing package:");
131// let (_ident, api) = super::prompts::prompt_for_package("Package", None, check, api).await?;
132// Ok(PackageWizardOutput {
133// api,
134// local_path: None,
135// local_manifest: None,
136// })
137// }
138//
139// pub async fn run(
140// self,
141// api: Option<&WasmerClient>,
142// ) -> Result<PackageWizardOutput, anyhow::Error> {
143// match self.create_mode {
144// CreateMode::Create => self.build_new_package(),
145// CreateMode::SelectExisting => self.prompt_existing_package(api).await,
146// CreateMode::CreateOrSelect => {
147// let theme = ColorfulTheme::default();
148// let index = Select::with_theme(&theme)
149// .with_prompt("What package do you want to use?")
150// .items(&["Create new package", "Use existing package"])
151// .default(0)
152// .interact()?;
153//
154// match index {
155// 0 => self.build_new_package(),
156// 1 => self.prompt_existing_package(api).await,
157// other => {
158// unreachable!("Unexpected index: {other}");
159// }
160// }
161// }
162// }
163// }
164// }
165//
166// fn initialize_static_site(path: &Path) -> Result<wasmer_config::package::Manifest, anyhow::Error> {
167// let pubdir_name = "public";
168// let pubdir = path.join(pubdir_name);
169// if !pubdir.is_dir() {
170// std::fs::create_dir_all(&pubdir)
171// .with_context(|| format!("Failed to create directory: '{}'", pubdir.display()))?;
172// }
173// let index = pubdir.join("index.html");
174//
175// let static_html = SAMPLE_INDEX_HTML.replace("{{title}}", "My static website");
176//
177// if !index.is_file() {
178// std::fs::write(&index, static_html.as_str())
179// .with_context(|| "Could not write index.html file".to_string())?;
180// } else {
181// // The index.js file already exists, so we can ask the user if they want to overwrite it
182// let theme = dialoguer::theme::ColorfulTheme::default();
183// let should_overwrite = dialoguer::Confirm::with_theme(&theme)
184// .with_prompt("index.html already exists. Do you want to overwrite it?")
185// .interact()
186// .unwrap();
187// if should_overwrite {
188// std::fs::write(&index, static_html.as_str())
189// .with_context(|| "Could not write index.html file".to_string())?;
190// }
191// }
192//
193// let raw_static_site_toml = format!(
194// r#"
195// [dependencies]
196// "{}" = "{}"
197//
198// [fs]
199// public = "{}"
200// "#,
201// WASM_STATIC_SERVER_PACKAGE, WASM_STATIC_SERVER_VERSION, pubdir_name
202// );
203//
204// let manifest = wasmer_config::package::Manifest::parse(raw_static_site_toml.as_str())
205// .map_err(|e| anyhow::anyhow!("Could not parse js worker manifest: {}", e))?;
206//
207// Ok(manifest)
208// }
209//
210// fn initialize_js_worker(path: &Path) -> Result<wasmer_config::package::Manifest, anyhow::Error> {
211// let srcdir_name = "src";
212// let srcdir = path.join(srcdir_name);
213// if !srcdir.is_dir() {
214// std::fs::create_dir_all(&srcdir)
215// .with_context(|| format!("Failed to create directory: '{}'", srcdir.display()))?;
216// }
217//
218// let index_js = srcdir.join("index.js");
219//
220// let sample_js = SAMPLE_JS_WORKER.replace("{{package}}", "My JS worker");
221//
222// if !index_js.is_file() {
223// std::fs::write(&index_js, sample_js.as_str())
224// .with_context(|| "Could not write index.js file".to_string())?;
225// }
226//
227// // get the remote repository if it exists
228// // Todo: add this to the manifest
229// // let remote_repo_url = Command::new("git")
230// // .arg("remote")
231// // .arg("get-url")
232// // .arg("origin")
233// // .output()
234// // .map_or("".to_string(), |f| String::from_utf8(f.stdout).unwrap());
235//
236// let raw_js_worker_toml = format!(
237// r#"
238// [dependencies]
239// "{winterjs_pkg}" = "{winterjs_version}"
240//
241// [fs]
242// "/src" = "./src"
243//
244// [[command]]
245// name = "script"
246// module = "{winterjs_pkg}:winterjs"
247// runner = "https://webc.org/runner/wasi"
248//
249// [command.annotations.wasi]
250// main-args = ["/src/index.js"]
251// env = ["JS_PATH=/src/index.js"]
252// "#,
253// winterjs_pkg = WASMER_WINTER_JS_PACKAGE,
254// winterjs_version = WASMER_WINTER_JS_VERSION,
255// );
256//
257// let manifest = wasmer_config::package::Manifest::parse(raw_js_worker_toml.as_str())
258// .map_err(|e| anyhow::anyhow!("Could not parse js worker manifest: {}", e))?;
259//
260// Ok(manifest)
261// }
262//
263// fn initialize_py_worker(path: &Path) -> Result<wasmer_config::package::Manifest, anyhow::Error> {
264// let appdir_name = "src";
265// let appdir = path.join(appdir_name);
266// if !appdir.is_dir() {
267// std::fs::create_dir_all(&appdir)
268// .with_context(|| format!("Failed to create directory: '{}'", appdir.display()))?;
269// }
270// let main_py = appdir.join("main.py");
271//
272// let sample_main = SAMPLE_PY_APPLICATION.replace("{{package}}", "My Python Worker");
273//
274// if !main_py.is_file() {
275// std::fs::write(&main_py, sample_main.as_str())
276// .with_context(|| "Could not write main.py file".to_string())?;
277// }
278//
279// // Todo: add this to the manifest
280// // let remote_repo_url = Command::new("git")
281// // .arg("remote")
282// // .arg("get-url")
283// // .arg("origin")
284// // .output()
285// // .map_or("".to_string(), |f| String::from_utf8(f.stdout).unwrap());
286//
287// let raw_py_worker_toml = format!(
288// r#"
289// [dependencies]
290// "{}" = "{}"
291//
292// [fs]
293// "/src" = "./src"
294// # "/.env" = "./.env/" # Bundle the virtualenv
295//
296// [[command]]
297// name = "script"
298// module = "{}:python" # The "python" atom from "wasmer/python"
299// runner = "wasi"
300//
301// [command.annotations.wasi]
302// main-args = ["/src/main.py"]
303// # env = ["PYTHON_PATH=/app/.env:/etc/python3.12/site-packages"] # Make our virtualenv accessible
304// "#,
305// WASM_PYTHON_PACKAGE, WASM_PYTHON_VERSION, WASM_PYTHON_PACKAGE
306// );
307//
308// let manifest = wasmer_config::package::Manifest::parse(raw_py_worker_toml.as_str())
309// .map_err(|e| anyhow::anyhow!("Could not parse py worker manifest: {}", e))?;
310//
311// Ok(manifest)
312// }
313// #[cfg(test)]
314// mod tests {
315// use super::*;
316//
317// #[tokio::test]
318// async fn test_package_wizard_create_static_site() {
319// let dir = tempfile::tempdir().unwrap();
320//
321// PackageWizard {
322// path: dir.path().to_owned(),
323// type_: Some(PackageType::StaticWebsite),
324// create_mode: CreateMode::Create,
325// namespace: None,
326// namespace_default: None,
327// name: None,
328// user: None,
329// }
330// .run(None)
331// .await
332// .unwrap();
333//
334// let manifest = std::fs::read_to_string(dir.path().join("wasmer.toml")).unwrap();
335// pretty_assertions::assert_eq!(
336// manifest,
337// r#"[dependencies]
338// "wasmer/static-web-server" = "^1"
339//
340// [fs]
341// public = "public"
342// "#,
343// );
344//
345// assert!(dir.path().join("public").join("index.html").is_file());
346// }
347//
348// #[tokio::test]
349// async fn test_package_wizard_create_js_worker() {
350// let dir = tempfile::tempdir().unwrap();
351//
352// PackageWizard {
353// path: dir.path().to_owned(),
354// type_: Some(PackageType::JsWorker),
355// create_mode: CreateMode::Create,
356// namespace: None,
357// namespace_default: None,
358// name: None,
359// user: None,
360// }
361// .run(None)
362// .await
363// .unwrap();
364// let manifest = std::fs::read_to_string(dir.path().join("wasmer.toml")).unwrap();
365//
366// pretty_assertions::assert_eq!(
367// manifest,
368// r#"[dependencies]
369// "wasmer/winterjs" = "*"
370//
371// [fs]
372// "/src" = "./src"
373//
374// [[command]]
375// name = "script"
376// module = "wasmer/winterjs:winterjs"
377// runner = "https://webc.org/runner/wasi"
378//
379// [command.annotations.wasi]
380// env = ["JS_PATH=/src/index.js"]
381// main-args = ["/src/index.js"]
382// "#,
383// );
384//
385// assert!(dir.path().join("src").join("index.js").is_file());
386// }
387//
388// #[tokio::test]
389// async fn test_package_wizard_create_py_worker() {
390// let dir = tempfile::tempdir().unwrap();
391//
392// PackageWizard {
393// path: dir.path().to_owned(),
394// type_: Some(PackageType::PyApplication),
395// create_mode: CreateMode::Create,
396// namespace: None,
397// namespace_default: None,
398// name: None,
399// user: None,
400// }
401// .run(None)
402// .await
403// .unwrap();
404// let manifest = std::fs::read_to_string(dir.path().join("wasmer.toml")).unwrap();
405//
406// pretty_assertions::assert_eq!(
407// manifest,
408// r#"[dependencies]
409// "wasmer/python" = "^3.12.6"
410//
411// [fs]
412// "/src" = "./src"
413//
414// [[command]]
415// name = "script"
416// module = "wasmer/python:python"
417// runner = "wasi"
418//
419// [command.annotations.wasi]
420// main-args = ["/src/main.py"]
421// "#,
422// );
423//
424// assert!(dir.path().join("src").join("main.py").is_file());
425// }
426// }