Skip to content

Commit

Permalink
Add default 'create' command to create new app (#16)
Browse files Browse the repository at this point in the history
* Add default 'create' command to create new app

* Print welcome messages, start test
  • Loading branch information
Stephen Hanson authored Nov 3, 2023
1 parent 1a458e3 commit 6f539ce
Show file tree
Hide file tree
Showing 22 changed files with 379 additions and 149 deletions.
5 changes: 5 additions & 0 deletions __mocks__/child_process.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { vi } from 'vitest';

export const spawnSync = vi.fn();
export const execSync = vi.fn();
export const exec = vi.fn();
3 changes: 3 additions & 0 deletions __mocks__/fs-extra.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import actualfs from 'fs-extra';
import { fs } from 'memfs';
import { vi } from 'vitest';

const DONT_MOCK_PATTERNS = ['templates/'];

export default {
...fs.promises,
// todo: build actual mock
copySync: vi.fn(),
exists(path) {
return new Promise((resolve) => {
fs.exists(path, (exists) => resolve(exists));
Expand Down
3 changes: 3 additions & 0 deletions __mocks__/process.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default {
chdir: vi.fn(),
};
20 changes: 11 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
"lint:prettier": "prettier --check '**/*' --ignore-unknown",
"lint:types": "tsc",
"fix:prettier": "prettier --write '**/*' --ignore-unknown",
"test": "vitest watch",
"test:run": "test run",
"test": "vitest",
"test:run": "vitest run",
"test:all": "yarn lint && yarn test:run"
},
"bin": {
Expand All @@ -21,13 +21,13 @@
"author": "thoughtbot, Inc.",
"license": "MIT",
"dependencies": {
"@inquirer/prompts": "^3.2.0",
"@thoughtbot/eslint-config": "^1.0.2",
"chalk": "^5.2.0",
"commander": "^10.0.1",
"eslint": "^8.45.0",
"eta": "^2.1.1",
"fs-extra": "^11.1.1",
"inquirer": "^9.2.0",
"ora": "^6.3.0",
"prettier": "^3.0.1",
"ts-node": "^10.9.1"
Expand All @@ -47,14 +47,16 @@
"@thoughtbot/eslint-config/prettier",
"@thoughtbot/eslint-config/typescript"
],
"rules": {
"no-console": "off",
"import/order": "off"
},
"ignorePatterns": [
"src/commands/templates",
"__mocks__/**/*.js",
"bin/belt.js",
"/build"
]
"/build",
"vitest.setup.js"
],
"rules": {
"no-console": "off",
"import/order": "off"
}
}
}
16 changes: 15 additions & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,26 @@
import { program } from 'commander';
import buildAction from './util/buildAction';
import printWelcome from './util/print/printWelcome';

export default function runCli() {
program
.name('thoughtbelt')
.description(
'Perform React Native and Expo setup and redundant tasks without your pants falling down!',
)
.showHelpAfterError();

program
.command('create', { isDefault: true })
.description('Create new app')
.argument(
'[appName]',
'The name of the app and directory it will be created in',
'',
)
.action(buildAction(import('./commands/createApp')));

program
.command('eslint')
.description('Configure ESLint')
.action(buildAction(import('./commands/eslint')));
Expand All @@ -22,7 +35,8 @@ export default function runCli() {
.description('Install and configure TypeScript')
.action(buildAction(import('./commands/typescript')));

program.showHelpAfterError().parse();
printWelcome();
program.parse();
}

runCli();
27 changes: 27 additions & 0 deletions src/commands/__tests__/createApp.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { confirm } from '@inquirer/prompts';
import { vol } from 'memfs';
import { Mock, afterEach, test, vi } from 'vitest';
import print from '../../util/print';
import { createApp } from '../createApp';

vi.mock('@inquirer/prompts', () => ({
confirm: vi.fn(),
}));
vi.mock('../../util/addDependency');
vi.mock('../../util/print', () => ({ default: vi.fn() }));

afterEach(() => {
vol.reset();
(print as Mock).mockReset();
});

test("doesn't error", async () => {
(confirm as Mock).mockResolvedValueOnce(true);
vi.spyOn(process, 'chdir').mockImplementation(() => {
const json = {
'package.json': '{ "dependencies": {}, "devDependencies": {} }',
};
vol.fromJSON(json, './');
});
await createApp('MyApp');
});
6 changes: 1 addition & 5 deletions src/commands/__tests__/typescript.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,8 @@ import addDependency from '../../util/addDependency';
import print from '../../util/print';
import addTypescript from '../typescript';

vi.mock('fs-extra');
vi.mock('../../util/addDependency');
vi.mock('../../util/print', () => ({
// __esModule: true,
default: vi.fn(),
}));
vi.mock('../../util/print', () => ({ default: vi.fn() }));

afterEach(() => {
vol.reset();
Expand Down
60 changes: 60 additions & 0 deletions src/commands/createApp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { confirm, input } from '@inquirer/prompts';
import { execSync, spawnSync } from 'child_process';
import print from '../util/print';
import addEslint from './eslint';
import createScaffold from './scaffold';
import addTypescript from './typescript';

export async function createApp(name: string | undefined) {
const appName = name || (await getAppName());
await printIntro();

spawnSync('npx', ['--yes', 'create-expo-app@latest', appName], {
stdio: 'inherit',
});

process.chdir(`./${appName}`);

await addTypescript();
execSync('git add .');
execSync('git commit -m "Add TypeScript"');

await addEslint();
execSync('git add .');
execSync('git commit -m "Configure ESLint"');

await createScaffold();
execSync('git add .');
execSync('git commit -m "Add app scaffold"');
}

async function getAppName() {
return input({ message: 'What is the name of your app?' });
}

/**
* Commander requires this signature to be ...args: unknown[]
* Actual args are:
* ([<appName>, <Options hash>, <Command>])
* or ([<Options hash>, <Command>]) if <appName> not passed)
*/
export default function createAppAction(...args: unknown[]) {
// if argument ommitted, args[0] is options
const appNameArg = (args[0] as string[])[0];
return createApp(appNameArg);
}

async function printIntro() {
print('👖 Let’s get started!');
print(`\nWe will now perform the following tasks:
- Create a new app using the latest create-expo-app
- Add and configure TypeScript
- Add and configure ESLint
- Add and configure Prettier
- Create the project directory structure
`);

if (!(await confirm({ message: 'Ready to proceed?' }))) {
process.exit(0);
}
}
12 changes: 6 additions & 6 deletions src/commands/eslint.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
import { log } from 'console';
import path from 'path';
import { fileURLToPath, URL } from 'url';
import chalk from 'chalk';
import * as eta from 'eta';
import fs from 'fs-extra';
import path from 'path';
import { fileURLToPath, URL } from 'url';
import addDependency from '../util/addDependency';
import getProjectDir from '../util/getProjectDir';
import isEslintConfigured from '../util/isEslintConfigured';
import isPackageInstalled from '../util/isPackageInstalled';
import print from '../util/print';
import writeFile from '../util/writeFile';

const dirname = fileURLToPath(new URL('.', import.meta.url));

export default async function runEslint() {
export default async function addEslint() {
const projectDir = await getProjectDir();

if (await isEslintConfigured()) {
log('eslint config already exists');
print('eslint config already exists');
} else {
const hasTypeScript = await isPackageInstalled('typescript');

Expand All @@ -34,6 +34,6 @@ export default async function runEslint() {

await addDependency('@thoughtbot/eslint-config', { dev: true });

log(chalk.green('🎉 ESLint successfully configured'));
print(chalk.green('🎉 ESLint successfully configured'));
}
}
20 changes: 20 additions & 0 deletions src/commands/scaffold.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';
import { URL, fileURLToPath } from 'url';
import print from '../util/print';

const dirname = fileURLToPath(new URL('.', import.meta.url));

export default async function createScaffold() {
print(chalk.bold('👖 Creating directory structure'));
print(`
src/
components/
util/
hooks/
test/
`);
fs.copySync(path.join(dirname, 'templates', 'scaffold', 'src'), './src');
print('✅ Created directories');
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {
StyleSheet,
TouchableOpacity,
TouchableOpacityProps,
} from 'react-native';

type ButtonProps = TouchableOpacityProps;

export default function PrimaryButton({
style,
text,
textStyle,
children,
...props
}: ButtonProps) {
return (
<TouchableOpacity
activeOpacity={0.6}
style={[styles.button, style]}
{...props}
/>
);
}

const styles = StyleSheet.create({
button: {
paddingVertical: 16,
paddingHorizontal: 14,
backgroundColor: '#08f',
borderRadius: 12,
justifyContent: 'center',
},
});
Empty file.
Empty file.
Empty file.
6 changes: 3 additions & 3 deletions src/util/addDependency.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { exec } from 'child_process';
import { execSync } from 'child_process';
import fs from 'fs-extra';
import * as path from 'path';
import getProjectDir from './getProjectDir';
Expand All @@ -7,8 +7,8 @@ export default async function addDependency(deps: string, { dev = false }) {
const isYarn = await fs.exists(path.join(await getProjectDir(), 'yarn.lock'));

if (isYarn) {
exec(`yarn add ${dev ? '--dev' : ''} ${deps}`);
execSync(`yarn add ${dev ? '--dev' : ''} ${deps}`);
} else {
exec(`npm install ${dev ? '--save-dev' : '--save'} ${deps}`);
execSync(`npm install ${dev ? '--save-dev' : '--save'} ${deps}`);
}
}
4 changes: 2 additions & 2 deletions src/util/formatFile.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { exec } from 'child_process';
import { execSync } from 'child_process';

export default async function formatFile(filePath: string) {
exec(`npx prettier --write '${filePath}'`);
execSync(`npx prettier --write '${filePath}'`);
}
8 changes: 8 additions & 0 deletions src/util/print/__tests__/printWelcome.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { test, vi } from 'vitest';
import printWelcome from '../printWelcome';

vi.mock('../../print', () => ({ default: vi.fn() }));

test('doesnt error', () => {
printWelcome();
});
9 changes: 9 additions & 0 deletions src/util/print/printWelcome.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import chalk from 'chalk';
import print from '../print';

export default function printWelcome() {
print(chalk.bold('\n\n\t👖 thoughtbelt 👖\n'));
print(
'Perform project setup and redundant tasks\n without your pants falling down!\n\n',
);
}
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@
"esm": true,
"experimentalSpecifierResolution": "node"
},
"include": ["./**/*.ts"]
"include": ["./**/*.ts"],
"exclude": ["./src/commands/templates/**/*"]
}
1 change: 1 addition & 0 deletions vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ import { configDefaults, defineConfig } from 'vitest/config';
export default defineConfig({
test: {
exclude: [...configDefaults.exclude, 'build/**/*'],
setupFiles: ['./vitest.setup.js'],
},
});
4 changes: 4 additions & 0 deletions vitest.setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { vi } from 'vitest';

vi.mock('child_process');
vi.mock('fs-extra');
Loading

0 comments on commit 6f539ce

Please sign in to comment.