diff --git a/packages/cli/src/create/__tests__/command.spec.ts b/packages/cli/src/create/__tests__/command.spec.ts new file mode 100644 index 0000000000..89c9fdb05e --- /dev/null +++ b/packages/cli/src/create/__tests__/command.spec.ts @@ -0,0 +1,100 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +const { mockRunCommand } = vi.hoisted(() => ({ + mockRunCommand: vi.fn(), +})); + +vi.mock('../../../binding/index.js', () => ({ + runCommand: mockRunCommand, +})); + +const { runCommandAndDetectProjectDir } = await import('../command.js'); + +const tempDirs: string[] = []; + +function makeTempDir() { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'vp-create-command-')); + tempDirs.push(dir); + return dir; +} + +describe('runCommandAndDetectProjectDir', () => { + beforeEach(() => { + mockRunCommand.mockReset(); + }); + + afterEach(() => { + for (const dir of tempDirs.splice(0)) { + fs.rmSync(dir, { recursive: true, force: true }); + } + }); + + it('detects a project created in the current directory', async () => { + mockRunCommand.mockResolvedValueOnce({ + exitCode: 0, + pathAccesses: { + 'package.json': { write: true }, + }, + }); + + const result = await runCommandAndDetectProjectDir({ + command: 'node', + args: ['create.js'], + cwd: '/tmp/workspace', + envs: {}, + }); + + expect(result).toEqual({ exitCode: 0, projectDir: '.' }); + }); + + it('prefers a generated child directory over the current directory', async () => { + mockRunCommand.mockResolvedValueOnce({ + exitCode: 0, + pathAccesses: { + 'package.json': { write: true }, + 'my-app/package.json': { write: true }, + }, + }); + + const result = await runCommandAndDetectProjectDir({ + command: 'node', + args: ['create.js'], + cwd: '/tmp/workspace', + envs: {}, + }); + + expect(result).toEqual({ exitCode: 0, projectDir: 'my-app' }); + }); + + it('returns the parent directory when the project is created at that parent root', async () => { + const cwd = makeTempDir(); + mockRunCommand.mockResolvedValueOnce({ + exitCode: 0, + pathAccesses: { + 'package.json': { write: true }, + }, + }); + + const result = await runCommandAndDetectProjectDir( + { + command: 'node', + args: ['create.js'], + cwd, + envs: {}, + }, + 'apps', + ); + + expect(result).toEqual({ exitCode: 0, projectDir: 'apps' }); + expect(mockRunCommand).toHaveBeenCalledWith({ + binName: 'node', + args: ['create.js'], + envs: {}, + cwd: path.join(cwd, 'apps'), + }); + }); +}); diff --git a/packages/cli/src/create/command.ts b/packages/cli/src/create/command.ts index 0083c60b55..778d84f2af 100644 --- a/packages/cli/src/create/command.ts +++ b/packages/cli/src/create/command.ts @@ -38,6 +38,7 @@ export async function runCommandAndDetectProjectDir( // Detect project directory from path accesses // Find the closest directory containing package.json relative to cwd let projectDir: string | undefined; + let currentDirPackageJsonWritten = false; let minDepth = Infinity; for (const [filePath, pathAccess] of Object.entries(result.pathAccesses)) { @@ -50,8 +51,9 @@ export async function runCommandAndDetectProjectDir( // Extract directory from package.json path const dir = path.dirname(filePath); - // Skip if it's the current directory + // Defer current directory until after checking for a generated child directory. if (dir === '.' || dir === '') { + currentDirPackageJsonWritten = true; continue; } // Skip if this is an existing directory (created before the command ran) @@ -70,6 +72,10 @@ export async function runCommandAndDetectProjectDir( } } + if (!projectDir && currentDirPackageJsonWritten) { + projectDir = '.'; + } + // If parentDir is provided, join it with the project directory if (parentDir && projectDir) { projectDir = path.join(parentDir, projectDir);