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