= {
readonly [P in keyof T]: FluxActionCreator;
};
export type ReducerMap = {
readonly [T in keyof A]: Reducer;
};
export function mapReducers(
initialState: S,
reducers: R,
actionCreators: ActionCreatorMap): Reducer {
const reducerMap = new Map(Object.entries(actionCreators).map(([key, val]): [string, Reducer] =>
[getType(val), reducers[key]]));
return (state: S = initialState, action: AnyAction) => {
if (!("type" in action)) {
return state;
}
const reducer = reducerMap.get(action.type);
if (!reducer) {
return state;
}
return reducer(state, action);
};
}
================================================
FILE: boilerplate/App/Navigation/AppNavigation.tsx
================================================
import { StackNavigator } from "react-navigation";
import LaunchScreen from "../Containers/LaunchScreen";
import styles from "./Styles/NavigationStyles";
// Manifest of possible screens
const PrimaryNav = StackNavigator({
LaunchScreen: { screen: LaunchScreen },
}, {
// Default config for all screens
headerMode: "none",
initialRouteName: "LaunchScreen",
navigationOptions: {
headerStyle: styles.header,
},
});
export default PrimaryNav;
================================================
FILE: boilerplate/App/Navigation/ReduxNavigation.tsx
================================================
import * as React from "react";
import * as ReactNavigation from "react-navigation";
import { connect } from "react-redux";
import AppNavigation from "./AppNavigation";
// here is our redux-aware smart component
function ReduxNavigation(props) {
const { dispatch, nav } = props;
const navigation = ReactNavigation.addNavigationHelpers({
dispatch,
state: nav,
});
return ;
}
const mapStateToProps = (state) => ({ nav: state.nav });
export default connect(mapStateToProps)(ReduxNavigation);
================================================
FILE: boilerplate/App/Navigation/Styles/NavigationStyles.ts
================================================
import { StyleSheet } from "react-native";
import { Colors } from "../../Themes/index";
export default StyleSheet.create({
header: {
backgroundColor: Colors.background,
},
});
================================================
FILE: boilerplate/App/Reducers/CreateStore.tsx
================================================
import Reactotron from "reactotron-react-native";
import { applyMiddleware, compose, createStore, Reducer } from "redux";
import sagaMiddlewareFactory, { Monitor, SagaIterator } from "redux-saga";
import Config from "../Config/DebugConfig";
import ScreenTracking from "./ScreenTrackingMiddleware";
// creates the store
export default (rootReducer: Reducer, rootSaga: () => SagaIterator) => {
/* ------------- Redux Configuration ------------- */
const middleware = [];
const enhancers = [];
/* ------------- Analytics Middleware ------------- */
if (Config.useReactotron) {
middleware.push(ScreenTracking);
}
/* ------------- Saga Middleware ------------- */
let opts = {};
if (Config.useReactotron) {
const sagaMonitor: Monitor = Reactotron.createSagaMonitor();
opts = { sagaMonitor };
}
const sagaMiddleware = sagaMiddlewareFactory(opts);
middleware.push(sagaMiddleware);
/* ------------- Assemble Middleware ------------- */
enhancers.push(applyMiddleware(...middleware));
// if Reactotron is enabled (default for __DEV__), we'll create the store through Reactotron
const createAppropriateStore = Config.useReactotron ? Reactotron.createStore : createStore;
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createAppropriateStore(rootReducer, composeEnhancers(...enhancers));
// kick off root saga
const sagasManager = sagaMiddleware.run(rootSaga);
return {
store,
sagasManager,
sagaMiddleware,
};
};
================================================
FILE: boilerplate/App/Reducers/GithubReducers/GithubReducersTest.tsx
================================================
///
import { GithubActions, GithubReducer, INITIAL_STATE } from "./index";
test("request", () => {
const username = "taco";
const state = GithubReducer(INITIAL_STATE, GithubActions.userRequest({username}));
expect(state.fetching).toBe(true);
expect(state.username).toBe(username);
expect(state.avatar).toBeNull();
});
test("success", () => {
const avatar = "http://placekitten.com/200/300";
const state = GithubReducer(INITIAL_STATE, GithubActions.userSuccess({avatar}));
expect(state.fetching).toBe(false);
expect(state.avatar).toBe(avatar);
expect(state.error).toBeNull();
});
test("failure", () => {
const state = GithubReducer(INITIAL_STATE, GithubActions.userFailure());
expect(state.fetching).toBe(false);
expect(state.error).toBe(true);
expect(state.avatar).toBeNull();
});
================================================
FILE: boilerplate/App/Reducers/GithubReducers/index.tsx
================================================
import { Action, AnyAction, Reducer } from "redux";
import * as SI from "seamless-immutable";
import { createAction, PayloadAction } from "typesafe-actions";
import { mapReducers, ReducerMap } from "../../Lib/ReduxHelpers";
/* ------------- Types and Action Creators ------------- */
interface RequestParams {username: string; }
interface SuccessParams {avatar: string; }
const actions = {
userRequest: createAction("githubUserRequest", (params: RequestParams) =>
({type: "githubUserRequest", payload: params})),
userSuccess: createAction("githubUserSuccess", (params: SuccessParams) =>
({type: "githubUserSuccess", payload: params})),
userFailure: createAction("githubUserFailure"),
};
export const GithubActions = actions;
interface GithubState {
avatar?: string | null;
fetching?: boolean | null;
error?: boolean | null;
username?: string | null;
}
export type GithubAction = PayloadAction;
export type ImmutableGithubState = SI.ImmutableObject;
/* ------------- Initial State ------------- */
export const INITIAL_STATE: ImmutableGithubState = SI.from({
avatar: null,
fetching: null,
error: null,
username: null,
});
/* ------------- Reducers ------------- */
// request the avatar for a user
export const userRequest: Reducer =
(state: ImmutableGithubState, { payload }: AnyAction & {payload?: RequestParams}) =>
payload ? state.merge({ fetching: true, username: payload.username, avatar: null }) : state;
// successful avatar lookup
export const userSuccess: Reducer =
(state: ImmutableGithubState, { payload }: AnyAction & {payload?: SuccessParams}) =>
payload ? state.merge({ fetching: false, error: null, avatar: payload.avatar }) : state;
// failed to get the avatar
export const userFailure: Reducer = (state: ImmutableGithubState) =>
state.merge({ fetching: false, error: true, avatar: null });
/* ------------- Hookup Reducers To Types ------------- */
const reducerMap: ReducerMap = {
userRequest,
userSuccess,
userFailure,
};
export const GithubReducer = mapReducers(INITIAL_STATE, reducerMap, actions);
export default GithubReducer;
================================================
FILE: boilerplate/App/Reducers/NavigationReducers/index.tsx
================================================
import { NavigationAction, NavigationState } from "react-navigation";
import AppNavigation from "../../Navigation/AppNavigation";
export type NavigationState = NavigationState;
export const NavigationReducer = (state: NavigationState, action: NavigationAction) => {
const newState = AppNavigation.router.getStateForAction(action, state);
return newState || state;
};
================================================
FILE: boilerplate/App/Reducers/ScreenTrackingMiddleware.tsx
================================================
import { NavigationActions } from "react-navigation";
import Reactotron from "reactotron-react-native";
// gets the current screen from navigation state
const getCurrentRouteName = (navigationState) => {
if (!navigationState) {
return null;
}
const route = navigationState.routes[navigationState.index];
// dive into nested navigators
if (route.routes) {
return getCurrentRouteName(route);
}
return route.routeName;
};
const screenTracking = ({ getState }) => (next) => (action) => {
if (
action.type !== NavigationActions.NAVIGATE &&
action.type !== NavigationActions.BACK
) {
return next(action);
}
const currentScreen = getCurrentRouteName(getState().nav);
const result = next(action);
const nextScreen = getCurrentRouteName(getState().nav);
if (nextScreen !== currentScreen) {
try {
Reactotron.log(`NAVIGATING ${currentScreen} to ${nextScreen}`);
// Example: Analytics.trackEvent('user_navigation', {currentScreen, nextScreen})
} catch (e) {
Reactotron.log(e);
}
}
return result;
};
export default screenTracking;
================================================
FILE: boilerplate/App/Reducers/StartupReducers/index.tsx
================================================
import { createAction } from "typesafe-actions";
/* ------------- Types and Action Creators ------------- */
const actions = {
startup: createAction("startup"),
};
export const StartupActions = actions;
================================================
FILE: boilerplate/App/Reducers/index.ts
================================================
///
import { combineReducers } from "redux";
import root from "../Sagas";
import configureStore from "./CreateStore";
import { GithubReducer, ImmutableGithubState } from "./GithubReducers";
import { NavigationReducer, NavigationState } from "./NavigationReducers";
/* ------------- Assemble The Reducers ------------- */
export const reducers = combineReducers({
nav: NavigationReducer,
github: GithubReducer,
});
export interface State {
github: ImmutableGithubState;
nav: NavigationState;
}
export default () => {
// tslint:disable-next-line:prefer-const
let { store, sagasManager, sagaMiddleware } = configureStore(reducers, root);
if (module.hot) {
module.hot.accept(() => {
const nextRootReducer = require("./").reducers;
store.replaceReducer(nextRootReducer);
const newYieldedSagas = require("../Sagas").default;
sagasManager.cancel();
sagasManager.done.then(() => {
sagasManager = sagaMiddleware.run(newYieldedSagas);
});
});
}
return store;
};
================================================
FILE: boilerplate/App/Sagas/GithubSagas/GithubSagaTest.ts
================================================
///
import { path } from "ramda";
import { call, put } from "redux-saga/effects";
import { GithubActions } from "../../Reducers/GithubReducers";
import FixtureAPI from "../../Services/FixtureApi";
import { getUserAvatar } from "./index";
const stepper = (fn) => (mock?) => fn.next(mock).value;
test("first calls API", () => {
const step = stepper(getUserAvatar(FixtureAPI, GithubActions.userRequest({username: "ascorbic"})));
// first yield is API
expect(step()).toEqual(call(FixtureAPI.getUser, "ascorbic"));
});
test("success path", () => {
FixtureAPI.getUser("ascorbic").then((response) => {
// tslint:disable-next-line:max-line-length
const step = stepper(getUserAvatar(FixtureAPI, GithubActions.userRequest({username: "ascorbic"})));
// first step API
step();
// Second step successful return
const stepResponse = step(response);
// Get the avatar Url from the response
const firstUser = path(["data", "items"], response)[0];
const avatar = firstUser.avatar_url;
expect(stepResponse).toEqual(put(GithubActions.userSuccess(avatar)));
});
});
test("failure path", () => {
const response = {ok: false};
const step = stepper(getUserAvatar(FixtureAPI, GithubActions.userRequest({username: "ascorbic"})));
// first step API
step();
// Second step failed response
expect(step(response)).toEqual(put(GithubActions.userFailure()));
});
================================================
FILE: boilerplate/App/Sagas/GithubSagas/index.ts
================================================
import { ApiResponse } from "apisauce";
import { path } from "ramda";
import { SagaIterator } from "redux-saga";
import { call, put } from "redux-saga/effects";
import { GithubAction, GithubActions } from "../../Reducers/GithubReducers";
import { GithubApi, GithubResponse, GithubUser } from "../../Services/GithubApi";
export function * getUserAvatar(api: GithubApi, action: GithubAction): SagaIterator {
const { payload } = action;
// make the call to the api
const response: ApiResponse = yield call(api.getUser, payload.username);
if (response.ok) {
const firstUser = path(["data", "items"], response)[0];
const avatar = firstUser.avatar_url;
// do data conversion here if needed
yield put(GithubActions.userSuccess({avatar}));
} else {
yield put(GithubActions.userFailure());
}
}
================================================
FILE: boilerplate/App/Sagas/StartupSagas/StartupSagaTest.ts
================================================
///
import { put, select } from "redux-saga/effects";
import { GithubActions } from "../../Reducers/GithubReducers";
import { selectAvatar, startup } from "./index";
const stepper = (fn) => (mock) => fn.next(mock).value;
test("watches for the right action", () => {
const step = stepper(startup());
expect(step()).toEqual(select(selectAvatar));
expect(step()).toEqual(put(GithubActions.userRequest({username: "ascorbic"})));
});
================================================
FILE: boilerplate/App/Sagas/StartupSagas/index.ts
================================================
import { is } from "ramda";
import Reactotron from "reactotron-react-native";
import { Action } from "redux";
import { SagaIterator } from "redux-saga";
import { put, select } from "redux-saga/effects";
import { GithubAction, GithubActions } from "../../Reducers/GithubReducers";
import { StartupActions } from "../../Reducers/StartupReducers";
// exported to make available for tests
export const selectAvatar = (state: any) => state.github.avatar;
// process STARTUP actions
export function * startup(action?: Action): SagaIterator {
if (__DEV__) {
// straight-up string logging
Reactotron.log("Hello, I'm an example of how to log via Reactotron.");
Reactotron.log(action);
// logging an object for better clarity
Reactotron.log({
message: "pass objects for better logging",
someGeneratorFunction: selectAvatar,
});
// fully customized!
const subObject = { a: 1, b: [1, 2, 3], c: true, circularDependency: undefined as any};
subObject.circularDependency = subObject; // osnap!
Reactotron.display({
name: "🔥 IGNITE 🔥",
preview: "You should totally expand this",
value: {
"💃": "Welcome to the future!",
subObject,
"someInlineFunction": () => true,
"someGeneratorFunction": startup,
"someNormalFunction": selectAvatar,
},
});
}
const avatar = yield select(selectAvatar);
// only get if we don't have it yet
if (!is(String, avatar)) {
yield put(GithubActions.userRequest({username: "ascorbic"}));
}
}
================================================
FILE: boilerplate/App/Sagas/index.ts
================================================
import { all, takeLatest } from "redux-saga/effects";
import { getType } from "typesafe-actions";
import DebugConfig from "../Config/DebugConfig";
import FixtureAPI from "../Services/FixtureApi";
import {createAPI, GithubApi} from "../Services/GithubApi";
/* ------------- Types ------------- */
import { GithubActions } from "../Reducers/GithubReducers";
import { StartupActions } from "../Reducers/StartupReducers";
/* ------------- Sagas ------------- */
import { getUserAvatar } from "./GithubSagas";
import { startup } from "./StartupSagas";
/* ------------- API ------------- */
// The API we use is only used from Sagas, so we create it here and pass along
// to the sagas which need it.
const api = DebugConfig.useFixtures ? FixtureAPI : createAPI();
/* ------------- Connect Types To Sagas ------------- */
export default function * root() {
yield all([
// some sagas only receive an action
takeLatest(getType(StartupActions.startup), startup),
// some sagas receive extra parameters in addition to an action
takeLatest(getType(GithubActions.userRequest), getUserAvatar, api),
]);
}
================================================
FILE: boilerplate/App/Services/Api.ts
================================================
// This is for the devscreens
import {createAPI} from "./GithubApi";
export default {
create: createAPI,
};
================================================
FILE: boilerplate/App/Services/ExamplesRegistry.tsx
================================================
import R from "ramda";
import * as React from "react";
import { Text, View } from "react-native";
import DebugConfig from "../Config/DebugConfig";
import { ApplicationStyles } from "../Themes";
const globalComponentExamplesRegistry = [];
const globalPluginExamplesRegistry = [];
export const addComponentExample = (title, usage = () => {}) => { if (DebugConfig.includeExamples) globalComponentExamplesRegistry.push({title, usage}); }; // eslint-disable-line
export const addPluginExample = (title, usage = () => {}) => { if (DebugConfig.includeExamples) globalPluginExamplesRegistry.push({title, usage}); }; // eslint-disable-line
const renderComponentExample = (example) => {
return (
{example.title}
{example.usage.call()}
);
};
const renderPluginExample = (example) => {
return (
{example.title}
{example.usage.call()}
);
};
export const renderComponentExamples = () => R.map(renderComponentExample, globalComponentExamplesRegistry);
export const renderPluginExamples = () => R.map(renderPluginExample, globalPluginExamplesRegistry);
// Default for readability
export default {
renderComponentExamples,
addComponentExample,
renderPluginExamples,
addPluginExample,
};
================================================
FILE: boilerplate/App/Services/FixtureAPITest.tsx
================================================
///
import * as R from "ramda";
import FixtureAPI from "../../App/Services/FixtureApi";
import API from "../../App/Services/GithubApi";
test("All fixtures map to actual API", () => {
const fixtureKeys = R.keys(FixtureAPI).sort();
const apiKeys = R.keys(API.createAPI());
const intersection = R.intersection(fixtureKeys, apiKeys).sort();
// There is no difference between the intersection and all fixtures
expect(R.equals(fixtureKeys, intersection)).toBe(true);
});
test("FixtureAPI getRate returns the right file", () => {
const expectedFile = require("../../App/Fixtures/rateLimit.json");
return FixtureAPI.getRate().then((data) => expect(data).toEqual({
ok: true,
data: expectedFile,
}));
});
test("FixtureAPI getUser returns the right file for gantman", () => {
const expectedFile = require("../../App/Fixtures/gantman.json");
return FixtureAPI.getUser("GantMan").then((data) => expect(data).toEqual({
ok: true,
data: expectedFile,
}));
});
test("FixtureAPI getUser returns the right file for skellock as default", () => {
const expectedFile = require("../../App/Fixtures/skellock.json");
return FixtureAPI.getUser("Whatever").then((data) => expect(data).toEqual({
ok: true,
data: expectedFile,
}));
});
================================================
FILE: boilerplate/App/Services/FixtureApi.tsx
================================================
import {ApiResponse} from "apisauce";
import {GithubApi, GithubResponse} from "./GithubApi";
export default {
// Functions return fixtures
getRoot: (): Promise> => {
return Promise.resolve({
ok: true,
data: require("../Fixtures/root.json"),
} as ApiResponse);
},
getRate: (): Promise> => {
return Promise.resolve({
ok: true,
data: require("../Fixtures/rateLimit.json"),
} as ApiResponse);
},
getUser: (username: string): Promise> => {
// This fixture only supports gantman or else returns skellock
const gantmanData = require("../Fixtures/gantman.json");
const skellockData = require("../Fixtures/skellock.json");
return Promise.resolve({
ok: true,
data: username.toLowerCase() === "gantman" ? gantmanData : skellockData,
} as ApiResponse);
},
} as GithubApi;
================================================
FILE: boilerplate/App/Services/GithubApi.tsx
================================================
// a library to wrap and simplify api calls
import {ApiResponse, create as apicreate} from "apisauce";
export interface GithubApi {
getRoot: () => Promise>;
getRate: () => Promise>;
getUser: (username: string) => Promise>;
}
export interface GithubUser {
login: string;
id: number;
avatar_url: string;
gravatar_id: string;
url: string;
html_url: string;
followers_url: string;
following_url: string;
gists_url: string;
starred_url: string;
subscriptions_url: string;
organizations_url: string;
repos_url: string;
events_url: string;
received_events_url: string;
type: "User";
site_admin: boolean;
score: number;
}
export interface GithubResponse {
total_count: number;
incomplete_results: false;
items: GithubUser[];
}
// our "constructor"
export const createAPI = (baseURL = "https://api.github.com/"): GithubApi => {
// ------
// STEP 1
// ------
//
// Create and configure an apisauce-based api object.
//
const api = apicreate({
// base URL is read from the "constructor"
baseURL,
// here are some default headers
headers: {
"Cache-Control": "no-cache",
},
// 10 second timeout...
timeout: 10000,
});
// ------
// STEP 2
// ------
//
// Define some functions that call the api. The goal is to provide
// a thin wrapper of the api layer providing nicer feeling functions
// rather than "get", "post" and friends.
//
// I generally don't like wrapping the output at this level because
// sometimes specific actions need to be take on `403` or `401`, etc.
//
// Since we can't hide from that, we embrace it by getting out of the
// way at this level.
//
const getRoot = () => api.get("");
const getRate = () => api.get("rate_limit");
const getUser = (username: string) => api.get("search/users", {q: username});
// ------
// STEP 3
// ------
//
// Return back a collection of functions that we would consider our
// interface. Most of the time it'll be just the list of all the
// methods in step 2.
//
// Notice we're not returning back the `api` created in step 1? That's
// because it is scoped privately. This is one way to create truly
// private scoped goodies in JavaScript.
//
return {
// a list of the API functions from step 2
getRoot,
getRate,
getUser,
};
};
// let's return back our create method as the default.
export default {
createAPI,
};
================================================
FILE: boilerplate/App/Services/ImmutablePersistenceTransform.tsx
================================================
import R from "ramda";
import * as SeamlessImmutable from "seamless-immutable";
// is this object already Immutable?
const isImmutable = R.has("asMutable");
// change this Immutable object into a JS object
const convertToJs = (state: any) => state.asMutable({deep: true});
// optionally convert this object into a JS object if it is Immutable
const fromImmutable = R.when(isImmutable, convertToJs);
// convert this JS object into an Immutable object
const toImmutable = (raw: any) => SeamlessImmutable(raw);
// the transform interface that redux-persist is expecting
export default {
out: (state: any) => {
// console.log({ retrieving: state })
return toImmutable(state);
},
in: (raw: any) => {
// console.log({ storing: raw })
return fromImmutable(raw);
},
};
================================================
FILE: boilerplate/App/Services/RehydrationServices.tsx
================================================
import { AsyncStorage } from "react-native";
import { persistStore } from "redux-persist";
import DebugConfig from "../Config/DebugConfig";
import ReduxPersist from "../Config/ReduxPersist";
import StartupActions from "../Redux/StartupRedux";
const updateReducers = (store) => {
const reducerVersion = ReduxPersist.reducerVersion;
const config = ReduxPersist.storeConfig;
const startup = () => store.dispatch(StartupActions.startup());
// Check to ensure latest reducer version
AsyncStorage.getItem("reducerVersion").then((localVersion) => {
if (localVersion !== reducerVersion) {
if (DebugConfig.useReactotron) {
console.tron.display({
name: "PURGE",
value: {
"Old Version:": localVersion,
"New Version:": reducerVersion,
},
preview: "Reducer Version Change Detected",
important: true,
});
}
// Purge store
persistStore(store, config, startup).purge();
AsyncStorage.setItem("reducerVersion", reducerVersion);
} else {
persistStore(store, config, startup);
}
}).catch(() => {
persistStore(store, config, startup);
AsyncStorage.setItem("reducerVersion", reducerVersion);
});
};
export default {updateReducers};
================================================
FILE: boilerplate/App/Themes/ApplicationStyles.ts
================================================
import { TextStyle, ViewStyle } from "react-native";
import Colors from "./Colors";
import Fonts from "./Fonts";
import Metrics from "./Metrics";
// This file is for a reusable grouping of Theme items.
// Similar to an XML fragment layout in Android
const ApplicationStyles = {
screen: {
mainContainer: {
flex: 1,
backgroundColor: Colors.transparent,
} as ViewStyle,
backgroundImage: {
position: "absolute",
top: 0,
left: 0,
bottom: 0,
right: 0,
} as ViewStyle,
container: {
flex: 1,
paddingTop: Metrics.baseMargin,
backgroundColor: Colors.transparent,
} as ViewStyle,
section: {
margin: Metrics.section,
padding: Metrics.baseMargin,
} as ViewStyle,
sectionText: {
...Fonts.style.normal,
paddingVertical: Metrics.doubleBaseMargin,
color: Colors.snow,
marginVertical: Metrics.smallMargin,
textAlign: "center",
} as TextStyle,
subtitle: {
color: Colors.snow,
padding: Metrics.smallMargin,
marginBottom: Metrics.smallMargin,
marginHorizontal: Metrics.smallMargin,
} as TextStyle,
titleText: {
...Fonts.style.h2,
fontSize: 14,
color: Colors.text,
} as TextStyle,
},
darkLabelContainer: {
padding: Metrics.smallMargin,
paddingBottom: Metrics.doubleBaseMargin,
borderBottomColor: Colors.border,
borderBottomWidth: 1,
marginBottom: Metrics.baseMargin,
} as ViewStyle,
darkLabel: {
fontFamily: Fonts.type.bold,
color: Colors.snow,
} as TextStyle,
groupContainer: {
margin: Metrics.smallMargin,
flexDirection: "row",
justifyContent: "space-around",
alignItems: "center",
} as ViewStyle,
sectionTitle: {
...Fonts.style.h4,
color: Colors.coal,
backgroundColor: Colors.ricePaper,
padding: Metrics.smallMargin,
marginTop: Metrics.smallMargin,
marginHorizontal: Metrics.baseMargin,
borderWidth: 1,
borderColor: Colors.ember,
alignItems: "center",
textAlign: "center",
} as TextStyle,
};
export default ApplicationStyles;
================================================
FILE: boilerplate/App/Themes/Colors.ts
================================================
const colors = {
background: "#1F0808",
clear: "rgba(0,0,0,0)",
facebook: "#3b5998",
transparent: "rgba(0,0,0,0)",
silver: "#F7F7F7",
steel: "#CCCCCC",
error: "rgba(200, 0, 0, 0.8)",
ricePaper: "rgba(255,255,255, 0.75)",
frost: "#D8D8D8",
cloud: "rgba(200,200,200, 0.35)",
windowTint: "rgba(0, 0, 0, 0.4)",
panther: "#161616",
charcoal: "#595959",
coal: "#2d2d2d",
bloodOrange: "#fb5f26",
snow: "white",
ember: "rgba(164, 0, 48, 0.5)",
fire: "#e73536",
drawer: "rgba(30, 30, 29, 0.95)",
eggplant: "#251a34",
border: "#483F53",
banner: "#5F3E63",
text: "#E0D7E5",
};
export default colors;
================================================
FILE: boilerplate/App/Themes/Fonts.ts
================================================
const type = {
base: "Avenir-Book",
bold: "Avenir-Black",
emphasis: "HelveticaNeue-Italic",
};
const size = {
h1: 38,
h2: 34,
h3: 30,
h4: 26,
h5: 20,
h6: 19,
input: 18,
regular: 17,
medium: 14,
small: 12,
tiny: 8.5,
};
const style = {
h1: {
fontFamily: type.base,
fontSize: size.h1,
},
h2: {
fontWeight: "bold",
fontSize: size.h2,
},
h3: {
fontFamily: type.emphasis,
fontSize: size.h3,
},
h4: {
fontFamily: type.base,
fontSize: size.h4,
},
h5: {
fontFamily: type.base,
fontSize: size.h5,
},
h6: {
fontFamily: type.emphasis,
fontSize: size.h6,
},
normal: {
fontFamily: type.base,
fontSize: size.regular,
},
description: {
fontFamily: type.base,
fontSize: size.medium,
},
};
export default {
type,
size,
style,
};
================================================
FILE: boilerplate/App/Themes/Images.ts
================================================
// leave off @2x/@3x
const images = {
logo: require("../Images/ir.png"),
clearLogo: require("../Images/top_logo.png"),
launch: require("../Images/launch-icon.png"),
ready: require("../Images/your-app.png"),
ignite: require("../Images/ignite_logo.png"),
igniteClear: require("../Images/ignite-logo-transparent.png"),
tileBg: require("../Images/tile_bg.png"),
background: require("../Images/BG.png"),
buttonBackground: require("../Images/button-bg.png"),
api: require("../Images/Icons/icon-api-testing.png"),
components: require("../Images/Icons/icon-components.png"),
deviceInfo: require("../Images/Icons/icon-device-information.png"),
faq: require("../Images/Icons/faq-icon.png"),
home: require("../Images/Icons/icon-home.png"),
theme: require("../Images/Icons/icon-theme.png"),
usageExamples: require("../Images/Icons/icon-usage-examples.png"),
chevronRight: require("../Images/Icons/chevron-right.png"),
hamburger: require("../Images/Icons/hamburger.png"),
backButton: require("../Images/Icons/back-button.png"),
closeButton: require("../Images/Icons/close-button.png"),
};
export default images;
================================================
FILE: boilerplate/App/Themes/Metrics.ts
================================================
import {Dimensions, Platform} from "react-native";
const { width, height } = Dimensions.get("window");
// Used via Metrics.baseMargin
const metrics = {
marginHorizontal: 10,
marginVertical: 10,
section: 25,
baseMargin: 10,
doubleBaseMargin: 20,
smallMargin: 5,
doubleSection: 50,
horizontalLineHeight: 1,
screenWidth: width < height ? width : height,
screenHeight: width < height ? height : width,
navBarHeight: (Platform.OS === "ios") ? 64 : 54,
buttonRadius: 4,
icons: {
tiny: 15,
small: 20,
medium: 30,
large: 45,
xl: 50,
},
images: {
small: 20,
medium: 40,
large: 60,
logo: 200,
},
};
export default metrics;
================================================
FILE: boilerplate/App/Themes/README.md
================================================
### Themes Folder
Application specific themes
* Base Styles
* Fonts
* Metrics
* Colors
etc.
================================================
FILE: boilerplate/App/Themes/index.ts
================================================
import ApplicationStyles from "./ApplicationStyles";
import Colors from "./Colors";
import Fonts from "./Fonts";
import Images from "./Images";
import Metrics from "./Metrics";
export { Colors, Fonts, Images, Metrics, ApplicationStyles };
================================================
FILE: boilerplate/App/Transforms/ConvertFromKelvin.ts
================================================
export default (kelvin: number) => {
const celsius = kelvin - 273.15;
const fahrenheit = (celsius * 1.8000) + 32;
return Math.round(fahrenheit);
};
================================================
FILE: boilerplate/App/Transforms/README.md
================================================
# Transforms
A common pattern when working with APIs is to change data to play nice between your app & the API.
We've found this to be the case in every project we've worked on. So much so that we're recommending that you create a folder dedicated to these transformations.
Transforms are not necessarily a bad thing (although an API might have you transforming more than you'd like).
For example, you may:
* turn appropriate strings to date objects
* convert snake case to camel case
* normalize or denormalize things
* create lookup tables
================================================
FILE: boilerplate/README.md
================================================
# <%= props.name %>
* TypeScript React Native App Utilizing [Ignite](https://github.com/infinitered/ignite)
## :arrow_up: How to Setup
**Step 1:** git clone this repo:
**Step 2:** cd to the cloned repo:
**Step 3:** Install the Application with `yarn` or `npm i`
## :arrow_forward: How to Run App
1. cd to the repo
2. Run `npm run compile`
3. Run Build for either OS
* for iOS
* run `react-native run-ios`
* for Android
* Run Genymotion
* run `react-native run-android`
**To Lint on Commit**
This is implemented using [husky](https://github.com/typicode/husky). There is no additional setup needed.
## :closed_lock_with_key: Secrets
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
and other sensitive information in a `.env` file:
```
API_URL=https://myapi.com
GOOGLE_MAPS_API_KEY=abcdefgh
```
and access them from React Native like so:
```
import Secrets from 'react-native-config'
Secrets.API_URL // 'https://myapi.com'
Secrets.GOOGLE_MAPS_API_KEY // 'abcdefgh'
```
The `.env` file is ignored by git keeping those secrets out of your repo.
### Get started:
1. Copy .env.example to .env
2. Add your config variables
3. Follow instructions at [https://github.com/luggit/react-native-config#setup](https://github.com/luggit/react-native-config#setup)
4. Done!
================================================
FILE: boilerplate/Tests/Setup.tsx.ejs
================================================
///
// Mock your external modules here if needed
<%_ if (props.i18n === 'react-native-i18n') { _%>
jest
.mock('react-native-i18n', () => {
const english = require('../App/I18n/languages/english.json')
const keys = require('ramda')
const replace = require('ramda')
const forEach = require('ramda')
return {
t: (key, replacements) => {
let value = english[key]
if (!value) return key
if (!replacements) return value
forEach((r) => {
value = replace(`{{${r}}}`, replacements[r], value)
}, keys(replacements))
return value
}
}
})
<%_ } else { _%>
// jest
// .mock('react-native-device-info', () => {
// return { isTablet: jest.fn(() => { return false }) }
// })
<%_ } _%>
================================================
FILE: boilerplate/Tests/StoriesTest.ts
================================================
import initStoryshots from "@storybook/addon-storyshots";
initStoryshots();
================================================
FILE: boilerplate/ignite.json.ejs
================================================
{
"createdWith": "<%= props.igniteVersion %>",
"examples": "classic",
"navigation": "react-navigation",
"askToOverride": true
}
================================================
FILE: boilerplate/index.js.ejs
================================================
import "./App/Config/ReactotronConfig"
import { AppRegistry } from 'react-native';
import App from "./App/Containers/App";
AppRegistry.registerComponent("<%= props.name %>", () => App);
================================================
FILE: boilerplate/package.json.ejs
================================================
{
"version": "0.0.1",
"scripts": {
"start": "node node_modules/react-native/local-cli/cli.js start",
"test": "jest",
"clean": "rm -rf $TMPDIR/react-* && watchman watch-del-all && npm cache clean --force",
"clean:android": "cd android/ && ./gradlew clean && cd .. && react-native run-android",
"newclear": "rm -rf $TMPDIR/react-* && watchman watch-del-all && rm -rf ios/build && rm -rf node_modules/ && npm cache clean --force && npm i",
"test:watch": "jest --watch",
"updateSnapshot": "jest --updateSnapshot",
"coverage": "jest --coverage && open coverage/lcov-report/index.html || xdg-open coverage/lcov-report/index.html",
"android:build": "cd android && ./gradlew assembleRelease",
"android:install": "cd android && ./gradlew assembleRelease && ./gradlew installRelease",
"android:hockeyapp": "cd android && ./gradlew assembleRelease && puck -submit=auto app/build/outputs/apk/app-release.apk",
"android:devices": "$ANDROID_HOME/platform-tools/adb devices",
"android:logcat": "$ANDROID_HOME/platform-tools/adb logcat *:S ReactNative:V ReactNativeJS:V",
"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",
"storybook": "storybook start -p 7007",
"lint": "tslint --project . -e '**/*.js' -t verbose | snazzy",
"lintdiff": "git diff --name-only --cached --relative | grep '\\.tsx?$' | xargs tslint | snazzy",
"fixcode": "tslint --project . -e '**/*.js' --fix",
"git-hook": "npm run lint -s && npm run test -s"
},
"dependencies": {
"apisauce": "^0.14.0",
"format-json": "^1.0.3",
"lodash": "^4.17.2",
"querystringify": "0.0.4",
"ramda": "^0.24.1",
"react-native-config": "^0.6.0",
"react-native-drawer": "^2.3.0",
"react-navigation": "^1.0.0-beta.7",
"react-redux": "^5.0.2",
"react-redux-typescript": "^2.3.0",
"redux": "^3.6.0",
"redux-persist": "^4.1.0",
"redux-saga": "^0.15.6",
"seamless-immutable": "^7.0.1",
"typesafe-actions": "1.1.2"
},
"devDependencies": {
"@storybook/addon-storyshots": "^3.2.3",
"@storybook/react-native": "^3.2.3",
"@types/enzyme": "^3.1.0",
"@types/ramda": "^0.24.17",
"@types/react-navigation": "^1.0.21",
"@types/react-redux": "^5.0.10",
"@types/redux": "^3.6.0",
"@types/jest": "^21.1.4",
"@types/react": "^16.0.13",
"@types/react-native": "^0.49.2",
"@types/react-test-renderer": "^16.0.0",
"@types/seamless-immutable": "^7.1.1",
"@types/storybook__react": "^3.0.5",
"@types/webpack-env": "^1.13.2",
"babel-jest": "21.2.0",
"babel-plugin-ignite-ignore-reactotron": "^0.3.0",
"babel-preset-es2015": "^6.18.0",
"babel-preset-react-native": "3.0.2",
"enzyme": "^2.6.0",
"jest-preset-typescript-react-native": "^1.2.0",
"husky": "^0.13.1",
"ignite-animatable": "^1.0.0",
"ignite-dev-screens": "^2.2.0",
"ignite-vector-icons": "^1.1.0",
"jest": "^21.2.1",
"mockery": "^2.0.0",
"react-addons-test-utils": "~15.4.1",
"react-dom": "16.0.0-alpha.12",
"react-test-renderer": "16.0.0-beta.5",
"reactotron-react-native": "^1.12.0",
"reactotron-redux": "^1.11.1",
"reactotron-redux-saga": "^1.11.1",
"snazzy": "^7.0.0",
"ts-jest": "^21.1.3",
"tslint": "^5.7.0",
"tslint-react": "^3.2.0",
"typescript": "^2.5.3",
"react-native-typescript-transformer": "^1.1.4"
},
"jest": {
"preset": "jest-preset-typescript-react-native",
"testMatch": [
"**/Tests/**/*.ts?(x)",
"**/App/**/*Test.ts?(x)"
],
"testPathIgnorePatterns": [
"\\.snap$",
"/node_modules/",
"/lib/",
"Tests/Setup"
],
"setupFiles": [
"./Tests/Setup.tsx"
],
"moduleFileExtensions": [
"js",
"jsx",
"ts",
"tsx",
"json"
],
"cacheDirectory": ".jest/cache"
},
"config": {}
}
================================================
FILE: boilerplate/rn-cli.config.js
================================================
module.exports = {
getTransformModulePath() {
return require.resolve('react-native-typescript-transformer')
},
getSourceExts() {
return ['ts', 'tsx'];
}
}
================================================
FILE: boilerplate/storybook/addons.js
================================================
import '@storybook/addon-actions/register'
import '@storybook/addon-links/register'
================================================
FILE: boilerplate/storybook/index.js
================================================
import StorybookUI from './storybook'
export default StorybookUI
================================================
FILE: boilerplate/storybook/storybook.ejs
================================================
import { AppRegistry } from 'react-native'
import { getStorybookUI, configure } from '@storybook/react-native'
// import stories
configure(() => {
require('../App/Components/Stories')
}, module)
// This assumes that storybook is running on the same host as your RN packager,
// to set manually use, e.g. host: 'localhost' option
const StorybookUI = getStorybookUI({ port: 7007, onDeviceUI: true })
AppRegistry.registerComponent('<%= props.name %>', () => StorybookUI)
export default StorybookUI
================================================
FILE: boilerplate/tsconfig.json
================================================
{
"compilerOptions": {
/* Basic Options */
"target": "ESNEXT", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */
"module": "ESNext", /* Specify module code generation: 'none', commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
// "lib": [], /* Specify library files to be included in the compilation: */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
"jsx": "react-native", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
"sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
//"outDir": "./dist", /* Redirect output structure to the directory. */
// "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
"baseUrl": "types", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
"typeRoots": ["types"], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
"allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
/* Source Map Options */
// "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
}
// "include": [
// "index.ts", "App/Components/Stories.tsx"
// ]
}
================================================
FILE: boilerplate/tslint.json
================================================
{
"extends": [
"tslint:latest", "tslint-react"
],
"rules": {
"interface-name": false,
"object-literal-sort-keys": false,
"no-object-literal-type-assertion": false,
"no-empty-interface": false,
"no-submodule-imports": false
}
}
================================================
FILE: boilerplate/types/@storybook/react-native.d.ts
================================================
// Type definitions for @storybook/react 3.0
// Project: https://github.com/storybooks/storybook
// Definitions by: Joscha Feth
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.3
///
import * as React from 'react';
export type Renderable = React.ComponentType | JSX.Element;
export type RenderFunction = () => Renderable;
export type StoryDecorator = (story: RenderFunction, context: { kind: string, story: string }) => Renderable | null;
export interface Story {
readonly kind: string;
add(storyName: string, callback: RenderFunction): this;
addDecorator(decorator: StoryDecorator): this;
}
export function addDecorator(decorator: StoryDecorator): void;
export function configure(fn: () => void, module?: NodeModule): void;
export function setAddon(addon: object): void;
export function storiesOf(name: string, module?: NodeModule): Story;
export function storiesOf(name: string, module?: NodeModule): Story & T;
export interface StoryObject {
name: string;
render: RenderFunction;
}
export interface StoryBucket {
kind: string;
stories: StoryObject[];
}
export function getStorybook(): StoryBucket[];
================================================
FILE: boilerplate/types/reduxsauce/index.d.ts
================================================
import { Action, AnyAction, Reducer, ReducersMapObject } from "redux";
export interface ActionTypes {
[key: string]: string;
}
export interface ActionConfig {
[key: string]: string[] | ActionCreator | {[key: string]: any} | null;
}
export type ActionCreator = (...args: any[]) => Action;
export interface ActionCreators {
[key: string]: ActionCreator;
}
export function createActions(config: ActionConfig, options?: {prefix?: string}): {Types: ActionTypes, Creators: ActionCreators };
/**
* Creates a reducer.
* @param {object} initialState - The initial state for this reducer.
* @param {object} handlers - Keys are action types (strings), values are reducers (functions).
* @return {Reducer} A reducer object.
*/
export function createReducer(initialState: S, handlers: ReducersMapObject): Reducer;
export function createTypes(types: string, options?: {prefix?: string, [key: string]: any}): ActionTypes;
/**
* Allows your reducers to be reset.
*
* @param {string} typeToReset - The action type to listen for.
* @param {Reducer} originalReducer - The reducer to wrap.
*/
export function resettableReducer(type: string, originalReducer: Reducer): Reducer;
export function resettableReducer(type: string): (originalReducer: Reducer) => Reducer;
================================================
FILE: boilerplate.js
================================================
const options = require('./options')
const { merge, pipe, assoc, omit, __ } = require('ramda')
const { getReactNativeVersion } = require('./lib/react-native-version')
/**
* Is Android installed?
*
* $ANDROID_HOME/tools folder has to exist.
*
* @param {*} context - The gluegun context.
* @returns {boolean}
*/
const isAndroidInstalled = function (context) {
const androidHome = process.env['ANDROID_HOME']
const hasAndroidEnv = !context.strings.isBlank(androidHome)
const hasAndroid = hasAndroidEnv && context.filesystem.exists(`${androidHome}/tools`) === 'dir'
return Boolean(hasAndroid)
}
/**
* Let's install.
*
* @param {any} context - The gluegun context.
*/
async function install (context) {
const {
filesystem,
parameters,
ignite,
reactNative,
print,
system,
prompt,
template
} = context
const { colors } = print
const { red, yellow, bold, gray, blue, green } = colors
const perfStart = (new Date()).getTime()
const name = parameters.third
const logo = red(`
-aeaeaeaeaeae—
-eaeaeaeaeaeaeaeaeaeae-
/aeaeaeaeaeaeaeaeaeaeaeaeae\\
/aeaeaeaeaeaeaeaeaeaeaeaeaeaeae\\
/eaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaea\\
/aeaeaeaeaeaeaeaeaeaea/ |aeaeaeaeaeae\\
/aaeaeaeaeaeaeaeaeaeae/ |aeaeaeaeaeaea\\
aeaeaeaeaeaeaeaeaeae/ |eaeaeaeaeaeaea
|aeaeaeaeaeaeaeaeae/ |eaeaeaeaeaeaea|
aeaeaeaeaeaeaeaea/ |eaeaeaeaeaeaeae
eaeaeaeaeaeaeae/`) + yellow(`:`) + red(`\\ |aeaeaeaeaeaeaea
aeaeaeaeaeaea/`) + yellow(`::::`) + red(`\\ |eaeaeaeaeaeaeae
|aeaeaeaeae/`) + yellow(`:::::::`) + red(`\\ |eaeaeaeaeaeaea|
aeaeaeaeaeaeaeaeaea\\ |`) + yellow(`::::`) + red(`/aeaeaeaea
\\eaeaeaeaeaeaeaeaeaea\\ |`) + yellow(`:::`) + red(`/aeaeaeaea/
\\aeaeaeaeaeaeaeaeaeae\\ |`) + yellow(`::`) + red(`/aeaeaeaea/
\\aeaeaeaeaeaeaeaeaeae\\ |`) + yellow(`:`) + red(`/eaeeaeaea/
\\aeaeaeaeaeaeaeaeaea\\|/aeaeaeae/
\\aeaeaeaeaeaeaeaeaeaeaeaeae/
-eaeaeaeaeaeaeaeaeaeae-
-aeaeaeaeaeae—
__ _ ___ _ __ _ __ _ _ __
/ _' |/ _ \\ '__| |/ _' | '_ \\
| (_| | __/ | | | (_| | | | |
\\__,_|\\___|_| |_|\\__,_|_| |_|
`) + green(`
🌳 Crafted with care in the Cotswolds. 🌳`) + yellow(`
https://aerian.com/
`);
print.info(logo)
const spinner = print
.spin(`using the TypeScript boilerplate from Aerian Studios. You might want to make a cuppa while we get this ready. ☕️`)
.succeed()
// attempt to install React Native or die trying
const rnInstall = await reactNative.install({
name,
version: getReactNativeVersion(context)
})
if (rnInstall.exitCode > 0) process.exit(rnInstall.exitCode)
// remove the __tests__ directory and App.js that come with React Native
filesystem.remove('__tests__')
filesystem.remove('App.js')
// copy our App, Tests & storybook directories
spinner.text = '▸ copying files'
spinner.start()
filesystem.copy(`${ignite.ignitePluginPath()}/boilerplate/App`, `${process.cwd()}/App`, {
overwrite: true,
matching: '!*.ejs'
})
filesystem.copy(`${ignite.ignitePluginPath()}/boilerplate/Tests`, `${process.cwd()}/Tests`, {
overwrite: true,
matching: '!*.ejs'
})
filesystem.copy(`${ignite.ignitePluginPath()}/boilerplate/storybook`, `${process.cwd()}/storybook`, {
overwrite: true,
matching: '!*.ejs'
})
filesystem.copy(`${ignite.ignitePluginPath()}/boilerplate/types`, `${process.cwd()}/types`, {
overwrite: true,
matching: '!*.ejs'
})
spinner.stop()
// --max, --min, interactive
let answers
if (parameters.options.max) {
answers = options.answers.max
} else if (parameters.options.min) {
answers = options.answers.min
} else {
answers = await prompt.ask(options.questions)
}
// generate some templates
spinner.text = '▸ generating files'
const templates = [
{ template: 'index.js.ejs', target: 'index.js' },
{ template: 'README.md', target: 'README.md' },
{ template: 'ignite.json.ejs', target: 'ignite/ignite.json' },
{ template: '.editorconfig', target: '.editorconfig' },
{ template: '.babelrc', target: '.babelrc' },
{ template: 'tsconfig.json', target: 'tsconfig.json' },
{ template: 'tslint.json', target: 'tslint.json' },
{ template: 'rn-cli.config.js', target: 'rn-cli.config.js' },
{ template: 'Tests/Setup.tsx.ejs', target: 'Tests/Setup.tsx' },
{ template: 'storybook/storybook.ejs', target: 'storybook/storybook.js' },
{ template: '.env.example', target: '.env.example' }
]
const templateProps = {
name,
igniteVersion: ignite.version,
reactNativeVersion: rnInstall.version,
vectorIcons: answers['vector-icons'],
animatable: answers['animatable'],
i18n: answers['i18n']
}
await ignite.copyBatch(context, templates, templateProps, {
quiet: false,
directory: `${ignite.ignitePluginPath()}/boilerplate`
})
/**
* Append to files
*/
// https://github.com/facebook/react-native/issues/12724
filesystem.appendAsync('.gitattributes', '*.bat text eol=crlf')
filesystem.append('.gitignore', '\n# Misc\n#')
filesystem.append('.gitignore', '\n.env.example\n')
filesystem.append('.gitignore', '.env\n')
filesystem.append('.gitignore', 'dist\n')
filesystem.append('.gitignore', '.jest\n')
/**
* Merge the package.json from our template into the one provided from react-native init.
*/
async function mergePackageJsons () {
// transform our package.json in case we need to replace variables
const rawJson = await template.generate({
directory: `${ignite.ignitePluginPath()}/boilerplate`,
template: 'package.json.ejs',
props: templateProps
})
const newPackageJson = JSON.parse(rawJson)
// read in the react-native created package.json
const currentPackage = filesystem.read('package.json', 'json')
// deep merge, lol
const newPackage = pipe(
assoc(
'dependencies',
merge(currentPackage.dependencies, newPackageJson.dependencies)
),
assoc(
'devDependencies',
merge(currentPackage.devDependencies, newPackageJson.devDependencies)
),
assoc('scripts', merge(currentPackage.scripts, newPackageJson.scripts)),
merge(
__,
omit(['dependencies', 'devDependencies', 'scripts'], newPackageJson)
)
)(currentPackage)
// write this out
filesystem.write('package.json', newPackage, { jsonIndent: 2 })
}
await mergePackageJsons()
spinner.stop()
// react native link -- must use spawn & stdio: ignore or it hangs!! :(
spinner.text = `▸ linking native libraries`
spinner.start()
await system.spawn('react-native link', { stdio: 'ignore' })
spinner.stop()
// pass long the debug flag if we're running in that mode
const debugFlag = parameters.options.debug ? '--debug' : ''
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
// NOTE(steve): I'm re-adding this here because boilerplates now hold permanent files
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
try {
// boilerplate adds itself to get plugin.js/generators etc
// Could be directory, npm@version, or just npm name. Default to passed in values
const boilerplate = parameters.options.b || parameters.options.boilerplate || 'ignite-typescript-boilerplate'
await system.spawn(`ignite add ${boilerplate} ${debugFlag}`, { stdio: 'inherit' })
// now run install of Ignite Plugins
if (answers['dev-screens'] === 'Yes') {
await system.spawn(`ignite add dev-screens@"~>2.2.0" ${debugFlag}`, {
stdio: 'inherit'
})
}
if (answers['vector-icons'] === 'react-native-vector-icons') {
await system.spawn(`ignite add vector-icons@"~>1.0.0" ${debugFlag}`, {
stdio: 'inherit'
})
}
if (answers['i18n'] === 'react-native-i18n') {
await system.spawn(`ignite add i18n@"~>1.0.0" ${debugFlag}`, { stdio: 'inherit' })
}
if (answers['animatable'] === 'react-native-animatable') {
await system.spawn(`ignite add animatable@"~>1.0.0" ${debugFlag}`, {
stdio: 'inherit'
})
}
} catch (e) {
ignite.log(e)
throw e
}
// git configuration
const gitExists = await filesystem.exists('./.git')
if (!gitExists && !parameters.options['skip-git'] && system.which('git')) {
// initial git
const spinner = print.spin('configuring git')
// TODO: Make husky hooks optional
const huskyCmd = '' // `&& node node_modules/husky/bin/install .`
system.run(`git init . && git add . && git commit -m "Initial commit." ${huskyCmd}`)
spinner.succeed(`configured git`)
}
const perfDuration = parseInt(((new Date()).getTime() - perfStart) / 10) / 100
spinner.succeed(`ignited ${yellow(name)} in ${perfDuration}s`)
const androidInfo = isAndroidInstalled(context) ? ''
: `\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.`
const successMessage = `
${red('Ignite CLI')} ignited ${yellow(name)} in ${gray(`${perfDuration}s`)}
To get started:
cd ${name}
react-native run-ios
react-native run-android${androidInfo}
ignite --help
${gray('Read the walkthrough at https://github.com/aerian-studios/ignite-typescript-boilerplate/blob/master/readme.md#boilerplate-walkthrough')}
${bold('Now get cooking! 🍽')}
`
print.info(successMessage)
}
module.exports = {
install
}
================================================
FILE: commands/component.js
================================================
// @cliDescription Generates a stateless component, styles, and an optional test.
module.exports = async function (context) {
// grab some features
const { parameters, strings, print, ignite } = context
const { pascalCase, isBlank } = strings
const config = ignite.loadIgniteConfig()
const { tests } = config
// validation
if (isBlank(parameters.first)) {
print.info(`${context.runtime.brand} generate component \n`)
print.info('A name is required.')
return
}
// read some configuration
const name = pascalCase(parameters.first)
const props = { name }
const jobs = [
{
template: 'component.ejs',
target: `App/Components/${name}/${name}.tsx`
},
{
template: 'component-style.ejs',
target: `App/Components/${name}/${name}Style.ts`
},
{
template: 'component-index.ejs',
target: `App/Components/${name}/index.ts`
},
{
template: 'component-story.ejs',
target: `App/Components/${name}/${name}.story.tsx`
},
tests === 'ava' &&
{
template: 'component-test-ava.ejs',
target: `App/Components/${name}/${name}Test.tsx`
},
tests === 'jest' &&
{
template: 'component-test-jest.ejs',
target: `App/Components/${name}/${name}Test.tsx`
}
]
await ignite.copyBatch(context, jobs, props)
}
================================================
FILE: commands/container.js
================================================
// @cliDescription Generates a redux smart component.
const patterns = require('../lib/patterns')
module.exports = async function (context) {
// grab some features
const { parameters, strings, print, ignite, filesystem } = context
const { pascalCase, isBlank } = strings
const config = ignite.loadIgniteConfig()
// validation
if (isBlank(parameters.first)) {
print.info(`${context.runtime.brand} generate container \n`)
print.info('A name is required.')
return
}
const name = pascalCase(parameters.first)
const props = { name }
const jobs = [
{
template: 'container.ejs',
target: `App/Containers/${name}/${name}.tsx`
},
{
template: 'container-style.ejs',
target: `App/Containers/${name}/${name}Style.ts`
}
]
await ignite.copyBatch(context, jobs, props)
// if using `react-navigation` go the extra step
// and insert the container into the nav router
if (config.navigation === 'react-navigation') {
const containerName = name
const appNavFilePath = `${process.cwd()}/App/Navigation/AppNavigation.tsx`
const importToAdd = `import ${containerName} from "../Containers/${containerName}";`
const routeToAdd = ` ${containerName}: { screen: ${containerName} },`
if (!filesystem.exists(appNavFilePath)) {
const msg = `No '${appNavFilePath}' file found. Can't insert container.`
print.error(msg)
process.exit(1)
}
// insert container import
ignite.patchInFile(appNavFilePath, {
after: patterns[patterns.constants.PATTERN_IMPORTS],
insert: importToAdd
})
// insert container route
ignite.patchInFile(appNavFilePath, {
after: patterns[patterns.constants.PATTERN_ROUTES],
insert: routeToAdd
})
} else {
print.info('Container created, manually add it to your navigation')
}
}
================================================
FILE: commands/list.js
================================================
// @cliDescription Generates a screen with a ListView/Flatlist/SectionList + walkthrough.
const patterns = require('../lib/patterns')
module.exports = async function (context) {
// grab some features
const { print, parameters, strings, ignite, filesystem } = context
const { pascalCase, isBlank } = strings
const config = ignite.loadIgniteConfig()
// validation
if (isBlank(parameters.first)) {
print.info(`${context.runtime.brand} generate list \n`)
print.info('A name is required.')
return
}
const name = pascalCase(parameters.first)
const props = { name }
// which type of layout?
const typeMessage = 'What kind of List would you like to generate?'
const typeChoices = ['Row', 'Grid']
// Sections or no?
const typeDataMessage = 'How will your data be presented on this list?'
const typeDataChoices = ['Single', 'Sectioned']
// Check for parameters to bypass questions
let typeCode = parameters.options.codeType
let type = parameters.options.type
let dataType = parameters.options.dataType
// only prompt if type is not defined
if (!typeCode) {
typeCode = 'flatlist';
}
if (!type) {
// ask question 2
const answers = await context.prompt.ask({
name: 'type',
type: 'list',
message: typeMessage,
choices: typeChoices
})
type = answers.type
}
if (!dataType) {
// ask question 3
const dataAnswers = await context.prompt.ask({
name: 'type',
type: 'list',
message: typeDataMessage,
choices: typeDataChoices
})
dataType = dataAnswers.type
}
// Sorry the following is so confusing, but so are React Native lists
// There are 3 options and therefore 8 possible combinations
let componentTemplate = dataType.toLowerCase() === 'sectioned'
? typeCode + '-sections'
: typeCode
let styleTemplate = ''
// Different logic depending on code types
if (typeCode === 'flatlist') {
/*
* The following mess is because FlatList supports numColumns
* where SectionList does not.
*/
if (type.toLowerCase() === 'grid' && dataType.toLowerCase() === 'sectioned') {
// grid + section means we need wrap
styleTemplate = 'listview-grid-style'
} else if (type.toLowerCase() === 'grid') {
componentTemplate = componentTemplate + '-grid'
// grid + single = no wrap, use columns
styleTemplate = 'flatlist-grid-style'
} else {
// no grids, flatlist basic
styleTemplate = 'listview-style'
}
} else {
// listview builder
styleTemplate = type.toLowerCase() === 'grid'
? 'listview-grid-style'
: 'listview-style'
}
const jobs = [
{
template: `${componentTemplate}.ejs`,
target: `App/Containers/${name}/${name}.tsx`
},
{
template: `${styleTemplate}.ejs`,
target: `App/Containers/${name}/${name}Style.ts`
}
]
await ignite.copyBatch(context, jobs, props)
// if using `react-navigation` go the extra step
// and insert the screen into the nav router
if (config.navigation === 'react-navigation') {
const screenName = `${name}`
const appNavFilePath = `${process.cwd()}/App/Navigation/AppNavigation.tsx`
const importToAdd = `import { ${screenName} } from "../Containers/${screenName}";`
const routeToAdd = ` ${screenName}: { screen: ${screenName} },`
if (!filesystem.exists(appNavFilePath)) {
const msg = `No '${appNavFilePath}' file found. Can't insert list screen.`
print.error(msg)
process.exit(1)
}
// insert list screen import
ignite.patchInFile(appNavFilePath, {
after: patterns[patterns.constants.PATTERN_IMPORTS],
insert: importToAdd
})
// insert list screen route
ignite.patchInFile(appNavFilePath, {
after: patterns[patterns.constants.PATTERN_ROUTES],
insert: routeToAdd
})
} else {
print.info('List screen created, manually add it to your navigation')
}
}
================================================
FILE: commands/reducers.js
================================================
// @cliDescription Generates a action/creator/reducer set for Redux.
module.exports = async function (context) {
// grab some features
const { parameters, ignite, strings, print } = context
const { isBlank, pascalCase } = strings
const config = ignite.loadIgniteConfig()
// validation
if (isBlank(parameters.first)) {
print.info(`${context.runtime.brand} generate reducers \n`)
print.info('A name is required.')
return
}
const name = pascalCase(parameters.first)
const props = { name }
const jobs = [{ template: `reducers.ejs`, target: `App/Reducers/${name}Reducers/index.tsx` }]
if (config.tests && config.tests !== 'none') {
jobs.push({
template: `reducers-test-${config.tests}.ejs`,
target: `App/Reducers/${name}Reducers/${name}ReducersTest.tsx`
})
}
await ignite.copyBatch(context, jobs, props)
}
================================================
FILE: commands/saga.js
================================================
// @cliDescription Generates a saga with an optional test.
module.exports = async function (context) {
// grab some features
const { parameters, ignite, print, strings } = context
const { pascalCase, isBlank } = strings
const config = ignite.loadIgniteConfig()
const { tests } = config
// validation
if (isBlank(parameters.first)) {
print.info(`${context.runtime.brand} generate saga \n`)
print.info('A name is required.')
return
}
const name = pascalCase(parameters.first)
const props = { name }
const jobs = [{ template: `saga.ejs`, target: `App/Sagas/${name}Sagas/index.ts` }]
if (tests) {
jobs.push({
template: `saga-test-${tests}.ejs`,
target: `App/Sagas/${name}Sagas/${name}SagaTest.ts`
})
}
// make the templates
await ignite.copyBatch(context, jobs, props)
}
================================================
FILE: commands/screen.js
================================================
// @cliDescription Generates an opinionated container.
const patterns = require('../lib/patterns')
module.exports = async function (context) {
// grab some features
const { parameters, print, strings, ignite, filesystem } = context
const { pascalCase, isBlank } = strings
const config = ignite.loadIgniteConfig()
// validation
if (isBlank(parameters.first)) {
print.info(`${context.runtime.brand} generate screen \n`)
print.info('A name is required.')
return
}
const name = pascalCase(parameters.first)
const screenName = name.endsWith('Screen') ? name : `${name}Screen`
const props = { name: screenName }
const jobs = [
{
template: `screen.ejs`,
target: `App/Containers/${screenName}/${screenName}.tsx`
},
{
template: `screen-style.ejs`,
target: `App/Containers/${screenName}/${screenName}Style.tsx`
},
{
template: 'component-index.ejs',
target: `App/Containers/${screenName}/index.ts`
}
]
// make the templates
await ignite.copyBatch(context, jobs, props)
// if using `react-navigation` go the extra step
// and insert the screen into the nav router
if (config.navigation === 'react-navigation') {
const appNavFilePath = `${process.cwd()}/App/Navigation/AppNavigation.tsx`
const importToAdd = `import ${screenName} from "../Containers/${screenName}";`
const routeToAdd = ` ${screenName}: { screen: ${screenName} },`
if (!filesystem.exists(appNavFilePath)) {
const msg = `No '${appNavFilePath}' file found. Can't insert screen.`
print.error(msg)
process.exit(1)
}
// insert screen import
ignite.patchInFile(appNavFilePath, {
after: patterns[patterns.constants.PATTERN_IMPORTS],
insert: importToAdd
})
// insert screen route
ignite.patchInFile(appNavFilePath, {
after: patterns[patterns.constants.PATTERN_ROUTES],
insert: routeToAdd
})
} else {
print.info(`Screen ${screenName} created, manually add it to your navigation`)
}
}
================================================
FILE: ignite.json
================================================
{
"generators": [
"component",
"container",
"list",
"reducers",
"saga",
"screen"
]
}
================================================
FILE: lib/patterns.js
================================================
const constants = {
PATTERN_IMPORTS: 'imports',
PATTERN_ROUTES: 'routes'
}
module.exports = {
constants,
[constants.PATTERN_IMPORTS]: `import[\\s\\S]*from\\s+"react-navigation";?`,
[constants.PATTERN_ROUTES]: 'const PrimaryNav'
}
================================================
FILE: lib/react-native-version.js
================================================
const { pathOr, is } = require('ramda')
// the default React Native version for this boilerplate
const REACT_NATIVE_VERSION = '0.51.0'
// where the version lives under gluegun
const pathToVersion = ['parameters', 'options', 'react-native-version']
// accepts the context and returns back the version
const getVersionFromContext = pathOr(REACT_NATIVE_VERSION, pathToVersion)
/**
* Gets the React Native version to use.
*
* Attempts to read it from the command line, and if not there, falls back
* to the version we want for this boilerplate. For example:
*
* $ ignite new Custom --react-native-version 0.44.1
*
* @param {*} context - The gluegun context.
*/
const getReactNativeVersion = (context = {}) => {
const version = getVersionFromContext(context)
return is(String, version) ? version : REACT_NATIVE_VERSION
}
module.exports = {
REACT_NATIVE_VERSION,
getReactNativeVersion
}
================================================
FILE: options.js
================================================
/**
* The questions to ask during the install process.
*/
const questions = [
{
name: 'dev-screens',
message: 'Would you like Ignite Development Screens?',
type: 'list',
choices: ['No', 'Yes']
},
{
name: 'vector-icons',
message: 'What vector icon library will you use?',
type: 'list',
choices: ['none', 'react-native-vector-icons']
},
{
name: 'i18n',
message: 'What internationalization library will you use?',
type: 'list',
choices: ['none', 'react-native-i18n']
},
{
name: 'animatable',
message: 'What animation library will you use?',
type: 'list',
choices: ['none', 'react-native-animatable']
},
{
name: 'tests',
message: 'What test library will you use?',
type: 'list',
choices: ['none', 'jest']
}
]
/**
* The max preset.
*/
const max = {
'dev-screens': 'Yes',
'vector-icons': 'react-native-vector-icons',
i18n: 'react-native-i18n',
animatable: 'react-native-animatable',
tests: 'jest'
}
/**
* The min preset.
*/
const min = {
'dev-screens': 'No',
'vector-icons': 'none',
i18n: 'none',
animatable: 'none',
tests: 'none'
}
module.exports = {
questions,
answers: { min, max }
}
================================================
FILE: package.json
================================================
{
"name": "ignite-typescript-boilerplate",
"description": "TypeScript boilerplate for React Native.",
"license": "MIT",
"repository": "aerian-studios/ignite-typescript-boilerplate",
"homepage": "https://github.com/aerian-studios/ignite-typescript-boilerplate",
"version": "0.1.5",
"files": [
"boilerplate",
"commands",
"lib",
"templates",
"boilerplate.js",
"ignite.json",
"options.js",
"readme.md",
"plugin.js"
],
"author": {
"name": "Aerian Studios",
"email": "matt.kane@aerian.com",
"url": "https://github.com/aerian-studios/ignite-typescript-boilerplate"
},
"scripts": {
"lint": "tslint",
"test": "jest",
"watch": "jest --runInBand --watch",
"coverage": "jest --runInBand --coverage",
"shipit": "np"
},
"devDependencies": {
"@types/jest": "^21.1.5",
"@types/ramda": "^0.24.18",
"@types/react": "^16.0.18",
"@types/react-native": "^0.49.2",
"@types/react-navigation": "^1.0.21",
"@types/react-redux": "^5.0.10",
"@types/redux": "^3.6.0",
"@types/seamless-immutable": "^7.1.1",
"@types/webpack-env": "^1.13.2",
"fs-jetpack": "^1.0.0",
"jest": "^20.0.4",
"np": "^2.15.0",
"react": "16.0.0-beta.5",
"redux": "^3.7.2",
"redux-saga": "^0.16.0",
"reduxsauce": "^0.7.0",
"seamless-immutable": "^7.1.2",
"sinon": "^2.3.1",
"tempy": "^0.1.0",
"tslint": "^5.8.0",
"tslint-react": "^3.2.0",
"typesafe-actions": "^1.1.2",
"typescript": "^2.6.1"
},
"dependencies": {
"ramda": "^0.23.0"
}
}
================================================
FILE: plugin.js
================================================
// Ignite CLI plugin for Ts
// ----------------------------------------------------------------------------
const add = async function (context) {
// No-op, as we do this all in `boilerplate.js`
}
/**
* Remove yourself from the project.
*/
const remove = async function (context) {
// No-op, as we do this all in `boilerplate.js`
}
// Required in all Ignite CLI plugins
module.exports = { add, remove }
================================================
FILE: readme.md
================================================
## Ignite TypeScript Boilerplate for React Native
### The easiest way to develop React Native apps in TypeScript.
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.
Originally based on a port of the [Ignite IR Boilerplate](https://github.com/infinitered/ignite-ir-boilerplate) to TypeScript.
Currently includes:
* React Native 0.51.0 (but you can change this if you want to experiment)
* React Navigation
* Redux
* Redux Sagas
* And more!
## Quick Start
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:
```sh
ignite new MyLatestCreation --b ignite-typescript-boilerplate
```
You can also change the React Native version, just keep in mind, we may not have tested this just yet.
```sh
ignite new MyLatestCreation --b ignite-typescript-boilerplate --react-native-version 0.46.0-rc.2
```
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:
```sh
ignite new MyLatestCreation --b ignite-typescript-boilerplate --max
```
If you want very few of these extras:
```sh
ignite new MyLatestCreation --b ignite-typescript-boilerplate --min
```
## Using TypeScript with React Native
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.
## Coding style
We use `tslint` to enforce coding style, with rules based on [Palantir's tslint-react](https://github.com/palantir/tslint-react),
and a few changes to accommodate some Ignite quirks. If you install a plugin, your editor can probably automatically fix problems.
In VS Code, set `"tslint.autoFixOnSave": true` in your
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.
## Boilerplate walkthrough
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.
### Components
React components go here. We generate these as stateless functional components by default, as recommended by the React team.
### Containers
Containers are Redux-connected components, and are mostly full screens.
* `App.tsx` - your main application. We create a Redux store and configure it here
* `RootContainer.tsx` - main view of your application. Contains your status bar and navigation component
* `LaunchScreen.tsx` - this is the first screen shown in your application. It's loaded into the Navigation component
### Navigation
Your primary and other navigation components reside here.
* `AppNavigation.tsx` - loads in your initial screen and creates your menu(s) in a StackNavigation
* `Styles` - styling for the navigation
### Storybook
[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.
### Themes
Styling themes used throughout your app styles.
* `ApplicationStyles.ts` - app-wide styles
* `Colors.ts` - defined colors for your app
* `Fonts.ts` - defined fonts for your app
* `Images.ts` - loads and caches images used in your app
* `Metrics.ts` - useful measurements of things like navBarHeight
### Config
Initialize and configure things here.
* `AppConfig.ts` - simple React Native configuration here
* `DebugConfig.js` - define how you want your debug environment to act. This is a .js file because that's what
Ignite expects to find.
* `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)
### Fixtures
Contains json files that mimic API responses for quicker development. These are used by the `Services/FixtureApi.ts` object to mock API responses.
### Redux, Sagas
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
type checking of our reducers and actions. Take a look at `Lib/ReduxHelpers.ts` for some extra functions that
we use to make them more Ignite-y.
### Services
Contains your API service and other important utilities for your application.
* `Api.tsx` - main API service, giving you an interface to communicate with your back end
* `ExamplesRegistry.tsx` - lets you view component and Ignite plugin examples in your app
* `FixtureApi.tsx` - mocks your API service, making it faster to develop early on in your app
### Lib
We recommend using this folder for modules that can be extracted into their own NPM packages at some point.
### Images
Contains actual images (usually png) used in your application.
### Transforms
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.
### Tests
We create Jest tests alongside the components, reducers and sagas. Enable this by adding `"tests": "jest"` to `ignite/ignite.json`.
### Code generation
Currently, the following code generation commands work properly:
* `ignite generate component MyComponent` - generates a stateless functional component.
* `ignite generate container MyContainer` - generates a Redux-connected React.Component, with state and view lifecycle.
* `ignite generate screen MyScreen` - generates a Redux-connected React.Component, with state, view lifecycle and react-navigation.
* `ignite generate reducers MyNew` - generates a set of Redux reducers.
* `ignite generate saga MySaga` - generates a Redux Saga
* `ignite generate list MyList` - generates a FlatList, formatted either as a grid or list.
### Further reading
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.
Microsoft created [TypeScript React Native Starter](https://github.com/Microsoft/TypeScript-React-Native-Starter), which includes a walkthrough on switching projects to TypeScript.
[React TypeScript Tutorial](https://github.com/DanielRosenwasser/React-TypeScript-Tutorial) is React rather than React Native, but has useful guides.
[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.
### Credits
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.
================================================
FILE: templates/component-index.ejs
================================================
import <%= props.name %> from "./<%= props.name %>";
export default <%= props.name %>;
================================================
FILE: templates/component-story.ejs
================================================
import { storiesOf } from "@storybook/react-native";
import * as React from "react";
import <%= props.name %> from "./<%= props.name %>";
storiesOf("<%= props.name %>", module)
.add("Default", () => (
<<%= props.name %> />
));
================================================
FILE: templates/component-style.ejs
================================================
import { StyleSheet } from "react-native";
export default StyleSheet.create({
container: {
flex: 1,
},
});
================================================
FILE: templates/component-test-jest.ejs
================================================
///
import * as React from "react";
import { <%= props.name %> } from "./<%= props.name %>";
import * as renderer from "react-test-renderer";
test("<%= props.name %> component renders correctly", () => {
const tree = renderer.create(<<%= props.name %> someProperty="howdy" anotherProperty={false} />).toJSON();
expect(tree).toMatchSnapshot();
});
================================================
FILE: templates/component.ejs
================================================
import * as React from "react";
import { Text, View } from "react-native";
import styles from "./<%= props.name %>Style";
interface Props {
someProperty: string;
anotherProperty: boolean;
}
const <%= props.name %>: React.SFC = ({someProperty, anotherProperty}: Props) => (
<%= props.name %> Component
);
export default <%= props.name %>;
================================================
FILE: templates/container-style.ejs
================================================
import { StyleSheet } from "react-native";
import { Colors, Metrics } from "../../Themes/";
export default StyleSheet.create({
container: {
flex: 1,
marginTop: Metrics.navBarHeight,
backgroundColor: Colors.background,
},
});
================================================
FILE: templates/container.ejs
================================================
import * as React from "react";
import { Text, View } from "react-native";
import { connect } from "react-redux";
import * as Redux from "redux";
import { RootState } from "../../Reducers";
import { Images } from "../../Themes";
import Metrics from "../../Themes/Metrics";
// Styles
import styles from "./<%= props.name %>Style";
/**
* The properties passed to the component
*/
export interface OwnProps {
}
/**
* The properties mapped from Redux dispatch
*/
export interface DispatchProps {
}
/**
* The properties mapped from the global state
*/
export interface StateProps {
}
/**
* The local state
*/
export interface State {
}
type Props = StateProps & DispatchProps & OwnProps;
class <%= props.name %> extends
React.Component {
public state = {
}
public render() {
return (
Hello <%= props.name %>
);
}
}
const mapDispatchToProps = (dispatch: Redux.Dispatch): DispatchProps => ({
});
const mapStateToProps = (state: RootState, ownProps: OwnProps): StateProps => {
return {};
};
export default connect(mapStateToProps, mapDispatchToProps)(<%= props.name %>) as React.ComponentClass;
================================================
FILE: templates/flatlist-grid-style.ejs
================================================
import { StyleSheet } from 'react-native'
import { ApplicationStyles, Metrics, Colors } from '../../../Themes'
export default StyleSheet.create({
...ApplicationStyles.screen,
container: {
flex: 1,
backgroundColor: Colors.background
},
row: {
flex: 1,
backgroundColor: Colors.fire,
marginVertical: Metrics.smallMargin,
justifyContent: 'center',
margin: 10,
padding: 5,
paddingVertical: 10,
borderRadius: Metrics.smallMargin
},
boldLabel: {
fontWeight: 'bold',
alignSelf: 'center',
color: Colors.snow,
textAlign: 'center',
marginBottom: Metrics.smallMargin
},
label: {
textAlign: 'center',
color: Colors.snow
},
listContent: {
marginTop: Metrics.baseMargin
}
})
================================================
FILE: templates/flatlist-grid.ejs
================================================
import * as React from "react";
import { View, Text, FlatList } from "react-native";
import { connect } from "react-redux";
// More info here: https://facebook.github.io/react-native/docs/flatlist.html
// Styles
import styles from "./<%= props.name %>Style";
export interface RowItem {
title: string;
description: string;
}
interface Props {
data: RowItem[];
}
export default class <%= props.name %> extends React.PureComponent {
/* ***********************************************************
* `renderRow` function. How each cell/row should be rendered
* It's our best practice to place a single component here:
*
* e.g.
return
*************************************************************/
renderRow ({item}: {item: RowItem}) {
return (
{item.title}
{item.description}
)
}
// Render a header?
renderHeader = () =>
- Header -
// Render a footer?
renderFooter = () =>
- Footer -
// Show this when data is empty
renderEmpty = () =>
- Nothing to See Here -
renderSeparator = () =>
- ~~~~~ -
// The default function if no Key is provided is index
// an identifiable key is important if you plan on
// item reordering. Otherwise index is fine
keyExtractor = (item: RowItem, index: number) => index
// How many items should be kept im memory as we scroll?
oneScreensWorth = 20
// extraData is for anything that is not indicated in data
// for instance, if you kept "favorites" in `this.state.favs`
// pass that in, so changes in favorites will cause a re-render
// and your renderItem will have access to change depending on state
// e.g. `extraData`={this.state.favs}
// Optimize your list if the height of each item can be calculated
// by supplying a constant height, there is no need to measure each
// item after it renders. This can save significant time for lists
// of a size 100+
// e.g. itemLayout={(data, index) => (
// {length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index}
// )}
render () {
return (
)
}
}
================================================
FILE: templates/flatlist-sections.ejs
================================================
import * as React from "react";
import { View, SectionList, Text } from "react-native";
// More info here: https://facebook.github.io/react-native/docs/sectionlist.html
// Styles
import styles from "./<%= props.name %>Style";
export interface Section {
key: string;
data: RowItem[];
}
export interface RowItem {
title: string;
description: string;
}
interface Props {
data: Section[];
}
class <%= props.name %> extends React.PureComponent {
/* ***********************************************************
* `renderItem` function - How each cell should be rendered
* It's our best practice to place a single component here:
*
* e.g.
* return
*
* For sections with different cells (heterogeneous lists), you can do branch
* logic here based on section.key OR at the data level, you can provide
* `renderItem` functions in each section.
*
* Note: You can remove section/separator functions and jam them in here
*************************************************************/
renderItem ({section, item}: {section: Section, item: RowItem}) {
return (
Section {section.key} - {item.title}
{item.description}
)
}
// Conditional branching for section headers, also see step 3
renderSectionHeader ({section}: {section: Section}) {
switch (section.key) {
case "First":
return First Section
default:
return Second Section
}
}
/* ***********************************************************
* Consider the configurations we've set below. Customize them
* to your liking! Each with some friendly advice.
*
* Removing a function here will make SectionList use default
*************************************************************/
// Render a header?
renderHeader = () =>
- Full List Header -
// Render a footer?
renderFooter = () =>
- Full List Footer -
// Does each section need a footer?
renderSectionFooter = () =>
END SECTION!!!!
// Show this when data is empty
renderEmpty = () =>
- Nothing to See Here -
renderSeparator = () =>
- ~~~~~ -
renderSectionSeparator = () =>
\/\/\/\/\/\/\/\/
// The default function if no Key is provided is index
// an identifiable key is important if you plan on
// item reordering. Otherwise index is fine
keyExtractor = (item, index) => index
// How many items should be kept im memory as we scroll?
oneScreensWorth = 20
// extraData is for anything that is not indicated in data
// for instance, if you kept "favorites" in `this.state.favs`
// pass that in, so changes in favorites will cause a re-render
// and your renderItem will have access to change depending on state
// e.g. `extraData`={this.state.favs}
// Optimize your list if the height of each item can be calculated
// by supplying a constant height, there is no need to measure each
// item after it renders. This can save significant time for lists
// of a size 100+
// e.g. itemLayout={(data, index) => (
// {length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index}
// )}
render () {
return (
)
}
}
================================================
FILE: templates/flatlist.ejs
================================================
import * as React from "react";
import { View, Text, FlatList } from "react-native";
// More info here: https://facebook.github.io/react-native/docs/flatlist.html
// Styles
import styles from "./<%= props.name %>Style";
export interface RowItem {
title: string;
description: string;
}
interface Props {
data: RowItem[];
}
export default class <%= props.name %> extends React.PureComponent {
/* ***********************************************************
* `renderRow` function. How each cell/row should be rendered
* It's our best practice to place a single component here:
*
* e.g.
return
*************************************************************/
renderRow ({item}: {item: RowItem}) {
return (
{item.title}
{item.description}
)
}
// Render a header?
renderHeader = () =>
- Header -
// Render a footer?
renderFooter = () =>
- Footer -
// Show this when data is empty
renderEmpty = () =>
- Nothing to See Here -
renderSeparator = () =>
- ~~~~~ -
// The default function if no Key is provided is index
// an identifiable key is important if you plan on
// item reordering. Otherwise index is fine
keyExtractor = (item: RowItem, index: number) => index
// How many items should be kept im memory as we scroll?
oneScreensWorth = 20
// extraData is for anything that is not indicated in data
// for instance, if you kept "favorites" in `this.state.favs`
// pass that in, so changes in favorites will cause a re-render
// and your renderItem will have access to change depending on state
// e.g. `extraData`={this.state.favs}
// Optimize your list if the height of each item can be calculated
// by supplying a constant height, there is no need to measure each
// item after it renders. This can save significant time for lists
// of a size 100+
// e.g. itemLayout={(data, index) => (
// {length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index}
// )}
render () {
return (
)
}
}
================================================
FILE: templates/listview-grid-style.ejs
================================================
import { StyleSheet } from "react-native";
import { ApplicationStyles, Metrics, Colors } from "../../../Themes";
export default StyleSheet.create({
...ApplicationStyles.screen,
container: {
flex: 1,
backgroundColor: Colors.background
},
row: {
width: 100,
height: 100,
justifyContent: 'center',
alignItems: 'center',
margin: Metrics.baseMargin,
backgroundColor: Colors.fire,
borderRadius: Metrics.smallMargin
},
sectionHeader: {
paddingTop: Metrics.doubleBaseMargin,
width: Metrics.screenWidth,
alignSelf: 'center',
margin: Metrics.baseMargin,
backgroundColor: Colors.background
},
boldLabel: {
fontWeight: 'bold',
alignSelf: 'center',
color: Colors.snow,
textAlign: 'center',
marginBottom: Metrics.smallMargin
},
label: {
alignSelf: 'center',
color: Colors.snow,
textAlign: 'center'
},
listContent: {
justifyContent: 'space-around',
flexDirection: 'row',
flexWrap: 'wrap'
}
})
================================================
FILE: templates/reducers-test-jest.ejs
================================================
import <%= props.name %>Actions, <%= props.name %>Reducer as reducer, INITIAL_STATE from "./<%= props.name %>Reducers";
it("attempt", () => {
const state = reducer(INITIAL_STATE, <%= props.name %>Actions.request('data'))
expect(state.fetching).toBe(true)
})
it("success", () => {
const state = reducer(INITIAL_STATE, <%= props.name %>Actions.success('hi'))
expect(state.payload).toBe('hi')
})
it("failure", () => {
const state = reducer(INITIAL_STATE, <%= props.name %>Actions.failure())
expect(state.fetching).toBe(false)
expect(state.error).toBe(true)
})
================================================
FILE: templates/reducers.ejs
================================================
import { Action, AnyAction, Reducer } from "redux";
import * as SI from "seamless-immutable";
import { createAction, PayloadAction } from "typesafe-actions";
import { mapReducers, ReducerMap } from "../../Lib/ReduxHelpers";
/* ------------- Types and Action Creators ------------- */
interface <%= pascalCase(props.name) %>SuccessParams {data: string; }
const actionCreators = {
request: createAction("<%= snakeCase(props.name).toUpperCase() %>_REQUEST"),
success: (payload: <%= pascalCase(props.name) %>SuccessParams) => ({type: "<%= snakeCase(props.name).toUpperCase() %>_SUCCESS", payload})),
failure: createAction("<%= snakeCase(props.name).toUpperCase() %>_FAILURE"),
};
export const <%= pascalCase(props.name) %>Actions = actionCreators;
export interface <%= pascalCase(props.name) %>State {
data?: string | null;
error?: boolean | null;
fetching?: boolean | null;
}
export type <%= pascalCase(props.name) %>Action = PayloadActionState>;
export type Immutable<%= pascalCase(props.name) %>State = SI.ImmutableObject<<%= pascalCase(props.name) %>State>;
/* ------------- Initial State ------------- */
export const INITIAL_STATE: Immutable<%= pascalCase(props.name) %>State = SI.from({
data: null,
error: null,
fetching: null,
});
/* ------------- Reducers ------------- */
export const request: ReducerState> =
(state: Immutable<%= pascalCase(props.name) %>State) =>
state.merge({ fetching: true });
export const success: ReducerState> =
(state: Immutable<%= pascalCase(props.name) %>State, action: AnyAction & {payload?: <%= pascalCase(props.name) %>SuccessParams}) => {
if (!action.payload) {
return failure(state, action);
}
const { data } = action.payload;
return state.merge({ fetching: false, error: null, data });
};
export const failure: ReducerState> = (state: Immutable<%= pascalCase(props.name) %>State) =>
state.merge({ fetching: false, error: true, data: null });
/* ------------- Hookup Reducers To Types ------------- */
const reducerMap: ReducerMapState> = {
request,
failure,
success,
};
export const <%= pascalCase(props.name) %>Reducer = mapReducers(INITIAL_STATE, reducerMap, actionCreators);
export default <%= pascalCase(props.name) %>Reducer;
================================================
FILE: templates/saga-test-jest.ejs
================================================
/* ***********************************************************
* Wiring Instructions
* To make this test work, you'll need to:
* - Add a Fixture named get<%= props.name %> to the
* ./App/Services/FixtureApi file. You can just keep adding
* functions to that file.
*************************************************************/
import FixtureAPI from '../../Services/FixtureApi'
import { call, put } from 'redux-saga/effects'
import { get<%= props.name %> } from './index'
import <%= props.name %>Actions from '../../Reducers/<%= props.name %>Reducers'
const stepper = (fn) => (mock) => fn.next(mock).value
it('first calls API', () => {
const step = stepper(get<%= props.name %>(FixtureAPI, {data: 'taco'}))
// first yield is the API
expect(step()).toEqual(call(FixtureAPI.get<%= props.name %>, 'taco'))
})
it('success path', () => {
const response = FixtureAPI.get<%= props.name %>('taco')
const step = stepper(get<%= props.name %>(FixtureAPI, {data: 'taco'}))
// Step 1: Hit the api
step()
// Step 2: Successful return and data!
expect(step(response)).toEqual(put(<%= pascalCase(props.name) %>Actions.<%= camelCase(props.name) %>Success(21)))
})
it('failure path', () => {
const response = {ok: false}
const step = stepper(get<%= props.name %>(FixtureAPI, {data: 'taco'}))
// Step 1: Hit the api
step()
// Step 2: Failed response.
expect(step(response)).toEqual(put(<%= pascalCase(props.name) %>Actions.<%= camelCase(props.name) %>Failure()))
})
================================================
FILE: templates/saga.ejs
================================================
/* ***********************************************************
* A short word on how to use this automagically generated file.
* We're often asked in the ignite gitter channel how to connect
* to a to a third party api, so we thought we'd demonstrate - but
* you should know you can use sagas for other flow control too.
*
* Other points:
* - You'll need to add this saga to sagas/index.ts
* - This template uses the api declared in sagas/index.ts, so
* you'll need to define a constant in that file.
*************************************************************/
import { ApiResponse } from "apisauce";
import { SagaIterator } from "redux-saga";
import { call, put } from "redux-saga/effects";
import { <%= props.name %>Actions, <%= props.name %>Action } from "../../Reducers/<%= props.name %>Reducers";
import { <%= props.name %>Api, <%= props.name %>Response } from "../../Services/<%= props.name %>Api";
export function * get<%= props.name %> (api: <%= props.name %>Api, action: <%= props.name %>Action): SagaIterator {
const { data } = action;
// make the call to the api
const response: ApiResponse<<%= props.name %>Response> = yield call(api.get<%= camelCase(props.name) %>, data);
// success?
if (response.ok) {
// You might need to change the response here - do this with a 'transform',
// located in ../../Transforms/. Otherwise, just pass the data back from the api.
yield put(<%= props.name %>Actions.success({data: response.data}));
} else {
yield put(<%= props.name %>Actions.failure());
}
}
================================================
FILE: templates/screen-index.ejs
================================================
import <%= props.name %> from "./<%= props.name %>";
export default <%= props.name %>;
================================================
FILE: templates/screen-style.ejs
================================================
import { StyleSheet } from "react-native";
import { Colors, Metrics } from "../../Themes/";
export default StyleSheet.create({
container: {
flex: 1,
marginTop: Metrics.navBarHeight,
backgroundColor: Colors.background,
},
});
================================================
FILE: templates/screen.ejs
================================================
import * as React from "react";
import { Alert, Image, Text, TouchableOpacity, View } from "react-native";
import Icon from "react-native-vector-icons/FontAwesome";
import { NavigationAction, NavigationDrawerScreenOptions, NavigationScreenProps, NavigationState } from "react-navigation";
import { connect } from "react-redux";
import * as Redux from "redux";
import { RootState } from "../../Reducers";
import { Images } from "../../Themes";
import Metrics from "../../Themes/Metrics";
// Styles
import styles from "./<%= props.name %>Style";
/**
* The properties passed to the component
*/
export interface OwnProps {
}
/**
* The properties mapped from Redux dispatch
*/
export interface DispatchProps {
}
/**
* The properties mapped from the global state
*/
export interface StateProps {
}
/**
* The local state
*/
export interface State {
}
type Props = StateProps & DispatchProps & OwnProps & NavigationScreenProps<{}>;
class <%= props.name %> extends
React.Component {
public state = {
};
public static navigationOptions: NavigationDrawerScreenOptions = {
drawerLabel: "Welcome",
drawerIcon: ({ tintColor, focused }: {tintColor: string, focused: boolean}) => (
),
};
public render() {
return (
Hello <%= props.name %>
);
}
}
const mapDispatchToProps = (dispatch: Redux.Dispatch): DispatchProps => ({
});
const mapStateToProps = (state: RootState, ownProps: OwnProps): StateProps => {
return {};
};
export default connect(mapStateToProps, mapDispatchToProps)(<%= props.name %>) as React.ComponentClass;
================================================
FILE: test/generators-integration.test.js
================================================
const execa = require('execa')
const jetpack = require('fs-jetpack')
const tempy = require('tempy')
const IGNITE = 'ignite'
const APP = 'IntegrationTest'
const BOILERPLATE = `${__dirname}/..`
console.warn(BOILERPLATE)
// calling the ignite cli takes a while
jasmine.DEFAULT_TIMEOUT_INTERVAL = 600000
describe('without a linter', () => {
beforeAll(async () => {
// creates a new temp directory
process.chdir(tempy.directory())
await execa(IGNITE, ['new', APP, '--min', '--skip-git', '--no-lint', '--boilerplate', BOILERPLATE])
process.chdir(APP)
})
test('does not have a linting script', async () => {
expect(jetpack.read('package.json', 'json')['scripts']['lint']).toBe(undefined)
})
})
describe('generators', () => {
beforeAll(async () => {
// creates a new temp directory
process.chdir(tempy.directory())
await execa(IGNITE, ['new', APP, '--min', '--skip-git', '--boilerplate', BOILERPLATE])
process.chdir(APP)
})
test('generates a component', async () => {
await execa(IGNITE, ['g', 'component', 'Test'], { preferLocal: false })
expect(jetpack.exists('App/Components/Test.tsx')).toBe('file')
expect(jetpack.exists('App/Components/Styles/TestStyle.ts')).toBe('file')
const lint = await execa('npm', ['-s', 'run', 'lint'])
expect(lint.stderr).toBe('')
})
test('generate listview of type row works', async () => {
await execa(IGNITE, ['g', 'list', 'TestRow', '--type=Row', '--codeType=listview', '--dataType=Single'], { preferLocal: false })
expect(jetpack.exists('App/Containers/TestRow.tsx')).toBe('file')
expect(jetpack.exists('App/Containers/Styles/TestRowStyle.ts')).toBe('file')
const lint = await execa('npm', ['run', 'lint'])
expect(lint.stderr).toBe('')
})
test('generate flatlist of type row works', async () => {
await execa(IGNITE, ['g', 'list', 'TestFlatRow', '--type=Row', '--codeType=flatlist', '--dataType=Single'], { preferLocal: false })
expect(jetpack.exists('App/Containers/TestFlatRow.tsx')).toBe('file')
expect(jetpack.exists('App/Containers/Styles/TestFlatRowStyle.ts')).toBe('file')
const lint = await execa('npm', ['run', 'lint'])
expect(lint.stderr).toBe('')
})
test('generate listview of sections works', async () => {
await execa(IGNITE, ['g', 'list', 'TestSection', '--type=Row', '--codeType=listview', '--dataType=Sectioned'], { preferLocal: false })
expect(jetpack.exists('App/Containers/TestSection.tsx')).toBe('file')
expect(jetpack.exists('App/Containers/Styles/TestSectionStyle.ts')).toBe('file')
const lint = await execa('npm', ['run', 'lint'])
expect(lint.stderr).toBe('')
})
test('generate flatlist of sections works', async () => {
await execa(IGNITE, ['g', 'list', 'TestFlatSection', '--type=Row', '--codeType=flatlist', '--dataType=Sectioned'], { preferLocal: false })
expect(jetpack.exists('App/Containers/TestFlatSection.tsx')).toBe('file')
expect(jetpack.exists('App/Containers/Styles/TestFlatSectionStyle.ts')).toBe('file')
const lint = await execa('npm', ['run', 'lint'])
expect(lint.stderr).toBe('')
})
test('generate listview of type grid works', async () => {
await execa(IGNITE, ['g', 'list', 'TestGrid', '--type=Grid', '--codeType=listview', '--dataType=Single'], { preferLocal: false })
expect(jetpack.exists('App/Containers/TestGrid.tsx')).toBe('file')
expect(jetpack.exists('App/Containers/Styles/TestGridStyle.ts')).toBe('file')
const lint = await execa('npm', ['run', 'lint'])
expect(lint.stderr).toBe('')
})
test('generate redux works', async () => {
await execa(IGNITE, ['g', 'redux', 'Test'], { preferLocal: false })
expect(jetpack.exists('App/Redux/TestRedux.tsx')).toBe('file')
const lint = await execa('npm', ['run', 'lint'])
expect(lint.stderr).toBe('')
})
test('generate container works', async () => {
await execa(IGNITE, ['g', 'container', 'Container'], { preferLocal: false })
expect(jetpack.exists('App/Containers/Container.tsx')).toBe('file')
expect(jetpack.exists('App/Containers/Styles/ContainerStyle.ts')).toBe('file')
const lint = await execa('npm', ['run', 'lint'])
expect(lint.stderr).toBe('')
})
test('generate saga works', async () => {
await execa(IGNITE, ['g', 'saga', 'Test'], { preferLocal: false })
expect(jetpack.exists('App/Sagas/TestSagas.tsx')).toBe('file')
const lint = await execa('npm', ['run', 'lint'])
expect(lint.stderr).toBe('')
})
test('generate screen works', async () => {
await execa(IGNITE, ['g', 'screen', 'Test'], { preferLocal: false })
expect(jetpack.exists('App/Containers/TestScreen.tsx')).toBe('file')
expect(jetpack.exists('App/Containers/Styles/TestScreenStyle.ts')).toBe('file')
const lint = await execa('npm', ['run', 'lint'])
expect(lint.stderr).toBe('')
})
})
================================================
FILE: test/interface.test.js
================================================
const boilerplate = require('../boilerplate')
const plugin = require('../plugin')
test('boilerplate interface', async () => {
expect(typeof boilerplate.install).toBe('function')
})
test('plugin interface', async () => {
expect(typeof plugin.add).toBe('function')
expect(typeof plugin.remove).toBe('function')
})
================================================
FILE: test/react-native-version.test.js
================================================
const boilerplate = require('../lib/react-native-version')
// grab a few things from the boilerplate module
const get = boilerplate.getReactNativeVersion
const DEFAULT = boilerplate.REACT_NATIVE_VERSION
/**
* Runs with a valid gluegun context and a staged version number.
*
* @param {*} reactNativeVersion The React Native version to use.
* @return {string} The version number we should be using.
*/
const mock = reactNativeVersion => get({
parameters: {
options: {
'react-native-version': reactNativeVersion
}
}
})
// this would only happen if we screwed something up in our boilerplate.js
test('it handles strange inputs from code', () => {
expect(get()).toBe(DEFAULT)
expect(get(null)).toBe(DEFAULT)
expect(get(true)).toBe(DEFAULT)
expect(get(8)).toBe(DEFAULT)
expect(get('hello')).toBe(DEFAULT)
expect(get([])).toBe(DEFAULT)
expect(get({})).toBe(DEFAULT)
expect(get(() => true)).toBe(DEFAULT)
})
// this could happen because it's valid input via minimist from the user
test('it handles strange input from the user', () => {
expect(mock(true)).toBe(DEFAULT)
expect(mock(false)).toBe(DEFAULT)
expect(mock([])).toBe(DEFAULT)
expect(mock({})).toBe(DEFAULT)
})
// very edge-casey
test('it handles not-quite semver numbers', () => {
expect(mock(0)).toBe(DEFAULT)
expect(mock(0.25)).toBe(DEFAULT)
})
// happy path
test('it handles valid versions', () => {
expect(mock('0.41.0')).toBe('0.41.0')
expect(mock('0.41.0-beta.1')).toBe('0.41.0-beta.1')
expect(mock(DEFAULT)).toBe(DEFAULT)
expect(mock('next')).toBe('next')
})
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
/* Basic Options */
"target": "ESNEXT", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */
"module": "ESNext", /* Specify module code generation: 'none', commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
// "lib": [], /* Specify library files to be included in the compilation: */
"allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
"jsx": "react-native", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
"sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "./dist", /* Redirect output structure to the directory. */
// "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
"baseUrl": "./boilerplate/", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
"typeRoots": ["./boilerplate/types/", "./node_modules/"], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
"allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
/* Source Map Options */
// "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
}
}