nix_bundler/
lib.rs

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/// Nixファイルのインポート情報を表す構造体
10#[derive(Clone)]
11struct NixImport {
12    /// インポートパス(相対パスまたは絶対パス)
13    path: PathBuf,
14    /// ソースコード内での位置(行、列)
15    _position: (usize, usize),
16    /// インポート文の全体(置換用)
17    full_import: String,
18}
19
20/// Nixファイルの解析結果を表す構造体
21#[derive(Clone)]
22struct NixFile {
23    /// ファイルパス
24    _path: PathBuf,
25    /// ファイルの内容
26    content: String,
27    /// インポート情報のリスト
28    imports: Vec<NixImport>,
29}
30
31/// Nixファイルをバンドルする関数
32pub fn bundle_nix_files(entry_point: &Path) -> Result<String> {
33    // 処理済みファイルを追跡するためのセット
34    let mut processed_files = HashSet::new();
35    // ファイルパスとその内容のマップ
36    let mut file_contents = HashMap::new();
37
38    // エントリーポイントから再帰的に依存関係を解析
39    process_nix_file(entry_point, &mut processed_files, &mut file_contents)?;
40
41    // エントリーポイントの絶対パスを取得
42    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    // クリーンなパスに変換
49    let clean_entry_path = PathBuf::from(clean(abs_entry_path.to_string_lossy().as_ref()));
50
51    // エントリーポイントが存在するか確認
52    if !file_contents.contains_key(&clean_entry_path) {
53        return Err(anyhow!(
54            "エントリーポイントの内容が見つかりません: {}",
55            clean_entry_path.display()
56        ));
57    }
58
59    // インライン化された内容を生成
60    let bundled_content = inline_imports(&clean_entry_path, &file_contents)?;
61
62    Ok(bundled_content)
63}
64
65/// Nixファイルを解析して依存関係を処理する関数
66fn process_nix_file(
67    file_path: &Path,
68    processed_files: &mut HashSet<PathBuf>,
69    file_contents: &mut HashMap<PathBuf, NixFile>,
70) -> Result<()> {
71    // 絶対パスに変換
72    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    // クリーンなパスに変換
79    let clean_path = PathBuf::from(clean(abs_path.to_string_lossy().as_ref()));
80
81    // すでに処理済みの場合はスキップ
82    if processed_files.contains(&clean_path) {
83        return Ok(());
84    }
85
86    // ファイルが存在するか確認
87    if !clean_path.exists() {
88        return Err(anyhow!("ファイルが存在しません: {}", clean_path.display()));
89    }
90
91    // ファイルの内容を読み込む
92    let content = fs::read_to_string(&clean_path)
93        .with_context(|| format!("ファイルの読み込みに失敗しました: {}", clean_path.display()))?;
94
95    // インポート文を解析
96    let imports = parse_imports(&content, &clean_path)?;
97
98    // ファイル情報を保存
99    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    // 処理済みとしてマーク
107    processed_files.insert(clean_path.clone());
108
109    // 依存ファイルを再帰的に処理
110    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
118/// インポートパスを解決する関数
119fn resolve_import_path(import_path: &Path, current_file: &Path) -> Result<PathBuf> {
120    // インポートパスが絶対パスの場合はそのまま返す
121    if import_path.is_absolute() {
122        return Ok(import_path.to_path_buf());
123    }
124
125    // 相対パスの場合は、現在のファイルのディレクトリを基準に解決
126    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
136/// Nixファイル内のインポート文を解析する関数
137fn parse_imports(content: &str, file_path: &Path) -> Result<Vec<NixImport>> {
138    let mut imports = Vec::new();
139
140    // importステートメントを検出する正規表現
141    // 注意: これは簡易的な実装で、すべてのケースをカバーしていない可能性があります
142    let import_regex = Regex::new(r#"import\s+(?:(?:"([^"]+)")|(?:'([^']+)')|([^\s;]+))"#)?;
143
144    // 各行を処理
145    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
176/// インポートをインライン化する関数
177fn inline_imports(entry_point: &Path, file_contents: &HashMap<PathBuf, NixFile>) -> Result<String> {
178    // インライン化済みファイルを追跡
179    let mut inlined_files = HashSet::new();
180
181    // 再帰的にインライン化
182    inline_file_recursive(entry_point, file_contents, &mut inlined_files)
183}
184
185/// ファイルを再帰的にインライン化する関数
186fn inline_file_recursive(
187    file_path: &Path,
188    file_contents: &HashMap<PathBuf, NixFile>,
189    inlined_files: &mut HashSet<PathBuf>,
190) -> Result<String> {
191    // 絶対パスに変換
192    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    // クリーンなパスに変換
199    let clean_path = PathBuf::from(clean(abs_path.to_string_lossy().as_ref()));
200
201    // ファイル情報を取得
202    let nix_file = file_contents
203        .get(&clean_path)
204        .ok_or_else(|| anyhow!("ファイル情報が見つかりません: {}", clean_path.display()))?;
205
206    // すでにインライン化済みの場合は空文字列を返す(循環参照を防ぐ)
207    if inlined_files.contains(&clean_path) {
208        return Ok(String::new());
209    }
210
211    // インライン化済みとしてマーク
212    inlined_files.insert(clean_path.clone());
213
214    let mut result = nix_file.content.clone();
215
216    // インポートを逆順に処理(テキスト位置が変わらないように)
217    for import in nix_file.imports.iter().rev() {
218        let import_path = resolve_import_path(&import.path, &clean_path)?;
219
220        // インポートファイルをインライン化
221        let inlined_content = inline_file_recursive(&import_path, file_contents, inlined_files)?;
222
223        // インポート文を置換
224        // 注意: これは簡易的な実装で、複雑なケースでは問題が発生する可能性があります
225        result = result.replace(&import.full_import, &inlined_content);
226    }
227
228    // インライン化済みとしてマークを解除(他のパスからの参照のため)
229    inlined_files.remove(&clean_path);
230
231    Ok(result)
232}