Skip to content

Commit f70f12f

Browse files
feat: support devEngines.runtime filed in package.json
1 parent 64ef825 commit f70f12f

File tree

6 files changed

+208
-10
lines changed

6 files changed

+208
-10
lines changed

e2e/basic.test.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,97 @@ for (const shell of [Bash, Zsh, Fish, PowerShell, WinCmd]) {
6868
.execute(shell)
6969
})
7070

71+
test(`package.json devEngines.runtime`, async () => {
72+
await writeFile(
73+
join(testCwd(), "package.json"),
74+
JSON.stringify({
75+
devEngines: { runtime: { name: "node", version: "8.11.3" } },
76+
}),
77+
)
78+
await script(shell)
79+
.then(shell.env({ resolveDevEngines: true }))
80+
.then(shell.call("fnm", ["install"]))
81+
.then(shell.call("fnm", ["use"]))
82+
.then(testNodeVersion(shell, "v8.11.3"))
83+
.takeSnapshot(shell)
84+
.execute(shell)
85+
})
86+
87+
test(`package.json devEngines.runtime with semver range`, async () => {
88+
await writeFile(
89+
join(testCwd(), "package.json"),
90+
JSON.stringify({
91+
devEngines: { runtime: { name: "node", version: "^6 < 6.17.1" } },
92+
}),
93+
)
94+
await script(shell)
95+
.then(shell.env({ resolveDevEngines: true }))
96+
.then(shell.call("fnm", ["install"]))
97+
.then(shell.call("fnm", ["use"]))
98+
.then(testNodeVersion(shell, "v6.17.0"))
99+
.takeSnapshot(shell)
100+
.execute(shell)
101+
})
102+
103+
test(`package.json devEngines.runtime (array)`, async () => {
104+
await writeFile(
105+
join(testCwd(), "package.json"),
106+
JSON.stringify({
107+
devEngines: {
108+
runtime: [
109+
{ name: "bun", version: "1.0.0" },
110+
{ name: "node", version: "8.11.3" },
111+
],
112+
},
113+
}),
114+
)
115+
await script(shell)
116+
.then(shell.env({ resolveDevEngines: true }))
117+
.then(shell.call("fnm", ["install"]))
118+
.then(shell.call("fnm", ["use"]))
119+
.then(testNodeVersion(shell, "v8.11.3"))
120+
.takeSnapshot(shell)
121+
.execute(shell)
122+
})
123+
124+
test(`package.json devEngines.runtime with semver range (array)`, async () => {
125+
await writeFile(
126+
join(testCwd(), "package.json"),
127+
JSON.stringify({
128+
devEngines: {
129+
runtime: [
130+
{ name: "bun", version: "1.0.0" },
131+
{ name: "node", version: "^6 < 6.17.1" },
132+
],
133+
},
134+
}),
135+
)
136+
await script(shell)
137+
.then(shell.env({ resolveDevEngines: true }))
138+
.then(shell.call("fnm", ["install"]))
139+
.then(shell.call("fnm", ["use"]))
140+
.then(testNodeVersion(shell, "v6.17.0"))
141+
.takeSnapshot(shell)
142+
.execute(shell)
143+
})
144+
145+
test(`package.json engines.node & devEngines.runtime`, async () => {
146+
await writeFile(
147+
join(testCwd(), "package.json"),
148+
JSON.stringify({
149+
engines: { node: "1.0.0" },
150+
devEngines: { runtime: { name: "node", version: "8.11.3" } },
151+
}),
152+
)
153+
await script(shell)
154+
.then(shell.env({ resolveDevEngines: true, resolveEngines: true }))
155+
.then(shell.call("fnm", ["install"]))
156+
.then(shell.call("fnm", ["use"]))
157+
.then(testNodeVersion(shell, "v8.11.3"))
158+
.takeSnapshot(shell)
159+
.execute(shell)
160+
})
161+
71162
test(`resolves partial semver`, async () => {
72163
await script(shell)
73164
.then(shell.env({}))

e2e/shellcode/shells/cmdEnv.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ type EnvConfig = {
55
useOnCd: boolean
66
logLevel: string
77
corepackEnabled: boolean
8+
resolveDevEngines: boolean
89
resolveEngines: boolean
910
nodeDistMirror: string
1011
}
@@ -15,6 +16,7 @@ function stringify(envConfig: Partial<EnvConfig> = {}) {
1516
useOnCd,
1617
logLevel,
1718
corepackEnabled,
19+
resolveDevEngines,
1820
resolveEngines,
1921
executableName = "fnm",
2022
nodeDistMirror,
@@ -24,6 +26,7 @@ function stringify(envConfig: Partial<EnvConfig> = {}) {
2426
useOnCd && "--use-on-cd",
2527
logLevel && `--log-level=${logLevel}`,
2628
corepackEnabled && "--corepack-enabled",
29+
resolveDevEngines && `--resolve-dev-engines`,
2730
resolveEngines && `--resolve-engines`,
2831
nodeDistMirror && `--node-dist-mirror=${JSON.stringify(nodeDistMirror)}`,
2932
]

e2e/use-on-cd.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,29 @@ for (const shell of [Bash, Zsh, Fish, PowerShell, WinCmd]) {
4141
.execute(shell)
4242
})
4343

44+
test(`with resolve-dev-engines`, async () => {
45+
await mkdir(join(testCwd(), "subdir"), { recursive: true })
46+
await writeFile(
47+
join(testCwd(), "subdir", "package.json"),
48+
JSON.stringify({
49+
name: "hello",
50+
devEngines: {
51+
runtime: {
52+
name: "node",
53+
version: "v12.22.12",
54+
},
55+
},
56+
}),
57+
)
58+
await script(shell)
59+
.then(shell.env({ useOnCd: true, resolveEngines: true }))
60+
.then(shell.call("fnm", ["install", "v8.11.3"]))
61+
.then(shell.call("fnm", ["install", "v12.22.12"]))
62+
.then(shell.call("cd", ["subdir"]))
63+
.then(testNodeVersion(shell, "v12.22.12"))
64+
.execute(shell)
65+
})
66+
4467
test(`doesn't throw on missing env data`, async () => {
4568
await mkdir(join(testCwd(), "subdir"), { recursive: true })
4669
await writeFile(

src/config.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,22 @@ pub struct FnmConfig {
7676
)]
7777
corepack_enabled: bool,
7878

79+
/// Resolve `devEngines.runtime` field in `package.json` whenever a `.node-version` or `.nvmrc` file is not present.
80+
/// This feature is enabled by default. To disable it, provide `--resolve-dev-engines=false`.
81+
/// The `devEngines.runtime` field has priority over the `engines.node` field.
82+
#[clap(
83+
long,
84+
env = "FNM_RESOLVE_DEV_ENGINES",
85+
global = true,
86+
hide_env_values = true,
87+
verbatim_doc_comment
88+
)]
89+
#[expect(
90+
clippy::option_option,
91+
reason = "clap Option<Option<T>> supports --x and --x=value syntaxes"
92+
)]
93+
resolve_dev_engines: Option<Option<bool>>,
94+
7995
/// Resolve `engines.node` field in `package.json` whenever a `.node-version` or `.nvmrc` file is not present.
8096
/// This feature is enabled by default. To disable it, provide `--resolve-engines=false`.
8197
///
@@ -110,6 +126,7 @@ impl Default for FnmConfig {
110126
arch: Arch::default(),
111127
version_file_strategy: VersionFileStrategy::default(),
112128
corepack_enabled: false,
129+
resolve_dev_engines: None,
113130
resolve_engines: None,
114131
directories: Directories::default(),
115132
}
@@ -125,6 +142,10 @@ impl FnmConfig {
125142
self.corepack_enabled
126143
}
127144

