fob_graph/
module.rs

1use std::path::{Path, PathBuf};
2use std::sync::Arc;
3
4use serde::{Deserialize, Serialize};
5
6use super::symbol::SymbolTable;
7use super::{Export, Import, ModuleId};
8
9/// Resolved module metadata used by graph algorithms and builders.
10///
11/// Heavy collections (imports, exports, symbol_table) are wrapped in Arc
12/// to make cloning cheap when returning modules from the graph.
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct Module {
15    pub id: ModuleId,
16    pub path: PathBuf,
17    pub source_type: SourceType,
18    #[serde(with = "arc_vec_serde")]
19    pub imports: Arc<Vec<Import>>,
20    #[serde(with = "arc_vec_serde")]
21    pub exports: Arc<Vec<Export>>,
22    pub has_side_effects: bool,
23    pub is_entry: bool,
24    pub is_external: bool,
25    pub original_size: usize,
26    pub bundled_size: Option<usize>,
27    /// Symbol table from semantic analysis (intra-file dead code detection)
28    #[serde(with = "arc_symbol_table_serde")]
29    pub symbol_table: Arc<SymbolTable>,
30    /// Module format (ESM vs CJS) from rolldown analysis
31    pub module_format: ModuleFormat,
32    /// Export structure kind (ESM, CJS, or None)
33    pub exports_kind: ExportsKind,
34    /// True if module has star re-exports (`export * from`)
35    pub has_star_exports: bool,
36    /// Execution order in module graph (topological sort)
37    pub execution_order: Option<u32>,
38}
39
40// Serde helper for Arc<Vec<T>>
41mod arc_vec_serde {
42    use super::*;
43    use serde::de::Deserializer;
44    use serde::ser::Serializer;
45
46    pub fn serialize<S, T>(value: &Arc<Vec<T>>, serializer: S) -> Result<S::Ok, S::Error>
47    where
48        S: Serializer,
49        T: Serialize,
50    {
51        value.as_ref().serialize(serializer)
52    }
53
54    pub fn deserialize<'de, D, T>(deserializer: D) -> Result<Arc<Vec<T>>, D::Error>
55    where
56        D: Deserializer<'de>,
57        T: Deserialize<'de>,
58    {
59        Vec::deserialize(deserializer).map(Arc::new)
60    }
61}
62
63// Serde helper for Arc<SymbolTable>
64mod arc_symbol_table_serde {
65    use super::*;
66    use serde::de::Deserializer;
67    use serde::ser::Serializer;
68
69    pub fn serialize<S>(value: &Arc<SymbolTable>, serializer: S) -> Result<S::Ok, S::Error>
70    where
71        S: Serializer,
72    {
73        value.as_ref().serialize(serializer)
74    }
75
76    pub fn deserialize<'de, D>(deserializer: D) -> Result<Arc<SymbolTable>, D::Error>
77    where
78        D: Deserializer<'de>,
79    {
80        SymbolTable::deserialize(deserializer).map(Arc::new)
81    }
82}
83
84impl Module {
85    /// Create a new module builder with sensible defaults.
86    pub fn builder(id: ModuleId, path: PathBuf, source_type: SourceType) -> ModuleBuilder {
87        ModuleBuilder {
88            module: Self {
89                id,
90                path,
91                source_type,
92                imports: Arc::new(Vec::new()),
93                exports: Arc::new(Vec::new()),
94                has_side_effects: false,
95                is_entry: false,
96                is_external: false,
97                original_size: 0,
98                bundled_size: None,
99                symbol_table: Arc::new(SymbolTable::new()),
100                module_format: ModuleFormat::Unknown,
101                exports_kind: ExportsKind::None,
102                has_star_exports: false,
103                execution_order: None,
104            },
105        }
106    }
107
108    /// Mark the module as an entry module.
109    pub fn mark_entry(&mut self) {
110        self.is_entry = true;
111    }
112
113    /// Mark the module as an external dependency.
114    pub fn mark_external(&mut self) {
115        self.is_external = true;
116    }
117
118    /// Toggle side-effect tracking on the module.
119    pub fn set_side_effects(&mut self, has_side_effects: bool) {
120        self.has_side_effects = has_side_effects;
121    }
122
123    /// Update bundled size information (if available).
124    pub fn set_bundled_size(&mut self, size: Option<usize>) {
125        self.bundled_size = size;
126    }
127
128    /// Get an iterator over imports.
129    pub fn imports_iter(&self) -> impl Iterator<Item = &Import> {
130        self.imports.iter()
131    }
132
133    /// Get an iterator over exports.
134    pub fn exports_iter(&self) -> impl Iterator<Item = &Export> {
135        self.exports.iter()
136    }
137
138    /// Get mutable access to exports (for external tools like framework rules).
139    ///
140    /// This uses Arc::make_mut to create a mutable copy only when needed.
141    ///
142    /// # Example
143    ///
144    /// ```rust,ignore
145    /// for export in module.exports_mut() {
146    ///     if export.name.starts_with("use") {
147    ///         export.mark_framework_used();
148    ///     }
149    /// }
150    /// ```
151    pub fn exports_mut(&mut self) -> &mut Vec<Export> {
152        Arc::make_mut(&mut self.exports)
153    }
154
155    /// Get imports that reference a specific module.
156    ///
157    /// # Example
158    ///
159    /// ```rust,ignore
160    /// use fob_graph::graph::ModuleId;
161    ///
162    /// let react_id = ModuleId::new("node_modules/react/index.js")?;
163    /// let imports = module.imports_from(&react_id);
164    /// assert_eq!(imports.len(), 1);
165    /// ```
166    pub fn imports_from(&self, target: &ModuleId) -> Vec<&Import> {
167        self.imports
168            .iter()
169            .filter(|imp| imp.resolved_to.as_ref() == Some(target))
170            .collect()
171    }
172
173    /// Check if this module imports from a specific source specifier.
174    ///
175    /// This is useful for framework detection - checking if a module imports
176    /// from "react", "vue", "svelte", etc.
177    ///
178    /// # Example
179    ///
180    /// ```rust,ignore
181    /// if module.has_import_from("react") {
182    ///     // This is a React module
183    /// }
184    /// ```
185    pub fn has_import_from(&self, source: &str) -> bool {
186        self.imports.iter().any(|imp| imp.source == source)
187    }
188
189    /// Get all import sources (for dependency analysis).
190    ///
191    /// Returns a vector of source specifiers that this module imports from.
192    ///
193    /// # Example
194    ///
195    /// ```rust,ignore
196    /// let sources = module.import_sources();
197    /// // sources = ["react", "lodash", "./utils"]
198    /// ```
199    pub fn import_sources(&self) -> Vec<&str> {
200        self.imports.iter().map(|imp| imp.source.as_str()).collect()
201    }
202}
203
204/// Builder for `Module` to avoid long argument lists in constructors.
205pub struct ModuleBuilder {
206    module: Module,
207}
208
209impl ModuleBuilder {
210    pub fn imports(mut self, imports: Vec<Import>) -> Self {
211        self.module.imports = Arc::new(imports);
212        self
213    }
214
215    pub fn exports(mut self, exports: Vec<Export>) -> Self {
216        self.module.exports = Arc::new(exports);
217        self
218    }
219
220    pub fn side_effects(mut self, has_side_effects: bool) -> Self {
221        self.module.has_side_effects = has_side_effects;
222        self
223    }
224
225    pub fn entry(mut self, is_entry: bool) -> Self {
226        self.module.is_entry = is_entry;
227        self
228    }
229
230    pub fn external(mut self, is_external: bool) -> Self {
231        self.module.is_external = is_external;
232        self
233    }
234
235    pub fn original_size(mut self, original_size: usize) -> Self {
236        self.module.original_size = original_size;
237        self
238    }
239
240    pub fn bundled_size(mut self, bundled_size: Option<usize>) -> Self {
241        self.module.bundled_size = bundled_size;
242        self
243    }
244
245    pub fn symbol_table(mut self, symbol_table: SymbolTable) -> Self {
246        self.module.symbol_table = Arc::new(symbol_table);
247        self
248    }
249
250    pub fn module_format(mut self, module_format: ModuleFormat) -> Self {
251        self.module.module_format = module_format;
252        self
253    }
254
255    pub fn exports_kind(mut self, exports_kind: ExportsKind) -> Self {
256        self.module.exports_kind = exports_kind;
257        self
258    }
259
260    pub fn has_star_exports(mut self, has_star_exports: bool) -> Self {
261        self.module.has_star_exports = has_star_exports;
262        self
263    }
264
265    pub fn execution_order(mut self, execution_order: Option<u32>) -> Self {
266        self.module.execution_order = execution_order;
267        self
268    }
269
270    pub fn build(self) -> Module {
271        self.module
272    }
273}
274
275/// Module definition format (ESM vs CJS).
276#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
277pub enum ModuleFormat {
278    /// ECMAScript Module (.mjs or "type": "module")
279    EsmMjs,
280    /// ECMAScript Module (package.json "type": "module")
281    EsmPackageJson,
282    /// ECMAScript Module (regular .js with ESM syntax)
283    Esm,
284    /// CommonJS (package.json "type": "commonjs")
285    CjsPackageJson,
286    /// CommonJS (regular require/module.exports)
287    Cjs,
288    /// Unknown format
289    Unknown,
290}
291
292/// Export structure kind.
293#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
294pub enum ExportsKind {
295    /// Module uses ESM exports
296    Esm,
297    /// Module uses CommonJS exports
298    CommonJs,
299    /// No exports detected
300    None,
301}
302
303/// Resolved module source type derived from file extensions.
304#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
305pub enum SourceType {
306    JavaScript,
307    TypeScript,
308    Jsx,
309    Tsx,
310    Json,
311    Css,
312    Unknown,
313}
314
315impl SourceType {
316    /// Derive the source type from a file extension string.
317    pub fn from_extension(ext: &str) -> Self {
318        match ext {
319            "js" | "mjs" | "cjs" => Self::JavaScript,
320            "ts" | "mts" | "cts" => Self::TypeScript,
321            "jsx" => Self::Jsx,
322            "tsx" => Self::Tsx,
323            "json" => Self::Json,
324            "css" => Self::Css,
325            _ => Self::Unknown,
326        }
327    }
328
329    /// Attempt to infer the source type from a file path.
330    pub fn from_path(path: &Path) -> Self {
331        path.extension()
332            .and_then(|ext| ext.to_str())
333            .map_or(Self::Unknown, Self::from_extension)
334    }
335
336    /// Returns true if the file is JavaScript/TypeScript based.
337    pub fn is_javascript_like(&self) -> bool {
338        matches!(
339            self,
340            Self::JavaScript | Self::TypeScript | Self::Jsx | Self::Tsx
341        )
342    }
343}