= {
7 | readonly [P in keyof T]: FluxActionCreator;
8 | };
9 |
10 | export type ReducerMap = {
11 | readonly [T in keyof A]: Reducer;
12 | };
13 |
14 | export function mapReducers(
15 | initialState: S,
16 | reducers: R,
17 | actionCreators: ActionCreatorMap): Reducer {
18 | const reducerMap = new Map(Object.entries(actionCreators).map(([key, val]): [string, Reducer] =>
19 | [getType(val), reducers[key]]));
20 |
21 | return (state: S = initialState, action: AnyAction) => {
22 | if (!("type" in action)) {
23 | return state;
24 | }
25 | const reducer = reducerMap.get(action.type);
26 | if (!reducer) {
27 | return state;
28 | }
29 | return reducer(state, action);
30 | };
31 | }
32 |
--------------------------------------------------------------------------------
/boilerplate/App/Reducers/index.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { combineReducers } from "redux";
3 | import root from "../Sagas";
4 | import configureStore from "./CreateStore";
5 | import { GithubReducer, ImmutableGithubState } from "./GithubReducers";
6 | import { NavigationReducer, NavigationState } from "./NavigationReducers";
7 |
8 | /* ------------- Assemble The Reducers ------------- */
9 | export const reducers = combineReducers({
10 | nav: NavigationReducer,
11 | github: GithubReducer,
12 | });
13 |
14 | export interface State {
15 | github: ImmutableGithubState;
16 | nav: NavigationState;
17 | }
18 |
19 | export default () => {
20 | // tslint:disable-next-line:prefer-const
21 | let { store, sagasManager, sagaMiddleware } = configureStore(reducers, root);
22 |
23 | if (module.hot) {
24 | module.hot.accept(() => {
25 | const nextRootReducer = require("./").reducers;
26 | store.replaceReducer(nextRootReducer);
27 |
28 | const newYieldedSagas = require("../Sagas").default;
29 | sagasManager.cancel();
30 | sagasManager.done.then(() => {
31 | sagasManager = sagaMiddleware.run(newYieldedSagas);
32 | });
33 | });
34 | }
35 |
36 | return store;
37 | };
38 |
--------------------------------------------------------------------------------
/boilerplate/App/Themes/Images.ts:
--------------------------------------------------------------------------------
1 | // leave off @2x/@3x
2 | const images = {
3 | logo: require("../Images/ir.png"),
4 | clearLogo: require("../Images/top_logo.png"),
5 | launch: require("../Images/launch-icon.png"),
6 | ready: require("../Images/your-app.png"),
7 | ignite: require("../Images/ignite_logo.png"),
8 | igniteClear: require("../Images/ignite-logo-transparent.png"),
9 | tileBg: require("../Images/tile_bg.png"),
10 | background: require("../Images/BG.png"),
11 | buttonBackground: require("../Images/button-bg.png"),
12 | api: require("../Images/Icons/icon-api-testing.png"),
13 | components: require("../Images/Icons/icon-components.png"),
14 | deviceInfo: require("../Images/Icons/icon-device-information.png"),
15 | faq: require("../Images/Icons/faq-icon.png"),
16 | home: require("../Images/Icons/icon-home.png"),
17 | theme: require("../Images/Icons/icon-theme.png"),
18 | usageExamples: require("../Images/Icons/icon-usage-examples.png"),
19 | chevronRight: require("../Images/Icons/chevron-right.png"),
20 | hamburger: require("../Images/Icons/hamburger.png"),
21 | backButton: require("../Images/Icons/back-button.png"),
22 | closeButton: require("../Images/Icons/close-button.png"),
23 | };
24 |
25 | export default images;
26 |
--------------------------------------------------------------------------------
/boilerplate/App/Sagas/index.ts:
--------------------------------------------------------------------------------
1 | import { all, takeLatest } from "redux-saga/effects";
2 | import { getType } from "typesafe-actions";
3 |
4 | import DebugConfig from "../Config/DebugConfig";
5 | import FixtureAPI from "../Services/FixtureApi";
6 | import {createAPI, GithubApi} from "../Services/GithubApi";
7 |
8 | /* ------------- Types ------------- */
9 |
10 | import { GithubActions } from "../Reducers/GithubReducers";
11 | import { StartupActions } from "../Reducers/StartupReducers";
12 |
13 | /* ------------- Sagas ------------- */
14 |
15 | import { getUserAvatar } from "./GithubSagas";
16 | import { startup } from "./StartupSagas";
17 |
18 | /* ------------- API ------------- */
19 |
20 | // The API we use is only used from Sagas, so we create it here and pass along
21 | // to the sagas which need it.
22 | const api = DebugConfig.useFixtures ? FixtureAPI : createAPI();
23 |
24 | /* ------------- Connect Types To Sagas ------------- */
25 |
26 | export default function * root() {
27 | yield all([
28 | // some sagas only receive an action
29 | takeLatest(getType(StartupActions.startup), startup),
30 |
31 | // some sagas receive extra parameters in addition to an action
32 | takeLatest(getType(GithubActions.userRequest), getUserAvatar, api),
33 | ]);
34 | }
35 |
--------------------------------------------------------------------------------
/boilerplate/App/Reducers/ScreenTrackingMiddleware.tsx:
--------------------------------------------------------------------------------
1 | import { NavigationActions } from "react-navigation";
2 | import Reactotron from "reactotron-react-native";
3 |
4 | // gets the current screen from navigation state
5 | const getCurrentRouteName = (navigationState) => {
6 | if (!navigationState) {
7 | return null;
8 | }
9 | const route = navigationState.routes[navigationState.index];
10 | // dive into nested navigators
11 | if (route.routes) {
12 | return getCurrentRouteName(route);
13 | }
14 | return route.routeName;
15 | };
16 |
17 | const screenTracking = ({ getState }) => (next) => (action) => {
18 | if (
19 | action.type !== NavigationActions.NAVIGATE &&
20 | action.type !== NavigationActions.BACK
21 | ) {
22 | return next(action);
23 | }
24 |
25 | const currentScreen = getCurrentRouteName(getState().nav);
26 | const result = next(action);
27 | const nextScreen = getCurrentRouteName(getState().nav);
28 | if (nextScreen !== currentScreen) {
29 | try {
30 | Reactotron.log(`NAVIGATING ${currentScreen} to ${nextScreen}`);
31 | // Example: Analytics.trackEvent('user_navigation', {currentScreen, nextScreen})
32 | } catch (e) {
33 | Reactotron.log(e);
34 | }
35 | }
36 | return result;
37 | };
38 |
39 | export default screenTracking;
40 |
--------------------------------------------------------------------------------
/boilerplate/App/Components/RoundedButton/RoundedButton.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Text, TouchableOpacity } from "react-native";
3 | import ExamplesRegistry from "../../Services/ExamplesRegistry";
4 | import styles from "./RoundedButtonStyles";
5 |
6 | // Note that this file (App/Components/RoundedButton) needs to be
7 | // imported in your app somewhere, otherwise your component won't be
8 | // compiled and added to the examples dev screen.
9 |
10 | // Ignore in coverage report
11 | /* istanbul ignore next */
12 | ExamplesRegistry.addComponentExample("Rounded Button", () =>
13 | (
14 | window.alert("Rounded Button Pressed!")}
18 | />),
19 | );
20 |
21 | interface Props {
22 | onPress?: () => any;
23 | text?: string;
24 | children?: string;
25 | }
26 |
27 | // tslint:disable-next-line:no-empty
28 | const RoundedButton: React.SFC = ({ text, children, onPress = () => { } }: Props) => {
29 | const buttonText = (text || children || "").toUpperCase();
30 | return (
31 |
32 | {buttonText}
33 |
34 | );
35 | };
36 |
37 | export default RoundedButton;
38 |
--------------------------------------------------------------------------------
/boilerplate/App/Containers/LaunchScreen/LaunchScreen.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { Image, ScrollView, Text, View } from "react-native";
3 | import DevscreensButton from "../../../ignite/DevScreens/DevscreensButton";
4 |
5 | import { Images } from "../../Themes";
6 |
7 | // Styles
8 | import styles from "./LaunchScreenStyles";
9 |
10 | export default class LaunchScreen extends Component {
11 | public render() {
12 | return (
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | This probably isn't what your app is going to look like. Unless your designer
24 | handed you this screen and, in that case, congrats! You're ready to ship.
25 | For everyone else, this is where you'll see a live preview of your fully functioning app using Ignite.
26 |
27 |
28 |
29 |
30 |
31 |
32 | );
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/boilerplate/types/@storybook/react-native.d.ts:
--------------------------------------------------------------------------------
1 | // Type definitions for @storybook/react 3.0
2 | // Project: https://github.com/storybooks/storybook
3 | // Definitions by: Joscha Feth
4 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
5 | // TypeScript Version: 2.3
6 |
7 | ///
8 |
9 | import * as React from 'react';
10 |
11 | export type Renderable = React.ComponentType | JSX.Element;
12 | export type RenderFunction = () => Renderable;
13 |
14 | export type StoryDecorator = (story: RenderFunction, context: { kind: string, story: string }) => Renderable | null;
15 |
16 | export interface Story {
17 | readonly kind: string;
18 | add(storyName: string, callback: RenderFunction): this;
19 | addDecorator(decorator: StoryDecorator): this;
20 | }
21 |
22 | export function addDecorator(decorator: StoryDecorator): void;
23 | export function configure(fn: () => void, module?: NodeModule): void;
24 | export function setAddon(addon: object): void;
25 | export function storiesOf(name: string, module?: NodeModule): Story;
26 | export function storiesOf(name: string, module?: NodeModule): Story & T;
27 |
28 | export interface StoryObject {
29 | name: string;
30 | render: RenderFunction;
31 | }
32 |
33 | export interface StoryBucket {
34 | kind: string;
35 | stories: StoryObject[];
36 | }
37 |
38 | export function getStorybook(): StoryBucket[];
39 |
--------------------------------------------------------------------------------
/boilerplate/App/Services/RehydrationServices.tsx:
--------------------------------------------------------------------------------
1 | import { AsyncStorage } from "react-native";
2 | import { persistStore } from "redux-persist";
3 | import DebugConfig from "../Config/DebugConfig";
4 | import ReduxPersist from "../Config/ReduxPersist";
5 | import StartupActions from "../Redux/StartupRedux";
6 |
7 | const updateReducers = (store) => {
8 | const reducerVersion = ReduxPersist.reducerVersion;
9 | const config = ReduxPersist.storeConfig;
10 | const startup = () => store.dispatch(StartupActions.startup());
11 |
12 | // Check to ensure latest reducer version
13 | AsyncStorage.getItem("reducerVersion").then((localVersion) => {
14 | if (localVersion !== reducerVersion) {
15 | if (DebugConfig.useReactotron) {
16 | console.tron.display({
17 | name: "PURGE",
18 | value: {
19 | "Old Version:": localVersion,
20 | "New Version:": reducerVersion,
21 | },
22 | preview: "Reducer Version Change Detected",
23 | important: true,
24 | });
25 | }
26 | // Purge store
27 | persistStore(store, config, startup).purge();
28 | AsyncStorage.setItem("reducerVersion", reducerVersion);
29 | } else {
30 | persistStore(store, config, startup);
31 | }
32 | }).catch(() => {
33 | persistStore(store, config, startup);
34 | AsyncStorage.setItem("reducerVersion", reducerVersion);
35 | });
36 | };
37 |
38 | export default {updateReducers};
39 |
--------------------------------------------------------------------------------
/options.js:
--------------------------------------------------------------------------------
1 | /**
2 | * The questions to ask during the install process.
3 | */
4 | const questions = [
5 | {
6 | name: 'dev-screens',
7 | message: 'Would you like Ignite Development Screens?',
8 | type: 'list',
9 | choices: ['No', 'Yes']
10 | },
11 | {
12 | name: 'vector-icons',
13 | message: 'What vector icon library will you use?',
14 | type: 'list',
15 | choices: ['none', 'react-native-vector-icons']
16 | },
17 | {
18 | name: 'i18n',
19 | message: 'What internationalization library will you use?',
20 | type: 'list',
21 | choices: ['none', 'react-native-i18n']
22 | },
23 | {
24 | name: 'animatable',
25 | message: 'What animation library will you use?',
26 | type: 'list',
27 | choices: ['none', 'react-native-animatable']
28 | },
29 | {
30 | name: 'tests',
31 | message: 'What test library will you use?',
32 | type: 'list',
33 | choices: ['none', 'jest']
34 | }
35 | ]
36 |
37 | /**
38 | * The max preset.
39 | */
40 | const max = {
41 | 'dev-screens': 'Yes',
42 | 'vector-icons': 'react-native-vector-icons',
43 | i18n: 'react-native-i18n',
44 | animatable: 'react-native-animatable',
45 | tests: 'jest'
46 | }
47 |
48 | /**
49 | * The min preset.
50 | */
51 | const min = {
52 | 'dev-screens': 'No',
53 | 'vector-icons': 'none',
54 | i18n: 'none',
55 | animatable: 'none',
56 | tests: 'none'
57 | }
58 |
59 | module.exports = {
60 | questions,
61 | answers: { min, max }
62 | }
63 |
--------------------------------------------------------------------------------
/boilerplate/App/Services/FixtureAPITest.tsx:
--------------------------------------------------------------------------------
1 | ///
2 | import * as R from "ramda";
3 | import FixtureAPI from "../../App/Services/FixtureApi";
4 | import API from "../../App/Services/GithubApi";
5 |
6 | test("All fixtures map to actual API", () => {
7 | const fixtureKeys = R.keys(FixtureAPI).sort();
8 | const apiKeys = R.keys(API.createAPI());
9 |
10 | const intersection = R.intersection(fixtureKeys, apiKeys).sort();
11 |
12 | // There is no difference between the intersection and all fixtures
13 | expect(R.equals(fixtureKeys, intersection)).toBe(true);
14 | });
15 |
16 | test("FixtureAPI getRate returns the right file", () => {
17 | const expectedFile = require("../../App/Fixtures/rateLimit.json");
18 |
19 | return FixtureAPI.getRate().then((data) => expect(data).toEqual({
20 | ok: true,
21 | data: expectedFile,
22 | }));
23 | });
24 |
25 | test("FixtureAPI getUser returns the right file for gantman", () => {
26 | const expectedFile = require("../../App/Fixtures/gantman.json");
27 | return FixtureAPI.getUser("GantMan").then((data) => expect(data).toEqual({
28 | ok: true,
29 | data: expectedFile,
30 | }));
31 | });
32 |
33 | test("FixtureAPI getUser returns the right file for skellock as default", () => {
34 | const expectedFile = require("../../App/Fixtures/skellock.json");
35 | return FixtureAPI.getUser("Whatever").then((data) => expect(data).toEqual({
36 | ok: true,
37 | data: expectedFile,
38 | }));
39 | });
40 |
--------------------------------------------------------------------------------
/boilerplate/types/reduxsauce/index.d.ts:
--------------------------------------------------------------------------------
1 | import { Action, AnyAction, Reducer, ReducersMapObject } from "redux";
2 | export interface ActionTypes {
3 | [key: string]: string;
4 | }
5 |
6 | export interface ActionConfig {
7 | [key: string]: string[] | ActionCreator | {[key: string]: any} | null;
8 | }
9 |
10 | export type ActionCreator = (...args: any[]) => Action;
11 |
12 | export interface ActionCreators {
13 | [key: string]: ActionCreator;
14 | }
15 |
16 | export function createActions(config: ActionConfig, options?: {prefix?: string}): {Types: ActionTypes, Creators: ActionCreators };
17 |
18 | /**
19 | * Creates a reducer.
20 | * @param {object} initialState - The initial state for this reducer.
21 | * @param {object} handlers - Keys are action types (strings), values are reducers (functions).
22 | * @return {Reducer} A reducer object.
23 | */
24 | export function createReducer(initialState: S, handlers: ReducersMapObject): Reducer;
25 |
26 | export function createTypes(types: string, options?: {prefix?: string, [key: string]: any}): ActionTypes;
27 |
28 | /**
29 | * Allows your reducers to be reset.
30 | *
31 | * @param {string} typeToReset - The action type to listen for.
32 | * @param {Reducer} originalReducer - The reducer to wrap.
33 | */
34 | export function resettableReducer(type: string, originalReducer: Reducer): Reducer;
35 |
36 | export function resettableReducer(type: string): (originalReducer: Reducer) => Reducer;
37 |
--------------------------------------------------------------------------------
/templates/container.ejs:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Text, View } from "react-native";
3 | import { connect } from "react-redux";
4 | import * as Redux from "redux";
5 | import { RootState } from "../../Reducers";
6 | import { Images } from "../../Themes";
7 | import Metrics from "../../Themes/Metrics";
8 |
9 | // Styles
10 | import styles from "./<%= props.name %>Style";
11 |
12 | /**
13 | * The properties passed to the component
14 | */
15 | export interface OwnProps {
16 |
17 | }
18 | /**
19 | * The properties mapped from Redux dispatch
20 | */
21 | export interface DispatchProps {
22 |
23 | }
24 |
25 | /**
26 | * The properties mapped from the global state
27 | */
28 | export interface StateProps {
29 |
30 | }
31 |
32 | /**
33 | * The local state
34 | */
35 | export interface State {
36 |
37 | }
38 |
39 | type Props = StateProps & DispatchProps & OwnProps;
40 |
41 | class <%= props.name %> extends
42 | React.Component {
43 | public state = {
44 |
45 | }
46 |
47 | public render() {
48 | return (
49 |
50 | Hello <%= props.name %>
51 |
52 | );
53 | }
54 | }
55 |
56 | const mapDispatchToProps = (dispatch: Redux.Dispatch): DispatchProps => ({
57 |
58 | });
59 |
60 | const mapStateToProps = (state: RootState, ownProps: OwnProps): StateProps => {
61 | return {};
62 | };
63 |
64 | export default connect(mapStateToProps, mapDispatchToProps)(<%= props.name %>) as React.ComponentClass;
65 |
--------------------------------------------------------------------------------
/commands/component.js:
--------------------------------------------------------------------------------
1 | // @cliDescription Generates a stateless component, styles, and an optional test.
2 |
3 | module.exports = async function (context) {
4 | // grab some features
5 | const { parameters, strings, print, ignite } = context
6 | const { pascalCase, isBlank } = strings
7 | const config = ignite.loadIgniteConfig()
8 | const { tests } = config
9 |
10 | // validation
11 | if (isBlank(parameters.first)) {
12 | print.info(`${context.runtime.brand} generate component \n`)
13 | print.info('A name is required.')
14 | return
15 | }
16 |
17 | // read some configuration
18 | const name = pascalCase(parameters.first)
19 | const props = { name }
20 | const jobs = [
21 | {
22 | template: 'component.ejs',
23 | target: `App/Components/${name}/${name}.tsx`
24 | },
25 | {
26 | template: 'component-style.ejs',
27 | target: `App/Components/${name}/${name}Style.ts`
28 | },
29 | {
30 | template: 'component-index.ejs',
31 | target: `App/Components/${name}/index.ts`
32 | },
33 | {
34 | template: 'component-story.ejs',
35 | target: `App/Components/${name}/${name}.story.tsx`
36 | },
37 | tests === 'ava' &&
38 | {
39 | template: 'component-test-ava.ejs',
40 | target: `App/Components/${name}/${name}Test.tsx`
41 | },
42 | tests === 'jest' &&
43 | {
44 | template: 'component-test-jest.ejs',
45 | target: `App/Components/${name}/${name}Test.tsx`
46 | }
47 | ]
48 |
49 | await ignite.copyBatch(context, jobs, props)
50 | }
51 |
--------------------------------------------------------------------------------
/boilerplate/App/Sagas/GithubSagas/GithubSagaTest.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { path } from "ramda";
3 | import { call, put } from "redux-saga/effects";
4 | import { GithubActions } from "../../Reducers/GithubReducers";
5 | import FixtureAPI from "../../Services/FixtureApi";
6 | import { getUserAvatar } from "./index";
7 |
8 | const stepper = (fn) => (mock?) => fn.next(mock).value;
9 |
10 | test("first calls API", () => {
11 | const step = stepper(getUserAvatar(FixtureAPI, GithubActions.userRequest({username: "ascorbic"})));
12 | // first yield is API
13 | expect(step()).toEqual(call(FixtureAPI.getUser, "ascorbic"));
14 | });
15 |
16 | test("success path", () => {
17 | FixtureAPI.getUser("ascorbic").then((response) => {
18 |
19 | // tslint:disable-next-line:max-line-length
20 | const step = stepper(getUserAvatar(FixtureAPI, GithubActions.userRequest({username: "ascorbic"})));
21 | // first step API
22 | step();
23 | // Second step successful return
24 | const stepResponse = step(response);
25 | // Get the avatar Url from the response
26 | const firstUser = path(["data", "items"], response)[0];
27 | const avatar = firstUser.avatar_url;
28 | expect(stepResponse).toEqual(put(GithubActions.userSuccess(avatar)));
29 | });
30 | });
31 |
32 | test("failure path", () => {
33 | const response = {ok: false};
34 | const step = stepper(getUserAvatar(FixtureAPI, GithubActions.userRequest({username: "ascorbic"})));
35 | // first step API
36 | step();
37 | // Second step failed response
38 | expect(step(response)).toEqual(put(GithubActions.userFailure()));
39 | });
40 |
--------------------------------------------------------------------------------
/templates/saga-test-jest.ejs:
--------------------------------------------------------------------------------
1 | /* ***********************************************************
2 | * Wiring Instructions
3 | * To make this test work, you'll need to:
4 | * - Add a Fixture named get<%= props.name %> to the
5 | * ./App/Services/FixtureApi file. You can just keep adding
6 | * functions to that file.
7 | *************************************************************/
8 |
9 | import FixtureAPI from '../../Services/FixtureApi'
10 | import { call, put } from 'redux-saga/effects'
11 | import { get<%= props.name %> } from './index'
12 | import <%= props.name %>Actions from '../../Reducers/<%= props.name %>Reducers'
13 |
14 | const stepper = (fn) => (mock) => fn.next(mock).value
15 |
16 | it('first calls API', () => {
17 | const step = stepper(get<%= props.name %>(FixtureAPI, {data: 'taco'}))
18 | // first yield is the API
19 | expect(step()).toEqual(call(FixtureAPI.get<%= props.name %>, 'taco'))
20 | })
21 |
22 | it('success path', () => {
23 | const response = FixtureAPI.get<%= props.name %>('taco')
24 | const step = stepper(get<%= props.name %>(FixtureAPI, {data: 'taco'}))
25 | // Step 1: Hit the api
26 | step()
27 | // Step 2: Successful return and data!
28 | expect(step(response)).toEqual(put(<%= pascalCase(props.name) %>Actions.<%= camelCase(props.name) %>Success(21)))
29 | })
30 |
31 | it('failure path', () => {
32 | const response = {ok: false}
33 | const step = stepper(get<%= props.name %>(FixtureAPI, {data: 'taco'}))
34 | // Step 1: Hit the api
35 | step()
36 | // Step 2: Failed response.
37 | expect(step(response)).toEqual(put(<%= pascalCase(props.name) %>Actions.<%= camelCase(props.name) %>Failure()))
38 | })
39 |
--------------------------------------------------------------------------------
/boilerplate/README.md:
--------------------------------------------------------------------------------
1 | # <%= props.name %>
2 |
3 | * TypeScript React Native App Utilizing [Ignite](https://github.com/infinitered/ignite)
4 |
5 | ## :arrow_up: How to Setup
6 |
7 | **Step 1:** git clone this repo:
8 |
9 | **Step 2:** cd to the cloned repo:
10 |
11 | **Step 3:** Install the Application with `yarn` or `npm i`
12 |
13 |
14 | ## :arrow_forward: How to Run App
15 |
16 | 1. cd to the repo
17 | 2. Run `npm run compile`
18 | 3. Run Build for either OS
19 | * for iOS
20 | * run `react-native run-ios`
21 | * for Android
22 | * Run Genymotion
23 | * run `react-native run-android`
24 |
25 | **To Lint on Commit**
26 |
27 | This is implemented using [husky](https://github.com/typicode/husky). There is no additional setup needed.
28 |
29 | ## :closed_lock_with_key: Secrets
30 |
31 | This project uses [react-native-config](https://github.com/luggit/react-native-config) to expose config variables to your javascript code in React Native. You can store API keys
32 | and other sensitive information in a `.env` file:
33 |
34 | ```
35 | API_URL=https://myapi.com
36 | GOOGLE_MAPS_API_KEY=abcdefgh
37 | ```
38 |
39 | and access them from React Native like so:
40 |
41 | ```
42 | import Secrets from 'react-native-config'
43 |
44 | Secrets.API_URL // 'https://myapi.com'
45 | Secrets.GOOGLE_MAPS_API_KEY // 'abcdefgh'
46 | ```
47 |
48 | The `.env` file is ignored by git keeping those secrets out of your repo.
49 |
50 | ### Get started:
51 | 1. Copy .env.example to .env
52 | 2. Add your config variables
53 | 3. Follow instructions at [https://github.com/luggit/react-native-config#setup](https://github.com/luggit/react-native-config#setup)
54 | 4. Done!
55 |
--------------------------------------------------------------------------------
/templates/saga.ejs:
--------------------------------------------------------------------------------
1 | /* ***********************************************************
2 | * A short word on how to use this automagically generated file.
3 | * We're often asked in the ignite gitter channel how to connect
4 | * to a to a third party api, so we thought we'd demonstrate - but
5 | * you should know you can use sagas for other flow control too.
6 | *
7 | * Other points:
8 | * - You'll need to add this saga to sagas/index.ts
9 | * - This template uses the api declared in sagas/index.ts, so
10 | * you'll need to define a constant in that file.
11 | *************************************************************/
12 | import { ApiResponse } from "apisauce";
13 | import { SagaIterator } from "redux-saga";
14 | import { call, put } from "redux-saga/effects";
15 | import { <%= props.name %>Actions, <%= props.name %>Action } from "../../Reducers/<%= props.name %>Reducers";
16 | import { <%= props.name %>Api, <%= props.name %>Response } from "../../Services/<%= props.name %>Api";
17 |
18 |
19 | export function * get<%= props.name %> (api: <%= props.name %>Api, action: <%= props.name %>Action): SagaIterator {
20 | const { data } = action;
21 | // make the call to the api
22 | const response: ApiResponse<<%= props.name %>Response> = yield call(api.get<%= camelCase(props.name) %>, data);
23 |
24 | // success?
25 | if (response.ok) {
26 | // You might need to change the response here - do this with a 'transform',
27 | // located in ../../Transforms/. Otherwise, just pass the data back from the api.
28 | yield put(<%= props.name %>Actions.success({data: response.data}));
29 | } else {
30 | yield put(<%= props.name %>Actions.failure());
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/boilerplate/App/Reducers/CreateStore.tsx:
--------------------------------------------------------------------------------
1 | import Reactotron from "reactotron-react-native";
2 | import { applyMiddleware, compose, createStore, Reducer } from "redux";
3 | import sagaMiddlewareFactory, { Monitor, SagaIterator } from "redux-saga";
4 | import Config from "../Config/DebugConfig";
5 | import ScreenTracking from "./ScreenTrackingMiddleware";
6 |
7 | // creates the store
8 | export default (rootReducer: Reducer, rootSaga: () => SagaIterator) => {
9 | /* ------------- Redux Configuration ------------- */
10 |
11 | const middleware = [];
12 | const enhancers = [];
13 |
14 | /* ------------- Analytics Middleware ------------- */
15 | if (Config.useReactotron) {
16 | middleware.push(ScreenTracking);
17 | }
18 | /* ------------- Saga Middleware ------------- */
19 |
20 | let opts = {};
21 | if (Config.useReactotron) {
22 | const sagaMonitor: Monitor = Reactotron.createSagaMonitor();
23 | opts = { sagaMonitor };
24 | }
25 | const sagaMiddleware = sagaMiddlewareFactory(opts);
26 | middleware.push(sagaMiddleware);
27 |
28 | /* ------------- Assemble Middleware ------------- */
29 |
30 | enhancers.push(applyMiddleware(...middleware));
31 |
32 | // if Reactotron is enabled (default for __DEV__), we'll create the store through Reactotron
33 | const createAppropriateStore = Config.useReactotron ? Reactotron.createStore : createStore;
34 |
35 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
36 |
37 | const store = createAppropriateStore(rootReducer, composeEnhancers(...enhancers));
38 |
39 | // kick off root saga
40 | const sagasManager = sagaMiddleware.run(rootSaga);
41 |
42 | return {
43 | store,
44 | sagasManager,
45 | sagaMiddleware,
46 | };
47 | };
48 |
--------------------------------------------------------------------------------
/boilerplate/App/Sagas/StartupSagas/index.ts:
--------------------------------------------------------------------------------
1 | import { is } from "ramda";
2 | import Reactotron from "reactotron-react-native";
3 | import { Action } from "redux";
4 | import { SagaIterator } from "redux-saga";
5 | import { put, select } from "redux-saga/effects";
6 | import { GithubAction, GithubActions } from "../../Reducers/GithubReducers";
7 | import { StartupActions } from "../../Reducers/StartupReducers";
8 |
9 | // exported to make available for tests
10 | export const selectAvatar = (state: any) => state.github.avatar;
11 |
12 | // process STARTUP actions
13 | export function * startup(action?: Action): SagaIterator {
14 | if (__DEV__) {
15 | // straight-up string logging
16 | Reactotron.log("Hello, I'm an example of how to log via Reactotron.");
17 | Reactotron.log(action);
18 | // logging an object for better clarity
19 | Reactotron.log({
20 | message: "pass objects for better logging",
21 | someGeneratorFunction: selectAvatar,
22 | });
23 |
24 | // fully customized!
25 | const subObject = { a: 1, b: [1, 2, 3], c: true, circularDependency: undefined as any};
26 | subObject.circularDependency = subObject; // osnap!
27 | Reactotron.display({
28 | name: "🔥 IGNITE 🔥",
29 | preview: "You should totally expand this",
30 | value: {
31 | "💃": "Welcome to the future!",
32 | subObject,
33 | "someInlineFunction": () => true,
34 | "someGeneratorFunction": startup,
35 | "someNormalFunction": selectAvatar,
36 | },
37 | });
38 | }
39 | const avatar = yield select(selectAvatar);
40 | // only get if we don't have it yet
41 | if (!is(String, avatar)) {
42 | yield put(GithubActions.userRequest({username: "ascorbic"}));
43 | }
44 |
45 | }
46 |
--------------------------------------------------------------------------------
/boilerplate/App/Services/ExamplesRegistry.tsx:
--------------------------------------------------------------------------------
1 | import R from "ramda";
2 | import * as React from "react";
3 | import { Text, View } from "react-native";
4 | import DebugConfig from "../Config/DebugConfig";
5 | import { ApplicationStyles } from "../Themes";
6 | const globalComponentExamplesRegistry = [];
7 | const globalPluginExamplesRegistry = [];
8 |
9 | export const addComponentExample = (title, usage = () => {}) => { if (DebugConfig.includeExamples) globalComponentExamplesRegistry.push({title, usage}); }; // eslint-disable-line
10 |
11 | export const addPluginExample = (title, usage = () => {}) => { if (DebugConfig.includeExamples) globalPluginExamplesRegistry.push({title, usage}); }; // eslint-disable-line
12 |
13 | const renderComponentExample = (example) => {
14 | return (
15 |
16 |
17 | {example.title}
18 |
19 | {example.usage.call()}
20 |
21 | );
22 | };
23 |
24 | const renderPluginExample = (example) => {
25 | return (
26 |
27 |
28 | {example.title}
29 |
30 | {example.usage.call()}
31 |
32 | );
33 | };
34 |
35 | export const renderComponentExamples = () => R.map(renderComponentExample, globalComponentExamplesRegistry);
36 |
37 | export const renderPluginExamples = () => R.map(renderPluginExample, globalPluginExamplesRegistry);
38 |
39 | // Default for readability
40 | export default {
41 | renderComponentExamples,
42 | addComponentExample,
43 | renderPluginExamples,
44 | addPluginExample,
45 | };
46 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ignite-typescript-boilerplate",
3 | "description": "TypeScript boilerplate for React Native.",
4 | "license": "MIT",
5 | "repository": "aerian-studios/ignite-typescript-boilerplate",
6 | "homepage": "https://github.com/aerian-studios/ignite-typescript-boilerplate",
7 | "version": "0.1.5",
8 | "files": [
9 | "boilerplate",
10 | "commands",
11 | "lib",
12 | "templates",
13 | "boilerplate.js",
14 | "ignite.json",
15 | "options.js",
16 | "readme.md",
17 | "plugin.js"
18 | ],
19 | "author": {
20 | "name": "Aerian Studios",
21 | "email": "matt.kane@aerian.com",
22 | "url": "https://github.com/aerian-studios/ignite-typescript-boilerplate"
23 | },
24 | "scripts": {
25 | "lint": "tslint",
26 | "test": "jest",
27 | "watch": "jest --runInBand --watch",
28 | "coverage": "jest --runInBand --coverage",
29 | "shipit": "np"
30 | },
31 | "devDependencies": {
32 | "@types/jest": "^21.1.5",
33 | "@types/ramda": "^0.24.18",
34 | "@types/react": "^16.0.18",
35 | "@types/react-native": "^0.49.2",
36 | "@types/react-navigation": "^1.0.21",
37 | "@types/react-redux": "^5.0.10",
38 | "@types/redux": "^3.6.0",
39 | "@types/seamless-immutable": "^7.1.1",
40 | "@types/webpack-env": "^1.13.2",
41 | "fs-jetpack": "^1.0.0",
42 | "jest": "^20.0.4",
43 | "np": "^2.15.0",
44 | "react": "16.0.0-beta.5",
45 | "redux": "^3.7.2",
46 | "redux-saga": "^0.16.0",
47 | "reduxsauce": "^0.7.0",
48 | "seamless-immutable": "^7.1.2",
49 | "sinon": "^2.3.1",
50 | "tempy": "^0.1.0",
51 | "tslint": "^5.8.0",
52 | "tslint-react": "^3.2.0",
53 | "typesafe-actions": "^1.1.2",
54 | "typescript": "^2.6.1"
55 | },
56 | "dependencies": {
57 | "ramda": "^0.23.0"
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/test/react-native-version.test.js:
--------------------------------------------------------------------------------
1 | const boilerplate = require('../lib/react-native-version')
2 |
3 | // grab a few things from the boilerplate module
4 | const get = boilerplate.getReactNativeVersion
5 | const DEFAULT = boilerplate.REACT_NATIVE_VERSION
6 |
7 | /**
8 | * Runs with a valid gluegun context and a staged version number.
9 | *
10 | * @param {*} reactNativeVersion The React Native version to use.
11 | * @return {string} The version number we should be using.
12 | */
13 | const mock = reactNativeVersion => get({
14 | parameters: {
15 | options: {
16 | 'react-native-version': reactNativeVersion
17 | }
18 | }
19 | })
20 |
21 | // this would only happen if we screwed something up in our boilerplate.js
22 | test('it handles strange inputs from code', () => {
23 | expect(get()).toBe(DEFAULT)
24 | expect(get(null)).toBe(DEFAULT)
25 | expect(get(true)).toBe(DEFAULT)
26 | expect(get(8)).toBe(DEFAULT)
27 | expect(get('hello')).toBe(DEFAULT)
28 | expect(get([])).toBe(DEFAULT)
29 | expect(get({})).toBe(DEFAULT)
30 | expect(get(() => true)).toBe(DEFAULT)
31 | })
32 |
33 | // this could happen because it's valid input via minimist from the user
34 | test('it handles strange input from the user', () => {
35 | expect(mock(true)).toBe(DEFAULT)
36 | expect(mock(false)).toBe(DEFAULT)
37 | expect(mock([])).toBe(DEFAULT)
38 | expect(mock({})).toBe(DEFAULT)
39 | })
40 |
41 | // very edge-casey
42 | test('it handles not-quite semver numbers', () => {
43 | expect(mock(0)).toBe(DEFAULT)
44 | expect(mock(0.25)).toBe(DEFAULT)
45 | })
46 |
47 | // happy path
48 | test('it handles valid versions', () => {
49 | expect(mock('0.41.0')).toBe('0.41.0')
50 | expect(mock('0.41.0-beta.1')).toBe('0.41.0-beta.1')
51 | expect(mock(DEFAULT)).toBe(DEFAULT)
52 | expect(mock('next')).toBe('next')
53 | })
54 |
--------------------------------------------------------------------------------
/templates/screen.ejs:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Alert, Image, Text, TouchableOpacity, View } from "react-native";
3 | import Icon from "react-native-vector-icons/FontAwesome";
4 | import { NavigationAction, NavigationDrawerScreenOptions, NavigationScreenProps, NavigationState } from "react-navigation";
5 | import { connect } from "react-redux";
6 | import * as Redux from "redux";
7 | import { RootState } from "../../Reducers";
8 | import { Images } from "../../Themes";
9 | import Metrics from "../../Themes/Metrics";
10 |
11 | // Styles
12 | import styles from "./<%= props.name %>Style";
13 |
14 | /**
15 | * The properties passed to the component
16 | */
17 | export interface OwnProps {
18 |
19 | }
20 | /**
21 | * The properties mapped from Redux dispatch
22 | */
23 | export interface DispatchProps {
24 |
25 | }
26 |
27 | /**
28 | * The properties mapped from the global state
29 | */
30 | export interface StateProps {
31 |
32 | }
33 |
34 | /**
35 | * The local state
36 | */
37 | export interface State {
38 |
39 | }
40 |
41 | type Props = StateProps & DispatchProps & OwnProps & NavigationScreenProps<{}>;
42 |
43 | class <%= props.name %> extends
44 | React.Component {
45 |
46 | public state = {
47 |
48 | };
49 |
50 | public static navigationOptions: NavigationDrawerScreenOptions = {
51 | drawerLabel: "Welcome",
52 | drawerIcon: ({ tintColor, focused }: {tintColor: string, focused: boolean}) => (
53 |
58 | ),
59 | };
60 |
61 | public render() {
62 | return (
63 |
64 | Hello <%= props.name %>
65 |
66 | );
67 | }
68 | }
69 |
70 | const mapDispatchToProps = (dispatch: Redux.Dispatch): DispatchProps => ({
71 |
72 | });
73 |
74 | const mapStateToProps = (state: RootState, ownProps: OwnProps): StateProps => {
75 | return {};
76 | };
77 |
78 | export default connect(mapStateToProps, mapDispatchToProps)(<%= props.name %>) as React.ComponentClass;
79 |
--------------------------------------------------------------------------------
/commands/container.js:
--------------------------------------------------------------------------------
1 | // @cliDescription Generates a redux smart component.
2 |
3 | const patterns = require('../lib/patterns')
4 |
5 | module.exports = async function (context) {
6 | // grab some features
7 | const { parameters, strings, print, ignite, filesystem } = context
8 | const { pascalCase, isBlank } = strings
9 | const config = ignite.loadIgniteConfig()
10 |
11 | // validation
12 | if (isBlank(parameters.first)) {
13 | print.info(`${context.runtime.brand} generate container \n`)
14 | print.info('A name is required.')
15 | return
16 | }
17 |
18 | const name = pascalCase(parameters.first)
19 | const props = { name }
20 |
21 | const jobs = [
22 | {
23 | template: 'container.ejs',
24 | target: `App/Containers/${name}/${name}.tsx`
25 | },
26 | {
27 | template: 'container-style.ejs',
28 | target: `App/Containers/${name}/${name}Style.ts`
29 | }
30 | ]
31 |
32 | await ignite.copyBatch(context, jobs, props)
33 |
34 | // if using `react-navigation` go the extra step
35 | // and insert the container into the nav router
36 | if (config.navigation === 'react-navigation') {
37 | const containerName = name
38 | const appNavFilePath = `${process.cwd()}/App/Navigation/AppNavigation.tsx`
39 | const importToAdd = `import ${containerName} from "../Containers/${containerName}";`
40 | const routeToAdd = ` ${containerName}: { screen: ${containerName} },`
41 |
42 | if (!filesystem.exists(appNavFilePath)) {
43 | const msg = `No '${appNavFilePath}' file found. Can't insert container.`
44 | print.error(msg)
45 | process.exit(1)
46 | }
47 |
48 | // insert container import
49 | ignite.patchInFile(appNavFilePath, {
50 | after: patterns[patterns.constants.PATTERN_IMPORTS],
51 | insert: importToAdd
52 | })
53 |
54 | // insert container route
55 | ignite.patchInFile(appNavFilePath, {
56 | after: patterns[patterns.constants.PATTERN_ROUTES],
57 | insert: routeToAdd
58 | })
59 | } else {
60 | print.info('Container created, manually add it to your navigation')
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/boilerplate/App/Fixtures/root.json:
--------------------------------------------------------------------------------
1 | {
2 | "current_user_url": "https://api.github.com/user",
3 | "current_user_authorizations_html_url": "https://github.com/settings/connections/applications{/client_id}",
4 | "authorizations_url": "https://api.github.com/authorizations",
5 | "code_search_url": "https://api.github.com/search/code?q={query}{&page,per_page,sort,order}",
6 | "commit_search_url": "https://api.github.com/search/commits?q={query}{&page,per_page,sort,order}",
7 | "emails_url": "https://api.github.com/user/emails",
8 | "emojis_url": "https://api.github.com/emojis",
9 | "events_url": "https://api.github.com/events",
10 | "feeds_url": "https://api.github.com/feeds",
11 | "followers_url": "https://api.github.com/user/followers",
12 | "following_url": "https://api.github.com/user/following{/target}",
13 | "gists_url": "https://api.github.com/gists{/gist_id}",
14 | "hub_url": "https://api.github.com/hub",
15 | "issue_search_url": "https://api.github.com/search/issues?q={query}{&page,per_page,sort,order}",
16 | "issues_url": "https://api.github.com/issues",
17 | "keys_url": "https://api.github.com/user/keys",
18 | "notifications_url": "https://api.github.com/notifications",
19 | "organization_repositories_url": "https://api.github.com/orgs/{org}/repos{?type,page,per_page,sort}",
20 | "organization_url": "https://api.github.com/orgs/{org}",
21 | "public_gists_url": "https://api.github.com/gists/public",
22 | "rate_limit_url": "https://api.github.com/rate_limit",
23 | "repository_url": "https://api.github.com/repos/{owner}/{repo}",
24 | "repository_search_url": "https://api.github.com/search/repositories?q={query}{&page,per_page,sort,order}",
25 | "current_user_repositories_url": "https://api.github.com/user/repos{?type,page,per_page,sort}",
26 | "starred_url": "https://api.github.com/user/starred{/owner}{/repo}",
27 | "starred_gists_url": "https://api.github.com/gists/starred",
28 | "team_url": "https://api.github.com/teams",
29 | "user_url": "https://api.github.com/users/{user}",
30 | "user_organizations_url": "https://api.github.com/user/orgs",
31 | "user_repositories_url": "https://api.github.com/users/{user}/repos{?type,page,per_page,sort}",
32 | "user_search_url": "https://api.github.com/search/users?q={query}{&page,per_page,sort,order}"
33 | }
--------------------------------------------------------------------------------
/commands/screen.js:
--------------------------------------------------------------------------------
1 | // @cliDescription Generates an opinionated container.
2 |
3 | const patterns = require('../lib/patterns')
4 |
5 | module.exports = async function (context) {
6 | // grab some features
7 | const { parameters, print, strings, ignite, filesystem } = context
8 | const { pascalCase, isBlank } = strings
9 | const config = ignite.loadIgniteConfig()
10 |
11 | // validation
12 | if (isBlank(parameters.first)) {
13 | print.info(`${context.runtime.brand} generate screen \n`)
14 | print.info('A name is required.')
15 | return
16 | }
17 |
18 | const name = pascalCase(parameters.first)
19 | const screenName = name.endsWith('Screen') ? name : `${name}Screen`
20 | const props = { name: screenName }
21 |
22 | const jobs = [
23 | {
24 | template: `screen.ejs`,
25 | target: `App/Containers/${screenName}/${screenName}.tsx`
26 | },
27 | {
28 | template: `screen-style.ejs`,
29 | target: `App/Containers/${screenName}/${screenName}Style.tsx`
30 | },
31 | {
32 | template: 'component-index.ejs',
33 | target: `App/Containers/${screenName}/index.ts`
34 | }
35 | ]
36 |
37 | // make the templates
38 | await ignite.copyBatch(context, jobs, props)
39 |
40 | // if using `react-navigation` go the extra step
41 | // and insert the screen into the nav router
42 | if (config.navigation === 'react-navigation') {
43 | const appNavFilePath = `${process.cwd()}/App/Navigation/AppNavigation.tsx`
44 | const importToAdd = `import ${screenName} from "../Containers/${screenName}";`
45 | const routeToAdd = ` ${screenName}: { screen: ${screenName} },`
46 |
47 | if (!filesystem.exists(appNavFilePath)) {
48 | const msg = `No '${appNavFilePath}' file found. Can't insert screen.`
49 | print.error(msg)
50 | process.exit(1)
51 | }
52 |
53 | // insert screen import
54 | ignite.patchInFile(appNavFilePath, {
55 | after: patterns[patterns.constants.PATTERN_IMPORTS],
56 | insert: importToAdd
57 | })
58 |
59 | // insert screen route
60 | ignite.patchInFile(appNavFilePath, {
61 | after: patterns[patterns.constants.PATTERN_ROUTES],
62 | insert: routeToAdd
63 | })
64 | } else {
65 | print.info(`Screen ${screenName} created, manually add it to your navigation`)
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/boilerplate/App/Themes/ApplicationStyles.ts:
--------------------------------------------------------------------------------
1 | import { TextStyle, ViewStyle } from "react-native";
2 |
3 | import Colors from "./Colors";
4 | import Fonts from "./Fonts";
5 | import Metrics from "./Metrics";
6 |
7 | // This file is for a reusable grouping of Theme items.
8 | // Similar to an XML fragment layout in Android
9 |
10 | const ApplicationStyles = {
11 | screen: {
12 | mainContainer: {
13 | flex: 1,
14 | backgroundColor: Colors.transparent,
15 | } as ViewStyle,
16 | backgroundImage: {
17 | position: "absolute",
18 | top: 0,
19 | left: 0,
20 | bottom: 0,
21 | right: 0,
22 | } as ViewStyle,
23 | container: {
24 | flex: 1,
25 | paddingTop: Metrics.baseMargin,
26 | backgroundColor: Colors.transparent,
27 | } as ViewStyle,
28 | section: {
29 | margin: Metrics.section,
30 | padding: Metrics.baseMargin,
31 | } as ViewStyle,
32 | sectionText: {
33 | ...Fonts.style.normal,
34 | paddingVertical: Metrics.doubleBaseMargin,
35 | color: Colors.snow,
36 | marginVertical: Metrics.smallMargin,
37 | textAlign: "center",
38 | } as TextStyle,
39 | subtitle: {
40 | color: Colors.snow,
41 | padding: Metrics.smallMargin,
42 | marginBottom: Metrics.smallMargin,
43 | marginHorizontal: Metrics.smallMargin,
44 | } as TextStyle,
45 | titleText: {
46 | ...Fonts.style.h2,
47 | fontSize: 14,
48 | color: Colors.text,
49 | } as TextStyle,
50 | },
51 | darkLabelContainer: {
52 | padding: Metrics.smallMargin,
53 | paddingBottom: Metrics.doubleBaseMargin,
54 | borderBottomColor: Colors.border,
55 | borderBottomWidth: 1,
56 | marginBottom: Metrics.baseMargin,
57 | } as ViewStyle,
58 | darkLabel: {
59 | fontFamily: Fonts.type.bold,
60 | color: Colors.snow,
61 | } as TextStyle,
62 | groupContainer: {
63 | margin: Metrics.smallMargin,
64 | flexDirection: "row",
65 | justifyContent: "space-around",
66 | alignItems: "center",
67 | } as ViewStyle,
68 | sectionTitle: {
69 | ...Fonts.style.h4,
70 | color: Colors.coal,
71 | backgroundColor: Colors.ricePaper,
72 | padding: Metrics.smallMargin,
73 | marginTop: Metrics.smallMargin,
74 | marginHorizontal: Metrics.baseMargin,
75 | borderWidth: 1,
76 | borderColor: Colors.ember,
77 | alignItems: "center",
78 | textAlign: "center",
79 | } as TextStyle,
80 | };
81 |
82 | export default ApplicationStyles;
83 |
--------------------------------------------------------------------------------
/boilerplate/App/Reducers/GithubReducers/index.tsx:
--------------------------------------------------------------------------------
1 | import { Action, AnyAction, Reducer } from "redux";
2 | import * as SI from "seamless-immutable";
3 | import { createAction, PayloadAction } from "typesafe-actions";
4 | import { mapReducers, ReducerMap } from "../../Lib/ReduxHelpers";
5 |
6 | /* ------------- Types and Action Creators ------------- */
7 | interface RequestParams {username: string; }
8 | interface SuccessParams {avatar: string; }
9 | const actions = {
10 | userRequest: createAction("githubUserRequest", (params: RequestParams) =>
11 | ({type: "githubUserRequest", payload: params})),
12 | userSuccess: createAction("githubUserSuccess", (params: SuccessParams) =>
13 | ({type: "githubUserSuccess", payload: params})),
14 | userFailure: createAction("githubUserFailure"),
15 | };
16 |
17 | export const GithubActions = actions;
18 |
19 | interface GithubState {
20 | avatar?: string | null;
21 | fetching?: boolean | null;
22 | error?: boolean | null;
23 | username?: string | null;
24 | }
25 |
26 | export type GithubAction = PayloadAction;
27 |
28 | export type ImmutableGithubState = SI.ImmutableObject;
29 |
30 | /* ------------- Initial State ------------- */
31 |
32 | export const INITIAL_STATE: ImmutableGithubState = SI.from({
33 | avatar: null,
34 | fetching: null,
35 | error: null,
36 | username: null,
37 | });
38 |
39 | /* ------------- Reducers ------------- */
40 |
41 | // request the avatar for a user
42 | export const userRequest: Reducer =
43 | (state: ImmutableGithubState, { payload }: AnyAction & {payload?: RequestParams}) =>
44 | payload ? state.merge({ fetching: true, username: payload.username, avatar: null }) : state;
45 |
46 | // successful avatar lookup
47 | export const userSuccess: Reducer =
48 | (state: ImmutableGithubState, { payload }: AnyAction & {payload?: SuccessParams}) =>
49 | payload ? state.merge({ fetching: false, error: null, avatar: payload.avatar }) : state;
50 |
51 | // failed to get the avatar
52 | export const userFailure: Reducer = (state: ImmutableGithubState) =>
53 | state.merge({ fetching: false, error: true, avatar: null });
54 |
55 | /* ------------- Hookup Reducers To Types ------------- */
56 |
57 | const reducerMap: ReducerMap = {
58 | userRequest,
59 | userSuccess,
60 | userFailure,
61 | };
62 |
63 | export const GithubReducer = mapReducers(INITIAL_STATE, reducerMap, actions);
64 |
65 | export default GithubReducer;
66 |
--------------------------------------------------------------------------------
/templates/reducers.ejs:
--------------------------------------------------------------------------------
1 | import { Action, AnyAction, Reducer } from "redux";
2 | import * as SI from "seamless-immutable";
3 | import { createAction, PayloadAction } from "typesafe-actions";
4 | import { mapReducers, ReducerMap } from "../../Lib/ReduxHelpers";
5 |
6 | /* ------------- Types and Action Creators ------------- */
7 | interface <%= pascalCase(props.name) %>SuccessParams {data: string; }
8 |
9 | const actionCreators = {
10 | request: createAction("<%= snakeCase(props.name).toUpperCase() %>_REQUEST"),
11 | success: (payload: <%= pascalCase(props.name) %>SuccessParams) => ({type: "<%= snakeCase(props.name).toUpperCase() %>_SUCCESS", payload})),
12 | failure: createAction("<%= snakeCase(props.name).toUpperCase() %>_FAILURE"),
13 | };
14 |
15 | export const <%= pascalCase(props.name) %>Actions = actionCreators;
16 |
17 | export interface <%= pascalCase(props.name) %>State {
18 | data?: string | null;
19 | error?: boolean | null;
20 | fetching?: boolean | null;
21 | }
22 |
23 | export type <%= pascalCase(props.name) %>Action = PayloadActionState>;
24 |
25 | export type Immutable<%= pascalCase(props.name) %>State = SI.ImmutableObject<<%= pascalCase(props.name) %>State>;
26 |
27 | /* ------------- Initial State ------------- */
28 |
29 | export const INITIAL_STATE: Immutable<%= pascalCase(props.name) %>State = SI.from({
30 | data: null,
31 | error: null,
32 | fetching: null,
33 | });
34 |
35 | /* ------------- Reducers ------------- */
36 |
37 | export const request: ReducerState> =
38 | (state: Immutable<%= pascalCase(props.name) %>State) =>
39 | state.merge({ fetching: true });
40 |
41 | export const success: ReducerState> =
42 | (state: Immutable<%= pascalCase(props.name) %>State, action: AnyAction & {payload?: <%= pascalCase(props.name) %>SuccessParams}) => {
43 | if (!action.payload) {
44 | return failure(state, action);
45 | }
46 | const { data } = action.payload;
47 |
48 | return state.merge({ fetching: false, error: null, data });
49 | };
50 |
51 | export const failure: ReducerState> = (state: Immutable<%= pascalCase(props.name) %>State) =>
52 | state.merge({ fetching: false, error: true, data: null });
53 |
54 | /* ------------- Hookup Reducers To Types ------------- */
55 |
56 | const reducerMap: ReducerMapState> = {
57 | request,
58 | failure,
59 | success,
60 | };
61 |
62 | export const <%= pascalCase(props.name) %>Reducer = mapReducers(INITIAL_STATE, reducerMap, actionCreators);
63 |
64 | export default <%= pascalCase(props.name) %>Reducer;
65 |
--------------------------------------------------------------------------------
/boilerplate/App/Services/GithubApi.tsx:
--------------------------------------------------------------------------------
1 | // a library to wrap and simplify api calls
2 | import {ApiResponse, create as apicreate} from "apisauce";
3 |
4 | export interface GithubApi {
5 | getRoot: () => Promise>;
6 | getRate: () => Promise>;
7 | getUser: (username: string) => Promise>;
8 | }
9 |
10 | export interface GithubUser {
11 | login: string;
12 | id: number;
13 | avatar_url: string;
14 | gravatar_id: string;
15 | url: string;
16 | html_url: string;
17 | followers_url: string;
18 | following_url: string;
19 | gists_url: string;
20 | starred_url: string;
21 | subscriptions_url: string;
22 | organizations_url: string;
23 | repos_url: string;
24 | events_url: string;
25 | received_events_url: string;
26 | type: "User";
27 | site_admin: boolean;
28 | score: number;
29 | }
30 |
31 | export interface GithubResponse {
32 | total_count: number;
33 | incomplete_results: false;
34 | items: GithubUser[];
35 | }
36 |
37 | // our "constructor"
38 | export const createAPI = (baseURL = "https://api.github.com/"): GithubApi => {
39 | // ------
40 | // STEP 1
41 | // ------
42 | //
43 | // Create and configure an apisauce-based api object.
44 | //
45 | const api = apicreate({
46 | // base URL is read from the "constructor"
47 | baseURL,
48 | // here are some default headers
49 | headers: {
50 | "Cache-Control": "no-cache",
51 | },
52 | // 10 second timeout...
53 | timeout: 10000,
54 | });
55 |
56 | // ------
57 | // STEP 2
58 | // ------
59 | //
60 | // Define some functions that call the api. The goal is to provide
61 | // a thin wrapper of the api layer providing nicer feeling functions
62 | // rather than "get", "post" and friends.
63 | //
64 | // I generally don't like wrapping the output at this level because
65 | // sometimes specific actions need to be take on `403` or `401`, etc.
66 | //
67 | // Since we can't hide from that, we embrace it by getting out of the
68 | // way at this level.
69 | //
70 | const getRoot = () => api.get("");
71 | const getRate = () => api.get("rate_limit");
72 | const getUser = (username: string) => api.get("search/users", {q: username});
73 |
74 | // ------
75 | // STEP 3
76 | // ------
77 | //
78 | // Return back a collection of functions that we would consider our
79 | // interface. Most of the time it'll be just the list of all the
80 | // methods in step 2.
81 | //
82 | // Notice we're not returning back the `api` created in step 1? That's
83 | // because it is scoped privately. This is one way to create truly
84 | // private scoped goodies in JavaScript.
85 | //
86 | return {
87 | // a list of the API functions from step 2
88 | getRoot,
89 | getRate,
90 | getUser,
91 | };
92 | };
93 |
94 | // let's return back our create method as the default.
95 | export default {
96 | createAPI,
97 | };
98 |
--------------------------------------------------------------------------------
/templates/flatlist.ejs:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { View, Text, FlatList } from "react-native";
3 |
4 | // More info here: https://facebook.github.io/react-native/docs/flatlist.html
5 |
6 | // Styles
7 | import styles from "./<%= props.name %>Style";
8 |
9 | export interface RowItem {
10 | title: string;
11 | description: string;
12 | }
13 | interface Props {
14 | data: RowItem[];
15 | }
16 |
17 | export default class <%= props.name %> extends React.PureComponent {
18 |
19 | /* ***********************************************************
20 | * `renderRow` function. How each cell/row should be rendered
21 | * It's our best practice to place a single component here:
22 | *
23 | * e.g.
24 | return
25 | *************************************************************/
26 | renderRow ({item}: {item: RowItem}) {
27 | return (
28 |
29 | {item.title}
30 | {item.description}
31 |
32 | )
33 | }
34 |
35 | // Render a header?
36 | renderHeader = () =>
37 | - Header -
38 |
39 | // Render a footer?
40 | renderFooter = () =>
41 | - Footer -
42 |
43 | // Show this when data is empty
44 | renderEmpty = () =>
45 | - Nothing to See Here -
46 |
47 | renderSeparator = () =>
48 | - ~~~~~ -
49 |
50 | // The default function if no Key is provided is index
51 | // an identifiable key is important if you plan on
52 | // item reordering. Otherwise index is fine
53 | keyExtractor = (item: RowItem, index: number) => index
54 |
55 | // How many items should be kept im memory as we scroll?
56 | oneScreensWorth = 20
57 |
58 | // extraData is for anything that is not indicated in data
59 | // for instance, if you kept "favorites" in `this.state.favs`
60 | // pass that in, so changes in favorites will cause a re-render
61 | // and your renderItem will have access to change depending on state
62 | // e.g. `extraData`={this.state.favs}
63 |
64 | // Optimize your list if the height of each item can be calculated
65 | // by supplying a constant height, there is no need to measure each
66 | // item after it renders. This can save significant time for lists
67 | // of a size 100+
68 | // e.g. itemLayout={(data, index) => (
69 | // {length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index}
70 | // )}
71 |
72 | render () {
73 | return (
74 |
75 |
86 |
87 | )
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/templates/flatlist-grid.ejs:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { View, Text, FlatList } from "react-native";
3 | import { connect } from "react-redux";
4 |
5 | // More info here: https://facebook.github.io/react-native/docs/flatlist.html
6 |
7 | // Styles
8 | import styles from "./<%= props.name %>Style";
9 |
10 | export interface RowItem {
11 | title: string;
12 | description: string;
13 | }
14 | interface Props {
15 | data: RowItem[];
16 | }
17 |
18 | export default class <%= props.name %> extends React.PureComponent {
19 |
20 | /* ***********************************************************
21 | * `renderRow` function. How each cell/row should be rendered
22 | * It's our best practice to place a single component here:
23 | *
24 | * e.g.
25 | return
26 | *************************************************************/
27 | renderRow ({item}: {item: RowItem}) {
28 | return (
29 |
30 | {item.title}
31 | {item.description}
32 |
33 | )
34 | }
35 |
36 | // Render a header?
37 | renderHeader = () =>
38 | - Header -
39 |
40 | // Render a footer?
41 | renderFooter = () =>
42 | - Footer -
43 |
44 | // Show this when data is empty
45 | renderEmpty = () =>
46 | - Nothing to See Here -
47 |
48 | renderSeparator = () =>
49 | - ~~~~~ -
50 |
51 | // The default function if no Key is provided is index
52 | // an identifiable key is important if you plan on
53 | // item reordering. Otherwise index is fine
54 | keyExtractor = (item: RowItem, index: number) => index
55 |
56 | // How many items should be kept im memory as we scroll?
57 | oneScreensWorth = 20
58 |
59 | // extraData is for anything that is not indicated in data
60 | // for instance, if you kept "favorites" in `this.state.favs`
61 | // pass that in, so changes in favorites will cause a re-render
62 | // and your renderItem will have access to change depending on state
63 | // e.g. `extraData`={this.state.favs}
64 |
65 | // Optimize your list if the height of each item can be calculated
66 | // by supplying a constant height, there is no need to measure each
67 | // item after it renders. This can save significant time for lists
68 | // of a size 100+
69 | // e.g. itemLayout={(data, index) => (
70 | // {length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index}
71 | // )}
72 |
73 | render () {
74 | return (
75 |
76 |
88 |
89 | )
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/boilerplate/package.json.ejs:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.0.1",
3 | "scripts": {
4 | "start": "node node_modules/react-native/local-cli/cli.js start",
5 | "test": "jest",
6 | "clean": "rm -rf $TMPDIR/react-* && watchman watch-del-all && npm cache clean --force",
7 | "clean:android": "cd android/ && ./gradlew clean && cd .. && react-native run-android",
8 | "newclear": "rm -rf $TMPDIR/react-* && watchman watch-del-all && rm -rf ios/build && rm -rf node_modules/ && npm cache clean --force && npm i",
9 | "test:watch": "jest --watch",
10 | "updateSnapshot": "jest --updateSnapshot",
11 | "coverage": "jest --coverage && open coverage/lcov-report/index.html || xdg-open coverage/lcov-report/index.html",
12 | "android:build": "cd android && ./gradlew assembleRelease",
13 | "android:install": "cd android && ./gradlew assembleRelease && ./gradlew installRelease",
14 | "android:hockeyapp": "cd android && ./gradlew assembleRelease && puck -submit=auto app/build/outputs/apk/app-release.apk",
15 | "android:devices": "$ANDROID_HOME/platform-tools/adb devices",
16 | "android:logcat": "$ANDROID_HOME/platform-tools/adb logcat *:S ReactNative:V ReactNativeJS:V",
17 | "android:shake": "$ANDROID_HOME/platform-tools/adb devices | grep '\\t' | awk '{print $1}' | sed 's/\\s//g' | xargs -I {} $ANDROID_HOME/platform-tools/adb -s {} shell input keyevent 82",
18 | "storybook": "storybook start -p 7007",
19 | "lint": "tslint --project . -e '**/*.js' -t verbose | snazzy",
20 | "lintdiff": "git diff --name-only --cached --relative | grep '\\.tsx?$' | xargs tslint | snazzy",
21 | "fixcode": "tslint --project . -e '**/*.js' --fix",
22 | "git-hook": "npm run lint -s && npm run test -s"
23 | },
24 | "dependencies": {
25 | "apisauce": "^0.14.0",
26 | "format-json": "^1.0.3",
27 | "lodash": "^4.17.2",
28 | "querystringify": "0.0.4",
29 | "ramda": "^0.24.1",
30 | "react-native-config": "^0.6.0",
31 | "react-native-drawer": "^2.3.0",
32 | "react-navigation": "^1.0.0-beta.7",
33 | "react-redux": "^5.0.2",
34 | "react-redux-typescript": "^2.3.0",
35 | "redux": "^3.6.0",
36 | "redux-persist": "^4.1.0",
37 | "redux-saga": "^0.15.6",
38 | "seamless-immutable": "^7.0.1",
39 | "typesafe-actions": "1.1.2"
40 | },
41 | "devDependencies": {
42 | "@storybook/addon-storyshots": "^3.2.3",
43 | "@storybook/react-native": "^3.2.3",
44 | "@types/enzyme": "^3.1.0",
45 | "@types/ramda": "^0.24.17",
46 | "@types/react-navigation": "^1.0.21",
47 | "@types/react-redux": "^5.0.10",
48 | "@types/redux": "^3.6.0",
49 | "@types/jest": "^21.1.4",
50 | "@types/react": "^16.0.13",
51 | "@types/react-native": "^0.49.2",
52 | "@types/react-test-renderer": "^16.0.0",
53 | "@types/seamless-immutable": "^7.1.1",
54 | "@types/storybook__react": "^3.0.5",
55 | "@types/webpack-env": "^1.13.2",
56 | "babel-jest": "21.2.0",
57 | "babel-plugin-ignite-ignore-reactotron": "^0.3.0",
58 | "babel-preset-es2015": "^6.18.0",
59 | "babel-preset-react-native": "3.0.2",
60 | "enzyme": "^2.6.0",
61 | "jest-preset-typescript-react-native": "^1.2.0",
62 | "husky": "^0.13.1",
63 | "ignite-animatable": "^1.0.0",
64 | "ignite-dev-screens": "^2.2.0",
65 | "ignite-vector-icons": "^1.1.0",
66 | "jest": "^21.2.1",
67 | "mockery": "^2.0.0",
68 | "react-addons-test-utils": "~15.4.1",
69 | "react-dom": "16.0.0-alpha.12",
70 | "react-test-renderer": "16.0.0-beta.5",
71 | "reactotron-react-native": "^1.12.0",
72 | "reactotron-redux": "^1.11.1",
73 | "reactotron-redux-saga": "^1.11.1",
74 | "snazzy": "^7.0.0",
75 | "ts-jest": "^21.1.3",
76 | "tslint": "^5.7.0",
77 | "tslint-react": "^3.2.0",
78 | "typescript": "^2.5.3",
79 | "react-native-typescript-transformer": "^1.1.4"
80 | },
81 | "jest": {
82 | "preset": "jest-preset-typescript-react-native",
83 | "testMatch": [
84 | "**/Tests/**/*.ts?(x)",
85 | "**/App/**/*Test.ts?(x)"
86 | ],
87 | "testPathIgnorePatterns": [
88 | "\\.snap$",
89 | "/node_modules/",
90 | "/lib/",
91 | "Tests/Setup"
92 | ],
93 | "setupFiles": [
94 | "./Tests/Setup.tsx"
95 | ],
96 | "moduleFileExtensions": [
97 | "js",
98 | "jsx",
99 | "ts",
100 | "tsx",
101 | "json"
102 | ],
103 | "cacheDirectory": ".jest/cache"
104 | },
105 | "config": {}
106 | }
107 |
--------------------------------------------------------------------------------
/commands/list.js:
--------------------------------------------------------------------------------
1 | // @cliDescription Generates a screen with a ListView/Flatlist/SectionList + walkthrough.
2 |
3 | const patterns = require('../lib/patterns')
4 |
5 | module.exports = async function (context) {
6 | // grab some features
7 | const { print, parameters, strings, ignite, filesystem } = context
8 | const { pascalCase, isBlank } = strings
9 | const config = ignite.loadIgniteConfig()
10 |
11 | // validation
12 | if (isBlank(parameters.first)) {
13 | print.info(`${context.runtime.brand} generate list \n`)
14 | print.info('A name is required.')
15 | return
16 | }
17 |
18 | const name = pascalCase(parameters.first)
19 | const props = { name }
20 |
21 |
22 | // which type of layout?
23 | const typeMessage = 'What kind of List would you like to generate?'
24 | const typeChoices = ['Row', 'Grid']
25 |
26 | // Sections or no?
27 | const typeDataMessage = 'How will your data be presented on this list?'
28 | const typeDataChoices = ['Single', 'Sectioned']
29 |
30 | // Check for parameters to bypass questions
31 | let typeCode = parameters.options.codeType
32 | let type = parameters.options.type
33 | let dataType = parameters.options.dataType
34 |
35 | // only prompt if type is not defined
36 | if (!typeCode) {
37 | typeCode = 'flatlist';
38 | }
39 |
40 | if (!type) {
41 | // ask question 2
42 | const answers = await context.prompt.ask({
43 | name: 'type',
44 | type: 'list',
45 | message: typeMessage,
46 | choices: typeChoices
47 | })
48 | type = answers.type
49 | }
50 |
51 | if (!dataType) {
52 | // ask question 3
53 | const dataAnswers = await context.prompt.ask({
54 | name: 'type',
55 | type: 'list',
56 | message: typeDataMessage,
57 | choices: typeDataChoices
58 | })
59 | dataType = dataAnswers.type
60 | }
61 |
62 | // Sorry the following is so confusing, but so are React Native lists
63 | // There are 3 options and therefore 8 possible combinations
64 | let componentTemplate = dataType.toLowerCase() === 'sectioned'
65 | ? typeCode + '-sections'
66 | : typeCode
67 | let styleTemplate = ''
68 | // Different logic depending on code types
69 | if (typeCode === 'flatlist') {
70 | /*
71 | * The following mess is because FlatList supports numColumns
72 | * where SectionList does not.
73 | */
74 | if (type.toLowerCase() === 'grid' && dataType.toLowerCase() === 'sectioned') {
75 | // grid + section means we need wrap
76 | styleTemplate = 'listview-grid-style'
77 | } else if (type.toLowerCase() === 'grid') {
78 | componentTemplate = componentTemplate + '-grid'
79 | // grid + single = no wrap, use columns
80 | styleTemplate = 'flatlist-grid-style'
81 | } else {
82 | // no grids, flatlist basic
83 | styleTemplate = 'listview-style'
84 | }
85 | } else {
86 | // listview builder
87 | styleTemplate = type.toLowerCase() === 'grid'
88 | ? 'listview-grid-style'
89 | : 'listview-style'
90 | }
91 |
92 | const jobs = [
93 | {
94 | template: `${componentTemplate}.ejs`,
95 | target: `App/Containers/${name}/${name}.tsx`
96 | },
97 | {
98 | template: `${styleTemplate}.ejs`,
99 | target: `App/Containers/${name}/${name}Style.ts`
100 | }
101 | ]
102 |
103 | await ignite.copyBatch(context, jobs, props)
104 |
105 | // if using `react-navigation` go the extra step
106 | // and insert the screen into the nav router
107 | if (config.navigation === 'react-navigation') {
108 | const screenName = `${name}`
109 | const appNavFilePath = `${process.cwd()}/App/Navigation/AppNavigation.tsx`
110 | const importToAdd = `import { ${screenName} } from "../Containers/${screenName}";`
111 | const routeToAdd = ` ${screenName}: { screen: ${screenName} },`
112 |
113 | if (!filesystem.exists(appNavFilePath)) {
114 | const msg = `No '${appNavFilePath}' file found. Can't insert list screen.`
115 | print.error(msg)
116 | process.exit(1)
117 | }
118 |
119 | // insert list screen import
120 | ignite.patchInFile(appNavFilePath, {
121 | after: patterns[patterns.constants.PATTERN_IMPORTS],
122 | insert: importToAdd
123 | })
124 |
125 | // insert list screen route
126 | ignite.patchInFile(appNavFilePath, {
127 | after: patterns[patterns.constants.PATTERN_ROUTES],
128 | insert: routeToAdd
129 | })
130 | } else {
131 | print.info('List screen created, manually add it to your navigation')
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/boilerplate/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Basic Options */
4 | "target": "ESNEXT", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */
5 | "module": "ESNext", /* Specify module code generation: 'none', commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
6 | // "lib": [], /* Specify library files to be included in the compilation: */
7 | // "allowJs": true, /* Allow javascript files to be compiled. */
8 | // "checkJs": true, /* Report errors in .js files. */
9 | "jsx": "react-native", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
10 | // "declaration": true, /* Generates corresponding '.d.ts' file. */
11 | "sourceMap": true, /* Generates corresponding '.map' file. */
12 | // "outFile": "./", /* Concatenate and emit output to single file. */
13 | //"outDir": "./dist", /* Redirect output structure to the directory. */
14 | // "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
15 | // "removeComments": true, /* Do not emit comments to output. */
16 | // "noEmit": true, /* Do not emit outputs. */
17 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */
18 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
19 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
20 |
21 | /* Strict Type-Checking Options */
22 | "strict": true, /* Enable all strict type-checking options. */
23 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
24 | // "strictNullChecks": true, /* Enable strict null checks. */
25 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
26 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
27 |
28 | /* Additional Checks */
29 | // "noUnusedLocals": true, /* Report errors on unused locals. */
30 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
31 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
32 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
33 |
34 | /* Module Resolution Options */
35 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
36 | "baseUrl": "types", /* Base directory to resolve non-absolute module names. */
37 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
38 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
39 | "typeRoots": ["types"], /* List of folders to include type definitions from. */
40 | // "types": [], /* Type declaration files to be included in compilation. */
41 | "allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
42 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
43 |
44 | /* Source Map Options */
45 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
46 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */
47 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
48 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
49 |
50 | /* Experimental Options */
51 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
52 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
53 | }
54 | // "include": [
55 | // "index.ts", "App/Components/Stories.tsx"
56 | // ]
57 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Basic Options */
4 | "target": "ESNEXT", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */
5 | "module": "ESNext", /* Specify module code generation: 'none', commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
6 | // "lib": [], /* Specify library files to be included in the compilation: */
7 | "allowJs": true, /* Allow javascript files to be compiled. */
8 | // "checkJs": true, /* Report errors in .js files. */
9 | "jsx": "react-native", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
10 | // "declaration": true, /* Generates corresponding '.d.ts' file. */
11 | "sourceMap": true, /* Generates corresponding '.map' file. */
12 | // "outFile": "./", /* Concatenate and emit output to single file. */
13 | "outDir": "./dist", /* Redirect output structure to the directory. */
14 | // "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
15 | // "removeComments": true, /* Do not emit comments to output. */
16 | // "noEmit": true, /* Do not emit outputs. */
17 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */
18 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
19 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
20 |
21 | /* Strict Type-Checking Options */
22 | "strict": true, /* Enable all strict type-checking options. */
23 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
24 | // "strictNullChecks": true, /* Enable strict null checks. */
25 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
26 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
27 |
28 | /* Additional Checks */
29 | // "noUnusedLocals": true, /* Report errors on unused locals. */
30 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
31 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
32 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
33 |
34 | /* Module Resolution Options */
35 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
36 | "baseUrl": "./boilerplate/", /* Base directory to resolve non-absolute module names. */
37 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
38 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
39 | "typeRoots": ["./boilerplate/types/", "./node_modules/"], /* List of folders to include type definitions from. */
40 | // "types": [], /* Type declaration files to be included in compilation. */
41 | "allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
42 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
43 |
44 | /* Source Map Options */
45 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
46 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */
47 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
48 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
49 |
50 | /* Experimental Options */
51 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
52 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
53 | }
54 | }
--------------------------------------------------------------------------------
/templates/flatlist-sections.ejs:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { View, SectionList, Text } from "react-native";
3 |
4 | // More info here: https://facebook.github.io/react-native/docs/sectionlist.html
5 |
6 | // Styles
7 | import styles from "./<%= props.name %>Style";
8 |
9 | export interface Section {
10 | key: string;
11 | data: RowItem[];
12 | }
13 | export interface RowItem {
14 | title: string;
15 | description: string;
16 | }
17 | interface Props {
18 | data: Section[];
19 | }
20 |
21 | class <%= props.name %> extends React.PureComponent {
22 |
23 |
24 | /* ***********************************************************
25 | * `renderItem` function - How each cell should be rendered
26 | * It's our best practice to place a single component here:
27 | *
28 | * e.g.
29 | * return
30 | *
31 | * For sections with different cells (heterogeneous lists), you can do branch
32 | * logic here based on section.key OR at the data level, you can provide
33 | * `renderItem` functions in each section.
34 | *
35 | * Note: You can remove section/separator functions and jam them in here
36 | *************************************************************/
37 | renderItem ({section, item}: {section: Section, item: RowItem}) {
38 | return (
39 |
40 | Section {section.key} - {item.title}
41 | {item.description}
42 |
43 | )
44 | }
45 |
46 | // Conditional branching for section headers, also see step 3
47 | renderSectionHeader ({section}: {section: Section}) {
48 | switch (section.key) {
49 | case "First":
50 | return First Section
51 | default:
52 | return Second Section
53 | }
54 | }
55 |
56 | /* ***********************************************************
57 | * Consider the configurations we've set below. Customize them
58 | * to your liking! Each with some friendly advice.
59 | *
60 | * Removing a function here will make SectionList use default
61 | *************************************************************/
62 | // Render a header?
63 | renderHeader = () =>
64 | - Full List Header -
65 |
66 | // Render a footer?
67 | renderFooter = () =>
68 | - Full List Footer -
69 |
70 | // Does each section need a footer?
71 | renderSectionFooter = () =>
72 | END SECTION!!!!
73 |
74 | // Show this when data is empty
75 | renderEmpty = () =>
76 | - Nothing to See Here -
77 |
78 | renderSeparator = () =>
79 | - ~~~~~ -
80 |
81 | renderSectionSeparator = () =>
82 | \/\/\/\/\/\/\/\/
83 |
84 | // The default function if no Key is provided is index
85 | // an identifiable key is important if you plan on
86 | // item reordering. Otherwise index is fine
87 | keyExtractor = (item, index) => index
88 |
89 | // How many items should be kept im memory as we scroll?
90 | oneScreensWorth = 20
91 |
92 | // extraData is for anything that is not indicated in data
93 | // for instance, if you kept "favorites" in `this.state.favs`
94 | // pass that in, so changes in favorites will cause a re-render
95 | // and your renderItem will have access to change depending on state
96 | // e.g. `extraData`={this.state.favs}
97 |
98 | // Optimize your list if the height of each item can be calculated
99 | // by supplying a constant height, there is no need to measure each
100 | // item after it renders. This can save significant time for lists
101 | // of a size 100+
102 | // e.g. itemLayout={(data, index) => (
103 | // {length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index}
104 | // )}
105 |
106 | render () {
107 | return (
108 |
109 |
124 |
125 | )
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/test/generators-integration.test.js:
--------------------------------------------------------------------------------
1 | const execa = require('execa')
2 | const jetpack = require('fs-jetpack')
3 | const tempy = require('tempy')
4 |
5 | const IGNITE = 'ignite'
6 | const APP = 'IntegrationTest'
7 | const BOILERPLATE = `${__dirname}/..`
8 | console.warn(BOILERPLATE)
9 | // calling the ignite cli takes a while
10 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 600000
11 |
12 | describe('without a linter', () => {
13 | beforeAll(async () => {
14 | // creates a new temp directory
15 | process.chdir(tempy.directory())
16 | await execa(IGNITE, ['new', APP, '--min', '--skip-git', '--no-lint', '--boilerplate', BOILERPLATE])
17 | process.chdir(APP)
18 | })
19 |
20 | test('does not have a linting script', async () => {
21 | expect(jetpack.read('package.json', 'json')['scripts']['lint']).toBe(undefined)
22 | })
23 | })
24 |
25 | describe('generators', () => {
26 | beforeAll(async () => {
27 | // creates a new temp directory
28 | process.chdir(tempy.directory())
29 | await execa(IGNITE, ['new', APP, '--min', '--skip-git', '--boilerplate', BOILERPLATE])
30 | process.chdir(APP)
31 | })
32 |
33 | test('generates a component', async () => {
34 | await execa(IGNITE, ['g', 'component', 'Test'], { preferLocal: false })
35 | expect(jetpack.exists('App/Components/Test.tsx')).toBe('file')
36 | expect(jetpack.exists('App/Components/Styles/TestStyle.ts')).toBe('file')
37 | const lint = await execa('npm', ['-s', 'run', 'lint'])
38 | expect(lint.stderr).toBe('')
39 | })
40 |
41 | test('generate listview of type row works', async () => {
42 | await execa(IGNITE, ['g', 'list', 'TestRow', '--type=Row', '--codeType=listview', '--dataType=Single'], { preferLocal: false })
43 | expect(jetpack.exists('App/Containers/TestRow.tsx')).toBe('file')
44 | expect(jetpack.exists('App/Containers/Styles/TestRowStyle.ts')).toBe('file')
45 | const lint = await execa('npm', ['run', 'lint'])
46 | expect(lint.stderr).toBe('')
47 | })
48 |
49 | test('generate flatlist of type row works', async () => {
50 | await execa(IGNITE, ['g', 'list', 'TestFlatRow', '--type=Row', '--codeType=flatlist', '--dataType=Single'], { preferLocal: false })
51 | expect(jetpack.exists('App/Containers/TestFlatRow.tsx')).toBe('file')
52 | expect(jetpack.exists('App/Containers/Styles/TestFlatRowStyle.ts')).toBe('file')
53 | const lint = await execa('npm', ['run', 'lint'])
54 | expect(lint.stderr).toBe('')
55 | })
56 |
57 | test('generate listview of sections works', async () => {
58 | await execa(IGNITE, ['g', 'list', 'TestSection', '--type=Row', '--codeType=listview', '--dataType=Sectioned'], { preferLocal: false })
59 | expect(jetpack.exists('App/Containers/TestSection.tsx')).toBe('file')
60 | expect(jetpack.exists('App/Containers/Styles/TestSectionStyle.ts')).toBe('file')
61 | const lint = await execa('npm', ['run', 'lint'])
62 | expect(lint.stderr).toBe('')
63 | })
64 |
65 | test('generate flatlist of sections works', async () => {
66 | await execa(IGNITE, ['g', 'list', 'TestFlatSection', '--type=Row', '--codeType=flatlist', '--dataType=Sectioned'], { preferLocal: false })
67 | expect(jetpack.exists('App/Containers/TestFlatSection.tsx')).toBe('file')
68 | expect(jetpack.exists('App/Containers/Styles/TestFlatSectionStyle.ts')).toBe('file')
69 | const lint = await execa('npm', ['run', 'lint'])
70 | expect(lint.stderr).toBe('')
71 | })
72 |
73 | test('generate listview of type grid works', async () => {
74 | await execa(IGNITE, ['g', 'list', 'TestGrid', '--type=Grid', '--codeType=listview', '--dataType=Single'], { preferLocal: false })
75 | expect(jetpack.exists('App/Containers/TestGrid.tsx')).toBe('file')
76 | expect(jetpack.exists('App/Containers/Styles/TestGridStyle.ts')).toBe('file')
77 | const lint = await execa('npm', ['run', 'lint'])
78 | expect(lint.stderr).toBe('')
79 | })
80 |
81 | test('generate redux works', async () => {
82 | await execa(IGNITE, ['g', 'redux', 'Test'], { preferLocal: false })
83 | expect(jetpack.exists('App/Redux/TestRedux.tsx')).toBe('file')
84 | const lint = await execa('npm', ['run', 'lint'])
85 | expect(lint.stderr).toBe('')
86 | })
87 |
88 | test('generate container works', async () => {
89 | await execa(IGNITE, ['g', 'container', 'Container'], { preferLocal: false })
90 | expect(jetpack.exists('App/Containers/Container.tsx')).toBe('file')
91 | expect(jetpack.exists('App/Containers/Styles/ContainerStyle.ts')).toBe('file')
92 | const lint = await execa('npm', ['run', 'lint'])
93 | expect(lint.stderr).toBe('')
94 | })
95 |
96 | test('generate saga works', async () => {
97 | await execa(IGNITE, ['g', 'saga', 'Test'], { preferLocal: false })
98 | expect(jetpack.exists('App/Sagas/TestSagas.tsx')).toBe('file')
99 | const lint = await execa('npm', ['run', 'lint'])
100 | expect(lint.stderr).toBe('')
101 | })
102 |
103 | test('generate screen works', async () => {
104 | await execa(IGNITE, ['g', 'screen', 'Test'], { preferLocal: false })
105 | expect(jetpack.exists('App/Containers/TestScreen.tsx')).toBe('file')
106 | expect(jetpack.exists('App/Containers/Styles/TestScreenStyle.ts')).toBe('file')
107 | const lint = await execa('npm', ['run', 'lint'])
108 | expect(lint.stderr).toBe('')
109 | })
110 | })
111 |
--------------------------------------------------------------------------------
/boilerplate/App/Fixtures/gantman.json:
--------------------------------------------------------------------------------
1 | {
2 | "total_count": 7,
3 | "incomplete_results": false,
4 | "items": [
5 | {
6 | "login": "GantMan",
7 | "id": 997157,
8 | "avatar_url": "https://avatars.githubusercontent.com/u/997157?v=3",
9 | "gravatar_id": "",
10 | "url": "https://api.github.com/users/GantMan",
11 | "html_url": "https://github.com/GantMan",
12 | "followers_url": "https://api.github.com/users/GantMan/followers",
13 | "following_url": "https://api.github.com/users/GantMan/following{/other_user}",
14 | "gists_url": "https://api.github.com/users/GantMan/gists{/gist_id}",
15 | "starred_url": "https://api.github.com/users/GantMan/starred{/owner}{/repo}",
16 | "subscriptions_url": "https://api.github.com/users/GantMan/subscriptions",
17 | "organizations_url": "https://api.github.com/users/GantMan/orgs",
18 | "repos_url": "https://api.github.com/users/GantMan/repos",
19 | "events_url": "https://api.github.com/users/GantMan/events{/privacy}",
20 | "received_events_url": "https://api.github.com/users/GantMan/received_events",
21 | "type": "User",
22 | "site_admin": false,
23 | "score": 122.12115
24 | },
25 | {
26 | "login": "vlad-G",
27 | "id": 13520880,
28 | "avatar_url": "https://avatars.githubusercontent.com/u/13520880?v=3",
29 | "gravatar_id": "",
30 | "url": "https://api.github.com/users/vlad-G",
31 | "html_url": "https://github.com/vlad-G",
32 | "followers_url": "https://api.github.com/users/vlad-G/followers",
33 | "following_url": "https://api.github.com/users/vlad-G/following{/other_user}",
34 | "gists_url": "https://api.github.com/users/vlad-G/gists{/gist_id}",
35 | "starred_url": "https://api.github.com/users/vlad-G/starred{/owner}{/repo}",
36 | "subscriptions_url": "https://api.github.com/users/vlad-G/subscriptions",
37 | "organizations_url": "https://api.github.com/users/vlad-G/orgs",
38 | "repos_url": "https://api.github.com/users/vlad-G/repos",
39 | "events_url": "https://api.github.com/users/vlad-G/events{/privacy}",
40 | "received_events_url": "https://api.github.com/users/vlad-G/received_events",
41 | "type": "User",
42 | "site_admin": false,
43 | "score": 12.69848
44 | },
45 | {
46 | "login": "gantmani",
47 | "id": 3034094,
48 | "avatar_url": "https://avatars.githubusercontent.com/u/3034094?v=3",
49 | "gravatar_id": "",
50 | "url": "https://api.github.com/users/gantmani",
51 | "html_url": "https://github.com/gantmani",
52 | "followers_url": "https://api.github.com/users/gantmani/followers",
53 | "following_url": "https://api.github.com/users/gantmani/following{/other_user}",
54 | "gists_url": "https://api.github.com/users/gantmani/gists{/gist_id}",
55 | "starred_url": "https://api.github.com/users/gantmani/starred{/owner}{/repo}",
56 | "subscriptions_url": "https://api.github.com/users/gantmani/subscriptions",
57 | "organizations_url": "https://api.github.com/users/gantmani/orgs",
58 | "repos_url": "https://api.github.com/users/gantmani/repos",
59 | "events_url": "https://api.github.com/users/gantmani/events{/privacy}",
60 | "received_events_url": "https://api.github.com/users/gantmani/received_events",
61 | "type": "User",
62 | "site_admin": false,
63 | "score": 11.641713
64 | },
65 | {
66 | "login": "sgantman",
67 | "id": 5911526,
68 | "avatar_url": "https://avatars.githubusercontent.com/u/5911526?v=3",
69 | "gravatar_id": "",
70 | "url": "https://api.github.com/users/sgantman",
71 | "html_url": "https://github.com/sgantman",
72 | "followers_url": "https://api.github.com/users/sgantman/followers",
73 | "following_url": "https://api.github.com/users/sgantman/following{/other_user}",
74 | "gists_url": "https://api.github.com/users/sgantman/gists{/gist_id}",
75 | "starred_url": "https://api.github.com/users/sgantman/starred{/owner}{/repo}",
76 | "subscriptions_url": "https://api.github.com/users/sgantman/subscriptions",
77 | "organizations_url": "https://api.github.com/users/sgantman/orgs",
78 | "repos_url": "https://api.github.com/users/sgantman/repos",
79 | "events_url": "https://api.github.com/users/sgantman/events{/privacy}",
80 | "received_events_url": "https://api.github.com/users/sgantman/received_events",
81 | "type": "User",
82 | "site_admin": false,
83 | "score": 7.926345
84 | },
85 | {
86 | "login": "michaelgantman",
87 | "id": 16693070,
88 | "avatar_url": "https://avatars.githubusercontent.com/u/16693070?v=3",
89 | "gravatar_id": "",
90 | "url": "https://api.github.com/users/michaelgantman",
91 | "html_url": "https://github.com/michaelgantman",
92 | "followers_url": "https://api.github.com/users/michaelgantman/followers",
93 | "following_url": "https://api.github.com/users/michaelgantman/following{/other_user}",
94 | "gists_url": "https://api.github.com/users/michaelgantman/gists{/gist_id}",
95 | "starred_url": "https://api.github.com/users/michaelgantman/starred{/owner}{/repo}",
96 | "subscriptions_url": "https://api.github.com/users/michaelgantman/subscriptions",
97 | "organizations_url": "https://api.github.com/users/michaelgantman/orgs",
98 | "repos_url": "https://api.github.com/users/michaelgantman/repos",
99 | "events_url": "https://api.github.com/users/michaelgantman/events{/privacy}",
100 | "received_events_url": "https://api.github.com/users/michaelgantman/received_events",
101 | "type": "User",
102 | "site_admin": false,
103 | "score": 7.926345
104 | },
105 | {
106 | "login": "gantmanis",
107 | "id": 19141249,
108 | "avatar_url": "https://avatars.githubusercontent.com/u/19141249?v=3",
109 | "gravatar_id": "",
110 | "url": "https://api.github.com/users/gantmanis",
111 | "html_url": "https://github.com/gantmanis",
112 | "followers_url": "https://api.github.com/users/gantmanis/followers",
113 | "following_url": "https://api.github.com/users/gantmanis/following{/other_user}",
114 | "gists_url": "https://api.github.com/users/gantmanis/gists{/gist_id}",
115 | "starred_url": "https://api.github.com/users/gantmanis/starred{/owner}{/repo}",
116 | "subscriptions_url": "https://api.github.com/users/gantmanis/subscriptions",
117 | "organizations_url": "https://api.github.com/users/gantmanis/orgs",
118 | "repos_url": "https://api.github.com/users/gantmanis/repos",
119 | "events_url": "https://api.github.com/users/gantmanis/events{/privacy}",
120 | "received_events_url": "https://api.github.com/users/gantmanis/received_events",
121 | "type": "User",
122 | "site_admin": false,
123 | "score": 7.8813524
124 | },
125 | {
126 | "login": "Gantman2014",
127 | "id": 7669410,
128 | "avatar_url": "https://avatars.githubusercontent.com/u/7669410?v=3",
129 | "gravatar_id": "",
130 | "url": "https://api.github.com/users/Gantman2014",
131 | "html_url": "https://github.com/Gantman2014",
132 | "followers_url": "https://api.github.com/users/Gantman2014/followers",
133 | "following_url": "https://api.github.com/users/Gantman2014/following{/other_user}",
134 | "gists_url": "https://api.github.com/users/Gantman2014/gists{/gist_id}",
135 | "starred_url": "https://api.github.com/users/Gantman2014/starred{/owner}{/repo}",
136 | "subscriptions_url": "https://api.github.com/users/Gantman2014/subscriptions",
137 | "organizations_url": "https://api.github.com/users/Gantman2014/orgs",
138 | "repos_url": "https://api.github.com/users/Gantman2014/repos",
139 | "events_url": "https://api.github.com/users/Gantman2014/events{/privacy}",
140 | "received_events_url": "https://api.github.com/users/Gantman2014/received_events",
141 | "type": "User",
142 | "site_admin": false,
143 | "score": 7.8813524
144 | }
145 | ]
146 | }
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | ## Ignite TypeScript Boilerplate for React Native
2 |
3 | ### The easiest way to develop React Native apps in TypeScript.
4 | Get up and running with TypeScript React Native development in minutes. A batteries-included, opinionated starter project, and code generators for your components, reducers, sagas and more.
5 | Originally based on a port of the [Ignite IR Boilerplate](https://github.com/infinitered/ignite-ir-boilerplate) to TypeScript.
6 |
7 | Currently includes:
8 |
9 | * React Native 0.51.0 (but you can change this if you want to experiment)
10 | * React Navigation
11 | * Redux
12 | * Redux Sagas
13 | * And more!
14 |
15 | ## Quick Start
16 |
17 | When you've installed the [Ignite CLI](https://github.com/infinitered/ignite), (tl;dr: `npm install -g ignite-cli`) you can get started with this boilerplate like this:
18 |
19 | ```sh
20 | ignite new MyLatestCreation --b ignite-typescript-boilerplate
21 | ```
22 |
23 | You can also change the React Native version, just keep in mind, we may not have tested this just yet.
24 |
25 | ```sh
26 | ignite new MyLatestCreation --b ignite-typescript-boilerplate --react-native-version 0.46.0-rc.2
27 | ```
28 |
29 | By default we'll ask you some questions during install as to which features you'd like. If you just want them all, you can skip the questions:
30 |
31 | ```sh
32 | ignite new MyLatestCreation --b ignite-typescript-boilerplate --max
33 | ```
34 |
35 | If you want very few of these extras:
36 |
37 | ```sh
38 | ignite new MyLatestCreation --b ignite-typescript-boilerplate --min
39 | ```
40 |
41 | ## Using TypeScript with React Native
42 |
43 | Thanks to the beauty of [react-native-typescript-transformer](https://github.com/ds300/react-native-typescript-transformer), we can seamlessly use TypeScript in our React Native project. Source maps and hot reloading all work just like you would expect.
44 |
45 | ## Coding style
46 |
47 | We use `tslint` to enforce coding style, with rules based on [Palantir's tslint-react](https://github.com/palantir/tslint-react),
48 | and a few changes to accommodate some Ignite quirks. If you install a plugin, your editor can probably automatically fix problems.
49 | In VS Code, set `"tslint.autoFixOnSave": true` in your
50 | workspace settings. You can run the linter from the command line. `npm run lint` runs the linter, while `npm run fixcode` tries to autofix problems.
51 |
52 | ## Boilerplate walkthrough
53 |
54 | Your `App` folder is where most of the goodies are found in an Ignite app. Let's walk through them in more detail. Start with `Containers/App.tsx` (described below) and work your way down the walkthrough in order.
55 |
56 | ### Components
57 |
58 | React components go here. We generate these as stateless functional components by default, as recommended by the React team.
59 |
60 | ### Containers
61 |
62 | Containers are Redux-connected components, and are mostly full screens.
63 |
64 | * `App.tsx` - your main application. We create a Redux store and configure it here
65 | * `RootContainer.tsx` - main view of your application. Contains your status bar and navigation component
66 | * `LaunchScreen.tsx` - this is the first screen shown in your application. It's loaded into the Navigation component
67 |
68 | ### Navigation
69 |
70 | Your primary and other navigation components reside here.
71 |
72 | * `AppNavigation.tsx` - loads in your initial screen and creates your menu(s) in a StackNavigation
73 | * `Styles` - styling for the navigation
74 |
75 | ### Storybook
76 |
77 | [Storybook](https://storybook.js.org/) has been setup to show off components in the different states. Storybook is a great way to develop and test components outside of use in your app. Simply run `yarn run storybook` to get started. All stories are contained in the `*.story.tsx` files along side the components.
78 |
79 | ### Themes
80 |
81 | Styling themes used throughout your app styles.
82 |
83 | * `ApplicationStyles.ts` - app-wide styles
84 | * `Colors.ts` - defined colors for your app
85 | * `Fonts.ts` - defined fonts for your app
86 | * `Images.ts` - loads and caches images used in your app
87 | * `Metrics.ts` - useful measurements of things like navBarHeight
88 |
89 | ### Config
90 |
91 | Initialize and configure things here.
92 |
93 | * `AppConfig.ts` - simple React Native configuration here
94 | * `DebugConfig.js` - define how you want your debug environment to act. This is a .js file because that's what
95 | Ignite expects to find.
96 | * `ReactotronConfig.ts` - configures [Reactotron](https://github.com/infinitered/reactotron) in your project (Note: this [will be extracted](https://github.com/infinitered/ignite/issues/779) into a plugin in the future)
97 |
98 | ### Fixtures
99 |
100 | Contains json files that mimic API responses for quicker development. These are used by the `Services/FixtureApi.ts` object to mock API responses.
101 |
102 | ### Redux, Sagas
103 |
104 | Contains a preconfigured Redux and Redux-Sagas setup. Review each file carefully to see how Redux interacts with your application. You will find these in the Reducers and Sagas folders. We use [typesafe-actions](https://github.com/piotrwitek/typesafe-actions) to get lovely
105 | type checking of our reducers and actions. Take a look at `Lib/ReduxHelpers.ts` for some extra functions that
106 | we use to make them more Ignite-y.
107 |
108 | ### Services
109 |
110 | Contains your API service and other important utilities for your application.
111 |
112 | * `Api.tsx` - main API service, giving you an interface to communicate with your back end
113 | * `ExamplesRegistry.tsx` - lets you view component and Ignite plugin examples in your app
114 | * `FixtureApi.tsx` - mocks your API service, making it faster to develop early on in your app
115 |
116 |
117 | ### Lib
118 |
119 | We recommend using this folder for modules that can be extracted into their own NPM packages at some point.
120 |
121 | ### Images
122 |
123 | Contains actual images (usually png) used in your application.
124 |
125 | ### Transforms
126 |
127 | Helpers for transforming data between API and your application and vice versa. An example is provided that you can look at to see how it works.
128 |
129 | ### Tests
130 |
131 | We create Jest tests alongside the components, reducers and sagas. Enable this by adding `"tests": "jest"` to `ignite/ignite.json`.
132 |
133 | ### Code generation
134 |
135 | Currently, the following code generation commands work properly:
136 | * `ignite generate component MyComponent` - generates a stateless functional component.
137 | * `ignite generate container MyContainer` - generates a Redux-connected React.Component, with state and view lifecycle.
138 | * `ignite generate screen MyScreen` - generates a Redux-connected React.Component, with state, view lifecycle and react-navigation.
139 | * `ignite generate reducers MyNew` - generates a set of Redux reducers.
140 | * `ignite generate saga MySaga` - generates a Redux Saga
141 | * `ignite generate list MyList` - generates a FlatList, formatted either as a grid or list.
142 |
143 | ### Further reading
144 |
145 | A comprehensive guide to best practice with TypeScript in React is [the React Redux TypeScript Guide](https://github.com/piotrwitek/react-redux-typescript-guide), which covers a lot more than just Redux. We have adopted a lot of the patterns from this. The `typesafe-actions` library that we use was created by @piotrwitek, the author of the guide.
146 |
147 | Microsoft created [TypeScript React Native Starter](https://github.com/Microsoft/TypeScript-React-Native-Starter), which includes a walkthrough on switching projects to TypeScript.
148 |
149 | [React TypeScript Tutorial](https://github.com/DanielRosenwasser/React-TypeScript-Tutorial) is React rather than React Native, but has useful guides.
150 |
151 | [This post](http://blog.novanet.no/easy-typescript-with-react-native/) is a good run-through of the [react-native-typescript-transfomer](https://github.com/ds300/react-native-typescript-transformer), which allows us to skip the transpile step that we were using before. Thanks [@wormyy] for the heads-up on this.
152 |
153 | ### Credits
154 | Created by [Matt Kane](https://github.com/ascorbic) at [Aerian Studios](https://www.aerian.com). Based on [Ignite IR Boilerplate](https://github.com/infinitered/ignite-ir-boilerplate), by Infinite Red.
--------------------------------------------------------------------------------
/boilerplate.js:
--------------------------------------------------------------------------------
1 | const options = require('./options')
2 | const { merge, pipe, assoc, omit, __ } = require('ramda')
3 | const { getReactNativeVersion } = require('./lib/react-native-version')
4 |
5 | /**
6 | * Is Android installed?
7 | *
8 | * $ANDROID_HOME/tools folder has to exist.
9 | *
10 | * @param {*} context - The gluegun context.
11 | * @returns {boolean}
12 | */
13 | const isAndroidInstalled = function (context) {
14 | const androidHome = process.env['ANDROID_HOME']
15 | const hasAndroidEnv = !context.strings.isBlank(androidHome)
16 | const hasAndroid = hasAndroidEnv && context.filesystem.exists(`${androidHome}/tools`) === 'dir'
17 |
18 | return Boolean(hasAndroid)
19 | }
20 |
21 | /**
22 | * Let's install.
23 | *
24 | * @param {any} context - The gluegun context.
25 | */
26 | async function install (context) {
27 | const {
28 | filesystem,
29 | parameters,
30 | ignite,
31 | reactNative,
32 | print,
33 | system,
34 | prompt,
35 | template
36 | } = context
37 | const { colors } = print
38 | const { red, yellow, bold, gray, blue, green } = colors
39 |
40 | const perfStart = (new Date()).getTime()
41 |
42 | const name = parameters.third
43 | const logo = red(`
44 |
45 |
46 | -aeaeaeaeaeae—
47 | -eaeaeaeaeaeaeaeaeaeae-
48 | /aeaeaeaeaeaeaeaeaeaeaeaeae\\
49 | /aeaeaeaeaeaeaeaeaeaeaeaeaeaeae\\
50 | /eaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaea\\
51 | /aeaeaeaeaeaeaeaeaeaea/ |aeaeaeaeaeae\\
52 | /aaeaeaeaeaeaeaeaeaeae/ |aeaeaeaeaeaea\\
53 | aeaeaeaeaeaeaeaeaeae/ |eaeaeaeaeaeaea
54 | |aeaeaeaeaeaeaeaeae/ |eaeaeaeaeaeaea|
55 | aeaeaeaeaeaeaeaea/ |eaeaeaeaeaeaeae
56 | eaeaeaeaeaeaeae/`) + yellow(`:`) + red(`\\ |aeaeaeaeaeaeaea
57 | aeaeaeaeaeaea/`) + yellow(`::::`) + red(`\\ |eaeaeaeaeaeaeae
58 | |aeaeaeaeae/`) + yellow(`:::::::`) + red(`\\ |eaeaeaeaeaeaea|
59 | aeaeaeaeaeaeaeaeaea\\ |`) + yellow(`::::`) + red(`/aeaeaeaea
60 | \\eaeaeaeaeaeaeaeaeaea\\ |`) + yellow(`:::`) + red(`/aeaeaeaea/
61 | \\aeaeaeaeaeaeaeaeaeae\\ |`) + yellow(`::`) + red(`/aeaeaeaea/
62 | \\aeaeaeaeaeaeaeaeaeae\\ |`) + yellow(`:`) + red(`/eaeeaeaea/
63 | \\aeaeaeaeaeaeaeaeaea\\|/aeaeaeae/
64 | \\aeaeaeaeaeaeaeaeaeaeaeaeae/
65 | -eaeaeaeaeaeaeaeaeaeae-
66 | -aeaeaeaeaeae—
67 |
68 | __ _ ___ _ __ _ __ _ _ __
69 | / _' |/ _ \\ '__| |/ _' | '_ \\
70 | | (_| | __/ | | | (_| | | | |
71 | \\__,_|\\___|_| |_|\\__,_|_| |_|
72 |
73 | `) + green(`
74 | 🌳 Crafted with care in the Cotswolds. 🌳`) + yellow(`
75 |
76 | https://aerian.com/
77 |
78 | `);
79 |
80 | print.info(logo)
81 | const spinner = print
82 | .spin(`using the TypeScript boilerplate from Aerian Studios. You might want to make a cuppa while we get this ready. ☕️`)
83 | .succeed()
84 |
85 | // attempt to install React Native or die trying
86 | const rnInstall = await reactNative.install({
87 | name,
88 | version: getReactNativeVersion(context)
89 | })
90 | if (rnInstall.exitCode > 0) process.exit(rnInstall.exitCode)
91 |
92 | // remove the __tests__ directory and App.js that come with React Native
93 | filesystem.remove('__tests__')
94 | filesystem.remove('App.js')
95 | // copy our App, Tests & storybook directories
96 | spinner.text = '▸ copying files'
97 | spinner.start()
98 | filesystem.copy(`${ignite.ignitePluginPath()}/boilerplate/App`, `${process.cwd()}/App`, {
99 | overwrite: true,
100 | matching: '!*.ejs'
101 | })
102 | filesystem.copy(`${ignite.ignitePluginPath()}/boilerplate/Tests`, `${process.cwd()}/Tests`, {
103 | overwrite: true,
104 | matching: '!*.ejs'
105 | })
106 | filesystem.copy(`${ignite.ignitePluginPath()}/boilerplate/storybook`, `${process.cwd()}/storybook`, {
107 | overwrite: true,
108 | matching: '!*.ejs'
109 | })
110 | filesystem.copy(`${ignite.ignitePluginPath()}/boilerplate/types`, `${process.cwd()}/types`, {
111 | overwrite: true,
112 | matching: '!*.ejs'
113 | })
114 | spinner.stop()
115 |
116 | // --max, --min, interactive
117 | let answers
118 | if (parameters.options.max) {
119 | answers = options.answers.max
120 | } else if (parameters.options.min) {
121 | answers = options.answers.min
122 | } else {
123 | answers = await prompt.ask(options.questions)
124 | }
125 |
126 | // generate some templates
127 | spinner.text = '▸ generating files'
128 | const templates = [
129 | { template: 'index.js.ejs', target: 'index.js' },
130 | { template: 'README.md', target: 'README.md' },
131 | { template: 'ignite.json.ejs', target: 'ignite/ignite.json' },
132 | { template: '.editorconfig', target: '.editorconfig' },
133 | { template: '.babelrc', target: '.babelrc' },
134 | { template: 'tsconfig.json', target: 'tsconfig.json' },
135 | { template: 'tslint.json', target: 'tslint.json' },
136 | { template: 'rn-cli.config.js', target: 'rn-cli.config.js' },
137 | { template: 'Tests/Setup.tsx.ejs', target: 'Tests/Setup.tsx' },
138 | { template: 'storybook/storybook.ejs', target: 'storybook/storybook.js' },
139 | { template: '.env.example', target: '.env.example' }
140 | ]
141 | const templateProps = {
142 | name,
143 | igniteVersion: ignite.version,
144 | reactNativeVersion: rnInstall.version,
145 | vectorIcons: answers['vector-icons'],
146 | animatable: answers['animatable'],
147 | i18n: answers['i18n']
148 | }
149 | await ignite.copyBatch(context, templates, templateProps, {
150 | quiet: false,
151 | directory: `${ignite.ignitePluginPath()}/boilerplate`
152 | })
153 |
154 | /**
155 | * Append to files
156 | */
157 | // https://github.com/facebook/react-native/issues/12724
158 | filesystem.appendAsync('.gitattributes', '*.bat text eol=crlf')
159 | filesystem.append('.gitignore', '\n# Misc\n#')
160 | filesystem.append('.gitignore', '\n.env.example\n')
161 | filesystem.append('.gitignore', '.env\n')
162 | filesystem.append('.gitignore', 'dist\n')
163 | filesystem.append('.gitignore', '.jest\n')
164 |
165 |
166 | /**
167 | * Merge the package.json from our template into the one provided from react-native init.
168 | */
169 | async function mergePackageJsons () {
170 | // transform our package.json in case we need to replace variables
171 | const rawJson = await template.generate({
172 | directory: `${ignite.ignitePluginPath()}/boilerplate`,
173 | template: 'package.json.ejs',
174 | props: templateProps
175 | })
176 | const newPackageJson = JSON.parse(rawJson)
177 |
178 | // read in the react-native created package.json
179 | const currentPackage = filesystem.read('package.json', 'json')
180 |
181 | // deep merge, lol
182 | const newPackage = pipe(
183 | assoc(
184 | 'dependencies',
185 | merge(currentPackage.dependencies, newPackageJson.dependencies)
186 | ),
187 | assoc(
188 | 'devDependencies',
189 | merge(currentPackage.devDependencies, newPackageJson.devDependencies)
190 | ),
191 | assoc('scripts', merge(currentPackage.scripts, newPackageJson.scripts)),
192 | merge(
193 | __,
194 | omit(['dependencies', 'devDependencies', 'scripts'], newPackageJson)
195 | )
196 | )(currentPackage)
197 |
198 | // write this out
199 | filesystem.write('package.json', newPackage, { jsonIndent: 2 })
200 | }
201 | await mergePackageJsons()
202 |
203 | spinner.stop()
204 |
205 | // react native link -- must use spawn & stdio: ignore or it hangs!! :(
206 | spinner.text = `▸ linking native libraries`
207 | spinner.start()
208 | await system.spawn('react-native link', { stdio: 'ignore' })
209 | spinner.stop()
210 |
211 | // pass long the debug flag if we're running in that mode
212 | const debugFlag = parameters.options.debug ? '--debug' : ''
213 |
214 | // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
215 | // NOTE(steve): I'm re-adding this here because boilerplates now hold permanent files
216 | // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
217 | try {
218 | // boilerplate adds itself to get plugin.js/generators etc
219 | // Could be directory, npm@version, or just npm name. Default to passed in values
220 | const boilerplate = parameters.options.b || parameters.options.boilerplate || 'ignite-typescript-boilerplate'
221 |
222 | await system.spawn(`ignite add ${boilerplate} ${debugFlag}`, { stdio: 'inherit' })
223 |
224 | // now run install of Ignite Plugins
225 | if (answers['dev-screens'] === 'Yes') {
226 | await system.spawn(`ignite add dev-screens@"~>2.2.0" ${debugFlag}`, {
227 | stdio: 'inherit'
228 | })
229 | }
230 |
231 | if (answers['vector-icons'] === 'react-native-vector-icons') {
232 | await system.spawn(`ignite add vector-icons@"~>1.0.0" ${debugFlag}`, {
233 | stdio: 'inherit'
234 | })
235 | }
236 |
237 | if (answers['i18n'] === 'react-native-i18n') {
238 | await system.spawn(`ignite add i18n@"~>1.0.0" ${debugFlag}`, { stdio: 'inherit' })
239 | }
240 |
241 | if (answers['animatable'] === 'react-native-animatable') {
242 | await system.spawn(`ignite add animatable@"~>1.0.0" ${debugFlag}`, {
243 | stdio: 'inherit'
244 | })
245 | }
246 |
247 | } catch (e) {
248 | ignite.log(e)
249 | throw e
250 | }
251 |
252 | // git configuration
253 | const gitExists = await filesystem.exists('./.git')
254 | if (!gitExists && !parameters.options['skip-git'] && system.which('git')) {
255 | // initial git
256 | const spinner = print.spin('configuring git')
257 |
258 | // TODO: Make husky hooks optional
259 | const huskyCmd = '' // `&& node node_modules/husky/bin/install .`
260 | system.run(`git init . && git add . && git commit -m "Initial commit." ${huskyCmd}`)
261 |
262 | spinner.succeed(`configured git`)
263 | }
264 |
265 | const perfDuration = parseInt(((new Date()).getTime() - perfStart) / 10) / 100
266 | spinner.succeed(`ignited ${yellow(name)} in ${perfDuration}s`)
267 |
268 | const androidInfo = isAndroidInstalled(context) ? ''
269 | : `\n\nTo run in Android, make sure you've followed the latest react-native setup instructions at https://facebook.github.io/react-native/docs/getting-started.html before using ignite.\nYou won't be able to run ${bold('react-native run-android')} successfully until you have.`
270 |
271 | const successMessage = `
272 | ${red('Ignite CLI')} ignited ${yellow(name)} in ${gray(`${perfDuration}s`)}
273 |
274 | To get started:
275 |
276 | cd ${name}
277 | react-native run-ios
278 | react-native run-android${androidInfo}
279 | ignite --help
280 |
281 | ${gray('Read the walkthrough at https://github.com/aerian-studios/ignite-typescript-boilerplate/blob/master/readme.md#boilerplate-walkthrough')}
282 |
283 | ${bold('Now get cooking! 🍽')}
284 | `
285 |
286 |
287 | print.info(successMessage)
288 | }
289 |
290 | module.exports = {
291 | install
292 | }
293 |
--------------------------------------------------------------------------------