1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
use std::{rc::Rc, sync::Arc};
use crate::resolver::BasicNpmResolver;
use crate::{
    error::{Error, catch_exception},
    module::{JsModule, ModuleInitializer, JsModuleType},
};
use deno_runtime::{
    deno_core::{v8, serde_v8::from_v8, Extension, FsModuleLoader, ModuleSpecifier},
    deno_napi::v8::GetPropertyNamesArgs,
    permissions::PermissionsContainer,
    worker::{MainWorker, WorkerOptions},
};

/// options for instantiating a [JsWorker]
#[derive(Debug, Clone)]
pub struct JsWorkerInitOptions {
    pub main_module_initializer: ModuleInitializer,
    pub node_modules_url: Option<ModuleSpecifier>,
}

/// The main struct that wraps the deno js runtime and provides methods to easily load js modules
/// and interact with them
pub struct JsWorker {
    pub(crate) main_worker: MainWorker,
    pub(crate) main_module: JsModule,
    pub(crate) node_modules_url: Option<ModuleSpecifier>,
}

impl JsWorker {
    /// get main worker [MainWorker] of this instance
    pub fn main_worker(&mut self) -> &mut MainWorker {
        &mut self.main_worker
    }

    /// get main module [JsModule] of this instance
    pub fn main_module(&self) -> &JsModule {
        &self.main_module
    }

    /// get node_modules url of this instance
    pub fn node_modules_url(&self) -> Option<ModuleSpecifier> {
        self.node_modules_url.clone()
    }

    /// creates a new instance, if no path node_modules is provided, it will default to
    /// main_module_path/node_modules
    pub async fn init(
        options: JsWorkerInitOptions,
        extensions: Option<Vec<Extension>>,
    ) -> Result<JsWorker, Error> {
        let node_modules_path = if let Some(p) = &options.node_modules_url {
            p.clone()
        } else {
            options
                .main_module_initializer
                .url
                .join("..")?
                .join("node_modules")?
        };

        let basic_npm_resolver = BasicNpmResolver {
            node_modules_url: node_modules_path,
        };
        let mut main_worker = MainWorker::bootstrap_from_options(
            options.main_module_initializer.url.clone(),
            PermissionsContainer::allow_all(),
            WorkerOptions {
                module_loader: Rc::new(FsModuleLoader),
                npm_resolver: Some(Arc::new(basic_npm_resolver)),
                extensions: extensions.unwrap_or_default(),
                ..Default::default()
            },
        );

        // load main module
        let main_module_id = if options.main_module_initializer.mod_type == JsModuleType::Esm {
            main_worker
                .preload_main_module(&options.main_module_initializer.url)
                .await?
        } else {
            // load require and put in globalThis to be accessible by all cjs modules
            let require_mod_id = main_worker
                .js_runtime
                .load_side_es_module_from_code(
                    &ModuleSpecifier::parse("ext:__requireLoader____")?,
                    format!(
                        r#"import {{ createRequire as __internalCreateRequire____ }} from "node:module";
globalThis.require = __internalCreateRequire____("{}");"#,
                        options.main_module_initializer.url.as_str(),
                    ),
                )
                .await?;
            main_worker.evaluate_module(require_mod_id).await?;

            main_worker
                .js_runtime
                .load_side_es_module_from_code(
                    &ModuleSpecifier::parse("ext:__cjsMainModuleExporter____")?,
                    format!(
                        r#"const __moduleExports____ = require("{}"); export default __moduleExports____;"#,
                        options.main_module_initializer.url.path()
                    ),
                )
                .await?
        };
        main_worker.evaluate_module(main_module_id).await?;

        // run eventloop to finish
        main_worker.run_event_loop(false).await?;

        // get export keys of main module
        let exports = {
            let module = main_worker
                .js_runtime
                .get_module_namespace(main_module_id)?;
            let mut scope = main_worker.js_runtime.handle_scope();
            let module = module.open(&mut scope);
            if options.main_module_initializer.mod_type == JsModuleType::Esm {
                let names = module.get_property_names(&mut scope, GetPropertyNamesArgs::default());
                if let Some(v) = names {
                    from_v8::<Vec<String>>(&mut scope, v.into())?
                } else {
                    vec![]
                }
            } else {
                let mut all_exports = vec!["default".to_string()];
                let default_key = v8::String::new(&mut scope, "default")
                    .ok_or(Error::FailedToGetV8Value)?
                    .into();
                let default_export = module
                    .get(&mut scope, default_key)
                    .ok_or(Error::FailedToGetV8Value)?
                    .to_object(&mut scope);
                if let Some(default_export) = default_export {
                    let inner_exports = default_export
                        .get_property_names(&mut scope, GetPropertyNamesArgs::default())
                        .ok_or(Error::FailedToGetV8Value)?;
                    let inner_exports = from_v8::<Vec<String>>(&mut scope, inner_exports.into())?;
                    all_exports.extend_from_slice(&inner_exports);
                }
                all_exports
            }
        };

        Ok(JsWorker {
            main_worker,
            node_modules_url: options.node_modules_url,
            main_module: JsModule {
                id: main_module_id,
                mod_type: options.main_module_initializer.mod_type,
                exports,
                url: options.main_module_initializer.url,
            },
        })
    }

