1use std::collections::{HashMap, HashSet};
2use std::fs;
3use std::path::{Path, PathBuf};
4
5use anyhow::{Context, Result, anyhow};
6use path_clean::clean;
7use regex::Regex;
8
9#[derive(Clone)]
11struct NixImport {
12 path: PathBuf,
14 _position: (usize, usize),
16 full_import: String,
18}
19
20#[derive(Clone)]
22struct NixFile {
23 _path: PathBuf,
25 content: String,
27 imports: Vec<NixImport>,
29}
30
31pub fn bundle_nix_files(entry_point: &Path) -> Result<String> {
33 let mut processed_files = HashSet::new();
35 let mut file_contents = HashMap::new();
37
38 process_nix_file(entry_point, &mut processed_files, &mut file_contents)?;
40
41 let abs_entry_path = if entry_point.is_absolute() {
43 entry_point.to_path_buf()
44 } else {
45 std::env::current_dir()?.join(entry_point)
46 };
47
48 let clean_entry_path = PathBuf::from(clean(abs_entry_path.to_string_lossy().as_ref()));
50
51 if !file_contents.contains_key(&clean_entry_path) {
53 return Err(anyhow!(
54 "エントリーポイントの内容が見つかりません: {}",
55 clean_entry_path.display()
56 ));
57 }
58
59 let bundled_content = inline_imports(&clean_entry_path, &file_contents)?;
61
62 Ok(bundled_content)
63}
64
65fn process_nix_file(
67 file_path: &Path,
68 processed_files: &mut HashSet<PathBuf>,
69 file_contents: &mut HashMap<PathBuf, NixFile>,
70) -> Result<()> {
71 let abs_path = if file_path.is_absolute() {
73 file_path.to_path_buf()
74 } else {
75 std::env::current_dir()?.join(file_path)
76 };
77
78 let clean_path = PathBuf::from(clean(abs_path.to_string_lossy().as_ref()));
80
81 if processed_files.contains(&clean_path) {
83 return Ok(());
84 }
85
86 if !clean_path.exists() {
88 return Err(anyhow!("ファイルが存在しません: {}", clean_path.display()));
89 }
90
91 let content = fs::read_to_string(&clean_path)
93 .with_context(|| format!("ファイルの読み込みに失敗しました: {}", clean_path.display()))?;
94
95 let imports = parse_imports(&content, &clean_path)?;
97
98 let nix_file = NixFile {
100 _path: clean_path.clone(),
101 content: content.clone(),
102 imports: imports.clone(),
103 };
104 file_contents.insert(clean_path.clone(), nix_file);
105
106 processed_files.insert(clean_path.clone());
108
109 for import in imports {
111 let import_path = resolve_import_path(&import.path, &clean_path)?;
112 process_nix_file(&import_path, processed_files, file_contents)?;
113 }
114
115 Ok(())
116}
117
118fn resolve_import_path(import_path: &Path, current_file: &Path) -> Result<PathBuf> {
120 if import_path.is_absolute() {
122 return Ok(import_path.to_path_buf());
123 }
124
125 let parent_dir = current_file
127 .parent()
128 .ok_or_else(|| anyhow!("親ディレクトリが見つかりません: {}", current_file.display()))?;
129
130 let resolved_path = parent_dir.join(import_path);
131 let clean_resolved_path = PathBuf::from(clean(resolved_path.to_string_lossy().as_ref()));
132
133 Ok(clean_resolved_path)
134}
135
136fn parse_imports(content: &str, file_path: &Path) -> Result<Vec<NixImport>> {
138 let mut imports = Vec::new();
139
140 let import_regex = Regex::new(r#"import\s+(?:(?:"([^"]+)")|(?:'([^']+)')|([^\s;]+))"#)?;
143
144 for (line_idx, line) in content.lines().enumerate() {
146 for captures in import_regex.captures_iter(line) {
147 let path_str = captures
148 .get(1)
149 .or_else(|| captures.get(2))
150 .or_else(|| captures.get(3))
151 .ok_or_else(|| {
152 anyhow!(
153 "インポートパスが見つかりません: {}:{}",
154 file_path.display(),
155 line_idx + 1
156 )
157 })?
158 .as_str();
159
160 let full_import = captures.get(0).unwrap().as_str().to_string();
161 let column = captures.get(0).unwrap().start();
162
163 let import_path = PathBuf::from(path_str);
164
165 imports.push(NixImport {
166 path: import_path,
167 _position: (line_idx + 1, column),
168 full_import,
169 });
170 }
171 }
172
173 Ok(imports)
174}
175
176fn inline_imports(entry_point: &Path, file_contents: &HashMap<PathBuf, NixFile>) -> Result<String> {
178 let mut inlined_files = HashSet::new();
180
181 inline_file_recursive(entry_point, file_contents, &mut inlined_files)
183}
184
185fn inline_file_recursive(
187 file_path: &Path,
188 file_contents: &HashMap<PathBuf, NixFile>,
189 inlined_files: &mut HashSet<PathBuf>,
190) -> Result<String> {
191 let abs_path = if file_path.is_absolute() {
193 file_path.to_path_buf()
194 } else {
195 std::env::current_dir()?.join(file_path)
196 };
197
198 let clean_path = PathBuf::from(clean(abs_path.to_string_lossy().as_ref()));
200
201 let nix_file = file_contents
203 .get(&clean_path)
204 .ok_or_else(|| anyhow!("ファイル情報が見つかりません: {}", clean_path.display()))?;
205
206 if inlined_files.contains(&clean_path) {
208 return Ok(String::new());
209 }
210
211 inlined_files.insert(clean_path.clone());
213
214 let mut result = nix_file.content.clone();
215
216 for import in nix_file.imports.iter().rev() {
218 let import_path = resolve_import_path(&import.path, &clean_path)?;
219
220 let inlined_content = inline_file_recursive(&import_path, file_contents, inlined_files)?;
222
223 result = result.replace(&import.full_import, &inlined_content);
226 }
227
228 inlined_files.remove(&clean_path);
230
231 Ok(result)
232}