diff --git a/src/commands/__tests__/createApp.test.ts b/src/commands/__tests__/createApp.test.ts index 75c10d0..3991b38 100644 --- a/src/commands/__tests__/createApp.test.ts +++ b/src/commands/__tests__/createApp.test.ts @@ -1,4 +1,4 @@ -import { confirm, input } from '@inquirer/prompts'; +import { confirm, input, select } from '@inquirer/prompts'; import { fs, vol } from 'memfs'; import { Mock, afterEach, describe, expect, test, vi } from 'vitest'; import exec from '../../util/exec'; @@ -8,19 +8,21 @@ import { createApp } from '../createApp'; vi.mock('@inquirer/prompts', () => ({ input: vi.fn(), confirm: vi.fn(), + select: vi.fn(), })); vi.mock('../../util/addDependency'); vi.mock('../../util/print', () => ({ default: vi.fn() })); afterEach(() => { vol.reset(); + vi.clearAllMocks(); (print as Mock).mockReset(); }); test('creates app, substituting the app name where appropriate', async () => { (confirm as Mock).mockResolvedValueOnce(true); vol.fromJSON({ 'file.txt': '{}' }, './'); - await createApp('MyApp'); + await createApp('MyApp', { yarn: true }); expectFileContents('MyApp/package.json', '"name": "my-app"'); expectFileContents('MyApp/app.json', '"name": "MyApp"'); @@ -31,7 +33,7 @@ test('creates app, substituting the app name where appropriate', async () => { test('prompts for app name if not supplied', async () => { (confirm as Mock).mockResolvedValueOnce(true); (input as Mock).mockReturnValue('MyApp'); - await createApp(undefined); + await createApp(undefined, { yarn: true }); expectFileContents('MyApp/package.json', '"name": "my-app"'); expectFileContents('MyApp/app.json', '"name": "MyApp"'); @@ -46,7 +48,7 @@ test('exits if directory already exists', async () => { vol.fromJSON({ 'MyApp/package.json': '{}' }, './'); - await createApp('my-app'); // gets sanitized to MyApp + await createApp('my-app', { yarn: true }); // gets sanitized to MyApp expect(print).toHaveBeenCalledWith(expect.stringMatching(/already exists/)); // eslint-disable-next-line @typescript-eslint/unbound-method @@ -56,7 +58,7 @@ test('exits if directory already exists', async () => { test('converts directory to camel case and strips special characters', async () => { (confirm as Mock).mockResolvedValueOnce(true); vol.fromJSON({ 'file.txt': '{}' }, './'); - await createApp('my-$%-app'); + await createApp('my-$%-app', { yarn: true }); expectFileContents('MyApp/package.json', '"name": "my-app"'); expectFileContents('MyApp/app.json', '"name": "MyApp"'); @@ -70,7 +72,7 @@ test('exits if app name does not start with a letter', async () => { process.exit = vi.fn(); vol.fromJSON({ 'MyApp/package.json': '{}' }, './'); - await createApp('555MyApp'); + await createApp('555MyApp', { yarn: true }); expect(print).toHaveBeenCalledWith( expect.stringMatching('App name must start with a letter.'), @@ -99,6 +101,44 @@ describe('package manager options', () => { }); }); +describe('user preferred package manager prompt', () => { + test('prompts user for preferred package manager when none was provided', async () => { + (confirm as Mock).mockResolvedValueOnce(true); + await createApp('MyApp'); + + expect(select).toHaveBeenCalledWith( + expect.objectContaining({ + message: expect.stringMatching( + 'What package manager would you like Belt to use?', + ) as string, + }), + ); + }); + + test('allows user select a preferred package manager from list', async () => { + (confirm as Mock).mockResolvedValueOnce(true); + (select as Mock).mockResolvedValueOnce('pnpm'); + await createApp('MyApp'); + + expect(exec).toHaveBeenCalledWith('pnpm install'); + }); + + test('user prompt for preferred package manager has correct choices', async () => { + (confirm as Mock).mockResolvedValueOnce(true); + await createApp('MyApp'); + expect(select).toHaveBeenCalledWith( + expect.objectContaining({ + choices: expect.arrayContaining([ + expect.objectContaining({ value: 'npm' }), + expect.objectContaining({ value: 'pnpm' }), + expect.objectContaining({ value: 'yarn' }), + expect.objectContaining({ value: 'bun' }), + ]) as Array<{ value: string }>, + }), + ); + }); +}); + function expectFileContents(file: string, expected: string) { try { const contents = fs.readFileSync(file, 'utf8'); diff --git a/src/commands/createApp.ts b/src/commands/createApp.ts index f484934..950714d 100644 --- a/src/commands/createApp.ts +++ b/src/commands/createApp.ts @@ -55,9 +55,9 @@ export async function createApp( spinner.succeed('Created new Belt app with Expo'); process.chdir(`./${appName}`); + const packageManager = await getPackageManager(options); spinner.start('Installing dependencies'); - const packageManager = getPackageManager(options); await exec(`${packageManager} install`); await exec('git init'); await commit('Initial commit'); diff --git a/src/util/getUserPackageManager.ts b/src/util/getUserPackageManager.ts index ab56add..56e775b 100644 --- a/src/util/getUserPackageManager.ts +++ b/src/util/getUserPackageManager.ts @@ -1,24 +1,18 @@ -export type PackageManager = 'npm' | 'pnpm' | 'yarn' | 'bun'; +import { select } from '@inquirer/prompts'; -/** - * attempts to detect the runtime / package manager that was used to - * run the current process - */ -export default function getUserPackageManager(): PackageManager { - // This environment variable is set by npm and yarn but pnpm seems less consistent - const userAgent = process.env.npm_config_user_agent; +const PACKAGE_MANAGER_CHOICES = ['npm', 'pnpm', 'yarn', 'bun'] as const; - if (userAgent?.startsWith('yarn')) { - return 'yarn'; - } - if (userAgent?.startsWith('pnpm')) { - return 'pnpm'; - } - // bun sets Bun.env if running in Bun process. userAgent bit doesn't seem to work - if (userAgent?.startsWith('bun') || typeof Bun !== 'undefined') { - return 'bun'; - } +export type PackageManager = (typeof PACKAGE_MANAGER_CHOICES)[number]; - // If no user agent is set, assume npm - return 'npm'; +/** + * requests the user to select their preferred package manager from the + * provided list + */ +export default async function getUserPackageManager(): Promise { + return select({ + message: 'What package manager would you like Belt to use?', + choices: PACKAGE_MANAGER_CHOICES.map((packageManager) => ({ + value: packageManager, + })), + }); }