    /// get module object instance
    pub fn get_main_module_instance(&mut self) -> Result<v8::Global<v8::Object>, Error> {
        let module = self
            .main_worker
            .js_runtime
            .get_module_namespace(self.main_module.id)?;
        Ok(module)
    }

    /// get the export value
    pub fn get_export(&mut self, name: &str) -> Result<v8::Global<v8::Value>, Error> {
        if !self.main_module.export_exists(name) {
            return Err(Error::UndefinedExport);
        }

        let mut module = self.get_main_module_instance()?;
        let scope = &mut self.main_worker.js_runtime.handle_scope();

        if self.main_module.mod_type == JsModuleType::Cjs {
            let module_instance = module.open(scope);
            let default_key = v8::String::new(scope, "default").ok_or(Error::FailedToGetV8Value)?;
            let default_export = module_instance
                .get(scope, default_key.into())
                .ok_or(Error::FailedToGetV8Value)?;

            let default_export = default_export
                .to_object(scope)
                .ok_or(Error::FailedToGetV8Value)?;

            module = v8::Global::new(scope, default_export);
        }

        let module = module.open(scope);
        let key = v8::String::new(scope, name).ok_or(Error::FailedToGetV8Value)?;
        let value = module
            .get(scope, key.into())
            .ok_or(Error::FailedToGetV8Value)?;

        Ok(v8::Global::new(scope, value))
    }

    /// call js function
    pub fn call_fn(
        &mut self,
        name: &str,
        args: &[v8::Global<v8::Value>],
    ) -> Result<v8::Global<v8::Value>, Error> {
        let function = self.get_export(name)?;

        let scope = &mut self.main_worker.js_runtime.handle_scope();
        let scope = &mut v8::TryCatch::new(scope);
        let undefined = v8::undefined(scope).into();

        let function: v8::Local<v8::Function> = v8::Local::new(scope, function).try_into()?;
        let mut local_args = vec![];
        args.iter()
            .for_each(|v| local_args.push(v8::Local::new(scope, v)));
        let result = function.call(scope, undefined, &local_args);

        result
            .map(|v| v8::Global::new(scope, v))
            .ok_or(catch_exception(scope))
    }

