Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Testing library command, heavily refactor #18

Merged
merged 3 commits into from
Nov 10, 2023
Merged
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
66 changes: 54 additions & 12 deletions __mocks__/fs-extra.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,70 @@
import actualfs from 'fs-extra';
import { fs } from 'memfs';
import { vi } from 'vitest';
import path from 'path';

const DONT_MOCK_PATTERNS = ['templates/'];
let DONT_MOCK_PATTERNS = ['templates/'];

// Most of this CLI uses fs-extra for file-system operations
// This mock mocks this module to instead use memfs, while also allowing
// read operations to read from 'templates' so our tests do not need to
// mock templates
export default {
// allow mocking templates with memfs (normally tests read these from regular FS)
mockTemplates() {
DONT_MOCK_PATTERNS = [];
},
...fs.promises,
// todo: build actual mock
copySync: vi.fn(),
exists(path) {
return new Promise((resolve) => {
fs.exists(path, (exists) => resolve(exists));
});
},
readFile: (...args) => {
const path = args[0];
const useActual = DONT_MOCK_PATTERNS.some((pattern) =>
path.includes(pattern),
);
isDirectory(src) {
return fs.statSync(src).isDirectory(src);
},
// currently having to manually copy the sync methods over, there's prob a better way
rmSync: fs.rmSync,
readFileSync(file, options) {
if (dontMock(file)) {
return actualfs.readFileSync(file, options);
}

if (useActual) {
return actualfs.readFile(...args);
return fs.readFileSync(file, options);
},
writeFileSync: fs.writeFileSync,
existsSync: fs.existsSync,
appendFileSync: fs.appendFileSync,
readdirSync(path, options) {
if (dontMock(path)) {
return actualfs.readdirSync(path, options);
}

return fs.promises.readFile(...args);
return fs.readdirSync(path, options);
},
copySync(src, dest) {
const sourceFS = dontMock(src) ? actualfs : fs;

if (sourceFS.existsSync(src) && sourceFS.statSync(src).isDirectory(src)) {
fs.mkdirSync(dest, { recursive: true });
sourceFS.readdirSync(src).forEach((childItemName) => {
this.copySync(
path.join(src, childItemName),
path.join(dest, childItemName),
);
});
} else {
fs.writeFileSync(dest, sourceFS.readFileSync(src, 'utf-8'));
}
},
readFile: (path, ...args) => {
if (dontMock(path)) {
return actualfs.readFile(path, ...args);
}

return fs.promises.readFile(path, ...args);
},
};

function dontMock(src) {
return DONT_MOCK_PATTERNS.some((pattern) => src.includes(pattern));
}
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
"@types/fs-extra": "^11.0.3",
"@types/node": "^18.16.3",
"@types/react": "^18.2.6",
"memfs": "^4.2.0",
"memfs": "^4.5.1",
"npm-run-all": "^4.1.5",
"tsup": "^7.2.0",
"typescript": "^5.0.4",
Expand All @@ -71,7 +71,8 @@
],
"rules": {
"no-console": "off",
"import/order": "off"
"import/order": "off",
"no-continue": "off"
}
}
}
10 changes: 10 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ export default function runCli() {
'The name of the app and directory it will be created in',
'',
)
// todo: remove this option, since we will always end up installing during creation
.option(
'--no-testing',
'Pass true to skip installing Jest and React Native Testing Library',
)
.action(buildAction(import('./commands/createApp')));

program
Expand All @@ -35,6 +40,11 @@ export default function runCli() {
.description('Install and configure TypeScript')
.action(buildAction(import('./commands/typescript')));

program
.command('testing')
.description('Install and configure Jest and Testing Library')
.action(buildAction(import('./commands/testingLibrary')));

printWelcome();
program.parse();
}
9 changes: 7 additions & 2 deletions src/commands/__tests__/createApp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,14 @@ test("doesn't error", async () => {
(confirm as Mock).mockResolvedValueOnce(true);
vi.spyOn(process, 'chdir').mockImplementation(() => {
const json = {
'package.json': '{ "dependencies": {}, "devDependencies": {} }',
'package.json': JSON.stringify({
scripts: {},
dependencies: {},
devDependencies: {},
}),
'yarn.lock': '',
};
vol.fromJSON(json, './');
});
await createApp('MyApp');
await createApp('MyApp', { testing: true });
});
26 changes: 26 additions & 0 deletions src/commands/__tests__/testingLibrary.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { vol } from 'memfs';
import { expect, test, vi } from 'vitest';
import addDependency from '../../util/addDependency';
import addTestingLibrary from '../testingLibrary';

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

