turbopack_core/
node_addon_module.rs

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