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 React Navigation support #22

Merged
merged 11 commits into from
Dec 12, 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
2 changes: 1 addition & 1 deletion bin/belt.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ async function run() {
}

function getNodeRunner() {
return Bun && Bun.env ? 'bun' : 'node';
return typeof Bun !== 'undefined' && Bun.env ? 'bun' : 'node';
}

void run();
5 changes: 5 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ export default function runCli() {
.description('Install and configure Jest and Testing Library')
.action(buildAction(import('./commands/testingLibrary')));

program
.command('navigation')
.description('Install and configure React Navigation')
.action(buildAction(import('./commands/navigation')));

printWelcome();
program.parse();
}
9 changes: 8 additions & 1 deletion src/commands/__tests__/createApp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,12 @@ test('creates app', async () => {
});
await createApp('MyApp', { testing: true });

expect(fs.readFileSync('App.tsx', 'utf8')).toMatch('expo-status-bar');
const app = fs.readFileSync('App.tsx', 'utf8');
expect(app).toMatch('<NavigationContainer>');
const homeScreen = fs.readFileSync(
'src/screens/HomeScreen/HomeScreen.tsx',
'utf8',
);

expect(homeScreen).toMatch('expo-status-bar');
});
30 changes: 30 additions & 0 deletions src/commands/__tests__/navigation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { vol } from 'memfs';
import { expect, test, vi } from 'vitest';
import addDependency from '../../util/addDependency';
import copyTemplateDirectory from '../../util/copyTemplateDirectory';
import addNavigation from '../navigation';

vi.mock('../../util/addDependency');
vi.mock('../../util/copyTemplateDirectory');

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

await addNavigation();

expect(addDependency).toHaveBeenCalledWith(
'@react-navigation/native @react-navigation/native-stack',
);
expect(copyTemplateDirectory).toHaveBeenCalledWith({
templateDir: 'reactNavigation',
});
});
76 changes: 76 additions & 0 deletions src/commands/__tests__/reactQuery.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { fs, vol } from 'memfs';
import { Mock, afterEach, expect, test, vi } from 'vitest';
import addDependency from '../../util/addDependency';
import print from '../../util/print';
import addTypescript from '../typescript';

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

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

test('exits with message if tsconfig.json already exists', async () => {
const json = {
'package.json': JSON.stringify({
scripts: {},
dependencies: {},
}),
'tsconfig.json': '1',
};
vol.fromJSON(json, './');

await addTypescript();
expect(print).toHaveBeenCalledWith(
expect.stringMatching(/tsconfig\.json already exists/),
);

// doesn't modify
expect(fs.readFileSync('tsconfig.json', 'utf8')).toEqual('1');
});

test('writes new tsconfig.json, adds dependencies', async () => {
vol.fromJSON({
'package.json': JSON.stringify({
scripts: {},
dependencies: {
expo: '1.0.0',
},
}),
});

await addTypescript();

expect(addDependency).toHaveBeenCalledWith('typescript @types/react', {
dev: true,
});

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

expect(print).not.toHaveBeenCalledWith(
expect.stringMatching(/already exists/),
);
});

test("doesn't extend expo/tsconfig.base if not an Expo project", async () => {
vol.fromJSON({
'package.json': JSON.stringify({
scripts: {},
dependencies: {},
}),
});

await addTypescript();

expect(addDependency).toHaveBeenCalledWith('typescript @types/react', {
dev: true,
});

expect(fs.readFileSync('tsconfig.json', 'utf8')).not.toMatch(
'expo/tsconfig.base',
);
});
6 changes: 6 additions & 0 deletions src/commands/createApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import exec from '../util/exec';
import getUserPackageManager from '../util/getUserPackageManager';
import print from '../util/print';
import addEslint from './eslint';
import addNavigation from './navigation';
import addPrettier from './prettier';
import createScaffold from './scaffold';
import addTestingLibrary from './testingLibrary';
Expand Down Expand Up @@ -85,7 +86,11 @@ export async function createApp(name: string | undefined, options: Options) {
await commit('Add jest, Testing Library');
}

await addNavigation();
await commit('Add navigation');

await copyTemplateDirectory({ templateDir: 'createApp' });
await commit('Add scaffold');

print(chalk.green(`\n\n👖 ${appName} successfully configured!`));

