Skip to content
Open
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,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');
Original file line number Diff line number Diff line change
@@ -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"]]
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# create_managed_npm

## `node verify.cjs npm managed`

```
npm: managed version pinned
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# create_missing_system_pnpm

## `node verify.cjs pnpm missing`

```
pnpm: managed version pinned
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# create_system_bun

## `node verify.cjs bun system`

```
bun: system version pinned
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# create_system_bun

## `node verify.cjs bun system`

```
bun: system version pinned
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# create_system_npm

## `node verify.cjs npm system`

```
npm: system version pinned
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# create_system_npm

## `node verify.cjs npm system`

```
npm: system version pinned
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# create_system_npm_install

## `node install.cjs`

```
System npm version pinned and dependencies installed successfully
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# create_system_pnpm

## `node verify.cjs pnpm system`

```
pnpm: system version pinned
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# create_system_pnpm

## `node verify.cjs pnpm system`

```
pnpm: system version pinned
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# create_system_yarn

## `node verify.cjs yarn system`

```
yarn: system version pinned
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# create_system_yarn

## `node verify.cjs yarn system`

```
yarn: system version pinned
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# create_system_yarn_classic

## `node verify.cjs yarn system classic`

```
yarn: system version pinned
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# create_system_yarn_classic

## `node verify.cjs yarn system classic`

```
yarn: system version pinned
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# create_system_yarn_classic_external

## `node verify.cjs yarn system external`

```
yarn: external template generated with npx
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# create_system_yarn_classic_external

## `node verify.cjs yarn system external`

```
yarn: external template generated with npx
```
Original file line number Diff line number Diff line change
@@ -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'}`);
31 changes: 30 additions & 1 deletion packages/cli/src/create/__tests__/command.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
}));
Expand All @@ -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[] = [];

Expand Down Expand Up @@ -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'],
});
});
});
61 changes: 61 additions & 0 deletions packages/cli/src/create/__tests__/package-manager.spec.ts
Original file line number Diff line number Diff line change
@@ -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'),
}),
);
});
});
Loading
Loading