diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/npm_global_uninstall_prefix/private-runtime.mjs b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/npm_global_uninstall_prefix/private-runtime.mjs new file mode 100644 index 0000000000..11805b87c7 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/npm_global_uninstall_prefix/private-runtime.mjs @@ -0,0 +1,15 @@ +import assert from 'node:assert/strict'; +import { cpSync, lstatSync, mkdirSync, symlinkSync, unlinkSync, writeFileSync } from 'node:fs'; +import { dirname, join, resolve } from 'node:path'; + +// Keep global npm installs out of the runner's shared runtime seed. +const runtimeLink = join(process.env.VP_HOME, 'js_runtime'); +assert(lstatSync(runtimeLink).isSymbolicLink()); +const privateRuntime = resolve('private-runtime'); +const nodePrefix = join(privateRuntime, 'node', process.versions.node); +mkdirSync(dirname(nodePrefix), { recursive: true }); +cpSync(dirname(dirname(process.execPath)), nodePrefix, { recursive: true, verbatimSymlinks: true }); +unlinkSync(runtimeLink); +symlinkSync(privateRuntime, runtimeLink, 'dir'); +symlinkSync(nodePrefix, 'node-prefix', 'dir'); +writeFileSync('.node-version', process.versions.node); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/npm_global_uninstall_prefix/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/npm_global_uninstall_prefix/snapshots.toml index 8a5c06a549..68e2e17940 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/npm_global_uninstall_prefix/snapshots.toml +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/npm_global_uninstall_prefix/snapshots.toml @@ -11,3 +11,117 @@ steps = [ { argv = ["vpt", "stat-file", "$VP_HOME/bin/npm-global-prefix-cli"], comment = "Should be gone", continue-on-failure = true }, { argv = ["vpt", "rm", "-rf", "custom-prefix"], comment = "Cleanup", continue-on-failure = true }, ] + +[[case]] +name = "npm_global_uninstall_dry_run" +vp = "global" +skip-platforms = ["windows"] +steps = [ + { tty = false, argv = ["npm", "install", "-g", "--prefix", "./custom-prefix", "./npm-global-prefix-pkg"], comment = "Install a local package and create its Vite+ link" }, + { tty = false, argv = ["npm", "uninstall", "-g", "--prefix", "./custom-prefix", "npm-global-prefix-pkg", "--dry-run"], comment = "CLI dry-run must preserve the package, npm binary, link, and BinConfig" }, + ["vpt", "stat-file", "custom-prefix/lib/node_modules/npm-global-prefix-pkg/package.json", "--assert", "file"], + ["vpt", "stat-file", "custom-prefix/bin/npm-global-prefix-cli", "--assert", "symlink"], + ["vpt", "stat-file", "$VP_HOME/bin/npm-global-prefix-cli", "--assert", "symlink"], + ["vpt", "stat-file", "$VP_HOME/bins/npm-global-prefix-cli.json", "--assert", "file"], + ["npm-global-prefix-cli"], + { tty = false, argv = ["npm", "uninstall", "-g", "--prefix", "./custom-prefix", "npm-global-prefix-pkg"], envs = [["npm_config_dry_run", "true"]], comment = "Environment-configured dry-run must also preserve installed state" }, + ["vpt", "stat-file", "custom-prefix/lib/node_modules/npm-global-prefix-pkg/package.json", "--assert", "file"], + ["vpt", "stat-file", "custom-prefix/bin/npm-global-prefix-cli", "--assert", "symlink"], + ["vpt", "stat-file", "$VP_HOME/bin/npm-global-prefix-cli", "--assert", "symlink"], + ["vpt", "stat-file", "$VP_HOME/bins/npm-global-prefix-cli.json", "--assert", "file"], + ["npm-global-prefix-cli"], + ["vpt", "write-file", "dry-run.npmrc", "dry-run=true\n"], + { tty = false, argv = ["npm", "uninstall", "-g", "--prefix", "./custom-prefix", "npm-global-prefix-pkg"], envs = [["npm_config_userconfig", "${workspace}/dry-run.npmrc"]], comment = "User npmrc dry-run must also preserve installed state" }, + ["vpt", "stat-file", "custom-prefix/lib/node_modules/npm-global-prefix-pkg/package.json", "--assert", "file"], + ["vpt", "stat-file", "custom-prefix/bin/npm-global-prefix-cli", "--assert", "symlink"], + ["vpt", "stat-file", "$VP_HOME/bin/npm-global-prefix-cli", "--assert", "symlink"], + ["vpt", "stat-file", "$VP_HOME/bins/npm-global-prefix-cli.json", "--assert", "file"], + ["npm-global-prefix-cli"], + { tty = false, argv = ["npm", "uninstall", "-g", "--prefix", "./custom-prefix", "npm-global-prefix-pkg"], comment = "A real uninstall must still clean up the package, npm binary, link, and BinConfig" }, + ["vpt", "stat-file", "custom-prefix/lib/node_modules/npm-global-prefix-pkg", "--assert", "missing"], + ["vpt", "stat-file", "custom-prefix/bin/npm-global-prefix-cli", "--assert", "missing"], + ["vpt", "stat-file", "$VP_HOME/bin/npm-global-prefix-cli", "--assert", "missing"], + ["vpt", "stat-file", "$VP_HOME/bins/npm-global-prefix-cli.json", "--assert", "missing"], +] + +[[case]] +name = "npm_global_uninstall_preserves_local_copy" +vp = "global" +skip-platforms = ["windows"] +steps = [ + ["vpt", "mkdir", "-p", "custom-prefix"], + ["vpt", "write-file", "custom-prefix/package.json", "{\"private\":true}"], + { tty = false, argv = ["npm", "install", "--prefix", "./custom-prefix", "--ignore-scripts", "--no-save", "--no-package-lock", "--no-audit", "./npm-global-prefix-pkg"], comment = "Install a local copy under the same prefix" }, + { tty = false, argv = ["npm", "install", "-g", "--prefix", "./custom-prefix", "./npm-global-prefix-pkg"], comment = "Install a separate global copy and create its Vite+ link" }, + ["vpt", "stat-file", "custom-prefix/node_modules/npm-global-prefix-pkg/package.json", "--assert", "file"], + ["vpt", "stat-file", "custom-prefix/lib/node_modules/npm-global-prefix-pkg/package.json", "--assert", "file"], + ["vpt", "stat-file", "$VP_HOME/bin/npm-global-prefix-cli", "--assert", "symlink"], + ["vpt", "stat-file", "$VP_HOME/bins/npm-global-prefix-cli.json", "--assert", "file"], + { tty = false, argv = ["npm", "uninstall", "-g", "--prefix", "./custom-prefix", "npm-global-prefix-pkg", "--dry-run"], comment = "Dry-run must preserve the global command even when a local copy also exists" }, + ["npm-global-prefix-cli"], + ["vpt", "stat-file", "$VP_HOME/bins/npm-global-prefix-cli.json", "--assert", "file"], + { tty = false, argv = ["npm", "uninstall", "-g", "--prefix", "./custom-prefix", "npm-global-prefix-pkg"], comment = "Real uninstall must remove the global links without confusing the remaining local copy for the global installation" }, + ["vpt", "stat-file", "custom-prefix/lib/node_modules/npm-global-prefix-pkg", "--assert", "missing"], + ["vpt", "stat-file", "custom-prefix/bin/npm-global-prefix-cli", "--assert", "missing"], + ["vpt", "stat-file", "$VP_HOME/bin/npm-global-prefix-cli", "--assert", "missing"], + ["vpt", "stat-file", "$VP_HOME/bins/npm-global-prefix-cli.json", "--assert", "missing"], + ["vpt", "stat-file", "custom-prefix/node_modules/npm-global-prefix-pkg/package.json", "--assert", "file"], + ["node", "custom-prefix/node_modules/npm-global-prefix-pkg/cli.js"], +] + +[[case]] +name = "npm_global_uninstall_userconfig_fallback" +vp = "global" +skip-platforms = ["windows"] +# Require the seeded runtime instead of downloading one for this case. +env = { VP_NODE_DIST_MIRROR = "http://127.0.0.1:9" } +unset-env = ["NPM_CONFIG_PREFIX"] +steps = [ + ["node", "private-runtime.mjs"], + { tty = false, argv = ["npm", "install", "-g", "./npm-global-prefix-pkg"], comment = "Install into a private copy of the selected Node runtime" }, + ["vpt", "stat-file", "node-prefix/lib/node_modules/npm-global-prefix-pkg/package.json", "--assert", "file"], + ["vpt", "stat-file", "$VP_HOME/bin/npm-global-prefix-cli", "--assert", "symlink"], + ["vpt", "stat-file", "$VP_HOME/bins/npm-global-prefix-cli.json", "--assert", "file"], + { tty = false, argv = ["npm", "config", "set", "prefix", "./other-prefix", "--location=user"], comment = "Make the normal user config point to another prefix" }, + ["vpt", "write-file", "empty.npmrc", ""], + { tty = false, argv = ["npm", "uninstall", "-g", "--userconfig=empty.npmrc", "npm-global-prefix-pkg", "--dry-run"], comment = "A config override dry-run must preserve the fallback installation" }, + ["npm-global-prefix-cli"], + ["vpt", "stat-file", "$VP_HOME/bins/npm-global-prefix-cli.json", "--assert", "file"], + { tty = false, argv = ["npm", "uninstall", "-g", "npm-global-prefix-pkg"], comment = "Uninstalling from an empty prefix must not remove links to the surviving fallback package" }, + ["npm-global-prefix-cli"], + ["vpt", "stat-file", "$VP_HOME/bins/npm-global-prefix-cli.json", "--assert", "file"], + { tty = false, argv = ["npm", "uninstall", "-g", "--userconfig", "empty.npmrc", "npm-global-prefix-pkg"], comment = "A real uninstall using the override must clean up the fallback package's links" }, + ["vpt", "stat-file", "node-prefix/lib/node_modules/npm-global-prefix-pkg", "--assert", "missing"], + ["vpt", "stat-file", "node-prefix/bin/npm-global-prefix-cli", "--assert", "missing"], + ["vpt", "stat-file", "$VP_HOME/bin/npm-global-prefix-cli", "--assert", "missing"], + ["vpt", "stat-file", "$VP_HOME/bins/npm-global-prefix-cli.json", "--assert", "missing"], +] + +[[case]] +name = "npm_global_uninstall_prefix_shorthand" +vp = "global" +skip-platforms = ["windows"] +unset-env = ["NPM_CONFIG_PREFIX"] +steps = [ + { tty = false, argv = ["npm", "install", "-g", "--prefix", "./custom-prefix", "./npm-global-prefix-pkg"] }, + ["vpt", "stat-file", "$VP_HOME/bin/npm-global-prefix-cli", "--assert", "symlink"], + ["vpt", "stat-file", "$VP_HOME/bins/npm-global-prefix-cli.json", "--assert", "file"], + { tty = false, argv = ["npm", "uninstall", "-g", "-C", "./custom-prefix", "npm-global-prefix-pkg", "--dry-run"], envs = [["NPM_CONFIG_PREFIX", "${workspace}/other-prefix"]], comment = "A shorthand prefix override must still preserve links during dry-run" }, + ["npm-global-prefix-cli"], + ["vpt", "stat-file", "$VP_HOME/bins/npm-global-prefix-cli.json", "--assert", "file"], + { tty = false, argv = ["npm", "uninstall", "-g", "-C", "./custom-prefix", "npm-global-prefix-pkg"], envs = [["NPM_CONFIG_PREFIX", "${workspace}/other-prefix"]], comment = "-C must override the environment prefix for uninstall link cleanup" }, + ["vpt", "stat-file", "custom-prefix/lib/node_modules/npm-global-prefix-pkg", "--assert", "missing"], + ["vpt", "stat-file", "custom-prefix/bin/npm-global-prefix-cli", "--assert", "missing"], + ["vpt", "stat-file", "$VP_HOME/bin/npm-global-prefix-cli", "--assert", "missing"], + ["vpt", "stat-file", "$VP_HOME/bins/npm-global-prefix-cli.json", "--assert", "missing"], + ["vpt", "write-file", "prefix.npmrc", "prefix=${HOME}/npm-prefix\n"], + { tty = false, argv = ["npm", "install", "-g", "-C=./custom-prefix", "./npm-global-prefix-pkg"], envs = [["npm_config_userconfig", "${workspace}/prefix.npmrc"]], comment = "-C= must override the user npmrc prefix when creating links" }, + ["vpt", "stat-file", "$VP_HOME/bin/npm-global-prefix-cli", "--assert", "symlink"], + ["vpt", "stat-file", "$VP_HOME/bins/npm-global-prefix-cli.json", "--assert", "file"], + ["npm-global-prefix-cli"], + { tty = false, argv = ["npm", "-C", "./custom-prefix", "uninstall", "-g", "npm-global-prefix-pkg"], envs = [["npm_config_userconfig", "${workspace}/prefix.npmrc"]], comment = "-C before the subcommand must also override the user npmrc prefix for cleanup" }, + ["vpt", "stat-file", "custom-prefix/lib/node_modules/npm-global-prefix-pkg", "--assert", "missing"], + ["vpt", "stat-file", "custom-prefix/bin/npm-global-prefix-cli", "--assert", "missing"], + ["vpt", "stat-file", "$VP_HOME/bin/npm-global-prefix-cli", "--assert", "missing"], + ["vpt", "stat-file", "$VP_HOME/bins/npm-global-prefix-cli.json", "--assert", "missing"], +] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/npm_global_uninstall_prefix/snapshots/npm_global_uninstall_dry_run.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/npm_global_uninstall_prefix/snapshots/npm_global_uninstall_dry_run.md new file mode 100644 index 0000000000..ade04a31ea --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/npm_global_uninstall_prefix/snapshots/npm_global_uninstall_dry_run.md @@ -0,0 +1,171 @@ +# npm_global_uninstall_dry_run + +## `npm install -g --prefix ./custom-prefix ./npm-global-prefix-pkg` + +Install a local package and create its Vite+ link + +``` + +added 1 package in +Linked 'npm-global-prefix-cli' to /.vite-plus/bin/npm-global-prefix-cli +``` + +## `npm uninstall -g --prefix ./custom-prefix npm-global-prefix-pkg --dry-run` + +CLI dry-run must preserve the package, npm binary, link, and BinConfig + +``` +remove npm-global-prefix-pkg 1.0.0 + +removed 1 package in +``` + +## `vpt stat-file custom-prefix/lib/node_modules/npm-global-prefix-pkg/package.json --assert file` + +``` +custom-prefix/lib/node_modules/npm-global-prefix-pkg/package.json: file +``` + +## `vpt stat-file custom-prefix/bin/npm-global-prefix-cli --assert symlink` + +``` +custom-prefix/bin/npm-global-prefix-cli: symlink +``` + +## `vpt stat-file $VP_HOME/bin/npm-global-prefix-cli --assert symlink` + +``` +/.vite-plus/bin/npm-global-prefix-cli: symlink +``` + +## `vpt stat-file $VP_HOME/bins/npm-global-prefix-cli.json --assert file` + +``` +/.vite-plus/bins/npm-global-prefix-cli.json: file +``` + +## `npm-global-prefix-cli` + +``` +npm-global-prefix-cli works +``` + +## `npm_config_dry_run=true npm uninstall -g --prefix ./custom-prefix npm-global-prefix-pkg` + +Environment-configured dry-run must also preserve installed state + +``` +remove npm-global-prefix-pkg 1.0.0 + +removed 1 package in +``` + +## `vpt stat-file custom-prefix/lib/node_modules/npm-global-prefix-pkg/package.json --assert file` + +``` +custom-prefix/lib/node_modules/npm-global-prefix-pkg/package.json: file +``` + +## `vpt stat-file custom-prefix/bin/npm-global-prefix-cli --assert symlink` + +``` +custom-prefix/bin/npm-global-prefix-cli: symlink +``` + +## `vpt stat-file $VP_HOME/bin/npm-global-prefix-cli --assert symlink` + +``` +/.vite-plus/bin/npm-global-prefix-cli: symlink +``` + +## `vpt stat-file $VP_HOME/bins/npm-global-prefix-cli.json --assert file` + +``` +/.vite-plus/bins/npm-global-prefix-cli.json: file +``` + +## `npm-global-prefix-cli` + +``` +npm-global-prefix-cli works +``` + +## `vpt write-file dry-run.npmrc 'dry-run=true +'` + +``` +``` + +## `npm_config_userconfig=${workspace}/dry-run.npmrc npm uninstall -g --prefix ./custom-prefix npm-global-prefix-pkg` + +User npmrc dry-run must also preserve installed state + +``` +remove npm-global-prefix-pkg 1.0.0 + +removed 1 package in +``` + +## `vpt stat-file custom-prefix/lib/node_modules/npm-global-prefix-pkg/package.json --assert file` + +``` +custom-prefix/lib/node_modules/npm-global-prefix-pkg/package.json: file +``` + +## `vpt stat-file custom-prefix/bin/npm-global-prefix-cli --assert symlink` + +``` +custom-prefix/bin/npm-global-prefix-cli: symlink +``` + +## `vpt stat-file $VP_HOME/bin/npm-global-prefix-cli --assert symlink` + +``` +/.vite-plus/bin/npm-global-prefix-cli: symlink +``` + +## `vpt stat-file $VP_HOME/bins/npm-global-prefix-cli.json --assert file` + +``` +/.vite-plus/bins/npm-global-prefix-cli.json: file +``` + +## `npm-global-prefix-cli` + +``` +npm-global-prefix-cli works +``` + +## `npm uninstall -g --prefix ./custom-prefix npm-global-prefix-pkg` + +A real uninstall must still clean up the package, npm binary, link, and BinConfig + +``` + +removed 1 package in +Removed link 'npm-global-prefix-cli' from /.vite-plus/bin/npm-global-prefix-cli +``` + +## `vpt stat-file custom-prefix/lib/node_modules/npm-global-prefix-pkg --assert missing` + +``` +custom-prefix/lib/node_modules/npm-global-prefix-pkg: missing +``` + +## `vpt stat-file custom-prefix/bin/npm-global-prefix-cli --assert missing` + +``` +custom-prefix/bin/npm-global-prefix-cli: missing +``` + +## `vpt stat-file $VP_HOME/bin/npm-global-prefix-cli --assert missing` + +``` +/.vite-plus/bin/npm-global-prefix-cli: missing +``` + +## `vpt stat-file $VP_HOME/bins/npm-global-prefix-cli.json --assert missing` + +``` +/.vite-plus/bins/npm-global-prefix-cli.json: missing +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/npm_global_uninstall_prefix/snapshots/npm_global_uninstall_prefix_shorthand.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/npm_global_uninstall_prefix/snapshots/npm_global_uninstall_prefix_shorthand.md new file mode 100644 index 0000000000..77f77aaaaf --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/npm_global_uninstall_prefix/snapshots/npm_global_uninstall_prefix_shorthand.md @@ -0,0 +1,145 @@ +# npm_global_uninstall_prefix_shorthand + +## `npm install -g --prefix ./custom-prefix ./npm-global-prefix-pkg` + +``` + +added 1 package in +Linked 'npm-global-prefix-cli' to /.vite-plus/bin/npm-global-prefix-cli +``` + +## `vpt stat-file $VP_HOME/bin/npm-global-prefix-cli --assert symlink` + +``` +/.vite-plus/bin/npm-global-prefix-cli: symlink +``` + +## `vpt stat-file $VP_HOME/bins/npm-global-prefix-cli.json --assert file` + +``` +/.vite-plus/bins/npm-global-prefix-cli.json: file +``` + +## `NPM_CONFIG_PREFIX=${workspace}/other-prefix npm uninstall -g -C ./custom-prefix npm-global-prefix-pkg --dry-run` + +A shorthand prefix override must still preserve links during dry-run + +``` +remove npm-global-prefix-pkg 1.0.0 + +removed 1 package in +``` + +## `npm-global-prefix-cli` + +``` +npm-global-prefix-cli works +``` + +## `vpt stat-file $VP_HOME/bins/npm-global-prefix-cli.json --assert file` + +``` +/.vite-plus/bins/npm-global-prefix-cli.json: file +``` + +## `NPM_CONFIG_PREFIX=${workspace}/other-prefix npm uninstall -g -C ./custom-prefix npm-global-prefix-pkg` + +-C must override the environment prefix for uninstall link cleanup + +``` + +removed 1 package in +Removed link 'npm-global-prefix-cli' from /.vite-plus/bin/npm-global-prefix-cli +``` + +## `vpt stat-file custom-prefix/lib/node_modules/npm-global-prefix-pkg --assert missing` + +``` +custom-prefix/lib/node_modules/npm-global-prefix-pkg: missing +``` + +## `vpt stat-file custom-prefix/bin/npm-global-prefix-cli --assert missing` + +``` +custom-prefix/bin/npm-global-prefix-cli: missing +``` + +## `vpt stat-file $VP_HOME/bin/npm-global-prefix-cli --assert missing` + +``` +/.vite-plus/bin/npm-global-prefix-cli: missing +``` + +## `vpt stat-file $VP_HOME/bins/npm-global-prefix-cli.json --assert missing` + +``` +/.vite-plus/bins/npm-global-prefix-cli.json: missing +``` + +## `vpt write-file prefix.npmrc 'prefix=${HOME}/npm-prefix +'` + +``` +``` + +## `npm_config_userconfig=${workspace}/prefix.npmrc npm install -g -C=./custom-prefix ./npm-global-prefix-pkg` + +-C= must override the user npmrc prefix when creating links + +``` + +added 1 package in +Linked 'npm-global-prefix-cli' to /.vite-plus/bin/npm-global-prefix-cli +``` + +## `vpt stat-file $VP_HOME/bin/npm-global-prefix-cli --assert symlink` + +``` +/.vite-plus/bin/npm-global-prefix-cli: symlink +``` + +## `vpt stat-file $VP_HOME/bins/npm-global-prefix-cli.json --assert file` + +``` +/.vite-plus/bins/npm-global-prefix-cli.json: file +``` + +## `npm-global-prefix-cli` + +``` +npm-global-prefix-cli works +``` + +## `npm_config_userconfig=${workspace}/prefix.npmrc npm -C ./custom-prefix uninstall -g npm-global-prefix-pkg` + +-C before the subcommand must also override the user npmrc prefix for cleanup + +``` + +removed 1 package in +Removed link 'npm-global-prefix-cli' from /.vite-plus/bin/npm-global-prefix-cli +``` + +## `vpt stat-file custom-prefix/lib/node_modules/npm-global-prefix-pkg --assert missing` + +``` +custom-prefix/lib/node_modules/npm-global-prefix-pkg: missing +``` + +## `vpt stat-file custom-prefix/bin/npm-global-prefix-cli --assert missing` + +``` +custom-prefix/bin/npm-global-prefix-cli: missing +``` + +## `vpt stat-file $VP_HOME/bin/npm-global-prefix-cli --assert missing` + +``` +/.vite-plus/bin/npm-global-prefix-cli: missing +``` + +## `vpt stat-file $VP_HOME/bins/npm-global-prefix-cli.json --assert missing` + +``` +/.vite-plus/bins/npm-global-prefix-cli.json: missing +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/npm_global_uninstall_prefix/snapshots/npm_global_uninstall_preserves_local_copy.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/npm_global_uninstall_prefix/snapshots/npm_global_uninstall_preserves_local_copy.md new file mode 100644 index 0000000000..4cd12b5e3d --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/npm_global_uninstall_prefix/snapshots/npm_global_uninstall_preserves_local_copy.md @@ -0,0 +1,122 @@ +# npm_global_uninstall_preserves_local_copy + +## `vpt mkdir -p custom-prefix` + +``` +``` + +## `vpt write-file custom-prefix/package.json '{"private":true}'` + +``` +``` + +## `npm install --prefix ./custom-prefix --ignore-scripts --no-save --no-package-lock --no-audit ./npm-global-prefix-pkg` + +Install a local copy under the same prefix + +``` + +added 1 package in +``` + +## `npm install -g --prefix ./custom-prefix ./npm-global-prefix-pkg` + +Install a separate global copy and create its Vite+ link + +``` + +added 1 package in +Linked 'npm-global-prefix-cli' to /.vite-plus/bin/npm-global-prefix-cli +``` + +## `vpt stat-file custom-prefix/node_modules/npm-global-prefix-pkg/package.json --assert file` + +``` +custom-prefix/node_modules/npm-global-prefix-pkg/package.json: file +``` + +## `vpt stat-file custom-prefix/lib/node_modules/npm-global-prefix-pkg/package.json --assert file` + +``` +custom-prefix/lib/node_modules/npm-global-prefix-pkg/package.json: file +``` + +## `vpt stat-file $VP_HOME/bin/npm-global-prefix-cli --assert symlink` + +``` +/.vite-plus/bin/npm-global-prefix-cli: symlink +``` + +## `vpt stat-file $VP_HOME/bins/npm-global-prefix-cli.json --assert file` + +``` +/.vite-plus/bins/npm-global-prefix-cli.json: file +``` + +## `npm uninstall -g --prefix ./custom-prefix npm-global-prefix-pkg --dry-run` + +Dry-run must preserve the global command even when a local copy also exists + +``` +remove npm-global-prefix-pkg 1.0.0 + +removed 1 package in +``` + +## `npm-global-prefix-cli` + +``` +npm-global-prefix-cli works +``` + +## `vpt stat-file $VP_HOME/bins/npm-global-prefix-cli.json --assert file` + +``` +/.vite-plus/bins/npm-global-prefix-cli.json: file +``` + +## `npm uninstall -g --prefix ./custom-prefix npm-global-prefix-pkg` + +Real uninstall must remove the global links without confusing the remaining local copy for the global installation + +``` + +removed 1 package in +Removed link 'npm-global-prefix-cli' from /.vite-plus/bin/npm-global-prefix-cli +``` + +## `vpt stat-file custom-prefix/lib/node_modules/npm-global-prefix-pkg --assert missing` + +``` +custom-prefix/lib/node_modules/npm-global-prefix-pkg: missing +``` + +## `vpt stat-file custom-prefix/bin/npm-global-prefix-cli --assert missing` + +``` +custom-prefix/bin/npm-global-prefix-cli: missing +``` + +## `vpt stat-file $VP_HOME/bin/npm-global-prefix-cli --assert missing` + +``` +/.vite-plus/bin/npm-global-prefix-cli: missing +``` + +## `vpt stat-file $VP_HOME/bins/npm-global-prefix-cli.json --assert missing` + +``` +/.vite-plus/bins/npm-global-prefix-cli.json: missing +``` + +## `vpt stat-file custom-prefix/node_modules/npm-global-prefix-pkg/package.json --assert file` + +``` +custom-prefix/node_modules/npm-global-prefix-pkg/package.json: file +``` + +## `node custom-prefix/node_modules/npm-global-prefix-pkg/cli.js` + +``` +npm-global-prefix-cli works +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/npm_global_uninstall_prefix/snapshots/npm_global_uninstall_userconfig_fallback.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/npm_global_uninstall_prefix/snapshots/npm_global_uninstall_userconfig_fallback.md new file mode 100644 index 0000000000..f21183071f --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/npm_global_uninstall_prefix/snapshots/npm_global_uninstall_userconfig_fallback.md @@ -0,0 +1,123 @@ +# npm_global_uninstall_userconfig_fallback + +## `node private-runtime.mjs` + +``` +``` + +## `npm install -g ./npm-global-prefix-pkg` + +Install into a private copy of the selected Node runtime + +``` + +added 1 package in +Linked 'npm-global-prefix-cli' to /.vite-plus/bin/npm-global-prefix-cli +``` + +## `vpt stat-file node-prefix/lib/node_modules/npm-global-prefix-pkg/package.json --assert file` + +``` +node-prefix/lib/node_modules/npm-global-prefix-pkg/package.json: file +``` + +## `vpt stat-file $VP_HOME/bin/npm-global-prefix-cli --assert symlink` + +``` +/.vite-plus/bin/npm-global-prefix-cli: symlink +``` + +## `vpt stat-file $VP_HOME/bins/npm-global-prefix-cli.json --assert file` + +``` +/.vite-plus/bins/npm-global-prefix-cli.json: file +``` + +## `npm config set prefix ./other-prefix --location=user` + +Make the normal user config point to another prefix + +``` +``` + +## `vpt write-file empty.npmrc ` + +``` +``` + +## `npm uninstall -g --userconfig=empty.npmrc npm-global-prefix-pkg --dry-run` + +A config override dry-run must preserve the fallback installation + +``` +remove npm-global-prefix-pkg 1.0.0 + +removed 1 package in +``` + +## `npm-global-prefix-cli` + +``` +npm-global-prefix-cli works +``` + +## `vpt stat-file $VP_HOME/bins/npm-global-prefix-cli.json --assert file` + +``` +/.vite-plus/bins/npm-global-prefix-cli.json: file +``` + +## `npm uninstall -g npm-global-prefix-pkg` + +Uninstalling from an empty prefix must not remove links to the surviving fallback package + +``` + +up to date in +``` + +## `npm-global-prefix-cli` + +``` +npm-global-prefix-cli works +``` + +## `vpt stat-file $VP_HOME/bins/npm-global-prefix-cli.json --assert file` + +``` +/.vite-plus/bins/npm-global-prefix-cli.json: file +``` + +## `npm uninstall -g --userconfig empty.npmrc npm-global-prefix-pkg` + +A real uninstall using the override must clean up the fallback package's links + +``` + +removed 1 package in +Removed link 'npm-global-prefix-cli' from /.vite-plus/bin/npm-global-prefix-cli +``` + +## `vpt stat-file node-prefix/lib/node_modules/npm-global-prefix-pkg --assert missing` + +``` +node-prefix/lib/node_modules/npm-global-prefix-pkg: missing +``` + +## `vpt stat-file node-prefix/bin/npm-global-prefix-cli --assert missing` + +``` +node-prefix/bin/npm-global-prefix-cli: missing +``` + +## `vpt stat-file $VP_HOME/bin/npm-global-prefix-cli --assert missing` + +``` +/.vite-plus/bin/npm-global-prefix-cli: missing +``` + +## `vpt stat-file $VP_HOME/bins/npm-global-prefix-cli.json --assert missing` + +``` +/.vite-plus/bins/npm-global-prefix-cli.json: missing +``` diff --git a/crates/vp_global_cli/src/shim/dispatch.rs b/crates/vp_global_cli/src/shim/dispatch.rs index 7a12df4f7a..90a9429928 100644 --- a/crates/vp_global_cli/src/shim/dispatch.rs +++ b/crates/vp_global_cli/src/shim/dispatch.rs @@ -30,12 +30,12 @@ use crate::{ struct NpmGlobalCommand { /// Package names/specs extracted from args (e.g., ["codex", "typescript@5"]) packages: Vec, - /// Explicit `--prefix ` from the CLI args, if present. + /// Explicit `--prefix ` or `-C ` from the CLI args, if present. explicit_prefix: Option, } /// Value-bearing npm flags whose next arg should be skipped during package extraction. -/// Note: `--prefix` is handled separately to capture its value. +/// Note: `--prefix` and `-C` are handled separately to capture their values. const NPM_VALUE_FLAGS: &[&str] = &["--registry", "--tag", "--cache", "--tmp"]; /// Install subcommands recognized by npm. @@ -59,7 +59,7 @@ fn parse_npm_global_command(args: &[String], subcommands: &[&str]) -> Option Option Option<(AbsolutePathBuf, String)> { + [npm_prefix, node_dir].into_iter().find_map(|prefix| { + let dir = config::get_node_modules_dir(prefix, package_name); + std::fs::read_to_string(dir.join("package.json").as_path()) + .ok() + .map(|content| (dir, content)) }) } -/// Collect (bin_name, package_name) pairs from packages by reading their installed package.json files. +/// Collect bin names, package names, and their pre-uninstall installation paths. fn collect_bin_names_from_npm( packages: &[String], npm_prefix: &AbsolutePath, node_dir: &AbsolutePath, -) -> Vec<(String, String)> { +) -> Vec<(String, String, AbsolutePathBuf)> { let mut all_bins = Vec::new(); for spec in packages { let Some(package_name) = resolve_package_name(spec) else { continue }; - let Some(content) = read_npm_package_json(npm_prefix, node_dir, &package_name) else { + let Some((package_dir, content)) = + read_npm_package_json(npm_prefix, node_dir, &package_name) + else { continue; }; let Ok(package_json) = serde_json::from_str::(&content) else { continue; }; for bin_name in extract_bin_names(&package_json) { - all_bins.push((bin_name, package_name.clone())); + all_bins.push((bin_name, package_name.clone(), package_dir.clone())); } } @@ -1894,6 +1902,121 @@ mod tests { assert!(result.is_none(), "URLs should be filtered"); } + #[test] + fn test_npm_uninstall_preserves_links_until_package_is_removed() { + let temp = TempDir::new().unwrap(); + let npm_prefix = AbsolutePathBuf::new(temp.path().join("npm-prefix")).unwrap(); + let package_dir = config::get_node_modules_dir(&npm_prefix, "test-package"); + std::fs::create_dir_all(&package_dir).unwrap(); + let source = package_dir.join("cli.js"); + std::fs::write(&source, "console.log('test');").unwrap(); + std::fs::write( + package_dir.join("package.json"), + r#"{"name":"test-package","bin":{"test-cli":"cli.js"}}"#, + ) + .unwrap(); + + vp_shared::EnvConfig::with_vars([(env_vars::VP_HOME, temp.path())], |_| { + let bin_dir = config::get_bin_dir().unwrap(); + std::fs::create_dir_all(&bin_dir).unwrap(); + create_bin_link(&bin_dir, "test-cli", &source, "test-package", "22.18.0"); + let link = bin_dir.join("test-cli"); + let metadata_path = BinConfig::path("test-cli").unwrap(); + let metadata = std::fs::read(&metadata_path).unwrap(); + assert!(link.as_path().exists()); + + let bins = collect_bin_names_from_npm(&s(&["test-package"]), &npm_prefix, &npm_prefix); + assert_eq!(bins.len(), 1); + #[cfg(unix)] + let local_dir = npm_prefix.join("node_modules").join("test-package"); + #[cfg(unix)] + std::fs::create_dir_all(&local_dir).unwrap(); + remove_npm_global_uninstall_links(&bins, &npm_prefix); + + assert!(link.as_path().exists(), "dry-run must preserve the installed command"); + assert_eq!(std::fs::read(&metadata_path).unwrap(), metadata); + #[cfg(windows)] + assert!(bin_dir.join("test-cli.cmd").as_path().exists()); + + std::fs::remove_dir_all(&package_dir).unwrap(); + remove_npm_global_uninstall_links(&bins, &npm_prefix); + + assert!(std::fs::symlink_metadata(&link).is_err()); + assert!(!metadata_path.as_path().exists()); + #[cfg(unix)] + assert!(local_dir.as_path().exists()); + #[cfg(windows)] + assert!(!bin_dir.join("test-cli.cmd").as_path().exists()); + }); + } + + #[test] + fn test_npm_uninstall_prefers_configured_prefix() { + let temp = TempDir::new().unwrap(); + let npm_prefix = AbsolutePathBuf::new(temp.path().join("npm-prefix")).unwrap(); + let node_dir = AbsolutePathBuf::new(temp.path().join("node")).unwrap(); + for prefix in [&npm_prefix, &node_dir] { + let package_dir = config::get_node_modules_dir(prefix, "test-package"); + std::fs::create_dir_all(&package_dir).unwrap(); + std::fs::write( + package_dir.join("package.json"), + r#"{"name":"test-package","bin":{"test-cli":"cli.js"}}"#, + ) + .unwrap(); + } + assert_eq!( + collect_bin_names_from_npm(&s(&["test-package"]), &npm_prefix, &node_dir), + vec![( + "test-cli".to_string(), + "test-package".to_string(), + config::get_node_modules_dir(&npm_prefix, "test-package"), + )] + ); + } + + #[test] + fn test_npm_uninstall_preserves_fallback_links_until_package_is_removed() { + let temp = TempDir::new().unwrap(); + let npm_prefix = AbsolutePathBuf::new(temp.path().join("npm-prefix")).unwrap(); + let node_dir = AbsolutePathBuf::new(temp.path().join("node")).unwrap(); + let package_dir = config::get_node_modules_dir(&node_dir, "test-package"); + std::fs::create_dir_all(&package_dir).unwrap(); + std::fs::write( + package_dir.join("package.json"), + r#"{"name":"test-package","bin":{"test-cli":"cli.js"}}"#, + ) + .unwrap(); + + let source = package_dir.join("cli.js"); + std::fs::write(&source, "console.log('test');").unwrap(); + let bins = collect_bin_names_from_npm(&s(&["test-package"]), &npm_prefix, &node_dir); + assert_eq!( + bins, + vec![("test-cli".to_string(), "test-package".to_string(), package_dir.clone())] + ); + + vp_shared::EnvConfig::with_vars([(env_vars::VP_HOME, temp.path())], |_| { + let bin_dir = config::get_bin_dir().unwrap(); + std::fs::create_dir_all(&bin_dir).unwrap(); + create_bin_link(&bin_dir, "test-cli", &source, "test-package", "22.18.0"); + let link = bin_dir.join("test-cli"); + let metadata_path = BinConfig::path("test-cli").unwrap(); + let metadata = std::fs::read(&metadata_path).unwrap(); + + remove_npm_global_uninstall_links(&bins, &npm_prefix); + assert!( + link.as_path().exists(), + "a surviving fallback installation must keep its link" + ); + assert_eq!(std::fs::read(&metadata_path).unwrap(), metadata); + + std::fs::remove_dir_all(&package_dir).unwrap(); + remove_npm_global_uninstall_links(&bins, &npm_prefix); + assert!(std::fs::symlink_metadata(&link).is_err()); + assert!(!metadata_path.as_path().exists()); + }); + } + // --- parse_npm_global_uninstall tests --- #[test] @@ -2181,6 +2304,23 @@ mod tests { assert_eq!(parsed.explicit_prefix.as_deref(), Some("/custom/dir")); } + #[test] + fn test_parse_npm_global_command_prefix_shorthand() { + for command in ["install", "uninstall"] { + for args in [ + s(&[command, "-g", "-C", "/tmp/test", "pkg"]), + s(&["-C", "/tmp/test", command, "-g", "pkg"]), + s(&[command, "-g", "-C=/tmp/test", "pkg"]), + s(&[command, "-g", "--prefix", "/tmp/other", "-C", "/tmp/test", "pkg"]), + s(&[command, "-g", "-C", "/tmp/other", "--prefix", "/tmp/test", "pkg"]), + ] { + let parsed = parse_npm_global_command(&args, &[command]).unwrap(); + assert_eq!(parsed.explicit_prefix.as_deref(), Some("/tmp/test"), "{args:?}"); + assert_eq!(parsed.packages, vec!["pkg"], "{args:?}"); + } + } + } + // --- resolve_npm_prefix tests --- #[test]