diff --git a/README.md b/README.md index e83623c..724a6af 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,168 @@ -# generator-react-webpack [![Build Status](https://secure.travis-ci.org/newtriks/generator-react-webpack.png?branch=master)](https://travis-ci.org/newtriks/generator-react-webpack) +# generator-react-webpack [![Build Status](https://secure.travis-ci.org/newtriks/generator-react-webpack.png?branch=master)](https://travis-ci.org/newtriks/generator-react-webpack) [![Built with Grunt](https://cdn.gruntjs.com/builtwith.png)](http://gruntjs.com/) -A generator for [Yeoman](http://yeoman.io). +> Yeoman generator for [ReactJS](http://facebook.github.io/react/) - lets you quickly set up a project including karma test runner and [Webpack](http://webpack.github.io/) module system. -## Getting Started - -### What is Yeoman? - -Trick question. It's not a thing. It's this guy: - -![](http://i.imgur.com/JHaAlBJ.png) - -Basically, he wears a top hat, lives in your computer, and waits for you to tell him what kind of application you wish to create. - -Not every new computer comes with a Yeoman pre-installed. He lives in the [npm](https://npmjs.org) package repository. You only have to ask for him once, then he packs up and moves into your hard drive. *Make sure you clean up, he likes new and shiny things.* +## Usage +Install `generator-react-webpack`: ``` -$ npm install -g yo +npm install -g generator-react-webpack ``` -### Yeoman Generators - -Yeoman travels light. He didn't pack any generators when he moved in. You can think of a generator like a plug-in. You get to choose what type of application you wish to create, such as a Backbone application or even a Chrome extension. - -To install generator-react-webpack from npm, run: - +Make a new directory, and `cd` into it: ``` -$ npm install -g generator-react-webpack +mkdir my-new-project && cd $_ ``` -Finally, initiate the generator: - +Run `yo react-webpack`, optionally passing an app name: ``` -$ yo react-webpack +yo react-webpack [app-name] ``` -### Getting To Know Yeoman +Run `grunt` for building and `grunt serve` for preview in the browser at [localhost](http://localhost:8000). -Yeoman has a heart of gold. He's a person with feelings and opinions, but he's very easy to work with. If you think he's too opinionated, he can be easily convinced. +## Generators -If you'd like to get to know Yeoman better and meet some of his friends, [Grunt](http://gruntjs.com) and [Bower](http://bower.io), check out the complete [Getting Started Guide](https://github.com/yeoman/yeoman/wiki/Getting-Started). +Available generators: +* [react-webpack](#app) (aka [react-webpack:app](#app)) +* [react-webpack:component](#component) + +**Note: Generators are to be run from the root directory of your app.** + +### App + +Sets up a new ReactJS app, generating all the boilerplate you need to get started. The app generator also facilitates the following: + +1. Configures a Gruntfile to run the app on a local server. +2. Configures Webpack to modularise the app enabling [loading of various file formats](http://webpack.github.io/docs/loader-list.html) e.g. JSON, CSS, PNG, etc. +3. Configures [Karma](http://karma-runner.github.io) to run all tests. +4. Watches for changes and recompiles JS and refreshes the browser. + +Example: +```bash +yo react-webpack +``` + +### Component + +Generates a [JSX](http://facebook.github.io/react/docs/jsx-in-depth.html) component in `src/scripts/components` and it's corresponding test in `src/spec/components`. + +Example: +```bash +yo react-webpack:component foo +``` + +Produces `src/scripts/components/Foo.js` (*javascript - JSX*): +``` +/** + * @jsx React.DOM + */ + +'use strict'; + +var React = require('react/addons'); + +var Foo = React.createClass({ + /*jshint ignore:start */ + render: function () { + return ( +
+

Content for Foo

