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..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 @@ -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 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 375354450f..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 @@ -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 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 aec68aaa9d..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 @@ -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 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 cc730352ab..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 @@ -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 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 a45d66a732..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 @@ -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 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 f61cd548fa..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 @@ -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 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 e4d2b58556..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 @@ -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 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 4911bc941d..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 @@ -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 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 ae46ba2654..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 @@ -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 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 5a6b3afdcf..a0f813bc34 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 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_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_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..59b804c5da --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/dep-v2/package.json @@ -0,0 +1,5 @@ +{ + "name": "install-option-dep", + "version": "2.0.0", + "description": "Updated dependency fixture" +} 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..5cfad4d0c5 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots.toml @@ -0,0 +1,112 @@ +[[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", 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" }, + ["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" +# 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" }, + ["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 }, + ["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" }, + ["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..212f7019ee --- /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 does not support --frozen-lockfile. +➤ YN0000: · Yarn +➤ YN0000: ┌ Resolution step +➤ 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). +➤ 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=&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 +➤ 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=&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=&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=&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 +➤ 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=&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 +➤ 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..36fec4e261 --- /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=&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..bb1fa1a3e6 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/install_package_options/snapshots/yarn_classic_frozen_lockfile_modes.md @@ -0,0 +1,157 @@ +# yarn_classic_frozen_lockfile_modes + +## `vpt json-edit package.json packageManager yarn@1.22.22` + + +## `vp install ./dep --frozen-lockfile` + +named-package installs warn and drop the unsupported add option + +``` +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... + +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 . +``` + +## `vpt stat-file yarn.lock --assert file` + +``` +yarn.lock: file +``` + +## `vp add ./dep --frozen-lockfile` + +direct add also warns and continues without the option + +``` +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... + +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 . +``` + +## `vpt cp yarn.lock before.lock` + + +## `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 + +``` +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... + +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 + +**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_cli_snapshots/tests/cli_snapshots/redact.rs b/crates/vp_cli_snapshots/tests/cli_snapshots/redact.rs index 5cc9576271..17f9609e49 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/redact.rs +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/redact.rs @@ -276,6 +276,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 = @@ -591,6 +597,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 71a80ff3ad..9d6b8b212a 100644 --- a/crates/vp_cli_snapshots/tests/redact_unit.rs +++ b/crates/vp_cli_snapshots/tests/redact_unit.rs @@ -47,6 +47,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 diff --git a/crates/vp_pm_cli/src/cli.rs b/crates/vp_pm_cli/src/cli.rs index ffa93bb3c8..3196434d32 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), @@ -283,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)) @@ -301,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, } @@ -667,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"][..], @@ -689,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] @@ -760,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"][..], @@ -768,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:?}"); } } diff --git a/crates/vp_pm_cli/src/resolution/commands/add.rs b/crates/vp_pm_cli/src/resolution/commands/add.rs index 937f477031..49ce1cc68c 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,56 @@ 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 + #[arg( + long, + conflicts_with = "global", + overrides_with = "no_frozen_lockfile", + not_supported(npm, pnpm, yarn) + )] + 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 Vite+ output and enable native silent mode where supported + #[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 +201,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 +247,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 +268,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 +299,26 @@ 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("--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 +347,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 +654,69 @@ mod tests { assert_eq!(command.args, vec!["add", "--allow-build=react,napi", "react"]); } + #[test] + 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!( + resolution.diagnostics[0].message, + vt_str::format!("yarn does not support {flag}.").as_str(), + ); + } + } + } + + #[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);