1mod 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}