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(e) => {
195 bail!(e.as_error_message(dylib, &realpath_with_links).await?)
196 }
197 })),
198 );
199 }
200 _ => {}
201 }
202 }
203 }
204 return Ok(*ModuleResolveResult::modules_with_affecting_sources(
205 sources
206 .into_iter()
207 .map(|(key, source)| async move {
208 Ok((
209 RequestKey::new(key),
210 ResolvedVc::upcast(RawModule::new(source).to_resolved().await?),
211 ))
212 })
213 .try_join()
214 .await?,
215 affecting_paths
216 .into_iter()
217 .map(|p| async move {
218 anyhow::Ok(ResolvedVc::upcast(FileSource::new(p).to_resolved().await?))
219 })
220 .try_join()
221 .await?,
222 ));
223 };
224 Ok(*ModuleResolveResult::unresolvable())
225}
226
227#[turbo_tasks::value]
228#[derive(Hash, Clone, Debug, ValueToString)]
229#[value_to_string("node-gyp in {context_dir} for {compile_target}")]
230pub struct NodeGypBuildReference {
231 pub context_dir: FileSystemPath,
232 collect_affecting_sources: bool,
233 pub compile_target: ResolvedVc<CompileTarget>,
234}
235
236#[turbo_tasks::value_impl]
237impl NodeGypBuildReference {
238 #[turbo_tasks::function]
239 pub fn new(
240 context_dir: FileSystemPath,
241 collect_affecting_sources: bool,
242 compile_target: ResolvedVc<CompileTarget>,
243 ) -> Vc<Self> {
244 Self::cell(NodeGypBuildReference {
245 context_dir,
246 collect_affecting_sources,
247 compile_target,
248 })
249 }
250}
251
252#[turbo_tasks::value_impl]
253impl ModuleReference for NodeGypBuildReference {
254 #[turbo_tasks::function]
255 async fn resolve_reference(&self) -> Result<Vc<ModuleResolveResult>> {
256 resolve_node_gyp_build_files(
257 self.context_dir.clone(),
258 self.collect_affecting_sources,
259 self.compile_target,
260 )
261 .await
262 }
263
264 fn chunking_type(&self) -> Option<ChunkingType> {
265 Some(ChunkingType::Traced {
266 mode: TracedMode::Transitive,
267 })
268 }
269}
270
271async fn resolve_node_gyp_build_files(
272 context_dir: FileSystemPath,
273 collect_affecting_sources: bool,
274 compile_target: ResolvedVc<CompileTarget>,
275) -> Result<Vc<ModuleResolveResult>> {
276 static GYP_BUILD_TARGET_NAME: LazyLock<Regex> = LazyLock::new(|| {
278 Regex::new(r#"['"]target_name['"]\s*:\s*(?:"(.*?)"|'(.*?)')"#)
279 .expect("create napi_build_version regex failed")
280 });
281 let binding_gyp_pat = Pattern::new(Pattern::Constant(rcstr!("binding.gyp")));
282 let gyp_file = resolve_raw(
283 context_dir.clone(),
284 binding_gyp_pat,
285 collect_affecting_sources,
286 true,
287 );
288 let gyp_file = gyp_file.await?;
289 let mut primary_sources = gyp_file.primary_sources();
290 if let (Some(binding_gyp), None) = (primary_sources.next(), primary_sources.next()) {
291 let mut merged_affecting_sources = if collect_affecting_sources {
292 gyp_file.get_affecting_sources().collect::<Vec<_>>()
293 } else {
294 Vec::new()
295 };
296 if let AssetContent::File(file) = &*binding_gyp.content().await?
297 && let FileContent::Content(config_file) = &*file.await?
298 && let Some(captured) = GYP_BUILD_TARGET_NAME.captures(&config_file.content().to_str()?)
299 {
300 let mut resolved: FxIndexMap<RcStr, ResolvedVc<Box<dyn Source>>> =
301 FxIndexMap::with_capacity_and_hasher(captured.len(), Default::default());
302 for found in captured.iter().skip(1).flatten() {
303 let name = found.as_str();
304 let target_path = context_dir.join("build/Release")?;
305 let resolved_prebuilt_file = resolve_raw(
306 target_path,
307 Pattern::new(Pattern::Constant(format!("{name}.node").into())),
308 collect_affecting_sources,
309 true,
310 )
311 .await?;
312 if let Some((_, ResolveResultItem::Source(source))) =
313 resolved_prebuilt_file.primary.first()
314 {
315 resolved.insert(format!("build/Release/{name}.node").into(), *source);
316 if collect_affecting_sources {
317 merged_affecting_sources
318 .extend(resolved_prebuilt_file.affecting_sources.iter().copied());
319 }
320 }
321 }
322 if !resolved.is_empty() {
323 return Ok(*ModuleResolveResult::modules_with_affecting_sources(
324 resolved
325 .into_iter()
326 .map(|(key, source)| async move {
327 Ok((
328 RequestKey::new(key),
329 ResolvedVc::upcast(RawModule::new(*source).to_resolved().await?),
330 ))
331 })
332 .try_join()
333 .await?,
334 merged_affecting_sources,
335 ));
336 }
337 }
338 }
339 let compile_target = compile_target.await?;
340 let arch = compile_target.arch;
341 let platform = compile_target.platform;
342 let prebuilt_dir = format!("{platform}-{arch}");
343 Ok(resolve_raw(
344 context_dir,
345 Pattern::new(Pattern::Concatenation(vec![
346 Pattern::Constant(format!("prebuilds/{prebuilt_dir}/").into()),
347 Pattern::Dynamic,
348 Pattern::Constant(rcstr!(".node")),
349 ])),
350 collect_affecting_sources,
351 true,
352 )
353 .as_raw_module_result())
354}
355
356#[turbo_tasks::value]
357#[derive(Hash, Clone, Debug, ValueToString)]
358#[value_to_string("bindings in {context_dir}")]
359pub struct NodeBindingsReference {
360 pub context_dir: FileSystemPath,
361 pub file_name: RcStr,
362 pub collect_affecting_sources: bool,
363}
364
365#[turbo_tasks::value_impl]
366impl NodeBindingsReference {
367 #[turbo_tasks::function]
368 pub fn new(
369 context_dir: FileSystemPath,
370 file_name: RcStr,
371 collect_affecting_sources: bool,
372 ) -> Vc<Self> {
373 Self::cell(NodeBindingsReference {
374 context_dir,
375 file_name,
376 collect_affecting_sources,
377 })
378 }
379}
380
381#[turbo_tasks::value_impl]
382impl ModuleReference for NodeBindingsReference {
383 #[turbo_tasks::function]
384 async fn resolve_reference(&self) -> Result<Vc<ModuleResolveResult>> {
385 resolve_node_bindings_files(
386 self.context_dir.clone(),
387 self.file_name.clone(),
388 self.collect_affecting_sources,
389 )
390 .await
391 }
392
393 fn chunking_type(&self) -> Option<ChunkingType> {
394 Some(ChunkingType::Traced {
395 mode: TracedMode::Transitive,
396 })
397 }
398}
399
400async fn resolve_node_bindings_files(
401 context_dir: FileSystemPath,
402 file_name: RcStr,
403 collect_affecting_sources: bool,
404) -> Result<Vc<ModuleResolveResult>> {
405 static BINDINGS_TRY: LazyLock<[&'static str; 5]> = LazyLock::new(|| {
406 [
407 "build/bindings",
408 "build/Release",
409 "build/Release/bindings",
410 "out/Release/bindings",
411 "Release/bindings",
412 ]
413 });
414 let mut root_context_dir = context_dir;
415 loop {
416 let resolved = resolve_raw(
417 root_context_dir.clone(),
418 Pattern::new(Pattern::Constant(rcstr!("package.json"))),
419 collect_affecting_sources,
420 true,
421 )
422 .await?;
423 if let Some(asset) = resolved.first_source()
424 && let AssetContent::File(file) = &*asset.content().await?
425 && let FileContent::Content(_) = &*file.await?
426 {
427 break;
428 };
429 let current_context = root_context_dir.clone();
430 let parent = root_context_dir.parent();
431 if parent.path == current_context.path {
432 break;
433 }
434 root_context_dir = parent;
435 }
436
437 let try_path = |sub_path: RcStr| async move {
438 let path = root_context_dir.join(&sub_path)?;
439 Ok(
440 if matches!(*path.get_type().await?, FileSystemEntryType::File) {
441 Some((
442 RequestKey::new(sub_path),
443 ResolvedVc::upcast(
444 RawModule::new(Vc::upcast(FileSource::new(path.clone())))
445 .to_resolved()
446 .await?,
447 ),
448 ))
449 } else {
450 None
451 },
452 )
453 };
454
455 let modules = BINDINGS_TRY
456 .iter()
457 .map(|try_dir| try_path.clone()(format!("{}/{}", try_dir, file_name).into()))
458 .try_flat_join()
459 .await?;
460 Ok(*ModuleResolveResult::modules(modules))
461}