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
52 changes: 46 additions & 6 deletions src/commands/__tests__/createApp.test.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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"');
Expand All @@ -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"');
Expand All @@ -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
Expand All @@ -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"');
Expand All @@ -70,7 +72,7 @@ test('exits if app name does not start with a letter', async () => {
process.exit = vi.fn<typeof process.exit>();
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.'),
Expand Down Expand Up @@ -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');
Expand Down
2 changes: 1 addition & 1 deletion src/commands/createApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
34 changes: 14 additions & 20 deletions src/util/getUserPackageManager.ts
Original file line number Diff line number Diff line change
@@ -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<PackageManager> {
return select({
message: 'What package manager would you like Belt to use?',
choices: PACKAGE_MANAGER_CHOICES.map((packageManager) => ({
value: packageManager,
})),
});
}