From 9170fb07e97769df505b7559c8ac3a0ea425572d Mon Sep 17 00:00:00 2001 From: sakana <185575200+SaKaNa-Y@users.noreply.github.com> Date: Mon, 14 Sep 2026 22:46:01 +0800 Subject: [PATCH 1/6] fix(create): honor system package manager versions Use the selected system executable and version when scaffolding so the generated package-manager pin agrees with installation. Preserve managed downloads and local-only creation, and cover system selection, fallback, and npm installation. --- .../create_system_package_manager/install.cjs | 33 ++++++++++ .../snapshots.toml | 42 +++++++++++++ .../snapshots/create_managed_npm.md | 7 +++ .../snapshots/create_missing_system_pnpm.md | 7 +++ .../snapshots/create_system_bun.global.md | 7 +++ .../snapshots/create_system_bun.local.md | 7 +++ .../snapshots/create_system_npm.global.md | 7 +++ .../snapshots/create_system_npm.local.md | 7 +++ .../snapshots/create_system_npm_install.md | 7 +++ .../snapshots/create_system_pnpm.global.md | 7 +++ .../snapshots/create_system_pnpm.local.md | 7 +++ .../snapshots/create_system_yarn.global.md | 7 +++ .../snapshots/create_system_yarn.local.md | 7 +++ .../create_system_package_manager/verify.cjs | 43 +++++++++++++ .../create/__tests__/package-manager.spec.ts | 61 +++++++++++++++++++ packages/cli/src/create/bin.ts | 5 +- packages/cli/src/create/package-manager.ts | 61 +++++++++++++++++++ .../bun-catalog-file-protocol.spec.ts | 2 - .../src/migration/__tests__/migrator.spec.ts | 2 - packages/cli/src/migration/migrator/setup.ts | 2 +- packages/cli/src/types/workspace.ts | 2 +- .../cli/src/utils/__tests__/workspace.spec.ts | 2 - 22 files changed, 321 insertions(+), 11 deletions(-) create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/install.cjs create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots.toml create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_managed_npm.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_missing_system_pnpm.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_system_bun.global.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_system_bun.local.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_system_npm.global.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_system_npm.local.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_system_npm_install.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_system_pnpm.global.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_system_pnpm.local.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_system_yarn.global.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_system_yarn.local.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/verify.cjs create mode 100644 packages/cli/src/create/__tests__/package-manager.spec.ts create mode 100644 packages/cli/src/create/package-manager.ts 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..44926d6d9e --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots.toml @@ -0,0 +1,42 @@ +[[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"]] 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/verify.cjs b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/verify.cjs new file mode 100644 index 0000000000..d1e4499cf8 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/verify.cjs @@ -0,0 +1,43 @@ +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] = process.argv.slice(2); +const systemVersion = { 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 }; +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'); +run(['create', 'vite:application', '--directory', 'app', '--package-manager', manager, + '--no-interactive', '--no-agent', '--no-editor', '--no-hooks', '--no-git']); +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__/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/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, From 56e5f123ed42cd48d4b84f36e8d961cb6bc6f629 Mon Sep 17 00:00:00 2001 From: sakana <185575200+SaKaNa-Y@users.noreply.github.com> Date: Tue, 15 Sep 2026 21:43:05 +0800 Subject: [PATCH 2/6] fix(create): support system Yarn Classic template execution Use the resolved Yarn version to select the existing npx fallback for Classic, which has no dlx command. Preserve Yarn 2+ template execution and cover runner selection, external scaffolding, and the built-in template version pin. --- .../snapshots.toml | 12 +++++++ .../create_system_yarn_classic.global.md | 7 +++++ .../create_system_yarn_classic.local.md | 7 +++++ ...ate_system_yarn_classic_external.global.md | 7 +++++ ...eate_system_yarn_classic_external.local.md | 7 +++++ .../create_system_package_manager/verify.cjs | 19 +++++++++--- .../cli/src/create/__tests__/command.spec.ts | 31 ++++++++++++++++++- packages/cli/src/create/command.ts | 4 +++ 8 files changed, 89 insertions(+), 5 deletions(-) create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_system_yarn_classic.global.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_system_yarn_classic.local.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_system_yarn_classic_external.global.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_system_package_manager/snapshots/create_system_yarn_classic_external.local.md 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 index 44926d6d9e..ac78a2ed77 100644 --- 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 @@ -40,3 +40,15 @@ 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_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 index d1e4499cf8..5075f7987f 100644 --- 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 @@ -3,8 +3,8 @@ const fs = require('node:fs'); const path = require('node:path'); const { spawnSync } = require('node:child_process'); -const [manager, mode] = process.argv.slice(2); -const systemVersion = { npm: '10.9.3', pnpm: '10.18.0', yarn: '4.9.2', bun: '1.2.3' }[manager]; +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 }; const entryVp = env.PATH.split(path.delimiter).map(dir => path.join(dir, 'vp')).find(file => fs.existsSync(file)); @@ -35,8 +35,19 @@ 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'); -run(['create', 'vite:application', '--directory', 'app', '--package-manager', manager, - '--no-interactive', '--no-agent', '--no-editor', '--no-hooks', '--no-git']); +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); 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/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'], From b6134185daef371eec35d7aed67d32e271c0aaeb Mon Sep 17 00:00:00 2001 From: sakana <185575200+SaKaNa-Y@users.noreply.github.com> Date: Wed, 16 Sep 2026 09:52:41 +0800 Subject: [PATCH 3/6] test(create): isolate the Yarn Classic npx launcher The external-template regressions reached an npm shell launcher that crashes under fspy on macOS CI with an arm64/arm64e loader mismatch. Provision a fixed real npm and expose its npx JavaScript entry through a test-owned Node launcher. Keep both CLI flavors, real template execution, existing assertions, and snapshot expectations. --- .../create_system_package_manager/verify.cjs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) 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 index 5075f7987f..c30490d170 100644 --- 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 @@ -28,6 +28,22 @@ if (mode !== 'missing') { 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 (template === 'external') { + // Exercise real npx through a case-owned Node launcher. The managed npm + // shell shim crosses macOS's arm64e system interpreter before fspy can + // substitute its injectable shell, so it is not a portable test launcher. + run(['env', 'install', 'npm@10.9.3']); + const npm = JSON.parse(run(['env', 'current', 'npm', '--json'], { + ...env, VP_NPM_VERSION: '10.9.3', + })).package_manager; + const npmBin = path.dirname(fs.realpathSync(npm.bin_paths.npx)); + const npxCli = path.join(npmBin, 'npx-cli.js'); + assert.ok(fs.existsSync(npxCli)); + const runnerBin = path.resolve('template-bin'); + fs.mkdirSync(runnerBin); + fs.writeFileSync(path.join(runnerBin, 'npx'), `#!/usr/bin/env node\nrequire(${JSON.stringify(npxCli)});\n`, { mode: 0o755 }); + env.PATH = [runnerBin, env.PATH].join(path.delimiter); +} if (mode !== 'system') { fs.writeFileSync('package.json', JSON.stringify({ private: true, packageManager: `${manager}@${managedVersion}` })); } From 29b4be3c4075933426a4e05799114eed417ced43 Mon Sep 17 00:00:00 2001 From: sakana <185575200+SaKaNa-Y@users.noreply.github.com> Date: Wed, 16 Sep 2026 10:02:25 +0800 Subject: [PATCH 4/6] test(create): use a short direct Node interpreter for npx Avoid the system env interpreter as well as npm shell launchers under macOS file tracing. Use a temporary short symlink to the selected Node executable so the shebang stays within the tracer parser limit, assert that bound, and remove the symlink directory on exit. --- .../create_system_package_manager/verify.cjs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) 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 index c30490d170..120e150f45 100644 --- 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 @@ -1,6 +1,7 @@ const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); +const os = require('node:os'); const { spawnSync } = require('node:child_process'); const [manager, mode, template] = process.argv.slice(2); @@ -29,9 +30,9 @@ if (mode !== 'missing') { env.PATH = [path.join(env.VP_HOME, 'bin'), binDir, env.PATH].join(path.delimiter); } if (template === 'external') { - // Exercise real npx through a case-owned Node launcher. The managed npm - // shell shim crosses macOS's arm64e system interpreter before fspy can - // substitute its injectable shell, so it is not a portable test launcher. + // Use a direct Node shebang: system sh/env interpreters can reject fspy's + // arm64 preload on macOS. Keep it short enough for fspy's shebang parser; + // case-owned runtime paths can exceed its 128-byte read limit. run(['env', 'install', 'npm@10.9.3']); const npm = JSON.parse(run(['env', 'current', 'npm', '--json'], { ...env, VP_NPM_VERSION: '10.9.3', @@ -41,7 +42,12 @@ if (template === 'external') { assert.ok(fs.existsSync(npxCli)); const runnerBin = path.resolve('template-bin'); fs.mkdirSync(runnerBin); - fs.writeFileSync(path.join(runnerBin, 'npx'), `#!/usr/bin/env node\nrequire(${JSON.stringify(npxCli)});\n`, { mode: 0o755 }); + const interpreterDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vp-npx-')); + process.on('exit', () => fs.rmSync(interpreterDir, { recursive: true, force: true })); + const nodeBinary = path.join(interpreterDir, 'node'); + fs.symlinkSync(fs.realpathSync(process.execPath), nodeBinary); + assert.ok(Buffer.byteLength(`#!${nodeBinary}\n`) < 128); + fs.writeFileSync(path.join(runnerBin, 'npx'), `#!${nodeBinary}\nrequire(${JSON.stringify(npxCli)});\n`, { mode: 0o755 }); env.PATH = [runnerBin, env.PATH].join(path.delimiter); } if (mode !== 'system') { From df27015f04ce8502afc74a43118b058e6483dd14 Mon Sep 17 00:00:00 2001 From: sakana <185575200+SaKaNa-Y@users.noreply.github.com> Date: Wed, 16 Sep 2026 10:11:23 +0800 Subject: [PATCH 5/6] test(create): assert the selected npx entry and forwarded arguments Verify that external scaffolding actually enters the fixture-provided npx executable with the expected arguments. Include launcher-entry information when scaffolding fails so runner selection failures can be distinguished from downstream process failures. --- .../fixtures/create_system_package_manager/verify.cjs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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 index 120e150f45..7408c26735 100644 --- 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 @@ -13,7 +13,9 @@ 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); + const runnerStarted = template === 'external' && args[0] === 'create' + ? `\nPinned npx entered: ${fs.existsSync('npx-args.json')}` : ''; + assert.equal(result.status, 0, result.stdout + result.stderr + runnerStarted); return result.stdout; } @@ -47,7 +49,7 @@ if (template === 'external') { const nodeBinary = path.join(interpreterDir, 'node'); fs.symlinkSync(fs.realpathSync(process.execPath), nodeBinary); assert.ok(Buffer.byteLength(`#!${nodeBinary}\n`) < 128); - fs.writeFileSync(path.join(runnerBin, 'npx'), `#!${nodeBinary}\nrequire(${JSON.stringify(npxCli)});\n`, { mode: 0o755 }); + fs.writeFileSync(path.join(runnerBin, 'npx'), `#!${nodeBinary}\nrequire('node:fs').writeFileSync('npx-args.json', JSON.stringify(process.argv.slice(2)));\nrequire(${JSON.stringify(npxCli)});\n`, { mode: 0o755 }); env.PATH = [runnerBin, env.PATH].join(path.delimiter); } if (mode !== 'system') { @@ -63,6 +65,8 @@ const output = run(['create', ...templateArgs, '--package-manager', manager, ...(template === 'external' ? ['--', 'app', '--template', 'vanilla'] : [])]); if (template === 'external') { assert.match(output, /Running: npx --yes create-vite/); + assert.deepEqual(JSON.parse(fs.readFileSync('npx-args.json', 'utf8')), + ['--yes', 'create-vite', 'app', '--template', 'vanilla', '--no-immediate', '--no-rolldown']); const pkg = JSON.parse(fs.readFileSync('app/package.json', 'utf8')); assert.equal(pkg.name, 'app'); assert.ok(fs.existsSync('app/index.html')); From f9285c24d1732bafc94ed3a623dd5acdcfdfdd74 Mon Sep 17 00:00:00 2001 From: sakana <185575200+SaKaNa-Y@users.noreply.github.com> Date: Wed, 16 Sep 2026 10:19:23 +0800 Subject: [PATCH 6/6] test(create): use the canonical npm script shell The snapshot runner exposes sh through a symlink, but fspy recognizes system shells by their /bin or /usr/bin parent. npm resolves its default sh through PATH, bypassing the macOS injectable-shell substitution. Set npm_config_script_shell to /bin/sh for the Unix external-template fixture. Remove the unnecessary custom npx launcher and its temporary interpreter; keep real template execution and generated-file assertions. --- .../create_system_package_manager/verify.cjs | 33 ++++--------------- 1 file changed, 6 insertions(+), 27 deletions(-) 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 index 7408c26735..8698d75292 100644 --- 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 @@ -1,21 +1,23 @@ const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); -const os = require('node:os'); 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; - const runnerStarted = template === 'external' && args[0] === 'create' - ? `\nPinned npx entered: ${fs.existsSync('npx-args.json')}` : ''; - assert.equal(result.status, 0, result.stdout + result.stderr + runnerStarted); + assert.equal(result.status, 0, result.stdout + result.stderr); return result.stdout; } @@ -31,27 +33,6 @@ if (mode !== 'missing') { 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 (template === 'external') { - // Use a direct Node shebang: system sh/env interpreters can reject fspy's - // arm64 preload on macOS. Keep it short enough for fspy's shebang parser; - // case-owned runtime paths can exceed its 128-byte read limit. - run(['env', 'install', 'npm@10.9.3']); - const npm = JSON.parse(run(['env', 'current', 'npm', '--json'], { - ...env, VP_NPM_VERSION: '10.9.3', - })).package_manager; - const npmBin = path.dirname(fs.realpathSync(npm.bin_paths.npx)); - const npxCli = path.join(npmBin, 'npx-cli.js'); - assert.ok(fs.existsSync(npxCli)); - const runnerBin = path.resolve('template-bin'); - fs.mkdirSync(runnerBin); - const interpreterDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vp-npx-')); - process.on('exit', () => fs.rmSync(interpreterDir, { recursive: true, force: true })); - const nodeBinary = path.join(interpreterDir, 'node'); - fs.symlinkSync(fs.realpathSync(process.execPath), nodeBinary); - assert.ok(Buffer.byteLength(`#!${nodeBinary}\n`) < 128); - fs.writeFileSync(path.join(runnerBin, 'npx'), `#!${nodeBinary}\nrequire('node:fs').writeFileSync('npx-args.json', JSON.stringify(process.argv.slice(2)));\nrequire(${JSON.stringify(npxCli)});\n`, { mode: 0o755 }); - env.PATH = [runnerBin, env.PATH].join(path.delimiter); -} if (mode !== 'system') { fs.writeFileSync('package.json', JSON.stringify({ private: true, packageManager: `${manager}@${managedVersion}` })); } @@ -65,8 +46,6 @@ const output = run(['create', ...templateArgs, '--package-manager', manager, ...(template === 'external' ? ['--', 'app', '--template', 'vanilla'] : [])]); if (template === 'external') { assert.match(output, /Running: npx --yes create-vite/); - assert.deepEqual(JSON.parse(fs.readFileSync('npx-args.json', 'utf8')), - ['--yes', 'create-vite', 'app', '--template', 'vanilla', '--no-immediate', '--no-rolldown']); const pkg = JSON.parse(fs.readFileSync('app/package.json', 'utf8')); assert.equal(pkg.name, 'app'); assert.ok(fs.existsSync('app/index.html'));