use std::collections::HashMap;
use std::collections::HashSet;
use std::collections::VecDeque;
use std::ffi::OsStr;
use std::path::Path;
use std::path::PathBuf;
use deno_core::error::AnyError;
use deno_npm::NpmPackageId;
use deno_npm::NpmSystemInfo;
use crate::npm::CliNpmResolver;
#[derive(Debug, Clone)]
#[allow(
dead_code,
reason = "fields are kept for future precise-embedding work; v1 only uses the existence of any entry"
)]
pub struct NativeAddonPackage {
pub name: String,
pub folder: PathBuf,
pub id: Option<NpmPackageId>,
}
pub fn find_native_addon_packages(
npm_resolver: &CliNpmResolver,
npm_system_info: &NpmSystemInfo,
workspace_root: Option<&Path>,
) -> Result<Vec<NativeAddonPackage>, AnyError> {
match npm_resolver {
CliNpmResolver::Managed(managed) => {
let snapshot = managed
.resolution()
.snapshot()
.as_valid_serialized_for_system(npm_system_info);
let mut packages = Vec::new();
for pkg in snapshot.as_serialized().packages.iter() {
let folder = match managed.resolve_pkg_folder_from_pkg_id(&pkg.id) {
Ok(folder) => folder,
Err(
deno_resolver::npm::managed::ResolvePkgFolderFromPkgIdError::NotFound(_),
) => continue,
Err(err) => return Err(err.into()),
};
if folder.exists() && folder_contains_node_addon(&folder) {
packages.push(NativeAddonPackage {
name: pkg.id.nv.name.to_string(),
folder,
id: Some(pkg.id.clone()),
});
}
}
Ok(packages)
}
CliNpmResolver::Byonm(_) => {
let Some(workspace_root) = workspace_root else {
return Ok(Vec::new());
};
Ok(find_byonm_native_addon_packages(workspace_root))
}
}
}
pub fn collect_bundle_required_packages(
npm_resolver: &CliNpmResolver,
npm_system_info: &NpmSystemInfo,
referenced_paths: &[PathBuf],
) -> Result<Option<HashSet<NpmPackageId>>, AnyError> {
let CliNpmResolver::Managed(managed) = npm_resolver else {
return Ok(None);
};
let snapshot = managed.resolution().snapshot();
let native_packages =
find_native_addon_packages(npm_resolver, npm_system_info, None)?;
let mut folders: Vec<(NpmPackageId, PathBuf)> = Vec::new();
for pkg in snapshot.all_packages_for_every_system() {
if let Ok(folder) = managed.resolve_pkg_folder_from_pkg_id(&pkg.id)
&& folder.exists()
{
folders.push((pkg.id.clone(), folder));
}
}
let mut seeds: HashSet<NpmPackageId> =
native_packages.into_iter().filter_map(|p| p.id).collect();
let native_seeds: HashSet<NpmPackageId> = seeds.iter().cloned().collect();
for pkg in snapshot.all_packages_for_every_system() {
if pkg
.dependencies
.values()
.any(|dep_id| native_seeds.contains(dep_id))
{
seeds.insert(pkg.id.clone());
}
}
let mut path_owner_cache: HashMap<&PathBuf, Option<NpmPackageId>> =
HashMap::new();
for path in referenced_paths {
if path_owner_cache.contains_key(path) {
continue;
}
let mut best_match: Option<(&NpmPackageId, usize)> = None;
for (id, folder) in &folders {
let folder_str = folder.as_os_str();
if path.starts_with(folder) {
let folder_len = folder_str.len();
if best_match.map(|(_, len)| folder_len > len).unwrap_or(true) {
best_match = Some((id, folder_len));
}
}
}
let owner = best_match.map(|(id, _)| id.clone());
if let Some(ref id) = owner {
seeds.insert(id.clone());
}
path_owner_cache.insert(path, owner);
}
let mut closure: HashSet<NpmPackageId> = HashSet::new();
let mut stack: Vec<NpmPackageId> = seeds.into_iter().collect();
while let Some(id) = stack.pop() {
if !closure.insert(id.clone()) {
continue;
}
if let Some(pkg) = snapshot.package_from_id(&id) {
for dep_id in pkg.dependencies.values() {
if !closure.contains(dep_id) {
stack.push(dep_id.clone());
}
}
}
}
Ok(Some(closure))
}
fn find_byonm_native_addon_packages(
workspace_root: &Path,
) -> Vec<NativeAddonPackage> {
let mut node_modules_dirs = VecDeque::new();
let mut pending = VecDeque::from([workspace_root.to_path_buf()]);
while let Some(dir) = pending.pop_front() {
let Ok(entries) = std::fs::read_dir(&dir) else {
continue;
};
for entry in entries.flatten() {
let path = entry.path();
if !path.is_dir() {
continue;
}
if path.file_name() == Some(OsStr::new("node_modules")) {
node_modules_dirs.push_back(path);
} else {
pending.push_back(path);
}
}
}
let mut packages = Vec::new();
while let Some(node_modules) = node_modules_dirs.pop_front() {
let Ok(entries) = std::fs::read_dir(&node_modules) else {
continue;
};
for entry in entries.flatten() {
let path = entry.path();
if !path.is_dir() {
continue;
}
let nested = path.join("node_modules");
if nested.is_dir() {
node_modules_dirs.push_back(nested);
}
if folder_contains_node_addon(&path) {
packages.push(NativeAddonPackage {
name: path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_default(),
folder: path,
id: None,
});
}
}
}
packages
}
fn folder_contains_node_addon(folder: &Path) -> bool {
let mut pending = VecDeque::from([folder.to_path_buf()]);
while let Some(dir) = pending.pop_front() {
let Ok(entries) = std::fs::read_dir(&dir) else {
continue;
};
for entry in entries.flatten() {
let path = entry.path();
let file_name = path.file_name();
if path.is_dir() {
if file_name == Some(OsStr::new("node_modules")) {
continue;
}
pending.push_back(path);
} else if path.extension() == Some(OsStr::new("node")) {
return true;
}
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
fn touch(path: &Path) {
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(path, "").unwrap();
}
#[test]
fn byonm_detects_addon_in_workspace_node_modules() {
let tmp = tempfile::TempDir::new().unwrap();
let root = tmp.path();
let pkg = root.join("node_modules").join("pkg");
touch(&pkg.join("index.js"));
assert!(find_byonm_native_addon_packages(root).is_empty());
touch(&pkg.join("build").join("Release").join("addon.node"));
let found = find_byonm_native_addon_packages(root);
assert_eq!(found.len(), 1);
assert_eq!(found[0].folder, pkg);
assert!(found[0].id.is_none());
}
#[test]
fn byonm_detects_transitive_addon() {
let tmp = tempfile::TempDir::new().unwrap();
let root = tmp.path();
let nested = root
.join("node_modules")
.join("parent")
.join("node_modules")
.join("dep");
touch(&nested.join("addon.node"));
let found = find_byonm_native_addon_packages(root);
assert_eq!(found.len(), 1);
assert_eq!(found[0].folder, nested);
}
}