turbopack_resolve/
node_native_binding.rs

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