use std::{collections::HashSet, fs, path::Path};
use anyhow::Result;
use oxc_allocator::Allocator;
use oxc_ast::ast::{ObjectProperty, PropertyKey, VariableDeclaration};
use oxc_ast_visit::Visit;
use oxc_parser::{Parser, ParserReturn};
use oxc_span::SourceType;
use serde::{Deserialize, Serialize};
use crate::core::rendered_template::RenderedTemplatesCache;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ResourceType {
Database,
Cache,
Storage,
Queue,
Monitoring,
}
impl ResourceType {
pub fn as_str(&self) -> &'static str {
match self {
ResourceType::Database => "database",
ResourceType::Cache => "cache",
ResourceType::Storage => "storage",
ResourceType::Queue => "queue",
ResourceType::Monitoring => "monitoring",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct RuntimeDependency {
pub name: String,
pub resource_type: ResourceType,
}
pub struct RuntimeDepsVisitor {
pub dependencies: Vec<RuntimeDependency>,
in_runtime_deps: bool,
}
impl RuntimeDepsVisitor {
pub fn new() -> Self {
Self {
dependencies: Vec::new(),
in_runtime_deps: false,
}
}
fn map_dependency_to_resource_type(dep_name: &str) -> Option<ResourceType> {
match dep_name {
"Orm" | "MikroORM" => Some(ResourceType::Database),
"EntityMgr" | "EntityManager" => None,
"RedisClient" | "Redis" | "RedisCache" => Some(ResourceType::Cache),
"ObjectStore" | "S3ObjectStore" | "S3Client" | "S3" => Some(ResourceType::Storage),
"KafkaClient" | "Kafka" | "BullMQ" | "QueueClient" => Some(ResourceType::Queue),
"OtelCollector" | "OpenTelemetryCollector" | "PrometheusClient" => {
Some(ResourceType::Monitoring)
}
"SchemaValidator" | "Metrics" => None,
_ => None,
}
}
}
impl<'a> Visit<'a> for RuntimeDepsVisitor {
fn visit_variable_declaration(&mut self, decl: &VariableDeclaration<'a>) {
for declarator in &decl.declarations {
if let oxc_ast::ast::BindingPatternKind::BindingIdentifier(ident) = &declarator.id.kind
{
if ident.name == "runtimeDependencies" {
self.in_runtime_deps = true;
if let Some(init) = &declarator.init {
self.visit_expression(init);
}
self.in_runtime_deps = false;
}
}
}
oxc_ast_visit::walk::walk_variable_declaration(self, decl);
}
fn visit_object_property(&mut self, prop: &ObjectProperty<'a>) {
if self.in_runtime_deps {
if let PropertyKey::StaticIdentifier(ident) = &prop.key {
let dep_name = ident.name.to_string();
if let Some(resource_type) = Self::map_dependency_to_resource_type(&dep_name) {
self.dependencies.push(RuntimeDependency {
name: dep_name,
resource_type,
});
}
}
}
oxc_ast_visit::walk::walk_object_property(self, prop);
}
}
pub fn extract_runtime_deps_from_file(
file_path: &Path,
rendered_templates_cache: &RenderedTemplatesCache,
) -> Result<Vec<RuntimeDependency>> {
let source_code = rendered_templates_cache.get(file_path)?.unwrap().content;
extract_runtime_deps_from_source(&source_code)
}
pub fn extract_runtime_deps_from_source(source_code: &str) -> Result<Vec<RuntimeDependency>> {
let allocator = Allocator::default();
let ParserReturn {
program, errors, ..
} = Parser::new(
&allocator,
source_code,
SourceType::default().with_typescript(true),
)
.parse();
if !errors.is_empty() {
log::debug!(
"TypeScript parse errors during runtime deps scan: {:?}",
errors
);
}
let mut visitor = RuntimeDepsVisitor::new();
visitor.visit_program(&program);
Ok(visitor.dependencies)
}
pub fn find_all_runtime_deps(
modules_path: &Path,
rendered_templates_cache: &RenderedTemplatesCache,
) -> Result<std::collections::HashMap<String, Vec<RuntimeDependency>>> {
let mut all_deps = std::collections::HashMap::new();
if !modules_path.exists() {
return Ok(all_deps);
}
for entry in fs::read_dir(modules_path)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
let registrations_path = path.join("registrations.ts");
if registrations_path.exists() {
let project_name = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown")
.to_string();
let deps =
extract_runtime_deps_from_file(®istrations_path, rendered_templates_cache)?;
if deps.iter().any(|d| d.name == "MikroORM") {
if let Ok(Some(_database)) =
crate::core::sync::detection::detect_database_from_mikro_orm_config(&path)
{
}
}
if !deps.is_empty() {
all_deps.insert(project_name, deps);
}
}
}
}
Ok(all_deps)
}
pub fn get_unique_resource_types(deps: &[RuntimeDependency]) -> Vec<String> {
let unique: HashSet<ResourceType> = deps.iter().map(|d| d.resource_type.clone()).collect();
let mut types: Vec<ResourceType> = unique.into_iter().collect();
types.sort();
types.into_iter().map(|t| t.as_str().to_string()).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_runtime_deps() {
let source = r#"
const runtimeDependencies = environmentConfig.chain({
MikroORM: {
lifetime: Lifetime.Singleton,
type: MikroORM,
factory: () => new MikroORM(config)
},
RedisClient: {
lifetime: Lifetime.Singleton,
type: RedisClient,
factory: () => new RedisClient()
},
S3ObjectStore: {
lifetime: Lifetime.Singleton,
type: S3ObjectStore,
factory: () => new S3ObjectStore()
}
});
"#;
let deps = extract_runtime_deps_from_source(source).unwrap();
assert_eq!(deps.len(), 3);
assert!(
deps.iter()
.any(|d| d.name == "MikroORM" && d.resource_type == ResourceType::Database)
);
assert!(
deps.iter()
.any(|d| d.name == "RedisClient" && d.resource_type == ResourceType::Cache)
);
assert!(
deps.iter()
.any(|d| d.name == "S3ObjectStore" && d.resource_type == ResourceType::Storage)
);
}
#[test]
fn test_entity_manager_ignored() {
let source = r#"
const runtimeDependencies = environmentConfig.chain({
MikroORM: {
lifetime: Lifetime.Singleton,
type: MikroORM,
factory: () => new MikroORM(config)
},
EntityManager: {
lifetime: Lifetime.Scoped,
type: EntityManager,
factory: ({ MikroORM }) => MikroORM.em.fork()
}
});
"#;
let deps = extract_runtime_deps_from_source(source).unwrap();
assert_eq!(deps.len(), 1);
assert!(
deps.iter()
.any(|d| d.name == "MikroORM" && d.resource_type == ResourceType::Database)
);
assert!(
!deps.iter()
.any(|d| d.name == "EntityManager")
);
}
#[test]
fn test_new_token_names() {
let source = r#"
const runtimeDependencies = environmentConfig.chain({
Orm: {
lifetime: Lifetime.Singleton,
type: MikroORM,
factory: () => new MikroORM(config)
},
OtelCollector: {
lifetime: Lifetime.Singleton,
type: OpenTelemetryCollector,
factory: () => new OpenTelemetryCollector()
},
ObjectStore: {
lifetime: Lifetime.Singleton,
type: S3ObjectStore,
factory: () => new S3ObjectStore()
},
EntityMgr: {
lifetime: Lifetime.Scoped,
type: EntityManager,
factory: ({ Orm }) => Orm.em.fork()
}
});
"#;
let deps = extract_runtime_deps_from_source(source).unwrap();
assert_eq!(deps.len(), 3); assert!(
deps.iter()
.any(|d| d.name == "Orm" && d.resource_type == ResourceType::Database)
);
assert!(
deps.iter()
.any(|d| d.name == "OtelCollector" && d.resource_type == ResourceType::Monitoring)
);
assert!(
deps.iter()
.any(|d| d.name == "ObjectStore" && d.resource_type == ResourceType::Storage)
);
assert!(!deps.iter().any(|d| d.name == "EntityMgr"));
}
}