Skip to content

Commit

Permalink
Add Testing library command, heavily refactor (#18)
Browse files Browse the repository at this point in the history
* Add testing command

* Add copyTemplateDirectory utility

* Fix issues, tests, refactor
  • Loading branch information
Stephen Hanson authored Nov 10, 2023
1 parent c1c2d97 commit 9cc2fb1
Show file tree
Hide file tree
Showing 40 changed files with 728 additions and 125 deletions.
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

0 comments on commit 9cc2fb1

Please sign in to comment.