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