Skip to main content

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