turbopack_core/
node_addon_module.rs

1use std::sync::LazyLock;
2
3use anyhow::{Result, bail};
4use regex::Regex;
5use turbo_rcstr::rcstr;
6use turbo_tasks::{FxIndexSet, ResolvedVc, TryJoinIterExt, Vc};
7use turbo_tasks_fs::{FileSystemEntryType, FileSystemPath};
8
9use crate::{
10    file_source::FileSource,
11    ident::AssetIdent,
12    module::{Module, ModuleSideEffects},
13    raw_module::RawModule,
14    reference::{ModuleReferences, TracedModuleReference},
15    resolve::pattern::{Pattern, PatternMatch, read_matches},
16    source::{OptionSource, Source},
17};
18
19/// A module corresponding to `.node` files.
20#[turbo_tasks::value]
21pub struct NodeAddonModule {
22    source: ResolvedVc<Box<dyn Source>>,
23}
24
25#[turbo_tasks::value_impl]
26impl NodeAddonModule {
27    #[turbo_tasks::function]
28    pub fn new(source: ResolvedVc<Box<dyn Source>>) -> Vc<NodeAddonModule> {
29        NodeAddonModule { source }.cell()
30    }
31}
32
33#[turbo_tasks::value_impl]
34impl Module for NodeAddonModule {
35    #[turbo_tasks::function]
36    fn ident(&self) -> Vc<AssetIdent> {
37        self.source.ident().with_modifier(rcstr!("node addon"))
38    }
39
40    #[turbo_tasks::function]
41    fn source(&self) -> Vc<OptionSource> {
42        Vc::cell(Some(self.source))
43    }
44
45    #[turbo_tasks::function]
46    async fn references(&self) -> Result<Vc<ModuleReferences>> {
47        static SHARP_BINARY_REGEX: LazyLock<Regex> =
48            LazyLock::new(|| Regex::new("/sharp-(\\w+-\\w+).node$").unwrap());
49        let module_path = self.source.ident().path().await?;
50
51        // For most .node binaries, we usually assume that they are standalone dynamic library
52        // binaries that get loaded by some `require` call. So the binary itself doesn't read any
53        // files by itself, but only when instructed to from the JS side.
54        //
55        // For sharp, that is not the case:
56        // 1. `node_modules/sharp/lib/sharp.js` does `require("@img/sharp-${arch}/sharp.node")`
57        //    which ends up resolving to ...
58        // 2. @img/sharp-darwin-arm64/lib/sharp-darwin-arm64.node. That is however a dynamic library
59        //    that uses the OS loader to load yet another binary (you can view these via `otool -L`
60        //    on macOS or `ldd` on Linux):
61        // 3. @img/sharp-libvips-darwin-arm64/libvips.dylib
62        //
63        // We could either try to parse the binary and read these dependencies, or (as we do in the
64        // following) special case sharp and hardcode this dependency.
65        //
66        // The JS @vercel/nft implementation has a similar special case:
67        // https://github.com/vercel/nft/blob/7e915aa02073ec57dc0d6528c419a4baa0f03d40/src/utils/special-cases.ts#L151-L181
68        if SHARP_BINARY_REGEX.is_match(&module_path.path) {
69            // module_path might be something like
70            // node_modules/@img/sharp-darwin-arm64/lib/sharp-darwin-arm64.node
71            let arch = SHARP_BINARY_REGEX
72                .captures(&module_path.path)
73                .unwrap()
74                .get(1)
75                .unwrap()
76                .as_str();
77
78            let package_name = format!("@img/sharp-libvips-{arch}");
79            for folder in [
80                // This is the list of rpaths (lookup paths) of the shared library, at least on
81                // macOS and Linux https://github.com/lovell/sharp/blob/c01e272db522a8b7d174bd3be7400a4a87f08702/src/binding.gyp#L158-L201
82                "../..",
83                "../../..",
84                "../../node_modules",
85                "../../../node_modules",
86            ]
87            .iter()
88            .filter_map(|p| module_path.parent().join(p).ok()?.join(&package_name).ok())
89            {
90                if matches!(
91                    &*folder.get_type().await?,
92                    FileSystemEntryType::Directory | FileSystemEntryType::Symlink
93                ) {
94                    return Ok(dir_references(folder));
95                }
96            }
97        };
98
99        // Most addon modules don't have references to other modules.
100        Ok(ModuleReferences::empty())
101    }
102
103    #[turbo_tasks::function]
104    fn side_effects(self: Vc<Self>) -> Vc<ModuleSideEffects> {
105        // We assume that a node addon could have arbitrary side effects when loading.
106        ModuleSideEffects::SideEffectful.cell()
107    }
108}
109
110#[turbo_tasks::function]
111async fn dir_references(package_dir: FileSystemPath) -> Result<Vc<ModuleReferences>> {
112    let matches = read_matches(
113        package_dir.clone(),
114        rcstr!(""),
115        true,
116        Pattern::new(Pattern::Dynamic),
117    )
118    .await?;
119
120    let mut results: FxIndexSet<FileSystemPath> = FxIndexSet::default();
121    for pat_match in matches.into_iter() {
122        match pat_match {
123            PatternMatch::File(_, file) => {
124                let realpath = file.realpath_with_links().await?;
125                results.extend(realpath.symlinks.iter().cloned());
126                match &realpath.path_result {
127                    Ok(path) => {
128                        results.insert(path.clone());
129                    }
130                    Err(e) => bail!(e.as_error_message(file, &realpath)),
131                }
132            }
133            PatternMatch::Directory(..) => {}
134        }
135    }
136
137    Ok(Vc::cell(
138        results
139            .into_iter()
140            .map(async |p| {
141                Ok(ResolvedVc::upcast(
142                    TracedModuleReference::new(Vc::upcast(RawModule::new(Vc::upcast(
143                        FileSource::new(p),
144                    ))))
145                    .to_resolved()
146                    .await?,
147                ))
148            })
149            .try_join()
150            .await?,
151    ))
152}