use anyhow::{Context as _, Result};
use std::collections::{BTreeMap, HashMap, HashSet};
use std::path::Path;
use std::str::FromStr;
use crate::core::ResourceType;
use crate::lockfile::lockfile_dependency_ref::LockfileDependencyRef;
use crate::lockfile::{LockedResource, ResourceId};
use super::extractors::{DependencyExtractor, create_dependency_ref_string};
use crate::templating::cache::RenderCacheKey;
use crate::templating::content::{
NON_TEMPLATED_LITERAL_GUARD_START, content_contains_template_syntax,
};
use crate::templating::context::DependencyData;
use crate::templating::renderer::TemplateRenderer;
use crate::templating::utils::to_native_path_display;
pub(crate) async fn build_dependencies_data<T: DependencyExtractor>(
extractor: &T,
current_resource: &LockedResource,
rendering_stack: &mut HashSet<String>,
) -> Result<BTreeMap<String, BTreeMap<String, DependencyData>>> {
let mut deps = BTreeMap::new();
let dependency_specs =
extractor.extract_dependency_specs(current_resource).await.with_context(|| {
format!(
"Failed to extract dependency specifications from resource '{}' (type: {:?})",
current_resource.name, current_resource.resource_type
)
})?;
let get_key_names =
|resource: &LockedResource, dep_type: &ResourceType| -> (String, String, String, String) {
let type_str_plural = dep_type.to_plural().to_string();
let type_str_singular = dep_type.to_string();
let key_name = if resource.name.contains('/') || resource.name.contains('\\') {
std::path::Path::new(&resource.name)
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or(&resource.name)
.to_string()
} else {
resource.name.clone()
};
let sanitized_key = key_name.replace('-', "_");
(type_str_plural, type_str_singular, key_name, sanitized_key)
};
let mut resources_to_process: Vec<(&LockedResource, ResourceType, bool)> = Vec::new();
let mut visited_dep_ids = HashSet::new();
for dep_ref in current_resource.parsed_dependencies() {
let dep_id = dep_ref.to_string();
if !visited_dep_ids.insert(dep_id.clone()) {
continue;
}
let resource_type = dep_ref.resource_type;
let name = &dep_ref.path;
let dep_spec = {
let normalized_path = {
let path = std::path::Path::new(&dep_ref.path);
let normalized = crate::utils::normalize_path(path);
normalized.to_string_lossy().to_string()
};
let normalized_dep_ref = LockfileDependencyRef::new(
dep_ref.source.clone(),
dep_ref.resource_type,
normalized_path,
dep_ref.version.clone(),
);
let normalized_dep_id = normalized_dep_ref.to_string();
dependency_specs.get(&normalized_dep_id)
};
tracing::debug!(
"Looking up dep_spec for dep_id='{}', found={}, available_keys={:?}",
dep_id,
dep_spec.is_some(),
dependency_specs.keys().collect::<Vec<_>>()
);
let dep_tool =
dep_spec.and_then(|spec| spec.tool.as_ref()).or(current_resource.tool.as_ref());
let dep_source = dep_ref.source.as_ref().or(current_resource.source.as_ref());
let dep_resource_id_with_parent_hash = ResourceId::new(
name.clone(),
dep_source.cloned(),
dep_tool.cloned(),
resource_type,
current_resource.variant_inputs.hash().to_string(),
);
tracing::debug!(
"[DEBUG] Template context looking up: name='{}', type={:?}, source={:?}, tool={:?}, hash={}",
name,
resource_type,
dep_source,
dep_tool,
¤t_resource.variant_inputs.hash().to_string()[..8]
);
let mut dep_resource =
extractor.lockfile().find_resource_by_id(&dep_resource_id_with_parent_hash);
if dep_resource.is_none() {
let dep_resource_id_empty_hash = ResourceId::new(
name.clone(),
dep_source.cloned(),
dep_tool.cloned(),
resource_type,
crate::resolver::lockfile_builder::VariantInputs::default().hash().to_string(),
);
dep_resource = extractor.lockfile().find_resource_by_id(&dep_resource_id_empty_hash);
if dep_resource.is_some() {
tracing::debug!(
" [DIRECT MANIFEST DEP] Found dependency '{}' with empty variant_hash (direct manifest dependency)",
name
);
}
}
if let Some(dep_resource) = dep_resource {
resources_to_process.push((dep_resource, resource_type, true));
tracing::debug!(
" [DIRECT DEP] Found dependency '{}' (tool: {:?}) for '{}'",
name,
dep_tool,
current_resource.name
);
} else {
tracing::warn!(
"Dependency '{}' (type: {:?}, tool: {:?}) not found in lockfile for resource '{}'",
name,
resource_type,
dep_tool,
current_resource.name
);
}
}
tracing::debug!(
"Building dependencies data with {} direct dependencies for '{}'",
resources_to_process.len(),
current_resource.name
);
resources_to_process.sort_by(|a, b| {
use std::cmp::Ordering;
match a.1.cmp(&b.1) {
Ordering::Equal => {
match a.0.name.cmp(&b.0.name) {
Ordering::Equal => {
b.2.cmp(&a.2) }
other => other,
}
}
other => other,
}
});
for (resource, dep_type, is_dep) in &resources_to_process {
tracing::debug!(
" [LOCKFILE] Resource: {} (type: {:?}, install: {:?}, is_dependency: {})",
resource.name,
dep_type,
resource.install,
is_dep
);
}
let current_resource_id = create_dependency_ref_string(
current_resource.source.clone(),
current_resource.resource_type,
current_resource.name.clone(),
current_resource.version.clone(),
);
for (resource, dep_type, is_dependency) in &resources_to_process {
let resource_id = create_dependency_ref_string(
resource.source.clone(),
*dep_type,
resource.name.clone(),
resource.version.clone(),
);
if resource_id == current_resource_id {
tracing::debug!(
" Skipping current resource: {} (preventing self-reference)",
resource.name
);
continue;
}
tracing::debug!(" Processing resource: {} ({})", resource.name, dep_type);
let (type_str_plural, type_str_singular, _key_name, sanitized_key) =
get_key_names(resource, dep_type);
let raw_content = extractor.extract_content(resource).await;
let should_render = if *is_dependency {
if let Some(content) = &raw_content {
if content.contains(NON_TEMPLATED_LITERAL_GUARD_START) {
false
} else {
content_contains_template_syntax(content)
}
} else {
false
}
} else {
false
};
let final_content: String = if should_render {
let cache_key = RenderCacheKey::new(
resource.path.clone(),
*dep_type,
resource.tool.clone(),
resource.variant_inputs.hash().to_string(),
resource.resolved_commit.clone(),
);
let cache_result = extractor
.render_cache()
.lock()
.map_err(|e| {
anyhow::anyhow!(
"Render cache lock poisoned for resource '{}': {}. \
This indicates a panic occurred while holding the lock.",
resource.name,
e
)
})?
.get(&cache_key)
.cloned();
if let Some(cached_content) = cache_result {
tracing::debug!("Render cache hit for '{}' ({})", resource.name, dep_type);
cached_content
} else {
tracing::debug!(
"Render cache miss for '{}' ({}), rendering...",
resource.name,
dep_type
);
let dep_id = create_dependency_ref_string(
resource.source.clone(),
*dep_type,
resource.name.clone(),
resource.version.clone(),
);
if rendering_stack.contains(&dep_id) {
let chain: Vec<String> = rendering_stack.iter().cloned().collect();
anyhow::bail!(
"Circular dependency detected while rendering '{}'. \
Dependency chain: {} -> {}",
resource.name,
chain.join(" -> "),
dep_id
);
}
rendering_stack.insert(dep_id.clone());
let dep_resource_id = ResourceId::from_resource(resource);
let render_result = Box::pin(extractor.build_context_with_visited(
&dep_resource_id,
resource.variant_inputs.json(),
rendering_stack,
))
.await;
rendering_stack.remove(&dep_id);
match render_result {
Ok(dep_context) => {
if let Some(content) = raw_content {
let mut renderer = TemplateRenderer::new(
true,
extractor.project_dir().clone(),
None,
)
.with_context(|| {
format!(
"Failed to create template renderer for dependency '{}' (type: {:?})",
resource.name,
dep_type
)
})?;
let metadata = crate::templating::renderer::RenderingMetadata {
resource_name: resource.name.clone(),
resource_type: *dep_type,
dependency_chain: vec![], source_path: None,
depth: rendering_stack.len(),
};
let rendered = renderer
.render_template(&content, &dep_context, Some(&metadata))
.with_context(|| {
format!(
"Failed to render dependency '{}' (type: {:?}). \
This is a HARD FAILURE - dependency content MUST render successfully.\n\
Resource: {} (source: {}, path: {})",
resource.name,
dep_type,
resource.name,
resource.source.as_deref().unwrap_or("local"),
resource.path
)
})?;
tracing::debug!(
"Successfully rendered dependency content for '{}'",
resource.name
);
if let Ok(mut cache) = extractor.render_cache().lock() {
cache.insert(cache_key.clone(), rendered.clone());
tracing::debug!(
"Stored rendered content in cache for '{}'",
resource.name
);
}
rendered
} else {
String::new()
}
}
Err(e) => {
return Err(e.context(format!(
"Failed to build template context for dependency '{}' (type: {:?}). \
This is a HARD FAILURE - all dependencies must have valid contexts.\n\
Resource: {} (source: {}, path: {})",
resource.name,
dep_type,
resource.name,
resource.source.as_deref().unwrap_or("local"),
resource.path
)));
}
}
}
} else {
raw_content.unwrap_or_default()
};
let dependency_data = DependencyData {
resource_type: type_str_singular,
name: resource.name.clone(),
install_path: to_native_path_display(&resource.installed_at),
source: resource.source.clone(),
version: resource.version.clone(),
resolved_commit: resource.resolved_commit.clone(),
checksum: resource.checksum.clone(),
path: resource.path.clone(),
content: final_content,
};
let type_deps: &mut BTreeMap<String, DependencyData> =
deps.entry(type_str_plural.clone()).or_insert_with(BTreeMap::new);
type_deps.insert(sanitized_key.clone(), dependency_data);
tracing::debug!(
" Added resource: {}[{}] -> {}",
type_str_plural,
sanitized_key,
resource.path
);
}
tracing::debug!(
"Extracting custom dependency names for direct deps of: '{}'",
current_resource.name
);
let current_custom_names =
extractor.extract_dependency_custom_names(current_resource).await.with_context(|| {
format!(
"Failed to extract custom dependency names from resource '{}' (type: {:?})",
current_resource.name, current_resource.resource_type
)
})?;
tracing::debug!(
"Extracted {} custom names from current resource '{}' (type: {:?})",
current_custom_names.len(),
current_resource.name,
current_resource.resource_type
);
if !current_custom_names.is_empty() || current_resource.name.contains("golang") {
tracing::info!(
"Extracted {} custom names from current resource '{}' (type: {:?})",
current_custom_names.len(),
current_resource.name,
current_resource.resource_type
);
for (dep_ref, custom_name) in ¤t_custom_names {
tracing::info!(" Will add alias: '{}' -> '{}'", dep_ref, custom_name);
}
}
for (dep_ref, custom_name) in current_custom_names {
add_custom_alias(&mut deps, &dep_ref, &custom_name);
}
tracing::debug!(
"Built dependencies data with {} resource types for '{}'",
deps.len(),
current_resource.name
);
for (resource_type, resources) in &deps {
tracing::debug!(" Type {}: {} resources", resource_type, resources.len());
if resource_type == "snippets" {
for (key, data) in resources {
tracing::debug!(" - key='{}', name='{}', path='{}'", key, data.name, data.path);
}
} else {
for name in resources.keys() {
tracing::debug!(" - {}", name);
}
}
}
Ok(deps)
}
pub(crate) fn add_custom_alias(
deps: &mut BTreeMap<String, BTreeMap<String, DependencyData>>,
dep_ref: &str,
custom_name: &str,
) {
let dep_ref_parsed = match LockfileDependencyRef::from_str(dep_ref) {
Ok(dep_ref) => dep_ref,
Err(e) => {
tracing::debug!(
"Skipping invalid dep_ref format '{}' for custom name '{}': {}",
dep_ref,
custom_name,
e
);
return;
}
};
let dep_type = dep_ref_parsed.resource_type;
let dep_name = &dep_ref_parsed.path;
let type_str_plural = dep_type.to_plural().to_string();
if let Some(type_deps) = deps.get_mut(&type_str_plural) {
let name_to_key: HashMap<String, String> = type_deps
.iter()
.flat_map(|(key, data)| {
let mut mappings = vec![(data.name.clone(), key.clone())];
if let Some(basename) = Path::new(&data.name).file_name().and_then(|n| n.to_str()) {
mappings.push((basename.to_string(), key.clone()));
}
if let Some(stem) = Path::new(&data.path).file_stem().and_then(|n| n.to_str()) {
mappings.push((stem.to_string(), key.clone()));
}
if let Some(path_basename) =
Path::new(&data.path).file_name().and_then(|n| n.to_str())
{
mappings.push((path_basename.to_string(), key.clone()));
}
mappings
})
.collect();
let existing_data =
name_to_key.get(dep_name).and_then(|key| type_deps.get(key).cloned()).or_else(|| {
Path::new(dep_name)
.file_name()
.and_then(|name| name.to_str())
.and_then(|basename| name_to_key.get(basename))
.and_then(|key| type_deps.get(key).cloned())
});
if let Some(data) = existing_data {
let sanitized_alias = custom_name.replace('-', "_");
tracing::info!(
"ā Added {} alias '{}' -> resource '{}' (path: {})",
type_str_plural,
sanitized_alias,
dep_name,
data.path
);
type_deps.insert(sanitized_alias.clone(), data);
} else {
tracing::error!(
"ā NOT FOUND: {} resource '{}' for alias '{}'.\n \
Dep ref: '{}'\n \
Available {} (first 5): {}",
type_str_plural,
dep_name,
custom_name,
dep_ref,
type_deps.len(),
type_deps
.iter()
.take(5)
.map(|(k, v)| format!("'{}' (name='{}')", k, v.name))
.collect::<Vec<_>>()
.join(", ")
);
}
} else {
tracing::debug!(
"Resource type '{}' not found in deps map when adding custom alias '{}' for '{}'",
type_str_plural,
custom_name,
dep_ref
);
}
}