Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const [packageManager, mode] = process.argv.slice(2);
const env = { ...process.env };
const pathKey = Object.keys(env).find((key) => key.toUpperCase() === 'PATH');
// Keep system tools such as Git, but remove every directory that exposes vp.
const vpExecutables = ['vp', 'vp.exe', 'vp.cmd', 'vp.bat'];
const pathsWithoutVp = env[pathKey]
.split(path.delimiter)
.filter((dir) => !vpExecutables.some((bin) => existsSync(path.join(dir, bin))));
env[pathKey] = [path.dirname(process.execPath), ...pathsWithoutVp].join(path.delimiter);
delete env.VP_CLI_BIN;
assert.equal(spawnSync('vp', ['--version'], { env }).error?.code, 'ENOENT');

if (mode === 'unrelated') {
const binDir = path.resolve('unrelated-bin');
mkdirSync(binDir);
writeFileSync(path.join(binDir, 'vp'), '#!/bin/sh\necho unrelated-vp >&2\nexit 99\n', { mode: 0o755 });
writeFileSync(path.join(binDir, 'vp.cmd'), '@echo unrelated-vp >&2\r\n@exit /b 99\r\n');
env[pathKey] = `${binDir}${path.delimiter}${env[pathKey]}`;
}

env.npm_config_user_agent = packageManager.replace('@', '/');
const cli = fileURLToPath(import.meta.resolve('vite-plus/bin'));
const projectDir = 'projects with spaces/app';
// Reuse the local bundled-template fixture, without an external template download.
const result = spawnSync(
process.execPath,
[
cli,
'create',
'@your-org:demo',
'--directory',
projectDir,
'--no-interactive',
'--no-agent',
'--no-editor',
'--git',
'--hooks',
'--verbose',
],
{ env, encoding: 'utf8', timeout: 210000 },
);
const output = `${result.stdout}\n${result.stderr}`;
assert.equal(result.status, 0, output || result.error?.message);
assert.match(output, /Dependencies installed/);
assert.match(output, /Code formatted/);
assert.match(output, /Git hooks configured/);
assert.doesNotMatch(output, /unrelated-vp|Failed to install git hooks|Install failed|Format failed/);
assert.ok(existsSync(path.join(projectDir, 'node_modules/vite-plus/package.json')));
assert.match(readFileSync(path.join(projectDir, '.git/config'), 'utf8'), /hooksPath/);
const lockfile = packageManager.startsWith('pnpm@') ? 'pnpm-lock.yaml' : 'yarn.lock';
assert.ok(existsSync(path.join(projectDir, lockfile)));
console.log(
`Created a project with ${packageManager.split('@')[0]} and ${mode === 'missing' ? 'no' : 'an unrelated'} vp on PATH.`,
);
console.log('Dependencies installed, code formatted, and Git hooks configured.');
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"@your-org/create": {
"name": "@your-org/create",
"dist-tags": {
"latest": "1.0.0"
},
"versions": {
"1.0.0": {
"version": "1.0.0",
"dist": {
"tarball": "{REGISTRY}/@your-org/create/-/create-1.0.0.tgz",
"integrity": "sha512-swwZCXG8RDBJNTmiw3zCOBCQx9bCg7wHqoW37aV7BgGYfBUU7n7c+efbSCg0+FpdLhgeUjEMP5IAO+8exmeFKg=="
},
"createConfig": {
"templates": [
{
"name": "demo",
"description": "Bundled demo template",
"template": "./templates/demo"
}
]
}
}
}
},
"pnpm/latest": {
"name": "pnpm",
"version": "12.11.2"
},
"@yarnpkg/cli-dist/latest": {
"name": "@yarnpkg/cli-dist",
"version": "4.18.1"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[[case]]
name = "pnpm_without_global_vp"
vp = "local"
local-registry = true
unset-env = ["VP_SKIP_INSTALL", "CI"]
steps = [
{ argv = ["node", "assert-create.mjs", "pnpm@12.11.2", "missing"], timeout = 240000 },
]

[[case]]
name = "yarn_without_global_vp"
vp = "local"
local-registry = true
unset-env = ["VP_SKIP_INSTALL", "CI"]
steps = [
{ argv = ["node", "assert-create.mjs", "yarn@4.18.1", "missing"], timeout = 240000 },
]

[[case]]
name = "ignores_unrelated_global_vp"
vp = "local"
local-registry = true
unset-env = ["VP_SKIP_INSTALL", "CI"]
steps = [
{ argv = ["node", "assert-create.mjs", "pnpm@12.11.2", "unrelated"], timeout = 240000 },
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# ignores_unrelated_global_vp

## `node assert-create.mjs pnpm@12.11.2 unrelated`

```
Created a project with pnpm and an unrelated vp on PATH.
Dependencies installed, code formatted, and Git hooks configured.
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# pnpm_without_global_vp

## `node assert-create.mjs pnpm@12.11.2 missing`

```
Created a project with pnpm and no vp on PATH.
Dependencies installed, code formatted, and Git hooks configured.
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# yarn_without_global_vp

## `node assert-create.mjs yarn@4.18.1 missing`

```
Created a project with yarn and no vp on PATH.
Dependencies installed, code formatted, and Git hooks configured.
```
Binary file not shown.
10 changes: 8 additions & 2 deletions packages/cli/src/migration/__tests__/prettier.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
vi.mock('@voidzero-dev/vite-plus-prompts', () => ({
log: { info: vi.fn(), success: vi.fn(), warn: vi.fn() },
}));
vi.mock('../../utils/command.ts', () => ({
vi.mock(import('../../utils/command.ts'), async (importOriginal) => ({
...(await importOriginal()),
runCommandSilently: vi.fn(),
}));

Expand All @@ -27,11 +28,13 @@ describe('Prettier migration', () => {
};

beforeEach(() => {
vi.stubEnv('VP_CLI_BIN', undefined);
projectPath = fs.mkdtempSync(path.join(os.tmpdir(), 'vp-test-prettier-'));
fs.writeFileSync(path.join(projectPath, 'package.json'), JSON.stringify(packageJson));
});

afterEach(() => {
vi.unstubAllEnvs();
fs.rmSync(projectPath, { recursive: true, force: true });
mockRunCommandSilently.mockReset();
});
Expand All @@ -56,7 +59,10 @@ describe('Prettier migration', () => {
).resolves.toBe('skipped');

expect(mockRunCommandSilently).toHaveBeenCalledWith(
expect.objectContaining({ args: ['fmt', '--migrate=prettier'], cwd: projectPath }),
expect.objectContaining({
args: [expect.any(String), 'fmt', '--migrate=prettier'],
cwd: projectPath,
}),
);
expect(fs.readFileSync(path.join(projectPath, configFile), 'utf8')).toBe(config);
expect(fs.readFileSync(path.join(projectPath, 'package.json'), 'utf8')).toBe(
Expand Down
16 changes: 14 additions & 2 deletions packages/cli/src/migration/__tests__/tsup.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ vi.mock('@voidzero-dev/vite-plus-prompts', () => ({
},
}));

vi.mock('../../utils/command.ts', () => ({
vi.mock(import('../../utils/command.ts'), async (importOriginal) => ({
...(await importOriginal()),
runCommandSilently: vi.fn(),
}));
vi.mock('../../utils/prompts.ts', () => ({
Expand Down Expand Up @@ -62,6 +63,7 @@ describe('tsup migration', () => {
let projectPath: string;

beforeEach(() => {
vi.stubEnv('VP_CLI_BIN', undefined);
projectPath = fs.mkdtempSync(path.join(os.tmpdir(), 'vp-test-tsup-'));
fs.writeFileSync(
path.join(projectPath, 'package.json'),
Expand All @@ -80,6 +82,7 @@ describe('tsup migration', () => {
});

afterEach(() => {
vi.unstubAllEnvs();
fs.rmSync(projectPath, { recursive: true, force: true });
mockRunCommandSilently.mockReset();
mockConfirm.mockReset();
Expand All @@ -96,7 +99,16 @@ describe('tsup migration', () => {

expect(mockRunCommandSilently).toHaveBeenCalledWith(
expect.objectContaining({
args: ['dlx', 'tsdown-migrate@0.99.0', '--yes', '--package-manager', 'npm', '--no-install'],
command: process.execPath,
args: [
expect.any(String),
'dlx',
'tsdown-migrate@0.99.0',
'--yes',
'--package-manager',
'npm',
'--no-install',
],
}),
);
});
Expand Down
9 changes: 2 additions & 7 deletions packages/cli/src/migration/migrator/eslint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { compareReversed, isLessThanOrEqual, normalize } from 'verkit';

import { ResolverFactory, rewriteEslint } from '../../../binding/index.js';
import { type WorkspacePackage } from '../../types/index.ts';
import { runCommandSilently } from '../../utils/command.ts';
import { getVpCommand, runCommandSilently } from '../../utils/command.ts';
import { editJsonFile, isJsonFile, readJsonFile } from '../../utils/json.ts';
import { fetchNpmResource, getNpmRegistry } from '../../utils/npm-config.ts';
import { displayRelative } from '../../utils/path.ts';
Expand Down Expand Up @@ -180,7 +180,6 @@ export async function resolveOxlintMigrateVersion(oxlintVersion: string): Promis
* Returns true on success, false on failure (spawn error or non-zero exit).
*/
async function runOxlintMigrateStep(
vpBin: string,
cwd: string,
migratePackage: string,
args: string[],
Expand All @@ -190,8 +189,7 @@ async function runOxlintMigrateStep(
): Promise<{ ok: boolean; skippedRules?: string }> {
try {
const result = await runCommandSilently({
command: vpBin,
args: ['dlx', migratePackage, ...args],
...getVpCommand(['dlx', migratePackage, ...args]),
cwd,
envs: process.env,
});
Expand Down Expand Up @@ -219,7 +217,6 @@ export async function migrateEslintToOxlint(
packages?: WorkspacePackage[],
options?: { silent?: boolean; report?: MigrationReport },
): Promise<boolean> {
const vpBin = process.env.VP_CLI_BIN ?? 'vp';
const spinner = options?.silent ? getSilentSpinner() : getSpinner(interactive);

// Steps 1-2: Only run @oxlint/migrate if there's an eslint config at root
Expand All @@ -240,7 +237,6 @@ export async function migrateEslintToOxlint(
// Step 1: Generate .oxlintrc.json from ESLint config
spinner.start('Migrating ESLint config to Oxlint...');
const migrateResult = await runOxlintMigrateStep(
vpBin,
projectPath,
migratePackage,
migrateArgs,
Expand All @@ -259,7 +255,6 @@ export async function migrateEslintToOxlint(
// Step 2: Replace eslint-disable comments with oxlint-disable
spinner.start('Replacing ESLint comments with Oxlint equivalents...');
const replaceResult = await runOxlintMigrateStep(
vpBin,
projectPath,
migratePackage,
['--replace-eslint-comments'],
Expand Down
6 changes: 3 additions & 3 deletions packages/cli/src/migration/migrator/git-hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
SUPPORTED_GIT_HOOK_NAMES,
} from '../../config/hooks.ts';
import type { PackageManager, WorkspacePackage } from '../../types/index.ts';
import { getVpCommand } from '../../utils/command.ts';
import { documentationUrl } from '../../utils/documentation.ts';
import { findGitRoot } from '../../utils/git.ts';
import { editJsonFile, isJsonFile, readJsonFile } from '../../utils/json.ts';
Expand Down Expand Up @@ -460,10 +461,9 @@ export function installGitHooks(
return true;
}

const vpBin = process.env.VP_CLI_BIN ?? 'vp';

// Install git hooks via vp config (--no-agent to skip agent setup, handled by migration)
const configResult = spawn.sync(vpBin, ['config', '--no-agent'], {
const { command, args } = getVpCommand(['config', '--no-agent']);
const configResult = spawn.sync(command, args, {
cwd: projectPath,
stdio: 'pipe',
});
Expand Down
8 changes: 2 additions & 6 deletions packages/cli/src/migration/migrator/prettier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import * as prompts from '@voidzero-dev/vite-plus-prompts';

import { rewritePrettier } from '../../../binding/index.js';
import { type WorkspacePackage } from '../../types/index.ts';
import { runCommandSilently } from '../../utils/command.ts';
import { getVpCommand, runCommandSilently } from '../../utils/command.ts';
import { editJsonFile, readJsonFile } from '../../utils/json.ts';
import { displayRelative } from '../../utils/path.ts';
import { cancelAndExit } from '../../utils/prompts.ts';
Expand Down Expand Up @@ -61,16 +61,14 @@ export function detectPrettierProject(
* Unsupported config formats are skipped; other errors still fail migration.
*/
async function runPrettierMigrateStep(
vpBin: string,
cwd: string,
spinner: ReturnType<typeof getSpinner>,
failMessage: string,
manualHint: string,
): Promise<'migrated' | 'skipped' | 'failed'> {
try {
const result = await runCommandSilently({
command: vpBin,
args: ['fmt', '--migrate=prettier'],
...getVpCommand(['fmt', '--migrate=prettier']),
cwd,
envs: process.env,
});
Expand Down Expand Up @@ -109,7 +107,6 @@ export async function migratePrettierToOxfmt(
packages?: WorkspacePackage[],
options?: { silent?: boolean; report?: MigrationReport },
): Promise<'migrated' | 'skipped' | 'failed'> {
const vpBin = process.env.VP_CLI_BIN ?? 'vp';
const spinner = options?.silent ? getSilentSpinner() : getSpinner(interactive);

// Step 1: Generate .oxfmtrc.json from Prettier config
Expand All @@ -133,7 +130,6 @@ export async function migratePrettierToOxfmt(
try {
spinner.start('Migrating Prettier config to Oxfmt...');
const result = await runPrettierMigrateStep(
vpBin,
projectPath,
spinner,
'Prettier migration failed',
Expand Down
11 changes: 4 additions & 7 deletions packages/cli/src/migration/migrator/tsup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import * as prompts from '@voidzero-dev/vite-plus-prompts';
import corePkg from 'vite/package.json' with { type: 'json' };

import { PackageManager, type WorkspacePackage } from '../../types/index.ts';
import { runCommandSilently } from '../../utils/command.ts';
import { getVpCommand, runCommandSilently } from '../../utils/command.ts';
import { TSDOWN_MIGRATION_SKILL_URL } from '../../utils/constants.ts';
import { editJsonFile, readJsonFile } from '../../utils/json.ts';
import { displayRelative } from '../../utils/path.ts';
Expand Down Expand Up @@ -372,21 +372,19 @@ function extractTsdownMigrationWarnings(output: Buffer): string[] {

/** Run `vp dlx tsdown-migrate` in `cwd` with graceful error handling. */
async function runTsdownMigrateStep(
vpBin: string,
cwd: string,
packageManager: PackageManager,
): Promise<{ ok: boolean; warnings: string[] }> {
try {
const result = await runCommandSilently({
command: vpBin,
args: [
...getVpCommand([
'dlx',
`tsdown-migrate@${corePkg.bundledVersions.tsdown}`,
'--yes',
'--package-manager',
packageManager,
'--no-install',
],
]),
cwd,
envs: process.env,
});
Expand Down Expand Up @@ -417,7 +415,6 @@ export async function migrateTsupToTsdown(
packages?: WorkspacePackage[],
options?: { silent?: boolean; report?: MigrationReport },
): Promise<boolean> {
const vpBin = process.env.VP_CLI_BIN ?? 'vp';
const spinner = options?.silent ? getSilentSpinner() : getSpinner(interactive);

// A tsup config isn't necessarily workspace-wide the way an ESLint flat
Expand Down Expand Up @@ -491,7 +488,7 @@ export async function migrateTsupToTsdown(
for (const target of tsupTargets) {
const targetLabel =
target === rootPath ? 'the project root' : displayRelative(target, projectPath);
const migrateResult = await runTsdownMigrateStep(vpBin, target, packageManager);
const migrateResult = await runTsdownMigrateStep(target, packageManager);
if (!migrateResult.ok) {
spinner.stop();
const restoreFailures = restoreTsupMigrationTargets(snapshots);
Expand Down
Loading
Loading