Many months ago I began evaluating tools and frameworks in order to put together a baseline front end development environment. During that time it felt like there must have been half a dozen new tools and frameworks either announced or gaining adoption. After playing with various sample projects I finally settled on a set of tools for the front end environment:

  • Webpack
  • React
  • ES6 and ES7 (via Babel)
  • Karma and Jasmine for testing

These tools/frameworks are proving to be an ideal fit for my team and I thus far. I expect there to be changes but if you're working with, or wanting to work with a similar set up check out the details below.

Project Structure

The project structure is made up of two main directories: src and assets

  • [project root]
    • assets/
    • src/
      • app/
      • images/
      • styles/
      • tests/
      • vendor/
    • karma.conf.js
    • package.json
    • webpack.config.js

The src directory is where all the application source code and source assets exist. This includes images, CSS, unit tests, and third party libraries (vendor).

The assets directory is where webpack will output the built application code (js) and assets (images, styles, etc).

Configuring the Environment

I had a few goals for the front end environment:

  • I should be able to work on front end independently from back end
    • Meaning communication with back end will happen primarily via web services
  • Building should be simple for the sake of productivity

NPM

NPM is used for managing packages/dependencies as well as the interface for running simple tasks. I didn't use a more robust task runner, such as Gulp, since NPM provided what I needed and my tasks at this time are very minimal.

Below is the package.json configuration:

{
    "name": "HelloWorld",
    "version": "1.0.0",
    "description": "",
    "repository": "",
    "private": true,
    "scripts": {
        "start": "SET NODE_ENV=development& NODE_ENV=development& webpack --watch",
        "package": "SET NODE_ENV=production& NODE_ENV=production& webpack",
        "test": "karma start &"
    },
    "dependencies": {
        "react": "0.13.x",
        "react-intl": "^1.2.0"
    },
    "devDependencies": {
        "autoprefixer-loader": "^2.0.0",
        "babel": "^5.0.0",
        "babel-loader": "^5.0.0",
        "css-loader": "~0.9.0",
        "del": "^1.2.0",
        "jasmine-core": "^2.3.4",
        "karma": "^0.12.37",
        "karma-chrome-launcher": "^0.2.0",
        "karma-cli": "^0.1.0",
        "karma-jasmine": "^0.3.6",
        "karma-sourcemap-loader": "^0.3.5",
        "karma-webpack": "^1.5.1",
        "node-sass": "^3.2.0",
        "react-hot-loader": "^1.0.7",
        "sass-loader": "^1.0.2",
        "style-loader": "~0.8.0",
        "url-loader": "~0.5.5",
        "webpack": "~1.10.0",
        "webpack-dev-server": "^1.9.0"
    }
}

Scripts

There are 3 scripts defined: start, package, and test.

Why is NODE_ENV=... set twice in start and package?

I am not using Node for back-end code but I stuck with the NODE_ENV variable to define dev and prod builds. Additionally setting the variable twice is just a hack that will work in either Windows or OSX. So on each OS one of the NODE_ENV commands will fail, but at least one will succeed. This works because I used the single & which will run the next command regardless if the current one fails.

start runs webpack in development mode with the --watch argument which will allow webpack to watch the src folder for changes and automatically build/bundle as needed. This command can be run as either: npm run start or npm start.

package runs webpack in production mode and does not watch the source directory for changes. This will tell our webpack script (shown later) that it should build the project with production settings. This command is run with: npm run package.

test kicks off Karma (test runner) to run our Jasmine tests. This command is run as either: npm run test or npm test.

Dependencies

The remainder of the package.json file is just dependencies. I won't go through all of them, some are obvious, but a quick google search will give you more information about what each package is/does if you are unfamiliar.

Webpack

Now for the meat of the development environment: webpack. Lets take a look at the webpack.config.js file:

var path = require('path');  
var del = require('del');  
var webpack = require('webpack');

var debug = process.env.NODE_ENV !== 'production';

del.sync('./assets/*');

// Default plugins included in DEV/PROD
var plugins = [  
    new webpack.NoErrorsPlugin(),
    new webpack.optimize.OccurenceOrderPlugin(),
    new webpack.optimize.CommonsChunkPlugin({ name: 'common' })
];

