Repository: aerian-studios/ignite-typescript-boilerplate Branch: master Commit: 356ac6758859 Files: 129 Total size: 141.2 KB Directory structure: gitextract_1v1ble8m/ ├── .gitignore ├── boilerplate/ │ ├── .babelrc │ ├── .editorconfig │ ├── App/ │ │ ├── Components/ │ │ │ ├── AlertMessage/ │ │ │ │ ├── AlertMessage.story.tsx │ │ │ │ ├── AlertMessage.tsx │ │ │ │ ├── AlertMessageStyles.ts │ │ │ │ ├── AlertMessageTest.tsx │ │ │ │ └── index.ts │ │ │ ├── DrawerButton/ │ │ │ │ ├── DrawerButton.story.tsx │ │ │ │ ├── DrawerButton.tsx │ │ │ │ ├── DrawerButtonStyles.ts │ │ │ │ ├── DrawerButtonTest.tsx │ │ │ │ └── index.ts │ │ │ ├── FullButton/ │ │ │ │ ├── FullButton.story.tsx │ │ │ │ ├── FullButton.tsx │ │ │ │ ├── FullButtonStyles.ts │ │ │ │ ├── FullButtonTest.tsx │ │ │ │ └── index.ts │ │ │ ├── README.md │ │ │ ├── RoundedButton/ │ │ │ │ ├── RoundedButton.story.tsx │ │ │ │ ├── RoundedButton.tsx │ │ │ │ ├── RoundedButtonStyles.ts │ │ │ │ ├── RoundedButtonTest.tsx │ │ │ │ └── index.ts │ │ │ └── Stories.tsx │ │ ├── Config/ │ │ │ ├── AppConfig.ts │ │ │ ├── DebugConfig.js │ │ │ ├── README.md │ │ │ ├── ReactotronConfig.ts │ │ │ ├── ReduxPersist.ts │ │ │ └── index.ts │ │ ├── Containers/ │ │ │ ├── App.tsx │ │ │ ├── LaunchScreen/ │ │ │ │ ├── LaunchScreen.tsx │ │ │ │ ├── LaunchScreenStyles.ts │ │ │ │ └── index.ts │ │ │ ├── README.md │ │ │ └── RootContainer/ │ │ │ ├── RootContainer.tsx │ │ │ ├── RootContainerStyles.ts │ │ │ └── index.ts │ │ ├── Fixtures/ │ │ │ ├── README.md │ │ │ ├── gantman.json │ │ │ ├── rateLimit.json │ │ │ ├── root.json │ │ │ └── skellock.json │ │ ├── Images/ │ │ │ └── README.md │ │ ├── Lib/ │ │ │ ├── README.md │ │ │ └── ReduxHelpers.ts │ │ ├── Navigation/ │ │ │ ├── AppNavigation.tsx │ │ │ ├── ReduxNavigation.tsx │ │ │ └── Styles/ │ │ │ └── NavigationStyles.ts │ │ ├── Reducers/ │ │ │ ├── CreateStore.tsx │ │ │ ├── GithubReducers/ │ │ │ │ ├── GithubReducersTest.tsx │ │ │ │ └── index.tsx │ │ │ ├── NavigationReducers/ │ │ │ │ └── index.tsx │ │ │ ├── ScreenTrackingMiddleware.tsx │ │ │ ├── StartupReducers/ │ │ │ │ └── index.tsx │ │ │ └── index.ts │ │ ├── Sagas/ │ │ │ ├── GithubSagas/ │ │ │ │ ├── GithubSagaTest.ts │ │ │ │ └── index.ts │ │ │ ├── StartupSagas/ │ │ │ │ ├── StartupSagaTest.ts │ │ │ │ └── index.ts │ │ │ └── index.ts │ │ ├── Services/ │ │ │ ├── Api.ts │ │ │ ├── ExamplesRegistry.tsx │ │ │ ├── FixtureAPITest.tsx │ │ │ ├── FixtureApi.tsx │ │ │ ├── GithubApi.tsx │ │ │ ├── ImmutablePersistenceTransform.tsx │ │ │ └── RehydrationServices.tsx │ │ ├── Themes/ │ │ │ ├── ApplicationStyles.ts │ │ │ ├── Colors.ts │ │ │ ├── Fonts.ts │ │ │ ├── Images.ts │ │ │ ├── Metrics.ts │ │ │ ├── README.md │ │ │ └── index.ts │ │ └── Transforms/ │ │ ├── ConvertFromKelvin.ts │ │ └── README.md │ ├── README.md │ ├── Tests/ │ │ ├── Setup.tsx.ejs │ │ └── StoriesTest.ts │ ├── ignite.json.ejs │ ├── index.js.ejs │ ├── package.json.ejs │ ├── rn-cli.config.js │ ├── storybook/ │ │ ├── addons.js │ │ ├── index.js │ │ └── storybook.ejs │ ├── tsconfig.json │ ├── tslint.json │ └── types/ │ ├── @storybook/ │ │ └── react-native.d.ts │ └── reduxsauce/ │ └── index.d.ts ├── boilerplate.js ├── commands/ │ ├── component.js │ ├── container.js │ ├── list.js │ ├── reducers.js │ ├── saga.js │ └── screen.js ├── ignite.json ├── lib/ │ ├── patterns.js │ └── react-native-version.js ├── options.js ├── package.json ├── plugin.js ├── readme.md ├── templates/ │ ├── component-index.ejs │ ├── component-story.ejs │ ├── component-style.ejs │ ├── component-test-jest.ejs │ ├── component.ejs │ ├── container-style.ejs │ ├── container.ejs │ ├── flatlist-grid-style.ejs │ ├── flatlist-grid.ejs │ ├── flatlist-sections.ejs │ ├── flatlist.ejs │ ├── listview-grid-style.ejs │ ├── reducers-test-jest.ejs │ ├── reducers.ejs │ ├── saga-test-jest.ejs │ ├── saga.ejs │ ├── screen-index.ejs │ ├── screen-style.ejs │ └── screen.ejs ├── test/ │ ├── generators-integration.test.js │ ├── interface.test.js │ └── react-native-version.test.js └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # OSX # .DS_Store npm-debug.log npm-debug.log* coverage .nyc_output yarn.lock lerna-debug.log node_modules .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json testgrounds IntegrationTest integration_test ================================================ FILE: boilerplate/.babelrc ================================================ { "presets": ["react-native"], "env": { "production": { "plugins": ["ignite-ignore-reactotron"] } } } ================================================ FILE: boilerplate/.editorconfig ================================================ # EditorConfig is awesome: http://EditorConfig.org # top-most EditorConfig file root = true # Unix-style newlines with a newline ending every file [*] end_of_line = lf insert_final_newline = true indent_style = space indent_size = 2 charset = utf-8 trim_trailing_whitespace = true [*.gradle] indent_size = 4 ================================================ FILE: boilerplate/App/Components/AlertMessage/AlertMessage.story.tsx ================================================ import { storiesOf } from "@storybook/react-native"; import * as React from "react"; import AlertMessage from "./AlertMessage"; storiesOf("AlertMessage") .add("Default", () => ( )) .add("Hidden", () => ( )) .add("Custom Style", () => ( )); ================================================ FILE: boilerplate/App/Components/AlertMessage/AlertMessage.tsx ================================================ import * as React from "react"; import { Text, View, ViewStyle } from "react-native"; import styles from "./AlertMessageStyles"; interface Props { icon?: string; show?: boolean; style?: ViewStyle; title?: string; } const AlertMessage: React.SFC = ({ icon, show = true, style, title }: Props) => show ? ( {title && title.toUpperCase()} ) : null; export default AlertMessage; ================================================ FILE: boilerplate/App/Components/AlertMessage/AlertMessageStyles.ts ================================================ import { StyleSheet } from "react-native"; import { Colors, Fonts, Metrics } from "../../Themes/"; export default StyleSheet.create({ container: { justifyContent: "center", marginVertical: Metrics.section, }, contentContainer: { alignSelf: "center", alignItems: "center", }, message: { marginTop: Metrics.baseMargin, marginHorizontal: Metrics.baseMargin, textAlign: "center", fontFamily: Fonts.type.base, fontSize: Fonts.size.regular, fontWeight: "bold", color: Colors.steel, }, icon: { color: Colors.steel, }, }); ================================================ FILE: boilerplate/App/Components/AlertMessage/AlertMessageTest.tsx ================================================ /// import * as React from "react"; import "react-native"; import * as renderer from "react-test-renderer"; import AlertMessage from "./AlertMessage"; test("AlertMessage component renders correctly if show is true", () => { const tree = renderer.create().toJSON(); expect(tree).toMatchSnapshot(); }); test("AlertMessage component does not render if show is false", () => { const tree = renderer.create().toJSON(); expect(tree).toMatchSnapshot(); }); test("AlertMessage component renders correctly if backgroundColor prop is set", () => { const tree = renderer.create().toJSON(); expect(tree).toMatchSnapshot(); }); ================================================ FILE: boilerplate/App/Components/AlertMessage/index.ts ================================================ import AlertMessage from "./AlertMessage"; export default AlertMessage; ================================================ FILE: boilerplate/App/Components/DrawerButton/DrawerButton.story.tsx ================================================ import { storiesOf } from "@storybook/react-native"; import * as React from "react"; import { View } from "react-native"; import DrawerButton from "./DrawerButton"; storiesOf("DrawerButton") .add("Default", () => ( alert("Hi")} /> )); ================================================ FILE: boilerplate/App/Components/DrawerButton/DrawerButton.tsx ================================================ import * as React from "react"; import { Text, TouchableOpacity } from "react-native"; import ExamplesRegistry from "../../Services/ExamplesRegistry"; import styles from "./DrawerButtonStyles"; // Note that this file (App/Components/DrawerButton) needs to be // imported in your app somewhere, otherwise your component won't be // compiled and added to the examples dev screen. // Ignore in coverage report /* istanbul ignore next */ ExamplesRegistry.addComponentExample("Drawer Button", () => ( window.alert("Your drawers are showing")} />), ); interface Props { onPress: () => void; text: string; } // tslint:disable-next-line:no-empty const DrawerButton: React.SFC = ({text, onPress = () => {}}: Props) => ( {text} ); export default DrawerButton; ================================================ FILE: boilerplate/App/Components/DrawerButton/DrawerButtonStyles.ts ================================================ import Colors from "../../Themes/Colors"; import Fonts from "../../Themes/Fonts"; import Metrics from "../../Themes/Metrics"; export default { text: { ...Fonts.style.h5, color: Colors.snow, marginVertical: Metrics.baseMargin, }, }; ================================================ FILE: boilerplate/App/Components/DrawerButton/DrawerButtonTest.tsx ================================================ /// import { shallow } from "enzyme"; import * as React from "react"; import "react-native"; import * as renderer from "react-test-renderer"; import DrawerButton from "./DrawerButton"; test("AlertMessage component renders correctly", () => { const tree = renderer.create( {}} text="hi" />).toJSON(); expect(tree).toMatchSnapshot(); }); test("onPress", () => { let i = 0; const onPress = () => i++; const wrapperPress = shallow(); expect(wrapperPress.prop("onPress")).toBe(onPress); // uses the right handler expect(i).toBe(0); wrapperPress.simulate("press"); expect(i).toBe(1); }); ================================================ FILE: boilerplate/App/Components/DrawerButton/index.ts ================================================ import DrawerButton from "./DrawerButton"; export default DrawerButton; ================================================ FILE: boilerplate/App/Components/FullButton/FullButton.story.tsx ================================================ import { storiesOf } from "@storybook/react-native"; import * as React from "react"; import FullButton from "./FullButton"; storiesOf("FullButton") .add("Default", () => ( )) .add("Custom Style", () => ( )); ================================================ FILE: boilerplate/App/Components/FullButton/FullButton.tsx ================================================ import * as React from "react"; import { Text, TouchableOpacity, ViewStyle } from "react-native"; import ExamplesRegistry from "../../Services/ExamplesRegistry"; import styles from "./FullButtonStyles"; // Note that this file (App/Components/FullButton) needs to be // imported in your app somewhere, otherwise your component won't be // compiled and added to the examples dev screen. // Ignore in coverage report /* istanbul ignore next */ ExamplesRegistry.addComponentExample("Full Button", () => ( window.alert("Full Button Pressed!")} />), ); interface Props { onPress?: () => void; style?: ViewStyle; text?: string; } // tslint:disable-next-line:no-empty const FullButton: React.SFC = ({text, style, onPress = () => {}}: Props) => ( {text && text.toUpperCase()} ); export default FullButton; ================================================ FILE: boilerplate/App/Components/FullButton/FullButtonStyles.ts ================================================ import { StyleSheet } from "react-native"; import Colors from "../../Themes/Colors"; import Fonts from "../../Themes/Fonts"; export default StyleSheet.create({ button: { marginVertical: 5, borderTopColor: Colors.fire, borderBottomColor: Colors.bloodOrange, borderTopWidth: 1, borderBottomWidth: 1, backgroundColor: Colors.ember, }, buttonText: { margin: 18, textAlign: "center", color: Colors.snow, fontSize: Fonts.size.medium, fontFamily: Fonts.type.bold, }, }); ================================================ FILE: boilerplate/App/Components/FullButton/FullButtonTest.tsx ================================================ /// import { shallow } from "enzyme"; import * as React from "react"; import "react-native"; import * as renderer from "react-test-renderer"; import FullButton from "./FullButton"; test("FullButton component renders correctly", () => { const tree = renderer.create( {}} text="hi" />).toJSON(); expect(tree).toMatchSnapshot(); }); test("onPress", () => { let i = 0; // i guess i could have used sinon here too... less is more i guess const onPress = () => i++; const wrapperPress = shallow(); expect(wrapperPress.prop("onPress")).toBe(onPress); // uses the right handler expect(i).toBe(0); wrapperPress.simulate("press"); expect(i).toBe(1); }); ================================================ FILE: boilerplate/App/Components/FullButton/index.ts ================================================ import FullButton from "./FullButton"; export default FullButton; ================================================ FILE: boilerplate/App/Components/README.md ================================================ ### Components Folder All components are stored and organized here "In an ideal world, most of your components would be stateless functions because in the future we’ll also be able to make performance optimizations specific to these components by avoiding unnecessary checks and memory allocations. This is the recommended pattern, when possible." --React docs ================================================ FILE: boilerplate/App/Components/RoundedButton/RoundedButton.story.tsx ================================================ import { storiesOf } from "@storybook/react-native"; import * as React from "react"; import RoundedButton from "./RoundedButton"; storiesOf("RoundedButton", module) .add("Default", () => ( )) .add("Text as children", () => ( Hello from the children! )); ================================================ FILE: boilerplate/App/Components/RoundedButton/RoundedButton.tsx ================================================ import * as React from "react"; import { Text, TouchableOpacity } from "react-native"; import ExamplesRegistry from "../../Services/ExamplesRegistry"; import styles from "./RoundedButtonStyles"; // Note that this file (App/Components/RoundedButton) needs to be // imported in your app somewhere, otherwise your component won't be // compiled and added to the examples dev screen. // Ignore in coverage report /* istanbul ignore next */ ExamplesRegistry.addComponentExample("Rounded Button", () => ( window.alert("Rounded Button Pressed!")} />), ); interface Props { onPress?: () => any; text?: string; children?: string; } // tslint:disable-next-line:no-empty const RoundedButton: React.SFC = ({ text, children, onPress = () => { } }: Props) => { const buttonText = (text || children || "").toUpperCase(); return ( {buttonText} ); }; export default RoundedButton; ================================================ FILE: boilerplate/App/Components/RoundedButton/RoundedButtonStyles.ts ================================================ import { StyleSheet } from "react-native"; import Colors from "../../Themes/Colors"; import Fonts from "../../Themes/Fonts"; import Metrics from "../../Themes/Metrics"; export default StyleSheet.create({ button: { height: 45, borderRadius: 5, marginHorizontal: Metrics.section, marginVertical: Metrics.baseMargin, backgroundColor: Colors.fire, justifyContent: "center", }, buttonText: { color: Colors.snow, textAlign: "center", fontWeight: "bold", fontSize: Fonts.size.medium, marginVertical: Metrics.baseMargin, }, }); ================================================ FILE: boilerplate/App/Components/RoundedButton/RoundedButtonTest.tsx ================================================ /// import { shallow } from "enzyme"; import * as React from "react"; import "react-native"; import * as renderer from "react-test-renderer"; import RoundedButton from "./RoundedButton"; test("RoundedButton component renders correctly", () => { const tree = renderer.create( {}} text="howdy" />).toJSON(); expect(tree).toMatchSnapshot(); }); test("RoundedButton component with children renders correctly", () => { const tree = renderer.create( {}}>Howdy).toJSON(); expect(tree).toMatchSnapshot(); }); test("onPress", () => { let i = 0; // i guess i could have used sinon here too... less is more i guess const onPress = () => i++; const wrapperPress = shallow(); expect(wrapperPress.prop("onPress")).toBe(onPress); // uses the right handler expect(i).toBe(0); wrapperPress.simulate("press"); expect(i).toBe(1); }); ================================================ FILE: boilerplate/App/Components/RoundedButton/index.ts ================================================ import RoundedButton from "./RoundedButton"; export default RoundedButton; ================================================ FILE: boilerplate/App/Components/Stories.tsx ================================================ import "./AlertMessage/AlertMessage.story"; import "./DrawerButton/DrawerButton.story"; import "./FullButton/FullButton.story"; import "./RoundedButton/RoundedButton.story"; ================================================ FILE: boilerplate/App/Config/AppConfig.ts ================================================ // Simple React Native specific changes export default { // font scaling override - RN default is on allowTextFontScaling: true, }; ================================================ FILE: boilerplate/App/Config/DebugConfig.js ================================================ /** * This is a .js file because the Ignite scripts need to find it */ export default { showDevScreens: __DEV__, useFixtures: false, ezLogin: false, yellowBox: __DEV__, reduxLogging: __DEV__, includeExamples: __DEV__, useReactotron: __DEV__, }; ================================================ FILE: boilerplate/App/Config/README.md ================================================ ### Config Folder All application specific configuration falls in this folder. `AppConfig.js` - production values. `DebugConfig.js` - development-wide globals. `ReactotronConfig.js` - Reactotron client settings. `ReduxPersist.js` - rehydrate Redux state. ================================================ FILE: boilerplate/App/Config/ReactotronConfig.ts ================================================ import Reactotron from "reactotron-react-native"; import { reactotronRedux as reduxPlugin } from "reactotron-redux"; import sagaPlugin from "reactotron-redux-saga"; import ImmutableObject from "seamless-immutable"; import Config from "../Config/DebugConfig"; if (Config.useReactotron) { // https://github.com/infinitered/reactotron for more options! Reactotron .configure({ name: "Ignite App" }) .useReactNative() .use(reduxPlugin({ onRestore: ImmutableObject })) .use(sagaPlugin()) .connect(); // Let's clear Reactotron on every time we load the app Reactotron.clear(); // Totally hacky, but this allows you to not both importing reactotron-react-native // on every file. This is just DEV mode, so no big deal. (console as any).tron = Reactotron; } ================================================ FILE: boilerplate/App/Config/ReduxPersist.ts ================================================ import { AsyncStorage } from "react-native"; import immutablePersistenceTransform from "../Services/ImmutablePersistenceTransform"; // More info here: https://shift.infinite.red/shipping-persistant-reducers-7341691232b1 const REDUX_PERSIST = { active: false, reducerVersion: "1.0", storeConfig: { storage: AsyncStorage, blacklist: ["login", "search", "nav"], // reducer keys that you do NOT want stored to persistence here // whitelist: [], Optionally, just specify the keys you DO want stored to // persistence. An empty array means 'don't store any reducers' -> infinitered/ignite#409 transforms: [immutablePersistenceTransform], }, }; export default REDUX_PERSIST; ================================================ FILE: boilerplate/App/Config/index.ts ================================================ import { Text } from "react-native"; import AppConfig from "./AppConfig"; import DebugConfig from "./DebugConfig"; // Allow/disallow font-scaling in app if (!Text.defaultProps) { Text.defaultProps = {}; } Text.defaultProps.allowFontScaling = AppConfig.allowTextFontScaling; if (__DEV__) { // If ReactNative's yellow box warnings are too much, it is possible to turn // it off, but the healthier approach is to fix the warnings. =) console.disableYellowBox = !DebugConfig.yellowBox; } ================================================ FILE: boilerplate/App/Containers/App.tsx ================================================ import * as React from "react"; import { Provider } from "react-redux"; import Reactotron from "reactotron-react-native"; import "../Config"; import DebugConfig from "../Config/DebugConfig"; import createStore from "../Reducers"; import RootContainer from "./RootContainer"; // create our store const store = createStore(); /** * Provides an entry point into our application. Both index.ios.js and index.android.js * call this component first. * * We create our Redux store here, put it into a provider and then bring in our * RootContainer. * * We separate like this to play nice with React Native's hot reloading. */ class App extends React.Component { public render() { return ( ); } } // allow reactotron overlay for fast design in dev mode export default DebugConfig.useReactotron ? Reactotron.overlay(App) : App; ================================================ FILE: boilerplate/App/Containers/LaunchScreen/LaunchScreen.tsx ================================================ import React, { Component } from "react"; import { Image, ScrollView, Text, View } from "react-native"; import DevscreensButton from "../../../ignite/DevScreens/DevscreensButton"; import { Images } from "../../Themes"; // Styles import styles from "./LaunchScreenStyles"; export default class LaunchScreen extends Component { public render() { return ( This probably isn't what your app is going to look like. Unless your designer handed you this screen and, in that case, congrats! You're ready to ship. For everyone else, this is where you'll see a live preview of your fully functioning app using Ignite. ); } } ================================================ FILE: boilerplate/App/Containers/LaunchScreen/LaunchScreenStyles.ts ================================================ import { StyleSheet } from "react-native"; import { ApplicationStyles, Metrics } from "../../Themes/index"; export default StyleSheet.create({ ...ApplicationStyles.screen, container: { paddingBottom: Metrics.baseMargin, }, logo: { marginTop: Metrics.doubleSection, height: Metrics.images.logo, width: Metrics.images.logo, resizeMode: "contain", }, centered: { alignItems: "center", }, }); ================================================ FILE: boilerplate/App/Containers/LaunchScreen/index.ts ================================================ import LaunchScreen from "./LaunchScreen"; export default LaunchScreen; ================================================ FILE: boilerplate/App/Containers/README.md ================================================ ### Containers Folder A container is what they call a "Smart Component" in Redux. It is a component which knows about Redux. They are usually used as "Screens". Also located in here are 2 special containers: `App.js` and `RootContainer.js`. `App.js` is first component loaded after `index.ios.js` or `index.android.js`. The purpose of this file is to setup Redux or any other non-visual "global" modules. Having Redux setup here helps with the hot-reloading process in React Native during development as it won't try to reload your sagas and reducers should your colors change (for example). `RootContainer.js` is the first visual component in the app. It is the ancestor of all other screens and components. You'll probably find you'll have great mileage in Ignite apps without even touching these 2 files. They, of course, belong to you, so when you're ready to add something non-visual like Firebase or something visual like an overlay, you have spots to place these additions. ================================================ FILE: boilerplate/App/Containers/RootContainer/RootContainer.tsx ================================================ import * as React from "react"; import { StatusBar, View } from "react-native"; import { connect } from "react-redux"; import ReduxPersist from "../../Config/ReduxPersist"; import ReduxNavigation from "../../Navigation/ReduxNavigation"; import { StartupActions } from "../../Reducers/StartupReducers"; // Styles import styles from "./RootContainerStyles"; interface Props { startup: () => void; } interface State { } export class RootContainer extends React.Component { public componentDidMount() { // if redux persist is not active fire startup action if (!ReduxPersist.active) { this.props.startup(); } } public render() { return ( ); } } const mapDispatchToProps = (dispatch: any): Props => ({ startup: () => dispatch(StartupActions.startup()), }); export default connect(null, mapDispatchToProps)(RootContainer); ================================================ FILE: boilerplate/App/Containers/RootContainer/RootContainerStyles.ts ================================================ import {StyleSheet, TextStyle, ViewStyle } from "react-native"; import {Colors, Fonts, Metrics} from "../../Themes/index"; export default StyleSheet.create({ applicationView: { flex: 1, }, container: { flex: 1, justifyContent: "center", backgroundColor: Colors.background, }, welcome: { fontSize: 20, textAlign: "center", fontFamily: Fonts.type.base, margin: Metrics.baseMargin, }, myImage: { width: 200, height: 200, alignSelf: "center", }, }); ================================================ FILE: boilerplate/App/Containers/RootContainer/index.ts ================================================ import RootContainer from "./RootContainer"; export default RootContainer; ================================================ FILE: boilerplate/App/Fixtures/README.md ================================================ ### Fixtures folder All key API responses are housed here. These API responses can be used for several reasons. _E.G._: * To bypass logins when building any screen of the application * To quickly test API parsing in unit tests * To separate Network from Data concerns while coding ================================================ FILE: boilerplate/App/Fixtures/gantman.json ================================================ { "total_count": 7, "incomplete_results": false, "items": [ { "login": "GantMan", "id": 997157, "avatar_url": "https://avatars.githubusercontent.com/u/997157?v=3", "gravatar_id": "", "url": "https://api.github.com/users/GantMan", "html_url": "https://github.com/GantMan", "followers_url": "https://api.github.com/users/GantMan/followers", "following_url": "https://api.github.com/users/GantMan/following{/other_user}", "gists_url": "https://api.github.com/users/GantMan/gists{/gist_id}", "starred_url": "https://api.github.com/users/GantMan/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/GantMan/subscriptions", "organizations_url": "https://api.github.com/users/GantMan/orgs", "repos_url": "https://api.github.com/users/GantMan/repos", "events_url": "https://api.github.com/users/GantMan/events{/privacy}", "received_events_url": "https://api.github.com/users/GantMan/received_events", "type": "User", "site_admin": false, "score": 122.12115 }, { "login": "vlad-G", "id": 13520880, "avatar_url": "https://avatars.githubusercontent.com/u/13520880?v=3", "gravatar_id": "", "url": "https://api.github.com/users/vlad-G", "html_url": "https://github.com/vlad-G", "followers_url": "https://api.github.com/users/vlad-G/followers", "following_url": "https://api.github.com/users/vlad-G/following{/other_user}", "gists_url": "https://api.github.com/users/vlad-G/gists{/gist_id}", "starred_url": "https://api.github.com/users/vlad-G/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/vlad-G/subscriptions", "organizations_url": "https://api.github.com/users/vlad-G/orgs", "repos_url": "https://api.github.com/users/vlad-G/repos", "events_url": "https://api.github.com/users/vlad-G/events{/privacy}", "received_events_url": "https://api.github.com/users/vlad-G/received_events", "type": "User", "site_admin": false, "score": 12.69848 }, { "login": "gantmani", "id": 3034094, "avatar_url": "https://avatars.githubusercontent.com/u/3034094?v=3", "gravatar_id": "", "url": "https://api.github.com/users/gantmani", "html_url": "https://github.com/gantmani", "followers_url": "https://api.github.com/users/gantmani/followers", "following_url": "https://api.github.com/users/gantmani/following{/other_user}", "gists_url": "https://api.github.com/users/gantmani/gists{/gist_id}", "starred_url": "https://api.github.com/users/gantmani/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/gantmani/subscriptions", "organizations_url": "https://api.github.com/users/gantmani/orgs", "repos_url": "https://api.github.com/users/gantmani/repos", "events_url": "https://api.github.com/users/gantmani/events{/privacy}", "received_events_url": "https://api.github.com/users/gantmani/received_events", "type": "User", "site_admin": false, "score": 11.641713 }, { "login": "sgantman", "id": 5911526, "avatar_url": "https://avatars.githubusercontent.com/u/5911526?v=3", "gravatar_id": "", "url": "https://api.github.com/users/sgantman", "html_url": "https://github.com/sgantman", "followers_url": "https://api.github.com/users/sgantman/followers", "following_url": "https://api.github.com/users/sgantman/following{/other_user}", "gists_url": "https://api.github.com/users/sgantman/gists{/gist_id}", "starred_url": "https://api.github.com/users/sgantman/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/sgantman/subscriptions", "organizations_url": "https://api.github.com/users/sgantman/orgs", "repos_url": "https://api.github.com/users/sgantman/repos", "events_url": "https://api.github.com/users/sgantman/events{/privacy}", "received_events_url": "https://api.github.com/users/sgantman/received_events", "type": "User", "site_admin": false, "score": 7.926345 }, { "login": "michaelgantman", "id": 16693070, "avatar_url": "https://avatars.githubusercontent.com/u/16693070?v=3", "gravatar_id": "", "url": "https://api.github.com/users/michaelgantman", "html_url": "https://github.com/michaelgantman", "followers_url": "https://api.github.com/users/michaelgantman/followers", "following_url": "https://api.github.com/users/michaelgantman/following{/other_user}", "gists_url": "https://api.github.com/users/michaelgantman/gists{/gist_id}", "starred_url": "https://api.github.com/users/michaelgantman/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/michaelgantman/subscriptions", "organizations_url": "https://api.github.com/users/michaelgantman/orgs", "repos_url": "https://api.github.com/users/michaelgantman/repos", "events_url": "https://api.github.com/users/michaelgantman/events{/privacy}", "received_events_url": "https://api.github.com/users/michaelgantman/received_events", "type": "User", "site_admin": false, "score": 7.926345 }, { "login": "gantmanis", "id": 19141249, "avatar_url": "https://avatars.githubusercontent.com/u/19141249?v=3", "gravatar_id": "", "url": "https://api.github.com/users/gantmanis", "html_url": "https://github.com/gantmanis", "followers_url": "https://api.github.com/users/gantmanis/followers", "following_url": "https://api.github.com/users/gantmanis/following{/other_user}", "gists_url": "https://api.github.com/users/gantmanis/gists{/gist_id}", "starred_url": "https://api.github.com/users/gantmanis/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/gantmanis/subscriptions", "organizations_url": "https://api.github.com/users/gantmanis/orgs", "repos_url": "https://api.github.com/users/gantmanis/repos", "events_url": "https://api.github.com/users/gantmanis/events{/privacy}", "received_events_url": "https://api.github.com/users/gantmanis/received_events", "type": "User", "site_admin": false, "score": 7.8813524 }, { "login": "Gantman2014", "id": 7669410, "avatar_url": "https://avatars.githubusercontent.com/u/7669410?v=3", "gravatar_id": "", "url": "https://api.github.com/users/Gantman2014", "html_url": "https://github.com/Gantman2014", "followers_url": "https://api.github.com/users/Gantman2014/followers", "following_url": "https://api.github.com/users/Gantman2014/following{/other_user}", "gists_url": "https://api.github.com/users/Gantman2014/gists{/gist_id}", "starred_url": "https://api.github.com/users/Gantman2014/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/Gantman2014/subscriptions", "organizations_url": "https://api.github.com/users/Gantman2014/orgs", "repos_url": "https://api.github.com/users/Gantman2014/repos", "events_url": "https://api.github.com/users/Gantman2014/events{/privacy}", "received_events_url": "https://api.github.com/users/Gantman2014/received_events", "type": "User", "site_admin": false, "score": 7.8813524 } ] } ================================================ FILE: boilerplate/App/Fixtures/rateLimit.json ================================================ { "resources": { "core": { "limit": 60, "remaining": 42, "reset": 1488126913 }, "search": { "limit": 10, "remaining": 9, "reset": 1488126003 } }, "rate": { "limit": 60, "remaining": 42, "reset": 1488126913 } } ================================================ FILE: boilerplate/App/Fixtures/root.json ================================================ { "current_user_url": "https://api.github.com/user", "current_user_authorizations_html_url": "https://github.com/settings/connections/applications{/client_id}", "authorizations_url": "https://api.github.com/authorizations", "code_search_url": "https://api.github.com/search/code?q={query}{&page,per_page,sort,order}", "commit_search_url": "https://api.github.com/search/commits?q={query}{&page,per_page,sort,order}", "emails_url": "https://api.github.com/user/emails", "emojis_url": "https://api.github.com/emojis", "events_url": "https://api.github.com/events", "feeds_url": "https://api.github.com/feeds", "followers_url": "https://api.github.com/user/followers", "following_url": "https://api.github.com/user/following{/target}", "gists_url": "https://api.github.com/gists{/gist_id}", "hub_url": "https://api.github.com/hub", "issue_search_url": "https://api.github.com/search/issues?q={query}{&page,per_page,sort,order}", "issues_url": "https://api.github.com/issues", "keys_url": "https://api.github.com/user/keys", "notifications_url": "https://api.github.com/notifications", "organization_repositories_url": "https://api.github.com/orgs/{org}/repos{?type,page,per_page,sort}", "organization_url": "https://api.github.com/orgs/{org}", "public_gists_url": "https://api.github.com/gists/public", "rate_limit_url": "https://api.github.com/rate_limit", "repository_url": "https://api.github.com/repos/{owner}/{repo}", "repository_search_url": "https://api.github.com/search/repositories?q={query}{&page,per_page,sort,order}", "current_user_repositories_url": "https://api.github.com/user/repos{?type,page,per_page,sort}", "starred_url": "https://api.github.com/user/starred{/owner}{/repo}", "starred_gists_url": "https://api.github.com/gists/starred", "team_url": "https://api.github.com/teams", "user_url": "https://api.github.com/users/{user}", "user_organizations_url": "https://api.github.com/user/orgs", "user_repositories_url": "https://api.github.com/users/{user}/repos{?type,page,per_page,sort}", "user_search_url": "https://api.github.com/search/users?q={query}{&page,per_page,sort,order}" } ================================================ FILE: boilerplate/App/Fixtures/skellock.json ================================================ { "total_count": 1, "incomplete_results": false, "items": [ { "login": "skellock", "id": 68273, "avatar_url": "https://avatars.githubusercontent.com/u/68273?v=3", "gravatar_id": "", "url": "https://api.github.com/users/skellock", "html_url": "https://github.com/skellock", "followers_url": "https://api.github.com/users/skellock/followers", "following_url": "https://api.github.com/users/skellock/following{/other_user}", "gists_url": "https://api.github.com/users/skellock/gists{/gist_id}", "starred_url": "https://api.github.com/users/skellock/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/skellock/subscriptions", "organizations_url": "https://api.github.com/users/skellock/orgs", "repos_url": "https://api.github.com/users/skellock/repos", "events_url": "https://api.github.com/users/skellock/events{/privacy}", "received_events_url": "https://api.github.com/users/skellock/received_events", "type": "User", "site_admin": false, "score": 107.22611 } ] } ================================================ FILE: boilerplate/App/Images/README.md ================================================ ### Images folder Holds all images for the applications. ================================================ FILE: boilerplate/App/Lib/README.md ================================================ # Lib At first glance, this could appear to be a "miscellaneous" folder, but we recommend that you treat this as proving ground for components that could be reusable outside your project. Maybe you're writing a set of utilities that you could use outside your project, but they're not quite ready or battle tested. This folder would be a great place to put them. They ideally be pure functions have no dependencies on other things in your App folder. ================================================ FILE: boilerplate/App/Lib/ReduxHelpers.ts ================================================ import { ActionCreator, AnyAction, Reducer, ReducersMapObject } from "redux"; import { createAction, EmptyAction, FluxStandardAction, getType, PayloadAction, TypeGetter } from "typesafe-actions"; export type FluxActionCreator FluxStandardAction = any> = AC & TypeGetter; type ActionCreatorMap = { 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. */ } }