1use dashmap::{DashMap, DashSet};
19use thiserror::Error;
20
21#[derive(Debug, Error)]
23pub enum CollectionError {
24 #[error("Failed to parse module: {0}")]
26 ParseError(String),
27
28 #[error("Module not found: {0}")]
30 ModuleNotFound(String),
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum CollectedImportKind {
36 Static,
38 Dynamic,
40 TypeOnly,
42}
43
44#[derive(Debug, Clone)]
46pub struct CollectedModule {
47 pub id: String,
48 pub code: Option<String>,
49 pub is_entry: bool,
50 pub is_external: bool,
51 pub imports: Vec<CollectedImport>,
52 pub exports: Vec<CollectedExport>,
53 pub has_side_effects: bool,
54}
55
56#[derive(Debug, Clone)]
58pub struct CollectedImport {
59 pub source: String,
60 pub specifiers: Vec<CollectedImportSpecifier>,
61 pub kind: CollectedImportKind,
62 pub resolved_path: Option<String>,
65}
66
67#[derive(Debug, Clone)]
68pub enum CollectedImportSpecifier {
69 Named { imported: String, local: String },
70 Default { local: String },
71 Namespace { local: String },
72}
73
74#[derive(Debug, Clone)]
76pub enum CollectedExport {
77 Named {
78 exported: String,
79 local: Option<String>,
80 },
81 Default,
82 All {
83 source: String,
84 },
85}
86
87#[derive(Debug)]
91pub struct CollectionState {
92 pub modules: DashMap<String, CollectedModule>,
93 pub entry_points: DashSet<String>,
94 pub resolved_entry_ids: DashSet<String>,
96}
97
98impl CollectionState {
99 pub fn new() -> Self {
100 Self {
101 modules: DashMap::new(),
102 entry_points: DashSet::new(),
103 resolved_entry_ids: DashSet::new(),
104 }
105 }
106
107 pub fn add_module(&self, id: String, module: CollectedModule) {
108 self.modules.insert(id, module);
109 }
110
111 pub fn get_module(
112 &self,
113 id: &str,
114 ) -> Option<dashmap::mapref::one::Ref<'_, String, CollectedModule>> {
115 self.modules.get(id)
116 }
117
118 pub fn mark_entry(&self, id: String) {
124 self.entry_points.insert(id);
125 }
126
127 pub fn validate_entry_points(&self) -> Result<(), CollectionError> {
134 for entry in self.entry_points.iter() {
135 if !self.modules.contains_key(entry.key()) {
136 return Err(CollectionError::ModuleNotFound(entry.key().clone()));
137 }
138 }
139 Ok(())
140 }
141}
142
143impl Default for CollectionState {
144 fn default() -> Self {
145 Self::new()
146 }
147}
148
149pub fn parse_module_structure(
164 code: &str,
165) -> Result<(Vec<CollectedImport>, Vec<CollectedExport>, bool), CollectionError> {
166 use fob_gen::{ExportDeclaration, ParseOptions, QueryBuilder, parse};
167 use oxc_allocator::Allocator;
168 use oxc_ast::ast::Declaration;
169
170 let allocator = Allocator::default();
171
172 let parse_opts = if code.contains("import ") || code.contains("export ") {
174 if code.contains(": ")
175 || code.contains("interface ")
176 || code.contains("import type ")
177 || code.contains("export type ")
178 {
179 ParseOptions::tsx() } else {
181 ParseOptions::jsx() }
183 } else {
184 ParseOptions::default() };
186
187 let parsed = match parse(&allocator, code, parse_opts) {
189 Ok(parsed) => parsed,
190 Err(e) => {
191 return Err(CollectionError::ParseError(e.to_string()));
192 }
193 };
194
195 let mut imports = Vec::new();
196 let mut exports = Vec::new();
197 let has_side_effects = true; let query = QueryBuilder::new(&allocator, parsed.ast());
201
202 fn get_module_export_name_string(name: &oxc_ast::ast::ModuleExportName) -> String {
204 match name {
205 oxc_ast::ast::ModuleExportName::IdentifierName(ident) => ident.name.to_string(),
206 oxc_ast::ast::ModuleExportName::IdentifierReference(ident) => ident.name.to_string(),
207 oxc_ast::ast::ModuleExportName::StringLiteral(lit) => lit.value.to_string(),
208 }
209 }
210
211 for import_decl in query.find_imports(None).iter() {
213 let mut specifiers = Vec::new();
214 if let Some(specs) = &import_decl.specifiers {
215 for spec in specs {
216 match spec {
217 oxc_ast::ast::ImportDeclarationSpecifier::ImportDefaultSpecifier(
218 default_spec,
219 ) => {
220 specifiers.push(CollectedImportSpecifier::Default {
221 local: default_spec.local.name.to_string(),
222 });
223 }
224 oxc_ast::ast::ImportDeclarationSpecifier::ImportNamespaceSpecifier(ns_spec) => {
225 specifiers.push(CollectedImportSpecifier::Namespace {
226 local: ns_spec.local.name.to_string(),
227 });
228 }
229 oxc_ast::ast::ImportDeclarationSpecifier::ImportSpecifier(named_spec) => {
230 let imported = get_module_export_name_string(&named_spec.imported);
231 specifiers.push(CollectedImportSpecifier::Named {
232 imported,
233 local: named_spec.local.name.to_string(),
234 });
235 }
236 }
237 }
238 }
239 let kind = match import_decl.import_kind {
240 oxc_ast::ast::ImportOrExportKind::Value => CollectedImportKind::Static,
241 oxc_ast::ast::ImportOrExportKind::Type => CollectedImportKind::TypeOnly,
242 };
243
244 imports.push(CollectedImport {
245 source: import_decl.source.value.to_string(),
246 specifiers,
247 kind,
248 resolved_path: None,
249 });
250 }
251
252 for export_decl in query.find_exports().iter() {
254 match export_decl {
255 ExportDeclaration::Default(_) => {
256 exports.push(CollectedExport::Default);
257 }
258 ExportDeclaration::Named(named) => {
259 if let Some(src) = &named.source {
260 exports.push(CollectedExport::All {
261 source: src.value.to_string(),
262 });
263 } else if let Some(decl) = &named.declaration {
264 match decl {
265 Declaration::FunctionDeclaration(func) => {
266 if let Some(id) = &func.id {
267 exports.push(CollectedExport::Named {
268 exported: id.name.to_string(),
269 local: Some(id.name.to_string()),
270 });
271 }
272 }
273 Declaration::VariableDeclaration(var) => {
274 for decl in &var.declarations {
275 if let oxc_ast::ast::BindingPatternKind::BindingIdentifier(ident) =
276 &decl.id.kind
277 {
278 exports.push(CollectedExport::Named {
279 exported: ident.name.to_string(),
280 local: Some(ident.name.to_string()),
281 });
282 }
283 }
284 }
285 Declaration::ClassDeclaration(class) => {
286 if let Some(id) = &class.id {
287 exports.push(CollectedExport::Named {
288 exported: id.name.to_string(),
289 local: Some(id.name.to_string()),
290 });
291 }
292 }
293 _ => {}
294 }
295 } else {
296 for s in &named.specifiers {
298 let local = get_module_export_name_string(&s.local);
299 let exported = get_module_export_name_string(&s.exported);
300 exports.push(CollectedExport::Named {
301 exported,
302 local: Some(local),
303 });
304 }
305 }
306 }
307 ExportDeclaration::All(all) => {
308 exports.push(CollectedExport::All {
309 source: all.source.value.to_string(),
310 });
311 }
312 }
313 }
314
315 Ok((imports, exports, has_side_effects))
316}