From 83d2864760d064660d1582c8b23a2035f8450dca Mon Sep 17 00:00:00 2001 From: JongKyung Lee Date: Wed, 16 Sep 2026 14:30:47 +0900 Subject: [PATCH 01/12] fix(pm): preserve install options when adding packages Preserve common options when install with package names resolves to add, and map them to each package manager's supported flags. Diagnose options that do not apply to the selected install/add mode before conversion. Retain Yarn Classic's native frozen-lockfile behavior and warn for unsupported options without changing the warning-and-continue policy. Cover option forwarding and command modes with resolver tests and CLI snapshots. --- .../snapshots/command_add_bun.md | 10 + .../snapshots/command_add_npm10.md | 10 + .../snapshots/command_add_npm11.md | 10 + .../snapshots/command_add_pnpm10.global.md | 10 + .../snapshots/command_add_pnpm10.local.md | 20 ++ .../snapshots/command_add_pnpm11.md | 10 + .../snapshots/command_add_pnpm12.md | 10 + .../snapshots/command_add_pnpm9.md | 10 + .../snapshots/command_add_yarn4.md | 10 + .../dep-v2/package.json | 4 + .../install_package_options/dep/package.json | 4 + .../install_package_options/package.json | 6 + .../install_package_options/snapshots.toml | 109 +++++++++++ .../snapshots/bun_frozen_lockfile.md | 63 ++++++ ...ith_packages_drops_install_only_options.md | 42 ++++ ...without_packages_drops_add_only_options.md | 41 ++++ .../snapshots/npm_lockfile_only.md | 39 ++++ .../snapshots/npm_offline.md | 27 +++ .../pnpm_unsupported_install_options.md | 31 +++ .../yarn_berry_frozen_lockfile_modes.md | 128 ++++++++++++ .../snapshots/yarn_berry_lockfile_only.md | 43 +++++ .../yarn_classic_frozen_lockfile_modes.md | 117 +++++++++++ crates/vp_pm_cli/src/cli.rs | 5 +- .../vp_pm_cli/src/resolution/commands/add.rs | 182 +++++++++++++++++- .../src/resolution/commands/install.rs | 134 ++++++++++++- 25 files changed, 1057 insertions(+), 18 deletions(-) create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/dep-v2/package.json create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/dep/package.json create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/package.json create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots.toml create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/bun_frozen_lockfile.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/install_with_packages_drops_install_only_options.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/install_without_packages_drops_add_only_options.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/npm_lockfile_only.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/npm_offline.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/pnpm_unsupported_install_options.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_berry_frozen_lockfile_modes.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_berry_lockfile_only.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_classic_frozen_lockfile_modes.md diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_bun/snapshots/command_add_bun.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_bun/snapshots/command_add_bun.md index 375354450f..8e910cb567 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_bun/snapshots/command_add_bun.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_bun/snapshots/command_add_bun.md @@ -25,6 +25,16 @@ Options: --save-catalog Save the new dependency to the default catalog --allow-build A list of package names allowed to run postinstall --ignore-scripts Do not run lifecycle scripts + --no-optional Do not install optionalDependencies + --frozen-lockfile Fail if lockfile needs to be updated + --no-frozen-lockfile Allow lockfile updates + --lockfile-only Only update lockfile, don't install + --prefer-offline Use cached packages when available + --offline Only use packages already in cache + -f, --force Force reinstall all dependencies + --no-lockfile Don't read or generate lockfile + --shamefully-hoist Create flat node_modules (pnpm only) + --silent Suppress package manager output --filter Filter packages in monorepo (can be used multiple times) -w, --workspace-root Add to workspace root --workspace Only add if package exists in workspace (pnpm-specific) diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_npm10/snapshots/command_add_npm10.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_npm10/snapshots/command_add_npm10.md index aec68aaa9d..786bd04ea4 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_npm10/snapshots/command_add_npm10.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_npm10/snapshots/command_add_npm10.md @@ -25,6 +25,16 @@ Options: --save-catalog Save the new dependency to the default catalog --allow-build A list of package names allowed to run postinstall --ignore-scripts Do not run lifecycle scripts + --no-optional Do not install optionalDependencies + --frozen-lockfile Fail if lockfile needs to be updated + --no-frozen-lockfile Allow lockfile updates + --lockfile-only Only update lockfile, don't install + --prefer-offline Use cached packages when available + --offline Only use packages already in cache + -f, --force Force reinstall all dependencies + --no-lockfile Don't read or generate lockfile + --shamefully-hoist Create flat node_modules (pnpm only) + --silent Suppress package manager output --filter Filter packages in monorepo (can be used multiple times) -w, --workspace-root Add to workspace root --workspace Only add if package exists in workspace (pnpm-specific) diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_npm11/snapshots/command_add_npm11.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_npm11/snapshots/command_add_npm11.md index cc730352ab..0ff40ccc97 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_npm11/snapshots/command_add_npm11.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_npm11/snapshots/command_add_npm11.md @@ -25,6 +25,16 @@ Options: --save-catalog Save the new dependency to the default catalog --allow-build A list of package names allowed to run postinstall --ignore-scripts Do not run lifecycle scripts + --no-optional Do not install optionalDependencies + --frozen-lockfile Fail if lockfile needs to be updated + --no-frozen-lockfile Allow lockfile updates + --lockfile-only Only update lockfile, don't install + --prefer-offline Use cached packages when available + --offline Only use packages already in cache + -f, --force Force reinstall all dependencies + --no-lockfile Don't read or generate lockfile + --shamefully-hoist Create flat node_modules (pnpm only) + --silent Suppress package manager output --filter Filter packages in monorepo (can be used multiple times) -w, --workspace-root Add to workspace root --workspace Only add if package exists in workspace (pnpm-specific) diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_pnpm10/snapshots/command_add_pnpm10.global.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_pnpm10/snapshots/command_add_pnpm10.global.md index a45d66a732..f78ba8c3ae 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_pnpm10/snapshots/command_add_pnpm10.global.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_pnpm10/snapshots/command_add_pnpm10.global.md @@ -25,6 +25,16 @@ Options: --save-catalog Save the new dependency to the default catalog --allow-build A list of package names allowed to run postinstall --ignore-scripts Do not run lifecycle scripts + --no-optional Do not install optionalDependencies + --frozen-lockfile Fail if lockfile needs to be updated + --no-frozen-lockfile Allow lockfile updates + --lockfile-only Only update lockfile, don't install + --prefer-offline Use cached packages when available + --offline Only use packages already in cache + -f, --force Force reinstall all dependencies + --no-lockfile Don't read or generate lockfile + --shamefully-hoist Create flat node_modules (pnpm only) + --silent Suppress package manager output --filter Filter packages in monorepo (can be used multiple times) -w, --workspace-root Add to workspace root --workspace Only add if package exists in workspace (pnpm-specific) diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_pnpm10/snapshots/command_add_pnpm10.local.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_pnpm10/snapshots/command_add_pnpm10.local.md index f61cd548fa..41f5440d40 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_pnpm10/snapshots/command_add_pnpm10.local.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_pnpm10/snapshots/command_add_pnpm10.local.md @@ -32,6 +32,26 @@ Options: A list of package names allowed to run postinstall --ignore-scripts Do not run lifecycle scripts + --no-optional + Do not install optionalDependencies + --frozen-lockfile + Fail if lockfile needs to be updated + --no-frozen-lockfile + Allow lockfile updates + --lockfile-only + Only update lockfile, don't install + --prefer-offline + Use cached packages when available + --offline + Only use packages already in cache + -f, --force + Force reinstall all dependencies + --no-lockfile + Don't read or generate lockfile + --shamefully-hoist + Create flat node_modules (pnpm only) + --silent + Suppress package manager output --filter Filter packages in monorepo (can be used multiple times) -w, --workspace-root diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_pnpm11/snapshots/command_add_pnpm11.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_pnpm11/snapshots/command_add_pnpm11.md index e4d2b58556..98775e73f1 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_pnpm11/snapshots/command_add_pnpm11.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_pnpm11/snapshots/command_add_pnpm11.md @@ -25,6 +25,16 @@ Options: --save-catalog Save the new dependency to the default catalog --allow-build A list of package names allowed to run postinstall --ignore-scripts Do not run lifecycle scripts + --no-optional Do not install optionalDependencies + --frozen-lockfile Fail if lockfile needs to be updated + --no-frozen-lockfile Allow lockfile updates + --lockfile-only Only update lockfile, don't install + --prefer-offline Use cached packages when available + --offline Only use packages already in cache + -f, --force Force reinstall all dependencies + --no-lockfile Don't read or generate lockfile + --shamefully-hoist Create flat node_modules (pnpm only) + --silent Suppress package manager output --filter Filter packages in monorepo (can be used multiple times) -w, --workspace-root Add to workspace root --workspace Only add if package exists in workspace (pnpm-specific) diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_pnpm12/snapshots/command_add_pnpm12.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_pnpm12/snapshots/command_add_pnpm12.md index 4911bc941d..0f2fb08902 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_pnpm12/snapshots/command_add_pnpm12.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_pnpm12/snapshots/command_add_pnpm12.md @@ -25,6 +25,16 @@ Options: --save-catalog Save the new dependency to the default catalog --allow-build A list of package names allowed to run postinstall --ignore-scripts Do not run lifecycle scripts + --no-optional Do not install optionalDependencies + --frozen-lockfile Fail if lockfile needs to be updated + --no-frozen-lockfile Allow lockfile updates + --lockfile-only Only update lockfile, don't install + --prefer-offline Use cached packages when available + --offline Only use packages already in cache + -f, --force Force reinstall all dependencies + --no-lockfile Don't read or generate lockfile + --shamefully-hoist Create flat node_modules (pnpm only) + --silent Suppress package manager output --filter Filter packages in monorepo (can be used multiple times) -w, --workspace-root Add to workspace root --workspace Only add if package exists in workspace (pnpm-specific) diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_pnpm9/snapshots/command_add_pnpm9.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_pnpm9/snapshots/command_add_pnpm9.md index ae46ba2654..cf7206a661 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_pnpm9/snapshots/command_add_pnpm9.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_pnpm9/snapshots/command_add_pnpm9.md @@ -25,6 +25,16 @@ Options: --save-catalog Save the new dependency to the default catalog --allow-build A list of package names allowed to run postinstall --ignore-scripts Do not run lifecycle scripts + --no-optional Do not install optionalDependencies + --frozen-lockfile Fail if lockfile needs to be updated + --no-frozen-lockfile Allow lockfile updates + --lockfile-only Only update lockfile, don't install + --prefer-offline Use cached packages when available + --offline Only use packages already in cache + -f, --force Force reinstall all dependencies + --no-lockfile Don't read or generate lockfile + --shamefully-hoist Create flat node_modules (pnpm only) + --silent Suppress package manager output --filter Filter packages in monorepo (can be used multiple times) -w, --workspace-root Add to workspace root --workspace Only add if package exists in workspace (pnpm-specific) diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_yarn4/snapshots/command_add_yarn4.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_yarn4/snapshots/command_add_yarn4.md index fd5c4245e0..6e4158fd7e 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_yarn4/snapshots/command_add_yarn4.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_yarn4/snapshots/command_add_yarn4.md @@ -25,6 +25,16 @@ Options: --save-catalog Save the new dependency to the default catalog --allow-build A list of package names allowed to run postinstall --ignore-scripts Do not run lifecycle scripts + --no-optional Do not install optionalDependencies + --frozen-lockfile Fail if lockfile needs to be updated + --no-frozen-lockfile Allow lockfile updates + --lockfile-only Only update lockfile, don't install + --prefer-offline Use cached packages when available + --offline Only use packages already in cache + -f, --force Force reinstall all dependencies + --no-lockfile Don't read or generate lockfile + --shamefully-hoist Create flat node_modules (pnpm only) + --silent Suppress package manager output --filter Filter packages in monorepo (can be used multiple times) -w, --workspace-root Add to workspace root --workspace Only add if package exists in workspace (pnpm-specific) diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/dep-v2/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/dep-v2/package.json new file mode 100644 index 0000000000..295a4ceded --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/dep-v2/package.json @@ -0,0 +1,4 @@ +{ + "name": "install-option-dep", + "version": "2.0.0" +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/dep/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/dep/package.json new file mode 100644 index 0000000000..d624bdb8fc --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/dep/package.json @@ -0,0 +1,4 @@ +{ + "name": "install-option-dep", + "version": "1.0.0" +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/package.json new file mode 100644 index 0000000000..a5cf985ce1 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/package.json @@ -0,0 +1,6 @@ +{ + "name": "install-package-options", + "version": "1.0.0", + "private": true, + "packageManager": "npm@11.13.0" +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots.toml new file mode 100644 index 0000000000..1164afb34e --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots.toml @@ -0,0 +1,109 @@ +[[case]] +name = "install_with_packages_drops_install_only_options" +vp = "global" +steps = [ + { argv = ["vp", "install", "./dep", "--fix-lockfile", "--resolution-only", "--lockfile-only"], comment = "diagnose install-only options before converting to add, while preserving common options" }, + ["vpt", "stat-file", "package-lock.json", "--assert", "file"], + ["vpt", "stat-file", "node_modules", "--assert", "missing"], + ["vpt", "print-file", "package.json"], +] + +[[case]] +name = "install_without_packages_drops_add_only_options" +vp = "global" +steps = [ + { argv = ["vp", "install", "--save-exact", "--save-peer", "--save-optional", "--save-catalog", "--lockfile-only"], comment = "diagnose add-only options without package names instead of reporting manager support" }, + ["vpt", "stat-file", "package-lock.json", "--assert", "file"], + ["vpt", "stat-file", "node_modules", "--assert", "missing"], + ["vpt", "print-file", "package.json"], +] + +[[case]] +name = "npm_lockfile_only" +vp = "global" +steps = [ + { argv = ["vp", "install", "./dep", "--lockfile-only"], comment = "adding a package preserves lockfile-only" }, + ["vpt", "stat-file", "package-lock.json", "--assert", "file"], + ["vpt", "stat-file", "node_modules", "--assert", "missing"], + ["vpt", "print-file", "package.json"], +] + +[[case]] +name = "npm_offline" +vp = "global" +steps = [ + { argv = ["vp", "install", "vp-install-option-uncached-probe@1.0.0", "--offline", "--", "--registry=http://127.0.0.1:9", "--fetch-retries=0"], comment = "an uncached package fails in offline mode rather than attempting a connection", continue-on-failure = true }, + { argv = ["vpt", "stat-file", "node_modules", "--assert", "missing"], continue-on-failure = true }, + ["vpt", "stat-file", "package-lock.json", "--assert", "missing"], +] + +[[case]] +name = "bun_frozen_lockfile" +vp = "global" +steps = [ + { argv = ["vpt", "json-edit", "package.json", "packageManager", "bun@1.3.11"], snapshot = false }, + ["vp", "install", "./dep", "--lockfile-only"], + ["vpt", "stat-file", "node_modules", "--assert", "missing"], + { argv = ["vpt", "cp", "bun.lock", "before.lock"], snapshot = false }, + { argv = ["vp", "install", "./dep-v2", "--frozen-lockfile"], comment = "a named-package install must not rewrite a frozen lockfile", continue-on-failure = true }, + { argv = ["node", "-e", "const fs = require('node:fs'); if (!fs.readFileSync('bun.lock').equals(fs.readFileSync('before.lock'))) process.exit(1); console.log('lockfile unchanged');"], continue-on-failure = true }, + ["vpt", "stat-file", "node_modules", "--assert", "missing"], + ["vpt", "print-file", "package.json"], +] + +[[case]] +name = "pnpm_unsupported_install_options" +vp = "global" +steps = [ + { argv = ["vpt", "json-edit", "package.json", "packageManager", "pnpm@11.24.0"], snapshot = false }, + { argv = ["vp", "install", "./dep", "--lockfile-only", "--frozen-lockfile"], comment = "warn for frozen-lockfile that pnpm add cannot accept, but preserve lockfile-only" }, + ["vpt", "stat-file", "pnpm-lock.yaml", "--assert", "file"], + ["vpt", "stat-file", "node_modules", "--assert", "missing"], +] + +[[case]] +name = "yarn_berry_lockfile_only" +vp = "global" +env = { YARN_ENABLE_TELEMETRY = "0" } +steps = [ + { argv = ["vpt", "json-edit", "package.json", "packageManager", "yarn@4.10.3"], snapshot = false }, + { argv = ["vp", "install", "./dep", "--lockfile-only", "--ignore-scripts"], comment = "use update-lockfile rather than skip-build when both options are supplied" }, + ["vpt", "stat-file", "yarn.lock", "--assert", "file"], + ["vpt", "stat-file", "node_modules", "--assert", "missing"], + ["vpt", "stat-file", ".pnp.cjs", "--assert", "missing"], +] + +[[case]] +name = "yarn_classic_frozen_lockfile_modes" +vp = "global" +steps = [ + { argv = ["vpt", "json-edit", "package.json", "packageManager", "yarn@1.22.22"], snapshot = false }, + { argv = ["vp", "install", "./dep"], snapshot = false }, + { argv = ["vpt", "cp", "yarn.lock", "before.lock"], snapshot = false }, + { argv = ["vp", "install", "./dep-v2", "--frozen-lockfile"], comment = "Classic add preserves the lockfile but allows manifest changes" }, + ["node", "-e", "const fs = require('node:fs'); if (!fs.readFileSync('yarn.lock').equals(fs.readFileSync('before.lock'))) process.exit(1); console.log('lockfile unchanged');"], + ["node", "-p", "require('./package.json').dependencies['install-option-dep']"], + { argv = ["vp", "install", "--frozen-lockfile"], comment = "package-free installs still enforce the frozen lockfile", continue-on-failure = true }, + { argv = ["node", "-e", "const fs = require('node:fs'); if (!fs.readFileSync('yarn.lock').equals(fs.readFileSync('before.lock'))) process.exit(1); console.log('lockfile unchanged');"], continue-on-failure = true }, + { argv = ["vp", "install", "--no-frozen-lockfile"], comment = "package-free installs can explicitly allow lockfile changes" }, + ["node", "-p", "require('./node_modules/install-option-dep/package.json').version"], + { argv = ["vp", "add", "./dep", "--no-frozen-lockfile"], comment = "Classic add warns and drops the unsupported negated flag" }, + ["node", "-p", "require('./node_modules/install-option-dep/package.json').version"], +] + +[[case]] +name = "yarn_berry_frozen_lockfile_modes" +vp = "global" +env = { YARN_ENABLE_TELEMETRY = "0" } +steps = [ + { argv = ["vpt", "json-edit", "package.json", "packageManager", "yarn@4.10.3"], snapshot = false }, + { argv = ["vpt", "write-file", ".yarnrc.yml", "nodeLinker: node-modules\n"], snapshot = false }, + { argv = ["vp", "install", "./dep", "--frozen-lockfile"], comment = "named-package installs use add, which has no immutable option" }, + { argv = ["vpt", "cp", "yarn.lock", "before.lock"], snapshot = false }, + { argv = ["vpt", "json-edit", "package.json", "dependencies.install-option-dep", "file:./dep-v2"], snapshot = false }, + { argv = ["vp", "install", "--frozen-lockfile"], comment = "package-free installs map frozen-lockfile to immutable", continue-on-failure = true }, + { argv = ["node", "-e", "const fs = require('node:fs'); if (!fs.readFileSync('yarn.lock').equals(fs.readFileSync('before.lock'))) process.exit(1); console.log('lockfile unchanged');"], continue-on-failure = true }, + { argv = ["vp", "install", "--no-frozen-lockfile"], comment = "package-free installs map no-frozen-lockfile to no-immutable" }, + ["node", "-p", "require('./node_modules/install-option-dep/package.json').version"], + { argv = ["vp", "add", "./dep", "--no-frozen-lockfile"], comment = "direct add also warns for the unsupported immutable override" }, +] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/bun_frozen_lockfile.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/bun_frozen_lockfile.md new file mode 100644 index 0000000000..9478c22b12 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/bun_frozen_lockfile.md @@ -0,0 +1,63 @@ +# bun_frozen_lockfile + +## `vpt json-edit package.json packageManager bun@1.3.11` + + +## `vp install ./dep --lockfile-only` + +``` +VITE+ - The Unified Toolchain for the Web + +bun add () + +Saved bun.lock (2 packages) [] +``` + +## `vpt stat-file node_modules --assert missing` + +``` +node_modules: missing +``` + +## `vpt cp bun.lock before.lock` + + +## `vp install ./dep-v2 --frozen-lockfile` + +a named-package install must not rewrite a frozen lockfile + +**Exit code:** 1 + +``` +VITE+ - The Unified Toolchain for the Web + +bun add () +error: lockfile had changes, but lockfile is frozen +note: try re-running without --frozen-lockfile and commit the updated lockfile +``` + +## `node -e 'const fs = require('\''node:fs'\''); if ('\!'fs.readFileSync('\''bun.lock'\'').equals(fs.readFileSync('\''before.lock'\''))) process.exit(1); console.log('\''lockfile unchanged'\'');'` + +``` +lockfile unchanged +``` + +## `vpt stat-file node_modules --assert missing` + +``` +node_modules: missing +``` + +## `vpt print-file package.json` + +``` +{ + "name": "install-package-options", + "packageManager": "bun@1.3.11", + "private": true, + "version": "1.0.0", + "dependencies": { + "install-option-dep": "./dep" + } +} +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/install_with_packages_drops_install_only_options.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/install_with_packages_drops_install_only_options.md new file mode 100644 index 0000000000..c59e4f8b64 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/install_with_packages_drops_install_only_options.md @@ -0,0 +1,42 @@ +# install_with_packages_drops_install_only_options + +## `vp install ./dep --fix-lockfile --resolution-only --lockfile-only` + +diagnose install-only options before converting to add, while preserving common options + +``` +VITE+ - The Unified Toolchain for the Web + +warn: install with package names does not support --fix-lockfile. +warn: install with package names does not support --resolution-only. + +up to date, audited 3 packages in + +found 0 vulnerabilities +``` + +## `vpt stat-file package-lock.json --assert file` + +``` +package-lock.json: file +``` + +## `vpt stat-file node_modules --assert missing` + +``` +node_modules: missing +``` + +## `vpt print-file package.json` + +``` +{ + "name": "install-package-options", + "version": "1.0.0", + "private": true, + "packageManager": "npm@11.13.0", + "dependencies": { + "install-option-dep": "file:dep" + } +} +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/install_without_packages_drops_add_only_options.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/install_without_packages_drops_add_only_options.md new file mode 100644 index 0000000000..0030cb1530 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/install_without_packages_drops_add_only_options.md @@ -0,0 +1,41 @@ +# install_without_packages_drops_add_only_options + +## `vp install --save-exact --save-peer --save-optional --save-catalog --lockfile-only` + +diagnose add-only options without package names instead of reporting manager support + +``` +VITE+ - The Unified Toolchain for the Web + +warn: install without package names does not support --save-exact. +warn: install without package names does not support --save-peer. +warn: install without package names does not support --save-optional. +warn: install without package names does not support --save-catalog. + +up to date, audited 1 package in + +found 0 vulnerabilities +``` + +## `vpt stat-file package-lock.json --assert file` + +``` +package-lock.json: file +``` + +## `vpt stat-file node_modules --assert missing` + +``` +node_modules: missing +``` + +## `vpt print-file package.json` + +``` +{ + "name": "install-package-options", + "version": "1.0.0", + "private": true, + "packageManager": "npm@11.13.0" +} +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/npm_lockfile_only.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/npm_lockfile_only.md new file mode 100644 index 0000000000..50d0d932a8 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/npm_lockfile_only.md @@ -0,0 +1,39 @@ +# npm_lockfile_only + +## `vp install ./dep --lockfile-only` + +adding a package preserves lockfile-only + +``` +VITE+ - The Unified Toolchain for the Web + +up to date, audited 3 packages in + +found 0 vulnerabilities +``` + +## `vpt stat-file package-lock.json --assert file` + +``` +package-lock.json: file +``` + +## `vpt stat-file node_modules --assert missing` + +``` +node_modules: missing +``` + +## `vpt print-file package.json` + +``` +{ + "name": "install-package-options", + "version": "1.0.0", + "private": true, + "packageManager": "npm@11.13.0", + "dependencies": { + "install-option-dep": "file:dep" + } +} +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/npm_offline.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/npm_offline.md new file mode 100644 index 0000000000..b5893e6aaf --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/npm_offline.md @@ -0,0 +1,27 @@ +# npm_offline + +## `vp install vp-install-option-uncached-probe@1.0.0 --offline -- --registry=http://127.0.0.1:9 --fetch-retries=0` + +an uncached package fails in offline mode rather than attempting a connection + +**Exit code:** 1 + +``` +VITE+ - The Unified Toolchain for the Web + +npm error code ENOTCACHED +npm error request to http://127.0.0.1:/vp-install-option-uncached-probe failed: cache mode is 'only-if-cached' but no cached response is available. +npm error A complete log of this run can be found in: /.npm/_logs/-debug-0.log +``` + +## `vpt stat-file node_modules --assert missing` + +``` +node_modules: missing +``` + +## `vpt stat-file package-lock.json --assert missing` + +``` +package-lock.json: missing +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/pnpm_unsupported_install_options.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/pnpm_unsupported_install_options.md new file mode 100644 index 0000000000..23990127e6 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/pnpm_unsupported_install_options.md @@ -0,0 +1,31 @@ +# pnpm_unsupported_install_options + +## `vpt json-edit package.json packageManager pnpm@11.24.0` + + +## `vp install ./dep --lockfile-only --frozen-lockfile` + +warn for frozen-lockfile that pnpm add cannot accept, but preserve lockfile-only + +``` +VITE+ - The Unified Toolchain for the Web + +warn: pnpm does not support --frozen-lockfile. + +dependencies: + install-option-dep link:dep + +Done in using pnpm +``` + +## `vpt stat-file pnpm-lock.yaml --assert file` + +``` +pnpm-lock.yaml: file +``` + +## `vpt stat-file node_modules --assert missing` + +``` +node_modules: missing +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_berry_frozen_lockfile_modes.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_berry_frozen_lockfile_modes.md new file mode 100644 index 0000000000..427e424269 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_berry_frozen_lockfile_modes.md @@ -0,0 +1,128 @@ +# yarn_berry_frozen_lockfile_modes + +## `vpt json-edit package.json packageManager yarn@4.10.3` + + +## `vpt write-file .yarnrc.yml 'nodeLinker: node-modules +'` + + +## `vp install ./dep --frozen-lockfile` + +named-package installs use add, which has no immutable option + +``` +VITE+ - The Unified Toolchain for the Web + +warn: yarn >=2 does not support --frozen-lockfile. +➤ YN0000: · Yarn +➤ YN0000: ┌ Resolution step +➤ YN0085: │ + install-option-dep@file:./dep#./dep::hash=8572a9&locator=install-package-options%40workspace%3A. +➤ YN0000: └ Completed +➤ YN0000: ┌ Fetch step +➤ YN0013: │ A package was added to the project (+ KiB). +➤ YN0000: └ Completed +➤ YN0000: ┌ Link step +➤ YN0000: └ Completed +➤ YN0000: · Done in +``` + +## `vpt cp yarn.lock before.lock` + + +## `vpt json-edit package.json dependencies.install-option-dep file:./dep-v2` + + +## `vp install --frozen-lockfile` + +package-free installs map frozen-lockfile to immutable + +**Exit code:** 1 + +``` +VITE+ - The Unified Toolchain for the Web + +➤ YN0000: · Yarn +➤ YN0000: ┌ Resolution step +➤ YN0085: │ + install-option-dep@file:./dep-v2#./dep-v2::hash=5590bc&locator=install-package-options%40workspace%3A. +➤ YN0085: │ - install-option-dep@file:./dep#./dep::hash=8572a9&locator=install-package-options%40workspace%3A. +➤ YN0000: └ Completed + +➤ YN0000: ┌ Post-resolution validation +➤ YN0000: │ @@ -4,18 +4,17 @@ +➤ YN0000: │ __metadata: +➤ YN0000: │ version: 8 +➤ YN0000: │ cacheKey: 10c0 +➤ YN0000: │ +➤ YN0028: │ -"install-option-dep@file:./dep::locator=install-package-options%40workspace%3A.": +➤ YN0028: │ - version: 1.0.0 +➤ YN0028: │ - resolution: "install-option-dep@file:./dep#./dep::hash=8572a9&locator=install-package-options%40workspace%3A." +➤ YN0028: │ - checksum: 10c0/32675cb2e55f886e9f975fdaefa9cb706071880078bda04af25dd0343941bf33780453c0ec848b30d3aa53cb9d5d5b3a6aeef902552100f255ed35fac8a8ff06 +➤ YN0028: │ +"install-option-dep@file:./dep-v2::locator=install-package-options%40workspace%3A.": +➤ YN0028: │ + version: 2.0.0 +➤ YN0028: │ + resolution: "install-option-dep@file:./dep-v2#./dep-v2::hash=5590bc&locator=install-package-options%40workspace%3A." +➤ YN0000: │ languageName: node +➤ YN0000: │ linkType: hard +➤ YN0000: │ +➤ YN0000: │ "install-package-options@workspace:.": +➤ YN0000: │ version: 0.0.0-use.local +➤ YN0000: │ resolution: "install-package-options@workspace:." +➤ YN0000: │ dependencies: +➤ YN0028: │ - install-option-dep: ./dep +➤ YN0028: │ + install-option-dep: "file:./dep-v2" +➤ YN0000: │ languageName: unknown +➤ YN0000: │ linkType: soft +➤ YN0000: │ +➤ YN0028: │ The lockfile would have been modified by this install, which is explicitly forbidden. +➤ YN0000: └ Completed +➤ YN0000: · Failed with errors in +``` + +## `node -e 'const fs = require('\''node:fs'\''); if ('\!'fs.readFileSync('\''yarn.lock'\'').equals(fs.readFileSync('\''before.lock'\''))) process.exit(1); console.log('\''lockfile unchanged'\'');'` + +``` +lockfile unchanged +``` + +## `vp install --no-frozen-lockfile` + +package-free installs map no-frozen-lockfile to no-immutable + +``` +VITE+ - The Unified Toolchain for the Web + +➤ YN0000: · Yarn +➤ YN0000: ┌ Resolution step +➤ YN0085: │ + install-option-dep@file:./dep-v2#./dep-v2::hash=5590bc&locator=install-package-options%40workspace%3A. +➤ YN0085: │ - install-option-dep@file:./dep#./dep::hash=8572a9&locator=install-package-options%40workspace%3A. +➤ YN0000: └ Completed +➤ YN0000: ┌ Fetch step +➤ YN0000: └ Completed +➤ YN0000: ┌ Link step +➤ YN0000: └ Completed +➤ YN0000: · Done in +``` + +## `node -p require('./node_modules/install-option-dep/package.json').version` + +``` +2.0.0 +``` + +## `vp add ./dep --no-frozen-lockfile` + +direct add also warns for the unsupported immutable override + +``` +warn: yarn does not support --no-frozen-lockfile. +➤ YN0000: · Yarn +➤ YN0000: ┌ Resolution step +➤ YN0085: │ + install-option-dep@file:./dep#./dep::hash=8572a9&locator=install-package-options%40workspace%3A. +➤ YN0085: │ - install-option-dep@file:./dep-v2#./dep-v2::hash=5590bc&locator=install-package-options%40workspace%3A. +➤ YN0000: └ Completed +➤ YN0000: ┌ Fetch step +➤ YN0000: └ Completed +➤ YN0000: ┌ Link step +➤ YN0000: └ Completed +➤ YN0000: · Done in +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_berry_lockfile_only.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_berry_lockfile_only.md new file mode 100644 index 0000000000..4d6ee082f1 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_berry_lockfile_only.md @@ -0,0 +1,43 @@ +# yarn_berry_lockfile_only + +## `vpt json-edit package.json packageManager yarn@4.10.3` + + +## `vp install ./dep --lockfile-only --ignore-scripts` + +use update-lockfile rather than skip-build when both options are supplied + +``` +VITE+ - The Unified Toolchain for the Web + +warn: yarn@2+ --mode can only be specified once; --lockfile-only takes priority over --ignore-scripts +➤ YN0000: · Yarn +➤ YN0000: ┌ Resolution step +➤ YN0085: │ + install-option-dep@file:./dep#./dep::hash=8572a9&locator=install-package-options%40workspace%3A. +➤ YN0000: └ Completed +➤ YN0000: ┌ Fetch step +➤ YN0013: │ A package was added to the project (+ KiB). +➤ YN0000: └ Completed +➤ YN0000: ┌ Link step +➤ YN0073: │ Skipped due to mode=update-lockfile +➤ YN0000: └ Completed +➤ YN0000: · Done with warnings in +``` + +## `vpt stat-file yarn.lock --assert file` + +``` +yarn.lock: file +``` + +## `vpt stat-file node_modules --assert missing` + +``` +node_modules: missing +``` + +## `vpt stat-file .pnp.cjs --assert missing` + +``` +.pnp.cjs: missing +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_classic_frozen_lockfile_modes.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_classic_frozen_lockfile_modes.md new file mode 100644 index 0000000000..9107a42f4d --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_classic_frozen_lockfile_modes.md @@ -0,0 +1,117 @@ +# yarn_classic_frozen_lockfile_modes + +## `vpt json-edit package.json packageManager yarn@1.22.22` + + +## `vp install ./dep` + + +## `vpt cp yarn.lock before.lock` + + +## `vp install ./dep-v2 --frozen-lockfile` + +Classic add preserves the lockfile but allows manifest changes + +``` +VITE+ - The Unified Toolchain for the Web + +yarn add +[1/4] Resolving packages... +[2/4] Fetching packages... +[3/4] Linking dependencies... +[4/4] Building fresh packages... + +success Saved 1 new dependency. +info Direct dependencies +└─ install-option-dep@2.0.0 +info All dependencies +└─ install-option-dep@2.0.0 + +Done in . +``` + +## `node -e 'const fs = require('\''node:fs'\''); if ('\!'fs.readFileSync('\''yarn.lock'\'').equals(fs.readFileSync('\''before.lock'\''))) process.exit(1); console.log('\''lockfile unchanged'\'');'` + +``` +lockfile unchanged +``` + +## `node -p require('./package.json').dependencies['install-option-dep']` + +``` +./dep-v2 +``` + +## `vp install --frozen-lockfile` + +package-free installs still enforce the frozen lockfile + +**Exit code:** 1 + +``` +VITE+ - The Unified Toolchain for the Web + +yarn install +[1/4] Resolving packages... +error Your lockfile needs to be updated, but yarn was run with `--frozen-lockfile`. +info Visit https://yarnpkg.com/en/docs/cli/install for documentation about this command. +``` + +## `node -e 'const fs = require('\''node:fs'\''); if ('\!'fs.readFileSync('\''yarn.lock'\'').equals(fs.readFileSync('\''before.lock'\''))) process.exit(1); console.log('\''lockfile unchanged'\'');'` + +``` +lockfile unchanged +``` + +## `vp install --no-frozen-lockfile` + +package-free installs can explicitly allow lockfile changes + +``` +VITE+ - The Unified Toolchain for the Web + +yarn install +[1/4] Resolving packages... +[2/4] Fetching packages... +[3/4] Linking dependencies... +[4/4] Building fresh packages... + +success Saved lockfile. + +Done in . +``` + +## `node -p require('./node_modules/install-option-dep/package.json').version` + +``` +2.0.0 +``` + +## `vp add ./dep --no-frozen-lockfile` + +Classic add warns and drops the unsupported negated flag + +``` +warn: yarn does not support --no-frozen-lockfile. +yarn add +[1/4] Resolving packages... +[2/4] Fetching packages... +[3/4] Linking dependencies... +[4/4] Building fresh packages... + +success Saved lockfile. +success Saved 1 new dependency. +info Direct dependencies +└─ install-option-dep@1.0.0 +info All dependencies +└─ install-option-dep@1.0.0 + +Done in . +``` + +## `node -p require('./node_modules/install-option-dep/package.json').version` + +``` +1.0.0 +``` diff --git a/crates/vp_pm_cli/src/cli.rs b/crates/vp_pm_cli/src/cli.rs index ffa93bb3c8..d6d2cb354b 100644 --- a/crates/vp_pm_cli/src/cli.rs +++ b/crates/vp_pm_cli/src/cli.rs @@ -207,10 +207,7 @@ impl PackageManagerCommand { /// only command whose typed clap shape selects between two resolvers. pub(crate) fn resolve_for_manager(self, manager: &PackageManager) -> Result { match self { - Self::Install(args) if !args.packages.is_empty() => { - resolve_args_for_manager(manager, args.into_add_args()) - } - Self::Install(args) => resolve_args_for_manager(manager, args), + Self::Install(args) => args.resolve_args_for_manager(manager), Self::Add(args) => resolve_args_for_manager(manager, args), Self::Remove(args) => resolve_args_for_manager(manager, args), Self::Update(args) => resolve_args_for_manager(manager, args), diff --git a/crates/vp_pm_cli/src/resolution/commands/add.rs b/crates/vp_pm_cli/src/resolution/commands/add.rs index 937f477031..2c051efe08 100644 --- a/crates/vp_pm_cli/src/resolution/commands/add.rs +++ b/crates/vp_pm_cli/src/resolution/commands/add.rs @@ -2,7 +2,7 @@ use vp_pm_cli_macros::pm_args; use super::parse_positive_usize; use crate::resolution::{ - Bun, CommandBuilder, CommandResolution, Diagnostics, Npm, Pnpm, Resolve, Yarn, + Bun, CommandBuilder, CommandResolution, DiagnosticKind, Diagnostics, Npm, Pnpm, Resolve, Yarn, }; #[pm_args] @@ -31,6 +31,52 @@ pub struct AddArgs { #[arg(long)] pub(crate) ignore_scripts: bool, + /// Do not install optionalDependencies + #[arg(long, conflicts_with = "global", not_supported(yarn >= "2"))] + pub(crate) no_optional: bool, + + /// Fail if lockfile needs to be updated + // Yarn Classic accepts this on `add` but skips install's frozen-lockfile validation. + #[arg(long, conflicts_with = "global", overrides_with = "no_frozen_lockfile", not_supported(npm, pnpm, yarn >= "2"))] + pub(crate) frozen_lockfile: bool, + + /// Allow lockfile updates + #[arg( + long, + conflicts_with = "global", + overrides_with = "frozen_lockfile", + not_supported(npm, pnpm, yarn) + )] + pub(crate) no_frozen_lockfile: bool, + + /// Only update lockfile, don't install + #[arg(long, conflicts_with = "global", not_supported(yarn < "2"))] + pub(crate) lockfile_only: bool, + + /// Use cached packages when available + #[arg(long, conflicts_with = "global", not_supported(yarn >= "2", bun))] + pub(crate) prefer_offline: bool, + + /// Only use packages already in cache + #[arg(long, conflicts_with = "global", not_supported(yarn >= "2", bun))] + pub(crate) offline: bool, + + /// Force reinstall all dependencies + #[arg(short = 'f', long, conflicts_with = "global", not_supported(yarn >= "2"))] + pub(crate) force: bool, + + /// Don't read or generate lockfile + #[arg(long, conflicts_with = "global", not_supported(yarn >= "2", bun))] + pub(crate) no_lockfile: bool, + + /// Create flat node_modules (pnpm only) + #[arg(long, conflicts_with = "global", not_supported(npm, yarn, bun))] + pub(crate) shamefully_hoist: bool, + + /// Suppress package manager output + #[arg(long, conflicts_with = "global", not_supported(yarn >= "2"))] + pub(crate) silent: bool, + /// Filter packages in monorepo (can be used multiple times) #[arg(long, value_name = "PATTERN", not_supported(bun < "1.4"))] pub(crate) filter: Vec, @@ -151,6 +197,14 @@ impl Resolve for Pnpm { cmd.arg(vt_str::format!("--allow-build={allow_build}")); } cmd.arg_if("--ignore-scripts", args.ignore_scripts) + .arg_if("--no-optional", args.no_optional) + .arg_if("--lockfile-only", args.lockfile_only) + .arg_if("--prefer-offline", args.prefer_offline) + .arg_if("--offline", args.offline) + .arg_if("--force", args.force) + .arg_if("--no-lockfile", args.no_lockfile) + .arg_if("--shamefully-hoist", args.shamefully_hoist) + .arg_if("--silent", args.silent) .extend(args.pass_through_args.iter()) .extend(args.packages.iter()); cmd.into() @@ -189,8 +243,16 @@ impl Npm { } cmd.arg_if("--save-exact", args.save_exact) .arg_if("--ignore-scripts", args.ignore_scripts) - .extend(args.pass_through_args.iter()) - .extend(args.packages.iter()); + .arg_if("--omit=optional", args.no_optional) + .arg_if("--package-lock-only", args.lockfile_only) + .arg_if("--prefer-offline", args.prefer_offline) + .arg_if("--offline", args.offline) + .arg_if("--force", args.force) + .arg_if("--no-package-lock", args.no_lockfile); + if args.silent { + cmd.arg("--loglevel").arg("silent"); + } + cmd.extend(args.pass_through_args.iter()).extend(args.packages.iter()); cmd.into() } } @@ -202,7 +264,7 @@ impl Resolve for Npm { } impl Resolve for Yarn { - fn resolve(&self, args: &AddArgs, _diag: &mut Diagnostics) -> CommandResolution { + fn resolve(&self, args: &AddArgs, diag: &mut Diagnostics) -> CommandResolution { if args.global { return Npm::resolve_add(args); } @@ -233,12 +295,27 @@ impl Resolve for Yarn { Some(SaveDependencyTarget::Production) | None => {} } cmd.arg_if("--exact", args.save_exact); - if args.ignore_scripts { - if self.is_berry() { + if self.is_berry() { + if args.lockfile_only { + cmd.arg("--mode").arg("update-lockfile"); + if args.ignore_scripts { + diag.warn( + DiagnosticKind::BehaviorChange, + "yarn@2+ --mode can only be specified once; --lockfile-only takes priority over --ignore-scripts", + ); + } + } else if args.ignore_scripts { cmd.arg("--mode").arg("skip-build"); - } else { - cmd.arg("--ignore-scripts"); } + } else { + cmd.arg_if("--ignore-scripts", args.ignore_scripts) + .arg_if("--ignore-optional", args.no_optional) + .arg_if("--frozen-lockfile", args.frozen_lockfile) + .arg_if("--prefer-offline", args.prefer_offline) + .arg_if("--offline", args.offline) + .arg_if("--force", args.force) + .arg_if("--no-lockfile", args.no_lockfile) + .arg_if("--silent", args.silent); } cmd.extend(args.pass_through_args.iter()).extend(args.packages.iter()); cmd.into() @@ -267,8 +344,18 @@ impl Resolve for Bun { cmd.arg_if("--exact", args.save_exact) .arg_if("--catalog", args.save_catalog) .arg_if("--ignore-scripts", args.ignore_scripts) - .extend(args.pass_through_args.iter()) - .extend(args.packages.iter()); + .arg_if("--lockfile-only", args.lockfile_only) + .arg_if("--force", args.force) + .arg_if("--silent", args.silent); + if args.no_optional { + cmd.arg("--omit").arg("optional"); + } + if args.no_frozen_lockfile { + cmd.arg("--no-frozen-lockfile"); + } else { + cmd.arg_if("--frozen-lockfile", args.frozen_lockfile); + } + cmd.extend(args.pass_through_args.iter()).extend(args.packages.iter()); cmd.into() } } @@ -564,6 +651,81 @@ mod tests { assert_eq!(command.args, vec!["add", "--allow-build=react,napi", "react"]); } + #[test] + fn yarn_add_respects_frozen_lockfile_support() { + for flag in ["--frozen-lockfile", "--no-frozen-lockfile"] { + let args = parse_args::([flag, "react"]).unwrap(); + let classic = resolve(&yarn("1.22.22"), args.clone()); + if flag == "--frozen-lockfile" { + assert_eq!(expect_run(classic.outcome).args, ["add", flag, "react"]); + assert!(classic.diagnostics.is_empty()); + } else { + assert_eq!(expect_run(classic.outcome).args, ["add", "react"]); + assert_eq!(classic.diagnostics.len(), 1); + assert_eq!( + classic.diagnostics[0].message, + "yarn does not support --no-frozen-lockfile.", + ); + } + + let berry = resolve(&yarn("4.0.0"), args); + assert_eq!(expect_run(berry.outcome).args, ["add", "react"]); + assert_eq!(berry.diagnostics.len(), 1); + assert_eq!(berry.diagnostics[0].kind, DiagnosticKind::UnsupportedOptionDropped); + assert_eq!( + berry.diagnostics[0].message, + if flag == "--frozen-lockfile" { + "yarn >=2 does not support --frozen-lockfile." + } else { + "yarn does not support --no-frozen-lockfile." + }, + ); + } + } + + #[test] + fn yarn_lockfile_only_takes_priority_over_ignore_scripts() { + let resolution = resolve( + &yarn("4.0.0"), + AddArgs { lockfile_only: true, ignore_scripts: true, ..add_args(&["react"]) }, + ); + let command = expect_run(resolution.outcome); + assert_eq!(command.args, ["add", "--mode", "update-lockfile", "react"]); + assert_eq!(resolution.diagnostics.len(), 1); + assert_eq!(resolution.diagnostics[0].kind, DiagnosticKind::BehaviorChange); + } + + #[test] + fn add_install_options_do_not_change_managed_global_commands() { + for flag in [ + "--no-optional", + "--frozen-lockfile", + "--no-frozen-lockfile", + "--lockfile-only", + "--prefer-offline", + "--offline", + "--force", + "--no-lockfile", + "--shamefully-hoist", + "--silent", + ] { + let error = parse_args::(["--global", flag, "react"]).unwrap_err(); + assert_eq!(error.kind(), clap::error::ErrorKind::ArgumentConflict, "{flag}"); + } + } + + #[test] + fn add_frozen_lockfile_flags_use_last_value() { + for (first, last) in [ + ("--frozen-lockfile", "--no-frozen-lockfile"), + ("--no-frozen-lockfile", "--frozen-lockfile"), + ] { + let args = parse_args::([first, last, "react"]).unwrap(); + let command = expect_run(resolve(&bun("1.3.11"), args).outcome); + assert_eq!(command.args, ["add", last, "react"]); + } + } + #[test] fn test_bun_basic_add() { let resolution = resolve(&bun("1.3.11"), add_args(&["react"])); diff --git a/crates/vp_pm_cli/src/resolution/commands/install.rs b/crates/vp_pm_cli/src/resolution/commands/install.rs index 0f151049a1..01012a0945 100644 --- a/crates/vp_pm_cli/src/resolution/commands/install.rs +++ b/crates/vp_pm_cli/src/resolution/commands/install.rs @@ -1,9 +1,12 @@ use vp_pm_cli_macros::pm_args; use super::parse_positive_usize; -use crate::resolution::{ - AddArgs, Bun, CommandBuilder, CommandResolution, DiagnosticKind, Diagnostics, Npm, Pnpm, - Resolve, SaveDependencyArgs, Yarn, +use crate::{ + Error, PackageManager, + resolution::{ + AddArgs, Bun, CommandBuilder, CommandResolution, DiagnosticKind, Diagnostics, Npm, Pnpm, + Resolution, Resolve, SaveDependencyArgs, Yarn, resolve_for_manager, + }, }; #[pm_args] @@ -143,7 +146,49 @@ impl Resolve for Pnpm { } impl InstallArgs { - pub(crate) fn into_add_args(self) -> AddArgs { + pub(crate) fn resolve_args_for_manager( + mut self, + manager: &PackageManager, + ) -> Result { + let adding_packages = !self.packages.is_empty(); + // Diagnose the selected mode before conversion discards fields, and before + // manager-specific support rules can produce misleading or duplicate warnings. + let (mode, unsupported) = if adding_packages { + ( + "with package names", + vec![ + ("--fix-lockfile", std::mem::take(&mut self.fix_lockfile)), + ("--resolution-only", std::mem::take(&mut self.resolution_only)), + ], + ) + } else { + ( + "without package names", + vec![ + ("--save-exact", std::mem::take(&mut self.save_exact)), + ("--save-peer", std::mem::take(&mut self.save_peer)), + ("--save-optional", std::mem::take(&mut self.save_optional)), + ("--save-catalog", std::mem::take(&mut self.save_catalog)), + ], + ) + }; + let mut resolution = if adding_packages { + resolve_for_manager(manager, self.into_add_args())? + } else { + resolve_for_manager(manager, self)? + }; + for (option, supplied) in unsupported { + if supplied { + resolution.diagnostics.warn( + DiagnosticKind::UnsupportedOptionDropped, + vt_str::format!("install {mode} does not support {option}."), + ); + } + } + Ok(resolution) + } + + fn into_add_args(self) -> AddArgs { let save_dependency = if self.dev { SaveDependencyArgs { save_dev: true, ..Default::default() } } else if self.save_peer { @@ -163,6 +208,16 @@ impl InstallArgs { save_catalog: self.save_catalog, allow_build: None, ignore_scripts: self.ignore_scripts, + no_optional: self.no_optional, + frozen_lockfile: self.frozen_lockfile, + no_frozen_lockfile: self.no_frozen_lockfile, + lockfile_only: self.lockfile_only, + prefer_offline: self.prefer_offline, + offline: self.offline, + force: self.force, + no_lockfile: self.no_lockfile, + shamefully_hoist: self.shamefully_hoist, + silent: self.silent, filter: self.filter, workspace_root: self.workspace_root, workspace: false, @@ -295,6 +350,77 @@ mod tests { test_utils::{bun, expect_run, npm, pnpm, yarn}, }; + #[test] + fn install_with_packages_warns_on_install_only_options() { + let manager = crate::PackageManager::from_bin_prefix( + crate::PackageManagerType::Pnpm, + "11.24.0", + vt_path::current_dir().unwrap().join(".test-package-manager/bin"), + ); + let args = InstallArgs { + packages: vec!["react".to_string()], + fix_lockfile: true, + resolution_only: true, + lockfile_only: true, + save_exact: true, + ..Default::default() + }; + let resolution = + crate::cli::PackageManagerCommand::Install(args).resolve_for_manager(&manager).unwrap(); + assert_eq!( + expect_run(resolution.outcome).args, + ["add", "--save-exact", "--lockfile-only", "react"] + ); + assert_eq!( + resolution + .diagnostics + .iter() + .map(|diagnostic| diagnostic.message.as_str()) + .collect::>(), + [ + "install with package names does not support --fix-lockfile.", + "install with package names does not support --resolution-only.", + ], + ); + } + + #[test] + fn install_without_packages_warns_on_add_only_options() { + let manager = crate::PackageManager::from_bin_prefix( + crate::PackageManagerType::Npm, + "11.13.0", + vt_path::current_dir().unwrap().join(".test-package-manager/bin"), + ); + let args = InstallArgs { + save_exact: true, + save_peer: true, + save_optional: true, + save_catalog: true, + lockfile_only: true, + offline: true, + ..Default::default() + }; + let resolution = + crate::cli::PackageManagerCommand::Install(args).resolve_for_manager(&manager).unwrap(); + assert_eq!( + expect_run(resolution.outcome).args, + ["install", "--package-lock-only", "--offline"] + ); + assert_eq!( + resolution + .diagnostics + .iter() + .map(|diagnostic| diagnostic.message.as_str()) + .collect::>(), + [ + "install without package names does not support --save-exact.", + "install without package names does not support --save-peer.", + "install without package names does not support --save-optional.", + "install without package names does not support --save-catalog.", + ], + ); + } + #[test] fn test_pnpm_basic_install() { let command = expect_run(resolve(&pnpm("10.0.0"), InstallArgs::default()).outcome); From ab6a92dfbc48604c5f02d4b3dc3f198c3cc413d8 Mon Sep 17 00:00:00 2001 From: JongKyung Lee Date: Wed, 16 Sep 2026 15:05:00 +0900 Subject: [PATCH 02/12] fix(pm): reject frozen package additions with Yarn Classic Classic add accepts --frozen-lockfile without checking manifest and lockfile consistency. Reject this combination before execution for both add and install with package names. Preserve package-free frozen installs and other package managers. Verify rejected additions leave the manifest, lockfile, and installed dependency unchanged, while package-free installs still enforce the frozen lockfile. --- .../install_package_options/snapshots.toml | 10 ++-- .../yarn_classic_frozen_lockfile_modes.md | 52 +++++++++++++------ .../vp_pm_cli/src/resolution/commands/add.rs | 16 ++++-- 3 files changed, 56 insertions(+), 22 deletions(-) diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots.toml index 1164afb34e..302d83bdb3 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots.toml +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots.toml @@ -80,9 +80,13 @@ steps = [ { argv = ["vpt", "json-edit", "package.json", "packageManager", "yarn@1.22.22"], snapshot = false }, { argv = ["vp", "install", "./dep"], snapshot = false }, { argv = ["vpt", "cp", "yarn.lock", "before.lock"], snapshot = false }, - { argv = ["vp", "install", "./dep-v2", "--frozen-lockfile"], comment = "Classic add preserves the lockfile but allows manifest changes" }, - ["node", "-e", "const fs = require('node:fs'); if (!fs.readFileSync('yarn.lock').equals(fs.readFileSync('before.lock'))) process.exit(1); console.log('lockfile unchanged');"], - ["node", "-p", "require('./package.json').dependencies['install-option-dep']"], + { argv = ["vpt", "cp", "package.json", "before.json"], snapshot = false }, + { argv = ["vp", "install", "./dep-v2", "--frozen-lockfile"], comment = "reject named-package frozen installs before invoking Classic add", continue-on-failure = true }, + { argv = ["vp", "add", "./dep-v2", "--frozen-lockfile"], comment = "direct add rejects the same combination", continue-on-failure = true }, + { argv = ["node", "-e", "const fs = require('node:fs'); for (const [file, before] of [['package.json', 'before.json'], ['yarn.lock', 'before.lock']]) { if (!fs.readFileSync(file).equals(fs.readFileSync(before))) process.exit(1); } console.log('manifest and lockfile unchanged');"], continue-on-failure = true }, + ["node", "-p", "require('./node_modules/install-option-dep/package.json').version"], + { argv = ["vp", "install", "--frozen-lockfile"], comment = "package-free frozen installs succeed when the manifest and lockfile agree" }, + { argv = ["vpt", "json-edit", "package.json", "dependencies.install-option-dep", "file:./dep-v2"], snapshot = false }, { argv = ["vp", "install", "--frozen-lockfile"], comment = "package-free installs still enforce the frozen lockfile", continue-on-failure = true }, { argv = ["node", "-e", "const fs = require('node:fs'); if (!fs.readFileSync('yarn.lock').equals(fs.readFileSync('before.lock'))) process.exit(1); console.log('lockfile unchanged');"], continue-on-failure = true }, { argv = ["vp", "install", "--no-frozen-lockfile"], comment = "package-free installs can explicitly allow lockfile changes" }, diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_classic_frozen_lockfile_modes.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_classic_frozen_lockfile_modes.md index 9107a42f4d..6d098502af 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_classic_frozen_lockfile_modes.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_classic_frozen_lockfile_modes.md @@ -9,40 +9,60 @@ ## `vpt cp yarn.lock before.lock` +## `vpt cp package.json before.json` + + ## `vp install ./dep-v2 --frozen-lockfile` -Classic add preserves the lockfile but allows manifest changes +reject named-package frozen installs before invoking Classic add + +**Exit code:** 1 ``` VITE+ - The Unified Toolchain for the Web -yarn add -[1/4] Resolving packages... -[2/4] Fetching packages... -[3/4] Linking dependencies... -[4/4] Building fresh packages... +Invalid argument: Yarn Classic `add` cannot enforce `--frozen-lockfile`. +``` -success Saved 1 new dependency. -info Direct dependencies -└─ install-option-dep@2.0.0 -info All dependencies -└─ install-option-dep@2.0.0 +## `vp add ./dep-v2 --frozen-lockfile` -Done in . +direct add rejects the same combination + +**Exit code:** 1 + +``` +Invalid argument: Yarn Classic `add` cannot enforce `--frozen-lockfile`. ``` -## `node -e 'const fs = require('\''node:fs'\''); if ('\!'fs.readFileSync('\''yarn.lock'\'').equals(fs.readFileSync('\''before.lock'\''))) process.exit(1); console.log('\''lockfile unchanged'\'');'` +## `node -e 'const fs = require('\''node:fs'\''); for (const [file, before] of [['\''package.json'\'', '\''before.json'\''], ['\''yarn.lock'\'', '\''before.lock'\'']]) { if ('\!'fs.readFileSync(file).equals(fs.readFileSync(before))) process.exit(1); } console.log('\''manifest and lockfile unchanged'\'');'` ``` -lockfile unchanged +manifest and lockfile unchanged ``` -## `node -p require('./package.json').dependencies['install-option-dep']` +## `node -p require('./node_modules/install-option-dep/package.json').version` + +``` +1.0.0 +``` + +## `vp install --frozen-lockfile` + +package-free frozen installs succeed when the manifest and lockfile agree ``` -./dep-v2 +VITE+ - The Unified Toolchain for the Web + +yarn install +[1/4] Resolving packages... +success Already up-to-date. + +Done in . ``` +## `vpt json-edit package.json dependencies.install-option-dep file:./dep-v2` + + ## `vp install --frozen-lockfile` package-free installs still enforce the frozen lockfile diff --git a/crates/vp_pm_cli/src/resolution/commands/add.rs b/crates/vp_pm_cli/src/resolution/commands/add.rs index 2c051efe08..166866d0a3 100644 --- a/crates/vp_pm_cli/src/resolution/commands/add.rs +++ b/crates/vp_pm_cli/src/resolution/commands/add.rs @@ -36,7 +36,6 @@ pub struct AddArgs { pub(crate) no_optional: bool, /// Fail if lockfile needs to be updated - // Yarn Classic accepts this on `add` but skips install's frozen-lockfile validation. #[arg(long, conflicts_with = "global", overrides_with = "no_frozen_lockfile", not_supported(npm, pnpm, yarn >= "2"))] pub(crate) frozen_lockfile: bool, @@ -269,6 +268,14 @@ impl Resolve for Yarn { return Npm::resolve_add(args); } + // Classic accepts the flag but skips the frozen-lockfile consistency check on add. + if !self.is_berry() && args.frozen_lockfile { + return CommandResolution::InvalidArgument( + "Invalid argument: Yarn Classic `add` cannot enforce `--frozen-lockfile`." + .to_string(), + ); + } + let mut cmd = CommandBuilder::new("yarn"); if !args.filter.is_empty() { if !self.is_berry() { @@ -310,7 +317,6 @@ impl Resolve for Yarn { } else { cmd.arg_if("--ignore-scripts", args.ignore_scripts) .arg_if("--ignore-optional", args.no_optional) - .arg_if("--frozen-lockfile", args.frozen_lockfile) .arg_if("--prefer-offline", args.prefer_offline) .arg_if("--offline", args.offline) .arg_if("--force", args.force) @@ -657,7 +663,11 @@ mod tests { let args = parse_args::([flag, "react"]).unwrap(); let classic = resolve(&yarn("1.22.22"), args.clone()); if flag == "--frozen-lockfile" { - assert_eq!(expect_run(classic.outcome).args, ["add", flag, "react"]); + assert!(matches!( + classic.outcome, + CommandResolution::InvalidArgument(message) + if message == "Invalid argument: Yarn Classic `add` cannot enforce `--frozen-lockfile`." + )); assert!(classic.diagnostics.is_empty()); } else { assert_eq!(expect_run(classic.outcome).args, ["add", "react"]); From a09f66a836e201e12c08ea666cebc9e87fd7d021 Mon Sep 17 00:00:00 2001 From: JongKyung Lee Date: Wed, 16 Sep 2026 15:24:36 +0900 Subject: [PATCH 03/12] fix(pm): warn on frozen lockfile options for Yarn add Use the shared unsupported-option policy for all Yarn add versions instead of rejecting frozen additions only on Classic. Warn, drop the option, and continue with the add command. Preserve frozen-lockfile enforcement for package-free installs and Bun add. Cover both add entrypoints and package-free frozen installs in snapshots. --- .../install_package_options/snapshots.toml | 8 +-- .../yarn_berry_frozen_lockfile_modes.md | 2 +- .../yarn_classic_frozen_lockfile_modes.md | 62 ++++++++++++------- .../vp_pm_cli/src/resolution/commands/add.rs | 57 ++++++----------- 4 files changed, 64 insertions(+), 65 deletions(-) diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots.toml index 302d83bdb3..9a259f0878 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots.toml +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots.toml @@ -78,12 +78,10 @@ name = "yarn_classic_frozen_lockfile_modes" vp = "global" steps = [ { argv = ["vpt", "json-edit", "package.json", "packageManager", "yarn@1.22.22"], snapshot = false }, - { argv = ["vp", "install", "./dep"], snapshot = false }, + { argv = ["vp", "install", "./dep", "--frozen-lockfile"], comment = "named-package installs warn and drop the unsupported add option" }, + ["vpt", "stat-file", "yarn.lock", "--assert", "file"], + { argv = ["vp", "add", "./dep", "--frozen-lockfile"], comment = "direct add also warns and continues without the option" }, { argv = ["vpt", "cp", "yarn.lock", "before.lock"], snapshot = false }, - { argv = ["vpt", "cp", "package.json", "before.json"], snapshot = false }, - { argv = ["vp", "install", "./dep-v2", "--frozen-lockfile"], comment = "reject named-package frozen installs before invoking Classic add", continue-on-failure = true }, - { argv = ["vp", "add", "./dep-v2", "--frozen-lockfile"], comment = "direct add rejects the same combination", continue-on-failure = true }, - { argv = ["node", "-e", "const fs = require('node:fs'); for (const [file, before] of [['package.json', 'before.json'], ['yarn.lock', 'before.lock']]) { if (!fs.readFileSync(file).equals(fs.readFileSync(before))) process.exit(1); } console.log('manifest and lockfile unchanged');"], continue-on-failure = true }, ["node", "-p", "require('./node_modules/install-option-dep/package.json').version"], { argv = ["vp", "install", "--frozen-lockfile"], comment = "package-free frozen installs succeed when the manifest and lockfile agree" }, { argv = ["vpt", "json-edit", "package.json", "dependencies.install-option-dep", "file:./dep-v2"], snapshot = false }, diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_berry_frozen_lockfile_modes.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_berry_frozen_lockfile_modes.md index 427e424269..972b1c3a2a 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_berry_frozen_lockfile_modes.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_berry_frozen_lockfile_modes.md @@ -14,7 +14,7 @@ named-package installs use add, which has no immutable option ``` VITE+ - The Unified Toolchain for the Web -warn: yarn >=2 does not support --frozen-lockfile. +warn: yarn does not support --frozen-lockfile. ➤ YN0000: · Yarn ➤ YN0000: ┌ Resolution step ➤ YN0085: │ + install-option-dep@file:./dep#./dep::hash=8572a9&locator=install-package-options%40workspace%3A. diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_classic_frozen_lockfile_modes.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_classic_frozen_lockfile_modes.md index 6d098502af..bb1fa1a3e6 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_classic_frozen_lockfile_modes.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_classic_frozen_lockfile_modes.md @@ -3,42 +3,60 @@ ## `vpt json-edit package.json packageManager yarn@1.22.22` -## `vp install ./dep` +## `vp install ./dep --frozen-lockfile` +named-package installs warn and drop the unsupported add option -## `vpt cp yarn.lock before.lock` - - -## `vpt cp package.json before.json` +``` +VITE+ - The Unified Toolchain for the Web +warn: yarn does not support --frozen-lockfile. +yarn add +info No lockfile found. +[1/4] Resolving packages... +[2/4] Fetching packages... +[3/4] Linking dependencies... +[4/4] Building fresh packages... -## `vp install ./dep-v2 --frozen-lockfile` +success Saved lockfile. +success Saved 1 new dependency. +info Direct dependencies +└─ install-option-dep@1.0.0 +info All dependencies +└─ install-option-dep@1.0.0 -reject named-package frozen installs before invoking Classic add +Done in . +``` -**Exit code:** 1 +## `vpt stat-file yarn.lock --assert file` ``` -VITE+ - The Unified Toolchain for the Web - -Invalid argument: Yarn Classic `add` cannot enforce `--frozen-lockfile`. +yarn.lock: file ``` -## `vp add ./dep-v2 --frozen-lockfile` +## `vp add ./dep --frozen-lockfile` -direct add rejects the same combination +direct add also warns and continues without the option -**Exit code:** 1 - -``` -Invalid argument: Yarn Classic `add` cannot enforce `--frozen-lockfile`. ``` +warn: yarn does not support --frozen-lockfile. +yarn add +[1/4] Resolving packages... +[2/4] Fetching packages... +[3/4] Linking dependencies... +[4/4] Building fresh packages... -## `node -e 'const fs = require('\''node:fs'\''); for (const [file, before] of [['\''package.json'\'', '\''before.json'\''], ['\''yarn.lock'\'', '\''before.lock'\'']]) { if ('\!'fs.readFileSync(file).equals(fs.readFileSync(before))) process.exit(1); } console.log('\''manifest and lockfile unchanged'\'');'` +success Saved 1 new dependency. +info Direct dependencies +└─ install-option-dep@1.0.0 +info All dependencies +└─ install-option-dep@1.0.0 +Done in . ``` -manifest and lockfile unchanged -``` + +## `vpt cp yarn.lock before.lock` + ## `node -p require('./node_modules/install-option-dep/package.json').version` @@ -55,7 +73,9 @@ VITE+ - The Unified Toolchain for the Web yarn install [1/4] Resolving packages... -success Already up-to-date. +[2/4] Fetching packages... +[3/4] Linking dependencies... +[4/4] Building fresh packages... Done in . ``` diff --git a/crates/vp_pm_cli/src/resolution/commands/add.rs b/crates/vp_pm_cli/src/resolution/commands/add.rs index 166866d0a3..eb00a11789 100644 --- a/crates/vp_pm_cli/src/resolution/commands/add.rs +++ b/crates/vp_pm_cli/src/resolution/commands/add.rs @@ -36,7 +36,12 @@ pub struct AddArgs { pub(crate) no_optional: bool, /// Fail if lockfile needs to be updated - #[arg(long, conflicts_with = "global", overrides_with = "no_frozen_lockfile", not_supported(npm, pnpm, yarn >= "2"))] + #[arg( + long, + conflicts_with = "global", + overrides_with = "no_frozen_lockfile", + not_supported(npm, pnpm, yarn) + )] pub(crate) frozen_lockfile: bool, /// Allow lockfile updates @@ -268,14 +273,6 @@ impl Resolve for Yarn { return Npm::resolve_add(args); } - // Classic accepts the flag but skips the frozen-lockfile consistency check on add. - if !self.is_berry() && args.frozen_lockfile { - return CommandResolution::InvalidArgument( - "Invalid argument: Yarn Classic `add` cannot enforce `--frozen-lockfile`." - .to_string(), - ); - } - let mut cmd = CommandBuilder::new("yarn"); if !args.filter.is_empty() { if !self.is_berry() { @@ -658,38 +655,22 @@ mod tests { } #[test] - fn yarn_add_respects_frozen_lockfile_support() { - for flag in ["--frozen-lockfile", "--no-frozen-lockfile"] { - let args = parse_args::([flag, "react"]).unwrap(); - let classic = resolve(&yarn("1.22.22"), args.clone()); - if flag == "--frozen-lockfile" { - assert!(matches!( - classic.outcome, - CommandResolution::InvalidArgument(message) - if message == "Invalid argument: Yarn Classic `add` cannot enforce `--frozen-lockfile`." - )); - assert!(classic.diagnostics.is_empty()); - } else { - assert_eq!(expect_run(classic.outcome).args, ["add", "react"]); - assert_eq!(classic.diagnostics.len(), 1); + fn yarn_add_drops_frozen_lockfile_options() { + for version in ["1.22.22", "4.0.0"] { + for flag in ["--frozen-lockfile", "--no-frozen-lockfile"] { + let args = parse_args::([flag, "react"]).unwrap(); + let resolution = resolve(&yarn(version), args); + assert_eq!(expect_run(resolution.outcome).args, ["add", "react"]); + assert_eq!(resolution.diagnostics.len(), 1); + assert_eq!( + resolution.diagnostics[0].kind, + DiagnosticKind::UnsupportedOptionDropped + ); assert_eq!( - classic.diagnostics[0].message, - "yarn does not support --no-frozen-lockfile.", + resolution.diagnostics[0].message, + vt_str::format!("yarn does not support {flag}.").as_str(), ); } - - let berry = resolve(&yarn("4.0.0"), args); - assert_eq!(expect_run(berry.outcome).args, ["add", "react"]); - assert_eq!(berry.diagnostics.len(), 1); - assert_eq!(berry.diagnostics[0].kind, DiagnosticKind::UnsupportedOptionDropped); - assert_eq!( - berry.diagnostics[0].message, - if flag == "--frozen-lockfile" { - "yarn >=2 does not support --frozen-lockfile." - } else { - "yarn does not support --no-frozen-lockfile." - }, - ); } } From ac242219c356f277265f945087351fdd821e3126 Mon Sep 17 00:00:00 2001 From: JongKyung Lee Date: Wed, 16 Sep 2026 15:43:41 +0900 Subject: [PATCH 04/12] fix(pm): honor add silent mode in wrapper output Include add's silent flag in the shared quiet-mode and diagnostics checks so Vite+ suppresses compatibility warnings and cached upgrade notices. Cover silent and normal add classification, and verify a silent add leaves the pending upgrade notice available for the next interactive command. --- .../command_upgrade_check/snapshots.toml | 5 +++++ .../command_upgrade_background_notice.md | 19 +++++++++++++++++++ crates/vp_pm_cli/src/cli.rs | 11 ++++++++++- 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_upgrade_check/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_upgrade_check/snapshots.toml index 493fa12dca..d718be0715 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_upgrade_check/snapshots.toml +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_upgrade_check/snapshots.toml @@ -17,6 +17,11 @@ steps = [ { argv = ["node", "-e", "(async()=>{const fs=require('node:fs');const path=require('node:path');const file=path.join(process.env.VP_HOME,'cache','upgrade-check.json');const deadline=Date.now()+5000;for(;;){try{if(JSON.parse(fs.readFileSync(file,'utf8')).status==='available')return}catch{}if(Date.now()>=deadline)process.exit(1);await new Promise(resolve=>setTimeout(resolve,25))}})()"], snapshot = false }, { argv = ["vpt", "grep-file", "$VP_HOME/cache/upgrade-check.json", '"status":"available"'], snapshot = false }, { argv = ["vp", "env", "list", "--json"], comment = "Machine-readable output does not consume the pending notice.", snapshot = false }, + { argv = ["vpt", "write-file", "package.json", '{"name":"silent-add","private":true,"packageManager":"npm@11.13.0"}'], snapshot = false }, + { argv = ["vpt", "mkdir", "dep"], snapshot = false }, + { argv = ["vpt", "write-file", "dep/package.json", '{"name":"silent-add-dep","version":"1.0.0"}'], snapshot = false }, + { argv = ["vp", "add", "./dep", "--silent", "--save-catalog", "--lockfile-only"], comment = "Silent add suppresses compatibility diagnostics and leaves the cached update notice pending." }, + { argv = ["vpt", "stat-file", "package-lock.json", "--assert", "file"], snapshot = false }, { argv = ["vp", "env", "off"], comment = "The next interactive command displays the cached update notice." }, { argv = ["vp", "env", "off"], comment = "A subsequent command stays quiet after the notice timestamp is recorded." }, ] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_upgrade_check/snapshots/command_upgrade_background_notice.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_upgrade_check/snapshots/command_upgrade_background_notice.md index 96ff3840a1..64b3baf14f 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_upgrade_check/snapshots/command_upgrade_background_notice.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_upgrade_check/snapshots/command_upgrade_background_notice.md @@ -18,6 +18,25 @@ The foreground command launches the detached checker and returns without waiting Machine-readable output does not consume the pending notice. +## `vpt write-file package.json '{"name":"silent-add","private":true,"packageManager":"npm@11.13.0"}'` + + +## `vpt mkdir dep` + + +## `vpt write-file dep/package.json '{"name":"silent-add-dep","version":"1.0.0"}'` + + +## `vp add ./dep --silent --save-catalog --lockfile-only` + +Silent add suppresses compatibility diagnostics and leaves the cached update notice pending. + +``` +``` + +## `vpt stat-file package-lock.json --assert file` + + ## `vp env off` The next interactive command displays the cached update notice. diff --git a/crates/vp_pm_cli/src/cli.rs b/crates/vp_pm_cli/src/cli.rs index d6d2cb354b..3196434d32 100644 --- a/crates/vp_pm_cli/src/cli.rs +++ b/crates/vp_pm_cli/src/cli.rs @@ -280,6 +280,7 @@ impl PackageManagerCommand { pub fn is_quiet_or_machine_readable(&self) -> bool { match self { Self::Install(args) => args.silent, + Self::Add(args) => args.silent, Self::Dlx(args) => args.silent, Self::Outdated(args) => { matches!(args.format, Some(OutdatedFormat::Json | OutdatedFormat::List)) @@ -298,6 +299,7 @@ impl PackageManagerCommand { pub(crate) fn should_render_diagnostics(&self) -> bool { match self { Self::Install(args) => !args.silent, + Self::Add(args) => !args.silent, Self::Dlx(args) => !args.silent, _ => true, } @@ -664,6 +666,7 @@ mod tests { fn classifies_quiet_and_machine_readable_commands() { for args in [ &["install", "--silent"][..], + &["add", "react", "--silent"][..], &["dlx", "--silent", "tsx"][..], &["outdated", "--format", "json"][..], &["why", "react", "--parseable"][..], @@ -686,6 +689,7 @@ mod tests { assert!(!parse(args).unwrap().is_quiet_or_machine_readable(), "{args:?}"); } assert!(!parse(&["install"]).unwrap().is_quiet_or_machine_readable()); + assert!(!parse(&["add", "react"]).unwrap().is_quiet_or_machine_readable()); } #[test] @@ -757,6 +761,7 @@ mod tests { #[test] fn suppresses_diagnostics_only_for_explicit_silent_modes() { for args in [ + &["add", "react"][..], &["outdated", "--format", "json"][..], &["why", "react", "--parseable"][..], &["info", "react", "--json"][..], @@ -765,7 +770,11 @@ mod tests { assert!(parse(args).unwrap().should_render_diagnostics(), "{args:?}"); } - for args in [&["install", "--silent"][..], &["dlx", "--silent", "tsx"][..]] { + for args in [ + &["install", "--silent"][..], + &["add", "react", "--silent"][..], + &["dlx", "--silent", "tsx"][..], + ] { assert!(!parse(args).unwrap().should_render_diagnostics(), "{args:?}"); } } From 62034d33ba9e8170c0e47d6ed8329228e51a3b18 Mon Sep 17 00:00:00 2001 From: JongKyung Lee Date: Wed, 16 Sep 2026 16:03:31 +0900 Subject: [PATCH 05/12] test(pm): stabilize install option fixtures and refresh help Use project npm configuration for the offline probe and isolate Berry's cache. Give the updated local dependency a distinct manifest size so Yarn Classic does not skip copying files with identical size and mtime. Refresh the add help and affected snapshots. Leave cross-platform Yarn hash normalization for a separate change. --- .../cli_helper_message/snapshots/cli_helper_message.md | 10 ++++++++++ .../install_package_options/dep-v2/package.json | 3 ++- .../fixtures/install_package_options/snapshots.toml | 8 +++++--- .../install_package_options/snapshots/npm_offline.md | 10 ++++++++-- .../snapshots/yarn_berry_frozen_lockfile_modes.md | 10 ++++++---- 5 files changed, 31 insertions(+), 10 deletions(-) diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/cli_helper_message/snapshots/cli_helper_message.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/cli_helper_message/snapshots/cli_helper_message.md index 0f320133e7..332f564d54 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/cli_helper_message/snapshots/cli_helper_message.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/cli_helper_message/snapshots/cli_helper_message.md @@ -160,6 +160,16 @@ Options: --save-catalog Save the new dependency to the default catalog --allow-build A list of package names allowed to run postinstall --ignore-scripts Do not run lifecycle scripts + --no-optional Do not install optionalDependencies + --frozen-lockfile Fail if lockfile needs to be updated + --no-frozen-lockfile Allow lockfile updates + --lockfile-only Only update lockfile, don't install + --prefer-offline Use cached packages when available + --offline Only use packages already in cache + -f, --force Force reinstall all dependencies + --no-lockfile Don't read or generate lockfile + --shamefully-hoist Create flat node_modules (pnpm only) + --silent Suppress package manager output --filter Filter packages in monorepo (can be used multiple times) -w, --workspace-root Add to workspace root --workspace Only add if package exists in workspace (pnpm-specific) diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/dep-v2/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/dep-v2/package.json index 295a4ceded..59b804c5da 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/dep-v2/package.json +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/dep-v2/package.json @@ -1,4 +1,5 @@ { "name": "install-option-dep", - "version": "2.0.0" + "version": "2.0.0", + "description": "Updated dependency fixture" } diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots.toml index 9a259f0878..b6bcce133e 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots.toml +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots.toml @@ -32,7 +32,8 @@ steps = [ name = "npm_offline" vp = "global" steps = [ - { argv = ["vp", "install", "vp-install-option-uncached-probe@1.0.0", "--offline", "--", "--registry=http://127.0.0.1:9", "--fetch-retries=0"], comment = "an uncached package fails in offline mode rather than attempting a connection", continue-on-failure = true }, + { argv = ["vpt", "write-file", ".npmrc", "registry=http://127.0.0.1:9\nfetch-retries=0\ncache=.npm-cache\n"], snapshot = false }, + { argv = ["vp", "install", "vp-install-option-uncached-probe@1.0.0", "--offline"], comment = "an uncached package fails in offline mode rather than attempting a connection", continue-on-failure = true }, { argv = ["vpt", "stat-file", "node_modules", "--assert", "missing"], continue-on-failure = true }, ["vpt", "stat-file", "package-lock.json", "--assert", "missing"], ] @@ -64,7 +65,7 @@ steps = [ [[case]] name = "yarn_berry_lockfile_only" vp = "global" -env = { YARN_ENABLE_TELEMETRY = "0" } +env = { YARN_ENABLE_TELEMETRY = "0", YARN_ENABLE_GLOBAL_CACHE = "0" } steps = [ { argv = ["vpt", "json-edit", "package.json", "packageManager", "yarn@4.10.3"], snapshot = false }, { argv = ["vp", "install", "./dep", "--lockfile-only", "--ignore-scripts"], comment = "use update-lockfile rather than skip-build when both options are supplied" }, @@ -76,6 +77,7 @@ steps = [ [[case]] name = "yarn_classic_frozen_lockfile_modes" vp = "global" +# dep-v2's manifest must differ in size because Classic skips copying files with equal size/mtime. steps = [ { argv = ["vpt", "json-edit", "package.json", "packageManager", "yarn@1.22.22"], snapshot = false }, { argv = ["vp", "install", "./dep", "--frozen-lockfile"], comment = "named-package installs warn and drop the unsupported add option" }, @@ -96,7 +98,7 @@ steps = [ [[case]] name = "yarn_berry_frozen_lockfile_modes" vp = "global" -env = { YARN_ENABLE_TELEMETRY = "0" } +env = { YARN_ENABLE_TELEMETRY = "0", YARN_ENABLE_GLOBAL_CACHE = "0" } steps = [ { argv = ["vpt", "json-edit", "package.json", "packageManager", "yarn@4.10.3"], snapshot = false }, { argv = ["vpt", "write-file", ".yarnrc.yml", "nodeLinker: node-modules\n"], snapshot = false }, diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/npm_offline.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/npm_offline.md index b5893e6aaf..93ebb9fbfa 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/npm_offline.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/npm_offline.md @@ -1,6 +1,12 @@ # npm_offline -## `vp install vp-install-option-uncached-probe@1.0.0 --offline -- --registry=http://127.0.0.1:9 --fetch-retries=0` +## `vpt write-file .npmrc 'registry=http://127.0.0.1:9 +fetch-retries=0 +cache=.npm-cache +'` + + +## `vp install vp-install-option-uncached-probe@1.0.0 --offline` an uncached package fails in offline mode rather than attempting a connection @@ -11,7 +17,7 @@ VITE+ - The Unified Toolchain for the Web npm error code ENOTCACHED npm error request to http://127.0.0.1:/vp-install-option-uncached-probe failed: cache mode is 'only-if-cached' but no cached response is available. -npm error A complete log of this run can be found in: /.npm/_logs/-debug-0.log +npm error A complete log of this run can be found in: /.npm-cache/_logs/-debug-0.log ``` ## `vpt stat-file node_modules --assert missing` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_berry_frozen_lockfile_modes.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_berry_frozen_lockfile_modes.md index 972b1c3a2a..777d8637e9 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_berry_frozen_lockfile_modes.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_berry_frozen_lockfile_modes.md @@ -44,7 +44,7 @@ VITE+ - The Unified Toolchain for the Web ➤ YN0000: · Yarn ➤ YN0000: ┌ Resolution step -➤ YN0085: │ + install-option-dep@file:./dep-v2#./dep-v2::hash=5590bc&locator=install-package-options%40workspace%3A. +➤ YN0085: │ + install-option-dep@file:./dep-v2#./dep-v2::hash=ca2da2&locator=install-package-options%40workspace%3A. ➤ YN0085: │ - install-option-dep@file:./dep#./dep::hash=8572a9&locator=install-package-options%40workspace%3A. ➤ YN0000: └ Completed @@ -60,7 +60,7 @@ VITE+ - The Unified Toolchain for the Web ➤ YN0028: │ - checksum: 10c0/32675cb2e55f886e9f975fdaefa9cb706071880078bda04af25dd0343941bf33780453c0ec848b30d3aa53cb9d5d5b3a6aeef902552100f255ed35fac8a8ff06 ➤ YN0028: │ +"install-option-dep@file:./dep-v2::locator=install-package-options%40workspace%3A.": ➤ YN0028: │ + version: 2.0.0 -➤ YN0028: │ + resolution: "install-option-dep@file:./dep-v2#./dep-v2::hash=5590bc&locator=install-package-options%40workspace%3A." +➤ YN0028: │ + resolution: "install-option-dep@file:./dep-v2#./dep-v2::hash=ca2da2&locator=install-package-options%40workspace%3A." ➤ YN0000: │ languageName: node ➤ YN0000: │ linkType: hard ➤ YN0000: │ @@ -93,10 +93,11 @@ VITE+ - The Unified Toolchain for the Web ➤ YN0000: · Yarn ➤ YN0000: ┌ Resolution step -➤ YN0085: │ + install-option-dep@file:./dep-v2#./dep-v2::hash=5590bc&locator=install-package-options%40workspace%3A. +➤ YN0085: │ + install-option-dep@file:./dep-v2#./dep-v2::hash=ca2da2&locator=install-package-options%40workspace%3A. ➤ YN0085: │ - install-option-dep@file:./dep#./dep::hash=8572a9&locator=install-package-options%40workspace%3A. ➤ YN0000: └ Completed ➤ YN0000: ┌ Fetch step +➤ YN0013: │ A package was added to the project, and one was removed (+ KiB). ➤ YN0000: └ Completed ➤ YN0000: ┌ Link step ➤ YN0000: └ Completed @@ -118,9 +119,10 @@ warn: yarn does not support --no-frozen-lockfile. ➤ YN0000: · Yarn ➤ YN0000: ┌ Resolution step ➤ YN0085: │ + install-option-dep@file:./dep#./dep::hash=8572a9&locator=install-package-options%40workspace%3A. -➤ YN0085: │ - install-option-dep@file:./dep-v2#./dep-v2::hash=5590bc&locator=install-package-options%40workspace%3A. +➤ YN0085: │ - install-option-dep@file:./dep-v2#./dep-v2::hash=ca2da2&locator=install-package-options%40workspace%3A. ➤ YN0000: └ Completed ➤ YN0000: ┌ Fetch step +➤ YN0013: │ A package was added to the project, and one was removed (- KiB). ➤ YN0000: └ Completed ➤ YN0000: ┌ Link step ➤ YN0000: └ Completed From 2590c2c000e940791da17754ea1485acaf54d45a Mon Sep 17 00:00:00 2001 From: JongKyung Lee Date: Wed, 16 Sep 2026 16:09:02 +0900 Subject: [PATCH 06/12] test(pm): defer Windows-specific fixture fixes Restore the npm offline probe and Berry cache settings, retaining only the help snapshot update and the Classic file-copy fixture correction. Regenerate snapshots without changing redaction. --- .../fixtures/install_package_options/snapshots.toml | 7 +++---- .../install_package_options/snapshots/npm_offline.md | 10 ++-------- .../snapshots/yarn_berry_frozen_lockfile_modes.md | 2 -- 3 files changed, 5 insertions(+), 14 deletions(-) diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots.toml index b6bcce133e..535de02fb3 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots.toml +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots.toml @@ -32,8 +32,7 @@ steps = [ name = "npm_offline" vp = "global" steps = [ - { argv = ["vpt", "write-file", ".npmrc", "registry=http://127.0.0.1:9\nfetch-retries=0\ncache=.npm-cache\n"], snapshot = false }, - { argv = ["vp", "install", "vp-install-option-uncached-probe@1.0.0", "--offline"], comment = "an uncached package fails in offline mode rather than attempting a connection", continue-on-failure = true }, + { argv = ["vp", "install", "vp-install-option-uncached-probe@1.0.0", "--offline", "--", "--registry=http://127.0.0.1:9", "--fetch-retries=0"], comment = "an uncached package fails in offline mode rather than attempting a connection", continue-on-failure = true }, { argv = ["vpt", "stat-file", "node_modules", "--assert", "missing"], continue-on-failure = true }, ["vpt", "stat-file", "package-lock.json", "--assert", "missing"], ] @@ -65,7 +64,7 @@ steps = [ [[case]] name = "yarn_berry_lockfile_only" vp = "global" -env = { YARN_ENABLE_TELEMETRY = "0", YARN_ENABLE_GLOBAL_CACHE = "0" } +env = { YARN_ENABLE_TELEMETRY = "0" } steps = [ { argv = ["vpt", "json-edit", "package.json", "packageManager", "yarn@4.10.3"], snapshot = false }, { argv = ["vp", "install", "./dep", "--lockfile-only", "--ignore-scripts"], comment = "use update-lockfile rather than skip-build when both options are supplied" }, @@ -98,7 +97,7 @@ steps = [ [[case]] name = "yarn_berry_frozen_lockfile_modes" vp = "global" -env = { YARN_ENABLE_TELEMETRY = "0", YARN_ENABLE_GLOBAL_CACHE = "0" } +env = { YARN_ENABLE_TELEMETRY = "0" } steps = [ { argv = ["vpt", "json-edit", "package.json", "packageManager", "yarn@4.10.3"], snapshot = false }, { argv = ["vpt", "write-file", ".yarnrc.yml", "nodeLinker: node-modules\n"], snapshot = false }, diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/npm_offline.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/npm_offline.md index 93ebb9fbfa..b5893e6aaf 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/npm_offline.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/npm_offline.md @@ -1,12 +1,6 @@ # npm_offline -## `vpt write-file .npmrc 'registry=http://127.0.0.1:9 -fetch-retries=0 -cache=.npm-cache -'` - - -## `vp install vp-install-option-uncached-probe@1.0.0 --offline` +## `vp install vp-install-option-uncached-probe@1.0.0 --offline -- --registry=http://127.0.0.1:9 --fetch-retries=0` an uncached package fails in offline mode rather than attempting a connection @@ -17,7 +11,7 @@ VITE+ - The Unified Toolchain for the Web npm error code ENOTCACHED npm error request to http://127.0.0.1:/vp-install-option-uncached-probe failed: cache mode is 'only-if-cached' but no cached response is available. -npm error A complete log of this run can be found in: /.npm-cache/_logs/-debug-0.log +npm error A complete log of this run can be found in: /.npm/_logs/-debug-0.log ``` ## `vpt stat-file node_modules --assert missing` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_berry_frozen_lockfile_modes.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_berry_frozen_lockfile_modes.md index 777d8637e9..600f5156a5 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_berry_frozen_lockfile_modes.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_berry_frozen_lockfile_modes.md @@ -97,7 +97,6 @@ VITE+ - The Unified Toolchain for the Web ➤ YN0085: │ - install-option-dep@file:./dep#./dep::hash=8572a9&locator=install-package-options%40workspace%3A. ➤ YN0000: └ Completed ➤ YN0000: ┌ Fetch step -➤ YN0013: │ A package was added to the project, and one was removed (+ KiB). ➤ YN0000: └ Completed ➤ YN0000: ┌ Link step ➤ YN0000: └ Completed @@ -122,7 +121,6 @@ warn: yarn does not support --no-frozen-lockfile. ➤ YN0085: │ - install-option-dep@file:./dep-v2#./dep-v2::hash=ca2da2&locator=install-package-options%40workspace%3A. ➤ YN0000: └ Completed ➤ YN0000: ┌ Fetch step -➤ YN0013: │ A package was added to the project, and one was removed (- KiB). ➤ YN0000: └ Completed ➤ YN0000: ┌ Link step ➤ YN0000: └ Completed From c5d9384d4caad8462de5ef90e92977ad0323e97e Mon Sep 17 00:00:00 2001 From: JongKyung Lee Date: Wed, 16 Sep 2026 16:31:34 +0900 Subject: [PATCH 07/12] test(pm): normalize platform-dependent Yarn snapshot hashes Mask file locator hashes and checksums in Yarn immutable lockfile diffs while preserving package versions and diagnostics. Add redaction coverage and regenerate the affected snapshots. --- .../yarn_berry_frozen_lockfile_modes.md | 20 +++++++++---------- .../snapshots/yarn_berry_lockfile_only.md | 2 +- .../tests/cli_snapshots/redact.rs | 8 ++++++++ crates/vp_cli_snapshots/tests/redact_unit.rs | 17 ++++++++++++++++ 4 files changed, 36 insertions(+), 11 deletions(-) diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_berry_frozen_lockfile_modes.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_berry_frozen_lockfile_modes.md index 600f5156a5..212f7019ee 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_berry_frozen_lockfile_modes.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_berry_frozen_lockfile_modes.md @@ -17,7 +17,7 @@ VITE+ - The Unified Toolchain for the Web warn: yarn does not support --frozen-lockfile. ➤ YN0000: · Yarn ➤ YN0000: ┌ Resolution step -➤ YN0085: │ + install-option-dep@file:./dep#./dep::hash=8572a9&locator=install-package-options%40workspace%3A. +➤ YN0085: │ + install-option-dep@file:./dep#./dep::hash=&locator=install-package-options%40workspace%3A. ➤ YN0000: └ Completed ➤ YN0000: ┌ Fetch step ➤ YN0013: │ A package was added to the project (+ KiB). @@ -44,8 +44,8 @@ VITE+ - The Unified Toolchain for the Web ➤ YN0000: · Yarn ➤ YN0000: ┌ Resolution step -➤ YN0085: │ + install-option-dep@file:./dep-v2#./dep-v2::hash=ca2da2&locator=install-package-options%40workspace%3A. -➤ YN0085: │ - install-option-dep@file:./dep#./dep::hash=8572a9&locator=install-package-options%40workspace%3A. +➤ YN0085: │ + install-option-dep@file:./dep-v2#./dep-v2::hash=&locator=install-package-options%40workspace%3A. +➤ YN0085: │ - install-option-dep@file:./dep#./dep::hash=&locator=install-package-options%40workspace%3A. ➤ YN0000: └ Completed ➤ YN0000: ┌ Post-resolution validation @@ -56,11 +56,11 @@ VITE+ - The Unified Toolchain for the Web ➤ YN0000: │ ➤ YN0028: │ -"install-option-dep@file:./dep::locator=install-package-options%40workspace%3A.": ➤ YN0028: │ - version: 1.0.0 -➤ YN0028: │ - resolution: "install-option-dep@file:./dep#./dep::hash=8572a9&locator=install-package-options%40workspace%3A." -➤ YN0028: │ - checksum: 10c0/32675cb2e55f886e9f975fdaefa9cb706071880078bda04af25dd0343941bf33780453c0ec848b30d3aa53cb9d5d5b3a6aeef902552100f255ed35fac8a8ff06 +➤ YN0028: │ - resolution: "install-option-dep@file:./dep#./dep::hash=&locator=install-package-options%40workspace%3A." +➤ YN0028: │ - checksum: ➤ YN0028: │ +"install-option-dep@file:./dep-v2::locator=install-package-options%40workspace%3A.": ➤ YN0028: │ + version: 2.0.0 -➤ YN0028: │ + resolution: "install-option-dep@file:./dep-v2#./dep-v2::hash=ca2da2&locator=install-package-options%40workspace%3A." +➤ YN0028: │ + resolution: "install-option-dep@file:./dep-v2#./dep-v2::hash=&locator=install-package-options%40workspace%3A." ➤ YN0000: │ languageName: node ➤ YN0000: │ linkType: hard ➤ YN0000: │ @@ -93,8 +93,8 @@ VITE+ - The Unified Toolchain for the Web ➤ YN0000: · Yarn ➤ YN0000: ┌ Resolution step -➤ YN0085: │ + install-option-dep@file:./dep-v2#./dep-v2::hash=ca2da2&locator=install-package-options%40workspace%3A. -➤ YN0085: │ - install-option-dep@file:./dep#./dep::hash=8572a9&locator=install-package-options%40workspace%3A. +➤ YN0085: │ + install-option-dep@file:./dep-v2#./dep-v2::hash=&locator=install-package-options%40workspace%3A. +➤ YN0085: │ - install-option-dep@file:./dep#./dep::hash=&locator=install-package-options%40workspace%3A. ➤ YN0000: └ Completed ➤ YN0000: ┌ Fetch step ➤ YN0000: └ Completed @@ -117,8 +117,8 @@ direct add also warns for the unsupported immutable override warn: yarn does not support --no-frozen-lockfile. ➤ YN0000: · Yarn ➤ YN0000: ┌ Resolution step -➤ YN0085: │ + install-option-dep@file:./dep#./dep::hash=8572a9&locator=install-package-options%40workspace%3A. -➤ YN0085: │ - install-option-dep@file:./dep-v2#./dep-v2::hash=ca2da2&locator=install-package-options%40workspace%3A. +➤ YN0085: │ + install-option-dep@file:./dep#./dep::hash=&locator=install-package-options%40workspace%3A. +➤ YN0085: │ - install-option-dep@file:./dep-v2#./dep-v2::hash=&locator=install-package-options%40workspace%3A. ➤ YN0000: └ Completed ➤ YN0000: ┌ Fetch step ➤ YN0000: └ Completed diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_berry_lockfile_only.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_berry_lockfile_only.md index 4d6ee082f1..36fec4e261 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_berry_lockfile_only.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_berry_lockfile_only.md @@ -13,7 +13,7 @@ VITE+ - The Unified Toolchain for the Web warn: yarn@2+ --mode can only be specified once; --lockfile-only takes priority over --ignore-scripts ➤ YN0000: · Yarn ➤ YN0000: ┌ Resolution step -➤ YN0085: │ + install-option-dep@file:./dep#./dep::hash=8572a9&locator=install-package-options%40workspace%3A. +➤ YN0085: │ + install-option-dep@file:./dep#./dep::hash=&locator=install-package-options%40workspace%3A. ➤ YN0000: └ Completed ➤ YN0000: ┌ Fetch step ➤ YN0013: │ A package was added to the project (+ KiB). diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/redact.rs b/crates/vp_cli_snapshots/tests/cli_snapshots/redact.rs index 50d83699f9..1785b92cf0 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/redact.rs +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/redact.rs @@ -306,6 +306,12 @@ static YARN1_STEP_EMOJI_RE: LazyLock = LazyLock::new(|| { // depends on what ran earlier in the environment; strip it entirely. static YARN_TELEMETRY_RE: LazyLock = LazyLock::new(|| regex::Regex::new(r"(?m)^\u{27A4} YN0065: [^\n]*\n(?:[ \t]*\n)*").unwrap()); +// Yarn's file archive hashes and lockfile checksums differ across platforms. +static YARN_FILE_HASH_RE: LazyLock = + LazyLock::new(|| regex::Regex::new(r"(::hash=)[0-9a-f]+(&locator=)").unwrap()); +static YARN_LOCKFILE_CHECKSUM_RE: LazyLock = LazyLock::new(|| { + regex::Regex::new(r"(?m)^(\x{27A4} YN0028: [^\n]*checksum: )[0-9a-f]+/[0-9a-f]+").unwrap() +}); // `vp staged` reports the backup stash it created; the short hash covers a // commit of the working tree at run time, so it can never be stable. static STASH_HASH_RE: LazyLock = LazyLock::new(|| { @@ -622,6 +628,8 @@ pub fn redact_output( // notice, and the stash hash `vp staged` reports for its backup output = YARN1_STEP_EMOJI_RE.replace_all(&output, "${1} ").into_owned(); output = YARN_TELEMETRY_RE.replace_all(&output, "").into_owned(); + output = YARN_FILE_HASH_RE.replace_all(&output, "${1}${2}").into_owned(); + output = YARN_LOCKFILE_CHECKSUM_RE.replace_all(&output, "${1}").into_owned(); output = STASH_HASH_RE.replace_all(&output, "${1}${2}").into_owned(); // Mask the local-registry proxy's ephemeral port, npm's timestamped debug diff --git a/crates/vp_cli_snapshots/tests/redact_unit.rs b/crates/vp_cli_snapshots/tests/redact_unit.rs index 2143a39bd0..33b59224d5 100644 --- a/crates/vp_cli_snapshots/tests/redact_unit.rs +++ b/crates/vp_cli_snapshots/tests/redact_unit.rs @@ -128,6 +128,23 @@ fn masks_size_numbers_keeping_units_and_spares_plain_stems() { ); } +#[test] +fn masks_yarn_file_hashes_and_lockfile_diff_checksums() { + let input = concat!( + "➤ YN0085: │ + dep@file:./dep#./dep::hash=8572a9&locator=app%40workspace%3A.\n", + "➤ YN0028: │ - checksum: 10c0/deadbeef\n", + "➤ YN0028: │ + version: 2.0.0\n", + "checksum: 10c0/deadbeef\n", + ); + let expected = concat!( + "➤ YN0085: │ + dep@file:./dep#./dep::hash=&locator=app%40workspace%3A.\n", + "➤ YN0028: │ - checksum: \n", + "➤ YN0028: │ + version: 2.0.0\n", + "checksum: 10c0/deadbeef\n", + ); + assert_eq!(redact_output(input.to_owned(), &[], true), expected); +} + #[test] fn drops_the_vite_build_banner_line() { // The banner races the Rust reporter's same-line erase writes, so its From 2c6d58fe84a35ff8484a130f14e42b7a20172ef0 Mon Sep 17 00:00:00 2001 From: JongKyung Lee Date: Wed, 16 Sep 2026 16:51:12 +0900 Subject: [PATCH 08/12] test(pm): isolate Yarn lockfile-only snapshot cache Disable the global cache for yarn_berry_lockfile_only so its fetch summary does not depend on packages cached by other cases on Windows. --- .../fixtures/install_package_options/snapshots.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots.toml index 535de02fb3..5cfad4d0c5 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots.toml +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots.toml @@ -64,7 +64,7 @@ steps = [ [[case]] name = "yarn_berry_lockfile_only" vp = "global" -env = { YARN_ENABLE_TELEMETRY = "0" } +env = { YARN_ENABLE_TELEMETRY = "0", YARN_ENABLE_GLOBAL_CACHE = "0" } steps = [ { argv = ["vpt", "json-edit", "package.json", "packageManager", "yarn@4.10.3"], snapshot = false }, { argv = ["vp", "install", "./dep", "--lockfile-only", "--ignore-scripts"], comment = "use update-lockfile rather than skip-build when both options are supplied" }, From 331731d7589d49f11e9bee8f7eb8e3f090ae9712 Mon Sep 17 00:00:00 2001 From: JongKyung Lee Date: Wed, 16 Sep 2026 17:34:03 +0900 Subject: [PATCH 09/12] docs(pm): clarify add silent mode in help Explain that silent mode suppresses Vite+ output and enables native silent mode where supported. Refresh the global and local help snapshots without changing runtime behavior. --- .../fixtures/cli_helper_message/snapshots/cli_helper_message.md | 2 +- .../fixtures/command_add_bun/snapshots/command_add_bun.md | 2 +- .../fixtures/command_add_npm10/snapshots/command_add_npm10.md | 2 +- .../fixtures/command_add_npm11/snapshots/command_add_npm11.md | 2 +- .../command_add_pnpm10/snapshots/command_add_pnpm10.global.md | 2 +- .../command_add_pnpm10/snapshots/command_add_pnpm10.local.md | 2 +- .../fixtures/command_add_pnpm11/snapshots/command_add_pnpm11.md | 2 +- .../fixtures/command_add_pnpm12/snapshots/command_add_pnpm12.md | 2 +- .../fixtures/command_add_pnpm9/snapshots/command_add_pnpm9.md | 2 +- .../fixtures/command_add_yarn4/snapshots/command_add_yarn4.md | 2 +- crates/vp_pm_cli/src/resolution/commands/add.rs | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/cli_helper_message/snapshots/cli_helper_message.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/cli_helper_message/snapshots/cli_helper_message.md index 332f564d54..aa0b6e0fe5 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/cli_helper_message/snapshots/cli_helper_message.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/cli_helper_message/snapshots/cli_helper_message.md @@ -169,7 +169,7 @@ Options: -f, --force Force reinstall all dependencies --no-lockfile Don't read or generate lockfile --shamefully-hoist Create flat node_modules (pnpm only) - --silent Suppress package manager output + --silent Suppress Vite+ output and enable native silent mode where supported --filter Filter packages in monorepo (can be used multiple times) -w, --workspace-root Add to workspace root --workspace Only add if package exists in workspace (pnpm-specific) diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_bun/snapshots/command_add_bun.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_bun/snapshots/command_add_bun.md index 8e910cb567..65acc370c9 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_bun/snapshots/command_add_bun.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_bun/snapshots/command_add_bun.md @@ -34,7 +34,7 @@ Options: -f, --force Force reinstall all dependencies --no-lockfile Don't read or generate lockfile --shamefully-hoist Create flat node_modules (pnpm only) - --silent Suppress package manager output + --silent Suppress Vite+ output and enable native silent mode where supported --filter Filter packages in monorepo (can be used multiple times) -w, --workspace-root Add to workspace root --workspace Only add if package exists in workspace (pnpm-specific) diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_npm10/snapshots/command_add_npm10.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_npm10/snapshots/command_add_npm10.md index 786bd04ea4..fa89053e35 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_npm10/snapshots/command_add_npm10.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_npm10/snapshots/command_add_npm10.md @@ -34,7 +34,7 @@ Options: -f, --force Force reinstall all dependencies --no-lockfile Don't read or generate lockfile --shamefully-hoist Create flat node_modules (pnpm only) - --silent Suppress package manager output + --silent Suppress Vite+ output and enable native silent mode where supported --filter Filter packages in monorepo (can be used multiple times) -w, --workspace-root Add to workspace root --workspace Only add if package exists in workspace (pnpm-specific) diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_npm11/snapshots/command_add_npm11.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_npm11/snapshots/command_add_npm11.md index 0ff40ccc97..6994f3ef11 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_npm11/snapshots/command_add_npm11.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_npm11/snapshots/command_add_npm11.md @@ -34,7 +34,7 @@ Options: -f, --force Force reinstall all dependencies --no-lockfile Don't read or generate lockfile --shamefully-hoist Create flat node_modules (pnpm only) - --silent Suppress package manager output + --silent Suppress Vite+ output and enable native silent mode where supported --filter Filter packages in monorepo (can be used multiple times) -w, --workspace-root Add to workspace root --workspace Only add if package exists in workspace (pnpm-specific) diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_pnpm10/snapshots/command_add_pnpm10.global.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_pnpm10/snapshots/command_add_pnpm10.global.md index f78ba8c3ae..1d8c2acc5d 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_pnpm10/snapshots/command_add_pnpm10.global.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_pnpm10/snapshots/command_add_pnpm10.global.md @@ -34,7 +34,7 @@ Options: -f, --force Force reinstall all dependencies --no-lockfile Don't read or generate lockfile --shamefully-hoist Create flat node_modules (pnpm only) - --silent Suppress package manager output + --silent Suppress Vite+ output and enable native silent mode where supported --filter Filter packages in monorepo (can be used multiple times) -w, --workspace-root Add to workspace root --workspace Only add if package exists in workspace (pnpm-specific) diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_pnpm10/snapshots/command_add_pnpm10.local.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_pnpm10/snapshots/command_add_pnpm10.local.md index 41f5440d40..2c76ae81bf 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_pnpm10/snapshots/command_add_pnpm10.local.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_pnpm10/snapshots/command_add_pnpm10.local.md @@ -51,7 +51,7 @@ Options: --shamefully-hoist Create flat node_modules (pnpm only) --silent - Suppress package manager output + Suppress Vite+ output and enable native silent mode where supported --filter Filter packages in monorepo (can be used multiple times) -w, --workspace-root diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_pnpm11/snapshots/command_add_pnpm11.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_pnpm11/snapshots/command_add_pnpm11.md index 98775e73f1..f605e1480e 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_pnpm11/snapshots/command_add_pnpm11.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_pnpm11/snapshots/command_add_pnpm11.md @@ -34,7 +34,7 @@ Options: -f, --force Force reinstall all dependencies --no-lockfile Don't read or generate lockfile --shamefully-hoist Create flat node_modules (pnpm only) - --silent Suppress package manager output + --silent Suppress Vite+ output and enable native silent mode where supported --filter Filter packages in monorepo (can be used multiple times) -w, --workspace-root Add to workspace root --workspace Only add if package exists in workspace (pnpm-specific) diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_pnpm12/snapshots/command_add_pnpm12.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_pnpm12/snapshots/command_add_pnpm12.md index 0f2fb08902..c1c4688e28 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_pnpm12/snapshots/command_add_pnpm12.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_pnpm12/snapshots/command_add_pnpm12.md @@ -34,7 +34,7 @@ Options: -f, --force Force reinstall all dependencies --no-lockfile Don't read or generate lockfile --shamefully-hoist Create flat node_modules (pnpm only) - --silent Suppress package manager output + --silent Suppress Vite+ output and enable native silent mode where supported --filter Filter packages in monorepo (can be used multiple times) -w, --workspace-root Add to workspace root --workspace Only add if package exists in workspace (pnpm-specific) diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_pnpm9/snapshots/command_add_pnpm9.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_pnpm9/snapshots/command_add_pnpm9.md index cf7206a661..597153f2f6 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_pnpm9/snapshots/command_add_pnpm9.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_pnpm9/snapshots/command_add_pnpm9.md @@ -34,7 +34,7 @@ Options: -f, --force Force reinstall all dependencies --no-lockfile Don't read or generate lockfile --shamefully-hoist Create flat node_modules (pnpm only) - --silent Suppress package manager output + --silent Suppress Vite+ output and enable native silent mode where supported --filter Filter packages in monorepo (can be used multiple times) -w, --workspace-root Add to workspace root --workspace Only add if package exists in workspace (pnpm-specific) diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_yarn4/snapshots/command_add_yarn4.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_yarn4/snapshots/command_add_yarn4.md index 6e4158fd7e..6e70fbed1c 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_yarn4/snapshots/command_add_yarn4.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_add_yarn4/snapshots/command_add_yarn4.md @@ -34,7 +34,7 @@ Options: -f, --force Force reinstall all dependencies --no-lockfile Don't read or generate lockfile --shamefully-hoist Create flat node_modules (pnpm only) - --silent Suppress package manager output + --silent Suppress Vite+ output and enable native silent mode where supported --filter Filter packages in monorepo (can be used multiple times) -w, --workspace-root Add to workspace root --workspace Only add if package exists in workspace (pnpm-specific) diff --git a/crates/vp_pm_cli/src/resolution/commands/add.rs b/crates/vp_pm_cli/src/resolution/commands/add.rs index eb00a11789..49ce1cc68c 100644 --- a/crates/vp_pm_cli/src/resolution/commands/add.rs +++ b/crates/vp_pm_cli/src/resolution/commands/add.rs @@ -77,7 +77,7 @@ pub struct AddArgs { #[arg(long, conflicts_with = "global", not_supported(npm, yarn, bun))] pub(crate) shamefully_hoist: bool, - /// Suppress package manager output + /// Suppress Vite+ output and enable native silent mode where supported #[arg(long, conflicts_with = "global", not_supported(yarn >= "2"))] pub(crate) silent: bool, From a2117c1cc4f371af351f76168244a018e39cb176 Mon Sep 17 00:00:00 2001 From: MK Date: Tue, 22 Sep 2026 00:22:06 +0800 Subject: [PATCH 10/12] test: stabilize install option and workspace pack snapshots --- .../.npmignore | 2 ++ .../snapshots.toml | 2 -- .../command_pack_pnpm10_with_workspace.md | 26 ++----------------- .../install_package_options/snapshots.toml | 7 +++-- .../snapshots/npm_offline.md | 9 +++++-- .../yarn_berry_frozen_lockfile_modes.md | 8 +++--- .../snapshots/yarn_berry_lockfile_only.md | 2 +- 7 files changed, 22 insertions(+), 34 deletions(-) create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_pack_pnpm10_with_workspace/.npmignore diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_pack_pnpm10_with_workspace/.npmignore b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_pack_pnpm10_with_workspace/.npmignore new file mode 100644 index 0000000000..ed9f1852c7 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_pack_pnpm10_with_workspace/.npmignore @@ -0,0 +1,2 @@ +# Recursive pack writes child archives here while the root package is scanned. +*.tgz diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_pack_pnpm10_with_workspace/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_pack_pnpm10_with_workspace/snapshots.toml index 9ccc612941..956c325511 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_pack_pnpm10_with_workspace/snapshots.toml +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_pack_pnpm10_with_workspace/snapshots.toml @@ -6,12 +6,10 @@ steps = [ { argv = ["vp", "pm", "pack"], comment = "should pack current workspace root" }, { argv = ["vpt", "rm", "-f", "command-pack-pnpm10-with-workspace-1.0.0.tgz", "app-1.0.0.tgz", "vite-plus-test-utils-1.0.0.tgz"], snapshot = false, continue-on-failure = true }, { argv = ["node", "-e", "const {execFileSync}=require('node:child_process');const out=JSON.parse(execFileSync('vp',['pm','pack','--recursive','--json'],{encoding:'utf8'}));out.sort((a,b)=>a.nameb.name?1:0);console.log(JSON.stringify(out,null,2));"], comment = "should pack all packages in workspace (sorted by name for determinism)" }, - ["vpt", "print-file", "out.json"], { argv = ["vpt", "rm", "-f", "command-pack-pnpm10-with-workspace-1.0.0.tgz", "app-1.0.0.tgz", "vite-plus-test-utils-1.0.0.tgz"], snapshot = false, continue-on-failure = true }, { argv = ["vp", "pm", "pack", "--filter", "app"], comment = "should pack specific package (uses --filter app pack)" }, { argv = ["vpt", "rm", "-f", "command-pack-pnpm10-with-workspace-1.0.0.tgz", "app-1.0.0.tgz", "vite-plus-test-utils-1.0.0.tgz"], snapshot = false, continue-on-failure = true }, { argv = ["node", "-e", "const {execFileSync}=require('node:child_process');const out=JSON.parse(execFileSync('vp',['pm','pack','--filter','app','--filter','@vite-plus-test/utils','--json'],{encoding:'utf8'}));out.sort((a,b)=>a.nameb.name?1:0);console.log(JSON.stringify(out,null,2));"], comment = "should pack multiple packages (sorted by name for determinism)" }, - ["vpt", "print-file", "out.json"], { argv = ["vpt", "rm", "-f", "command-pack-pnpm10-with-workspace-1.0.0.tgz", "app-1.0.0.tgz", "vite-plus-test-utils-1.0.0.tgz"], snapshot = false, continue-on-failure = true }, { argv = ["vp", "pm", "pack", "--out", "./dist/package.tgz"], comment = "should pack with output file" }, { argv = ["vpt", "rm", "-rf", "./dist"], continue-on-failure = true }, diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_pack_pnpm10_with_workspace/snapshots/command_pack_pnpm10_with_workspace.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_pack_pnpm10_with_workspace/snapshots/command_pack_pnpm10_with_workspace.md index 031ca7c789..9c616a583b 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_pack_pnpm10_with_workspace/snapshots/command_pack_pnpm10_with_workspace.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_pack_pnpm10_with_workspace/snapshots/command_pack_pnpm10_with_workspace.md @@ -66,16 +66,8 @@ should pack all packages in workspace (sorted by name for determinism) ] ``` -## `vpt print-file out.json` - -**Exit code:** 1 - -``` -out.json: not found -missing file -``` +## `vpt rm -f command-pack-pnpm10-with-workspace-1.0.0.tgz app-1.0.0.tgz vite-plus-test-utils-1.0.0.tgz` -*(skipped 1 step(s) to the next line boundary: step failed)* ## `vp pm pack --filter app` @@ -121,16 +113,8 @@ should pack multiple packages (sorted by name for determinism) ] ``` -## `vpt print-file out.json` - -**Exit code:** 1 - -``` -out.json: not found -missing file -``` +## `vpt rm -f command-pack-pnpm10-with-workspace-1.0.0.tgz app-1.0.0.tgz vite-plus-test-utils-1.0.0.tgz` -*(skipped 1 step(s) to the next line boundary: step failed)* ## `vp pm pack --out ./dist/package.tgz` @@ -139,12 +123,10 @@ should pack with output file ``` 📦 command-pack-pnpm10-with-workspace@1.0.0 Tarball Contents -app-1.0.0.tgz package.json packages/app/package.json packages/utils/package.json pnpm-workspace.yaml -vite-plus-test-utils-1.0.0.tgz Tarball Details /dist/package.tgz ``` @@ -161,12 +143,10 @@ should pack with destination ``` 📦 command-pack-pnpm10-with-workspace@1.0.0 Tarball Contents -app-1.0.0.tgz package.json packages/app/package.json packages/utils/package.json pnpm-workspace.yaml -vite-plus-test-utils-1.0.0.tgz Tarball Details /dist/command-pack-pnpm10-with-workspace-1.0.0.tgz ``` @@ -183,12 +163,10 @@ should pack with gzip compression level ``` 📦 command-pack-pnpm10-with-workspace@1.0.0 Tarball Contents -app-1.0.0.tgz package.json packages/app/package.json packages/utils/package.json pnpm-workspace.yaml -vite-plus-test-utils-1.0.0.tgz Tarball Details command-pack-pnpm10-with-workspace-1.0.0.tgz ``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots.toml index 5cfad4d0c5..90bf326bd4 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots.toml +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots.toml @@ -31,8 +31,11 @@ steps = [ [[case]] name = "npm_offline" vp = "global" +# Keep registry configuration out of PowerShell shim arguments and use a fresh cache on every platform. +env = { npm_config_fetch_retries = "0", npm_config_cache = ".npm-cache" } steps = [ - { argv = ["vp", "install", "vp-install-option-uncached-probe@1.0.0", "--offline", "--", "--registry=http://127.0.0.1:9", "--fetch-retries=0"], comment = "an uncached package fails in offline mode rather than attempting a connection", continue-on-failure = true }, + { argv = ["npm", "--version"], comment = "provision npm before pointing its registry at an unreachable endpoint", snapshot = false }, + { argv = ["vp", "install", "vp-install-option-uncached-probe@1.0.0", "--offline"], envs = [["npm_config_registry", "http://127.0.0.1:9"]], comment = "an uncached package fails in offline mode rather than attempting a connection", continue-on-failure = true }, { argv = ["vpt", "stat-file", "node_modules", "--assert", "missing"], continue-on-failure = true }, ["vpt", "stat-file", "package-lock.json", "--assert", "missing"], ] @@ -97,7 +100,7 @@ steps = [ [[case]] name = "yarn_berry_frozen_lockfile_modes" vp = "global" -env = { YARN_ENABLE_TELEMETRY = "0" } +env = { YARN_ENABLE_TELEMETRY = "0", YARN_ENABLE_GLOBAL_CACHE = "0" } steps = [ { argv = ["vpt", "json-edit", "package.json", "packageManager", "yarn@4.10.3"], snapshot = false }, { argv = ["vpt", "write-file", ".yarnrc.yml", "nodeLinker: node-modules\n"], snapshot = false }, diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/npm_offline.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/npm_offline.md index b5893e6aaf..640860d8c3 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/npm_offline.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/npm_offline.md @@ -1,6 +1,11 @@ # npm_offline -## `vp install vp-install-option-uncached-probe@1.0.0 --offline -- --registry=http://127.0.0.1:9 --fetch-retries=0` +## `npm --version` + +provision npm before pointing its registry at an unreachable endpoint + + +## `npm_config_registry=http://127.0.0.1:9 vp install vp-install-option-uncached-probe@1.0.0 --offline` an uncached package fails in offline mode rather than attempting a connection @@ -11,7 +16,7 @@ VITE+ - The Unified Toolchain for the Web npm error code ENOTCACHED npm error request to http://127.0.0.1:/vp-install-option-uncached-probe failed: cache mode is 'only-if-cached' but no cached response is available. -npm error A complete log of this run can be found in: /.npm/_logs/-debug-0.log +npm error A complete log of this run can be found in: /.npm-cache/_logs/-debug-0.log ``` ## `vpt stat-file node_modules --assert missing` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_berry_frozen_lockfile_modes.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_berry_frozen_lockfile_modes.md index 212f7019ee..fe0a43e8f1 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_berry_frozen_lockfile_modes.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_berry_frozen_lockfile_modes.md @@ -24,7 +24,7 @@ warn: yarn does not support --frozen-lockfile. ➤ YN0000: └ Completed ➤ YN0000: ┌ Link step ➤ YN0000: └ Completed -➤ YN0000: · Done in +➤ YN0000: · Done in ``` ## `vpt cp yarn.lock before.lock` @@ -97,10 +97,11 @@ VITE+ - The Unified Toolchain for the Web ➤ YN0085: │ - install-option-dep@file:./dep#./dep::hash=&locator=install-package-options%40workspace%3A. ➤ YN0000: └ Completed ➤ YN0000: ┌ Fetch step +➤ YN0013: │ A package was added to the project, and one was removed (+ KiB). ➤ YN0000: └ Completed ➤ YN0000: ┌ Link step ➤ YN0000: └ Completed -➤ YN0000: · Done in +➤ YN0000: · Done in ``` ## `node -p require('./node_modules/install-option-dep/package.json').version` @@ -121,8 +122,9 @@ warn: yarn does not support --no-frozen-lockfile. ➤ YN0085: │ - install-option-dep@file:./dep-v2#./dep-v2::hash=&locator=install-package-options%40workspace%3A. ➤ YN0000: └ Completed ➤ YN0000: ┌ Fetch step +➤ YN0013: │ A package was added to the project, and one was removed (- KiB). ➤ YN0000: └ Completed ➤ YN0000: ┌ Link step ➤ YN0000: └ Completed -➤ YN0000: · Done in +➤ YN0000: · Done in ``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_berry_lockfile_only.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_berry_lockfile_only.md index 36fec4e261..f1967c5d56 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_berry_lockfile_only.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_berry_lockfile_only.md @@ -21,7 +21,7 @@ warn: yarn@2+ --mode can only be specified once; --lockfile-only takes priority ➤ YN0000: ┌ Link step ➤ YN0073: │ Skipped due to mode=update-lockfile ➤ YN0000: └ Completed -➤ YN0000: · Done with warnings in +➤ YN0000: · Done with warnings in ``` ## `vpt stat-file yarn.lock --assert file` From 2e0f186d7f23ec9f80ab8b6aa6a4f80f098f0755 Mon Sep 17 00:00:00 2001 From: MK Date: Tue, 22 Sep 2026 00:34:52 +0800 Subject: [PATCH 11/12] refactor(pm): simplify install option handling and fixtures --- .../assert-lockfile-unchanged.mjs | 8 ++++ .../install_package_options/snapshots.toml | 6 +-- .../snapshots/bun_frozen_lockfile.md | 2 +- .../yarn_berry_frozen_lockfile_modes.md | 4 +- .../yarn_classic_frozen_lockfile_modes.md | 2 +- .../tests/cli_snapshots/redact.rs | 2 +- crates/vp_cli_snapshots/tests/redact_unit.rs | 2 + crates/vp_pm_cli/src/cli.rs | 2 +- .../vp_pm_cli/src/resolution/commands/add.rs | 16 ++----- .../src/resolution/commands/install.rs | 45 +++++++++++-------- 10 files changed, 49 insertions(+), 40 deletions(-) create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/assert-lockfile-unchanged.mjs diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/assert-lockfile-unchanged.mjs b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/assert-lockfile-unchanged.mjs new file mode 100644 index 0000000000..674d7aef8c --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/assert-lockfile-unchanged.mjs @@ -0,0 +1,8 @@ +import { readFileSync } from 'node:fs'; + +const lockfile = readFileSync(process.argv[2]); +const original = readFileSync('before.lock'); +if (!lockfile.equals(original)) { + process.exit(1); +} +console.log('lockfile unchanged'); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots.toml index 90bf326bd4..b2ab13efc0 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots.toml +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots.toml @@ -49,7 +49,7 @@ steps = [ ["vpt", "stat-file", "node_modules", "--assert", "missing"], { argv = ["vpt", "cp", "bun.lock", "before.lock"], snapshot = false }, { argv = ["vp", "install", "./dep-v2", "--frozen-lockfile"], comment = "a named-package install must not rewrite a frozen lockfile", continue-on-failure = true }, - { argv = ["node", "-e", "const fs = require('node:fs'); if (!fs.readFileSync('bun.lock').equals(fs.readFileSync('before.lock'))) process.exit(1); console.log('lockfile unchanged');"], continue-on-failure = true }, + { argv = ["node", "assert-lockfile-unchanged.mjs", "bun.lock"], continue-on-failure = true }, ["vpt", "stat-file", "node_modules", "--assert", "missing"], ["vpt", "print-file", "package.json"], ] @@ -90,7 +90,7 @@ steps = [ { argv = ["vp", "install", "--frozen-lockfile"], comment = "package-free frozen installs succeed when the manifest and lockfile agree" }, { argv = ["vpt", "json-edit", "package.json", "dependencies.install-option-dep", "file:./dep-v2"], snapshot = false }, { argv = ["vp", "install", "--frozen-lockfile"], comment = "package-free installs still enforce the frozen lockfile", continue-on-failure = true }, - { argv = ["node", "-e", "const fs = require('node:fs'); if (!fs.readFileSync('yarn.lock').equals(fs.readFileSync('before.lock'))) process.exit(1); console.log('lockfile unchanged');"], continue-on-failure = true }, + { argv = ["node", "assert-lockfile-unchanged.mjs", "yarn.lock"], continue-on-failure = true }, { argv = ["vp", "install", "--no-frozen-lockfile"], comment = "package-free installs can explicitly allow lockfile changes" }, ["node", "-p", "require('./node_modules/install-option-dep/package.json').version"], { argv = ["vp", "add", "./dep", "--no-frozen-lockfile"], comment = "Classic add warns and drops the unsupported negated flag" }, @@ -108,7 +108,7 @@ steps = [ { argv = ["vpt", "cp", "yarn.lock", "before.lock"], snapshot = false }, { argv = ["vpt", "json-edit", "package.json", "dependencies.install-option-dep", "file:./dep-v2"], snapshot = false }, { argv = ["vp", "install", "--frozen-lockfile"], comment = "package-free installs map frozen-lockfile to immutable", continue-on-failure = true }, - { argv = ["node", "-e", "const fs = require('node:fs'); if (!fs.readFileSync('yarn.lock').equals(fs.readFileSync('before.lock'))) process.exit(1); console.log('lockfile unchanged');"], continue-on-failure = true }, + { argv = ["node", "assert-lockfile-unchanged.mjs", "yarn.lock"], continue-on-failure = true }, { argv = ["vp", "install", "--no-frozen-lockfile"], comment = "package-free installs map no-frozen-lockfile to no-immutable" }, ["node", "-p", "require('./node_modules/install-option-dep/package.json').version"], { argv = ["vp", "add", "./dep", "--no-frozen-lockfile"], comment = "direct add also warns for the unsupported immutable override" }, diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/bun_frozen_lockfile.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/bun_frozen_lockfile.md index 9478c22b12..ecbaca255d 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/bun_frozen_lockfile.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/bun_frozen_lockfile.md @@ -36,7 +36,7 @@ error: lockfile had changes, but lockfile is frozen note: try re-running without --frozen-lockfile and commit the updated lockfile ``` -## `node -e 'const fs = require('\''node:fs'\''); if ('\!'fs.readFileSync('\''bun.lock'\'').equals(fs.readFileSync('\''before.lock'\''))) process.exit(1); console.log('\''lockfile unchanged'\'');'` +## `node assert-lockfile-unchanged.mjs bun.lock` ``` lockfile unchanged diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_berry_frozen_lockfile_modes.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_berry_frozen_lockfile_modes.md index fe0a43e8f1..592297b179 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_berry_frozen_lockfile_modes.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_berry_frozen_lockfile_modes.md @@ -75,10 +75,10 @@ VITE+ - The Unified Toolchain for the Web ➤ YN0000: │ ➤ YN0028: │ The lockfile would have been modified by this install, which is explicitly forbidden. ➤ YN0000: └ Completed -➤ YN0000: · Failed with errors in +➤ YN0000: · Failed with errors in ``` -## `node -e 'const fs = require('\''node:fs'\''); if ('\!'fs.readFileSync('\''yarn.lock'\'').equals(fs.readFileSync('\''before.lock'\''))) process.exit(1); console.log('\''lockfile unchanged'\'');'` +## `node assert-lockfile-unchanged.mjs yarn.lock` ``` lockfile unchanged diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_classic_frozen_lockfile_modes.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_classic_frozen_lockfile_modes.md index bb1fa1a3e6..af1751833b 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_classic_frozen_lockfile_modes.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_classic_frozen_lockfile_modes.md @@ -98,7 +98,7 @@ error Your lockfile needs to be updated, but yarn was run with `--frozen-lockfil info Visit https://yarnpkg.com/en/docs/cli/install for documentation about this command. ``` -## `node -e 'const fs = require('\''node:fs'\''); if ('\!'fs.readFileSync('\''yarn.lock'\'').equals(fs.readFileSync('\''before.lock'\''))) process.exit(1); console.log('\''lockfile unchanged'\'');'` +## `node assert-lockfile-unchanged.mjs yarn.lock` ``` lockfile unchanged diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/redact.rs b/crates/vp_cli_snapshots/tests/cli_snapshots/redact.rs index 1785b92cf0..b1e42ccc25 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/redact.rs +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/redact.rs @@ -18,7 +18,7 @@ static DURATION_RE: LazyLock = // Match the entire elapsed field, not adjacent values in timing tables. static YARN_ELAPSED_RE: LazyLock = LazyLock::new(|| { regex::Regex::new( - r"(\b(?:Done(?: with (?:warnings|errors))?|[Cc]ompleted) in )\d+(?:\.\d+)?(?:ms|s|m|h)\b(?:[ \t]+\d+(?:\.\d+)?(?:ms|s|m|h)\b)*", + r"(\b(?:(?:Done|Failed)(?: with (?:warnings|errors))?|[Cc]ompleted) in )\d+(?:\.\d+)?(?:ms|s|m|h)\b(?:[ \t]+\d+(?:\.\d+)?(?:ms|s|m|h)\b)*", ) .unwrap() }); diff --git a/crates/vp_cli_snapshots/tests/redact_unit.rs b/crates/vp_cli_snapshots/tests/redact_unit.rs index 33b59224d5..155118417f 100644 --- a/crates/vp_cli_snapshots/tests/redact_unit.rs +++ b/crates/vp_cli_snapshots/tests/redact_unit.rs @@ -18,6 +18,7 @@ fn masks_yarn_compound_elapsed_times_as_one_duration() { "➤ YN0000: Done in {elapsed}\n\ ➤ YN0000: · Done with warnings in {elapsed}\n\ ➤ YN0000: · Done with errors in {elapsed}\n\ + ➤ YN0000: · Failed with errors in {elapsed}\n\ [app]: Process exited (exit code 0), completed in {elapsed}\n" ); assert_eq!( @@ -25,6 +26,7 @@ fn masks_yarn_compound_elapsed_times_as_one_duration() { "➤ YN0000: Done in \n\ ➤ YN0000: · Done with warnings in \n\ ➤ YN0000: · Done with errors in \n\ + ➤ YN0000: · Failed with errors in \n\ [app]: Process exited (exit code 0), completed in \n" ); } diff --git a/crates/vp_pm_cli/src/cli.rs b/crates/vp_pm_cli/src/cli.rs index 3196434d32..6078733c27 100644 --- a/crates/vp_pm_cli/src/cli.rs +++ b/crates/vp_pm_cli/src/cli.rs @@ -207,7 +207,7 @@ impl PackageManagerCommand { /// only command whose typed clap shape selects between two resolvers. pub(crate) fn resolve_for_manager(self, manager: &PackageManager) -> Result { match self { - Self::Install(args) => args.resolve_args_for_manager(manager), + Self::Install(args) => args.resolve_for_manager(manager), Self::Add(args) => resolve_args_for_manager(manager, args), Self::Remove(args) => resolve_args_for_manager(manager, args), Self::Update(args) => resolve_args_for_manager(manager, args), diff --git a/crates/vp_pm_cli/src/resolution/commands/add.rs b/crates/vp_pm_cli/src/resolution/commands/add.rs index 49ce1cc68c..fcc580893f 100644 --- a/crates/vp_pm_cli/src/resolution/commands/add.rs +++ b/crates/vp_pm_cli/src/resolution/commands/add.rs @@ -2,7 +2,7 @@ use vp_pm_cli_macros::pm_args; use super::parse_positive_usize; use crate::resolution::{ - Bun, CommandBuilder, CommandResolution, DiagnosticKind, Diagnostics, Npm, Pnpm, Resolve, Yarn, + Bun, CommandBuilder, CommandResolution, Diagnostics, Npm, Pnpm, Resolve, Yarn, }; #[pm_args] @@ -300,17 +300,7 @@ impl Resolve for Yarn { } cmd.arg_if("--exact", args.save_exact); if self.is_berry() { - if args.lockfile_only { - cmd.arg("--mode").arg("update-lockfile"); - if args.ignore_scripts { - diag.warn( - DiagnosticKind::BehaviorChange, - "yarn@2+ --mode can only be specified once; --lockfile-only takes priority over --ignore-scripts", - ); - } - } else if args.ignore_scripts { - cmd.arg("--mode").arg("skip-build"); - } + Self::apply_berry_install_mode(&mut cmd, args.lockfile_only, args.ignore_scripts, diag); } else { cmd.arg_if("--ignore-scripts", args.ignore_scripts) .arg_if("--ignore-optional", args.no_optional) @@ -367,7 +357,7 @@ impl Resolve for Bun { mod tests { use super::*; use crate::resolution::{ - resolve, + DiagnosticKind, resolve, test_utils::{bun, expect_run, npm, parse_args, pnpm, yarn}, }; diff --git a/crates/vp_pm_cli/src/resolution/commands/install.rs b/crates/vp_pm_cli/src/resolution/commands/install.rs index 01012a0945..afa5b93f40 100644 --- a/crates/vp_pm_cli/src/resolution/commands/install.rs +++ b/crates/vp_pm_cli/src/resolution/commands/install.rs @@ -146,25 +146,25 @@ impl Resolve for Pnpm { } impl InstallArgs { - pub(crate) fn resolve_args_for_manager( + pub(crate) fn resolve_for_manager( mut self, manager: &PackageManager, ) -> Result { let adding_packages = !self.packages.is_empty(); // Diagnose the selected mode before conversion discards fields, and before // manager-specific support rules can produce misleading or duplicate warnings. - let (mode, unsupported) = if adding_packages { + let (mode, unsupported): (&str, &[(&str, bool)]) = if adding_packages { ( "with package names", - vec![ - ("--fix-lockfile", std::mem::take(&mut self.fix_lockfile)), - ("--resolution-only", std::mem::take(&mut self.resolution_only)), + &[ + ("--fix-lockfile", self.fix_lockfile), + ("--resolution-only", self.resolution_only), ], ) } else { ( "without package names", - vec![ + &[ ("--save-exact", std::mem::take(&mut self.save_exact)), ("--save-peer", std::mem::take(&mut self.save_peer)), ("--save-optional", std::mem::take(&mut self.save_optional)), @@ -177,7 +177,7 @@ impl InstallArgs { } else { resolve_for_manager(manager, self)? }; - for (option, supplied) in unsupported { + for &(option, supplied) in unsupported { if supplied { resolution.diagnostics.warn( DiagnosticKind::UnsupportedOptionDropped, @@ -299,17 +299,7 @@ impl Yarn { } else { cmd.arg_if("--immutable", args.frozen_lockfile); } - if args.lockfile_only { - cmd.arg("--mode").arg("update-lockfile"); - if args.ignore_scripts { - diag.warn( - DiagnosticKind::BehaviorChange, - "yarn@2+ --mode can only be specified once; --lockfile-only takes priority over --ignore-scripts", - ); - } - } else if args.ignore_scripts { - cmd.arg("--mode").arg("skip-build"); - } + Self::apply_berry_install_mode(&mut cmd, args.lockfile_only, args.ignore_scripts, diag); if args.prod { diag.warn( DiagnosticKind::BehaviorChange, @@ -319,6 +309,25 @@ impl Yarn { cmd.arg_if("--refresh-lockfile", args.fix_lockfile).extend(args.pass_through_args.iter()); cmd.into() } + + pub(super) fn apply_berry_install_mode( + cmd: &mut CommandBuilder, + lockfile_only: bool, + ignore_scripts: bool, + diag: &mut Diagnostics, + ) { + if lockfile_only { + cmd.arg("--mode").arg("update-lockfile"); + if ignore_scripts { + diag.warn( + DiagnosticKind::BehaviorChange, + "yarn@2+ --mode can only be specified once; --lockfile-only takes priority over --ignore-scripts", + ); + } + } else if ignore_scripts { + cmd.arg("--mode").arg("skip-build"); + } + } } impl Resolve for Bun { From 21edaabd3f75e01066e1e906291630fe8d3db103 Mon Sep 17 00:00:00 2001 From: MK Date: Tue, 22 Sep 2026 00:45:31 +0800 Subject: [PATCH 12/12] fix(pm): require Yarn 3 for lockfile-only add --- .../install_package_options/snapshots.toml | 32 +++++++++ .../snapshots/yarn_2_drops_lockfile_only.md | 71 +++++++++++++++++++ .../snapshots/yarn_3_lockfile_only.md | 71 +++++++++++++++++++ crates/vp_pm_cli/src/cli.rs | 42 +++++++++++ .../vp_pm_cli/src/resolution/commands/add.rs | 2 +- 5 files changed, 217 insertions(+), 1 deletion(-) create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_2_drops_lockfile_only.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_3_lockfile_only.md diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots.toml index b2ab13efc0..a6757f3e67 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots.toml +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots.toml @@ -64,6 +64,38 @@ steps = [ ["vpt", "stat-file", "node_modules", "--assert", "missing"], ] +[[case]] +name = "yarn_2_drops_lockfile_only" +vp = "global" +env = { YARN_ENABLE_TELEMETRY = "0", YARN_ENABLE_GLOBAL_CACHE = "0", YARN_NODE_LINKER = "node-modules" } +steps = [ + { argv = ["vpt", "write-file", ".node-version", "22.18.0\n"], snapshot = false }, + { argv = ["vpt", "json-edit", "package.json", "packageManager", "yarn@2.4.2"], snapshot = false }, + { argv = ["vp", "add", "./dep", "--lockfile-only"], comment = "Yarn 2 warns and installs normally because add does not support --mode" }, + ["vpt", "stat-file", "yarn.lock", "--assert", "file"], + ["node", "-p", "require('./node_modules/install-option-dep/package.json').version"], + { argv = ["vpt", "rm", "-rf", "node_modules", "yarn.lock", ".yarn"], snapshot = false }, + { argv = ["vp", "install", "./dep-v2", "--lockfile-only"], comment = "positional install uses the same warning-and-drop policy" }, + ["vpt", "stat-file", "yarn.lock", "--assert", "file"], + ["node", "-p", "require('./node_modules/install-option-dep/package.json').version"], +] + +[[case]] +name = "yarn_3_lockfile_only" +vp = "global" +env = { YARN_ENABLE_TELEMETRY = "0", YARN_ENABLE_GLOBAL_CACHE = "0", YARN_NODE_LINKER = "node-modules" } +steps = [ + { argv = ["vpt", "write-file", ".node-version", "22.18.0\n"], snapshot = false }, + { argv = ["vpt", "json-edit", "package.json", "packageManager", "yarn@3.0.0"], snapshot = false }, + { argv = ["vp", "add", "./dep", "--lockfile-only"], comment = "Yarn 3 supports update-lockfile without installing packages" }, + ["vpt", "stat-file", "yarn.lock", "--assert", "file"], + ["vpt", "stat-file", "node_modules", "--assert", "missing"], + { argv = ["vpt", "rm", "-rf", "yarn.lock", ".yarn"], snapshot = false }, + { argv = ["vp", "install", "./dep-v2", "--lockfile-only"], comment = "positional install preserves lockfile-only at the first supported Yarn release" }, + ["vpt", "stat-file", "yarn.lock", "--assert", "file"], + ["vpt", "stat-file", "node_modules", "--assert", "missing"], +] + [[case]] name = "yarn_berry_lockfile_only" vp = "global" diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_2_drops_lockfile_only.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_2_drops_lockfile_only.md new file mode 100644 index 0000000000..e6b54b1665 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_2_drops_lockfile_only.md @@ -0,0 +1,71 @@ +# yarn_2_drops_lockfile_only + +## `vpt write-file .node-version '22.18.0 +'` + + +## `vpt json-edit package.json packageManager yarn@2.4.2` + + +## `vp add ./dep --lockfile-only` + +Yarn 2 warns and installs normally because add does not support --mode + +``` +warn: yarn <3 does not support --lockfile-only. +➤ YN0000: ┌ Resolution step +➤ YN0013: │ install-option-dep@file:./dep#./dep::hash=&locator=install-package-options%40workspace%3A. can't be found in the cache and will be fetched from the disk +➤ YN0000: └ Completed +➤ YN0000: ┌ Fetch step +➤ YN0013: │ install-option-dep@file:./dep#./dep::hash=&locator=install-package-options%40workspace%3A. can't be found in the cache and will be fetched from the disk +➤ YN0000: └ Completed +➤ YN0000: ┌ Link step +➤ YN0000: └ Completed +➤ YN0000: Done in +``` + +## `vpt stat-file yarn.lock --assert file` + +``` +yarn.lock: file +``` + +## `node -p require('./node_modules/install-option-dep/package.json').version` + +``` +1.0.0 +``` + +## `vpt rm -rf node_modules yarn.lock .yarn` + + +## `vp install ./dep-v2 --lockfile-only` + +positional install uses the same warning-and-drop policy + +``` +VITE+ - The Unified Toolchain for the Web + +warn: yarn <3 does not support --lockfile-only. +➤ YN0000: ┌ Resolution step +➤ YN0013: │ install-option-dep@file:./dep-v2#./dep-v2::hash=&locator=install-package-options%40workspace%3A. can't be found in the cache and will be fetched from the disk +➤ YN0000: └ Completed +➤ YN0000: ┌ Fetch step +➤ YN0013: │ install-option-dep@file:./dep-v2#./dep-v2::hash=&locator=install-package-options%40workspace%3A. can't be found in the cache and will be fetched from the disk +➤ YN0000: └ Completed +➤ YN0000: ┌ Link step +➤ YN0000: └ Completed +➤ YN0000: Done in +``` + +## `vpt stat-file yarn.lock --assert file` + +``` +yarn.lock: file +``` + +## `node -p require('./node_modules/install-option-dep/package.json').version` + +``` +2.0.0 +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_3_lockfile_only.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_3_lockfile_only.md new file mode 100644 index 0000000000..f2e702edee --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_3_lockfile_only.md @@ -0,0 +1,71 @@ +# yarn_3_lockfile_only + +## `vpt write-file .node-version '22.18.0 +'` + + +## `vpt json-edit package.json packageManager yarn@3.0.0` + + +## `vp add ./dep --lockfile-only` + +Yarn 3 supports update-lockfile without installing packages + +``` +➤ YN0000: ┌ Resolution step +➤ YN0013: │ install-option-dep@file:./dep#./dep::hash=&locator=install-package-options%40workspace%3A. can't be found in the cache and will be fetched from the disk +➤ YN0000: └ Completed +➤ YN0000: ┌ Fetch step +➤ YN0013: │ install-option-dep@file:./dep#./dep::hash=&locator=install-package-options%40workspace%3A. can't be found in the cache and will be fetched from the disk +➤ YN0000: └ Completed +➤ YN0000: ┌ Link step +➤ YN0073: │ Skipped due to mode=update-lockfile +➤ YN0000: └ Completed +➤ YN0000: Done with warnings in +``` + +## `vpt stat-file yarn.lock --assert file` + +``` +yarn.lock: file +``` + +## `vpt stat-file node_modules --assert missing` + +``` +node_modules: missing +``` + +## `vpt rm -rf yarn.lock .yarn` + + +## `vp install ./dep-v2 --lockfile-only` + +positional install preserves lockfile-only at the first supported Yarn release + +``` +VITE+ - The Unified Toolchain for the Web + +➤ YN0000: ┌ Resolution step +➤ YN0013: │ install-option-dep@file:./dep-v2#./dep-v2::hash=&locator=install-package-options%40workspace%3A. can't be found in the cache and will be fetched from the disk +➤ YN0000: └ Completed +➤ YN0000: ┌ Fetch step +➤ YN0013: │ install-option-dep@file:./dep-v2#./dep-v2::hash=&locator=install-package-options%40workspace%3A. can't be found in the cache and will be fetched from the disk +➤ YN0000: └ Completed +➤ YN0000: ┌ Link step +➤ YN0073: │ Skipped due to mode=update-lockfile +➤ YN0000: └ Completed +➤ YN0000: Done with warnings in +``` + +## `vpt stat-file yarn.lock --assert file` + +``` +yarn.lock: file +``` + +## `vpt stat-file node_modules --assert missing` + +``` +node_modules: missing +``` diff --git a/crates/vp_pm_cli/src/cli.rs b/crates/vp_pm_cli/src/cli.rs index 6078733c27..37d0a3ca02 100644 --- a/crates/vp_pm_cli/src/cli.rs +++ b/crates/vp_pm_cli/src/cli.rs @@ -512,6 +512,48 @@ mod tests { assert_eq!(add.args, vec!["add", "--save-dev", "react"]); } + #[test] + fn yarn_before_3_drops_lockfile_only_when_adding_packages() { + for version in ["2.4.2", "2.0.0", "1.22.22"] { + let manager = package_manager(PackageManagerType::Yarn, version); + for command in ["add", "install", "i"] { + let resolution = parse(&[command, "./dep", "--lockfile-only"]) + .unwrap() + .resolve_for_manager(&manager) + .unwrap(); + let resolved = crate::resolution::test_utils::expect_run(resolution.outcome); + + assert_eq!(resolved.args, ["add", "./dep"], "yarn@{version}: {command}"); + assert_eq!(resolution.diagnostics.len(), 1); + assert_eq!( + resolution.diagnostics[0].message, + "yarn <3 does not support --lockfile-only." + ); + } + } + } + + #[test] + fn yarn_from_3_preserves_lockfile_only_when_adding_packages() { + for version in ["3.0.0", "3.1.0", "4.10.3"] { + let manager = package_manager(PackageManagerType::Yarn, version); + for command in ["add", "install", "i"] { + let resolution = parse(&[command, "./dep", "--lockfile-only"]) + .unwrap() + .resolve_for_manager(&manager) + .unwrap(); + let resolved = crate::resolution::test_utils::expect_run(resolution.outcome); + + assert_eq!( + resolved.args, + ["add", "--mode", "update-lockfile", "./dep"], + "yarn@{version}: {command}" + ); + assert!(resolution.diagnostics.is_empty(), "yarn@{version}: {command}"); + } + } + } + #[test] fn install_and_add_preserve_ignore_scripts() { for (client, version, add_command, flags) in [ diff --git a/crates/vp_pm_cli/src/resolution/commands/add.rs b/crates/vp_pm_cli/src/resolution/commands/add.rs index fcc580893f..5c15e00757 100644 --- a/crates/vp_pm_cli/src/resolution/commands/add.rs +++ b/crates/vp_pm_cli/src/resolution/commands/add.rs @@ -54,7 +54,7 @@ pub struct AddArgs { pub(crate) no_frozen_lockfile: bool, /// Only update lockfile, don't install - #[arg(long, conflicts_with = "global", not_supported(yarn < "2"))] + #[arg(long, conflicts_with = "global", not_supported(yarn < "3"))] pub(crate) lockfile_only: bool, /// Use cached packages when available