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