diff --git a/packages/cli/src/create/bin.ts b/packages/cli/src/create/bin.ts index a1152fcc08..5d55b9dc43 100644 --- a/packages/cli/src/create/bin.ts +++ b/packages/cli/src/create/bin.ts @@ -917,6 +917,8 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h updateCreateProgress('Installing dependencies'); const installSummary = await runViteInstall(fullPath, options.interactive, installArgs, { silent: compactOutput, + packageManager: workspaceInfo.packageManager, + packageManagerVersion: workspaceInfo.downloadPackageManager.version, }); updateCreateProgress('Formatting code'); await runViteFmt(fullPath, options.interactive, undefined, { silent: compactOutput }); @@ -1038,6 +1040,8 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h updateCreateProgress('Installing dependencies'); installSummary = await runViteInstall(installCwd, options.interactive, installArgs, { silent: compactOutput, + packageManager: workspaceInfo.packageManager, + packageManagerVersion: workspaceInfo.downloadPackageManager.version, }); if (installSummary.status !== 'installed') { return; @@ -1126,6 +1130,8 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h updateCreateProgress('Installing dependencies'); installSummary = await runViteInstall(workspaceInfo.rootDir, options.interactive, installArgs, { silent: compactOutput, + packageManager: workspaceInfo.packageManager, + packageManagerVersion: workspaceInfo.downloadPackageManager.version, }); updateCreateProgress('Formatting code'); await runViteFmt(workspaceInfo.rootDir, options.interactive, [projectDir], { @@ -1148,6 +1154,8 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h updateCreateProgress('Installing dependencies'); installSummary = await runViteInstall(fullPath, options.interactive, installArgs, { silent: compactOutput, + packageManager: workspaceInfo.packageManager, + packageManagerVersion: workspaceInfo.downloadPackageManager.version, }); updateCreateProgress('Formatting code'); await runViteFmt(fullPath, options.interactive, undefined, { silent: compactOutput }); diff --git a/packages/cli/src/migration/bin.ts b/packages/cli/src/migration/bin.ts index e8a13ba432..925519421a 100644 --- a/packages/cli/src/migration/bin.ts +++ b/packages/cli/src/migration/bin.ts @@ -649,6 +649,8 @@ async function executeMigrationPlan( undefined, { silent: true, + packageManager: workspaceInfo.packageManager, + packageManagerVersion: workspaceInfo.downloadPackageManager.version, }, ); @@ -778,7 +780,11 @@ async function executeMigrationPlan( workspaceInfo.rootDir, interactive, installArgs, - { silent: true }, + { + silent: true, + packageManager: workspaceInfo.packageManager, + packageManagerVersion: workspaceInfo.downloadPackageManager.version, + }, ); clearMigrationProgress(); @@ -868,12 +874,30 @@ async function main() { updateMigrationProgress('Rewriting configs'); mergeViteConfigFiles(workspaceInfoOptional.rootDir, true, report); updateMigrationProgress('Installing dependencies'); + // Resolve the actual pnpm version that `vp install` will use so the + // auto-install can opt into `--ignore-scripts` on pnpm v11 (which fails + // unapproved build scripts with `ERR_PNPM_IGNORED_BUILDS`). + let resolvedVersion = workspaceInfoOptional.packageManagerVersion; + if ( + workspaceInfoOptional.packageManager && + !semver.valid(semver.coerce(resolvedVersion) ?? '') + ) { + const resolved = await downloadPackageManager( + workspaceInfoOptional.packageManager, + resolvedVersion, + options.interactive, + true, + ); + resolvedVersion = resolved.version; + } const installSummary = await runViteInstall( workspaceInfoOptional.rootDir, options.interactive, undefined, { silent: true, + packageManager: workspaceInfoOptional.packageManager, + packageManagerVersion: resolvedVersion, }, ); installDurationMs += installSummary.durationMs; diff --git a/packages/cli/src/utils/__tests__/prompts.spec.ts b/packages/cli/src/utils/__tests__/prompts.spec.ts new file mode 100644 index 0000000000..1698806174 --- /dev/null +++ b/packages/cli/src/utils/__tests__/prompts.spec.ts @@ -0,0 +1,39 @@ +import { describe, expect, it } from 'vitest'; + +import { PackageManager } from '../../types/index.ts'; +import { shouldIgnoreScriptsForAutoInstall } from '../prompts.ts'; + +describe('shouldIgnoreScriptsForAutoInstall', () => { + it('returns true for pnpm >= 11.0.0', () => { + expect(shouldIgnoreScriptsForAutoInstall(PackageManager.pnpm, '11.0.0')).toBe(true); + expect(shouldIgnoreScriptsForAutoInstall(PackageManager.pnpm, '11.0.8')).toBe(true); + expect(shouldIgnoreScriptsForAutoInstall(PackageManager.pnpm, '12.0.0')).toBe(true); + }); + + it('returns false for pnpm < 11.0.0', () => { + expect(shouldIgnoreScriptsForAutoInstall(PackageManager.pnpm, '10.33.2')).toBe(false); + expect(shouldIgnoreScriptsForAutoInstall(PackageManager.pnpm, '9.5.0')).toBe(false); + }); + + it('returns false for non-pnpm package managers', () => { + expect(shouldIgnoreScriptsForAutoInstall(PackageManager.npm, '11.0.0')).toBe(false); + expect(shouldIgnoreScriptsForAutoInstall(PackageManager.yarn, '11.0.0')).toBe(false); + expect(shouldIgnoreScriptsForAutoInstall(PackageManager.bun, '11.0.0')).toBe(false); + }); + + it('returns false when version is unknown or unparsable', () => { + expect(shouldIgnoreScriptsForAutoInstall(PackageManager.pnpm, undefined)).toBe(false); + expect(shouldIgnoreScriptsForAutoInstall(PackageManager.pnpm, 'latest')).toBe(false); + expect(shouldIgnoreScriptsForAutoInstall(PackageManager.pnpm, '')).toBe(false); + }); + + it('returns false when packageManager is undefined', () => { + expect(shouldIgnoreScriptsForAutoInstall(undefined, '11.0.0')).toBe(false); + }); + + it('coerces non-strict semver pnpm versions', () => { + expect(shouldIgnoreScriptsForAutoInstall(PackageManager.pnpm, 'v11.0.0')).toBe(true); + expect(shouldIgnoreScriptsForAutoInstall(PackageManager.pnpm, '11')).toBe(true); + expect(shouldIgnoreScriptsForAutoInstall(PackageManager.pnpm, '10')).toBe(false); + }); +}); diff --git a/packages/cli/src/utils/prompts.ts b/packages/cli/src/utils/prompts.ts index 3d6408dd53..7f882b8ff8 100644 --- a/packages/cli/src/utils/prompts.ts +++ b/packages/cli/src/utils/prompts.ts @@ -1,4 +1,5 @@ import * as prompts from '@voidzero-dev/vite-plus-prompts'; +import semver from 'semver'; import { downloadPackageManager as downloadPackageManagerBinding } from '../../binding/index.js'; import { PackageManager } from '../types/index.ts'; @@ -11,6 +12,28 @@ export interface CommandRunSummary { status: 'installed' | 'formatted' | 'failed' | 'skipped'; } +/** + * pnpm v11 promoted `ERR_PNPM_IGNORED_BUILDS` from a warning to a hard + * exit-1. Auto-installs run by `vp migrate` / `vp create` happen before the + * user has a chance to approve build scripts via `pnpm.onlyBuiltDependencies`, + * so transitive deps like `esbuild` would fail the install. Pass + * `--ignore-scripts` in that window so the orchestration succeeds; the user's + * own subsequent `vp install` keeps default pnpm behavior. + */ +export function shouldIgnoreScriptsForAutoInstall( + packageManager: PackageManager | undefined, + packageManagerVersion: string | undefined, +): boolean { + if (packageManager !== PackageManager.pnpm) { + return false; + } + const coerced = packageManagerVersion ? semver.coerce(packageManagerVersion)?.version : undefined; + if (!coerced) { + return false; + } + return semver.gte(coerced, '11.0.0'); +} + export function cancelAndExit(message = 'Operation cancelled', exitCode = 0): never { prompts.cancel(message); process.exit(exitCode); @@ -63,19 +86,31 @@ export async function runViteInstall( cwd: string, interactive?: boolean, extraArgs?: string[], - options?: { silent?: boolean }, + options?: { + silent?: boolean; + packageManager?: PackageManager; + packageManagerVersion?: string; + }, ) { // install dependencies on non-CI environment if (process.env.VP_SKIP_INSTALL) { return { durationMs: 0, status: 'skipped' } satisfies CommandRunSummary; } + const installArgs = [...(extraArgs ?? [])]; + if ( + shouldIgnoreScriptsForAutoInstall(options?.packageManager, options?.packageManagerVersion) && + !installArgs.includes('--ignore-scripts') + ) { + installArgs.push('--ignore-scripts'); + } + const spinner = options?.silent ? getSilentSpinner() : getSpinner(interactive); const startTime = Date.now(); spinner.start(`Installing dependencies...`); const { exitCode, stderr, stdout } = await runCommandSilently({ command: process.env.VP_CLI_BIN ?? 'vp', - args: ['install', ...(extraArgs ?? [])], + args: ['install', ...installArgs], cwd, envs: process.env, });