Skip to main content

fallow_types/
discover.rs

1//! File discovery types: discovered files, file IDs, and entry points.
2
3use std::path::{Path, PathBuf};
4
5/// A discovered source file on disk.
6///
7/// # Examples
8///
9/// ```
10/// use fallow_types::discover::{DiscoveredFile, FileId};
11/// use std::path::PathBuf;
12///
13/// let file = DiscoveredFile {
14///     id: FileId(0),
15///     path: PathBuf::from("/project/src/index.ts"),
16///     size_bytes: 2048,
17/// };
18/// assert_eq!(file.id, FileId(0));
19/// assert_eq!(file.size_bytes, 2048);
20/// ```
21#[derive(Debug, Clone)]
22pub struct DiscoveredFile {
23    /// Unique file index.
24    pub id: FileId,
25    /// Absolute path.
26    pub path: PathBuf,
27    /// File size in bytes (for sorting largest-first).
28    pub size_bytes: u64,
29}
30
31/// Compact file identifier.
32///
33/// A newtype wrapper around `u32` used as a stable index into file arrays.
34/// `FileId`s are path-sorted (not insertion order) for stable cross-run identity.
35///
36/// # Examples
37///
38/// ```
39/// use fallow_types::discover::FileId;
40///
41/// let id = FileId(42);
42/// assert_eq!(id.0, 42);
43///
44/// // Implements Copy
45/// let copy = id;
46/// assert_eq!(id, copy);
47/// ```
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
49pub struct FileId(pub u32);
50
51const _: () = assert!(std::mem::size_of::<FileId>() == 4);
52#[cfg(all(target_pointer_width = "64", unix))]
53const _: () = assert!(std::mem::size_of::<DiscoveredFile>() == 40);
54
55/// Persistable file identity for cache entries that need to survive `FileId`
56/// churn across runs.
57///
58/// `FileId` remains a dense in-memory index. This key is path-derived, root
59/// relative where possible, and uses `/` separators so graph-cache metadata can
60/// compare file identity without relying on platform path display quirks.
61#[derive(
62    Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
63)]
64pub struct StableFileKey(String);
65
66impl StableFileKey {
67    /// Build a stable key from an absolute path and the analysis root.
68    #[must_use]
69    pub fn from_root_relative(root: &Path, path: &Path) -> Self {
70        let relative = path.strip_prefix(root).unwrap_or(path);
71        Self(normalize_path(relative))
72    }
73
74    /// Build a stable key from an already-root-relative path.
75    #[must_use]
76    pub fn from_relative(path: &Path) -> Self {
77        Self(normalize_path(path))
78    }
79
80    /// Stable string used in persisted cache manifests.
81    #[must_use]
82    pub fn as_str(&self) -> &str {
83        &self.0
84    }
85}
86
87fn normalize_path(path: &Path) -> String {
88    path.to_string_lossy().replace('\\', "/")
89}
90
91/// An entry point into the module graph.
92#[derive(Debug, Clone)]
93pub struct EntryPoint {
94    /// Absolute path to the entry point file.
95    pub path: PathBuf,
96    /// How this entry point was discovered.
97    pub source: EntryPointSource,
98}
99
100impl std::fmt::Display for EntryPointSource {
101    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102        match self {
103            Self::PackageJsonMain => f.write_str("package.json main"),
104            Self::PackageJsonModule => f.write_str("package.json module"),
105            Self::PackageJsonExports => f.write_str("package.json exports"),
106            Self::PackageJsonBin => f.write_str("package.json bin"),
107            Self::PackageJsonScript => f.write_str("package.json script"),
108            Self::Plugin { name } => write!(f, "{name}"),
109            Self::TestFile => f.write_str("test file"),
110            Self::DefaultIndex => f.write_str("default index"),
111            Self::ManualEntry => f.write_str("manual entry"),
112            Self::InfrastructureConfig => f.write_str("infrastructure config"),
113            Self::DynamicallyLoaded => f.write_str("dynamically loaded"),
114        }
115    }
116}
117
118/// Where an entry point was discovered from.
119#[derive(Debug, Clone)]
120pub enum EntryPointSource {
121    /// The `main` field in package.json.
122    PackageJsonMain,
123    /// The `module` field in package.json.
124    PackageJsonModule,
125    /// The `exports` field in package.json.
126    PackageJsonExports,
127    /// The `bin` field in package.json.
128    PackageJsonBin,
129    /// A script command in package.json.
130    PackageJsonScript,
131    /// Detected by a framework plugin.
132    Plugin {
133        /// Name of the plugin that detected this entry point.
134        name: String,
135    },
136    /// A test file (e.g., `*.test.ts`, `*.spec.ts`).
137    TestFile,
138    /// A default index file (e.g., `src/index.ts`).
139    DefaultIndex,
140    /// Manually configured in fallow config.
141    ManualEntry,
142    /// Discovered from infrastructure config files (Dockerfile, Procfile, fly.toml).
143    InfrastructureConfig,
144    /// Declared in `dynamicallyLoaded` config as a runtime-loaded file.
145    DynamicallyLoaded,
146}
147
148#[cfg(test)]
149mod stable_file_key_tests {
150    use super::*;
151
152    #[test]
153    fn stable_file_key_strips_root_prefix() {
154        let key = StableFileKey::from_root_relative(
155            Path::new("/project"),
156            Path::new("/project/src/index.ts"),
157        );
158
159        assert_eq!(key.as_str(), "src/index.ts");
160    }
161
162    #[test]
163    fn stable_file_key_keeps_path_when_outside_root() {
164        let key =
165            StableFileKey::from_root_relative(Path::new("/project"), Path::new("/other/file.ts"));
166
167        assert_eq!(key.as_str(), "/other/file.ts");
168    }
169
170    #[test]
171    fn stable_file_key_normalizes_windows_separators() {
172        let key = StableFileKey::from_relative(Path::new(r"src\feature\file.ts"));
173
174        assert_eq!(key.as_str(), "src/feature/file.ts");
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181
182    #[test]
183    fn entry_point_source_display_all_variants() {
184        assert_eq!(
185            EntryPointSource::PackageJsonMain.to_string(),
186            "package.json main"
187        );
188        assert_eq!(
189            EntryPointSource::PackageJsonModule.to_string(),
190            "package.json module"
191        );
192        assert_eq!(
193            EntryPointSource::PackageJsonExports.to_string(),
194            "package.json exports"
195        );
196        assert_eq!(
197            EntryPointSource::PackageJsonBin.to_string(),
198            "package.json bin"
199        );
200        assert_eq!(
201            EntryPointSource::PackageJsonScript.to_string(),
202            "package.json script"
203        );
204        assert_eq!(
205            EntryPointSource::Plugin {
206                name: "vitest".to_string()
207            }
208            .to_string(),
209            "vitest"
210        );
211        assert_eq!(EntryPointSource::TestFile.to_string(), "test file");
212        assert_eq!(EntryPointSource::DefaultIndex.to_string(), "default index");
213        assert_eq!(EntryPointSource::ManualEntry.to_string(), "manual entry");
214        assert_eq!(
215            EntryPointSource::InfrastructureConfig.to_string(),
216            "infrastructure config"
217        );
218        assert_eq!(
219            EntryPointSource::DynamicallyLoaded.to_string(),
220            "dynamically loaded"
221        );
222    }
223}