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 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 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}