├── .editorconfig ├── .gitattributes ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── generators ├── app │ ├── index.js │ └── templates │ │ ├── .babelrc │ │ ├── config │ │ ├── README.md │ │ ├── development.config.js │ │ ├── production.config.js │ │ └── test.config.js │ │ ├── env │ │ ├── features │ │ └── redux │ │ │ ├── app.js │ │ │ ├── containers │ │ │ ├── App │ │ │ │ └── App.js │ │ │ └── DevTools │ │ │ │ └── DevTools.js │ │ │ └── redux │ │ │ ├── configureStore.js │ │ │ ├── modules │ │ │ └── README.md │ │ │ └── rootReducer.js │ │ ├── gitignore │ │ ├── karma.conf.js │ │ ├── package.json │ │ ├── src │ │ ├── app.css │ │ ├── app.js │ │ ├── components │ │ │ └── Header │ │ │ │ ├── Header.js │ │ │ │ ├── Header.spec.js │ │ │ │ └── styles.module.css │ │ ├── containers │ │ │ └── App │ │ │ │ ├── App.js │ │ │ │ ├── App.spec.js │ │ │ │ └── styles.module.css │ │ ├── routes.js │ │ ├── styles │ │ │ ├── base.css │ │ │ ├── colors.css │ │ │ └── queries.css │ │ └── views │ │ │ └── main │ │ │ ├── Container.js │ │ │ ├── about │ │ │ └── About.js │ │ │ ├── indexPage │ │ │ ├── IndexPage.js │ │ │ └── styles.module.css │ │ │ ├── routes.js │ │ │ └── styles.module.css │ │ ├── tests.webpack.js │ │ └── webpack.config.js └── redux-module │ ├── index.js │ └── templates │ └── module-template.js ├── gulpfile.js ├── package.json └── test └── app.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - v6 4 | - v5 5 | - v4 6 | - '0.12' 7 | - '0.10' 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Ari Lerner (http://fullstackreact.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Generate a starting point for your react app 2 | 3 | The `react-gen` generator is a yeoman generator that creates an app using the same structure detailed in the the [blog post detailing cloning yelp with React](http://fullstackreact.com/articles/react-tutorial-cloning-yelp/). 4 | 5 | Using this generator gives you a sane react structure with the following technologies: 6 | 7 | * Webpack with hot module reloading (hjs-webpack) 8 | * PostCSS and CSS modules, including autoprefixer, precss 9 | * Global CSS loading 10 | * React / ReactDOM 11 | * react-router 12 | * Nested routing with multiple views 13 | * Testing with karma, mocha, chai 14 | * React component testing helper enzyme 15 | * Multiple deployment environments with dotenv configuration 16 | * Babel with react, stage-0, and es2015 17 | * font-awesome 18 | * and more 19 | 20 | ## Installation 21 | 22 | First, install [Yeoman](http://yeoman.io) and generator-react-gen using [npm](https://www.npmjs.com/) (we assume you have pre-installed [node.js](https://nodejs.org/)). 23 | 24 | ```bash 25 | npm install -g yo 26 | npm install -g generator-react-gen 27 | ``` 28 | 29 | Then generate your new project, answer some questions about your project, and go! 30 | 31 | ```bash 32 | yo react-gen 33 | ``` 34 | 35 | ## Workflow 36 | 37 | Open your app in a text editor and start to work. To run the application, use the `npm start` script. This will boot a server with hot module reloading: 38 | 39 | ```bash 40 | npm run start 41 | ``` 42 | 43 | To run the tests in our app, we can use the `npm run test` script. This sets up enzyme, boots the tests with karma, and executes them: 44 | 45 | ```bash 46 | npm run test 47 | ``` 48 | 49 | As we're writing tests, sometimes it's just easier to run the tests as we update and edit files. The generator makes this easy using the `npm run test:watch` script. Run this command and then any changes to the files in our project will cause the tests to be run: 50 | 51 | ```bash 52 | npm run test:watch 53 | ``` 54 | 55 | To build the app for distribution, we can use the `npm run build` command: 56 | 57 | ```bash 58 | npm run build 59 | ``` 60 | 61 | ## Features 62 | 63 | Wanna add redux to the mix? No problem, pass the feature flag of `--redux` when calling generate, i.e.: 64 | 65 | ```bash 66 | yo react-gen --redux 67 | ``` 68 | 69 | The `--redux` flag will also install the [redux-devtools](https://github.com/gaearon/redux-devtools), hidden by default. Pressing the combination of `Ctrl+h` will make them visible. 70 | 71 | ## Contributing 72 | 73 | ```shell 74 | git clone https://github.com/fullstackreact/redux-modules.git 75 | cd redux-modules 76 | npm install 77 | npm start 78 | ``` 79 | ___ 80 | 81 | # Fullstack React Book 82 | 83 | 84 | Fullstack React Book 85 | 86 | 87 | This generator was built alongside the blog post [React Tutorial: Cloning Yelp](https://www.fullstackreact.com/articles/react-tutorial-cloning-yelp/). 88 | 89 | This repo was written and is maintained by the [Fullstack React](https://fullstackreact.com) team. In the book, we cover many more projects like this. We walk through each line of code, explain why it's there and how it works. 90 | 91 |
92 | 93 | ## License 94 | [MIT](/LICENSE) 95 | -------------------------------------------------------------------------------- /generators/app/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var yeoman = require('yeoman-generator'); 3 | var chalk = require('chalk'); 4 | var yosay = require('yosay'); 5 | 6 | const tplCopy = function (name, to, props) { 7 | this.fs.copyTpl( 8 | this.templatePath(name), 9 | this.destinationPath(to || name), 10 | (props || this.props) 11 | ); 12 | }; 13 | 14 | const fileListCopy = function (files, dest) { 15 | const cpy = tplCopy.bind(this); 16 | files.forEach(filename => { 17 | cpy(filename, dest, this.props); 18 | }); 19 | }; 20 | 21 | const availableFeatures = [ 22 | 'redux' 23 | ]; 24 | 25 | module.exports = yeoman.Base.extend({ 26 | prompting: function () { 27 | // Have Yeoman greet the user. 28 | this.log(yosay( 29 | 'Welcome to the ' + chalk.red('generator-react-gen') + ' generator!' 30 | )); 31 | 32 | var prompts = [{ 33 | type: 'input', 34 | name: 'title', 35 | message: 'App name', 36 | default: this.appname 37 | }, { 38 | type: 'input', 39 | name: 'authorName', 40 | message: 'Author name', 41 | default: 'Ari Lerner' 42 | }, { 43 | type: 'input', 44 | name: 'authorEmail', 45 | message: 'Author email', 46 | default: 'ari@fullstack.io' 47 | }]; 48 | 49 | // feature options 50 | availableFeatures.forEach(feature => this.option(feature)); 51 | 52 | return this.prompt(prompts) 53 | .then(function (props) { 54 | // To access props later use this.props.someAnswer; 55 | this.props = props; 56 | 57 | this.features = []; 58 | availableFeatures.forEach(feature => { 59 | if (this.options[feature]) { 60 | this.features.push(feature); 61 | } 62 | }); 63 | 64 | }.bind(this)); 65 | }, 66 | 67 | writing: function () { 68 | // root files 69 | var copyFiles = fileListCopy.bind(this); 70 | var baseFiles = ['package.json', 71 | 'tests.webpack.js', 72 | 'webpack.config.js', 73 | 'karma.conf.js', 74 | '.babelrc', 75 | 'src/', 76 | 'config/' 77 | ]; 78 | var dependencies = [ 79 | 'react', 'react-dom', 'classnames', 'font-awesome', 80 | 'react-router', 'react-router-redux' 81 | ]; 82 | var devDependencies = [ 83 | 'autoprefixer', 84 | 'babel-core', 85 | 'babel-loader', 86 | 'babel-plugin-transform-es2015-modules-umd', 87 | 'babel-polyfill', 88 | 'babel-preset-es2015', 89 | 'babel-preset-react', 90 | 'babel-preset-react-hmre', 91 | 'babel-preset-stage-0', 92 | 'babel-register', 93 | 'chai', 94 | 'chai-enzyme', 95 | 'cross-env', 96 | 'css-loader', 97 | 'cssnano', 98 | 'dotenv', 99 | 'enzyme', 100 | 'expect', 101 | 'file-loader', 102 | 'hjs-webpack', 103 | 'jasmine-core', 104 | 'json-loader', 105 | 'karma', 106 | 'karma-chai', 107 | 'karma-jasmine', 108 | 'karma-mocha', 109 | 'karma-phantomjs-launcher', 110 | 'karma-sourcemap-loader', 111 | 'karma-spec-reporter', 112 | 'karma-webpack', 113 | 'mocha', 114 | 'phantomjs-polyfill', 115 | 'phantomjs-prebuilt', 116 | 'postcss-loader', 117 | 'precss', 118 | 'react-addons-test-utils', 119 | 'sinon', 120 | 'style-loader', 121 | 'url-loader', 122 | 'webpack', 123 | 'yargs' 124 | ]; 125 | 126 | var featureFiles = { 127 | redux: { 128 | 'src/': { 129 | dependencies: ['react-redux', 130 | 'redux-module-builder', 'redux-thunk'], 131 | devDependencies: ['redux-devtools', 132 | 'redux-devtools-log-monitor', 'redux-devtools-dock-monitor'], 133 | files: ['features/redux/'] 134 | } 135 | } 136 | }; 137 | 138 | copyFiles(baseFiles); 139 | const cp = tplCopy.bind(this); 140 | cp("env", ".env", this.props); 141 | cp("gitignore", ".gitignore", this.props); 142 | 143 | this.features.forEach(featureKey => { 144 | this.log(`======> Enabling ` + chalk.blue(featureKey)); 145 | const feat = featureFiles[featureKey]; 146 | Object.keys(feat) 147 | .forEach(key => { 148 | const dest = key; 149 | const feature = feat[key]; 150 | const files = feature.files; 151 | copyFiles(files, dest); 152 | 153 | if (feature.dependencies) { 154 | dependencies = dependencies.concat(feature.dependencies); 155 | } 156 | if (feature.devDependencies) { 157 | devDependencies = 158 | devDependencies.concat(feature.devDependencies); 159 | } 160 | }); 161 | }); 162 | 163 | this.npmInstall(dependencies, {save: true}); 164 | this.npmInstall(devDependencies, {saveDev: true}); 165 | }, 166 | 167 | install: function () { 168 | this.installDependencies(); 169 | } 170 | }); 171 | -------------------------------------------------------------------------------- /generators/app/templates/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0", "react"], 3 | "env": { 4 | "development": { 5 | "presets": ["react-hmre"] 6 | }, 7 | "production": { 8 | "presets": [] 9 | }, 10 | "test": { 11 | "presets": [] 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /generators/app/templates/config/README.md: -------------------------------------------------------------------------------- 1 | ## Configuration 2 | 3 | These configuration files will automatically be loaded depending upon the `NODE_ENV` variable. These take precedence over the `/.env` file. 4 | -------------------------------------------------------------------------------- /generators/app/templates/config/development.config.js: -------------------------------------------------------------------------------- 1 | APP_NAME=<%= title %>.dev 2 | -------------------------------------------------------------------------------- /generators/app/templates/config/production.config.js: -------------------------------------------------------------------------------- 1 | APP_NAME=<%= title %> 2 | -------------------------------------------------------------------------------- /generators/app/templates/config/test.config.js: -------------------------------------------------------------------------------- 1 | APP_NAME=<%= title %>.test 2 | -------------------------------------------------------------------------------- /generators/app/templates/env: -------------------------------------------------------------------------------- 1 | APP_NAME=<%= title %> 2 | ROOT_URL='https://api.<%= title %>.com' 3 | -------------------------------------------------------------------------------- /generators/app/templates/features/redux/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | 4 | import 'font-awesome/css/font-awesome.css' 5 | import './app.css' 6 | 7 | import App from 'containers/App/App' 8 | 9 | import {hashHistory} from 'react-router' 10 | import makeRoutes from './routes' 11 | 12 | const initialState = {} 13 | import {configureStore} from './redux/configureStore' 14 | const {store, actions, history} = configureStore({initialState, historyType: hashHistory}); 15 | 16 | let render = (routerKey = null) => { 17 | const makeRoutes = require('./routes').default; 18 | const routes = makeRoutes(store) 19 | 20 | const mountNode = document.querySelector('#root'); 21 | ReactDOM.render( 22 | , mountNode); 27 | } 28 | 29 | if (__DEBUG__ && module.hot) { 30 | const renderApp = render; 31 | render = () => renderApp(Math.random()) 32 | 33 | module.hot.accept('./routes', () => render()); 34 | } 35 | 36 | render(); 37 | -------------------------------------------------------------------------------- /generators/app/templates/features/redux/containers/App/App.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes as T } from 'react'; 2 | import { Router } from 'react-router'; 3 | import { Provider } from 'react-redux'; 4 | 5 | class App extends React.Component { 6 | static contextTypes = { 7 | router: T.object 8 | } 9 | 10 | static propTypes = { 11 | history: T.object.isRequired, 12 | routes: T.element.isRequired, 13 | routerKey: T.number, 14 | actions: T.object 15 | }; 16 | 17 | get content() { 18 | const { history, routes, routerKey, store, actions } = this.props; 19 | let newProps = { 20 | actions, 21 | ...this.props 22 | } 23 | 24 | const createElement = (Component, props) => { 25 | return 26 | } 27 | 28 | return ( 29 | 30 | 35 | 36 | ) 37 | } 38 | 39 | get devTools () { 40 | if (__DEBUG__) { 41 | if (!window.devToolsExtension) { 42 | const DevTools = require('containers/DevTools/DevTools').default 43 | return 44 | } 45 | } 46 | } 47 | 48 | render () { 49 | return ( 50 | 51 |
52 | {this.content} 53 | {this.devTools} 54 |
55 |
56 | ) 57 | } 58 | } 59 | 60 | export default App; 61 | -------------------------------------------------------------------------------- /generators/app/templates/features/redux/containers/DevTools/DevTools.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createDevTools } from 'redux-devtools' 3 | import LogMonitor from 'redux-devtools-log-monitor' 4 | import DockMonitor from 'redux-devtools-dock-monitor' 5 | 6 | export default createDevTools( 7 | 11 | 12 | 13 | ) 14 | -------------------------------------------------------------------------------- /generators/app/templates/features/redux/redux/configureStore.js: -------------------------------------------------------------------------------- 1 | import { browserHistory } from 'react-router'; 2 | import { bindActionCreatorsToStore } from 'redux-module-builder'; 3 | import { createApiMiddleware } from 'redux-module-builder/api'; 4 | import { routerMiddleware, syncHistoryWithStore } from 'react-router-redux'; 5 | import thunkMiddleware from 'redux-thunk'; 6 | import { createStore, compose, applyMiddleware } from 'redux'; 7 | import { rootReducer, actions, initialState } from './rootReducer'; 8 | 9 | export const configureStore = ({ 10 | historyType = browserHistory, 11 | userInitialState = {}}) => { 12 | 13 | let middleware = [ 14 | createApiMiddleware({ 15 | baseUrl: __ROOT_URL__, 16 | headers: { 17 | 'X-Requested-By': '<%= title %> client' 18 | } 19 | }), 20 | thunkMiddleware, 21 | routerMiddleware(historyType) 22 | ] 23 | 24 | let tools = []; 25 | if (__DEBUG__) { 26 | const DevTools = require('containers/DevTools/DevTools').default; 27 | let devTools = window.devToolsExtension ? window.devToolsExtension : DevTools.instrument; 28 | if (typeof devTools === 'function') { 29 | tools.push(devTools()) 30 | } 31 | } 32 | 33 | let finalCreateStore; 34 | finalCreateStore = compose( 35 | applyMiddleware(...middleware), 36 | ...tools 37 | )(createStore); 38 | 39 | const store = finalCreateStore( 40 | rootReducer, 41 | Object.assign({}, initialState, userInitialState) 42 | ); 43 | 44 | const history = syncHistoryWithStore(historyType, store, { 45 | adjustUrlOnReplay: true 46 | }) 47 | 48 | if (module.hot) { 49 | module.hot.accept('./rootReducer', () => { 50 | const {rootReducer} = require('./rootReducer'); 51 | store.replaceReducer(rootReducer); 52 | }); 53 | } 54 | 55 | const boundActions = bindActionCreatorsToStore(actions, store); 56 | return {store, actions: boundActions, history} 57 | } 58 | -------------------------------------------------------------------------------- /generators/app/templates/features/redux/redux/modules/README.md: -------------------------------------------------------------------------------- 1 | ## Store your redux modules here 2 | -------------------------------------------------------------------------------- /generators/app/templates/features/redux/redux/rootReducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { routerReducer as routing, push } from 'react-router-redux'; 3 | 4 | // Require your modules here 5 | const modules = { 6 | } 7 | export let actions = { 8 | routing: { 9 | navigateTo: path => dispatch => dispatch(push(path)) 10 | } 11 | } 12 | 13 | export let initialState = {} 14 | export let reducers = {routing}; 15 | 16 | Object.keys(modules).forEach(key => { 17 | const module = modules[key]; 18 | initialState[key] = module.initialState || {}; 19 | actions[key] = module.actions; 20 | reducers[key] = module.reducer; 21 | }); 22 | 23 | export const rootReducer = combineReducers(reducers); 24 | -------------------------------------------------------------------------------- /generators/app/templates/gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | config/ 3 | npm-debug.log 4 | .env 5 | .awsDetails 6 | dist/ 7 | -------------------------------------------------------------------------------- /generators/app/templates/karma.conf.js: -------------------------------------------------------------------------------- 1 | var argv = require('yargs').argv; 2 | 3 | var webpackConfig = require('./webpack.config'); 4 | 5 | module.exports = function (config) { 6 | config.set({ 7 | basePath: '', 8 | frameworks: ['mocha', 'chai'], 9 | files: [ 10 | 'tests.webpack.js' 11 | ], 12 | 13 | preprocessors: { 14 | // add webpack as preprocessor 15 | 'tests.webpack.js': ['webpack', 'sourcemap'] 16 | }, 17 | 18 | webpack: webpackConfig, 19 | webpackServer: { 20 | noInfo: true 21 | }, 22 | 23 | plugins: [ 24 | 'karma-mocha', 25 | 'karma-chai', 26 | 'karma-webpack', 27 | 'karma-phantomjs-launcher', 28 | 'karma-spec-reporter', 29 | 'karma-sourcemap-loader' 30 | ], 31 | 32 | reporters: ['spec'], 33 | port: 9876, 34 | colors: true, 35 | logLevel: config.LOG_INFO, 36 | browsers: ['PhantomJS'], 37 | singleRun: !argv.watch 38 | }); 39 | }; 40 | -------------------------------------------------------------------------------- /generators/app/templates/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "<%= title %>", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "start": "./node_modules/.bin/cross-env NODE_ENV=development ./node_modules/.bin/hjs-dev-server", 7 | "clean": "rimraf dist", 8 | "build": "npm run clean && ./node_modules/.bin/cross-env NODE_ENV=production webpack", 9 | "publish_pages": "./node_modules/.bin/gh-pages -d dist", 10 | "ghpages": "npm run build && npm run publish_pages", 11 | "test": "./node_modules/.bin/cross-env NODE_ENV=test ./node_modules/karma/bin/karma start karma.conf.js", 12 | "test:watch": "npm run test -- --watch" 13 | }, 14 | "author": "<%= authorName %> <<%= authorEmail %>>", 15 | "license": "ISC" 16 | } 17 | -------------------------------------------------------------------------------- /generators/app/templates/src/app.css: -------------------------------------------------------------------------------- 1 | @import url("styles/colors.css"); 2 | 3 | *, 4 | *:after, 5 | *:before { 6 | box-sizing: border-box; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | font-smoothing: antialiased; 10 | text-rendering: optimizeLegibility; 11 | 12 | font-size: 16px; 13 | } 14 | 15 | body { 16 | color: var(--dark); 17 | font-weight: lighter; 18 | font: 400 15px/22px 'Open Sans', 'Helvetica Neue', Sans-serif; 19 | font-smoothing: antialiased; 20 | padding: 0; 21 | margin: 0; 22 | } 23 | -------------------------------------------------------------------------------- /generators/app/templates/src/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | 4 | import 'font-awesome/css/font-awesome.css' 5 | import './app.css' 6 | 7 | import App from 'containers/App/App' 8 | 9 | import {hashHistory} from 'react-router' 10 | import makeRoutes from './routes' 11 | 12 | const routes = makeRoutes() 13 | 14 | const mountNode = document.querySelector('#root'); 15 | ReactDOM.render( 16 | , 18 | mountNode); 19 | -------------------------------------------------------------------------------- /generators/app/templates/src/components/Header/Header.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes as T } from 'react' 2 | import {Link} from 'react-router' 3 | 4 | import styles from './styles.module.css'; 5 | 6 | export class Header extends React.Component { 7 | render() { 8 | const {title} = this.props; 9 | 10 | return ( 11 |
12 |

