Skip to main content

sfo_js/
js_pkg.rs

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        // 创建临时目录
272        let temp_dir = TempDir::new().unwrap();
273        let test_path = temp_dir.path();
274
275        // 创建测试包目录 pkg1
276        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        // 创建测试包目录 pkg2
287        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}