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