Expand Down Expand Up @@ -128,6 +133,7 @@ async function printIntro() {
- Add and configure ESLint
- Create the project directory structure
- Install and configure Jest and Testing Library
- Install and configure React Navigation
`);

if (!globals.interactive) {
Expand Down
30 changes: 30 additions & 0 deletions src/commands/navigation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import ora from 'ora';
import addDependency from '../util/addDependency';
import copyTemplateDirectory from '../util/copyTemplateDirectory';
import exec from '../util/exec';
import isExpo from '../util/isExpo';

export default async function addNavigation() {
const spinner = ora().start('Installing React Navigation');
const expo = await isExpo();

if (expo) {
await exec(
'npx expo install react-native-screens react-native-safe-area-context',
);
} else {
await addDependency('react-native-screens react-native-safe-area-context');
}

await addDependency(
'@react-navigation/native @react-navigation/native-stack',
);

await copyTemplateDirectory({
templateDir: 'reactNavigation',
});

spinner.succeed(
'Successfully installed React Navigation and Native Stack navigator',
);
}
19 changes: 4 additions & 15 deletions templates/createApp/App.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,17 @@
import { StatusBar } from 'expo-status-bar';
import { StyleSheet, Text, View } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import RootNavigator from 'src/navigators/RootNavigator';
import Providers, { Provider } from 'src/components/Providers';

// Add providers to this array
const providers: Provider[] = [
(children) => <NavigationContainer>{children}</NavigationContainer>,
// CODEGEN:BELT:PROVIDERS - do not remove
];

export default function App() {
return (
<Providers providers={providers}>
<View style={styles.container}>
<Text>Open up App.js to start working on your app!</Text>
<StatusBar style="auto" />
</View>
<RootNavigator />
</Providers>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});
8 changes: 8 additions & 0 deletions templates/createApp/src/__tests__/App.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { screen } from '@testing-library/react-native';
import RootNavigator from 'src/navigators/RootNavigator';
import render from 'src/test/render';

test('renders', async () => {
render(<RootNavigator />);
expect(await screen.findByText(/Open up App.tsx/)).toBeDefined();
});
20 changes: 20 additions & 0 deletions templates/createApp/src/screens/HomeScreen/HomeScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { StatusBar } from 'expo-status-bar';
import { StyleSheet, Text, View } from 'react-native';

export default function HomeScreen() {
return (
<View style={styles.container}>
<Text>Open up App.tsx to start working on your app!</Text>
<StatusBar style="auto" />
</View>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});
12 changes: 12 additions & 0 deletions templates/reactNavigation/src/navigators/RootNavigator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import HomeScreen from 'src/screens/HomeScreen/HomeScreen';

const Stack = createNativeStackNavigator();

export default function RootNavigator() {
return (
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} />
</Stack.Navigator>
);
}
6 changes: 6 additions & 0 deletions templates/reactNavigation/src/navigators/navigatorTypes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// add types for navigation here
// key: screen name
// value: params (use undefined if accepts none)
export type RootStackParamList = {
Home: undefined;
};
20 changes: 20 additions & 0 deletions templates/reactNavigation/src/screens/HomeScreen/HomeScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { StatusBar } from 'expo-status-bar';
import { StyleSheet, Text, View } from 'react-native';

export default function HomeScreen() {
return (
<View style={styles.container}>
<Text>Open up App.tsx to start working on your app!</Text>
<StatusBar style="auto" />
</View>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});
9 changes: 9 additions & 0 deletions templates/reactNavigation/src/types/global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { RootStackParamList } from '../navigators/navigatorTypes';

declare global {
namespace ReactNavigation {
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface RootParamList extends RootStackParamList {}
}
}
Empty file.
Empty file.
2 changes: 1 addition & 1 deletion templates/testingLibrary/jest.config.js.eta
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ module.exports = {
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
moduleNameMapper: {
'.+\\.(png|jpg|ttf|woff|woff2)$':
'<rootDir>/src/test/components/fileMock.js',
'<rootDir>/src/test/fileMock.js',
},
setupFilesAfterEnv: [
'@testing-library/jest-native/extend-expect',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ import {
render as TestingLibraryRender,
} from '@testing-library/react-native';
import { ReactElement } from 'react';
import { NavigationContainer } from '@react-navigation/native';

// TODO: this will become customized as the codebase progresses, so our
// tests can be wrapped with appropriate providers, mocks can be supplied, etc
export default function render(element: ReactElement): RenderAPI {
return TestingLibraryRender(element);
return TestingLibraryRender(
<NavigationContainer>{element}</NavigationContainer>,
);
}
2 changes: 1 addition & 1 deletion templates/typescript/tsconfig.json.eta
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"assets/*": ["assets/*"]
}
},
"include": ["src/**/*", "*.js", ".*.js", "*.ts", "*.tsx", "__mocks__"],
"include": ["src/**/*", "*.js", ".*.js", "*.ts", "*.tsx", "__mocks__"],
"exclude": [
"node_modules",
"babel.config.js",
Expand Down