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