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// }