{title}

13 |
14 | Fullstack.io 15 |
16 |
17 | ) 18 | } 19 | } 20 | 21 | Header.propTypes = { 22 | title: T.string 23 | } 24 | 25 | Header.defaultProps = { 26 | title: '<%= title %>' 27 | } 28 | 29 | export default Header 30 | -------------------------------------------------------------------------------- /generators/app/templates/src/components/Header/Header.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { expect } from 'chai' 3 | import { shallow } from 'enzyme' 4 | 5 | import Header from './Header' 6 | import styles from './styles.module.css' 7 | 8 | describe('
', () => { 9 | let wrapper; 10 | beforeEach(() => { 11 | wrapper = shallow(
) 12 | }); 13 | 14 | it('contains a title component with yelp', () => { 15 | expect(wrapper.find('h1').first().text()) 16 | .to.equal('<%= title %>') 17 | }); 18 | 19 | it('contains topbar styling', () => { 20 | expect(wrapper.find(`.${styles.topbar}`)) 21 | .to.have.length(1); 22 | }) 23 | 24 | it('contains a section menu with the title', () => { 25 | expect(wrapper.find('section').first().text()) 26 | .to.equal('Fullstack.io') 27 | }); 28 | 29 | }) 30 | -------------------------------------------------------------------------------- /generators/app/templates/src/components/Header/styles.module.css: -------------------------------------------------------------------------------- 1 | @import url("../../styles/base.css"); 2 | 3 | .topbar { 4 | position: fixed; 5 | z-index: 10; 6 | top: 0; 7 | left: 0; 8 | 9 | background: #48b5e9; 10 | width: 100%; 11 | padding: 0 25px; 12 | 13 | height: var(--topbar-height); 14 | line-height: var(--topbar-height); 15 | color: #fff; 16 | 17 | a { 18 | color: #fff; 19 | text-transform: uppercase; 20 | text-decoration: none; 21 | letter-spacing: 1px; 22 | 23 | line-height: 40px; 24 | h1 { 25 | font-size: 28px; 26 | } 27 | } 28 | section { 29 | position: absolute; 30 | top: 0px; 31 | right: 25px; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /generators/app/templates/src/containers/App/App.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import { Router } from 'react-router'; 3 | 4 | class App extends React.Component { 5 | static contextTypes = { 6 | router: PropTypes.object 7 | } 8 | 9 | static propTypes = { 10 | history: PropTypes.object.isRequired, 11 | routes: PropTypes.element.isRequired 12 | }; 13 | 14 | get content() { 15 | return ( 16 | 19 | ) 20 | } 21 | 22 | render () { 23 | return ( 24 |
25 | {this.content} 26 |
27 | ) 28 | } 29 | } 30 | 31 | export default App; 32 | -------------------------------------------------------------------------------- /generators/app/templates/src/containers/App/App.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { expect } from 'chai' 3 | import { shallow } from 'enzyme' 4 | 5 | import App from './App' 6 | import styles from './styles.module.css' 7 | 8 | describe('', () => { 9 | let wrapper; 10 | let history = {}; 11 | beforeEach(() => { 12 | wrapper = 13 | shallow() 14 | }) 15 | 16 | it('has a Router component', () => { 17 | expect(wrapper.find('Router')) 18 | .to.have.length(1); 19 | }); 20 | 21 | it('passes a history prop', () => { 22 | const props = wrapper.find('Router').props(); 23 | 24 | expect(props.history) 25 | .to.be.defined; 26 | }) 27 | 28 | }); 29 | -------------------------------------------------------------------------------- /generators/app/templates/src/containers/App/styles.module.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | display: flex; 3 | } 4 | -------------------------------------------------------------------------------- /generators/app/templates/src/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {Route} from 'react-router' 3 | 4 | import makeMainRoutes from 'views/main/routes'; 5 | 6 | export const makeRoutes = (store) => { 7 | const mainRoutes = makeMainRoutes(store); 8 | 9 | return ( 10 | 11 | {mainRoutes} 12 | 13 | ) 14 | } 15 | 16 | export default makeRoutes 17 | -------------------------------------------------------------------------------- /generators/app/templates/src/styles/base.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --topbar-height: 80px; 3 | --padding: 25px; 4 | } 5 | -------------------------------------------------------------------------------- /generators/app/templates/src/styles/colors.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --dark: #404040; 3 | --light-gray: #a2a2a2; 4 | --white: #ffffff; 5 | --highlight: #48b5e9; 6 | --heading-color: var(--highlight); 7 | } 8 | -------------------------------------------------------------------------------- /generators/app/templates/src/styles/queries.css: -------------------------------------------------------------------------------- 1 | @custom-media --screen-phone (width <= 35.5em); 2 | @custom-media --screen-phone-lg (width > 35.5em); 3 | 4 | @custom-media --screen-sm var(--screen-phone) and (width < 48em); 5 | @custom-media --screen-md (width >= 48em) and (width < 64em); 6 | @custom-media --screen-lg (width >= 64em) and (width < 80em); 7 | @custom-media --screen-xl (width >= 80em); 8 | -------------------------------------------------------------------------------- /generators/app/templates/src/views/main/Container.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes as T } from 'react' 2 | 3 | import Header from 'components/Header/Header' 4 | import styles from './styles.module.css' 5 | 6 | export class Container extends React.Component { 7 | renderChildren() { 8 | const childProps = { 9 | ...this.props 10 | }; 11 | const {children} = this.props; 12 | return React.Children.map(children, 13 | c => React.cloneElement(c, childProps)); 14 | } 15 | render() { 16 | return ( 17 |
18 |
19 |
20 | {this.renderChildren()} 21 |
22 |
23 | ) 24 | } 25 | } 26 | 27 | Container.contextTypes = { 28 | router: T.object 29 | } 30 | 31 | export default Container 32 | -------------------------------------------------------------------------------- /generators/app/templates/src/views/main/about/About.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const About = (props) => ( 4 |
5 | The about page 6 |
7 | ) 8 | 9 | export default About; 10 | -------------------------------------------------------------------------------- /generators/app/templates/src/views/main/indexPage/IndexPage.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes as T } from 'react'; 2 | import {Link} from 'react-router'; 3 | 4 | import styles from './styles.module.css'; 5 | 6 | export class IndexPage extends React.Component { 7 | render() { 8 | return ( 9 |
10 |

Hello from generated content

11 |

12 | For more information about the `react-gen` package and set up, check out the Blog post on building a yelp clone with React. 13 |

14 |

15 | About 16 |

17 |
18 | ) 19 | } 20 | } 21 | 22 | export default IndexPage; 23 | -------------------------------------------------------------------------------- /generators/app/templates/src/views/main/indexPage/styles.module.css: -------------------------------------------------------------------------------- 1 | @import url("../../../styles/base.css"); 2 | 3 | .content { 4 | padding: var(--padding); 5 | } 6 | -------------------------------------------------------------------------------- /generators/app/templates/src/views/main/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {Route, IndexRoute} from 'react-router' 3 | 4 | import Container from './Container' 5 | import IndexPage from './indexPage/IndexPage' 6 | 7 | export const makeMainRoutes = () => { 8 | return ( 9 | 10 | {/* Lazy-loading */} 11 | { 12 | require.ensure([], (require) => { 13 | const mod = require('./about/About'); 14 | cb(null, mod.default); 15 | }); 16 | }} /> 17 | {/* inline loading */} 18 | 19 | 20 | ) 21 | } 22 | 23 | export default makeMainRoutes 24 | -------------------------------------------------------------------------------- /generators/app/templates/src/views/main/styles.module.css: -------------------------------------------------------------------------------- 1 | @import url("../../styles/base.css"); 2 | @import url("../../styles/queries.css"); 3 | 4 | .wrapper { 5 | overflow-y: scroll; 6 | display: flex; 7 | margin: 0; 8 | 9 | height: 100vh; 10 | -webkit-box-orient: horizontal; 11 | -o-box-orient: horizontal; 12 | 13 | flex-direction: column; 14 | 15 | @media (--screen-phone-lg) { 16 | flex-direction: row; 17 | } 18 | 19 | } 20 | 21 | .content { 22 | position: relative; 23 | top: var(--topbar-height); 24 | left: 0; 25 | 26 | flex: 1; 27 | order: 1; 28 | 29 | @media (--screen-phone-lg) { 30 | flex: 2; 31 | order: 2; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /generators/app/templates/tests.webpack.js: -------------------------------------------------------------------------------- 1 | require('babel-polyfill'); 2 | // some setup first 3 | 4 | var chai = require('chai'); 5 | var chaiEnzyme = require('chai-enzyme'); 6 | 7 | chai.use(chaiEnzyme()); 8 | 9 | var context = require.context('./src', true, /\.spec\.js$/); 10 | context.keys().forEach(context); 11 | -------------------------------------------------------------------------------- /generators/app/templates/webpack.config.js: -------------------------------------------------------------------------------- 1 | const NODE_ENV = process.env.NODE_ENV || 'development'; 2 | const dotenv = require('dotenv'); 3 | 4 | const webpack = require('webpack'); 5 | const path = require('path'); 6 | 7 | const join = path.join; 8 | const resolve = path.resolve; 9 | 10 | const getConfig = require('hjs-webpack'); 11 | 12 | const isDev = NODE_ENV === 'development'; 13 | const isTest = NODE_ENV === 'test'; 14 | 15 | // devServer config 16 | const devHost = process.env.HOST || 'localhost'; 17 | const devPort = process.env.PORT || 3000; 18 | 19 | const setPublicPath = process.env.SET_PUBLIC_PATH !== 'false'; 20 | const publicPath = (isDev && setPublicPath) ? `//${devHost}:${devPort}/` : ''; 21 | 22 | const root = resolve(__dirname); 23 | const src = join(root, 'src'); 24 | const modules = join(root, 'node_modules'); 25 | const dest = join(root, 'dist'); 26 | const css = join(src, 'styles'); 27 | 28 | var config = getConfig({ 29 | isDev: isDev || isTest, 30 | in: join(src, 'app.js'), 31 | out: dest, 32 | html: function (context) { 33 | return { 34 | 'index.html': context.defaultTemplate({ 35 | title: '<%= title %>', 36 | publicPath, 37 | meta: {} 38 | }) 39 | }; 40 | } 41 | }); 42 | 43 | // ENV variables 44 | const dotEnvVars = dotenv.config(); 45 | const environmentEnv = dotenv.config({ 46 | path: join(root, 'config', `${NODE_ENV}.config.js`), 47 | silent: true 48 | }); 49 | const envVariables = 50 | Object.assign({}, dotEnvVars, environmentEnv); 51 | 52 | const defines = 53 | Object.keys(envVariables) 54 | .reduce((memo, key) => { 55 | const val = JSON.stringify(envVariables[key]); 56 | memo[`__${key.toUpperCase()}__`] = val; 57 | return memo; 58 | }, { 59 | __NODE_ENV__: JSON.stringify(NODE_ENV), 60 | __DEBUG__: isDev 61 | }); 62 | 63 | config.plugins = [ 64 | new webpack.DefinePlugin(defines) 65 | ].concat(config.plugins); 66 | // END ENV variables 67 | 68 | // CSS modules 69 | const cssModulesNames = `${isDev ? '[path][name]__[local]__' : ''}[hash:base64:5]`; 70 | 71 | const matchCssLoaders = /(^|!)(css-loader)($|!)/; 72 | 73 | const findLoader = (loaders, match) => { 74 | const found = loaders.filter(l => l && l.loader && l.loader.match(match)); 75 | return found ? found[0] : null; 76 | }; 77 | 78 | // existing css loader 79 | const cssloader = 80 | findLoader(config.module.loaders, matchCssLoaders); 81 | 82 | const newloader = Object.assign({}, cssloader, { 83 | test: /\.module\.css$/, 84 | include: [src], 85 | loader: cssloader.loader.replace(matchCssLoaders, `$1$2?modules&localIdentName=${cssModulesNames}$3`) 86 | }); 87 | config.module.loaders.push(newloader); 88 | cssloader.test = new RegExp(`[^module]${cssloader.test.source}`); 89 | cssloader.loader = newloader.loader; 90 | 91 | config.module.loaders.push({ 92 | test: /\.css$/, 93 | include: [modules], 94 | loader: 'style!css' 95 | }); 96 | 97 | // CSS modules 98 | 99 | // postcss 100 | config.postcss = [].concat([ 101 | require('precss')({}), 102 | require('autoprefixer')({}), 103 | require('cssnano')({}) 104 | ]); 105 | 106 | // END postcss 107 | 108 | // Roots 109 | config.resolve.root = [src, modules]; 110 | config.resolve.alias = { 111 | css: join(src, 'styles'), 112 | containers: join(src, 'containers'), 113 | components: join(src, 'components'), 114 | utils: join(src, 'utils'), 115 | styles: join(src, 'styles') 116 | }; 117 | // end Roots 118 | 119 | // Dev 120 | if (isDev) { 121 | config.devServer.port = devPort; 122 | config.devServer.hostname = devHost; 123 | } 124 | 125 | // Testing 126 | if (isTest) { 127 | config.externals = { 128 | 'react/addons': true, 129 | 'react/lib/ReactContext': true, 130 | 'react/lib/ExecutionEnvironment': true 131 | }; 132 | 133 | config.module.noParse = /[/\\]sinon\.js/; 134 | config.resolve.alias.sinon = 'sinon/pkg/sinon'; 135 | 136 | config.plugins = config.plugins.filter(p => { 137 | const name = p.constructor.toString(); 138 | const fnName = name.match(/^function (.*)\((.*\))/); 139 | 140 | const idx = [ 141 | 'DedupePlugin', 142 | 'UglifyJsPlugin' 143 | ].indexOf(fnName[1]); 144 | return idx < 0; 145 | }); 146 | } 147 | // End Testing 148 | 149 | module.exports = config; 150 | -------------------------------------------------------------------------------- /generators/redux-module/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var yeoman = require('yeoman-generator'); 3 | var chalk = require('chalk'); 4 | var yosay = require('yosay'); 5 | 6 | var fs = require('fs'); 7 | var path = require('path'); 8 | var process = require('process') 9 | var esprima = require('esprima'); 10 | var escodegen = require('escodegen'); 11 | var estraverse = require('estraverse'); 12 | 13 | const tplCopy = function (name, to, props) { 14 | this.fs.copyTpl( 15 | this.templatePath(name), 16 | this.destinationPath(to || name), 17 | (props || this.props) 18 | ); 19 | }; 20 | 21 | const fileListCopy = function (files, dest) { 22 | const cpy = tplCopy.bind(this); 23 | files.forEach(filename => { 24 | cpy(filename, dest, this.props); 25 | }); 26 | }; 27 | 28 | const updateRootReducer = function(props) { 29 | const moduleName = props.moduleName; 30 | const rootReducerPath = path.join(process.cwd(), 'src/redux/rootReducer.js'); 31 | const done = this.async(); 32 | 33 | // don't do any traversing yet 34 | this.log(`Make sure you update your rootReducer with the property: 35 | 36 | ${chalk.blue(`${props.moduleName}: require('./modules/${props.moduleName}')`)} 37 | 38 | const modules = { 39 | ${props.moduleName}: require('./modules/${props.moduleName}') 40 | } 41 | `); 42 | 43 | return done(); 44 | 45 | 46 | try { 47 | fs.stat(rootReducerPath, (err, stats) => { 48 | if (err) return done(err); 49 | 50 | let needsUpdate = false; 51 | if (stats.isFile()) { 52 | const data = fs.readFileSync(rootReducerPath, 'utf8'); 53 | const AST = esprima.parse(data, { 54 | comment: true, 55 | sourceType: 'module' 56 | }); 57 | estraverse.traverse(AST, { 58 | enter: (node, parent) => { 59 | if (node.type === 'VariableDeclarator' && 60 | node.id.type === 'Identifier' && 61 | node.id.name === 'modules') { 62 | 63 | const newKV = { 64 | 'type': 'Property', 65 | 'key': { 66 | 'type': 'Identifier', 67 | 'name': this.props.moduleName 68 | }, 69 | 'computed': false, 70 | 'value': { 71 | 'type': 'Identifier', 72 | 'name': `require('./modules/${this.props.moduleName}')` 73 | }, 74 | 'kind': 'init', 75 | 'method': false, 76 | 'shorthand': false 77 | } 78 | 79 | const alreadyExists = false; 80 | node.init.properties.forEach(prop => { 81 | if (prop.type === 'Property' && 82 | prop.key.type === 'Identifier' && 83 | prop.key.name === this.props.moduleName) { 84 | // Update this thing 85 | } 86 | }) 87 | // We found the const modules = {}; 88 | // let props = node.init.properties; 89 | // props.push(newKV); 90 | // needsUpdate = true; 91 | // return node; 92 | } 93 | } 94 | }); 95 | if (needsUpdate) { 96 | const finalCode = escodegen.generate(AST, { 97 | comment: true, 98 | format: { 99 | indent: { 100 | style: ' ' 101 | } 102 | }, 103 | comment: true 104 | }); 105 | fs.writeFile(rootReducerPath, finalCode, done); 106 | } else { 107 | done(); 108 | } 109 | } 110 | }); 111 | } catch (e) { 112 | this.log(`${chalk.red('Cannot update rootReducer')} 113 | 114 | Make sure you import this module into your root reducer.`); 115 | done('Cannot update rootReducer'); 116 | } 117 | } 118 | 119 | module.exports = yeoman.Base.extend({ 120 | prompting: function () { 121 | this.log('Create a redux module'); 122 | const done = this.async(); 123 | 124 | var prompts = [{ 125 | type: 'input', 126 | name: 'moduleName', 127 | message: 'What do you want your module to be called?', 128 | default: this.appname 129 | }]; 130 | 131 | return this.prompt(prompts) 132 | .then(function (props) { 133 | this.props = props; 134 | 135 | done(); 136 | }.bind(this)); 137 | }, 138 | 139 | writing: function () { 140 | // root files 141 | var copyFiles = fileListCopy.bind(this); 142 | const cp = tplCopy.bind(this); 143 | 144 | const modPath = `src/redux/modules/${this.props.moduleName}.js` 145 | cp('module-template.js', modPath, this.props); 146 | var dependencies = [ 147 | 'redux-module-builder' 148 | ]; 149 | 150 | updateRootReducer.call(this, this.props); 151 | this.npmInstall(dependencies, {save: true}); 152 | }, 153 | 154 | 155 | // Currently not used 156 | 157 | install: function () { 158 | this.installDependencies(); 159 | } 160 | }); 161 | -------------------------------------------------------------------------------- /generators/redux-module/templates/module-template.js: -------------------------------------------------------------------------------- 1 | import {createConstants, createReducer} from 'redux-module-builder' 2 | import {createApiHandler, createApiAction} from 'redux-module-builder/api' 3 | 4 | /* 5 | * Constants 6 | * 7 | * Basic constants can just be a name 8 | * i.e. 'GET_ME' 9 | * and api constants can be an Object 10 | * i.e. { 'GET_UPCOMING': { api: true }} 11 | */ 12 | export const types = createConstants('<%= moduleName %>')( 13 | ); 14 | 15 | /* 16 | * actions 17 | * 18 | * The actions object can be a simple function 19 | * i.e. getMe: () => (dispatch, getState) => { 20 | * dispatch({type: types.GET_ME}) 21 | * } 22 | * or using the `createApiAction()` function, we can create an 23 | * action that calls out to the api helpers of redux-module-builder 24 | * For instance: 25 | * getUpcomingEvents: createApiAction(types.GET_UPCOMING)((client, opts) => { 26 | * const {count} = opts; 27 | * return client.get({ 28 | * path: '/events/upcoming', 29 | * params: {count} 30 | * }).then(res => res.events) 31 | * }) 32 | */ 33 | export const actions = { 34 | 35 | } 36 | 37 | /* 38 | * reducers 39 | * 40 | * The reducers go here where we can either create a simple reducer 41 | * using our types object from above 42 | * i.e. [types.GET_ME]: (state, action) => ({ 43 | * ...state, 44 | * me: action.payload 45 | * }) 46 | * or it can be a a complex object, such as using the `createApiHandler` 47 | * method provided by `redux-module-builder` 48 | * 49 | * ...createApiHandler(types.GET_UPCOMING, (apiTypes) => { 50 | * // optional argument to handle apiTypes (i.e. loading and error) 51 | * return { 52 | * [apiTypes.LOADING]: (state) => ({...state, loading: true}), 53 | * [apiTypes.ERROR]: (state, {payload}) => { 54 | * return {...state, loading: false, errors: payload}; 55 | * } 56 | * })((state, {payload}) => { 57 | * return { 58 | * ...state, 59 | * loading: false, 60 | * errors: null, 61 | * events: payload 62 | * } 63 | * }) 64 | * 65 | */ 66 | export const reducer = createReducer({ 67 | 68 | }); 69 | 70 | /* 71 | * The initial state for this part of the component tree 72 | */ 73 | export const initialState = { 74 | loading: false, 75 | errors: null, 76 | }; 77 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var path = require('path'); 3 | var gulp = require('gulp'); 4 | var eslint = require('gulp-eslint'); 5 | var excludeGitignore = require('gulp-exclude-gitignore'); 6 | var mocha = require('gulp-mocha'); 7 | var istanbul = require('gulp-istanbul'); 8 | var nsp = require('gulp-nsp'); 9 | var plumber = require('gulp-plumber'); 10 | 11 | gulp.task('static', function () { 12 | return gulp.src('**/*.js') 13 | .pipe(excludeGitignore()) 14 | .pipe(eslint()) 15 | .pipe(eslint.format()) 16 | .pipe(eslint.failAfterError()); 17 | }); 18 | 19 | gulp.task('nsp', function (cb) { 20 | nsp({package: path.resolve('package.json')}, cb); 21 | }); 22 | 23 | gulp.task('pre-test', function () { 24 | return gulp.src('generators/**/*.js') 25 | .pipe(excludeGitignore()) 26 | .pipe(istanbul({ 27 | includeUntested: true 28 | })) 29 | .pipe(istanbul.hookRequire()); 30 | }); 31 | 32 | gulp.task('test', ['pre-test'], function (cb) { 33 | var mochaErr; 34 | 35 | gulp.src('test/**/*.js') 36 | .pipe(plumber()) 37 | .pipe(mocha({reporter: 'spec'})) 38 | .on('error', function (err) { 39 | mochaErr = err; 40 | }) 41 | .pipe(istanbul.writeReports()) 42 | .on('end', function () { 43 | cb(mochaErr); 44 | }); 45 | }); 46 | 47 | gulp.task('watch', function () { 48 | gulp.watch(['generators/**/*.js', 'test/**'], ['test']); 49 | }); 50 | 51 | gulp.task('prepublish', ['nsp']); 52 | gulp.task('default', ['static', 'test']); 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "generator-react-gen", 3 | "version": "1.1.0", 4 | "description": "Generate a react project in the fullstack way", 5 | "homepage": "", 6 | "author": { 7 | "name": "Ari Lerner", 8 | "email": "ari@fullstack.io", 9 | "url": "http://fullstackreact.com" 10 | }, 11 | "files": [ 12 | "generators/app", 13 | "generators/redux-module" 14 | ], 15 | "main": "generators/index.js", 16 | "keywords": [ 17 | "fullstackio", 18 | "react", 19 | "redux", 20 | "react-router", 21 | "hjs-webpack", 22 | "yeoman-generator" 23 | ], 24 | "dependencies": { 25 | "chalk": "^1.1.3", 26 | "escodegen": "^1.8.0", 27 | "esprima": "^2.7.2", 28 | "estraverse": "^4.2.0", 29 | "react": "^15.1.0", 30 | "react-dom": "^15.1.0", 31 | "react-router-redux": "^4.0.5", 32 | "redux": "^3.5.2", 33 | "redux-module-builder": "0.0.7", 34 | "redux-thunk": "^2.1.0", 35 | "yeoman-generator": "^0.23.3", 36 | "yosay": "^1.1.1" 37 | }, 38 | "devDependencies": { 39 | "cheerio": "^0.20.0", 40 | "cross-env": "^1.0.8", 41 | "eslint": "^2.12.0", 42 | "eslint-config-xo-space": "^0.13.0", 43 | "gulp": "^3.9.1", 44 | "gulp-eslint": "^2.0.0", 45 | "gulp-exclude-gitignore": "^1.0.0", 46 | "gulp-istanbul": "^1.0.0", 47 | "gulp-line-ending-corrector": "^1.0.1", 48 | "gulp-mocha": "^2.2.0", 49 | "gulp-nsp": "^2.4.1", 50 | "gulp-plumber": "^1.1.0", 51 | "yargs": "^4.7.1", 52 | "yeoman-assert": "^2.2.1", 53 | "yeoman-test": "^1.4.0" 54 | }, 55 | "eslintConfig": { 56 | "extends": "xo-space", 57 | "env": { 58 | "mocha": true 59 | } 60 | }, 61 | "repository": { 62 | "type": "git", 63 | "url": "https://github.com/fullstackreact/react-gen-generator" 64 | }, 65 | "scripts": { 66 | "prepublish": "gulp prepublish", 67 | "test": "gulp" 68 | }, 69 | "license": "MIT" 70 | } 71 | -------------------------------------------------------------------------------- /test/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var path = require('path'); 3 | var assert = require('yeoman-assert'); 4 | var helpers = require('yeoman-test'); 5 | 6 | describe('generator-react-gen:app', function () { 7 | before(function () { 8 | return helpers.run(path.join(__dirname, '../generators/app')) 9 | .withPrompts({someAnswer: true}) 10 | .toPromise(); 11 | }); 12 | 13 | it('creates files', function () { 14 | assert.file([ 15 | 'dummyfile.txt' 16 | ]); 17 | }); 18 | }); 19 | --------------------------------------------------------------------------------