sfo_js/
js_pkg.rs

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        // 创建临时目录
232        let temp_dir = TempDir::new().unwrap();
233        let test_path = temp_dir.path();
234
235        // 创建测试包目录 pkg1
236        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        // 创建测试包目录 pkg2
247        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}