Skip to main content

pecto_typescript/
lib.rs

1pub mod context;
2pub mod dependencies;
3pub mod extractors;
4pub mod flow;
5pub mod parser;
6
7use context::{AnalysisContext, ParsedFile};
8use pecto_core::model::ProjectSpec;
9use rayon::prelude::*;
10use std::path::Path;
11
12/// Analyze a TypeScript/JavaScript project and extract behavior specs.
13pub fn analyze_project(path: &Path) -> Result<ProjectSpec, TypeScriptAnalysisError> {
14    let project_name = path
15        .file_name()
16        .map(|n| n.to_string_lossy().to_string())
17        .unwrap_or_else(|| "unknown".to_string());
18
19    let ts_files: Vec<walkdir::DirEntry> = walkdir::WalkDir::new(path)
20        .into_iter()
21        .filter_map(|e| e.ok())
22        .filter(|e| {
23            let p = e.path().to_string_lossy();
24            let ext_ok = e
25                .path()
26                .extension()
27                .is_some_and(|ext| ext == "ts" || ext == "tsx" || ext == "js" || ext == "jsx");
28            ext_ok
29                && !p.contains("node_modules")
30                && !p.contains("/dist/")
31                && !p.contains("/build/")
32                && !p.contains("/.next/")
33                && !p.contains("/__tests__/")
34                && !p.contains("/__mocks__/")
35                && !p.contains("/test/")
36                && !p.contains("/tests/")
37                && !p.ends_with(".spec.ts")
38                && !p.ends_with(".test.ts")
39                && !p.ends_with(".spec.js")
40                && !p.ends_with(".test.js")
41                && !p.ends_with(".e2e-spec.ts")
42                && !p.ends_with(".d.ts")
43        })
44        .collect();
45
46    let parsed_results: Vec<Result<ParsedFile, TypeScriptAnalysisError>> = ts_files
47        .par_iter()
48        .map(|entry| {
49            let source = std::fs::read_to_string(entry.path()).map_err(|e| {
50                TypeScriptAnalysisError::IoError(entry.path().to_string_lossy().to_string(), e)
51            })?;
52
53            let relative_path = entry
54                .path()
55                .strip_prefix(path)
56                .unwrap_or(entry.path())
57                .to_string_lossy()
58                .to_string();
59
60            ParsedFile::parse(source, relative_path)
61        })
62        .collect();
63
64    let mut parsed_files = Vec::with_capacity(parsed_results.len());
65    for result in parsed_results {
66        parsed_files.push(result?);
67    }
68
69    let files_analyzed = parsed_files.len();
70    let ctx = AnalysisContext::new(parsed_files);
71
72    let mut spec = ProjectSpec::new(project_name);
73    spec.files_analyzed = files_analyzed;
74
75    for file in &ctx.files {
76        if let Some(capability) = extractors::controller::extract(file)
77            && !capability.is_empty()
78        {
79            spec.capabilities.push(capability);
80        }
81
82        if let Some(capability) = extractors::entity::extract(file)
83            && !capability.is_empty()
84        {
85            spec.capabilities.push(capability);
86        }
87
88        if let Some(capability) = extractors::service::extract(file)
89            && !capability.is_empty()
90        {
91            spec.capabilities.push(capability);
92        }
93    }
94
95    // Post-processing
96    dependencies::resolve_dependencies(&mut spec, &ctx);
97    flow::extract_flows(&mut spec, &ctx);
98
99    Ok(spec)
100}
101
102#[derive(Debug, thiserror::Error)]
103pub enum TypeScriptAnalysisError {
104    #[error("Failed to read {0}: {1}")]
105    IoError(String, std::io::Error),
106    #[error("Parse error in {0}: {1}")]
107    ParseError(String, String),
108}