145+
pub fn resolve_dev_engines(&self) -> bool {
146+
self.resolve_dev_engines.flatten().unwrap_or(true)
147+
}
148+
128149
pub fn resolve_engines(&self) -> bool {
129150
self.resolve_engines.flatten().unwrap_or(true)
130151
}

src/package_json.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,29 @@ struct EnginesField {
55
node: Option<node_semver::Range>,
66
}
77

8+
#[derive(Debug, Deserialize, Default)]
9+
struct DevEnginesField {
10+
runtime: Option<RuntimeField>,
11+
}
12+
13+
#[derive(Debug, Deserialize)]
14+
#[serde(untagged)]
15+
enum RuntimeField {
16+
Single(DevEngine),
17+
Multiple(Vec<DevEngine>),
18+
}
19+
20+
#[derive(Debug, Deserialize)]
21+
struct DevEngine {
22+
name: String,
23+
version: Option<node_semver::Range>,
24+
}
25+
826
#[derive(Debug, Deserialize, Default)]
927
pub struct PackageJson {
1028
engines: Option<EnginesField>,
29+
#[serde(rename = "devEngines")]
30+
dev_engines: Option<DevEnginesField>,
1131
}
1232

1333
impl PackageJson {
@@ -16,4 +36,20 @@ impl PackageJson {
1636
.as_ref()
1737
.and_then(|engines| engines.node.as_ref())
1838
}
39+
40+
pub fn dev_node_range(&self) -> Option<&node_semver::Range> {
41+
self.dev_engines
42+
.as_ref()
43+
.and_then(|dev_engines| dev_engines.runtime.as_ref())
44+
.and_then(|runtime| {
45+
let engines = match runtime {
46+
RuntimeField::Single(engine) => std::slice::from_ref(engine),
47+
RuntimeField::Multiple(engines) => engines.as_slice(),
48+
};
49+
engines
50+
.iter()
51+
.find(|engine| engine.name.to_lowercase() == "node")
52+
.and_then(|engine| engine.version.as_ref())
53+
})
54+
}
1955
}

