Skip to content

Commit 1e0f9d4

Browse files
authored
Turbopack: fix NFT tracing of sharp 0.34 (#82340)
We need a special case for this shared library loading
1 parent 78459a2 commit 1e0f9d4

File tree

10 files changed

+694
-32
lines changed

10 files changed

+694
-32
lines changed

turbopack/crates/turbopack-core/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ pub mod introspect;
2525
pub mod issue;
2626
pub mod module;
2727
pub mod module_graph;
28+
pub mod node_addon_module;
2829
pub mod output;
2930
pub mod package_json;
3031
pub mod proxied_asset;
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
use std::sync::LazyLock;
2+
3+
use anyhow::Result;
4+
use regex::Regex;
5+
use turbo_rcstr::rcstr;
6+
use turbo_tasks::{FxIndexSet, ResolvedVc, TryJoinIterExt, Vc};
7+
use turbo_tasks_fs::{FileSystemEntryType, FileSystemPath};
8+
9+
use 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]
22+
pub struct NodeAddonModule {
23+
source: ResolvedVc<Box<dyn Source>>,
24+
}
25+
26+
#[turbo_tasks::value_impl]
27+
impl 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]
35+
impl 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]
101+
impl Asset for NodeAddonModule {
102+
#[turbo_tasks::function]
103+
fn content(&self) -> Vc<AssetContent> {
104+
self.source.content()
105+
}
106+
}
107+
108+
#[turbo_tasks::function]
109+
async 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+
}

turbopack/crates/turbopack/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ use turbopack_core::{
3939
ident::Layer,
4040
issue::{IssueExt, IssueSource, StyledString, module::ModuleIssue},
4141
module::Module,
42+
node_addon_module::NodeAddonModule,
4243
output::OutputAsset,
4344
raw_module::RawModule,
4445
reference_type::{
@@ -236,6 +237,9 @@ async fn apply_module_type(
236237
}
237238
ModuleType::Json => ResolvedVc::upcast(JsonModuleAsset::new(*source).to_resolved().await?),
238239
ModuleType::Raw => ResolvedVc::upcast(RawModule::new(*source).to_resolved().await?),
240+
ModuleType::NodeAddon => {
241+
ResolvedVc::upcast(NodeAddonModule::new(*source).to_resolved().await?)
242+
}
239243
ModuleType::CssModule => ResolvedVc::upcast(
240244
ModuleCssAsset::new(*source, Vc::upcast(module_asset_context))
241245
.to_resolved()

turbopack/crates/turbopack/src/module_options/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,7 @@ impl ModuleOptions {
414414
RuleCondition::any(vec![RuleCondition::ResourcePathEndsWith(
415415
".node".to_string(),
416416
)]),
417-
vec![ModuleRuleEffect::ModuleType(ModuleType::Raw)],
417+
vec![ModuleRuleEffect::ModuleType(ModuleType::NodeAddon)],
418418
),
419419
// WebAssembly
420420
ModuleRule::new(

turbopack/crates/turbopack/src/module_options/module_rule.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ pub enum ModuleType {
119119
},
120120
Json,
121121
Raw,
122+
NodeAddon,
122123
CssModule,
123124
Css {
124125
ty: CssModuleAssetType,

turbopack/crates/turbopack/tests/node-file-trace.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,18 @@ static ALLOC: turbo_tasks_malloc::TurboMalloc = turbo_tasks_malloc::TurboMalloc;
158158
#[case::sentry("integration/sentry.js")]
159159
#[case::sequelize("integration/sequelize.js")]
160160
#[case::serialport("integration/serialport.js")]
161+
#[cfg_attr(
162+
target_os = "windows",
163+
should_panic(expected = "Something went wrong installing the \"sharp\" module"),
164+
case::sharp030("integration/sharp030.js")
165+
)]
166+
#[cfg_attr(not(target_os = "windows"), case::sharp030("integration/sharp030.js"))]
167+
#[cfg_attr(
168+
target_os = "windows",
169+
should_panic(expected = "Something went wrong installing the \"sharp\" module"),
170+
case::sharp033("integration/sharp033.js")
171+
)]
172+
#[cfg_attr(not(target_os = "windows"), case::sharp033("integration/sharp033.js"))]
161173
#[cfg_attr(
162174
target_os = "windows",
163175
should_panic(expected = "Something went wrong installing the \"sharp\" module"),
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
const sharp = require('sharp030')
2+
const path = require('path')
3+
4+
const roundedCorners = Buffer.from(
5+
'<svg><rect x="0" y="0" width="200" height="200" rx="50" ry="50"/></svg>'
6+
)
7+
8+
sharp(roundedCorners).resize(200, 200).png().toBuffer()
9+
10+
sharp(path.resolve(__dirname, 'fixtures/vercel.svg'))
11+
.resize({ width: 100, height: 100 })
12+
.jpeg()
13+
.toBuffer()
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
const sharp = require('sharp033')
2+
const path = require('path')
3+
4+
const roundedCorners = Buffer.from(
5+
'<svg><rect x="0" y="0" width="200" height="200" rx="50" ry="50"/></svg>'
6+
)
7+
8+
sharp(roundedCorners).resize(200, 200).png().toBuffer()
9+
10+
sharp(path.resolve(__dirname, 'fixtures/vercel.svg'))
11+
.resize({ width: 100, height: 100 })
12+
.jpeg()
13+
.toBuffer()

turbopack/crates/turbopack/tests/node-file-trace/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,9 @@
101101
"semver": "^7.1.1",
102102
"sequelize": "^5.9.3",
103103
"serialport": "^13.0.0",
104-
"sharp": "^0.30.0",
104+
"sharp030": "npm:sharp@^0.30.0",
105+
"sharp033": "npm:sharp@^0.33.0",
106+
"sharp": "^0.34.0",
105107
"shiki": "^0.14.5",
106108
"socket.io": "^2.4.0",
107109
"socket.io-client": "^2.2.0",

0 commit comments

Comments
 (0)