diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/install.cjs b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/install.cjs new file mode 100644 index 0000000000..0f11b20f65 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/install.cjs @@ -0,0 +1,33 @@ +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const { spawnSync } = require('node:child_process'); + +const env = { ...process.env }; +function run(args, envs = env) { + const result = spawnSync('vp', args, { env: envs, encoding: 'utf8' }); + if (result.error) throw result.error; + assert.equal(result.status, 0, result.stdout + result.stderr); + return result.stdout; +} + +// Provision a real npm, then copy it outside the managed installation so the +// system-first resolver sees an independent executable on PATH. +run(['env', 'install', 'npm@10.9.3']); +const current = JSON.parse(run(['env', 'current', 'npm', '--json'], { ...env, VP_NPM_VERSION: '10.9.3' })).package_manager; +const npmRoot = path.dirname(path.dirname(fs.realpathSync(current.bin_paths.npm))); +fs.cpSync(npmRoot, 'system-npm', { recursive: true }); +const binDir = path.resolve('system-bin'); +fs.mkdirSync(binDir); +fs.writeFileSync(path.join(binDir, 'npm'), `#!${process.execPath}\nrequire(${JSON.stringify(path.resolve('system-npm/bin/npm-cli.js'))});\n`, { mode: 0o755 }); +env.PATH = [path.join(env.VP_HOME, 'bin'), binDir, env.PATH].join(path.delimiter); +run(['env', 'off', 'npm']); +delete env.VP_SKIP_INSTALL; +const output = run(['create', 'vite:application', '--directory', 'app', '--package-manager', 'npm', + '--no-interactive', '--no-agent', '--no-editor', '--no-hooks', '--no-git']); +assert.doesNotMatch(output, /EBADDEVENGINES/); +const pin = JSON.parse(fs.readFileSync('app/package.json', 'utf8')).devEngines.packageManager; +assert.equal(pin.version, '10.9.3'); +assert.ok(fs.statSync('app/node_modules/vite-plus/package.json').isFile()); +assert.ok(fs.statSync('app/package-lock.json').isFile()); +console.log('System npm version pinned and dependencies installed successfully'); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots.toml new file mode 100644 index 0000000000..ac78a2ed77 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots.toml @@ -0,0 +1,54 @@ +[[case]] +name = "create_system_npm_install" +vp = "global" +skip-platforms = ["windows"] +local-registry = true +steps = [{ argv = ["node", "install.cjs"], timeout = 180000 }] + +[[case]] +name = "create_system_npm" +vp = ["local", "global"] +skip-platforms = ["windows"] +steps = [["node", "verify.cjs", "npm", "system"]] + +[[case]] +name = "create_system_pnpm" +vp = ["local", "global"] +skip-platforms = ["windows"] +steps = [["node", "verify.cjs", "pnpm", "system"]] + +[[case]] +name = "create_system_yarn" +vp = ["local", "global"] +skip-platforms = ["windows"] +steps = [["node", "verify.cjs", "yarn", "system"]] + +[[case]] +name = "create_system_bun" +vp = ["local", "global"] +skip-platforms = ["windows"] +steps = [["node", "verify.cjs", "bun", "system"]] + +[[case]] +name = "create_managed_npm" +vp = "global" +skip-platforms = ["windows"] +steps = [["node", "verify.cjs", "npm", "managed"]] + +[[case]] +name = "create_missing_system_pnpm" +vp = "global" +skip-platforms = ["windows"] +steps = [["node", "verify.cjs", "pnpm", "missing"]] + +[[case]] +name = "create_system_yarn_classic_external" +vp = ["local", "global"] +skip-platforms = ["windows"] +steps = [{ argv = ["node", "verify.cjs", "yarn", "system", "external"], timeout = 180000 }] + +[[case]] +name = "create_system_yarn_classic" +vp = ["local", "global"] +skip-platforms = ["windows"] +steps = [["node", "verify.cjs", "yarn", "system", "classic"]] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_managed_npm.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_managed_npm.md new file mode 100644 index 0000000000..0298a3a317 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_managed_npm.md @@ -0,0 +1,7 @@ +# create_managed_npm + +## `node verify.cjs npm managed` + +``` +npm: managed version pinned +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_missing_system_pnpm.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_missing_system_pnpm.md new file mode 100644 index 0000000000..76be056b3b --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_missing_system_pnpm.md @@ -0,0 +1,7 @@ +# create_missing_system_pnpm + +## `node verify.cjs pnpm missing` + +``` +pnpm: managed version pinned +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_system_bun.global.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_system_bun.global.md new file mode 100644 index 0000000000..65af351835 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_system_bun.global.md @@ -0,0 +1,7 @@ +# create_system_bun + +## `node verify.cjs bun system` + +``` +bun: system version pinned +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_system_bun.local.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_system_bun.local.md new file mode 100644 index 0000000000..65af351835 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_system_bun.local.md @@ -0,0 +1,7 @@ +# create_system_bun + +## `node verify.cjs bun system` + +``` +bun: system version pinned +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_system_npm.global.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_system_npm.global.md new file mode 100644 index 0000000000..dde5189245 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_system_npm.global.md @@ -0,0 +1,7 @@ +# create_system_npm + +## `node verify.cjs npm system` + +``` +npm: system version pinned +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_system_npm.local.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_system_npm.local.md new file mode 100644 index 0000000000..dde5189245 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_system_npm.local.md @@ -0,0 +1,7 @@ +# create_system_npm + +## `node verify.cjs npm system` + +``` +npm: system version pinned +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_system_npm_install.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_system_npm_install.md new file mode 100644 index 0000000000..34adc81545 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_system_npm_install.md @@ -0,0 +1,7 @@ +# create_system_npm_install + +## `node install.cjs` + +``` +System npm version pinned and dependencies installed successfully +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_system_pnpm.global.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_system_pnpm.global.md new file mode 100644 index 0000000000..4327b3ec7a --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_system_pnpm.global.md @@ -0,0 +1,7 @@ +# create_system_pnpm + +## `node verify.cjs pnpm system` + +``` +pnpm: system version pinned +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_system_pnpm.local.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_system_pnpm.local.md new file mode 100644 index 0000000000..4327b3ec7a --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_system_pnpm.local.md @@ -0,0 +1,7 @@ +# create_system_pnpm + +## `node verify.cjs pnpm system` + +``` +pnpm: system version pinned +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_system_yarn.global.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_system_yarn.global.md new file mode 100644 index 0000000000..1b52989776 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_system_yarn.global.md @@ -0,0 +1,7 @@ +# create_system_yarn + +## `node verify.cjs yarn system` + +``` +yarn: system version pinned +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_system_yarn.local.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_system_yarn.local.md new file mode 100644 index 0000000000..1b52989776 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_system_yarn.local.md @@ -0,0 +1,7 @@ +# create_system_yarn + +## `node verify.cjs yarn system` + +``` +yarn: system version pinned +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_system_yarn_classic.global.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_system_yarn_classic.global.md new file mode 100644 index 0000000000..e58763d741 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_system_yarn_classic.global.md @@ -0,0 +1,7 @@ +# create_system_yarn_classic + +## `node verify.cjs yarn system classic` + +``` +yarn: system version pinned +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_system_yarn_classic.local.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_system_yarn_classic.local.md new file mode 100644 index 0000000000..e58763d741 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_system_yarn_classic.local.md @@ -0,0 +1,7 @@ +# create_system_yarn_classic + +## `node verify.cjs yarn system classic` + +``` +yarn: system version pinned +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_system_yarn_classic_external.global.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_system_yarn_classic_external.global.md new file mode 100644 index 0000000000..5571989d30 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_system_yarn_classic_external.global.md @@ -0,0 +1,7 @@ +# create_system_yarn_classic_external + +## `node verify.cjs yarn system external` + +``` +yarn: external template generated with npx +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_system_yarn_classic_external.local.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_system_yarn_classic_external.local.md new file mode 100644 index 0000000000..5571989d30 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_system_yarn_classic_external.local.md @@ -0,0 +1,7 @@ +# create_system_yarn_classic_external + +## `node verify.cjs yarn system external` + +``` +yarn: external template generated with npx +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/verify.cjs b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/verify.cjs new file mode 100644 index 0000000000..8698d75292 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/verify.cjs @@ -0,0 +1,59 @@ +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const { spawnSync } = require('node:child_process'); + +const [manager, mode, template] = process.argv.slice(2); +const systemVersion = manager === 'yarn' && (template === 'external' || template === 'classic') ? '1.22.22' : { npm: '10.9.3', pnpm: '10.18.0', yarn: '4.9.2', bun: '1.2.3' }[manager]; +const managedVersion = manager === 'npm' ? '11.6.0' : '10.18.0'; +const env = { ...process.env }; +if (template === 'external' && process.platform !== 'win32') { + // The runner exposes sh through a symlink. Use its canonical system path so + // fspy can substitute an injectable shell on macOS when npm runs the template. + env.npm_config_script_shell = '/bin/sh'; +} +const entryVp = env.PATH.split(path.delimiter).map(dir => path.join(dir, 'vp')).find(file => fs.existsSync(file)); +const globalVp = path.join(env.VP_HOME, 'bin', 'vp'); +function run(args, envs = env) { + const result = spawnSync(args[0] === 'env' ? globalVp : entryVp, args, { env: envs, encoding: 'utf8' }); + if (result.error) throw result.error; + assert.equal(result.status, 0, result.stdout + result.stderr); + return result.stdout; +} + +if (mode !== 'missing') { + // Expose a real tool through a separate system PATH entry. Templates can + // invoke the tool as well as query its version, so a version-only stub is insufficient. + run(['env', 'install', `${manager}@${systemVersion}`]); + const selection = JSON.parse(run(['env', 'current', manager, '--json'], { + ...env, [`VP_${manager.toUpperCase()}_VERSION`]: systemVersion, + })).package_manager; + const binDir = path.resolve('system-bin'); + fs.mkdirSync(binDir); + fs.writeFileSync(path.join(binDir, manager), `#!${process.execPath}\nconst result = require('node:child_process').spawnSync(${JSON.stringify(selection.bin_paths[manager])}, process.argv.slice(2), { stdio: 'inherit' });\nif (result.error) throw result.error;\nprocess.exit(result.status ?? 1);\n`, { mode: 0o755 }); + env.PATH = [path.join(env.VP_HOME, 'bin'), binDir, env.PATH].join(path.delimiter); +} +if (mode !== 'system') { + fs.writeFileSync('package.json', JSON.stringify({ private: true, packageManager: `${manager}@${managedVersion}` })); +} +run(['env', mode === 'managed' ? 'on' : 'off', manager]); +const current = JSON.parse(run(['env', 'current', manager, '--json'])).package_manager; +if (mode === 'system') assert.equal(current.source, 'system PATH'); +else assert.notEqual(current.source, 'system PATH'); +const templateArgs = template === 'external' ? ['vite'] : ['vite:application', '--directory', 'app']; +const output = run(['create', ...templateArgs, '--package-manager', manager, + '--no-interactive', '--no-agent', '--no-editor', '--no-hooks', '--no-git', + ...(template === 'external' ? ['--', 'app', '--template', 'vanilla'] : [])]); +if (template === 'external') { + assert.match(output, /Running: npx --yes create-vite/); + const pkg = JSON.parse(fs.readFileSync('app/package.json', 'utf8')); + assert.equal(pkg.name, 'app'); + assert.ok(fs.existsSync('app/index.html')); + assert.equal(current.version, systemVersion); + console.log(`${manager}: external template generated with npx`); + process.exit(0); +} +const pin = JSON.parse(fs.readFileSync('app/package.json', 'utf8')).devEngines.packageManager; +assert.equal(pin.name, manager); +assert.equal(pin.version, mode === 'system' ? systemVersion : managedVersion); +console.log(`${manager}: ${mode === 'system' ? 'system version pinned' : 'managed version pinned'}`); diff --git a/packages/cli/src/create/__tests__/command.spec.ts b/packages/cli/src/create/__tests__/command.spec.ts index 89c9fdb05e..611d34b0bd 100644 --- a/packages/cli/src/create/__tests__/command.spec.ts +++ b/packages/cli/src/create/__tests__/command.spec.ts @@ -4,6 +4,8 @@ import path from 'node:path'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { PackageManager, type WorkspaceInfo } from '../../types/index.ts'; + const { mockRunCommand } = vi.hoisted(() => ({ mockRunCommand: vi.fn(), })); @@ -12,7 +14,7 @@ vi.mock('../../../binding/index.js', () => ({ runCommand: mockRunCommand, })); -const { runCommandAndDetectProjectDir } = await import('../command.js'); +const { formatDlxCommand, runCommandAndDetectProjectDir } = await import('../command.js'); const tempDirs: string[] = []; @@ -98,3 +100,30 @@ describe('runCommandAndDetectProjectDir', () => { }); }); }); + +describe('formatDlxCommand', () => { + it.each([ + ['1.22.22', 'npx', ['--yes']], + ['2.4.3', 'yarn', ['dlx']], + ['4.9.2', 'yarn', ['dlx']], + ])('runs templates with Yarn %s', (version, command, runnerArgs) => { + const workspace: WorkspaceInfo = { + rootDir: '/project', + isMonorepo: false, + monorepoScope: '', + workspacePatterns: [], + parentDirs: [], + packageManager: PackageManager.yarn, + // Ambient metadata can differ from the selected system version. + packageManagerVersion: '4.9.2', + downloadPackageManager: { name: 'yarn', version, binPrefix: '/system/bin' }, + packages: [], + }; + expect( + formatDlxCommand('@example/create-app@1.0.0', ['app', '--template', 'vanilla'], workspace), + ).toEqual({ + command, + args: [...runnerArgs, '@example/create-app@1.0.0', 'app', '--template', 'vanilla'], + }); + }); +}); diff --git a/packages/cli/src/create/__tests__/package-manager.spec.ts b/packages/cli/src/create/__tests__/package-manager.spec.ts new file mode 100644 index 0000000000..5d249aceb8 --- /dev/null +++ b/packages/cli/src/create/__tests__/package-manager.spec.ts @@ -0,0 +1,61 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import { PackageManager } from '../../types/index.ts'; +import { resolveCreatePackageManager } from '../package-manager.ts'; + +const { run, download } = vi.hoisted(() => ({ run: vi.fn(), download: vi.fn() })); + +vi.mock('../../../binding/index.js', () => ({ getVpDirs: () => ({ bin: '/vp/bin' }) })); +vi.mock('../../utils/command.ts', () => ({ runCommandSilently: run })); +vi.mock('../../utils/prompts.ts', () => ({ downloadPackageManager: download })); + +afterEach(() => { + vi.restoreAllMocks(); + vi.resetAllMocks(); + vi.unstubAllEnvs(); +}); + +describe('resolveCreatePackageManager', () => { + it('preserves local-only creation when no global CLI is installed', async () => { + vi.stubEnv('VP_CLI_BIN', undefined); + vi.spyOn(fs, 'existsSync').mockReturnValue(false); + const manager = { name: 'npm', version: '11.6.0', binPrefix: '/npm/bin' }; + download.mockResolvedValue(manager); + + expect(await resolveCreatePackageManager(PackageManager.npm, '11.6.0')).toBe(manager); + expect(run).not.toHaveBeenCalled(); + expect(download).toHaveBeenCalledWith(PackageManager.npm, '11.6.0', undefined, false); + }); + + it('does not replace an unreadable system version with a managed version', async () => { + vi.stubEnv('VP_CLI_BIN', undefined); + vi.spyOn(fs, 'existsSync').mockReturnValue(true); + run.mockResolvedValue({ + exitCode: 0, + stdout: Buffer.from( + JSON.stringify({ + package_manager: { + name: 'npm', + version: 'unknown', + source: 'system PATH', + bin_paths: { npm: '/system/bin/npm' }, + }, + }), + ), + stderr: Buffer.alloc(0), + }); + + await expect(resolveCreatePackageManager(PackageManager.npm, 'latest')).rejects.toThrow( + 'Could not determine the system npm version and executable', + ); + expect(download).not.toHaveBeenCalled(); + expect(run).toHaveBeenCalledWith( + expect.objectContaining({ + command: path.join('/vp/bin', process.platform === 'win32' ? 'vp.exe' : 'vp'), + }), + ); + }); +}); diff --git a/packages/cli/src/create/bin.ts b/packages/cli/src/create/bin.ts index 01bad1d372..7dddaf6b1a 100644 --- a/packages/cli/src/create/bin.ts +++ b/packages/cli/src/create/bin.ts @@ -46,7 +46,6 @@ import { cancelAndExit, type CommandRunSummary, defaultInteractive, - downloadPackageManager, promptGitHooks, resolveGitInit, runViteFmt, @@ -69,6 +68,7 @@ import { type OrgResolution, resolveOrgManifestForCreate, } from './org-resolve.ts'; +import { resolveCreatePackageManager } from './package-manager.ts'; import { checkProjectDirExists, promptPackageNameAndTargetDir, @@ -683,8 +683,7 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h : 'latest'; const shouldSilencePackageManagerInstallLog = compactOutput || (isMonorepo && workspaceInfoOptional.packageManager !== undefined); - // ensure the package manager is installed by vite-plus - const downloadResult = await downloadPackageManager( + const downloadResult = await resolveCreatePackageManager( packageManager, packageManagerVersion, options.interactive, diff --git a/packages/cli/src/create/command.ts b/packages/cli/src/create/command.ts index 778d84f2af..1e7f1fb0ed 100644 --- a/packages/cli/src/create/command.ts +++ b/packages/cli/src/create/command.ts @@ -96,6 +96,10 @@ export function getPackageRunner(workspaceInfo: WorkspaceInfo) { args: ['dlx'], }; case 'yarn': + // Match `vp dlx`: Yarn Classic has no dlx command. + if (workspaceInfo.downloadPackageManager.version.startsWith('1.')) { + return { command: 'npx', args: ['--yes'] }; + } return { command: 'yarn', args: ['dlx'], diff --git a/packages/cli/src/create/package-manager.ts b/packages/cli/src/create/package-manager.ts new file mode 100644 index 0000000000..b045d2faab --- /dev/null +++ b/packages/cli/src/create/package-manager.ts @@ -0,0 +1,61 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +import semver from 'semver'; + +import { getVpDirs } from '../../binding/index.js'; +import type { PackageManager, WorkspaceInfo } from '../types/index.ts'; +import { runCommandSilently } from '../utils/command.ts'; +import { downloadPackageManager } from '../utils/prompts.ts'; + +interface CurrentPackageManager { + name: string; + version: string; + source: string; + bin_paths: Record; +} + +export async function resolveCreatePackageManager( + packageManager: PackageManager, + version: string, + interactive?: boolean, + silent = false, +): Promise { + const globalVp = + process.env.VP_CLI_BIN ?? + path.join(getVpDirs().bin, process.platform === 'win32' ? 'vp.exe' : 'vp'); + // The local package can be used without a global installation. In that case + // there is no global environment selection to honor. + if (!process.env.VP_CLI_BIN && !fs.existsSync(globalVp)) { + return downloadPackageManager(packageManager, version, interactive, silent); + } + // Use the same mode and system-tool lookup as the subsequent `vp install`. + // A system-first mode can still fall back to managed when no system tool exists. + const result = await runCommandSilently({ + command: globalVp, + args: ['env', 'current', packageManager, '--json'], + cwd: process.cwd(), + envs: process.env, + }); + if (result.exitCode !== 0) { + throw new Error(`Failed to resolve ${packageManager}: ${result.stderr.toString().trim()}`); + } + const current = ( + JSON.parse(result.stdout.toString()) as { + package_manager?: CurrentPackageManager; + } + ).package_manager; + if (current?.source === 'system PATH') { + const binPath = current.bin_paths[packageManager]; + if (current.name !== packageManager || !semver.valid(current.version) || !binPath) { + throw new Error(`Could not determine the system ${packageManager} version and executable`); + } + return { + name: packageManager, + version: current.version, + binPrefix: path.dirname(binPath), + }; + } + + return downloadPackageManager(packageManager, version, interactive, silent); +} diff --git a/packages/cli/src/migration/__tests__/bun-catalog-file-protocol.spec.ts b/packages/cli/src/migration/__tests__/bun-catalog-file-protocol.spec.ts index 93cd6eae33..f2b818dff3 100644 --- a/packages/cli/src/migration/__tests__/bun-catalog-file-protocol.spec.ts +++ b/packages/cli/src/migration/__tests__/bun-catalog-file-protocol.spec.ts @@ -32,9 +32,7 @@ function makeWorkspaceInfo(rootDir: string, packageManager: PackageManager): Wor packageManagerVersion: '10.33.0', downloadPackageManager: { name: packageManager, - installDir: '/tmp', binPrefix: '/tmp/bin', - packageName: packageManager, version: '1.0.0', }, packages: [], diff --git a/packages/cli/src/migration/__tests__/migrator.spec.ts b/packages/cli/src/migration/__tests__/migrator.spec.ts index c395b594db..3a1f0f5ef0 100644 --- a/packages/cli/src/migration/__tests__/migrator.spec.ts +++ b/packages/cli/src/migration/__tests__/migrator.spec.ts @@ -1823,9 +1823,7 @@ function makeWorkspaceInfo( packageManagerVersion: version, downloadPackageManager: { name: packageManager, - installDir: '/tmp', binPrefix: '/tmp/bin', - packageName: packageManager, version, }, packages: [], diff --git a/packages/cli/src/migration/migrator/setup.ts b/packages/cli/src/migration/migrator/setup.ts index 1dfdc41b39..d55a37e3f2 100644 --- a/packages/cli/src/migration/migrator/setup.ts +++ b/packages/cli/src/migration/migrator/setup.ts @@ -15,7 +15,7 @@ import { isPlainRecord, warnMigration } from './shared.ts'; export function setPackageManager( projectDir: string, - downloadPackageManager: DownloadPackageManagerResult, + downloadPackageManager: Pick, ) { // Set the package manager pin. Compatibility-first rule (rfcs/dev-engines.md): // an existing `packageManager` field or `devEngines.packageManager` declaration diff --git a/packages/cli/src/types/workspace.ts b/packages/cli/src/types/workspace.ts index 97d1d2c8a1..5975c3d912 100644 --- a/packages/cli/src/types/workspace.ts +++ b/packages/cli/src/types/workspace.ts @@ -24,7 +24,7 @@ export interface WorkspaceInfo { parentDirs: string[]; packageManager: PackageManager; packageManagerVersion: string; - downloadPackageManager: DownloadPackageManagerResult; + downloadPackageManager: Pick; packages: WorkspacePackage[]; } diff --git a/packages/cli/src/utils/__tests__/workspace.spec.ts b/packages/cli/src/utils/__tests__/workspace.spec.ts index a8b860c021..bb1f047e88 100644 --- a/packages/cli/src/utils/__tests__/workspace.spec.ts +++ b/packages/cli/src/utils/__tests__/workspace.spec.ts @@ -40,9 +40,7 @@ function makeWorkspaceInfo(overrides: Partial = {}): WorkspaceInf packages: [], downloadPackageManager: { name: '', - installDir: '', binPrefix: '', - packageName: '', version: '', }, ...overrides,