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