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