    /// call an async js function and get the resolved/rejected results
    pub async fn call_async_fn(
        &mut self,
        name: &str,
        args: &[v8::Global<v8::Value>],
    ) -> Result<v8::Global<v8::Value>, Error> {
        let res = self.call_fn(name, args)?;
        let future = self.main_worker.js_runtime.resolve(res);
        Ok(self
            .main_worker
            .js_runtime
            .with_event_loop_future(future, Default::default())
            .await?)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::anyhow;

    #[tokio::test]
    async fn test_init_esm() -> anyhow::Result<()> {
        let options = JsWorkerInitOptions {
            main_module_initializer: ModuleInitializer {
                mod_type: JsModuleType::Esm,
                url: ModuleSpecifier::from_file_path(
                    std::env::current_dir().unwrap().join("data/esm.js"),
                )
                .unwrap(),
            },
            node_modules_url: None,
        };
        let js_worker = JsWorker::init(options, None).await?;
        let expected_exported_modules_keys = vec![
            "asyncFnReject".to_string(),
            "asyncFnResolve".to_string(),
            "fnWithError".to_string(),
            "topFn".to_string(),
        ];

        assert_eq!(
            js_worker.main_module.exports,
            expected_exported_modules_keys
        );

        Ok(())
    }

    #[tokio::test]
    async fn test_init_cjs() -> anyhow::Result<()> {
        let options = JsWorkerInitOptions {
            main_module_initializer: ModuleInitializer {
                mod_type: JsModuleType::Cjs,
                url: ModuleSpecifier::from_file_path(
                    std::env::current_dir().unwrap().join("data/cjs.js"),
                )
                .unwrap(),
            },
            node_modules_url: None,
        };
        let js_worker = JsWorker::init(options, None).await?;

        // cjs modules always have default exports
        let expected_exported_modules_keys = vec![
            "default".to_string(),
            "topFn".to_string(),
            "asyncFnResolve".to_string(),
            "asyncFnReject".to_string(),
            "fnWithError".to_string(),
        ];
        assert_eq!(
            js_worker.main_module.exports,
            expected_exported_modules_keys
        );

        Ok(())
    }

    #[tokio::test]
    async fn test_call_fn_esm() -> anyhow::Result<()> {
        let options = JsWorkerInitOptions {
            main_module_initializer: ModuleInitializer {
                mod_type: JsModuleType::Esm,
                url: ModuleSpecifier::from_file_path(
                    std::env::current_dir().unwrap().join("data/esm.js"),
                )
                .unwrap(),
            },
            node_modules_url: None,
        };
        let mut js_worker = JsWorker::init(options, None).await?;
        let arg = {
            let scope = &mut js_worker.main_worker.js_runtime.handle_scope();
            let arg: v8::Local<v8::Value> = v8::Integer::new(scope, 5).into();
            v8::Global::new(scope, arg)
        };

        let res1 = js_worker.call_fn("topFn", &[arg.clone()]).unwrap(); // sync fn without erroring
        let res2 = js_worker.call_async_fn("asyncFnResolve", &[arg]).await?; // async fn that will resolve
        let res3 = js_worker.call_fn("fnWithError", &[]); // sync fn that will error
        let res4 = js_worker.call_async_fn("asyncFnReject", &[]).await; // async fn that will reject

        let scope = &mut js_worker.main_worker.js_runtime.handle_scope();

        let res1 = v8::Local::new(scope, res1);
        let res1 = from_v8::<u32>(scope, res1)?;

        let res2 = v8::Local::new(scope, res2);
        let res2 = from_v8::<u32>(scope, res2)?;

        // res1 should return correct value ie 5 + 1
        assert_eq!(res1, 6);
        // res2 should return correct value ie 5
        assert_eq!(res2, 5);
        // res3 should error
        matches!(res3, Err(Error::JsException(_)));
        // res4 should error
        matches!(res4, Err(Error::DenoError(_)));

        Ok(())
    }

    #[tokio::test]
    async fn test_call_fn_cjs() -> anyhow::Result<()> {
        let options = JsWorkerInitOptions {
            main_module_initializer: ModuleInitializer {
                mod_type: JsModuleType::Cjs,
                url: ModuleSpecifier::from_file_path(
                    std::env::current_dir().unwrap().join("data/cjs.js"),
                )
                .unwrap(),
            },
            node_modules_url: None,
        };
        let mut js_worker = JsWorker::init(options, None).await?;
        let arg = {
            let scope = &mut js_worker.main_worker.js_runtime.handle_scope();
            let arg: v8::Local<v8::Value> = v8::Integer::new(scope, 5).into();
            v8::Global::new(scope, arg)
        };

        let res1 = js_worker.call_fn("topFn", &[arg.clone()]).unwrap(); // sync fn without erroring
        let res2 = js_worker.call_async_fn("asyncFnResolve", &[arg]).await?; // async fn that will resolve
        let res3 = js_worker.call_fn("fnWithError", &[]); // sync fn that will error
        let res4 = js_worker.call_async_fn("asyncFnReject", &[]).await; // async fn that will reject

        let scope = &mut js_worker.main_worker.js_runtime.handle_scope();

        let res1 = v8::Local::new(scope, res1);
        let res1 = from_v8::<u32>(scope, res1)?;

        let res2 = v8::Local::new(scope, res2);
        let res2 = from_v8::<u32>(scope, res2)?;

        // res1 should return correct value ie 5 + 1
        assert_eq!(res1, 6);
        // res2 should return correct value ie 5
        assert_eq!(res2, 5);
        // res3 should error
        matches!(res3, Err(Error::JsException(_)));
        // res4 should error
        matches!(res4, Err(Error::DenoError(_)));

        Ok(())
    }
}