src/version_files.rs

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -78,26 +78,50 @@ pub fn get_user_version_for_file(
7878
reader.read_to_string(&mut version).map(|_| version)
7979
};
8080

81-
match (file, is_pkg_json, config.resolve_engines()) {
82-
(_, true, false) => None,
83-
(Err(err), _, _) => {
81+
match (
82+
file,
83+
is_pkg_json,
84+
config.resolve_dev_engines(),
85+
config.resolve_engines(),
86+
) {
87+
(_, true, false, false) => None,
88+
(Err(err), _, _, _) => {
8489
info!("Can't read file: {}", err);
8590
None
8691
}
87-
(Ok(version), false, _) => {
92+
(Ok(version), false, _, _) => {
8893
info!("Found string {:?} in version file", version);
8994
UserVersion::from_str(version.trim()).ok()
9095
}
91-
(Ok(pkg_json), true, true) => {
96+
(Ok(pkg_json), true, resolve_dev_engines, resolve_engines) => {
9297
let pkg_json = serde_json::from_str::<PackageJson>(&pkg_json).ok();
93-
let range: Option<node_semver::Range> =
94-
pkg_json.as_ref().and_then(PackageJson::node_range).cloned();
95-
98+
let mut range: Option<node_semver::Range> = None;
99+
if resolve_dev_engines {
100+
range = pkg_json
101+
.as_ref()
102+
.and_then(PackageJson::dev_node_range)
103+
.cloned();
104+
if range.is_some() {
105+
info!(
106+
"Found package.json with node {:?} in devEngines.runtime field",
107+
range
108+
);
109+
} else {
110+
info!("No node range found in package.json devEngines.runtime field");
111+
}
112+
}
113+
if resolve_engines && range.is_none() {
114+
range = pkg_json.as_ref().and_then(PackageJson::node_range).cloned();
115+
if range.is_some() {
116+
info!("Found package.json with {:?} in engines.node field", range);
117+
} else {
118+
info!("No node range found in package.json engines.node field");
119+
}
120+
}
96121
if let Some(range) = range {
97-
info!("Found package.json with {:?} in engines.node field", range);
98122
Some(UserVersion::SemverRange(range))
99123
} else {
100-
info!("No engines.node range found in package.json");
124+
info!("No node range found in package.json");
101125
None
102126
}
103127
}

0 commit comments

Comments
 (0)