turbopack_resolve/
node_native_binding.rs

1use std::sync::LazyLock;
2
3use anyhow::Result;
4use regex::Regex;
5use serde::{Deserialize, Serialize};
6use turbo_rcstr::{RcStr, rcstr};
7use turbo_tasks::{FxIndexMap, ResolvedVc, TryFlatJoinIterExt, TryJoinIterExt, ValueToString, Vc};
8use turbo_tasks_fs::{
9    DirectoryContent, DirectoryEntry, FileContent, FileSystemEntryType, FileSystemPath,
10    json::parse_json_rope_with_source_context,
11};
12use turbopack_core::{
13    asset::{Asset, AssetContent},
14    file_source::FileSource,
15    raw_module::RawModule,
16    reference::ModuleReference,
17    resolve::{ModuleResolveResult, RequestKey, ResolveResultItem, pattern::Pattern, resolve_raw},
18    source::Source,
19    target::{CompileTarget, Platform},
20};
21
22#[derive(Serialize, Deserialize, Debug)]
23struct NodePreGypConfigJson {
24    binary: NodePreGypConfig,
25}
26
27#[derive(Serialize, Deserialize, Debug)]
28struct NodePreGypConfig {
29    module_name: String,
30    module_path: String,
31    napi_versions: Vec<u8>,
32}
33
34#[turbo_tasks::value]
35#[derive(Hash, Clone, Debug)]
36pub struct NodePreGypConfigReference {
37    pub context_dir: FileSystemPath,
38    pub config_file_pattern: ResolvedVc<Pattern>,
39    pub compile_target: ResolvedVc<CompileTarget>,
40}
41
42#[turbo_tasks::value_impl]
43impl NodePreGypConfigReference {
44    #[turbo_tasks::function]
45    pub fn new(
46        context_dir: FileSystemPath,
47        config_file_pattern: ResolvedVc<Pattern>,
48        compile_target: ResolvedVc<CompileTarget>,
49    ) -> Vc<Self> {
50        Self::cell(NodePreGypConfigReference {
51            context_dir,
52            config_file_pattern,
53            compile_target,
54        })
55    }
56}
57
58#[turbo_tasks::value_impl]
59impl ModuleReference for NodePreGypConfigReference {
60    #[turbo_tasks::function]
61    fn resolve_reference(&self) -> Vc<ModuleResolveResult> {
62        resolve_node_pre_gyp_files(
63            self.context_dir.clone(),
64            *self.config_file_pattern,
65            *self.compile_target,
66        )
67    }
68}
69
70#[turbo_tasks::value_impl]
71impl ValueToString for NodePreGypConfigReference {
72    #[turbo_tasks::function]
73    async fn to_string(&self) -> Result<Vc<RcStr>> {
74        let context_dir = self.context_dir.value_to_string().await?;
75        let config_file_pattern = self.config_file_pattern.to_string().await?;
76        let compile_target = self.compile_target.await?;
77        Ok(Vc::cell(
78            format!("node-gyp in {context_dir} with {config_file_pattern} for {compile_target}")
79                .into(),
80        ))
81    }
82}
83
84#[turbo_tasks::function]
85pub async fn resolve_node_pre_gyp_files(
86    context_dir: FileSystemPath,
87    config_file_pattern: Vc<Pattern>,
88    compile_target: Vc<CompileTarget>,
89) -> Result<Vc<ModuleResolveResult>> {
90    static NAPI_VERSION_TEMPLATE: LazyLock<Regex> = LazyLock::new(|| {
91        Regex::new(r"\{(napi_build_version|node_napi_label)\}")
92            .expect("create napi_build_version regex failed")
93    });
94    static PLATFORM_TEMPLATE: LazyLock<Regex> =
95        LazyLock::new(|| Regex::new(r"\{platform\}").expect("create node_platform regex failed"));
96    static ARCH_TEMPLATE: LazyLock<Regex> =
97        LazyLock::new(|| Regex::new(r"\{arch\}").expect("create node_arch regex failed"));
98    static LIBC_TEMPLATE: LazyLock<Regex> =
99        LazyLock::new(|| Regex::new(r"\{libc\}").expect("create node_libc regex failed"));
100    let config = resolve_raw(context_dir, config_file_pattern, true)
101        .first_source()
102        .await?;
103    let compile_target = compile_target.await?;
104    if let Some(config_asset) = *config
105        && let AssetContent::File(file) = &*config_asset.content().await?
106        && let FileContent::Content(config_file) = &*file.await?
107    {
108        let config_file_path = config_asset.ident().path().await?.clone_value();
109        let mut affecting_paths = vec![config_file_path.clone()];
110        let config_file_dir = config_file_path.parent();
111        let node_pre_gyp_config: NodePreGypConfigJson =
112            parse_json_rope_with_source_context(config_file.content())?;
113        let mut sources: FxIndexMap<RcStr, Vc<Box<dyn Source>>> = FxIndexMap::default();
114        for version in node_pre_gyp_config.binary.napi_versions.iter() {
115            let native_binding_path = NAPI_VERSION_TEMPLATE.replace(
116                node_pre_gyp_config.binary.module_path.as_str(),
117                format!("{version}"),
118            );
119            let platform = compile_target.platform;
120            let native_binding_path =
121                PLATFORM_TEMPLATE.replace(&native_binding_path, platform.as_str());
122            let native_binding_path =
123                ARCH_TEMPLATE.replace(&native_binding_path, compile_target.arch.as_str());
124            let native_binding_path: RcStr = LIBC_TEMPLATE
125                .replace(
126                    &native_binding_path,
127                    // node-pre-gyp only cares about libc on linux
128                    if platform == Platform::Linux {
129                        compile_target.libc.as_str()
130                    } else {
131                        "unknown"
132                    },
133                )
134                .into();
135
136            // Find all dynamic libraries in the given directory.
137            if let DirectoryContent::Entries(entries) = &*config_file_dir
138                .join(&native_binding_path)?
139                .read_dir()
140                .await?
141            {
142                let extension = compile_target.dylib_ext();
143                for (key, entry) in entries.iter().filter(|(k, _)| k.ends_with(extension)) {
144                    if let DirectoryEntry::File(dylib) | DirectoryEntry::Symlink(dylib) = entry {
145                        sources.insert(
146                            format!("{native_binding_path}/{key}").into(),
147                            Vc::upcast(FileSource::new(dylib.clone())),
148                        );
149                    }
150                }
151            }
152
153            let node_file_path: RcStr = format!(
154                "{}/{}.node",
155                native_binding_path, node_pre_gyp_config.binary.module_name
156            )
157            .into();
158            let resolved_file_vc = config_file_dir.join(&node_file_path)?;
159            if *resolved_file_vc.get_type().await? == FileSystemEntryType::File {
160                sources.insert(
161                    node_file_path,
162                    Vc::upcast(FileSource::new(resolved_file_vc)),
163                );
164            }
165        }
166        if let DirectoryContent::Entries(entries) = &*config_file_dir
167            // TODO
168            // read the dependencies path from `bindings.gyp`
169            .join("deps/lib")?
170            .read_dir()
171            .await?
172        {
173            for (key, entry) in entries.iter() {
174                match entry {
175                    DirectoryEntry::File(dylib) => {
176                        sources.insert(
177                            format!("deps/lib/{key}").into(),
178                            Vc::upcast(FileSource::new(dylib.clone())),
179                        );
180                    }
181                    DirectoryEntry::Symlink(dylib) => {
182                        let realpath_with_links = dylib.realpath_with_links().await?;
183                        for symlink in realpath_with_links.symlinks.iter() {
184                            affecting_paths.push(symlink.clone());
185                        }
186                        sources.insert(
187                            format!("deps/lib/{key}").into(),
188                            Vc::upcast(FileSource::new(realpath_with_links.path.clone())),
189                        );
190                    }
191                    _ => {}
192                }
193            }
194        }
195        return Ok(*ModuleResolveResult::modules_with_affecting_sources(
196            sources
197                .into_iter()
198                .map(|(key, source)| async move {
199                    Ok((
200                        RequestKey::new(key),
201                        ResolvedVc::upcast(RawModule::new(source).to_resolved().await?),
202                    ))
203                })
204                .try_join()
205                .await?,
206            affecting_paths
207                .into_iter()
208                .map(|p| async move {
209                    anyhow::Ok(ResolvedVc::upcast(FileSource::new(p).to_resolved().await?))
210                })
211                .try_join()
212                .await?,
213        ));
214    };
215    Ok(*ModuleResolveResult::unresolvable())
216}
217
218#[turbo_tasks::value]
219#[derive(Hash, Clone, Debug)]
220pub struct NodeGypBuildReference {
221    pub context_dir: FileSystemPath,
222    pub compile_target: ResolvedVc<CompileTarget>,
223}
224
225#[turbo_tasks::value_impl]
226impl NodeGypBuildReference {
227    #[turbo_tasks::function]
228    pub fn new(context_dir: FileSystemPath, compile_target: ResolvedVc<CompileTarget>) -> Vc<Self> {
229        Self::cell(NodeGypBuildReference {
230            context_dir,
231            compile_target,
232        })
233    }
234}
235
236#[turbo_tasks::value_impl]
237impl ModuleReference for NodeGypBuildReference {
238    #[turbo_tasks::function]
239    fn resolve_reference(&self) -> Vc<ModuleResolveResult> {
240        resolve_node_gyp_build_files(self.context_dir.clone(), *self.compile_target)
241    }
242}
243
244#[turbo_tasks::value_impl]
245impl ValueToString for NodeGypBuildReference {
246    #[turbo_tasks::function]
247    async fn to_string(&self) -> Result<Vc<RcStr>> {
248        let context_dir = self.context_dir.value_to_string().await?;
249        let compile_target = self.compile_target.await?;
250        Ok(Vc::cell(
251            format!("node-gyp in {context_dir} for {compile_target}").into(),
252        ))
253    }
254}
255
256#[turbo_tasks::function]
257pub async fn resolve_node_gyp_build_files(
258    context_dir: FileSystemPath,
259    compile_target: Vc<CompileTarget>,
260) -> Result<Vc<ModuleResolveResult>> {
261    // TODO Proper parser
262    static GYP_BUILD_TARGET_NAME: LazyLock<Regex> = LazyLock::new(|| {
263        Regex::new(r#"['"]target_name['"]\s*:\s*(?:"(.*?)"|'(.*?)')"#)
264            .expect("create napi_build_version regex failed")
265    });
266    let binding_gyp_pat = Pattern::new(Pattern::Constant(rcstr!("binding.gyp")));
267    let gyp_file = resolve_raw(context_dir.clone(), binding_gyp_pat, true);
268    if let [binding_gyp] = &gyp_file.primary_sources().await?[..] {
269        let mut merged_affecting_sources =
270            gyp_file.await?.get_affecting_sources().collect::<Vec<_>>();
271        if let AssetContent::File(file) = &*binding_gyp.content().await?
272            && let FileContent::Content(config_file) = &*file.await?
273            && let Some(captured) = GYP_BUILD_TARGET_NAME.captures(&config_file.content().to_str()?)
274        {
275            let mut resolved: FxIndexMap<RcStr, ResolvedVc<Box<dyn Source>>> =
276                FxIndexMap::with_capacity_and_hasher(captured.len(), Default::default());
277            for found in captured.iter().skip(1).flatten() {
278                let name = found.as_str();
279                let target_path = context_dir.join("build/Release")?;
280                let resolved_prebuilt_file = resolve_raw(
281                    target_path,
282                    Pattern::new(Pattern::Constant(format!("{name}.node").into())),
283                    true,
284                )
285                .await?;
286                if let Some((_, ResolveResultItem::Source(source))) =
287                    resolved_prebuilt_file.primary.first()
288                {
289                    resolved.insert(format!("build/Release/{name}.node").into(), *source);
290                    merged_affecting_sources
291                        .extend(resolved_prebuilt_file.affecting_sources.iter().copied());
292                }
293            }
294            if !resolved.is_empty() {
295                return Ok(*ModuleResolveResult::modules_with_affecting_sources(
296                    resolved
297                        .into_iter()
298                        .map(|(key, source)| async move {
299                            Ok((
300                                RequestKey::new(key),
301                                ResolvedVc::upcast(RawModule::new(*source).to_resolved().await?),
302                            ))
303                        })
304                        .try_join()
305                        .await?
306                        .into_iter(),
307                    merged_affecting_sources,
308                ));
309            }
310        }
311    }
312    let compile_target = compile_target.await?;
313    let arch = compile_target.arch;
314    let platform = compile_target.platform;
315    let prebuilt_dir = format!("{platform}-{arch}");
316    Ok(resolve_raw(
317        context_dir,
318        Pattern::new(Pattern::Concatenation(vec![
319            Pattern::Constant(format!("prebuilds/{prebuilt_dir}/").into()),
320            Pattern::Dynamic,
321            Pattern::Constant(rcstr!(".node")),
322        ])),
323        true,
324    )
325    .as_raw_module_result())
326}
327
328#[turbo_tasks::value]
329#[derive(Hash, Clone, Debug)]
330pub struct NodeBindingsReference {
331    pub context_dir: FileSystemPath,
332    pub file_name: RcStr,
333}
334
335#[turbo_tasks::value_impl]
336impl NodeBindingsReference {
337    #[turbo_tasks::function]
338    pub fn new(context_dir: FileSystemPath, file_name: RcStr) -> Vc<Self> {
339        Self::cell(NodeBindingsReference {
340            context_dir,
341            file_name,
342        })
343    }
344}
345
346#[turbo_tasks::value_impl]
347impl ModuleReference for NodeBindingsReference {
348    #[turbo_tasks::function]
349    fn resolve_reference(&self) -> Vc<ModuleResolveResult> {
350        resolve_node_bindings_files(self.context_dir.clone(), self.file_name.clone())
351    }
352}
353
354#[turbo_tasks::value_impl]
355impl ValueToString for NodeBindingsReference {
356    #[turbo_tasks::function]
357    async fn to_string(&self) -> Result<Vc<RcStr>> {
358        Ok(Vc::cell(
359            format!("bindings in {}", self.context_dir.value_to_string().await?,).into(),
360        ))
361    }
362}
363
364#[turbo_tasks::function]
365pub async fn resolve_node_bindings_files(
366    context_dir: FileSystemPath,
367    file_name: RcStr,
368) -> Result<Vc<ModuleResolveResult>> {
369    static BINDINGS_TRY: LazyLock<[&'static str; 5]> = LazyLock::new(|| {
370        [
371            "build/bindings",
372            "build/Release",
373            "build/Release/bindings",
374            "out/Release/bindings",
375            "Release/bindings",
376        ]
377    });
378    let mut root_context_dir = context_dir;
379    loop {
380        let resolved = resolve_raw(
381            root_context_dir.clone(),
382            Pattern::new(Pattern::Constant(rcstr!("package.json"))),
383            true,
384        )
385        .first_source()
386        .await?;
387        if let Some(asset) = *resolved
388            && let AssetContent::File(file) = &*asset.content().await?
389            && let FileContent::Content(_) = &*file.await?
390        {
391            break;
392        };
393        let current_context = root_context_dir.clone();
394        let parent = root_context_dir.parent();
395        if parent.path == current_context.path {
396            break;
397        }
398        root_context_dir = parent;
399    }
400
401    let try_path = |sub_path: RcStr| async move {
402        let path = root_context_dir.join(&sub_path)?;
403        Ok(
404            if matches!(*path.get_type().await?, FileSystemEntryType::File) {
405                Some((
406                    RequestKey::new(sub_path),
407                    ResolvedVc::upcast(
408                        RawModule::new(Vc::upcast(FileSource::new(path.clone())))
409                            .to_resolved()
410                            .await?,
411                    ),
412                ))
413            } else {
414                None
415            },
416        )
417    };
418
419    let modules = BINDINGS_TRY
420        .iter()
421        .map(|try_dir| try_path.clone()(format!("{}/{}", try_dir, &file_name).into()))
422        .try_flat_join()
423        .await?;
424    Ok(*ModuleResolveResult::modules(modules))
425}