1use std::path::{Path, PathBuf};
2use std::sync::Arc;
3use boa_engine::{JsString, JsValue};
4use boa_engine::object::builtins::JsArray;
5use serde::{Deserialize};
6pub use sfo_result::err as js_pkg_err;
7pub use sfo_result::into_err as into_js_pkg_err;
8use crate::errors::JSResult;
9use crate::{JsEngine, JsEngineInitCallback};
10
11pub type JsPkgResult<T> = sfo_result::Result<T, ()>;
12
13#[derive(Deserialize)]
14pub struct JsPkgConfig {
15 name: String,
16 main: Option<String>,
17 description: Option<String>,
18 help: Option<String>,
19}
20
21#[derive(Clone)]
22pub struct JsPkg {
23 name: String,
24 main: String,
25 description: String,
26 help: String,
27 enable_fetch: bool,
28 enable_console: bool,
29 enable_commonjs: bool,
30 init_callback: Option<JsEngineInitCallback>,
31}
32
33impl JsPkg {
34 pub fn new(name: impl Into<String>,
35 main: impl Into<String>,
36 description: impl Into<String>,
37 help: impl Into<String>) -> Self {
38 JsPkg {
39 name: name.into(),
40 main: main.into(),
41 description: description.into(),
42 help: help.into(),
43 enable_fetch: true,
44 enable_console: true,
45 enable_commonjs: true,
46 init_callback: None,
47 }
48 }
49
50 pub fn name(&self) -> &str {
51 self.name.as_str()
52 }
53
54 pub fn main(&self) -> &str {
55 self.main.as_str()
56 }
57
58 pub fn description(&self) -> &str {
59 self.description.as_str()
60 }
61
62 pub fn enable_fetch(&mut self, enable: bool) -> &mut Self {
63 self.enable_fetch = enable;
64 self
65 }
66
67 pub fn enable_console(&mut self, enable: bool) -> &mut Self {
68 self.enable_console = enable;
69 self
70 }
71
72 pub fn enable_commonjs(&mut self, enable: bool) -> &mut Self {
73 self.enable_commonjs = enable;
74 self
75 }
76
77 pub fn init_callback<F>(&mut self, callback: F) -> &mut Self
78 where
79 F: Fn(&mut JsEngine) -> JSResult<()> + Send + Sync + 'static,
80 {
81 self.init_callback = Some(Arc::new(callback));
82 self
83 }
84
85 pub async fn run(&self, args: Vec<String>) -> JsPkgResult<String> {
86 let enable_fetch = self.enable_fetch;
87 let enable_console = self.enable_console;
88 let enable_commonjs = self.enable_commonjs;
89 let init_callback = self.init_callback.clone();
90 let main = self.main.clone();
91 let ret = tokio::task::spawn_blocking(move || {
92 let mut builder = JsEngine::builder()
93 .enable_fetch(enable_fetch)
94 .enable_console(enable_console)
95 .enable_commonjs(enable_commonjs);
96 if let Some(init_callback) = init_callback {
97 builder = builder.init_callback(move |engine| (init_callback)(engine));
98 }
99 let mut js_engine = builder
100 .build()
101 .map_err(into_js_pkg_err!("build js engine error"))?;
102
103 js_engine.eval_file(Path::new(main.as_str()))
104 .map_err(into_js_pkg_err!("eval file {}", main.as_str()))?;
105
106 let args = args.iter()
107 .map(|v| JsValue::from(JsString::from(v.as_str())))
108 .collect::<Vec<_>>();
109 let args = JsArray::from_iter(args.into_iter(), js_engine.context());
110 let result = js_engine.call("main", vec![JsValue::from(args)])
111 .map_err(into_js_pkg_err!("call main"))?;
112 if result.is_string() {
113 Ok(result.as_string().unwrap().as_str().to_std_string_lossy())
114 } else {
115 Err(js_pkg_err!("main must return a string"))
116 }
117 }).await.map_err(into_js_pkg_err!("run {}", self.name))?;
118 ret
119 }
120
121 pub async fn help(&self) -> JsPkgResult<String> {
122 if self.help.is_empty() {
123 let enable_fetch = self.enable_fetch;
124 let enable_console = self.enable_console;
125 let enable_commonjs = self.enable_commonjs;
126 let init_callback = self.init_callback.clone();
127 let main = self.main.clone();
128 let ret = tokio::task::spawn_blocking(move || {
129 let mut builder = JsEngine::builder()
130 .enable_fetch(enable_fetch)
131 .enable_console(enable_console)
132 .enable_commonjs(enable_commonjs);
133 if let Some(init_callback) = init_callback {
134 builder = builder.init_callback(move |engine| (init_callback)(engine));
135 }
136 let mut js_engine = builder
137 .build()
138 .map_err(into_js_pkg_err!("build js engine error"))?;
139
140 js_engine.eval_file(Path::new(main.as_str()))
141 .map_err(into_js_pkg_err!("eval file {}", main.as_str()))?;
142
143 let args = vec![JsValue::from(JsString::from("--help"))];
144 let args = JsArray::from_iter(args.into_iter(), js_engine.context());
145 let _ = js_engine.call("main", vec![JsValue::from(args)])
146 .map_err(into_js_pkg_err!("call main"))?;
147
148 Ok(js_engine.get_output())
149 }).await.map_err(into_js_pkg_err!("run {}", self.name))?;
150 ret
151 } else {
152 Ok(self.help.clone())
153 }
154 }
155}
156pub struct JsPkgManager {
157 js_cmd_path: PathBuf,
158}
159pub type JsPkgManagerRef = Arc<JsPkgManager>;
160
161impl JsPkgManager {
162 pub fn new(js_cmd_path: PathBuf) -> Arc<Self> {
163 Arc::new(JsPkgManager {
164 js_cmd_path,
165 })
166 }
167
168 pub async fn list_pkgs(&self) -> JsPkgResult<Vec<JsPkg>> {
169 let dirs = self.js_cmd_path.read_dir()
170 .map_err(into_js_pkg_err!("read {:?}", self.js_cmd_path))?;
171 let mut pkgs = vec![];
172 for entry in dirs {
173 if let Ok(entry) = entry {
174 let path = entry.path();
175 if path.is_dir() {
176 let cmd = self.load_pkg(&path).await?;
177 pkgs.push(cmd);
178 }
179 }
180 }
181 Ok(pkgs)
182 }
183
184 async fn load_pkg(&self, path: &Path) -> JsPkgResult<JsPkg> {
185 let cfg_path = path.join("pkg.yaml");
186 if cfg_path.exists() {
187 let content = tokio::fs::read_to_string(cfg_path.as_path()).await
188 .map_err(into_js_pkg_err!("read file {}", cfg_path.to_string_lossy().to_string()))?;
189 let config = serde_yaml_ng::from_str::<JsPkgConfig>(content.as_str())
190 .map_err(into_js_pkg_err!("parse {}", content))?;
191 let main = config.main
192 .map(|v| path.join(v).to_string_lossy().to_string())
193 .unwrap_or(path.join("main.js").to_string_lossy().to_string());
194 Ok(JsPkg::new(
195 config.name,
196 main,
197 config.description.unwrap_or("".to_string()),
198 config.help.unwrap_or("".to_string()),
199 ))
200 } else {
201 let main_js = path.join("main.js");
202 if !main_js.exists() {
203 return Err(js_pkg_err!("{} not exists", main_js.to_string_lossy().to_string()));
204 }
205 if let Some(file_name) = path.file_name() {
206 Ok(JsPkg::new(
207 file_name.to_string_lossy().to_string(),
208 main_js.to_string_lossy().to_string(),
209 "",
210 "",
211 ))
212 } else {
213 Err(js_pkg_err!("{} not exists", main_js.to_string_lossy().to_string()))
214 }
215 }
216 }
217
218 pub async fn get_pkg(&self, name: impl Into<String>) -> JsPkgResult<JsPkg> {
219 self.load_pkg(self.js_cmd_path.join(name.into()).as_path()).await
220 }
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226 use tempfile::TempDir;
227 use tokio::fs;
228
229 #[tokio::test]
230 async fn test_list_pkgs() {
231 let temp_dir = TempDir::new().unwrap();
233 let test_path = temp_dir.path();
234
235 let pkg1_path = test_path.join("pkg1");
237 fs::create_dir_all(&pkg1_path).await.unwrap();
238 let main_js_content = r#"
239 export function main(args) {
240 console.log("Hello from pkg1");
241 return "pkg1 executed";
242 }
243 "#;
244 fs::write(pkg1_path.join("main.js"), main_js_content).await.unwrap();
245
246 let pkg2_path = test_path.join("pkg2");
248 fs::create_dir_all(&pkg2_path).await.unwrap();
249 let pkg2_yaml_content = r#"
250 name: "pkg2"
251 main: "index.js"
252 description: "A test package"
253 params: "test_params"
254 "#;
255 fs::write(pkg2_path.join("pkg.yaml"), pkg2_yaml_content).await.unwrap();
256 let index_js_content = r#"
257 export function main(args) {
258 console.log("Hello from pkg2");
259 console.log(args);
260 return "pkg2 executed";
261 }
262 "#;
263 fs::write(pkg2_path.join("index.js"), index_js_content).await.unwrap();
264
265 let manager = JsPkgManager::new(test_path.to_path_buf());
266 let pkgs = manager.list_pkgs().await.unwrap();
267 assert_eq!(pkgs.len(), 2);
268 assert_eq!(pkgs[0].name(), "pkg1");
269 assert_eq!(pkgs[1].name(), "pkg2");
270
271 let pkg1 = pkgs[0].clone();
272 let pkg2 = pkgs[1].clone();
273 pkg2.run(vec!["arg1".to_string(), "arg2".to_string()]).await.unwrap();
274 }
275}