Skip to main content

components_rs/
module_state.rs

1use std::collections::HashMap;
2use std::path::{Path, PathBuf};
3
4use crate::discovery::node_modules;
5use crate::discovery::package_json::{self, PackageJson, get_module_iri};
6use crate::error::Result;
7use crate::fs::Fs;
8
9/// Represents the fully-resolved state of all discoverable CJS modules.
10/// Mirrors the TypeScript `IModuleState`.
11#[derive(Debug, Clone)]
12pub struct ModuleState {
13    /// Path to the root project.
14    pub main_module_path: PathBuf,
15    /// All ancestor paths used for node_modules searching.
16    pub node_module_import_paths: Vec<PathBuf>,
17    /// All discovered node module directories.
18    pub node_module_paths: Vec<PathBuf>,
19    /// Parsed package.json by module path.
20    pub package_jsons: HashMap<PathBuf, PackageJson>,
21    /// Component modules: module IRI → (major version → absolute components.jsonld path).
22    pub component_modules: HashMap<String, HashMap<u64, PathBuf>>,
23    /// Contexts: context IRI → parsed JSON content of context file.
24    pub contexts: HashMap<String, serde_json::Value>,
25    /// Import paths: IRI prefix → absolute local directory path.
26    pub import_paths: HashMap<String, PathBuf>,
27}
28
29impl ModuleState {
30    /// Build the full module state from a project root path.
31    pub async fn build(fs: &dyn Fs, main_module_path: &Path) -> Result<Self> {
32        let main_module_path = fs
33            .canonicalize(main_module_path)
34            .await?;
35
36        tracing::info!("Building module state from: {}", main_module_path.display());
37
38        let node_module_import_paths =
39            node_modules::build_node_module_import_paths(&main_module_path);
40        let node_module_paths =
41            node_modules::build_node_module_paths(fs, &node_module_import_paths).await?;
42
43        tracing::info!("Discovered {} node module paths", node_module_paths.len());
44
45        let mut package_jsons = package_json::read_package_jsons(fs, &node_module_paths).await?;
46        package_json::preprocess_all(fs, &mut package_jsons).await;
47
48        let component_modules = build_component_modules(&package_jsons)?;
49        let contexts = build_component_contexts(fs, &package_jsons).await?;
50        let import_paths = build_component_import_paths(&package_jsons)?;
51
52        tracing::info!(
53            "Found {} component modules, {} contexts, {} import paths",
54            component_modules.len(),
55            contexts.len(),
56            import_paths.len()
57        );
58
59        Ok(ModuleState {
60            main_module_path,
61            node_module_import_paths,
62            node_module_paths,
63            package_jsons,
64            component_modules,
65            contexts,
66            import_paths,
67        })
68    }
69}
70
71/// Build the component modules map: module IRI → (major version → absolute path to components.jsonld).
72fn build_component_modules(
73    package_jsons: &HashMap<PathBuf, PackageJson>,
74) -> Result<HashMap<String, HashMap<u64, PathBuf>>> {
75    let mut modules: HashMap<String, HashMap<u64, PathBuf>> = HashMap::new();
76    let mut versions: HashMap<String, HashMap<u64, semver::Version>> = HashMap::new();
77
78    for (module_path, pkg) in package_jsons {
79        let Some(module_iri) = get_module_iri(pkg) else {
80            continue;
81        };
82        let Some(components_rel) = &pkg.lsd_components else {
83            continue;
84        };
85        let Ok(version) = semver::Version::parse(&pkg.version) else {
86            continue;
87        };
88
89        let major = version.major;
90        let absolute_path = module_path.join(components_rel);
91
92        let entry = modules.entry(module_iri.clone()).or_default();
93        let ver_entry = versions.entry(module_iri.clone()).or_default();
94
95        if let Some(existing_ver) = ver_entry.get(&major) {
96            if &version > existing_ver {
97                entry.insert(major, absolute_path);
98                ver_entry.insert(major, version);
99            }
100        } else {
101            entry.insert(major, absolute_path);
102            ver_entry.insert(major, version);
103        }
104    }
105
106    Ok(modules)
107}
108
109/// Build the contexts map: context IRI → parsed JSON content.
110async fn build_component_contexts(
111    fs: &dyn Fs,
112    package_jsons: &HashMap<PathBuf, PackageJson>,
113) -> Result<HashMap<String, serde_json::Value>> {
114    let mut contexts: HashMap<String, serde_json::Value> = HashMap::new();
115    let mut ctx_versions: HashMap<String, semver::Version> = HashMap::new();
116
117    for (module_path, pkg) in package_jsons {
118        let Some(ctx_map) = &pkg.lsd_contexts else {
119            continue;
120        };
121        let Ok(version) = semver::Version::parse(&pkg.version) else {
122            continue;
123        };
124
125        for (ctx_iri, rel_path) in ctx_map {
126            let file_path = module_path.join(rel_path);
127
128            // Check version priority
129            if let Some(existing_ver) = ctx_versions.get(ctx_iri) {
130                if &version <= existing_ver {
131                    continue;
132                }
133            }
134
135            match fs.read_to_string(&file_path).await {
136                Ok(contents) => match serde_json::from_str(&contents) {
137                    Ok(parsed) => {
138                        contexts.insert(ctx_iri.clone(), parsed);
139                        ctx_versions.insert(ctx_iri.clone(), version.clone());
140                    }
141                    Err(e) => {
142                        tracing::warn!(
143                            "Failed to parse context file {}: {}",
144                            file_path.display(),
145                            e
146                        );
147                    }
148                },
149                Err(e) => {
150                    tracing::warn!(
151                        "Failed to read context file {}: {}",
152                        file_path.display(),
153                        e
154                    );
155                }
156            }
157        }
158    }
159
160    Ok(contexts)
161}
162
163/// Build the import paths map: IRI prefix → absolute directory path.
164fn build_component_import_paths(
165    package_jsons: &HashMap<PathBuf, PackageJson>,
166) -> Result<HashMap<String, PathBuf>> {
167    let mut import_paths: HashMap<String, PathBuf> = HashMap::new();
168    let mut path_versions: HashMap<String, semver::Version> = HashMap::new();
169
170    for (module_path, pkg) in package_jsons {
171        let Some(ip_map) = &pkg.lsd_import_paths else {
172            continue;
173        };
174        let Ok(version) = semver::Version::parse(&pkg.version) else {
175            continue;
176        };
177
178        for (iri_prefix, rel_path) in ip_map {
179            let abs_path = module_path.join(rel_path);
180
181            if let Some(existing_ver) = path_versions.get(iri_prefix) {
182                if &version <= existing_ver {
183                    continue;
184                }
185            }
186
187            import_paths.insert(iri_prefix.clone(), abs_path);
188            path_versions.insert(iri_prefix.clone(), version.clone());
189        }
190    }
191
192    Ok(import_paths)
193}