Skip to main content

vs_core/
lib.rs

1//! Application orchestration for the `vs` CLI.
2
3mod app;
4mod error;
5mod models;
6mod plugin_source;
7mod registry_source;
8mod service;
9
10pub use app::App;
11pub use error::CoreError;
12pub use models::{
13    CurrentTool, InstalledVersion, MigrateSummary, PluginInfo, SelfUpgradeSummary, UninstallResult,
14    UseScope, VersionInfo,
15};
16
17#[cfg(test)]
18mod tests {
19    #[cfg(feature = "lua")]
20    use std::error::Error;
21    #[cfg(feature = "lua")]
22    use std::fs;
23
24    #[cfg(feature = "lua")]
25    use tempfile::TempDir;
26    #[cfg(feature = "lua")]
27    use vs_config::{
28        AppConfig, CacheConfig, HomeLayout, RegistryConfig, StorageConfig, write_app_config,
29    };
30    #[cfg(feature = "lua")]
31    use vs_plugin_api::PluginBackendKind;
32
33    #[cfg(feature = "lua")]
34    use crate::{App, UseScope};
35
36    #[cfg(feature = "lua")]
37    #[test]
38    fn use_tool_should_write_project_config() -> Result<(), Box<dyn Error>> {
39        let temp_dir = TempDir::new()?;
40        let home = temp_dir.path().join("home");
41        let cwd = temp_dir.path().join("project");
42        fs::create_dir_all(&cwd)?;
43        let app = App::new(
44            HomeLayout {
45                active_home: home,
46                migration_candidates: Vec::new(),
47            },
48            cwd.clone(),
49            Some(String::from("session")),
50        )?;
51
52        let source = temp_dir.path().join("nodejs-lua");
53        write_lua_fixture(&source);
54        app.add_plugin(
55            Some("nodejs"),
56            Some(source.display().to_string()),
57            Some(PluginBackendKind::Lua),
58            None,
59        )?;
60        app.install_plugin_version("nodejs", Some("20.11.1"))?;
61
62        app.use_tool("nodejs", "20.11.1", UseScope::Project, false)?;
63
64        let config = fs::read_to_string(cwd.join(".vs.toml"))?;
65        assert!(config.contains("nodejs = \"20.11.1\""));
66        Ok(())
67    }
68
69    #[cfg(feature = "lua")]
70    #[test]
71    fn available_plugins_should_bootstrap_registry_when_source_is_configured()
72    -> Result<(), Box<dyn Error>> {
73        let temp_dir = TempDir::new()?;
74        let home = temp_dir.path().join("home");
75        let cwd = temp_dir.path().join("project");
76        fs::create_dir_all(&cwd)?;
77        let registry_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
78            .join("../../fixtures/registry/index.json");
79
80        write_app_config(
81            &home,
82            &AppConfig {
83                proxy: Default::default(),
84                storage: Default::default(),
85                registry: RegistryConfig {
86                    address: registry_path.display().to_string(),
87                },
88                legacy_version_file: Default::default(),
89                cache: Default::default(),
90            },
91        )?;
92
93        let app = App::new(
94            HomeLayout {
95                active_home: home,
96                migration_candidates: Vec::new(),
97            },
98            cwd,
99            Some(String::from("session")),
100        )?;
101
102        let entries = app.available_plugins()?;
103        assert!(!entries.is_empty());
104        assert!(entries.iter().any(|entry| entry.name == "nodejs"));
105        Ok(())
106    }
107
108    #[cfg(feature = "lua")]
109    #[test]
110    fn available_plugins_should_fallback_to_cached_registry_when_refresh_fails()
111    -> Result<(), Box<dyn Error>> {
112        let temp_dir = TempDir::new()?;
113        let home = temp_dir.path().join("home");
114        let cwd = temp_dir.path().join("project");
115        fs::create_dir_all(&cwd)?;
116        let registry_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
117            .join("../../fixtures/registry/index.json");
118
119        write_app_config(
120            &home,
121            &AppConfig {
122                proxy: Default::default(),
123                storage: Default::default(),
124                registry: RegistryConfig {
125                    address: registry_path.display().to_string(),
126                },
127                legacy_version_file: Default::default(),
128                cache: Default::default(),
129            },
130        )?;
131
132        let app = App::new(
133            HomeLayout {
134                active_home: home.clone(),
135                migration_candidates: Vec::new(),
136            },
137            cwd.clone(),
138            Some(String::from("session")),
139        )?;
140        assert!(!app.available_plugins()?.is_empty());
141
142        write_app_config(
143            &home,
144            &AppConfig {
145                proxy: Default::default(),
146                storage: Default::default(),
147                registry: RegistryConfig {
148                    address: temp_dir
149                        .path()
150                        .join("missing/index.json")
151                        .display()
152                        .to_string(),
153                },
154                legacy_version_file: Default::default(),
155                cache: Default::default(),
156            },
157        )?;
158
159        let fallback = App::new(
160            HomeLayout {
161                active_home: home,
162                migration_candidates: Vec::new(),
163            },
164            cwd,
165            Some(String::from("session")),
166        )?;
167        let entries = fallback.available_plugins()?;
168        assert!(entries.iter().any(|entry| entry.name == "nodejs"));
169        Ok(())
170    }
171
172    #[cfg(feature = "lua")]
173    #[test]
174    fn add_plugin_should_fallback_to_cached_registry_when_refresh_fails()
175    -> Result<(), Box<dyn Error>> {
176        let temp_dir = TempDir::new()?;
177        let home = temp_dir.path().join("home");
178        let cwd = temp_dir.path().join("project");
179        fs::create_dir_all(&cwd)?;
180        let registry_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
181            .join("../../fixtures/registry/index.json");
182
183        write_app_config(
184            &home,
185            &AppConfig {
186                proxy: Default::default(),
187                storage: Default::default(),
188                registry: RegistryConfig {
189                    address: registry_path.display().to_string(),
190                },
191                legacy_version_file: Default::default(),
192                cache: Default::default(),
193            },
194        )?;
195
196        let app = App::new(
197            HomeLayout {
198                active_home: home.clone(),
199                migration_candidates: Vec::new(),
200            },
201            cwd.clone(),
202            Some(String::from("session")),
203        )?;
204        assert!(!app.available_plugins()?.is_empty());
205
206        write_app_config(
207            &home,
208            &AppConfig {
209                proxy: Default::default(),
210                storage: Default::default(),
211                registry: RegistryConfig {
212                    address: temp_dir
213                        .path()
214                        .join("missing/index.json")
215                        .display()
216                        .to_string(),
217                },
218                legacy_version_file: Default::default(),
219                cache: Default::default(),
220            },
221        )?;
222
223        let fallback = App::new(
224            HomeLayout {
225                active_home: home,
226                migration_candidates: Vec::new(),
227            },
228            cwd,
229            Some(String::from("session")),
230        )?;
231        let entry = fallback.add_plugin(Some("nodejs"), None, None, None)?;
232        assert_eq!(entry.name, "nodejs");
233        Ok(())
234    }
235
236    #[cfg(feature = "lua")]
237    #[test]
238    fn storage_sdk_path_should_redirect_runtime_installs() -> Result<(), Box<dyn Error>> {
239        let temp_dir = TempDir::new()?;
240        let home = temp_dir.path().join("home");
241        let storage_root = temp_dir.path().join("runtime-root");
242        let cwd = temp_dir.path().join("project");
243        let default_runtime_root = home.join("cache");
244        fs::create_dir_all(&cwd)?;
245
246        write_app_config(
247            &home,
248            &AppConfig {
249                storage: StorageConfig {
250                    sdk_path: storage_root.display().to_string(),
251                },
252                ..AppConfig::default()
253            },
254        )?;
255
256        let app = App::new(
257            HomeLayout {
258                active_home: home,
259                migration_candidates: Vec::new(),
260            },
261            cwd,
262            Some(String::from("session")),
263        )?;
264
265        let source = temp_dir.path().join("nodejs-lua");
266        write_lua_fixture(&source);
267        app.add_plugin(
268            Some("nodejs"),
269            Some(source.display().to_string()),
270            Some(PluginBackendKind::Lua),
271            None,
272        )?;
273        let installed = app.install_plugin_version("nodejs", Some("20.11.1"))?;
274
275        assert!(installed.install_dir.starts_with(&storage_root));
276        assert!(!installed.install_dir.starts_with(default_runtime_root));
277        Ok(())
278    }
279
280    #[cfg(feature = "lua")]
281    #[test]
282    fn project_tool_version_for_use_should_resolve_legacy_file() -> Result<(), Box<dyn Error>> {
283        let temp_dir = TempDir::new()?;
284        let home = temp_dir.path().join("home");
285        let cwd = temp_dir.path().join("project");
286        fs::create_dir_all(&cwd)?;
287        fs::write(cwd.join(".nvmrc"), "20.11.1\n")?;
288
289        let app = App::new(
290            HomeLayout {
291                active_home: home,
292                migration_candidates: Vec::new(),
293            },
294            cwd,
295            Some(String::from("session")),
296        )?;
297
298        let source = temp_dir.path().join("nodejs-lua");
299        write_lua_fixture(&source);
300        app.add_plugin(
301            Some("nodejs"),
302            Some(source.display().to_string()),
303            Some(PluginBackendKind::Lua),
304            None,
305        )?;
306
307        assert_eq!(
308            app.project_tool_version_for_use("nodejs")?,
309            Some(String::from("20.11.1"))
310        );
311        Ok(())
312    }
313
314    #[cfg(feature = "lua")]
315    #[test]
316    fn legacy_latest_installed_should_pick_the_newest_matching_runtime()
317    -> Result<(), Box<dyn Error>> {
318        let temp_dir = TempDir::new()?;
319        let home = temp_dir.path().join("home");
320        let cwd = temp_dir.path().join("project");
321        fs::create_dir_all(&cwd)?;
322        fs::write(cwd.join(".nvmrc"), "20\n")?;
323        write_app_config(
324            &home,
325            &AppConfig {
326                legacy_version_file: vs_config::LegacyVersionFileConfig {
327                    enable: true,
328                    strategy: String::from("latest_installed"),
329                },
330                ..AppConfig::default()
331            },
332        )?;
333
334        let app = App::new(
335            HomeLayout {
336                active_home: home,
337                migration_candidates: Vec::new(),
338            },
339            cwd,
340            Some(String::from("session")),
341        )?;
342
343        let source = temp_dir.path().join("nodejs-lua");
344        write_multi_version_lua_fixture(&source)?;
345        app.add_plugin(
346            Some("nodejs"),
347            Some(source.display().to_string()),
348            Some(PluginBackendKind::Lua),
349            None,
350        )?;
351        app.install_plugin_version("nodejs", Some("20.9.0"))?;
352        app.install_plugin_version("nodejs", Some("20.11.1"))?;
353
354        assert_eq!(
355            app.project_tool_version_for_use("nodejs")?,
356            Some(String::from("20.11.1"))
357        );
358        Ok(())
359    }
360
361    #[cfg(feature = "lua")]
362    #[test]
363    fn available_hook_cache_should_return_cached_versions_when_enabled()
364    -> Result<(), Box<dyn Error>> {
365        let temp_dir = TempDir::new()?;
366        let home = temp_dir.path().join("home");
367        let cwd = temp_dir.path().join("project");
368        fs::create_dir_all(&cwd)?;
369        write_app_config(
370            &home,
371            &AppConfig {
372                cache: CacheConfig {
373                    available_hook_duration: String::from("12h"),
374                },
375                ..AppConfig::default()
376            },
377        )?;
378
379        let app = App::new(
380            HomeLayout {
381                active_home: home,
382                migration_candidates: Vec::new(),
383            },
384            cwd,
385            Some(String::from("session")),
386        )?;
387
388        let source = temp_dir.path().join("nodejs-lua");
389        write_multi_version_lua_fixture(&source)?;
390        app.add_plugin(
391            Some("nodejs"),
392            Some(source.display().to_string()),
393            Some(PluginBackendKind::Lua),
394            None,
395        )?;
396
397        let versions = app.search_versions("nodejs", &[])?;
398        fs::remove_dir_all(&source)?;
399        let cached = app.search_versions("nodejs", &[])?;
400
401        assert_eq!(versions, cached);
402        Ok(())
403    }
404
405    #[cfg(feature = "lua")]
406    fn write_lua_fixture(root: &std::path::Path) {
407        if let Err(error) = fs::create_dir_all(root.join("hooks")) {
408            panic!("failed to create hooks directory: {error}");
409        }
410        if let Err(error) = fs::create_dir_all(root.join("packages/20.11.1/bin")) {
411            panic!("failed to create package directory: {error}");
412        }
413        fs::write(
414            root.join("metadata.lua"),
415            "PLUGIN = {}\nPLUGIN.name = 'nodejs'\nPLUGIN.version = '0.1.0'\nPLUGIN.legacyFilenames = { '.nvmrc' }\n",
416        )
417        .unwrap_or_else(|error| panic!("failed to write metadata fixture: {error}"));
418        fs::write(
419            root.join("hooks/available.lua"),
420            "function PLUGIN:Available(ctx)\n  return { { version = '20.11.1' } }\nend\n",
421        )
422        .unwrap_or_else(|error| panic!("failed to write available fixture: {error}"));
423        fs::write(
424            root.join("hooks/pre_install.lua"),
425            "function PLUGIN:PreInstall(ctx)\n  return { version = '20.11.1', url = 'packages/20.11.1' }\nend\n",
426        )
427        .unwrap_or_else(|error| panic!("failed to write pre_install fixture: {error}"));
428        fs::write(
429            root.join("hooks/env_keys.lua"),
430            "function PLUGIN:EnvKeys(ctx)\n  return { { key = 'NODEJS_HOME', value = ctx.path }, { key = 'PATH', value = ctx.path .. '/bin' } }\nend\n",
431        )
432        .unwrap_or_else(|error| panic!("failed to write env_keys fixture: {error}"));
433    }
434
435    #[cfg(feature = "lua")]
436    fn write_multi_version_lua_fixture(root: &std::path::Path) -> Result<(), Box<dyn Error>> {
437        fs::create_dir_all(root.join("hooks"))?;
438        for version in ["20.9.0", "20.11.1"] {
439            fs::create_dir_all(root.join(format!("packages/{version}/bin")))?;
440        }
441        fs::write(
442            root.join("metadata.lua"),
443            "PLUGIN = {}\nPLUGIN.name = 'nodejs'\nPLUGIN.version = '0.1.0'\nPLUGIN.legacyFilenames = { '.nvmrc' }\n",
444        )?;
445        fs::write(
446            root.join("hooks/available.lua"),
447            "function PLUGIN:Available(ctx)\n  return { { version = '20.11.1' }, { version = '20.9.0' } }\nend\n",
448        )?;
449        fs::write(
450            root.join("hooks/pre_install.lua"),
451            "function PLUGIN:PreInstall(ctx)\n  return { version = ctx.version, url = 'packages/' .. ctx.version }\nend\n",
452        )?;
453        fs::write(
454            root.join("hooks/env_keys.lua"),
455            "function PLUGIN:EnvKeys(ctx)\n  return { { key = 'PATH', value = ctx.path .. '/bin' } }\nend\n",
456        )?;
457        Ok(())
458    }
459}