Grunt + ReactJS + Browserify: multiple apps, common modules, and requiring with no relative paths

Grunt + ReactJS + Browserify: multiple apps, common modules, and requiring with no relative paths

Posted on Friday, June 26, 2015

When I built my first ReactJS CRUD application, I was mainly tinkering with the React framework and had all my components in one giant file. I got to the point where I decided I wanted to configure a build system to compile and watch for changes in the codebase, and followed this great blog article on converting my huge file to CommonJS modules and got my app all set up. Here is my folder structure when I started the new config:

app directory
- src/
-- components/
--- mixins/
-- img/
-- vendor/
-- style/
- build/
-- js/
-- img/
-- style/
- node_modules/
- Gruntfile.js
- package.json
- index.html

Running grunt compiles my JSX components in my src directory into javascript and the rest of the files into the build directory, and running grunt watch allowed me to recompile my code every time I made changes in any of my source. Saved it all to git, and verified npm install worked so my codebase was portable.

So great, everything works and I'm building my app. Along comes the idea for another app to be built. I want to maintain the same look and feel, and use some of the same components I had built for my first app (I had an AlertModal, ErrorBar, LoadingScreen, some form and data mixins, some skeletal components that help the app's interface, etc.). Before I knew what I was doing, I started copying and pasting code from one app to the other. But then I realized how modular and reusable these components were made, and why am I not reusing them across my apps?

So I decided to create a common repository with the components I anticipated reusing across apps. Simple enough:

common repo
- components/
-- mixins/
- img/
- vendor/
- style/

Okay, and in each of my apps, I cloned this common repo as a git submodule in the src directory. So my new file structure looks like this:

app directory
- src/
-- common/
--- components/
---- mixins/
--- img/
--- vendor/
--- style/
-- components/
--- mixins/
-- img/
-- vendor/
-- style/
- build/
-- js/
-- img/
-- style/
- node_modules/
- Gruntfile.js
- package.json
- index.html

I did it this way because I didn't want to deal with node_modules and I also wanted to be able to control which branch of my common repo each app would be working with. Just in case I wanted to do some development work in the common repo that only affected one app and experiment, and maybe some development work that affects the other app at the same time.

The next thing I had to figure out was how to require all the modules in such a way that I didn't have to care about filepaths. I wanted to be able to change around directories without having to go through all the modules and having to change all the require() statements. At the moment I was doing things like require('react') for npm modules and require('./ErrorBar') for my components and require('./mixins/DataMixin'), which I guess was fine.

But now, with the new common folder that contained additional components and mixins that I wanted, this was not ideal. I wanted the flexibility to be able to move components and mixins from common into my app component folder, and vice versa, without having to change all the require() statements that reference them. In fact, I wanted to do away with the paths altogether and have browserify do a lookup in the same way it did with node_modules.

I looked at the browserify github documentation and was attracted to the alias, require, and plugin directives, and even went searching for the remapify, aliasify, and pathmodify plugins it mentions. However, these didn't really do it for me. Remapify almost worked -- my expose value was '' so I was able to do things like require('ErrorBar'). However, I kept getting a parse error with unexpected token, and realized for some reason browserify was not applying my transform configuration ({transform: [ require('grunt-react').browserify ]}) and therefore trying to parse the code as JS not JSX.

So, after more digging around, I found the answer. It was simple. And it was not easy to find via Google.

I passed an array of places that I wanted browserify to look as my paths value under browserifyOptions in my Gruntfile. So my Gruntfile looks like this:

module.exports = function(grunt) {
    grunt.initConfig({
        pkg: grunt.file.readJSON('package.json'),
        browserify: {
            options: {
                transform: [ require('grunt-react').browserify ],
                // plugin: [ // This was my failed `remapify` attempt
                    // ['remapify', [
                    //     {
                    //         src: '*.js',
                    //         expose: '',
                    //         cwd: './src/BIL-common/components'
                    //     },
                    //     {
                    //         src: '**/*.js',
                    //         expose: '',
                    //         cwd: './src/BIL-common/components'
                    //     },
                    //     {
                    //         src: '*.js',
                    //         expose: '',
                    //         cwd: './src/BIL-common/vendor'
                    //     },
                    //     {
                    //         src: '*.js',
                    //         expose: '',
                    //         cwd: './src/components'
                    //     },
                    //     {
                    //         src: '**/*.js',
                    //         expose: '',
                    //         cwd: './src/components'
                    //     }
                    // ]],
                browserifyOptions: { // This worked!
                    paths: [
                        'src/common/components',
                        'src/common/components/mixins',
                        'src/common/vendor',
                        'src/components',
                        'src/components/mixins',
                        'src/vendor'
                    ]
                }
            },
            client: {
                src: [
                    './src/components/App.js'
                ],
                dest: 'build/js/app.js'
            }
        }
...

So, with that in place, I could have control over the common repo and be on whatever branch I need it to be, and I can do things like require('DataMixin') instead of require('../common/component/mixin/Datamixin') from within my app's component directory.

Next I'll probably programmatically generate that paths array with a glob and expand it with the path module so that at build time, all those folders in the glob are are included in the lookup automatically. So if I change or modify the folder structure, I don't have to keep updating that array.