green_copper_runtime/features/
require.rs

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
//! require
//!
//! this mod implements a require method which can be used to load CommonJS modules
//!
//! It uses the available ScriptModuleLoader instances in QuickJSRuntime
//!

use quickjs_runtime::builder::QuickJsRuntimeBuilder;
use quickjs_runtime::jsutils::{JsError, JsValueType, Script};
use quickjs_runtime::quickjsrealmadapter::QuickJsRealmAdapter;
use quickjs_runtime::quickjsruntimeadapter::QuickJsRuntimeAdapter;
use quickjs_runtime::quickjsvalueadapter::QuickJsValueAdapter;

pub fn init(builder: QuickJsRuntimeBuilder) -> QuickJsRuntimeBuilder {
    // todo.. this should utilize the script module loaders in order to obtain the source, then use a 'require' function in js to do the actual loading..
    builder.runtime_facade_init_hook(|rt| {
        // todo, impl with native function.. like now

        rt.loop_sync_mut(|js_rt| {
            js_rt.add_realm_init_hook(|_js_rt, realm| {
                //let global = get_global_q(q_ctx);
                //let require_func =
                //    new_native_function_q(q_ctx, "require", Some(require), 1, false)?;
                //set_property2_q(q_ctx, &global, "require", &require_func, 0)?;
                realm.install_function(&[], "require", require, 1)
            })
        })?;

        Ok(())
    })
}

const DEFAULT_EXTENSIONS: &[&str] = &["js", "mjs", "ts", "mts"];

fn require(
    runtime: &QuickJsRuntimeAdapter,
    realm: &QuickJsRealmAdapter,
    _this_val: &QuickJsValueAdapter,
    args: &[QuickJsValueAdapter],
) -> Result<QuickJsValueAdapter, JsError> {
    if args.len() != 1 || args[0].get_js_type() != JsValueType::String {
        Err(JsError::new_str(
            "require requires a single string argument",
        ))
    } else {
        let name = args[0].to_string()?;

        let mut cur_path = realm
            .get_script_or_module_name()
            .ok()
            .unwrap_or_else(|| "file:///node_modules/foo.js".to_string());

        // * if name does not start with / or ./ or ../ then use node_modules ref_path (if ref_path is file:///??)
        // todo , where do i cache these? a shutdown hook on a QuickJsContext would be nice to clear my own caches
        // much rather have a q_ctx.cache_region("").cache(id, obj)

        // see https://nodejs.org/en/knowledge/getting_started/what_is_require
        // * todo 2 support for directories, and then greco_jspreproc.js or package.json?

        // hmm if a module is loaded from https://somegit.somesite.com/scripts/kewlStuff.js and that does a require.. do we look in node_modules on disk?
        if !(name.contains("://")
            || name.starts_with("./")
            || name.starts_with("../")
            || name.starts_with('/'))
        {
            cur_path = format!("file:///node_modules/{name}/foo.js");
        }

        log::debug!("require: {} -> {}", cur_path, name);

        let module_script_opt = (|| {
            let opt = runtime.load_module_script(cur_path.as_str(), name.as_str());
            if opt.is_some() {
                return opt;
            }
            for ext in DEFAULT_EXTENSIONS {
                let opt = runtime.load_module_script(
                    cur_path.as_str(),
                    format!("{}.{}", name.as_str(), ext).as_str(),
                );
                if opt.is_some() {
                    return opt;
                }
            }

            // see if index.js exists
            let mut base_name = name.clone();
            if let Some(rpos) = base_name.rfind('/') {
                let _ = base_name.split_off(rpos + 1);
            } else {
                base_name = "".to_string();
            }
            let opt = runtime.load_module_script(
                cur_path.as_str(),
                format!("{}{}", base_name, "index.js").as_str(),
            );
            if opt.is_some() {
                return opt;
            }

            None
        })();

        if let Some(module_script) = module_script_opt {
            // todo need to wrap as ES6 module so ScriptOrModuleName is sound for children
            log::debug!("found module script at {}", module_script.get_path());

            let wrapped_eval_code = format!(
                    "(function(){{const module = {{exports:{{}}}};let exports = module.exports;{{{}\n}}; return(module.exports);}}())",
                    module_script.get_code()
                );
            let eval_res = realm.eval(Script::new(
                module_script.get_path(),
                wrapped_eval_code.as_str(),
            ));
            eval_res
        } else {
            log::error!("module not found: {} -> {}", cur_path, name);
            Err(JsError::new_string(format!(
                "module not found: {cur_path} -> {name}"
            )))
        }
    }
}

#[cfg(test)]
mod tests {

    #[test]
    fn test_eval() {}
}