Browse Source

experimental: support `package.json#engines` for `use` and `install` (#839)

Co-authored-by: Gal Schlezinger <gal@spitfire.co.il>
remotes/origin/list-filter
Amit Dahan 2 years ago committed by GitHub
parent
commit
97be792a44
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      .changeset/warm-parrots-drive.md
  2. 91
      docs/commands.md
  3. 82
      e2e/__snapshots__/basic.test.ts.snap
  4. 28
      e2e/basic.test.ts
  5. 1
      e2e/env.test.ts
  6. 2
      e2e/shellcode/shells.ts
  7. 4
      e2e/shellcode/shells/cmdEnv.ts
  8. 1
      src/commands/env.rs
  9. 17
      src/config.rs
  10. 1
      src/main.rs
  11. 19
      src/package_json.rs
  12. 3
      src/user_version.rs
  13. 2
      src/user_version_reader.rs
  14. 59
      src/version_files.rs

5
.changeset/warm-parrots-drive.md

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
---
"fnm": minor
---
Support resolving `engines.node` field via experimental `--resolve-engines` flag

91
docs/commands.md

@ -59,6 +59,13 @@ Options: @@ -59,6 +59,13 @@ Options:
[env: FNM_COREPACK_ENABLED]
--resolve-engines
Resolve `engines.node` field in `package.json` whenever a `.node-version` or `.nvmrc` file is not present.
Experimental: This feature is subject to change.
Note: `engines.node` can be any semver range, with the latest satisfying version being resolved.
[env: FNM_RESOLVE_ENGINES]
-h, --help
Print help (see a summary with '-h')
@ -112,6 +119,13 @@ Options: @@ -112,6 +119,13 @@ Options:
[env: FNM_COREPACK_ENABLED]
--resolve-engines
Resolve `engines.node` field in `package.json` whenever a `.node-version` or `.nvmrc` file is not present.
Experimental: This feature is subject to change.
Note: `engines.node` can be any semver range, with the latest satisfying version being resolved.
[env: FNM_RESOLVE_ENGINES]
-h, --help
Print help (see a summary with '-h')
```
@ -162,6 +176,13 @@ Options: @@ -162,6 +176,13 @@ Options:
[env: FNM_COREPACK_ENABLED]
--resolve-engines
Resolve `engines.node` field in `package.json` whenever a `.node-version` or `.nvmrc` file is not present.
Experimental: This feature is subject to change.
Note: `engines.node` can be any semver range, with the latest satisfying version being resolved.
[env: FNM_RESOLVE_ENGINES]
-h, --help
Print help (see a summary with '-h')
```
@ -222,6 +243,13 @@ Options: @@ -222,6 +243,13 @@ Options:
[env: FNM_COREPACK_ENABLED]
--resolve-engines
Resolve `engines.node` field in `package.json` whenever a `.node-version` or `.nvmrc` file is not present.
Experimental: This feature is subject to change.
Note: `engines.node` can be any semver range, with the latest satisfying version being resolved.
[env: FNM_RESOLVE_ENGINES]
-h, --help
Print help (see a summary with '-h')
```
@ -282,6 +310,13 @@ Options: @@ -282,6 +310,13 @@ Options:
[env: FNM_COREPACK_ENABLED]
--resolve-engines
Resolve `engines.node` field in `package.json` whenever a `.node-version` or `.nvmrc` file is not present.
Experimental: This feature is subject to change.
Note: `engines.node` can be any semver range, with the latest satisfying version being resolved.
[env: FNM_RESOLVE_ENGINES]
-h, --help
Print help (see a summary with '-h')
```
@ -347,6 +382,13 @@ Options: @@ -347,6 +382,13 @@ Options:
[env: FNM_COREPACK_ENABLED]
--resolve-engines
Resolve `engines.node` field in `package.json` whenever a `.node-version` or `.nvmrc` file is not present.
Experimental: This feature is subject to change.
Note: `engines.node` can be any semver range, with the latest satisfying version being resolved.
[env: FNM_RESOLVE_ENGINES]
-h, --help
Print help (see a summary with '-h')
```
@ -402,6 +444,13 @@ Options: @@ -402,6 +444,13 @@ Options:
[env: FNM_COREPACK_ENABLED]
--resolve-engines
Resolve `engines.node` field in `package.json` whenever a `.node-version` or `.nvmrc` file is not present.
Experimental: This feature is subject to change.
Note: `engines.node` can be any semver range, with the latest satisfying version being resolved.
[env: FNM_RESOLVE_ENGINES]
-h, --help
Print help (see a summary with '-h')
```
@ -459,6 +508,13 @@ Options: @@ -459,6 +508,13 @@ Options:
[env: FNM_COREPACK_ENABLED]
--resolve-engines
Resolve `engines.node` field in `package.json` whenever a `.node-version` or `.nvmrc` file is not present.
Experimental: This feature is subject to change.
Note: `engines.node` can be any semver range, with the latest satisfying version being resolved.
[env: FNM_RESOLVE_ENGINES]
-h, --help
Print help (see a summary with '-h')
```
@ -513,6 +569,13 @@ Options: @@ -513,6 +569,13 @@ Options:
[env: FNM_COREPACK_ENABLED]
--resolve-engines
Resolve `engines.node` field in `package.json` whenever a `.node-version` or `.nvmrc` file is not present.
Experimental: This feature is subject to change.
Note: `engines.node` can be any semver range, with the latest satisfying version being resolved.
[env: FNM_RESOLVE_ENGINES]
-h, --help
Print help (see a summary with '-h')
```
@ -569,6 +632,13 @@ Options: @@ -569,6 +632,13 @@ Options:
[env: FNM_COREPACK_ENABLED]
--resolve-engines
Resolve `engines.node` field in `package.json` whenever a `.node-version` or `.nvmrc` file is not present.
Experimental: This feature is subject to change.
Note: `engines.node` can be any semver range, with the latest satisfying version being resolved.
[env: FNM_RESOLVE_ENGINES]
-h, --help
Print help (see a summary with '-h')
```
@ -619,6 +689,13 @@ Options: @@ -619,6 +689,13 @@ Options:
[env: FNM_COREPACK_ENABLED]
--resolve-engines
Resolve `engines.node` field in `package.json` whenever a `.node-version` or `.nvmrc` file is not present.
Experimental: This feature is subject to change.
Note: `engines.node` can be any semver range, with the latest satisfying version being resolved.
[env: FNM_RESOLVE_ENGINES]
-h, --help
Print help (see a summary with '-h')
```
@ -681,6 +758,13 @@ Options: @@ -681,6 +758,13 @@ Options:
[env: FNM_COREPACK_ENABLED]
--resolve-engines
Resolve `engines.node` field in `package.json` whenever a `.node-version` or `.nvmrc` file is not present.
Experimental: This feature is subject to change.
Note: `engines.node` can be any semver range, with the latest satisfying version being resolved.
[env: FNM_RESOLVE_ENGINES]
-h, --help
Print help (see a summary with '-h')
```
@ -737,6 +821,13 @@ Options: @@ -737,6 +821,13 @@ Options:
[env: FNM_COREPACK_ENABLED]
--resolve-engines
Resolve `engines.node` field in `package.json` whenever a `.node-version` or `.nvmrc` file is not present.
Experimental: This feature is subject to change.
Note: `engines.node` can be any semver range, with the latest satisfying version being resolved.
[env: FNM_RESOLVE_ENGINES]
-h, --help
Print help (see a summary with '-h')
```

82
e2e/__snapshots__/basic.test.ts.snap

@ -42,6 +42,28 @@ if [ "$(node --version)" != "v8.11.3" ]; then @@ -42,6 +42,28 @@ if [ "$(node --version)" != "v8.11.3" ]; then
fi"
`;
exports[`Bash package.json engines.node with semver range: Bash 1`] = `
"set -e
eval "$(fnm env --resolve-engines)"
fnm install
fnm use
if [ "$(node --version)" != "v6.17.0" ]; then
echo "Expected node version to be v6.17.0. Got $(node --version)"
exit 1
fi"
`;
exports[`Bash package.json engines.node: Bash 1`] = `
"set -e
eval "$(fnm env --resolve-engines)"
fnm install
fnm use
if [ "$(node --version)" != "v8.11.3" ]; then
echo "Expected node version to be v8.11.3. Got $(node --version)"
exit 1
fi"
`;
exports[`Bash resolves partial semver: Bash 1`] = `
"set -e
eval "$(fnm env)"
@ -118,6 +140,28 @@ if test "$____test____" != "v8.11.3" @@ -118,6 +140,28 @@ if test "$____test____" != "v8.11.3"
end"
`;
exports[`Fish package.json engines.node with semver range: Fish 1`] = `
"fnm env --resolve-engines | source
fnm install
fnm use
set ____test____ (node --version)
if test "$____test____" != "v6.17.0"
echo "Expected node version to be v6.17.0. Got $____test____"
exit 1
end"
`;
exports[`Fish package.json engines.node: Fish 1`] = `
"fnm env --resolve-engines | source
fnm install
fnm use
set ____test____ (node --version)
if test "$____test____" != "v8.11.3"
echo "Expected node version to be v8.11.3. Got $____test____"
exit 1
end"
`;
exports[`Fish resolves partial semver: Fish 1`] = `
"fnm env | source
fnm install 6
@ -182,6 +226,22 @@ fnm use v8.11.3 @@ -182,6 +226,22 @@ fnm use v8.11.3
if ( "$(node --version)" -ne "v8.11.3" ) { echo "Expected node version to be v8.11.3. Got $(node --version)"; exit 1 }"
`;
exports[`PowerShell package.json engines.node with semver range: PowerShell 1`] = `
"$ErrorActionPreference = "Stop"
fnm env --resolve-engines | Out-String | Invoke-Expression
fnm install
fnm use
if ( "$(node --version)" -ne "v6.17.0" ) { echo "Expected node version to be v6.17.0. Got $(node --version)"; exit 1 }"
`;
exports[`PowerShell package.json engines.node: PowerShell 1`] = `
"$ErrorActionPreference = "Stop"
fnm env --resolve-engines | Out-String | Invoke-Expression
fnm install
fnm use
if ( "$(node --version)" -ne "v8.11.3" ) { echo "Expected node version to be v8.11.3. Got $(node --version)"; exit 1 }"
`;
exports[`PowerShell resolves partial semver: PowerShell 1`] = `
"$ErrorActionPreference = "Stop"
fnm env | Out-String | Invoke-Expression
@ -249,6 +309,28 @@ if [ "$(node --version)" != "v8.11.3" ]; then @@ -249,6 +309,28 @@ if [ "$(node --version)" != "v8.11.3" ]; then
fi"
`;
exports[`Zsh package.json engines.node with semver range: Zsh 1`] = `
"set -e
eval "$(fnm env --resolve-engines)"
fnm install
fnm use
if [ "$(node --version)" != "v6.17.0" ]; then
echo "Expected node version to be v6.17.0. Got $(node --version)"
exit 1
fi"
`;
exports[`Zsh package.json engines.node: Zsh 1`] = `
"set -e
eval "$(fnm env --resolve-engines)"
fnm install
fnm use
if [ "$(node --version)" != "v8.11.3" ]; then
echo "Expected node version to be v8.11.3. Got $(node --version)"
exit 1
fi"
`;
exports[`Zsh resolves partial semver: Zsh 1`] = `
"set -e
eval "$(fnm env)"

28
e2e/basic.test.ts

@ -40,6 +40,34 @@ for (const shell of [Bash, Zsh, Fish, PowerShell, WinCmd]) { @@ -40,6 +40,34 @@ for (const shell of [Bash, Zsh, Fish, PowerShell, WinCmd]) {
.execute(shell)
})
test(`package.json engines.node`, async () => {
await writeFile(
join(testCwd(), "package.json"),
JSON.stringify({ engines: { node: "8.11.3" } })
)
await script(shell)
.then(shell.env({ resolveEngines: true }))
.then(shell.call("fnm", ["install"]))
.then(shell.call("fnm", ["use"]))
.then(testNodeVersion(shell, "v8.11.3"))
.takeSnapshot(shell)
.execute(shell)
})
test(`package.json engines.node with semver range`, async () => {
await writeFile(
join(testCwd(), "package.json"),
JSON.stringify({ engines: { node: "^6 < 6.17.1" } })
)
await script(shell)
.then(shell.env({ resolveEngines: true }))
.then(shell.call("fnm", ["install"]))
.then(shell.call("fnm", ["use"]))
.then(testNodeVersion(shell, "v6.17.0"))
.takeSnapshot(shell)
.execute(shell)
})
test(`use on cd`, async () => {
await mkdir(join(testCwd(), "subdir"), { recursive: true })
await writeFile(join(testCwd(), "subdir", ".node-version"), "v12.22.12")

1
e2e/env.test.ts

@ -26,6 +26,7 @@ for (const shell of [Bash, Zsh, Fish, PowerShell, WinCmd]) { @@ -26,6 +26,7 @@ for (const shell of [Bash, Zsh, Fish, PowerShell, WinCmd]) {
FNM_LOGLEVEL: "info",
FNM_MULTISHELL_PATH: expect.any(String),
FNM_NODE_DIST_MIRROR: expect.any(String),
FNM_RESOLVE_ENGINES: "false",
FNM_COREPACK_ENABLED: "false",
FNM_VERSION_FILE_STRATEGY: "local",
})

2
e2e/shellcode/shells.ts

@ -61,7 +61,7 @@ export const PowerShell = { @@ -61,7 +61,7 @@ export const PowerShell = {
...define<Shell>({
binaryName: () => "pwsh",
forceFile: ".ps1",
currentlySupported: () => true,
currentlySupported: () => process.platform === "win32",
name: () => "PowerShell",
launchArgs: () => ["-NoProfile"],
escapeText: (x) => x,

4
e2e/shellcode/shells/cmdEnv.ts

@ -4,16 +4,18 @@ type EnvConfig = { @@ -4,16 +4,18 @@ type EnvConfig = {
useOnCd: boolean
logLevel: string
corepackEnabled: boolean
resolveEngines: boolean
}
export type HasEnv = { env(cfg: Partial<EnvConfig>): ScriptLine }
function stringify(envConfig: Partial<EnvConfig> = {}) {
const { useOnCd, logLevel, corepackEnabled } = envConfig
const { useOnCd, logLevel, corepackEnabled, resolveEngines } = envConfig
return [
`fnm env`,
useOnCd && "--use-on-cd",
logLevel && `--log-level=${logLevel}`,
corepackEnabled && "--corepack-enabled",
resolveEngines && `--resolve-engines`,
]
.filter(Boolean)
.join(" ")

1
src/commands/env.rs

@ -94,6 +94,7 @@ impl Command for Env { @@ -94,6 +94,7 @@ impl Command for Env {
"FNM_COREPACK_ENABLED",
config.corepack_enabled().to_string(),
),
("FNM_RESOLVE_ENGINES", config.resolve_engines().to_string()),
("FNM_ARCH", config.arch.to_string()),
]);

17
src/config.rs

@ -75,6 +75,18 @@ pub struct FnmConfig { @@ -75,6 +75,18 @@ pub struct FnmConfig {
hide_env_values = true
)]
corepack_enabled: bool,
/// Resolve `engines.node` field in `package.json` whenever a `.node-version` or `.nvmrc` file is not present.
/// Experimental: This feature is subject to change.
/// Note: `engines.node` can be any semver range, with the latest satisfying version being resolved.
#[clap(
long,
env = "FNM_RESOLVE_ENGINES",
global = true,
hide_env_values = true,
verbatim_doc_comment
)]
resolve_engines: bool,
}
impl Default for FnmConfig {
@ -87,6 +99,7 @@ impl Default for FnmConfig { @@ -87,6 +99,7 @@ impl Default for FnmConfig {
arch: Arch::default(),
version_file_strategy: VersionFileStrategy::default(),
corepack_enabled: false,
resolve_engines: false,
}
}
}
@ -100,6 +113,10 @@ impl FnmConfig { @@ -100,6 +113,10 @@ impl FnmConfig {
self.corepack_enabled
}
pub fn resolve_engines(&self) -> bool {
self.resolve_engines
}
pub fn multishell_path(&self) -> Option<&std::path::Path> {
match &self.multishell_path {
None => None,

1
src/main.rs

@ -20,6 +20,7 @@ mod fs; @@ -20,6 +20,7 @@ mod fs;
mod http;
mod installed_versions;
mod lts;
mod package_json;
mod path_ext;
mod remote_node_index;
mod shell;

19
src/package_json.rs

@ -0,0 +1,19 @@ @@ -0,0 +1,19 @@
use serde::Deserialize;
#[derive(Debug, Deserialize, Default)]
struct EnginesField {
node: Option<node_semver::Range>,
}
#[derive(Debug, Deserialize, Default)]
pub struct PackageJson {
engines: Option<EnginesField>,
}
impl PackageJson {
pub fn node_range(&self) -> Option<&node_semver::Range> {
self.engines
.as_ref()
.and_then(|engines| engines.node.as_ref())
}
}

3
src/user_version.rs

@ -5,6 +5,7 @@ use std::str::FromStr; @@ -5,6 +5,7 @@ use std::str::FromStr;
pub enum UserVersion {
OnlyMajor(u64),
MajorMinor(u64, u64),
SemverRange(node_semver::Range),
Full(Version),
}
@ -41,6 +42,7 @@ impl UserVersion { @@ -41,6 +42,7 @@ impl UserVersion {
}
}
}
(Self::SemverRange(range), Version::Semver(semver)) => semver.satisfies(range),
(_, Version::Bypassed | Version::Lts(_) | Version::Alias(_) | Version::Latest) => false,
(Self::OnlyMajor(major), Version::Semver(other)) => *major == other.major,
(Self::MajorMinor(major, minor), Version::Semver(other)) => {
@ -59,6 +61,7 @@ impl std::fmt::Display for UserVersion { @@ -59,6 +61,7 @@ impl std::fmt::Display for UserVersion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Full(x) => x.fmt(f),
Self::SemverRange(x) => x.fmt(f),
Self::OnlyMajor(major) => write!(f, "v{major}.x.x"),
Self::MajorMinor(major, minor) => write!(f, "v{major}.{minor}.x"),
}

2
src/user_version_reader.rs

@ -14,7 +14,7 @@ impl UserVersionReader { @@ -14,7 +14,7 @@ impl UserVersionReader {
pub fn into_user_version(self, config: &FnmConfig) -> Option<UserVersion> {
match self {
Self::Direct(uv) => Some(uv),
Self::Path(pathbuf) if pathbuf.is_file() => get_user_version_for_file(pathbuf),
Self::Path(pathbuf) if pathbuf.is_file() => get_user_version_for_file(pathbuf, config),
Self::Path(pathbuf) => get_user_version_for_directory(pathbuf, config),
}
}

59
src/version_files.rs

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
use crate::config::FnmConfig;
use crate::default_version;
use crate::package_json::PackageJson;
use crate::user_version::UserVersion;
use crate::version_file_strategy::VersionFileStrategy;
use encoding_rs_io::DecodeReaderBytes;
@ -8,28 +9,30 @@ use std::io::Read; @@ -8,28 +9,30 @@ use std::io::Read;
use std::path::Path;
use std::str::FromStr;
const PATH_PARTS: [&str; 2] = [".nvmrc", ".node-version"];
const PATH_PARTS: [&str; 3] = [".nvmrc", ".node-version", "package.json"];
pub fn get_user_version_for_directory(
path: impl AsRef<Path>,
config: &FnmConfig,
) -> Option<UserVersion> {
match config.version_file_strategy() {
VersionFileStrategy::Local => get_user_version_for_single_directory(path),
VersionFileStrategy::Recursive => {
get_user_version_for_directory_recursive(path).or_else(|| {
VersionFileStrategy::Local => get_user_version_for_single_directory(path, config),
VersionFileStrategy::Recursive => get_user_version_for_directory_recursive(path, config)
.or_else(|| {
info!("Did not find anything recursively. Falling back to default alias.");
default_version::find_default_version(config).map(UserVersion::Full)
})
}
}),
}
}
fn get_user_version_for_directory_recursive(path: impl AsRef<Path>) -> Option<UserVersion> {
fn get_user_version_for_directory_recursive(
path: impl AsRef<Path>,
config: &FnmConfig,
) -> Option<UserVersion> {
let mut current_path = Some(path.as_ref());
while let Some(child_path) = current_path {
if let Some(version) = get_user_version_for_single_directory(child_path) {
if let Some(version) = get_user_version_for_single_directory(child_path, config) {
return Some(version);
}
@ -39,7 +42,10 @@ fn get_user_version_for_directory_recursive(path: impl AsRef<Path>) -> Option<Us @@ -39,7 +42,10 @@ fn get_user_version_for_directory_recursive(path: impl AsRef<Path>) -> Option<Us
None
}
pub fn get_user_version_for_single_directory(path: impl AsRef<Path>) -> Option<UserVersion> {
fn get_user_version_for_single_directory(
path: impl AsRef<Path>,
config: &FnmConfig,
) -> Option<UserVersion> {
let path = path.as_ref();
for path_part in &PATH_PARTS {
@ -49,7 +55,7 @@ pub fn get_user_version_for_single_directory(path: impl AsRef<Path>) -> Option<U @@ -49,7 +55,7 @@ pub fn get_user_version_for_single_directory(path: impl AsRef<Path>) -> Option<U
new_path.display(),
new_path.exists()
);
if let Some(version) = get_user_version_for_file(&new_path) {
if let Some(version) = get_user_version_for_file(&new_path, config) {
return Some(version);
}
}
@ -57,22 +63,43 @@ pub fn get_user_version_for_single_directory(path: impl AsRef<Path>) -> Option<U @@ -57,22 +63,43 @@ pub fn get_user_version_for_single_directory(path: impl AsRef<Path>) -> Option<U
None
}
pub fn get_user_version_for_file(path: impl AsRef<Path>) -> Option<UserVersion> {
pub fn get_user_version_for_file(
path: impl AsRef<Path>,
config: &FnmConfig,
) -> Option<UserVersion> {
let is_pkg_json = match path.as_ref().file_name() {
Some(name) => name == "package.json",
None => false,
};
let file = std::fs::File::open(path).ok()?;
let version = {
let file = {
let mut reader = DecodeReaderBytes::new(file);
let mut version = String::new();
reader.read_to_string(&mut version).map(|_| version)
};
match version {
Err(err) => {
match (file, is_pkg_json, config.resolve_engines()) {
(_, true, false) => None,
(Err(err), _, _) => {
info!("Can't read file: {}", err);
None
}
Ok(version) => {
info!("Found string {:?} in version file", version);
(Ok(version), false, _) => {
info!("Found string {:?} in version file", version);
UserVersion::from_str(version.trim()).ok()
}
(Ok(pkg_json), true, true) => {
let pkg_json = serde_json::from_str::<PackageJson>(&pkg_json).ok();
let range: Option<node_semver::Range> =
pkg_json.as_ref().and_then(PackageJson::node_range).cloned();
if let Some(range) = range {
info!("Found package.json with {:?} in engines.node field", range);
Some(UserVersion::SemverRange(range))
} else {
info!("No engines.node range found in package.json");
None
}
}
}
}

Loading…
Cancel
Save