test('installs Testing Library', async () => {
vol.fromJSON({
'package.json': JSON.stringify({
scripts: {},
dependencies: {
expo: '1.0.0',
},
devDependencies: {},
}),
'yarn.lock': '',
});

await addTestingLibrary();

expect(addDependency).toHaveBeenCalled();
});
9 changes: 7 additions & 2 deletions src/commands/__tests__/typescript.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ afterEach(() => {

test('exits with message if tsconfig.json already exists', async () => {
const json = {
'package.json': '{}',
'package.json': JSON.stringify({
scripts: {},
dependencies: {},
}),
'tsconfig.json': '1',
};
vol.fromJSON(json, './');
Expand All @@ -31,6 +34,7 @@ test('exits with message if tsconfig.json already exists', async () => {
test('writes new tsconfig.json, adds dependencies', async () => {
vol.fromJSON({
'package.json': JSON.stringify({
scripts: {},
dependencies: {
expo: '1.0.0',
},
Expand All @@ -44,7 +48,7 @@ test('writes new tsconfig.json, adds dependencies', async () => {
});

expect(fs.readFileSync('tsconfig.json', 'utf8')).toMatch(
'"extends": "expo/tsconfig.base"',
'"compilerOptions": {',
);

expect(print).not.toHaveBeenCalledWith(
Expand All @@ -55,6 +59,7 @@ test('writes new tsconfig.json, adds dependencies', async () => {
test("doesn't extend expo/tsconfig.base if not an Expo project", async () => {
vol.fromJSON({
'package.json': JSON.stringify({
scripts: {},
dependencies: {},
}),
});
Expand Down
57 changes: 50 additions & 7 deletions src/commands/createApp.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
import { confirm, input } from '@inquirer/prompts';
import { execSync, spawnSync } from 'child_process';
import addDependency from '../util/addDependency';
import addPackageJsonScripts from '../util/addPackageJsonScripts';
import print from '../util/print';
import addEslint from './eslint';
import addPrettier from './prettier';
import createScaffold from './scaffold';
import addTestingLibrary from './testingLibrary';
import addTypescript from './typescript';

export async function createApp(name: string | undefined) {
type Options = {
testing: boolean;
};

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

Expand All @@ -15,17 +26,42 @@ export async function createApp(name: string | undefined) {

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

// add dependencies that every project will use
await addDependency(
[
'react-native-keyboard-aware-scrollview',
'react-native-safe-area-context',
'@react-native-async-storage/async-storage',
].join(' '),
);
commit('Add dependencies');

// must add TS before ESLint
await addTypescript();
execSync('git add .');
execSync('git commit -m "Add TypeScript"');
commit('Add TypeScript');

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

await addPrettier();
commit('Add and configure Prettier');

execSync('yarn fix:prettier');
commit('Run Prettier on project');

await addDependency('npm-run-all', { dev: true });
await addPackageJsonScripts({
lint: 'run-p lint:eslint lint:types lint:prettier',
});

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

if (testing) {
await addTestingLibrary();
commit('Add jest, Testing Library');
}
}

async function getAppName() {
Expand All @@ -41,7 +77,8 @@ async function getAppName() {
export default function createAppAction(...args: unknown[]) {
// if argument ommitted, args[0] is options
const appNameArg = (args[0] as string[])[0];
return createApp(appNameArg);
const options = (args[0] as unknown[])[1] as Options;
return createApp(appNameArg, options);
}

async function printIntro() {
Expand All @@ -52,9 +89,15 @@ async function printIntro() {
- Add and configure ESLint
- Add and configure Prettier
- Create the project directory structure
- Install and configure Jest and Testing Library
`);

if (!(await confirm({ message: 'Ready to proceed?' }))) {
process.exit(0);
}
}

function commit(message: string) {
execSync('git add .');
execSync(`git commit -m "${message}"`);
}
25 changes: 8 additions & 17 deletions src/commands/eslint.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,28 @@
import chalk from 'chalk';
import * as eta from 'eta';
import fs from 'fs-extra';
import path from 'path';
import { PACKAGE_ROOT } from '../constants';
import addDependency from '../util/addDependency';
import getProjectDir from '../util/getProjectDir';
import addPackageJsonScripts from '../util/addPackageJsonScripts';
import copyTemplateDirectory from '../util/copyTemplateDirectory';
import isEslintConfigured from '../util/isEslintConfigured';
import isPackageInstalled from '../util/isPackageInstalled';
import print from '../util/print';
import writeFile from '../util/writeFile';

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

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

const eslintConfigTemplate = await fs.readFile(
path.join(PACKAGE_ROOT, 'templates/eslintrc.js.eta'),
);
await addDependency('eslint @thoughtbot/eslint-config', { dev: true });

const fileContents = eta.render(eslintConfigTemplate.toString(), {
typescript: hasTypeScript,
await copyTemplateDirectory({
templateDir: 'eslint',
variables: { typescript: hasTypeScript },
});

await writeFile(path.join(projectDir, '.eslintrc.js'), fileContents, {
format: true,
await addPackageJsonScripts({
'lint:eslint': 'eslint --max-warnings=0 --ext js,jsx,ts,tsx .',
});

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

print(chalk.green('🎉 ESLint successfully configured'));
}
}
Loading