mirror of
https://github.com/pnpm/pnpm.git
synced 2026-06-28 18:05:29 -04:00
feat(pacquet): port runtime command (#12571)
Port the runtime command to pacquet by adding a runtime/rt CLI entry and routing runtime set <name> <version> through the existing add pipeline as <name>@runtime:<version>. The command matches pnpm's local save behavior: devEngines.runtime is the default, --save-prod targets engines.runtime, and --save-dev wins when both flags are present. The global path is parsed but rejected because pacquet does not yet support global package management, matching the existing update/outdated global boundary. Add the manifest writer conversion that folds runtime:<version> dependencies back into devEngines.runtime and engines.runtime before saving, complementing the existing read-time conversion used by install and lockfile checks.
This commit is contained in:
6
.changeset/prune-runtime-engine-removals.md
Normal file
6
.changeset/prune-runtime-engine-removals.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@pnpm/workspace.project-manifest-reader": patch
|
||||
"pnpm": patch
|
||||
---
|
||||
|
||||
Removing a runtime dependency now removes the matching `devEngines.runtime` or `engines.runtime` entry that was materialized from it.
|
||||
@@ -12,6 +12,7 @@ pub mod recursive;
|
||||
pub mod remove;
|
||||
pub mod restart;
|
||||
pub mod run;
|
||||
pub mod runtime;
|
||||
pub mod sanitize;
|
||||
pub mod stop;
|
||||
pub mod store;
|
||||
@@ -43,6 +44,7 @@ use pacquet_reporter::{
|
||||
use remove::RemoveArgs;
|
||||
use restart::RestartArgs;
|
||||
use run::RunArgs;
|
||||
use runtime::RuntimeArgs;
|
||||
use serde_json::Value;
|
||||
use std::{
|
||||
fs,
|
||||
@@ -167,6 +169,9 @@ pub enum CliCommand {
|
||||
Restart(RestartArgs),
|
||||
/// Lists the packages that include the file with the specified hash.
|
||||
FindHash(FindHashArgs),
|
||||
/// Manage runtimes.
|
||||
#[clap(visible_alias = "rt")]
|
||||
Runtime(RuntimeArgs),
|
||||
/// Managing the package store.
|
||||
#[clap(subcommand)]
|
||||
Store(StoreCommand),
|
||||
@@ -257,7 +262,8 @@ impl CliArgs {
|
||||
| CliCommand::Remove(_)
|
||||
| CliCommand::Install(_)
|
||||
| CliCommand::Dlx(_)
|
||||
| CliCommand::Create(_),
|
||||
| CliCommand::Create(_)
|
||||
| CliCommand::Runtime(_),
|
||||
);
|
||||
let manifest_path = || dir.join("package.json");
|
||||
// Resolve `.npmrc` / `pnpm-workspace.yaml` from the canonicalized
|
||||
@@ -476,6 +482,20 @@ impl CliArgs {
|
||||
CliCommand::FindHash(args) => {
|
||||
args.run(|| config().map(|m| &*m))?;
|
||||
}
|
||||
CliCommand::Runtime(args) => {
|
||||
args.reject_unsupported_global()?;
|
||||
match reporter {
|
||||
ReporterType::Default | ReporterType::AppendOnly => {
|
||||
Box::pin(args.run::<DefaultReporter>(state(false)?)).await?;
|
||||
}
|
||||
ReporterType::Ndjson => {
|
||||
Box::pin(args.run::<NdjsonReporter>(state(false)?)).await?;
|
||||
}
|
||||
ReporterType::Silent => {
|
||||
Box::pin(args.run::<SilentReporter>(state(false)?)).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
CliCommand::Store(command) => command.run(|| config().map(|m| &*m))?,
|
||||
CliCommand::Cache(command) => command.run(config()?)?,
|
||||
CliCommand::CatFile(args) => {
|
||||
|
||||
111
pacquet/crates/cli/src/cli_args/runtime.rs
Normal file
111
pacquet/crates/cli/src/cli_args/runtime.rs
Normal file
@@ -0,0 +1,111 @@
|
||||
use crate::{State, cli_args::add::add_package};
|
||||
use clap::Args;
|
||||
use derive_more::{Display, Error};
|
||||
use miette::Diagnostic;
|
||||
use pacquet_package_manifest::DependencyGroup;
|
||||
use pacquet_registry::PinnedVersion;
|
||||
use pacquet_reporter::Reporter;
|
||||
|
||||
/// Manage runtimes.
|
||||
#[derive(Debug, Args)]
|
||||
pub struct RuntimeArgs {
|
||||
/// Install the runtime globally.
|
||||
#[clap(short = 'g', long)]
|
||||
pub global: bool,
|
||||
|
||||
/// Save the runtime to `devEngines.runtime`. This is the default.
|
||||
#[clap(short = 'D', long = "save-dev")]
|
||||
pub save_dev: bool,
|
||||
|
||||
/// Save the runtime to `engines.runtime`.
|
||||
#[clap(short = 'P', long = "save-prod")]
|
||||
pub save_prod: bool,
|
||||
|
||||
/// Runtime subcommand and arguments.
|
||||
pub params: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Display, Error, Diagnostic, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub enum RuntimeError {
|
||||
#[display("Please specify the subcommand")]
|
||||
#[diagnostic(code(ERR_PNPM_RUNTIME_NO_SUBCOMMAND))]
|
||||
NoSubcommand,
|
||||
|
||||
#[display("Unknown subcommand: {subcommand}")]
|
||||
#[diagnostic(code(ERR_PNPM_RUNTIME_UNKNOWN_SUBCOMMAND))]
|
||||
UnknownSubcommand {
|
||||
#[error(not(source))]
|
||||
subcommand: String,
|
||||
},
|
||||
|
||||
#[display(
|
||||
r#""pnpm runtime set <name> <version>" requires a runtime name (e.g. node, deno, bun)"#
|
||||
)]
|
||||
#[diagnostic(code(ERR_PNPM_MISSING_RUNTIME_NAME))]
|
||||
MissingRuntimeName,
|
||||
|
||||
#[display(
|
||||
"`pacquet runtime set --global` is not supported yet; global package management has not been ported to pacquet."
|
||||
)]
|
||||
#[diagnostic(code(pacquet_cli::runtime_global_unsupported))]
|
||||
GlobalUnsupported,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct RuntimeSetRequest {
|
||||
package_name: String,
|
||||
dependency_group: DependencyGroup,
|
||||
}
|
||||
|
||||
impl RuntimeArgs {
|
||||
pub fn reject_unsupported_global(&self) -> Result<(), RuntimeError> {
|
||||
if self.global {
|
||||
return Err(RuntimeError::GlobalUnsupported);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Execute the subcommand.
|
||||
pub async fn run<Reporter: self::Reporter + 'static>(self, state: State) -> miette::Result<()> {
|
||||
let request = self.set_request()?;
|
||||
add_package::<Reporter, _, _>(
|
||||
state,
|
||||
&request.package_name,
|
||||
PinnedVersion::Major,
|
||||
None,
|
||||
false,
|
||||
None,
|
||||
|| std::iter::once(request.dependency_group),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
fn set_request(&self) -> Result<RuntimeSetRequest, RuntimeError> {
|
||||
let Some(subcommand) = self.params.first() else {
|
||||
return Err(RuntimeError::NoSubcommand);
|
||||
};
|
||||
if subcommand != "set" {
|
||||
return Err(RuntimeError::UnknownSubcommand { subcommand: subcommand.clone() });
|
||||
}
|
||||
let runtime_name = self
|
||||
.params
|
||||
.get(1)
|
||||
.map(|name| name.trim())
|
||||
.filter(|name| !name.is_empty())
|
||||
.ok_or(RuntimeError::MissingRuntimeName)?;
|
||||
let version_spec = self.params.get(2).map_or("", |version| version.trim());
|
||||
let dependency_group = if self.save_dev || !self.save_prod {
|
||||
DependencyGroup::Dev
|
||||
} else {
|
||||
DependencyGroup::Prod
|
||||
};
|
||||
Ok(RuntimeSetRequest {
|
||||
package_name: format!("{runtime_name}@runtime:{version_spec}"),
|
||||
dependency_group,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
76
pacquet/crates/cli/src/cli_args/runtime/tests.rs
Normal file
76
pacquet/crates/cli/src/cli_args/runtime/tests.rs
Normal file
@@ -0,0 +1,76 @@
|
||||
use pacquet_package_manifest::DependencyGroup;
|
||||
|
||||
use super::{RuntimeArgs, RuntimeError};
|
||||
|
||||
fn args(params: &[&str]) -> RuntimeArgs {
|
||||
RuntimeArgs {
|
||||
global: false,
|
||||
save_dev: false,
|
||||
save_prod: false,
|
||||
params: params.iter().map(|param| (*param).to_string()).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_request_defaults_to_dev_engines_runtime() {
|
||||
let request = args(&["set", "node", "22"]).set_request().unwrap();
|
||||
assert_eq!(request.package_name, "node@runtime:22");
|
||||
assert_eq!(request.dependency_group, DependencyGroup::Dev);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_request_saves_prod_when_save_prod_is_set() {
|
||||
let request =
|
||||
RuntimeArgs { save_prod: true, ..args(&["set", "node", "22"]) }.set_request().unwrap();
|
||||
assert_eq!(request.package_name, "node@runtime:22");
|
||||
assert_eq!(request.dependency_group, DependencyGroup::Prod);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_request_prefers_save_dev_over_save_prod() {
|
||||
let request = RuntimeArgs { save_dev: true, save_prod: true, ..args(&["set", "node", "22"]) }
|
||||
.set_request()
|
||||
.unwrap();
|
||||
assert_eq!(request.package_name, "node@runtime:22");
|
||||
assert_eq!(request.dependency_group, DependencyGroup::Dev);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_request_allows_missing_version_spec() {
|
||||
let request = args(&["set", "node"]).set_request().unwrap();
|
||||
assert_eq!(request.package_name, "node@runtime:");
|
||||
assert_eq!(request.dependency_group, DependencyGroup::Dev);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_request_works_with_deno() {
|
||||
let request = args(&["set", "deno", "2"]).set_request().unwrap();
|
||||
assert_eq!(request.package_name, "deno@runtime:2");
|
||||
assert_eq!(request.dependency_group, DependencyGroup::Dev);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_request_fails_without_subcommand() {
|
||||
let err = args(&[]).set_request().unwrap_err();
|
||||
assert_eq!(err, RuntimeError::NoSubcommand);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_request_fails_for_unknown_subcommand() {
|
||||
let err = args(&["foo"]).set_request().unwrap_err();
|
||||
assert_eq!(err, RuntimeError::UnknownSubcommand { subcommand: "foo".to_string() });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_request_fails_without_runtime_name() {
|
||||
let err = args(&["set"]).set_request().unwrap_err();
|
||||
assert_eq!(err, RuntimeError::MissingRuntimeName);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn global_is_rejected_before_state_initialization() {
|
||||
let err = RuntimeArgs { global: true, ..args(&["set", "node", "22"]) }
|
||||
.reject_unsupported_global()
|
||||
.unwrap_err();
|
||||
assert_eq!(err, RuntimeError::GlobalUnsupported);
|
||||
}
|
||||
@@ -60,6 +60,30 @@ fn filter_flag_split_across_subcommand_keeps_only_subcommand_side() {
|
||||
assert_eq!(parsed.filter, ["b"], "global-side `a` is dropped");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_alias_and_flags_parse() {
|
||||
let parsed = CliArgs::try_parse_from(["pacquet", "rt", "set", "node", "22", "-P"])
|
||||
.expect("parses runtime alias");
|
||||
let CliCommand::Runtime(args) = parsed.command else {
|
||||
panic!("expected runtime command");
|
||||
};
|
||||
assert!(!args.global);
|
||||
assert!(!args.save_dev);
|
||||
assert!(args.save_prod);
|
||||
assert_eq!(args.params, ["set", "node", "22"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_global_flag_parses_after_version() {
|
||||
let parsed = CliArgs::try_parse_from(["pacquet", "runtime", "set", "node", "22", "-g"])
|
||||
.expect("parses runtime global flag after params");
|
||||
let CliCommand::Runtime(args) = parsed.command else {
|
||||
panic!("expected runtime command");
|
||||
};
|
||||
assert!(args.global);
|
||||
assert_eq!(args.params, ["set", "node", "22"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn package_manager_to_sync_preserves_dev_engine_specifier() {
|
||||
let root = TempDir::new().expect("tmp dir");
|
||||
|
||||
@@ -75,6 +75,7 @@ impl BunResolver {
|
||||
let Some(version_spec) = bare_runtime_spec(wanted_dependency, "bun") else {
|
||||
return Ok(None);
|
||||
};
|
||||
let version_spec = normalize_runtime_spec(version_spec);
|
||||
|
||||
let npm_result = self
|
||||
.npm_resolver
|
||||
@@ -127,7 +128,8 @@ impl BunResolver {
|
||||
return Ok(None);
|
||||
};
|
||||
let version_spec =
|
||||
if query.compatible { manifest_spec.to_string() } else { "latest".to_string() };
|
||||
if query.compatible { normalize_runtime_spec(manifest_spec) } else { "latest" }
|
||||
.to_string();
|
||||
let mut resolve_opts = opts.clone();
|
||||
if !query.compatible {
|
||||
resolve_opts.update = UpdateBehavior::Latest;
|
||||
@@ -172,6 +174,10 @@ fn bare_runtime_spec<'a>(wanted: &'a WantedDependency, expected_alias: &str) ->
|
||||
wanted.bare_specifier.as_deref().and_then(|spec| spec.strip_prefix(BARE_SPEC_PREFIX))
|
||||
}
|
||||
|
||||
fn normalize_runtime_spec(version_spec: &str) -> &str {
|
||||
if version_spec.is_empty() { "latest" } else { version_spec }
|
||||
}
|
||||
|
||||
fn bun_bin_for_current_os(platform: &str) -> &'static str {
|
||||
if platform == "win32" { "bun.exe" } else { "bun" }
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use pacquet_network::ThrottledClient;
|
||||
use pacquet_resolving_resolver_base::{
|
||||
@@ -26,6 +26,29 @@ impl Resolver for StubResolver {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct CapturingResolver {
|
||||
seen: Mutex<Vec<Option<String>>>,
|
||||
}
|
||||
|
||||
impl Resolver for CapturingResolver {
|
||||
fn resolve<'a>(
|
||||
&'a self,
|
||||
wanted_dependency: &'a WantedDependency,
|
||||
_opts: &'a ResolveOptions,
|
||||
) -> ResolveFuture<'a> {
|
||||
self.seen.lock().unwrap().push(wanted_dependency.bare_specifier.clone());
|
||||
Box::pin(async { Ok::<Option<ResolveResult>, ResolveError>(None) })
|
||||
}
|
||||
fn resolve_latest<'a>(
|
||||
&'a self,
|
||||
_query: &'a pacquet_resolving_resolver_base::LatestQuery,
|
||||
_opts: &'a ResolveOptions,
|
||||
) -> ResolveLatestFuture<'a> {
|
||||
Box::pin(async { Ok::<Option<LatestInfo>, ResolveError>(None) })
|
||||
}
|
||||
}
|
||||
|
||||
fn resolver() -> BunResolver {
|
||||
BunResolver::new(Arc::new(ThrottledClient::new_for_installs()), Arc::new(StubResolver))
|
||||
}
|
||||
@@ -49,3 +72,21 @@ async fn declines_bun_without_runtime_prefix() {
|
||||
};
|
||||
assert!(resolver().resolve(&wanted, &ResolveOptions::default()).await.unwrap().is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn empty_runtime_spec_delegates_latest_to_npm_resolver() {
|
||||
let npm_resolver = Arc::new(CapturingResolver::default());
|
||||
let resolver = BunResolver::new(
|
||||
Arc::new(ThrottledClient::new_for_installs()),
|
||||
Arc::<CapturingResolver>::clone(&npm_resolver),
|
||||
);
|
||||
let wanted = WantedDependency {
|
||||
alias: Some("bun".to_string()),
|
||||
bare_specifier: Some("runtime:".to_string()),
|
||||
..WantedDependency::default()
|
||||
};
|
||||
|
||||
resolver.resolve(&wanted, &ResolveOptions::default()).await.unwrap_err();
|
||||
|
||||
assert_eq!(npm_resolver.seen.lock().unwrap().clone(), vec![Some("latest".to_string())]);
|
||||
}
|
||||
|
||||
@@ -81,6 +81,7 @@ impl DenoResolver {
|
||||
let Some(version_spec) = bare_runtime_spec(wanted_dependency, "deno") else {
|
||||
return Ok(None);
|
||||
};
|
||||
let version_spec = normalize_runtime_spec(version_spec);
|
||||
|
||||
let npm_result = self
|
||||
.npm_resolver
|
||||
@@ -134,7 +135,8 @@ impl DenoResolver {
|
||||
return Ok(None);
|
||||
};
|
||||
let version_spec =
|
||||
if query.compatible { manifest_spec.to_string() } else { "latest".to_string() };
|
||||
if query.compatible { normalize_runtime_spec(manifest_spec) } else { "latest" }
|
||||
.to_string();
|
||||
let mut resolve_opts = opts.clone();
|
||||
if !query.compatible {
|
||||
resolve_opts.update = UpdateBehavior::Latest;
|
||||
@@ -179,6 +181,10 @@ fn bare_runtime_spec<'a>(wanted: &'a WantedDependency, expected_alias: &str) ->
|
||||
wanted.bare_specifier.as_deref().and_then(|spec| spec.strip_prefix(BARE_SPEC_PREFIX))
|
||||
}
|
||||
|
||||
fn normalize_runtime_spec(version_spec: &str) -> &str {
|
||||
if version_spec.is_empty() { "latest" } else { version_spec }
|
||||
}
|
||||
|
||||
fn deno_bin_for_current_os(platform: &str) -> &'static str {
|
||||
if platform == "win32" { "deno.exe" } else { "deno" }
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use pacquet_network::ThrottledClient;
|
||||
use pacquet_resolving_resolver_base::{
|
||||
@@ -26,6 +26,29 @@ impl Resolver for StubResolver {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct CapturingResolver {
|
||||
seen: Mutex<Vec<Option<String>>>,
|
||||
}
|
||||
|
||||
impl Resolver for CapturingResolver {
|
||||
fn resolve<'a>(
|
||||
&'a self,
|
||||
wanted_dependency: &'a WantedDependency,
|
||||
_opts: &'a ResolveOptions,
|
||||
) -> ResolveFuture<'a> {
|
||||
self.seen.lock().unwrap().push(wanted_dependency.bare_specifier.clone());
|
||||
Box::pin(async { Ok::<Option<ResolveResult>, ResolveError>(None) })
|
||||
}
|
||||
fn resolve_latest<'a>(
|
||||
&'a self,
|
||||
_query: &'a pacquet_resolving_resolver_base::LatestQuery,
|
||||
_opts: &'a ResolveOptions,
|
||||
) -> ResolveLatestFuture<'a> {
|
||||
Box::pin(async { Ok::<Option<LatestInfo>, ResolveError>(None) })
|
||||
}
|
||||
}
|
||||
|
||||
fn resolver() -> DenoResolver {
|
||||
DenoResolver::new(Arc::new(ThrottledClient::new_for_installs()), Arc::new(StubResolver))
|
||||
}
|
||||
@@ -49,3 +72,21 @@ async fn declines_deno_without_runtime_prefix() {
|
||||
};
|
||||
assert!(resolver().resolve(&wanted, &ResolveOptions::default()).await.unwrap().is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn empty_runtime_spec_delegates_latest_to_npm_resolver() {
|
||||
let npm_resolver = Arc::new(CapturingResolver::default());
|
||||
let resolver = DenoResolver::new(
|
||||
Arc::new(ThrottledClient::new_for_installs()),
|
||||
Arc::<CapturingResolver>::clone(&npm_resolver),
|
||||
);
|
||||
let wanted = WantedDependency {
|
||||
alias: Some("deno".to_string()),
|
||||
bare_specifier: Some("runtime:".to_string()),
|
||||
..WantedDependency::default()
|
||||
};
|
||||
|
||||
resolver.resolve(&wanted, &ResolveOptions::default()).await.unwrap_err();
|
||||
|
||||
assert_eq!(npm_resolver.seen.lock().unwrap().clone(), vec![Some("latest".to_string())]);
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
//! versions, and pick the one (or the set) matching a user-supplied
|
||||
//! selector. The selector may be:
|
||||
//!
|
||||
//! - `latest` — the first entry in the index (the newest published
|
||||
//! build on that channel).
|
||||
//! - `latest` or an empty selector — the first entry in the index
|
||||
//! (the newest published build on that channel).
|
||||
//! - `lts` — the newest entry tagged with any LTS codename.
|
||||
//! - An LTS codename (`argon`, `iron`, ...) — `*` within that codename.
|
||||
//! - A semver range — pick the `max_satisfying` version.
|
||||
@@ -78,7 +78,7 @@ pub async fn resolve_node_version(
|
||||
node_mirror_base_url: Option<&str>,
|
||||
) -> Result<Option<String>, ResolveNodeVersionError> {
|
||||
let all_versions = fetch_all_versions(http_client, node_mirror_base_url).await?;
|
||||
if version_spec == "latest" {
|
||||
if is_latest_selector(version_spec) {
|
||||
return Ok(all_versions.first().map(|version| version.version.clone()));
|
||||
}
|
||||
let (versions, range) = filter_versions(&all_versions, version_spec);
|
||||
@@ -99,7 +99,7 @@ pub async fn resolve_node_versions(
|
||||
let Some(version_spec) = version_spec else {
|
||||
return Ok(all_versions.into_iter().map(|version| version.version).collect());
|
||||
};
|
||||
if version_spec == "latest" {
|
||||
if is_latest_selector(version_spec) {
|
||||
return Ok(all_versions
|
||||
.into_iter()
|
||||
.next()
|
||||
@@ -150,6 +150,10 @@ async fn fetch_all_versions(
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn is_latest_selector(version_spec: &str) -> bool {
|
||||
version_spec.is_empty() || version_spec == "latest"
|
||||
}
|
||||
|
||||
/// Decode the `lts` field upstream emits as `false | string`.
|
||||
fn lts_codename(value: serde_json::Value) -> Option<String> {
|
||||
match value {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::{NodeVersion, filter_versions};
|
||||
use pacquet_network::ThrottledClient;
|
||||
|
||||
use super::{NodeVersion, filter_versions, resolve_node_version, resolve_node_versions};
|
||||
|
||||
fn make_versions() -> Vec<NodeVersion> {
|
||||
vec![
|
||||
@@ -32,3 +34,28 @@ fn semver_range_passes_through() {
|
||||
assert_eq!(picked.len(), 5);
|
||||
assert_eq!(range, "^20");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn empty_selector_picks_latest_version() {
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let _index = server
|
||||
.mock("GET", "/index.json")
|
||||
.with_status(200)
|
||||
.with_body(
|
||||
r#"[
|
||||
{ "version": "v22.1.0", "lts": false },
|
||||
{ "version": "v20.10.0", "lts": "Iron" }
|
||||
]"#,
|
||||
)
|
||||
.expect(2)
|
||||
.create_async()
|
||||
.await;
|
||||
let base_url = format!("{}/", server.url());
|
||||
let http_client = ThrottledClient::new_for_installs();
|
||||
|
||||
let picked = resolve_node_version(&http_client, "", Some(&base_url)).await.unwrap();
|
||||
assert_eq!(picked, Some("22.1.0".to_string()));
|
||||
|
||||
let picked = resolve_node_versions(&http_client, Some(""), Some(&base_url)).await.unwrap();
|
||||
assert_eq!(picked, vec!["22.1.0"]);
|
||||
}
|
||||
|
||||
@@ -302,7 +302,7 @@ where
|
||||
.await
|
||||
.map_err(AddError::Install)?;
|
||||
|
||||
manifest.save().map_err(AddError::SaveManifest)?;
|
||||
let updated = manifest.save_and_get_written_value().map_err(AddError::SaveManifest)?;
|
||||
|
||||
// `pnpm:package-manifest updated` mirrors pnpm's emit at
|
||||
// <https://github.com/pnpm/pnpm/blob/086c5e91e8/installing/deps-resolver/src/index.ts#L238>:
|
||||
@@ -326,7 +326,7 @@ where
|
||||
.into_owned();
|
||||
Reporter::emit(&LogEvent::PackageManifest(PackageManifestLog {
|
||||
level: LogLevel::Debug,
|
||||
message: PackageManifestMessage::Updated { prefix, updated: manifest.value().clone() },
|
||||
message: PackageManifestMessage::Updated { prefix, updated },
|
||||
}));
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -139,7 +139,7 @@ impl Remove<'_> {
|
||||
.await
|
||||
.map_err(RemoveError::Install)?;
|
||||
|
||||
manifest.save().map_err(RemoveError::SaveManifest)?;
|
||||
let updated = manifest.save_and_get_written_value().map_err(RemoveError::SaveManifest)?;
|
||||
|
||||
// `pnpm:package-manifest updated` mirrors the post-mutation emit
|
||||
// pnpm fires after rewriting the manifest. See the parallel emit
|
||||
@@ -152,7 +152,7 @@ impl Remove<'_> {
|
||||
.into_owned();
|
||||
Reporter::emit(&LogEvent::PackageManifest(PackageManifestLog {
|
||||
level: LogLevel::Debug,
|
||||
message: PackageManifestMessage::Updated { prefix, updated: manifest.value().clone() },
|
||||
message: PackageManifestMessage::Updated { prefix, updated },
|
||||
}));
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -568,7 +568,8 @@ impl Update<'_> {
|
||||
.map_err(UpdateError::Install)?;
|
||||
|
||||
if persist_manifest {
|
||||
manifest.save().map_err(UpdateError::SaveManifest)?;
|
||||
let updated =
|
||||
manifest.save_and_get_written_value().map_err(UpdateError::SaveManifest)?;
|
||||
|
||||
let prefix = manifest
|
||||
.path()
|
||||
@@ -578,10 +579,7 @@ impl Update<'_> {
|
||||
.into_owned();
|
||||
Reporter::emit(&LogEvent::PackageManifest(PackageManifestLog {
|
||||
level: LogLevel::Debug,
|
||||
message: PackageManifestMessage::Updated {
|
||||
prefix,
|
||||
updated: manifest.value().clone(),
|
||||
},
|
||||
message: PackageManifestMessage::Updated { prefix, updated },
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -153,10 +153,18 @@ impl PackageManifest {
|
||||
&mut self.value
|
||||
}
|
||||
|
||||
pub fn save(&self) -> Result<(), PackageManifestError> {
|
||||
pub fn save_and_get_written_value(&self) -> Result<Value, PackageManifestError> {
|
||||
let mut value = self.value.clone();
|
||||
convert_dependencies_to_engines_runtime(&mut value, "devDependencies", "devEngines")?;
|
||||
convert_dependencies_to_engines_runtime(&mut value, "dependencies", "engines")?;
|
||||
let contents = serde_json::to_string_pretty(&value)?;
|
||||
let mut file = fs::File::create(&self.path)?;
|
||||
let contents = serde_json::to_string_pretty(&self.value)?;
|
||||
file.write_all(contents.as_bytes())?;
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
pub fn save(&self) -> Result<(), PackageManifestError> {
|
||||
self.save_and_get_written_value()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -382,6 +390,134 @@ pub fn convert_engines_runtime_to_dependencies(
|
||||
}
|
||||
}
|
||||
|
||||
/// Fold `runtime:<version>` dependency entries back into
|
||||
/// `devEngines.runtime` / `engines.runtime` before writing a manifest.
|
||||
///
|
||||
/// Mirrors upstream's `convertDependenciesToEnginesRuntime` writer hook in
|
||||
/// `workspace/project-manifest-reader`: the in-memory dependency form drives
|
||||
/// resolution and lockfile checks, while the on-disk manifest keeps the
|
||||
/// `devEngines.runtime` / `engines.runtime` contract.
|
||||
pub fn convert_dependencies_to_engines_runtime(
|
||||
manifest: &mut Value,
|
||||
deps_field: &str,
|
||||
engines_field: &str,
|
||||
) -> Result<(), PackageManifestError> {
|
||||
for runtime_name in RUNTIME_NAMES {
|
||||
let version = manifest
|
||||
.get(deps_field)
|
||||
.and_then(|deps| deps.get(runtime_name))
|
||||
.and_then(Value::as_str)
|
||||
.and_then(|dep| dep.strip_prefix("runtime:"))
|
||||
.map(str::to_string);
|
||||
if let Some(version) = version {
|
||||
upsert_runtime_entry(manifest, engines_field, runtime_name, &version)?;
|
||||
if let Some(deps) = manifest.get_mut(deps_field).and_then(Value::as_object_mut) {
|
||||
deps.remove(runtime_name);
|
||||
}
|
||||
} else {
|
||||
remove_managed_runtime_entry(manifest, engines_field, runtime_name);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_managed_runtime_entry(manifest: &mut Value, engines_field: &str, runtime_name: &str) {
|
||||
let Some(engines) = manifest.get_mut(engines_field).and_then(Value::as_object_mut) else {
|
||||
return;
|
||||
};
|
||||
let remove_runtime = match engines.get_mut("runtime") {
|
||||
Some(Value::Array(runtimes)) => {
|
||||
runtimes.retain(|runtime| !is_managed_runtime_entry(runtime, runtime_name));
|
||||
runtimes.is_empty()
|
||||
}
|
||||
Some(runtime) if is_managed_runtime_entry(runtime, runtime_name) => true,
|
||||
_ => false,
|
||||
};
|
||||
if remove_runtime {
|
||||
engines.remove("runtime");
|
||||
}
|
||||
}
|
||||
|
||||
fn is_managed_runtime_entry(runtime: &Value, runtime_name: &str) -> bool {
|
||||
runtime.get("name").and_then(Value::as_str) == Some(runtime_name)
|
||||
&& runtime.get("onFail").and_then(Value::as_str) == Some("download")
|
||||
&& runtime.get("version").and_then(Value::as_str).is_some()
|
||||
}
|
||||
|
||||
fn upsert_runtime_entry(
|
||||
manifest: &mut Value,
|
||||
engines_field: &str,
|
||||
runtime_name: &str,
|
||||
version: &str,
|
||||
) -> Result<(), PackageManifestError> {
|
||||
let runtime_entry = json!({
|
||||
"name": runtime_name,
|
||||
"version": version,
|
||||
"onFail": "download",
|
||||
});
|
||||
let engines = ensure_object_field(manifest, engines_field)?;
|
||||
match engines.get_mut("runtime") {
|
||||
None | Some(Value::Null) => {
|
||||
engines.insert("runtime".to_string(), runtime_entry);
|
||||
}
|
||||
Some(Value::Array(runtimes)) => {
|
||||
if let Some(existing) = runtimes
|
||||
.iter_mut()
|
||||
.find(|runtime| runtime.get("name").and_then(Value::as_str) == Some(runtime_name))
|
||||
{
|
||||
merge_runtime_entry(existing, runtime_name, version)?;
|
||||
} else {
|
||||
runtimes.push(runtime_entry);
|
||||
}
|
||||
}
|
||||
Some(Value::Object(runtime))
|
||||
if runtime.get("name").and_then(Value::as_str) == Some(runtime_name) =>
|
||||
{
|
||||
runtime.insert("name".to_string(), Value::String(runtime_name.to_string()));
|
||||
runtime.insert("version".to_string(), Value::String(version.to_string()));
|
||||
runtime.insert("onFail".to_string(), Value::String("download".to_string()));
|
||||
}
|
||||
Some(existing) => {
|
||||
*existing = Value::Array(vec![existing.clone(), runtime_entry]);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ensure_object_field<'a>(
|
||||
manifest: &'a mut Value,
|
||||
field: &str,
|
||||
) -> Result<&'a mut Map<String, Value>, PackageManifestError> {
|
||||
let Some(root) = manifest.as_object_mut() else {
|
||||
return Err(PackageManifestError::InvalidAttribute(
|
||||
"the manifest root must be an object".to_string(),
|
||||
));
|
||||
};
|
||||
let value = root.entry(field.to_string()).or_insert_with(|| Value::Object(Map::new()));
|
||||
if value.is_null() {
|
||||
*value = Value::Object(Map::new());
|
||||
}
|
||||
value.as_object_mut().ok_or_else(|| {
|
||||
PackageManifestError::InvalidAttribute(format!("the {field} field must be an object"))
|
||||
})
|
||||
}
|
||||
|
||||
fn merge_runtime_entry(
|
||||
runtime: &mut Value,
|
||||
runtime_name: &str,
|
||||
version: &str,
|
||||
) -> Result<(), PackageManifestError> {
|
||||
let Some(runtime) = runtime.as_object_mut() else {
|
||||
return Err(PackageManifestError::InvalidAttribute(
|
||||
"runtime entries must be objects".to_string(),
|
||||
));
|
||||
};
|
||||
runtime.insert("name".to_string(), Value::String(runtime_name.to_string()));
|
||||
runtime.insert("version".to_string(), Value::String(version.to_string()));
|
||||
runtime.insert("onFail".to_string(), Value::String("download".to_string()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read `<dir>/package.json` if it exists, returning `Ok(None)` when the file
|
||||
/// is absent. Other IO errors and JSON parse errors propagate.
|
||||
///
|
||||
|
||||
@@ -9,7 +9,7 @@ use tempfile::{NamedTempFile, tempdir};
|
||||
use super::safe_read_package_json_from_dir;
|
||||
use super::{
|
||||
BundleDependencies, PackageManifest, PackageManifestError,
|
||||
convert_engines_runtime_to_dependencies,
|
||||
convert_dependencies_to_engines_runtime, convert_engines_runtime_to_dependencies,
|
||||
};
|
||||
use crate::DependencyGroup;
|
||||
use serde_json::json;
|
||||
@@ -308,6 +308,244 @@ fn convert_engines_runtime_targets_dependencies_for_engines_field() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn convert_dependencies_runtime_writes_devengines_runtime() {
|
||||
let mut manifest = json!({
|
||||
"devDependencies": {
|
||||
"node": "runtime:22",
|
||||
},
|
||||
});
|
||||
convert_dependencies_to_engines_runtime(&mut manifest, "devDependencies", "devEngines")
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
manifest.get("devEngines"),
|
||||
Some(&json!({
|
||||
"runtime": {
|
||||
"name": "node",
|
||||
"version": "22",
|
||||
"onFail": "download",
|
||||
},
|
||||
})),
|
||||
);
|
||||
assert_eq!(manifest.get("devDependencies"), Some(&json!({})));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn convert_dependencies_runtime_updates_existing_single_entry() {
|
||||
let mut manifest = json!({
|
||||
"devEngines": {
|
||||
"runtime": {
|
||||
"name": "node",
|
||||
"version": "16",
|
||||
"onFail": "warn",
|
||||
},
|
||||
},
|
||||
"devDependencies": {
|
||||
"node": "runtime:22",
|
||||
},
|
||||
});
|
||||
convert_dependencies_to_engines_runtime(&mut manifest, "devDependencies", "devEngines")
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
manifest.get("devEngines"),
|
||||
Some(&json!({
|
||||
"runtime": {
|
||||
"name": "node",
|
||||
"version": "22",
|
||||
"onFail": "download",
|
||||
},
|
||||
})),
|
||||
);
|
||||
assert_eq!(manifest.get("devDependencies"), Some(&json!({})));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn convert_dependencies_runtime_preserves_other_single_runtime_as_array() {
|
||||
let mut manifest = json!({
|
||||
"devEngines": {
|
||||
"runtime": {
|
||||
"name": "deno",
|
||||
"version": "1",
|
||||
},
|
||||
},
|
||||
"devDependencies": {
|
||||
"node": "runtime:22",
|
||||
},
|
||||
});
|
||||
convert_dependencies_to_engines_runtime(&mut manifest, "devDependencies", "devEngines")
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
manifest.get("devEngines"),
|
||||
Some(&json!({
|
||||
"runtime": [
|
||||
{
|
||||
"name": "deno",
|
||||
"version": "1",
|
||||
},
|
||||
{
|
||||
"name": "node",
|
||||
"version": "22",
|
||||
"onFail": "download",
|
||||
},
|
||||
],
|
||||
})),
|
||||
);
|
||||
assert_eq!(manifest.get("devDependencies"), Some(&json!({})));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_converts_runtime_dependencies_before_writing() {
|
||||
let dir = tempdir().unwrap();
|
||||
let path = dir.path().join("package.json");
|
||||
let mut manifest = PackageManifest::create_if_needed(path.clone()).unwrap();
|
||||
manifest.add_dependency("node", "runtime:22", DependencyGroup::Dev).unwrap();
|
||||
manifest.add_dependency("bun", "runtime:1.2.0", DependencyGroup::Prod).unwrap();
|
||||
manifest.save().unwrap();
|
||||
|
||||
let saved: serde_json::Value =
|
||||
serde_json::from_str(&read_to_string(path).unwrap()).expect("parse saved manifest");
|
||||
assert_eq!(
|
||||
saved.get("devEngines"),
|
||||
Some(&json!({
|
||||
"runtime": {
|
||||
"name": "node",
|
||||
"version": "22",
|
||||
"onFail": "download",
|
||||
},
|
||||
})),
|
||||
);
|
||||
assert_eq!(
|
||||
saved.get("engines"),
|
||||
Some(&json!({
|
||||
"runtime": {
|
||||
"name": "bun",
|
||||
"version": "1.2.0",
|
||||
"onFail": "download",
|
||||
},
|
||||
})),
|
||||
);
|
||||
assert_eq!(saved.get("devDependencies"), Some(&json!({})));
|
||||
assert_eq!(saved.get("dependencies"), Some(&json!({})));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_and_get_written_value_returns_saved_manifest() {
|
||||
let dir = tempdir().unwrap();
|
||||
let path = dir.path().join("package.json");
|
||||
let mut manifest = PackageManifest::create_if_needed(path.clone()).unwrap();
|
||||
manifest.add_dependency("node", "runtime:22", DependencyGroup::Dev).unwrap();
|
||||
|
||||
let written = manifest.save_and_get_written_value().unwrap();
|
||||
let saved: serde_json::Value =
|
||||
serde_json::from_str(&read_to_string(path).unwrap()).expect("parse saved manifest");
|
||||
|
||||
assert_eq!(written, saved);
|
||||
assert_eq!(
|
||||
saved.get("devEngines"),
|
||||
Some(&json!({
|
||||
"runtime": {
|
||||
"name": "node",
|
||||
"version": "22",
|
||||
"onFail": "download",
|
||||
},
|
||||
})),
|
||||
);
|
||||
assert_eq!(saved.get("devDependencies"), Some(&json!({})));
|
||||
assert_eq!(manifest.value().get("devDependencies"), Some(&json!({ "node": "runtime:22" })));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_prunes_removed_reified_runtime_entry() {
|
||||
let dir = tempdir().unwrap();
|
||||
let path = dir.path().join("package.json");
|
||||
let raw = json!({
|
||||
"name": "fixture",
|
||||
"devEngines": {
|
||||
"runtime": {
|
||||
"name": "node",
|
||||
"version": "22",
|
||||
"onFail": "download",
|
||||
},
|
||||
},
|
||||
});
|
||||
std::fs::write(&path, serde_json::to_string_pretty(&raw).unwrap()).unwrap();
|
||||
|
||||
let mut manifest = PackageManifest::from_path(path.clone()).unwrap();
|
||||
manifest.remove_dependencies(&["node".to_string()], Some(DependencyGroup::Dev));
|
||||
manifest.save().unwrap();
|
||||
|
||||
let saved: serde_json::Value =
|
||||
serde_json::from_str(&read_to_string(path).unwrap()).expect("parse saved manifest");
|
||||
assert_eq!(saved.get("devEngines"), Some(&json!({})));
|
||||
assert_eq!(saved.get("devDependencies"), Some(&json!({})));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_prunes_only_removed_reified_runtime_entry_from_array() {
|
||||
let dir = tempdir().unwrap();
|
||||
let path = dir.path().join("package.json");
|
||||
let raw = json!({
|
||||
"name": "fixture",
|
||||
"devEngines": {
|
||||
"runtime": [
|
||||
{
|
||||
"name": "node",
|
||||
"version": "22",
|
||||
"onFail": "download",
|
||||
},
|
||||
{
|
||||
"name": "deno",
|
||||
"version": "2",
|
||||
"onFail": "download",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
std::fs::write(&path, serde_json::to_string_pretty(&raw).unwrap()).unwrap();
|
||||
|
||||
let mut manifest = PackageManifest::from_path(path.clone()).unwrap();
|
||||
manifest.remove_dependencies(&["node".to_string()], Some(DependencyGroup::Dev));
|
||||
manifest.save().unwrap();
|
||||
|
||||
let saved: serde_json::Value =
|
||||
serde_json::from_str(&read_to_string(path).unwrap()).expect("parse saved manifest");
|
||||
assert_eq!(
|
||||
saved.get("devEngines"),
|
||||
Some(&json!({
|
||||
"runtime": [
|
||||
{
|
||||
"name": "deno",
|
||||
"version": "2",
|
||||
"onFail": "download",
|
||||
},
|
||||
],
|
||||
})),
|
||||
);
|
||||
assert_eq!(saved.get("devDependencies"), Some(&json!({})));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_save_preserves_existing_file_contents() {
|
||||
let dir = tempdir().unwrap();
|
||||
let path = dir.path().join("package.json");
|
||||
let raw = serde_json::to_string_pretty(&json!({
|
||||
"name": "fixture",
|
||||
"devEngines": "invalid",
|
||||
"devDependencies": {
|
||||
"node": "runtime:22",
|
||||
},
|
||||
}))
|
||||
.unwrap();
|
||||
std::fs::write(&path, &raw).unwrap();
|
||||
|
||||
let manifest = PackageManifest::from_path(path.clone()).unwrap();
|
||||
assert!(matches!(manifest.save(), Err(PackageManifestError::InvalidAttribute(_))));
|
||||
assert_eq!(read_to_string(path).unwrap(), raw);
|
||||
}
|
||||
|
||||
/// Reading a manifest with `devEngines.runtime` set must apply the
|
||||
/// reification automatically — that's the hook upstream wires into
|
||||
/// `convertManifestAfterRead`. Verifies the `from_path` end of the
|
||||
|
||||
@@ -279,10 +279,36 @@ function convertDependenciesToEnginesRuntime (
|
||||
if (manifest[dependenciesFieldName]) {
|
||||
delete manifest[dependenciesFieldName][runtimeName]
|
||||
}
|
||||
} else {
|
||||
removeManagedRuntimeEntry(manifest[enginesFieldName], runtimeName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function removeManagedRuntimeEntry (
|
||||
enginesField: ProjectManifest['devEngines'] | ProjectManifest['engines'],
|
||||
runtimeName: string
|
||||
): void {
|
||||
if (!enginesField?.runtime) return
|
||||
|
||||
if (Array.isArray(enginesField.runtime)) {
|
||||
const runtimes = enginesField.runtime.filter((runtime) => !isManagedRuntimeEntry(runtime, runtimeName))
|
||||
if (runtimes.length === 0) {
|
||||
delete enginesField.runtime
|
||||
} else {
|
||||
enginesField.runtime = runtimes
|
||||
}
|
||||
} else if (isManagedRuntimeEntry(enginesField.runtime, runtimeName)) {
|
||||
delete enginesField.runtime
|
||||
}
|
||||
}
|
||||
|
||||
function isManagedRuntimeEntry (runtime: EngineDependency, runtimeName: string): boolean {
|
||||
return runtime.name === runtimeName &&
|
||||
runtime.onFail === 'download' &&
|
||||
typeof runtime.version === 'string'
|
||||
}
|
||||
|
||||
const dependencyKeys = new Set([
|
||||
'dependencies',
|
||||
'devDependencies',
|
||||
|
||||
@@ -96,6 +96,82 @@ test('readProjectManifest() converts engines runtime to dependencies', async ()
|
||||
})
|
||||
})
|
||||
|
||||
test('writeProjectManifest() removes a single devEngines runtime when its dependency was removed', async () => {
|
||||
const dir = f.prepare('package-json-with-dev-engines')
|
||||
const { manifest, writeProjectManifest } = await tryReadProjectManifest(dir)
|
||||
|
||||
delete manifest!.devDependencies!.node
|
||||
await writeProjectManifest(manifest!)
|
||||
|
||||
const pkgJson = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'))
|
||||
expect(pkgJson).toStrictEqual({
|
||||
devEngines: {},
|
||||
})
|
||||
})
|
||||
|
||||
test('writeProjectManifest() removes an empty-version devEngines runtime when its dependency was removed', async () => {
|
||||
const dir = f.prepare('package-json')
|
||||
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({
|
||||
devDependencies: {
|
||||
node: 'runtime:',
|
||||
},
|
||||
devEngines: {
|
||||
runtime: {
|
||||
name: 'node',
|
||||
version: '',
|
||||
onFail: 'download',
|
||||
},
|
||||
},
|
||||
}))
|
||||
const { manifest, writeProjectManifest } = await tryReadProjectManifest(dir)
|
||||
|
||||
delete manifest!.devDependencies!.node
|
||||
await writeProjectManifest(manifest!)
|
||||
|
||||
const pkgJson = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'))
|
||||
expect(pkgJson).toStrictEqual({
|
||||
devEngines: {},
|
||||
})
|
||||
})
|
||||
|
||||
test('writeProjectManifest() removes only the removed runtime from a devEngines runtime array', async () => {
|
||||
const dir = f.prepare('package-json')
|
||||
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({
|
||||
devEngines: {
|
||||
runtime: [
|
||||
{
|
||||
name: 'node',
|
||||
version: '24',
|
||||
onFail: 'download',
|
||||
},
|
||||
{
|
||||
name: 'deno',
|
||||
version: '2',
|
||||
onFail: 'download',
|
||||
},
|
||||
],
|
||||
},
|
||||
}))
|
||||
const { manifest, writeProjectManifest } = await tryReadProjectManifest(dir)
|
||||
|
||||
delete manifest!.devDependencies!.node
|
||||
await writeProjectManifest(manifest!)
|
||||
|
||||
const pkgJson = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'))
|
||||
expect(pkgJson).toStrictEqual({
|
||||
devDependencies: {},
|
||||
devEngines: {
|
||||
runtime: [
|
||||
{
|
||||
name: 'deno',
|
||||
version: '2',
|
||||
onFail: 'download',
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test.each([
|
||||
{
|
||||
name: 'creates devEngines when it is missing',
|
||||
|
||||
Reference in New Issue
Block a user