Skip to main content

napi_modules/
lib.rs

1use camino::Utf8PathBuf;
2use derive_more::{From, Into};
3use memoize::memoize;
4use napi::bindgen_prelude::*;
5use thiserror::Error;
6use url::{ParseError, Url};
7use std::{hash::{Hash, Hasher}, rc::Rc, result::Result};
8
9pub(crate) trait Sealed {}
10
11impl Sealed for Env {}
12
13#[allow(private_bounds)]
14pub trait EnvExt: Sealed {
15    fn filename(&self) -> napi::Result<Utf8PathBuf>;
16    fn require<T: FromNapiValue>(&self, id: impl AsRef<str>) -> napi::Result<T>;
17    fn require_resolve(&self, id: impl AsRef<str>) -> napi::Result<String>;
18    fn import(&self, specifier: impl AsRef<str>, options: Option<Object>) -> napi::Result<Promise<Object<'_>>>;
19    fn import_meta_resolve(&self, specifier: impl AsRef<str>) -> napi::Result<String>;
20    fn is_main(&self) -> napi::Result<bool>;
21}
22
23impl EnvExt for Env {
24    fn filename(&self) -> napi::Result<Utf8PathBuf> {
25        let file_url_string = self.get_module_file_name()?;
26        let path = file_url_string_to_utf8_path_buf(&file_url_string).map_err(|e| napi::Error::from_reason(e.to_string()))?;
27        Ok(path)
28    }
29    fn require<T: FromNapiValue>(&self, id: impl AsRef<str>) -> napi::Result<T> {
30        let require = require_for(self.clone().into()).map_err(|e| napi::Error::from_reason(e))?;
31        let require = require.borrow_back(self)?;
32        let module = require.call(id.as_ref())?;
33        let module: T = unsafe { module.cast()? };
34        Ok(module)
35    }
36    fn require_resolve(&self, id: impl AsRef<str>) -> napi::Result<String> {
37        let require = require_for(self.clone().into()).map_err(|e| napi::Error::from_reason(e))?;
38        let require = require.borrow_back(self)?;
39        let require_resolve: Function<&str, String> = require.get_named_property("resolve")?;
40        require_resolve.call(id.as_ref())
41    }
42    fn import(&self, specifier: impl AsRef<str>, options: Option<Object>) -> napi::Result<Promise<Object<'_>>> {
43        let esm_helpers = esm_helpers_for(self.clone().into()).map_err(|e| napi::Error::from_reason(e))?;
44        let esm_helpers = esm_helpers.get_value(self)?;
45        let import: Function<FnArgs<(&str, Option<Object>)>, Promise<Object>> = esm_helpers.get_named_property("import")?;
46        import.call((specifier.as_ref(), options).into())
47    }
48    fn import_meta_resolve(&self, specifier: impl AsRef<str>) -> napi::Result<String> {
49        let esm_helpers = esm_helpers_for(self.clone().into()).map_err(|e| napi::Error::from_reason(e))?;
50        let esm_helpers = esm_helpers.get_value(self)?;
51        let import_meta_resolve: Function<&str, String> = esm_helpers.get_named_property("importMetaResolve")?;
52        import_meta_resolve.call(specifier.as_ref())
53    }
54    fn is_main(&self) -> napi::Result<bool> {
55        let require = require_for(self.clone().into()).map_err(|e| napi::Error::from_reason(e))?;
56        let require = require.borrow_back(self)?;
57        let main: Option<Object> = require.get_named_property("main")?;
58        if let Some(main) = main {
59            let self_path = self.filename()?;
60            let main_path: String = main.get_named_property("filename")?;
61            let main_path = Utf8PathBuf::from(main_path);
62            Ok(self_path == main_path)
63        } else {
64            Ok(false)
65        }
66    }
67}
68
69#[derive(Debug, Clone, PartialEq, Eq, Error)]
70#[non_exhaustive]
71enum EsmHelpersPathForError {
72    #[error("I/O error: {0}")]
73    IoError(String),
74}
75
76#[memoize]
77fn esm_helpers_path_for(addon_path: Utf8PathBuf) -> Result<Utf8PathBuf, EsmHelpersPathForError> {
78    const ESM_HELPERS_JS: &str = r#"
79        const _import = (specifier, options) => import(specifier, options);
80        export { _import as "import" };
81        export const importMetaResolve = (specifier) => import.meta.resolve(specifier);
82    "#;
83    let esm_helpers_path = addon_path.with_added_extension("esm-helpers.js");
84    fs_err::write(&esm_helpers_path, ESM_HELPERS_JS).map_err(|e| EsmHelpersPathForError::IoError(e.to_string()))?;
85    Ok(esm_helpers_path)
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
89#[non_exhaustive]
90enum FileUrlStringToUtf8PathBufError {
91    #[error("parse error: {0}")]
92    UrlParseError(#[from] ParseError),
93    #[error("scheme is not 'file'")]
94    SchemeNotFile,
95    #[error("to_file_path failed")]
96    ToFilePathFailed,
97    #[error("path is not valid UTF-8")]
98    PathNotUtf8,
99}
100
101fn file_url_string_to_utf8_path_buf(file_url_string: &str) -> Result<Utf8PathBuf, FileUrlStringToUtf8PathBufError> {
102    let url = Url::parse(file_url_string).map_err(FileUrlStringToUtf8PathBufError::UrlParseError)?;
103    if url.scheme() != "file" {
104        return Err(FileUrlStringToUtf8PathBufError::SchemeNotFile);
105    }
106    let path = url.to_file_path().map_err(|_| FileUrlStringToUtf8PathBufError::ToFilePathFailed)?;
107    let utf8_path = Utf8PathBuf::from_path_buf(path).map_err(|_| FileUrlStringToUtf8PathBufError::PathNotUtf8)?;
108    Ok(utf8_path)
109}
110
111#[derive(Clone, Copy, From, Into)]
112#[repr(transparent)]
113pub(crate) struct EnvEqHash(pub Env);
114
115impl Eq for EnvEqHash {}
116
117impl PartialEq for EnvEqHash {
118    fn eq(&self, other: &Self) -> bool {
119        let self_file_url_string = self.0.get_module_file_name().expect("get_module_file_name should succeed");
120        let other_file_url_string = other.0.get_module_file_name().expect("get_module_file_name should succeed");
121        self_file_url_string.eq(&other_file_url_string)
122    }
123}
124
125impl Hash for EnvEqHash {
126    fn hash<H: Hasher>(&self, state: &mut H) {
127        let file_url_string = self.0.get_module_file_name().expect("get_module_file_name should succeed");
128        file_url_string.hash(state);
129    }
130}
131
132#[memoize]
133fn require_for(env: EnvEqHash) -> Result<Rc<FunctionRef<&'static str, Unknown<'static>>>, String> {
134    let env = env.0;
135    let global = env.get_global().map_err(|e| e.to_string())?;
136    let process: Object = global.get_named_property("process").map_err(|e| e.to_string())?;
137    let get_builtin_module: Function<&str, Unknown> = process.get_named_property("getBuiltinModule").map_err(|e| e.to_string())?;
138    let module = get_builtin_module.call("node:module").map_err(|e| e.to_string())?;
139    // SAFETY: `node:module` is an object.
140    let module: Object = unsafe { module.cast().map_err(|e| e.to_string())? };
141    let create_require: Function<&str, Function<&str, Unknown>> = module.get_named_property("createRequire").map_err(|e| e.to_string())?;
142    let path = env.filename().map_err(|e| e.to_string())?;
143    let require = create_require.call(path.as_str()).map_err(|e| e.to_string())?;
144    let require = require.create_ref().map_err(|e| e.to_string())?;
145    Ok(require.into())
146}
147
148#[memoize]
149fn esm_helpers_for(env: EnvEqHash) -> Result<Rc<ObjectRef>, String> {
150    let env = env.0;
151    let path = env.filename().map_err(|e| e.to_string())?;
152    let esm_helpers_path = esm_helpers_path_for(path).map_err(|e| e.to_string())?;
153    let require = require_for(env.into())?;
154    let require = require.borrow_back(&env).map_err(|e| e.to_string())?;
155    let esm_helpers = require.call(esm_helpers_path.as_str()).map_err(|e| e.to_string())?;
156    // SAFETY: `esm-helpers.js` is an object.
157    let esm_helpers: Object = unsafe { esm_helpers.cast().map_err(|e| e.to_string())? };
158    let esm_helpers = esm_helpers.create_ref().map_err(|e| e.to_string())?;
159    Ok(esm_helpers.into())
160}