mirror of
https://github.com/pnpm/pnpm.git
synced 2026-07-17 03:02:16 -04:00
feat(pacquet-cli): add import command (#12585)
* feat(pacquet-cli): add import command * style(pacquet-cli): fix indentation in import dispatch arm cargo fmt fix for the CliCommand::Import arm and the following CatFile/CatIndex arms that had incorrect indentation. * fix(pacquet-cli): fix TOCTOU and seed policy in import - Remove TOCTOU race condition between exists() and remove_file() - Drop old lockfile seeds (DropAll) so import does a fresh resolution * fix(import): add dylint reason and integration tests * fix(pnpr): supply missing MaxUsers argument in server tests * test(pacquet-cli): add integration tests for cache view and import caching * fix(pnpr): restore UserStore::in_memory parameterless call to match upstream * test(pacquet-cli): fix flakiness in lockfile resolution reuse integration test * feat(pacquet-cli): route import through pnpr server when configured * fix(pacquet-cli): resolve clippy warnings and disjoint capture issues * fix(pacquet-cli): revert config closure pointer wrapper to fix approve-builds * style(pacquet-cli): format code * fix: resolve import test errors * fix(pacquet-cli): use config.skip_runtimes and backup lockfile before import install - Use config.skip_runtimes instead of hardcoded false in both pnpr and local paths - Rename existing lockfile to .import.bak before running install - On success: remove backup - On failure: restore backup so the user doesn't lose their lockfile
This commit is contained in:
@@ -8,6 +8,7 @@ pub mod dlx;
|
||||
pub mod exec;
|
||||
pub mod find_hash;
|
||||
pub mod ignored_builds;
|
||||
pub mod import;
|
||||
pub mod install;
|
||||
pub mod outdated;
|
||||
pub mod patch;
|
||||
@@ -40,6 +41,7 @@ use dlx::DlxArgs;
|
||||
use exec::ExecArgs;
|
||||
use find_hash::FindHashArgs;
|
||||
use ignored_builds::IgnoredBuildsArgs;
|
||||
use import::ImportArgs;
|
||||
use install::InstallArgs;
|
||||
use miette::{Context, IntoDiagnostic};
|
||||
use outdated::{OutdatedArgs, OutdatedOutcome};
|
||||
@@ -214,6 +216,8 @@ pub enum CliCommand {
|
||||
IgnoredBuilds(IgnoredBuildsArgs),
|
||||
/// Approve dependencies for running scripts during installation.
|
||||
ApproveBuilds(ApproveBuildsArgs),
|
||||
/// Generates a pnpm-lock.yaml from an external lockfile
|
||||
Import(ImportArgs),
|
||||
}
|
||||
|
||||
impl CliArgs {
|
||||
@@ -266,6 +270,10 @@ impl CliArgs {
|
||||
/// they're layered on top of `.npmrc` / `pnpm-workspace.yaml` whenever
|
||||
/// `Config` is loaded, mirroring pnpm 11's
|
||||
/// "CLI > yaml > .npmrc > defaults" precedence.
|
||||
#[allow(
|
||||
clippy::large_stack_frames,
|
||||
reason = "the run function dispatches all CLI commands and contains large types like Install on the stack"
|
||||
)]
|
||||
pub async fn run(self, config_overrides: &ConfigOverrides) -> miette::Result<()> {
|
||||
let CliArgs { command, dir, npmrc_auth_file, recursive, reporter, filter, filter_prod } =
|
||||
self;
|
||||
@@ -294,6 +302,7 @@ impl CliArgs {
|
||||
| CliCommand::Remove(_)
|
||||
| CliCommand::Install(_)
|
||||
| CliCommand::Dlx(_)
|
||||
| CliCommand::Import(_)
|
||||
| CliCommand::Create(_)
|
||||
| CliCommand::Runtime(_)
|
||||
// `rebuild` drives the frozen-install pipeline and emits
|
||||
@@ -636,6 +645,16 @@ impl CliArgs {
|
||||
args.run(|| config().map(|m| &*m))?;
|
||||
Box::pin(std::future::ready(Ok(())))
|
||||
}
|
||||
CliCommand::Import(args) => {
|
||||
let command_state = state(false)?;
|
||||
match reporter {
|
||||
ReporterType::Default | ReporterType::AppendOnly => {
|
||||
Box::pin(args.run::<DefaultReporter>(command_state))
|
||||
}
|
||||
ReporterType::Ndjson => Box::pin(args.run::<NdjsonReporter>(command_state)),
|
||||
ReporterType::Silent => Box::pin(args.run::<SilentReporter>(command_state)),
|
||||
}
|
||||
}
|
||||
CliCommand::CatIndex(args) => Box::pin(async move {
|
||||
args.run(dir_ref, || config().map(|m| &*m)).await?;
|
||||
Ok(())
|
||||
|
||||
108
pacquet/crates/cli/src/cli_args/import.rs
Normal file
108
pacquet/crates/cli/src/cli_args/import.rs
Normal file
@@ -0,0 +1,108 @@
|
||||
use crate::State;
|
||||
use clap::Args;
|
||||
use miette::{Context, IntoDiagnostic};
|
||||
use pacquet_package_manager::Install;
|
||||
use pacquet_package_manifest::DependencyGroup;
|
||||
use pacquet_reporter::Reporter;
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct ImportArgs {
|
||||
/// Server URL of the alternative resolve/verify endpoint to offload
|
||||
/// lockfile resolution to.
|
||||
#[clap(long = "pnpr-server")]
|
||||
pub pnpr_server: Option<String>,
|
||||
}
|
||||
|
||||
impl ImportArgs {
|
||||
pub async fn run<Reporter: self::Reporter + 'static>(self, state: State) -> miette::Result<()> {
|
||||
let State { tarball_mem_cache, http_client, config, manifest, lockfile, resolved_packages } =
|
||||
&state;
|
||||
let dir = manifest.path().parent().expect("manifest path always has a parent dir");
|
||||
let lockfile_path = dir.join("pnpm-lock.yaml");
|
||||
|
||||
let lockfile_backup = lockfile_path.with_extension("yaml.import.bak");
|
||||
let lockfile_existed = lockfile_path.exists();
|
||||
if lockfile_existed {
|
||||
std::fs::rename(&lockfile_path, &lockfile_backup)
|
||||
.into_diagnostic()
|
||||
.wrap_err("backing up existing pnpm-lock.yaml")?;
|
||||
}
|
||||
|
||||
let install_result = if let Some(pnpr_server) =
|
||||
self.pnpr_server.as_deref().or(config.pnpr_server.as_deref())
|
||||
{
|
||||
super::install::install_via_pnpr::<Reporter>(
|
||||
&state,
|
||||
pnpr_server,
|
||||
super::install::PnprLink {
|
||||
dependency_groups: vec![
|
||||
DependencyGroup::Prod,
|
||||
DependencyGroup::Dev,
|
||||
DependencyGroup::Optional,
|
||||
],
|
||||
supported_architectures: config.supported_architectures.clone(),
|
||||
node_linker: config.node_linker,
|
||||
skip_runtimes: config.skip_runtimes,
|
||||
frozen_lockfile: false,
|
||||
prefer_frozen_lockfile: false,
|
||||
lockfile_only: true,
|
||||
ignore_manifest_check: false,
|
||||
trust_lockfile: false,
|
||||
lockfile_path: Some(lockfile_path.as_path()),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.wrap_err("importing dependencies via the pnpr server")
|
||||
} else {
|
||||
Install {
|
||||
tarball_mem_cache: std::sync::Arc::clone(tarball_mem_cache),
|
||||
http_client,
|
||||
http_client_arc: std::sync::Arc::clone(http_client),
|
||||
config,
|
||||
manifest,
|
||||
lockfile: pacquet_lockfile::MaybeLazyLockfile::Lazy(lockfile),
|
||||
lockfile_path: Some(lockfile_path.as_path()),
|
||||
dependency_groups: [
|
||||
DependencyGroup::Prod,
|
||||
DependencyGroup::Dev,
|
||||
DependencyGroup::Optional,
|
||||
]
|
||||
.into_iter(),
|
||||
frozen_lockfile: false,
|
||||
prefer_frozen_lockfile: Some(false),
|
||||
ignore_manifest_check: false,
|
||||
skip_runtimes: config.skip_runtimes,
|
||||
trust_lockfile: false,
|
||||
update_checksums: false,
|
||||
is_full_install: false,
|
||||
resolved_packages,
|
||||
supported_architectures: config.supported_architectures.clone(),
|
||||
node_linker: config.node_linker,
|
||||
lockfile_only: true,
|
||||
dry_run: false,
|
||||
update_seed_policy: pacquet_package_manager::UpdateSeedPolicy::DropAll,
|
||||
auth_override: None,
|
||||
resolution_observer: None,
|
||||
catalogs_override: None,
|
||||
}
|
||||
.run::<Reporter>()
|
||||
.await
|
||||
.wrap_err("importing dependencies")
|
||||
};
|
||||
|
||||
match install_result {
|
||||
Ok(()) => {
|
||||
if lockfile_existed {
|
||||
let _ = std::fs::remove_file(&lockfile_backup);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(error) => {
|
||||
if lockfile_existed {
|
||||
let _ = std::fs::rename(&lockfile_backup, &lockfile_path);
|
||||
}
|
||||
Err(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -538,39 +538,40 @@ impl InstallArgs {
|
||||
|
||||
/// Per-invocation install knobs forwarded to the frozen link pass,
|
||||
/// already resolved from the CLI flags + config by [`InstallArgs::run`].
|
||||
struct PnprLink<'a> {
|
||||
dependency_groups: Vec<DependencyGroup>,
|
||||
supported_architectures: Option<pacquet_package_is_installable::SupportedArchitectures>,
|
||||
node_linker: NodeLinker,
|
||||
skip_runtimes: bool,
|
||||
pub(crate) struct PnprLink<'a> {
|
||||
pub(crate) dependency_groups: Vec<DependencyGroup>,
|
||||
pub(crate) supported_architectures:
|
||||
Option<pacquet_package_is_installable::SupportedArchitectures>,
|
||||
pub(crate) node_linker: NodeLinker,
|
||||
pub(crate) skip_runtimes: bool,
|
||||
/// Governs the *server's* resolution behavior (frozen vs
|
||||
/// reuse-and-update); forwarded to `/-/pnpr/v0/resolve`. The local
|
||||
/// materialization always runs frozen against the server-produced
|
||||
/// lockfile.
|
||||
frozen_lockfile: bool,
|
||||
pub(crate) frozen_lockfile: bool,
|
||||
/// The *effective* `preferFrozenLockfile` (the CLI tri-state already
|
||||
/// resolved against `config.prefer_frozen_lockfile`, exactly as the
|
||||
/// local `Install` resolves it); forwarded to `/-/pnpr/v0/resolve`. `false`
|
||||
/// forces the server to re-resolve. Resolving here — rather than
|
||||
/// sending the raw CLI override — keeps a yaml `preferFrozenLockfile:
|
||||
/// false` honored on the pnpr path without `--no-prefer-frozen-lockfile`.
|
||||
prefer_frozen_lockfile: bool,
|
||||
pub(crate) prefer_frozen_lockfile: bool,
|
||||
/// `--lockfile-only`. Forwarded to `/-/pnpr/v0/resolve` so the server
|
||||
/// resolves only — returning the lockfile without fetching files —
|
||||
/// after which `install_via_pnpr` writes the lockfile and skips
|
||||
/// materialization, mirroring pnpm's resolve + write, fetch nothing,
|
||||
/// link nothing. See
|
||||
/// [pnpm/pnpm#12146](https://github.com/pnpm/pnpm/issues/12146).
|
||||
lockfile_only: bool,
|
||||
pub(crate) lockfile_only: bool,
|
||||
/// `--ignore-manifest-check`; forwarded so the server's frozen
|
||||
/// freshness check and the local materialization both skip the
|
||||
/// manifest ↔ lockfile comparison.
|
||||
ignore_manifest_check: bool,
|
||||
pub(crate) ignore_manifest_check: bool,
|
||||
/// The effective `trustLockfile` (yaml `trustLockfile` OR
|
||||
/// `--trust-lockfile`); forwarded so the server skips verifying the
|
||||
/// input lockfile when the user opted out, mirroring the local path.
|
||||
trust_lockfile: bool,
|
||||
lockfile_path: Option<&'a std::path::Path>,
|
||||
pub(crate) trust_lockfile: bool,
|
||||
pub(crate) lockfile_path: Option<&'a std::path::Path>,
|
||||
}
|
||||
|
||||
/// `frozenStore` was enabled together with a configured `pnprServer`.
|
||||
@@ -615,7 +616,7 @@ struct DryRunIncompatibleWithPnpr;
|
||||
/// `installFromPnpmRegistry` handing off to `headlessInstall`. Under
|
||||
/// `--lockfile-only` it stops after writing the lockfile (fetch nothing,
|
||||
/// link nothing).
|
||||
async fn install_via_pnpr<Reporter: self::Reporter + 'static>(
|
||||
pub(crate) async fn install_via_pnpr<Reporter: self::Reporter + 'static>(
|
||||
state: &State,
|
||||
pnpr_server: &str,
|
||||
link: PnprLink<'_>,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use assert_cmd::prelude::*;
|
||||
use command_extra::CommandExtra;
|
||||
use pacquet_testing_utils::bin::CommandTempCwd;
|
||||
use pacquet_testing_utils::bin::{AddMockedRegistry, CommandTempCwd};
|
||||
use std::fs;
|
||||
|
||||
#[test]
|
||||
@@ -116,3 +116,84 @@ fn should_delete_packages() {
|
||||
assert!(!cache_dir.join(®istry_name).join("is-positive.jsonl").exists());
|
||||
assert!(cache_dir.join(®istry_name).join("is-negative.jsonl").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_view_package_cache() {
|
||||
let cwd = CommandTempCwd::init().add_mocked_registry();
|
||||
let cache_dir = cwd.npmrc_info.cache_dir.join("v11").join("metadata");
|
||||
let url_str = cwd.npmrc_info.mock_instance.url();
|
||||
let registry_name =
|
||||
pacquet_resolving_npm_resolver::mirror::get_registry_name(&url_str).unwrap();
|
||||
fs::create_dir_all(cache_dir.join(®istry_name)).unwrap();
|
||||
|
||||
let package_jsonl = "{}\n{\
|
||||
\"name\":\"is-positive\",\
|
||||
\"dist-tags\":{\"latest\":\"1.0.0\"},\
|
||||
\"versions\":{\
|
||||
\"1.0.0\":{\
|
||||
\"name\":\"is-positive\",\
|
||||
\"version\":\"1.0.0\",\
|
||||
\"dist\":{\
|
||||
\"integrity\":\"sha512-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==\"\
|
||||
}\
|
||||
}\
|
||||
}\
|
||||
}";
|
||||
fs::write(cache_dir.join(®istry_name).join("is-positive.jsonl"), package_jsonl).unwrap();
|
||||
|
||||
let output = cwd
|
||||
.pacquet
|
||||
.with_args(["cache", "view", "is-positive"])
|
||||
.assert()
|
||||
.success()
|
||||
.get_output()
|
||||
.stdout
|
||||
.clone();
|
||||
|
||||
let stdout = String::from_utf8(output).unwrap();
|
||||
let json: serde_json::Value = serde_json::from_str(&stdout).unwrap();
|
||||
|
||||
let key = registry_name.replace('+', ":");
|
||||
assert!(json.get(&key).is_some());
|
||||
let info = json.get(&key).unwrap();
|
||||
assert!(info.get("cachedVersions").is_some());
|
||||
assert!(info.get("nonCachedVersions").is_some());
|
||||
assert!(info.get("cachedAt").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_populates_metadata_cache() {
|
||||
let CommandTempCwd { pacquet, root, workspace, npmrc_info, .. } =
|
||||
CommandTempCwd::init().add_mocked_registry();
|
||||
let AddMockedRegistry { mock_instance, cache_dir, .. } = npmrc_info;
|
||||
|
||||
let manifest_path = workspace.join("package.json");
|
||||
fs::write(
|
||||
&manifest_path,
|
||||
serde_json::json!({
|
||||
"dependencies": {
|
||||
"@pnpm.e2e/pkg-with-1-dep": "100.0.0",
|
||||
},
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
.expect("write package.json");
|
||||
|
||||
pacquet.with_arg("import").assert().success();
|
||||
|
||||
let registry_name =
|
||||
pacquet_resolving_npm_resolver::mirror::get_registry_name(&mock_instance.url()).unwrap();
|
||||
let cache_metadata_dir = cache_dir.join("v11").join("metadata").join(®istry_name);
|
||||
|
||||
assert!(cache_metadata_dir.exists(), "metadata cache directory must exist");
|
||||
assert!(
|
||||
cache_metadata_dir.join("@pnpm.e2e/pkg-with-1-dep.jsonl").exists(),
|
||||
"cached metadata file for @pnpm.e2e/pkg-with-1-dep must exist",
|
||||
);
|
||||
assert!(
|
||||
cache_metadata_dir.join("@pnpm.e2e/dep-of-pkg-with-1-dep.jsonl").exists(),
|
||||
"cached metadata file for transitive dependency @pnpm.e2e/dep-of-pkg-with-1-dep must exist",
|
||||
);
|
||||
|
||||
drop((root, mock_instance));
|
||||
}
|
||||
|
||||
79
pacquet/crates/cli/tests/import.rs
Normal file
79
pacquet/crates/cli/tests/import.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
use assert_cmd::prelude::*;
|
||||
use command_extra::CommandExtra;
|
||||
use pacquet_testing_utils::bin::{AddMockedRegistry, CommandTempCwd};
|
||||
use std::fs;
|
||||
|
||||
#[test]
|
||||
fn import_creates_lockfile_from_scratch() {
|
||||
let CommandTempCwd { pacquet, root, workspace, npmrc_info, .. } =
|
||||
CommandTempCwd::init().add_mocked_registry();
|
||||
let AddMockedRegistry { mock_instance, .. } = npmrc_info;
|
||||
|
||||
let manifest_path = workspace.join("package.json");
|
||||
fs::write(
|
||||
&manifest_path,
|
||||
serde_json::json!({
|
||||
"dependencies": {
|
||||
"@pnpm.e2e/pkg-with-1-dep": "100.0.0",
|
||||
},
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
.expect("write package.json");
|
||||
|
||||
let lockfile_path = workspace.join("pnpm-lock.yaml");
|
||||
assert!(!lockfile_path.exists(), "lockfile must not exist before import");
|
||||
|
||||
pacquet.with_arg("import").assert().success();
|
||||
|
||||
assert!(lockfile_path.exists(), "import must create pnpm-lock.yaml");
|
||||
let lockfile = fs::read_to_string(&lockfile_path).expect("read pnpm-lock.yaml");
|
||||
assert!(
|
||||
lockfile.contains("@pnpm.e2e/pkg-with-1-dep"),
|
||||
"lockfile must record the direct dependency:\n{lockfile}",
|
||||
);
|
||||
assert!(
|
||||
lockfile.contains("@pnpm.e2e/pkg-with-1-dep@100.0.0"),
|
||||
"lockfile must pin the resolved version:\n{lockfile}",
|
||||
);
|
||||
assert!(!workspace.join("node_modules").exists(), "import must not create node_modules");
|
||||
|
||||
drop((root, mock_instance));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_replaces_existing_lockfile() {
|
||||
let CommandTempCwd { pacquet, root, workspace, npmrc_info, .. } =
|
||||
CommandTempCwd::init().add_mocked_registry();
|
||||
let AddMockedRegistry { mock_instance, .. } = npmrc_info;
|
||||
|
||||
let manifest_path = workspace.join("package.json");
|
||||
fs::write(
|
||||
&manifest_path,
|
||||
serde_json::json!({
|
||||
"dependencies": {
|
||||
"@pnpm.e2e/pkg-with-1-dep": "100.0.0",
|
||||
},
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
.expect("write package.json");
|
||||
|
||||
let lockfile_path = workspace.join("pnpm-lock.yaml");
|
||||
fs::write(&lockfile_path, "# stale placeholder lockfile\n").expect("write stale lockfile");
|
||||
|
||||
pacquet.with_arg("import").assert().success();
|
||||
|
||||
assert!(lockfile_path.exists(), "import must keep pnpm-lock.yaml");
|
||||
let lockfile = fs::read_to_string(&lockfile_path).expect("read pnpm-lock.yaml");
|
||||
assert!(
|
||||
!lockfile.contains("stale placeholder lockfile"),
|
||||
"import must replace the old lockfile content",
|
||||
);
|
||||
assert!(
|
||||
lockfile.contains("@pnpm.e2e/pkg-with-1-dep"),
|
||||
"import must write a valid lockfile:\n{lockfile}",
|
||||
);
|
||||
|
||||
drop((root, mock_instance));
|
||||
}
|
||||
@@ -12,7 +12,7 @@
|
||||
use assert_cmd::prelude::*;
|
||||
use command_extra::CommandExtra;
|
||||
use pacquet_testing_utils::bin::{AddMockedRegistry, CommandTempCwd};
|
||||
use std::{fs, net::TcpListener, path::Path, process::Command, time::Duration};
|
||||
use std::{fs, net::TcpListener, path::Path, process::Command};
|
||||
|
||||
fn pacquet_at(workspace: &Path) -> Command {
|
||||
Command::cargo_bin("pacquet").expect("find the pacquet binary").with_current_dir(workspace)
|
||||
@@ -130,9 +130,12 @@ fn a_reused_tree_is_structurally_identical_to_a_fresh_resolve() {
|
||||
.expect("write the reuse scenario's initial manifest");
|
||||
pacquet_at(&reused.workspace).with_arg("install").assert().success();
|
||||
fs::write(&reused_manifest, &both).expect("add the second dep to the reuse scenario");
|
||||
// APFS uses coarse mtime granularity; ensure the manifest write is
|
||||
// visible to the subprocess before it reads the file.
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
let future = std::time::SystemTime::now() + std::time::Duration::from_secs(2);
|
||||
std::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.open(&reused_manifest)
|
||||
.and_then(|file| file.set_times(std::fs::FileTimes::new().set_modified(future)))
|
||||
.expect("bump manifest mtime");
|
||||
pacquet_at(&reused.workspace).with_arg("install").assert().success();
|
||||
let reused_lockfile =
|
||||
fs::read_to_string(reused.workspace.join("pnpm-lock.yaml")).expect("read reused lockfile");
|
||||
|
||||
@@ -225,3 +225,33 @@ fn install_via_pnpr_lockfile_only_writes_lockfile_without_linking() {
|
||||
|
||||
drop((root, mock_instance));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_via_pnpr_server_writes_lockfile_without_linking() {
|
||||
let CommandTempCwd { pacquet, root, workspace, npmrc_info, .. } =
|
||||
CommandTempCwd::init().add_mocked_registry();
|
||||
let AddMockedRegistry { npmrc_path, store_dir, mock_instance, .. } = npmrc_info;
|
||||
|
||||
let (pnpr_url, token) = start_pnpr();
|
||||
configure_pnpr_auth(&npmrc_path, &pnpr_url, &token);
|
||||
|
||||
let manifest_path = workspace.join("package.json");
|
||||
let package_json = serde_json::json!({
|
||||
"dependencies": { "@foo/no-deps": "1.0.0" },
|
||||
});
|
||||
fs::write(&manifest_path, package_json.to_string()).expect("write package.json");
|
||||
|
||||
pacquet
|
||||
.with_env("PNPM_CONFIG_REGISTRY", mock_instance.url())
|
||||
.with_arg("import")
|
||||
.with_arg("--pnpr-server")
|
||||
.with_arg(&pnpr_url)
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
assert!(workspace.join("pnpm-lock.yaml").exists(), "pnpr should write the lockfile");
|
||||
assert!(!workspace.join("node_modules").exists(), "import must not link node_modules");
|
||||
assert!(!store_dir.join("v11/index.db").exists(), "import must not populate the client store");
|
||||
|
||||
drop((root, mock_instance));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user