if (debug) {  
    // DEVELOPMENT
    //plugins.push(
    //  new webpack.HotModuleReplacementPlugin()
    //);
} else {
    // PRODUCTION
    plugins.push(
        new webpack.optimize.DedupePlugin(),
        new webpack.optimize.UglifyJsPlugin({
            dropDebugger: true,
            dropConsole: true,
            compressor: {
                warnings: false
            }
        })
    );
}

module.exports = {  
    cache: debug,
    debug: debug,
    devtool: debug ? 'eval' : 'source-map',

    context: path.join(__dirname, 'src'),

    entry: {
        myapp: './app/myapp/index.jsx'
    },

    output: {
        path: path.join(__dirname, 'assets'),
        filename: '[name].js',
        chunkFilename: '[name].[hash].js',
        publicPath: '/assets/'
    },

    stats: {
        colors: true,
        reasons: true
    },

    resolve: {
        extensions: ['', '.js', '.jsx', '.scss']
    },

    module: {
        noParse: /\.min\.js$/,
        loaders: [
            {
                test: /\.(js|jsx)$/,
                exclude: /node_modules/,
                loader: 'babel-loader?stage=0'
            },
            {
                test: /\.scss$/,
                loader: 'style-loader!css-loader!autoprefixer-loader!sass-loader?outputStyle=expanded'
            },
            {
                test: /\.css$/,
                loader: 'style-loader!css-loader|autoprefixer-loader'
            },
            {
                test: /\.(png|jpg|jpeg|gif|svg|woff|woff2)$/,
                loader: 'url-loader?limit=8192'
            }
        ]
    },

    plugins: plugins
};

The config file could be more robust, but at this time it's not necessary to optimize yet. The plugins used depend on whether we're running a development build or production build. In development we don't add or remove any of our defined base plugins but in production we've added the Uglify plugin.

The three parts worth looking at are entry, output, and module.

entry Entry allows us to define our application's entry points. In the configuration above we only have one entry which in turn generates one bundle: myapp: './app/myapp/index.jsx'. The bundle name in this case will be myapp.js. It is possible to specify multiple entry points as specified in the webpack entry documentation.

It's important to note that this configuration used the CommonsChunkPlugin which brings together common modules that are shared between entry points (like if 3 of your modules required React). This means when you include your entry bundle in your page you have to first include the common.js bundle (you can change the name of the script to suit your needs). Example:

<script src="commons.js" charset="utf-8"></script>

<script src="myapp.js" charset="utf-8"></script>

Learn more about the CommonsChunkPlugin.

output Output has various options, in this config we specify a custom filename and chunkFilename. With this configuration when webpack generates your scripts it will name the entry chunks with the same name you specified in the entry section: myapp.js. It will then name non-entry chunks in a similar way but also append a hash: myapp.hash.js.

When using hash it's important that you also use the OccurenceOrderPlugin. Also publicPath needs to be specified for webpack to know where to find the chunks.

module Module section allows us to specify our loaders. Loaders apply transformations on a resource file of your app. They are simply functions that take a resource file's source as the parameter and return a new source. in this config we've specified:

  • Babel: Gives us all the ES6/7 capabilities (stage 0).
  • Sass: We've specified multiple loaders here which allows us to use Sass and auto-prefixer.
  • CSS: So we can use CSS with auto-prefixer as well.
  • Images: We've specified various images that should be turned into a Data URL if their file size is less than 8192 bytes.

There are a ton of useful loaders you could incorporate into your config, it just depends on what you need. Check out some of the loaders at the webpack docs: list of loaders.

Conclusion

One thing I learned while starting this is that there are an infinite number of tools and ways to configure your environment. It's really up to you and your team to understand what you're trying to accomplish and pick the tools that you'll be comfortable using. Another thing to keep in mind is that new tools, frameworks, and their versions are being developed and improved all the time. I don't think you can expect your front end development environment to remain the same over long periods, over the course of a project sure, but don't be afraid to evolve your tooling and frameworks.

Do you have a better way of setting up your environment? Are there are issues in the environment I described above? Please share your thoughts in the comments.