diff --git a/crates/volta-core/src/error/kind.rs b/crates/volta-core/src/error/kind.rs index 6add88efa..b61acf7f5 100644 --- a/crates/volta-core/src/error/kind.rs +++ b/crates/volta-core/src/error/kind.rs @@ -530,6 +530,11 @@ pub enum ErrorKind { YarnVersionNotFound { matching: String, }, + + /// Thrown when reading a `.node_version` file fails. + NodeVersionSpecReadError { + file: PathBuf, + }, } impl fmt::Display for ErrorKind { diff --git a/crates/volta-core/src/project/mod.rs b/crates/volta-core/src/project/mod.rs index 745739db0..956d06382 100644 --- a/crates/volta-core/src/project/mod.rs +++ b/crates/volta-core/src/project/mod.rs @@ -20,7 +20,7 @@ mod serial; #[cfg(test)] mod tests; -use serial::{update_manifest, Manifest, ManifestKey}; +use serial::{update_manifest, Manifest, ManifestKey, NodeVersionSpec}; /// A lazily loaded Project pub struct LazyProject { @@ -66,16 +66,21 @@ impl Project { /// Will search ancestors to find a `package.json` and use that as the root of the project fn for_dir(base_dir: PathBuf) -> Fallible> { match find_closest_root(base_dir) { - Some(mut project) => { - project.push("package.json"); - Self::from_file(project).map(Some) + Some(mut root) => { + let node_version = NodeVersionSpec::from_file(&root.join(".node_version"))?; + + root.push("package.json"); + Self::from_manifest_file(root, node_version).map(Some) } None => Ok(None), } } /// Creates a Project instance from the given package manifest file (`package.json`) - fn from_file(manifest_file: PathBuf) -> Fallible { + fn from_manifest_file( + manifest_file: PathBuf, + node_version_spec: Option, // TODO: merge with manifest? + ) -> Fallible { let manifest = Manifest::from_file(&manifest_file)?; let mut dependencies: ChainMap = manifest.dependency_maps.collect(); let mut workspace_manifests = IndexSet::new(); diff --git a/crates/volta-core/src/project/serial.rs b/crates/volta-core/src/project/serial.rs index 398731796..78e6fe8a6 100644 --- a/crates/volta-core/src/project/serial.rs +++ b/crates/volta-core/src/project/serial.rs @@ -1,11 +1,11 @@ use std::collections::HashMap; -use std::fmt; use std::fs::{read_to_string, File}; use std::io::Write; use std::path::{Path, PathBuf}; +use std::{fmt, io}; use super::PartialPlatform; -use crate::error::{Context, ErrorKind, Fallible}; +use crate::error::{Context, ErrorKind, Fallible, VoltaError}; use crate::version::parse_version; use dunce::canonicalize; use node_semver::Version; @@ -138,6 +138,11 @@ pub(super) fn update_manifest( Ok(()) } +/// Representation of a `package.json` file as it appears on disk. +/// +/// This may or may not have literally *any* of the keys we care about. It is +/// only used for reading the `package.json` from disk, never for further +/// manipulation. See `Manifest` for the version we work with directly. #[derive(Deserialize)] struct RawManifest { dependencies: Option>, @@ -192,3 +197,23 @@ impl ToolchainSpec { Ok((platform, self.extends)) } } + +/// A Node version specifier in a `.node_version` file. Per the spec, we only +/// support versions with an optional leading `v` and then a full, i.e. at least +/// three-place, version (`1.2.3`, not `1` or `1.2`). +#[cfg_attr(test, derive(Debug))] +pub(super) struct NodeVersionSpec(Version); + +impl NodeVersionSpec { + pub(super) fn from_file(path: &Path) -> Fallible> { + match read_to_string(path) { + Ok(s) => parse_version(s).map(NodeVersionSpec).map(Some), + Err(e) => match e.kind() { + io::ErrorKind::NotFound => Ok(None), + _ => Err(e).with_context(|| ErrorKind::NodeVersionSpecReadError { + file: path.to_owned(), + }), + }, + } + } +} diff --git a/crates/volta-core/src/tool/node/mod.rs b/crates/volta-core/src/tool/node/mod.rs index c9788be59..3eff65ade 100644 --- a/crates/volta-core/src/tool/node/mod.rs +++ b/crates/volta-core/src/tool/node/mod.rs @@ -255,7 +255,10 @@ impl Tool for Node { if session.project()?.is_some() { let node_version = self.ensure_fetched(session)?; - // Note: We know this will succeed, since we checked above + // Note: We know this will succeed, since we checked above. We don't + // just use `if let Some(project) = session.project_mut()` because + // that results in an attempt to borrow session twice when we call + // `ensure_fetched` above. let project = session.project_mut()?.unwrap(); project.pin_node(self.version.clone())?;