├── .gitignore ├── template ├── assets │ └── .gitkeep ├── src │ ├── components │ │ └── .gitkeep │ ├── views │ │ └── template.ejs │ ├── utils │ │ └── styling-mixin.jsx │ ├── app.jsx │ └── server.js ├── .gitignore └── package.json ├── package.json ├── LICENSE ├── README.md ├── bin └── react └── templates └── component.ejs /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /template/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /template/src/components/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /template/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | # Runtime data 5 | pids 6 | *.pid 7 | *.seed 8 | # Dependency directory 9 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 10 | node_modules 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reactjs-cli", 3 | "version": "0.0.2", 4 | "description": "A command line interface to generate a new react app and/or components.", 5 | "author": "Made by Form", 6 | "licenses": [ 7 | { 8 | "type": "MIT", 9 | "url": "https://github.com/madebyform/react-cli/blob/master/LICENSE" 10 | } 11 | ], 12 | "keywords": [ 13 | "react-cli", 14 | "command line interface", 15 | "cli", 16 | "command line", 17 | "react", 18 | "reactjs" 19 | ], 20 | "dependencies": { 21 | "commander": "^2.7.1", 22 | "fs-extra": "^0.18.0", 23 | "ejs": "^2.3.1" 24 | }, 25 | "repository": "madebyform/react-cli", 26 | "bin": { 27 | "react": "bin/react" 28 | }, 29 | "preferGlobal": true 30 | } 31 | -------------------------------------------------------------------------------- /template/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "dependencies": { 4 | "babel": "^4.7.16", 5 | "babelify": "^5.0.4", 6 | "browserify": "^9.0.3", 7 | "connect-cachify": "0.0.17", 8 | "ejs": "^2.3.1", 9 | "express": "^4.12.3", 10 | "isomorphic-fetch": "^2.0.0", 11 | "react": "^0.13.1", 12 | "react-router": "^0.13.2" 13 | }, 14 | "devDependencies": { 15 | "nodemon": "^1.3.7", 16 | "uglifyjs": "^2.4.10", 17 | "watchify": "^2.4.0" 18 | }, 19 | "scripts": { 20 | "start": "npm run build:js && node src/server.js", 21 | "live": "npm run watch:js & NODE_ENV=development nodemon src/server.js", 22 | "build:js": "browserify src/app.jsx -t babelify --outfile assets/app.js && uglifyjs assets/app.js -o assets/app.min.js", 23 | "watch:js": "watchify src/app.jsx -t babelify --outfile assets/app.js" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Made by Form 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 all 13 | 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 THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /template/src/views/template.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Welcome to your new React app. 8 | 9 | 10 | 11 | 30 | 31 | 32 |
<%- output %>
33 | 34 | 38 | <%- cachify_js("/app.min.js") %> 39 | 40 | 41 | -------------------------------------------------------------------------------- /template/src/utils/styling-mixin.jsx: -------------------------------------------------------------------------------- 1 | /*jshint esnext:true, browserify:true */ 2 | /*globals components */ 3 | 'use strict'; 4 | 5 | /* 6 | * Styling Mixin 7 | * 8 | * Simple mixin with utility methods for styling components. 9 | */ 10 | let StylingMixin = { 11 | // 12 | // This sets the component's default for the remBase property. This property 13 | // is then used by remCalc() as the base body font-size. 14 | // 15 | getDefaultProps() { 16 | return { 17 | remBase: 16 // The base body font size value in pixels 18 | }; 19 | }, 20 | // 21 | // This is the `m` method from "CSS in JS" (goo.gl/ZRKFcR). It simply merges an 22 | // arbitrary number of given objects. Useful for conditionals. Usage example: 23 | // this.mergeStyles( 24 | // styles.example, 25 | // this.props.isOpen && styles.open 26 | // ) 27 | // 28 | mergeStyles(...args) { 29 | return Object.assign({}, ...args); 30 | }, 31 | // 32 | // Convert a list of values in pixels to rems. For example: 33 | // remCalc(16, 18, 32) # returns "1rem 1.125rem 2rem" 34 | // @param {Integer} remBase — The base body font size value in pixels 35 | // @param {Array} values — One or more values in pixels to be converted to rem 36 | // @return {String} Space delimited values that can be used in css styles 37 | // 38 | remCalc(...values) { 39 | let remBase = this.props.remBase; 40 | return values.map((value) => `${value/remBase}rem`).join(" "); 41 | } 42 | }; 43 | 44 | export default StylingMixin; 45 | -------------------------------------------------------------------------------- /template/src/app.jsx: -------------------------------------------------------------------------------- 1 | /*jshint esnext:true, browserify:true */ 2 | 'use strict'; 3 | 4 | import 'babel/polyfill'; 5 | import 'isomorphic-fetch'; 6 | import React from 'react/addons'; 7 | import Router from 'react-router'; 8 | import StylingMixin from './utils/styling-mixin.jsx'; 9 | 10 | let Route = Router.Route; 11 | let RouteHandler = Router.RouteHandler; 12 | 13 | export var App = React.createClass({ 14 | mixins: [StylingMixin], 15 | contextTypes: { 16 | router: React.PropTypes.func 17 | }, 18 | getInitialState() { 19 | return { 20 | isActive: false 21 | }; 22 | }, 23 | 24 | render() { 25 | 26 | return ( 27 |
28 |

It works!

29 | 32 |
33 | ); 34 | }, 35 | 36 | toggleActive() { 37 | this.setState({ 38 | isActive: !this.state.isActive 39 | }); 40 | } 41 | }); 42 | 43 | let styles = { 44 | wrap: { 45 | padding: "20px" 46 | }, 47 | header: { 48 | fontFamily: "serif", 49 | fontSize: "48px" 50 | }, 51 | green: { 52 | color: "green" 53 | } 54 | }; 55 | 56 | export var routes = ( 57 | 58 | ) 59 | 60 | if (typeof(document) !== "undefined") { 61 | Router.run(routes, Router.HistoryLocation, function(Handler, state) { 62 | React.render( 63 | , 64 | document.getElementById("container") 65 | ); 66 | }); 67 | } 68 | -------------------------------------------------------------------------------- /template/src/server.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true */ 2 | 'use strict'; 3 | 4 | // Start by registering a hook that makes calls to `require` run ES6 code 5 | // This will be the only file where JSX and ES6 features are not supported 6 | require('babel/register'); 7 | 8 | var fs = require('fs'); 9 | var React = require('react'); 10 | var Router = require('react-router'); 11 | var express = require('express'); 12 | var cachify = require('connect-cachify'); 13 | var ejs = require('ejs'); 14 | var server = express(); 15 | 16 | // List of assets where the keys are your production urls, and the value 17 | // is a list of development urls that produce the same asset 18 | var assets = { 19 | "/app.min.js": [ "/app.js" ] 20 | }; 21 | 22 | // Enable browser cache and HTTP caching (cache busting, etc.) 23 | server.use(cachify.setup(assets, { 24 | root: "assets", 25 | production: (process.env.NODE_ENV != "development") 26 | })); 27 | 28 | // Serve static files 29 | // TODO: remember to put the favicon and other relevant stuff there. 30 | server.use('/', express.static('assets')); 31 | 32 | // Use Embedded JavaScript to embed the output from React into our layout 33 | server.set('view engine', 'ejs'); 34 | server.set('views', 'src/views'); 35 | 36 | // Require and wrap the React main component in a factory before calling it 37 | // This is necessary because we'll do `App()` instead of 38 | var routes = require("./app.jsx").routes; 39 | 40 | // Redirect the user to the list of native components for iOS 41 | server.get('/', function(req, res) { 42 | Router.run(routes, req.url, function (handler, state) { 43 | // Render the app and send the markup for faster page loads and SEO 44 | // On the client, React will preserve the markup and only attach event handlers 45 | var Handler = React.createFactory(handler); 46 | var content = new Handler({ 47 | params: state.params, 48 | query: state.query 49 | 50 | // TODO: pass any additional initial state data here 51 | 52 | }); 53 | var output = React.renderToString(content); 54 | 55 | res.render('template', { 56 | output: output 57 | 58 | // TODO: pass any additional initial state data here 59 | }); 60 | }); 61 | }); 62 | 63 | // Listen for connections 64 | server.listen(process.env.PORT || 8080, function() { 65 | console.log('Server is listening...'); 66 | }); 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-cli 2 | 3 | A command line interface tool that seeks to automate some tasks when working on a React.js project. 4 | 5 | ## Getting Started 6 | 7 | Install the react command line tool globally so you can use it from any project directory. 8 | 9 | ``` 10 | $ npm install -g madebyform/react-cli 11 | ``` 12 | 13 | ## Commands 14 | 15 | ### Create an Application 16 | 17 | ``` 18 | $ react new my-app 19 | ``` 20 | 21 | This command sets up a new react application inside the `my-app` directory. This is what you get out of the box: 22 | 23 | * [Isomorphic](http://nerds.airbnb.com/isomorphic-javascript-future-web-apps/) Node configuration 24 | * ES6 support via [babel](https://babeljs.io) 25 | * Express server + EJS templating system 26 | * React router configuration 27 | 28 | The application will have the following directory structure: 29 | 30 | ``` 31 | my-app/ 32 | assets/ 33 | src/ 34 | components/ 35 | utils/ 36 | views/ 37 | ``` 38 | 39 | ### Generate React components 40 | 41 | ``` 42 | $ react generate MyComponent firstProp:string secondProp:requiredObject 43 | ``` 44 | 45 | This creates a skeleton `mycomponent.jsx` file inside your `src/components` directory. 46 | 47 | #### Component Lifecycle methods 48 | 49 | By default, the resulting file includes React lifecycle methods, like `componentWillMount`. You can skip these by passing in either `-l` or `--skip-lifecycle` to the command above. To know more run `$ react generate -h`. 50 | 51 | #### Property Types 52 | 53 | Currently supported property types: 54 | 55 | | Optional Props | Required Props | 56 | | --------------- | -------------- | 57 | | any | requiredAny | 58 | | array | requiredArray | 59 | | bool | requiredBool | 60 | | func | requiredFunc | 61 | | number | requiredNumber | 62 | | object | requiredObject | 63 | | string | requiredString | 64 | 65 | ## Usage 66 | 67 | Use the `--help` flag for a complete list of options and commands. 68 | 69 | ``` 70 | $ react --help 71 | 72 | Usage: react [options] 73 | 74 | 75 | Commands: 76 | 77 | new Generate a new react app boilerplate. 78 | generate [options] [propName:PropType...] Generate a new react component. 79 | Options: 80 | 81 | -h, --help output usage information 82 | -V, --version output the version number 83 | ``` 84 | 85 | ## License 86 | 87 | Available under [the MIT license](http://mths.be/mit). 88 | -------------------------------------------------------------------------------- /bin/react: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | process.bin = process.title = 'react'; 5 | 6 | var program = require('commander'); 7 | var pkg = require('../package.json'); 8 | var fs = require('fs-extra'); 9 | var path = require('path'); 10 | var ejs = require('ejs'); 11 | 12 | // Parse command line options 13 | 14 | program 15 | .version(pkg.version) 16 | .usage(" [options]"); 17 | 18 | program 19 | .command("new ") 20 | .description("Generate a new react app boilerplate.") 21 | .action(function(name, options) { 22 | 23 | var templatePath = path.join(__dirname, '../template'); 24 | var destinationPath = path.join(process.cwd(), name); 25 | 26 | console.log("Creating " + name + "..."); 27 | 28 | fs.copy(templatePath, destinationPath, function (err) { 29 | if (err){ 30 | return console.error(err); 31 | } 32 | console.log('Success!'); 33 | }); 34 | 35 | }); 36 | 37 | program 38 | .command("generate [propName:PropType...]") 39 | .option("-l, --skip-lifecycle", "Skip lifecycle method generation") 40 | .description("Generate a new react component.") 41 | .action(function(name, propDefs) { 42 | var props = []; 43 | var possibleTypes = { 44 | "any": "React.PropTypes.any", 45 | "requiredAny": "React.PropTypes.any.isRequired", 46 | "func": "React.PropTypes.func", 47 | "requiredFunc": "React.PropTypes.func.isRequired", 48 | "array": "React.PropTypes.array", 49 | "requiredArray": "React.PropTypes.array.isRequired", 50 | "bool": "React.PropTypes.bool", 51 | "requiredBool": "React.PropTypes.bool.isRequired", 52 | "number": "React.PropTypes.number", 53 | "requiredNumber": "React.PropTypes.number.isRequired", 54 | "object": "React.PropTypes.object", 55 | "requiredObject": "React.PropTypes.object.isRequired", 56 | "string": "React.PropTypes.string", 57 | "requiredString": "React.PropTypes.string.isRequired", 58 | }; 59 | propDefs.forEach(function(propDef) { 60 | var nameType = propDef.split(":"); 61 | props.push({ 62 | name: nameType[0], 63 | type: possibleTypes[nameType[1]] 64 | }); 65 | }); 66 | var lifecycle = !this.skipLifecycle; 67 | fs.readFile(path.join(__dirname, "../templates", "component.ejs"), 'utf8', function (err, data) { 68 | var component = ejs.render(data, { 69 | lifecycle: lifecycle, 70 | name: name, 71 | props: props 72 | }); 73 | fs.writeFile(path.join(process.cwd(), "src/components", name.toLowerCase()+".jsx"), component); 74 | }); 75 | }); 76 | 77 | // Failsafe that shows the help dialogue if the command is not recognized (`$ react xyz`) 78 | program.on('*', function(opt) { 79 | program.help(); 80 | }); 81 | 82 | program.parse(process.argv); 83 | 84 | // Handle case where no command is passed (`$ react`) 85 | if (!process.argv.slice(2).length) { 86 | program.help(); 87 | } 88 | -------------------------------------------------------------------------------- /templates/component.ejs: -------------------------------------------------------------------------------- 1 | /*jshint esnext:true, browserify:true, unused:vars */ 2 | 'use strict'; 3 | 4 | import React from 'react/addons'; 5 | 6 | /* 7 | * <%- name %> Component 8 | */ 9 | let <%- name %> = React.createClass({ 10 | 11 | /* 12 | * Mixins 13 | * see: https://facebook.github.io/react/docs/component-specs.html#mixins 14 | */ 15 | mixins: [/* TODO add mixins */], 16 | 17 | /* 18 | * propTypes 19 | * see: https://facebook.github.io/react/docs/component-specs.html#proptypes 20 | */ 21 | propTypes: { 22 | <% props.forEach(function(prop, i, a){ -%> 23 | <%- prop.name %>: <%- prop.type %><%- i+1 < a.length ? "," : ""%> 24 | <% }); -%> 25 | }, 26 | 27 | /* 28 | * statics 29 | * see: https://facebook.github.io/react/docs/component-specs.html#statics 30 | */ 31 | statics: {/* TODO define static methods */}, 32 | 33 | /* 34 | * object getDefaultProps() 35 | * see: https://facebook.github.io/react/docs/component-specs.html#getdefaultprops 36 | */ 37 | getDefaultProps() { 38 | return { 39 | // TODO define default prop values 40 | }; 41 | }, 42 | 43 | /* 44 | * object getInitialState() 45 | * see: https://facebook.github.io/react/docs/component-specs.html#getinitialstate 46 | */ 47 | getInitialState() { 48 | return { 49 | // TODO override initial state 50 | }; 51 | }, 52 | 53 | <% if (lifecycle) { -%> 54 | /* 55 | * componentWillMount() 56 | * see https://facebook.github.io/react/docs/component-specs.html#mounting-componentwillmount 57 | */ 58 | componentWillMount() { 59 | // TODO called before the component is mounted 60 | }, 61 | 62 | /* 63 | * componentDidMount() 64 | * see: https://facebook.github.io/react/docs/component-specs.html#mounting-componentdidmount 65 | */ 66 | componentDidMount() { 67 | // TODO called after the component is mounted 68 | }, 69 | 70 | /* 71 | * componentWillReceiveProps(object nextProps) 72 | * see: https://facebook.github.io/react/docs/component-specs.html#updating-componentwillreceiveprops 73 | */ 74 | componentWillReceiveProps(nextProps) { 75 | // TODO called before component receives props 76 | }, 77 | 78 | /* 79 | * boolean shouldComponentUpdate(object nextProps, object nextState) 80 | * see: https://facebook.github.io/react/docs/component-specs.html#updating-shouldcomponentupdate 81 | */ 82 | shouldComponentUpdate(nextProps, nextState) { 83 | // TODO signal if prop and state transition should trigger an update or not. 84 | // TLDR: a way to skip the diff algorithm and increase performance. 85 | }, 86 | 87 | /* 88 | * componentWillUpdate(object nextProps, object nextState) 89 | * see: https://facebook.github.io/react/docs/component-specs.html#updating-componentwillupdate 90 | */ 91 | componentWillUpdate(nextProps, nextState) { 92 | // TODO called before the component updates 93 | }, 94 | 95 | /* 96 | * componentDidUpdate(object prevProps, object prevState) 97 | * see: https://facebook.github.io/react/docs/component-specs.html#updating-componentdidupdate 98 | */ 99 | componentDidUpdate(prevProps, prevState) { 100 | // TODO called after the component updates 101 | }, 102 | 103 | /* 104 | * componentWillUnmount() 105 | * see: https://facebook.github.io/react/docs/component-specs.html#unmounting-componentwillunmount 106 | */ 107 | componentWillUnmount() { 108 | // TODO called before the component is unmounted 109 | }, 110 | 111 | <% } -%> 112 | /* 113 | * ReactElement render() 114 | * see: https://facebook.github.io/react/docs/component-specs.html#render 115 | */ 116 | render() { 117 | // TODO edit this scaffold to suit your needs. 118 | return ( 119 |
120 | <% props.forEach(function(prop){ -%> 121 |
<%- prop.name %>: {this.props.<%- prop.name %>}
122 | <% }); -%> 123 |
124 | ); 125 | } 126 | }); 127 | 128 | export default <%- name %>; 129 | --------------------------------------------------------------------------------