+
+ ) + } + /*jshint ignore:end */ +}); + +module.exports = Foo; +``` + +And `test/spec/components/Foo.js` (*javascript - jasmine*): +``` + +'use strict'; + +describe('Foo', function () { + var Foo, component; + + beforeEach(function () { + Foo = require('../../../src/scripts/components/Foo'); + component = Foo(); + }); + + it('should create a new instance of Foo', function () { + expect(component).toBeDefined(); + }); +}); +``` + +## Testing + +Running `grunt test` will run the unit tests with karma. Tests are written using [Jasmine](http://pivotal.github.io/jasmine/) by default. + +## Further Information + +### Project Structure + +The react-webpack generator automates the setup of a [ReactJS](http://facebook.github.io/react/) project using the specific structure detailed below: + +``` +project + - src + - scripts + -components + ComponentOne.js + ComponentTwo.js + main.js + - styles + main.css + reset.css + index.html + - test + - spec + - components + ComponentOne.js + ComponentTwo.js + - helpers + - react + addons.js + phantomjs-shims.js + Gruntfile.js + karma.conf.js +``` + +I have tried to keep the project structure as simple as possible and understand it may not suit everyone. + +### Naming Components + +I have opted to follow [@floydophone](https://twitter.com/floydophone) convention of uppercase for component file naming e.g. [Component.js](https://github.com/petehunt/ReactHack/tree/master/src/components). I am open to suggestions if there is a general objection to this decision. + +### Modules + +Each component is a module and can be required using the [Webpack](http://webpack.github.io/) module system. [Webpack](http://webpack.github.io/) uses [Loaders](http://webpack.github.io/docs/loaders.html) which means you can also require CSS and a host of other file types. Read the [Webpack documentation](http://webpack.github.io/docs/home.html) to find out more. + +### Grunt + +Out the box the [Gruntfile](http://gruntjs.com/api/grunt.file) is configured with the following: + +1. **webpack**: uses the [grunt-webpack](https://github.com/webpack/grunt-webpack) plugin to load all required modules and output to a single JS file `src/scripts/main.js`. This is included in the `src/index.html` file by default and will reload in the browser as and when it is recompiled. +2. **watch**: uses the [grunt-watch](https://github.com/gruntjs/grunt-contrib-watch) plugin and watches for file changes. Livereload support is included by default and thus on file change the *webpack* task is run and the browser will auto update. +3. **connect**: uses the [grunt-connect](https://github.com/gruntjs/grunt-contrib-connect) plugin to start a webserver at [localhost](http://localhost:8000). +4. **karma**: uses the [grunt-karma](https://github.com/karma-runner/grunt-karma) plugin to load the Karma configuration file `karma.conf.js` located in the project root. This will run all tests using [PhantomJS](http://phantomjs.org/) by default but supports many other browsers. + +### CSS + +Included in the project is [Eric Meyer's reset.css](http://meyerweb.com/eric/tools/css/reset/) script. There is also a `src/styles/main.css` script that's required by the core `src/scripts/components/App.js` component using Webpack. + +## Props + +Thanks to all who contributed to [generator-angular](https://github.com/yeoman/generator-angular) as the majority of code here has been shamelessy sourced from that repos. + +## Contribute + +Contributions are welcomed. When submitting a bugfix, write a test that exposes the bug and fails before applying your fix. Submit the test alongside the fix. ## License diff --git a/app/index.js b/app/index.js index 740dc80..fb5b70c 100644 --- a/app/index.js +++ b/app/index.js @@ -72,3 +72,7 @@ ReactWebpackGenerator.prototype.imageFiles = function () { this.sourceRoot(path.join(__dirname, 'templates')); this.directory('images', 'src/images', true); }; + +ReactWebpackGenerator.prototype.karmaFiles = function () { + this.copy('../../templates/common/karma.conf.js', 'karma.conf.js'); +}; diff --git a/component/index.js b/component/index.js new file mode 100644 index 0000000..043ac66 --- /dev/null +++ b/component/index.js @@ -0,0 +1,17 @@ +'use strict'; +var util = require('util'); +var ScriptBase = require('../script-base.js'); + +var ComponentGenerator = module.exports = function ComponentGenerator(args, options, config) { + ScriptBase.apply(this, arguments); +}; + +util.inherits(ComponentGenerator, ScriptBase); + +ComponentGenerator.prototype.createComponentFile = function createComponentFile() { + this.generateSourceAndTest( + 'component', + 'spec/component', + 'components' + ); +}; diff --git a/script-base.js b/script-base.js index 261047c..24a860c 100644 --- a/script-base.js +++ b/script-base.js @@ -14,6 +14,7 @@ var Generator = module.exports = function Generator() { this.appname = this._.slugify(this._.humanize(this.appname)); this.scriptAppName = this._.capitalize(this.appname) + generalUtils.appName(this); + this.classedName = this._.capitalize(this.name); if (typeof this.env.options.appPath === 'undefined') { this.env.options.appPath = this.env.options.appPath || 'src'; @@ -52,10 +53,7 @@ Generator.prototype.htmlTemplate = function (src, dest) { ]); }; -Generator.prototype.generateSourceAndTest = function (appTemplate, testTemplate, targetDirectory, skipAdd) { - this.appTemplate(appTemplate, path.join('scripts', targetDirectory, this.name)); - this.testTemplate(testTemplate, path.join(targetDirectory, this.name)); - if (!skipAdd) { - this.addScriptToIndex(path.join(targetDirectory, this.name)); - } +Generator.prototype.generateSourceAndTest = function (appTemplate, testTemplate, targetDirectory) { + this.appTemplate(appTemplate, path.join('scripts', targetDirectory, this._.capitalize(this.name))); + this.testTemplate(testTemplate, path.join(targetDirectory, this._.capitalize(this.name))); }; \ No newline at end of file diff --git a/templates/common/Gruntfile.js b/templates/common/Gruntfile.js index 3b10070..03ef33d 100644 --- a/templates/common/Gruntfile.js +++ b/templates/common/Gruntfile.js @@ -116,7 +116,7 @@ module.exports = function (grunt) { } }); - grunt.registerTask('server', function (target) { + grunt.registerTask('serve', function (target) { if (target === 'dist') { return grunt.task.run(['build', 'open', 'connect:dist:keepalive']); } diff --git a/templates/common/karma.conf.js b/templates/common/karma.conf.js new file mode 100644 index 0000000..3d7cfa4 --- /dev/null +++ b/templates/common/karma.conf.js @@ -0,0 +1,58 @@ +'use strict'; + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine'], + files: [ + 'test/helpers/**/*.js', + 'test/spec/components/**/*.js' + ], + preprocessors: { + 'test/spec/components/**/*.js': ['webpack'] + }, + webpack: { + cache: true, + module: { + loaders: [{ + test: /\.css$/, + loader: 'style!css' + }, { + test: /\.gif/, + loader: 'url-loader?limit=10000&minetype=image/gif' + }, { + test: /\.jpg/, + loader: 'url-loader?limit=10000&minetype=image/jpg' + }, { + test: /\.png/, + loader: 'url-loader?limit=10000&minetype=image/png' + }, { + test: /\.js$/, + loader: 'jsx-loader' + }] + } + }, + webpackServer: { + stats: { + colors: true + } + }, + exclude: [], + port: 8080, + logLevel: config.LOG_INFO, + colors: true, + autoWatch: false, + // Start these browsers, currently available: + // - Chrome + // - ChromeCanary + // - Firefox + // - Opera + // - Safari (only Mac) + // - PhantomJS + // - IE (only Windows) + browsers: ['PhantomJS'], + reporters: ['progress'], + captureTimeout: 60000, + singleRun: true + }); +}; diff --git a/templates/javascript/Component.js b/templates/javascript/Component.js new file mode 100644 index 0000000..2b12147 --- /dev/null +++ b/templates/javascript/Component.js @@ -0,0 +1,21 @@ +/** + * @jsx React.DOM + */ + +'use strict'; + +var React = require('react/addons'); + +var <%= classedName %> = React.createClass({ + /*jshint ignore:start */ + render: function () { + return ( +
+

Content for <%= classedName %>

+
+ ) + } + /*jshint ignore:end */ +}); + +module.exports = <%= classedName %>; diff --git a/templates/javascript/spec/Component.js b/templates/javascript/spec/Component.js new file mode 100644 index 0000000..7f03497 --- /dev/null +++ b/templates/javascript/spec/Component.js @@ -0,0 +1,14 @@ +'use strict'; + +describe('<%= classedName %>', function () { + var <%= classedName %>, component; + + beforeEach(function () { + <%= classedName %> = require('../../../src/scripts/components/<%= classedName %>'); + component = <%= classedName %>(); + }); + + it('should create a new instance of <%= classedName %>', function () { + expect(component).toBeDefined(); + }); +});