1use std::collections::HashMap;
18use std::sync::Arc;
19
20use rdf_parsers::jsonld::convert::{parse_json, JsonLdVal};
21use url::Url;
22
23use crate::discovery::node_modules;
24use crate::discovery::package_json::{self, get_module_iri, PackageJson};
25use crate::error::Result;
26use crate::fs::Fs;
27
28#[derive(Debug, Clone)]
31pub struct ModuleState {
32 pub main_module_path: Url,
34 pub node_module_import_paths: Vec<Url>,
36 pub node_module_paths: Vec<Url>,
38 pub package_jsons: HashMap<Url, PackageJson>,
40 pub component_modules: HashMap<String, HashMap<u64, Url>>,
42 pub contexts: Arc<HashMap<String, JsonLdVal>>,
45 pub import_paths: HashMap<String, Url>,
47 pub context_urls: HashMap<String, Url>,
49}
50
51impl ModuleState {
52 pub fn empty() -> Self {
53 Self {
54 main_module_path: Url::parse("file:///tmp/").unwrap(),
55 node_module_import_paths: Vec::new(),
56 node_module_paths: Vec::new(),
57 package_jsons: HashMap::new(),
58 component_modules: HashMap::new(),
59 contexts: Arc::new(HashMap::new()),
60 import_paths: HashMap::new(),
61 context_urls: HashMap::new(),
62 }
63 }
64 pub async fn build(fs: &dyn Fs, main_module_path: &Url) -> Result<Self> {
66 tracing::info!(
67 "[CJS] Building module state from: {}",
68 main_module_path.as_str()
69 );
70
71 let node_module_import_paths = vec![main_module_path.clone()];
72 let node_module_paths =
73 node_modules::build_node_module_paths(fs, &node_module_import_paths, false).await?;
74
75 tracing::info!("Discovered {} node module paths", node_module_paths.len());
76
77 let mut package_jsons = package_json::read_package_jsons(fs, &node_module_paths).await?;
78 package_json::preprocess_all(fs, &mut package_jsons).await;
79
80 let component_modules = build_component_modules(&package_jsons)?;
81 let contexts = Arc::new(build_component_contexts(fs, &package_jsons).await?);
82 let import_paths = build_component_import_paths(&package_jsons)?;
83 let context_urls = build_component_context_urls(&package_jsons)?;
84
85 tracing::info!(
86 "[CJS] Found {} component modules, {} contexts, {} import paths",
87 component_modules.len(),
88 contexts.len(),
89 import_paths.len()
90 );
91
92 Ok(ModuleState {
93 main_module_path: main_module_path.clone(),
94 node_module_import_paths,
95 node_module_paths,
96 package_jsons,
97 component_modules,
98 contexts,
99 import_paths,
100 context_urls,
101 })
102 }
103}
104
105fn build_component_modules(
107 package_jsons: &HashMap<Url, PackageJson>,
108) -> Result<HashMap<String, HashMap<u64, Url>>> {
109 let mut modules: HashMap<String, HashMap<u64, Url>> = HashMap::new();
110 let mut versions: HashMap<String, HashMap<u64, semver::Version>> = HashMap::new();
111
112 for (module_path, pkg) in package_jsons {
113 let Some(module_iri) = get_module_iri(pkg) else {
114 continue;
115 };
116 let Some(components_rel) = &pkg.lsd_components else {
117 continue;
118 };
119 let Ok(version) = semver::Version::parse(&pkg.version) else {
120 continue;
121 };
122
123 let absolute_path = match module_path.join(components_rel) {
124 Ok(u) => u,
125 Err(_) => continue,
126 };
127
128 let major = version.major;
129 let entry = modules.entry(module_iri.clone()).or_default();
130 let ver_entry = versions.entry(module_iri.clone()).or_default();
131
132 if let Some(existing_ver) = ver_entry.get(&major) {
133 if &version > existing_ver {
134 entry.insert(major, absolute_path);
135 ver_entry.insert(major, version);
136 }
137 } else {
138 entry.insert(major, absolute_path);
139 ver_entry.insert(major, version);
140 }
141 }
142
143 Ok(modules)
144}
145
146async fn build_component_contexts(
148 fs: &dyn Fs,
149 package_jsons: &HashMap<Url, PackageJson>,
150) -> Result<HashMap<String, JsonLdVal>> {
151 let mut contexts: HashMap<String, JsonLdVal> = HashMap::new();
152 let mut ctx_versions: HashMap<String, semver::Version> = HashMap::new();
153
154 for (module_path, pkg) in package_jsons {
155 let Some(ctx_map) = &pkg.lsd_contexts else {
156 continue;
157 };
158 let Ok(version) = semver::Version::parse(&pkg.version) else {
159 continue;
160 };
161
162 for (ctx_iri, rel_path) in ctx_map {
163 let file_url = match module_path.join(rel_path) {
164 Ok(u) => u,
165 Err(_) => continue,
166 };
167
168 if let Some(existing_ver) = ctx_versions.get(ctx_iri) {
169 if &version <= existing_ver {
170 continue;
171 }
172 }
173
174 match fs.read_to_string(&file_url).await {
175 Ok(contents) => match parse_json(&contents) {
176 Some(parsed) => {
177 contexts.insert(ctx_iri.clone(), parsed);
178 ctx_versions.insert(ctx_iri.clone(), version.clone());
179 }
180 None => {
181 tracing::warn!("Failed to parse context file {}", file_url.as_str());
182 }
183 },
184 Err(e) => {
185 tracing::warn!("Failed to read context file {}: {}", file_url.as_str(), e);
186 }
187 }
188 }
189 }
190
191 Ok(contexts)
192}
193
194fn build_component_import_paths(
196 package_jsons: &HashMap<Url, PackageJson>,
197) -> Result<HashMap<String, Url>> {
198 let mut import_paths: HashMap<String, Url> = HashMap::new();
199 let mut path_versions: HashMap<String, semver::Version> = HashMap::new();
200
201 for (module_path, pkg) in package_jsons {
202 let Some(ip_map) = &pkg.lsd_import_paths else {
203 continue;
204 };
205 let Ok(version) = semver::Version::parse(&pkg.version) else {
206 continue;
207 };
208
209 for (iri_prefix, rel_path) in ip_map {
210 let rel_dir = if rel_path.ends_with('/') {
212 rel_path.clone()
213 } else {
214 format!("{}/", rel_path)
215 };
216 let abs_url = match module_path.join(&rel_dir) {
217 Ok(u) => u,
218 Err(e) => {
219 tracing::warn!("Failed to join import path: {}", e);
220 continue;
221 }
222 };
223
224 if let Some(existing_ver) = path_versions.get(iri_prefix) {
225 if &version <= existing_ver {
226 continue;
227 }
228 }
229
230 import_paths.insert(iri_prefix.clone(), abs_url);
231 path_versions.insert(iri_prefix.clone(), version.clone());
232 }
233 }
234
235 Ok(import_paths)
236}
237
238fn build_component_context_urls(
240 package_jsons: &HashMap<Url, PackageJson>,
241) -> Result<HashMap<String, Url>> {
242 let mut context_urls: HashMap<String, Url> = HashMap::new();
243 let mut ctx_versions: HashMap<String, semver::Version> = HashMap::new();
244
245 for (module_path, pkg) in package_jsons {
246 let Some(ctx_map) = &pkg.lsd_contexts else {
247 continue;
248 };
249 let Ok(version) = semver::Version::parse(&pkg.version) else {
250 continue;
251 };
252
253 for (ctx_iri, rel_path) in ctx_map {
254 let file_url = match module_path.join(rel_path) {
255 Ok(u) => u,
256 Err(_) => continue,
257 };
258
259 if let Some(existing_ver) = ctx_versions.get(ctx_iri) {
260 if &version <= existing_ver {
261 continue;
262 }
263 }
264
265 context_urls.insert(ctx_iri.clone(), file_url);
266 ctx_versions.insert(ctx_iri.clone(), version.clone());
267 }
268 }
269
270 Ok(context_urls)
271}