Have Grunt generate index.html for different setups

Solution 1:

I recently discovered these Grunt v0.4.0 compatible tasks:

  • grunt-preprocess

    Grunt task around preprocess npm module.

  • grunt-env

    Grunt task to automate environment configuration for future tasks.

Below are snippets from my Gruntfile.js.

ENV setup:

env : {

    options : {

        /* Shared Options Hash */
        //globalOption : 'foo'

    },

    dev: {

        NODE_ENV : 'DEVELOPMENT'

    },

    prod : {

        NODE_ENV : 'PRODUCTION'

    }

},

Preprocess:

preprocess : {

    dev : {

        src : './src/tmpl/index.html',
        dest : './dev/index.html'

    },

    prod : {

        src : './src/tmpl/index.html',
        dest : '../<%= pkg.version %>/<%= now %>/<%= ver %>/index.html',
        options : {

            context : {
                name : '<%= pkg.name %>',
                version : '<%= pkg.version %>',
                now : '<%= now %>',
                ver : '<%= ver %>'
            }

        }

    }

}

Tasks:

grunt.registerTask('default', ['jshint']);

grunt.registerTask('dev', ['jshint', 'env:dev', 'clean:dev', 'preprocess:dev']);

grunt.registerTask('prod', ['jshint', 'env:prod', 'clean:prod', 'uglify:prod', 'cssmin:prod', 'copy:prod', 'preprocess:prod']);

And in the /src/tmpl/index.html template file (for example):

<!-- @if NODE_ENV == 'DEVELOPMENT' -->

    <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.js"></script>
    <script src="../src/js/foo1.js"></script>
    <script src="../src/js/foo2.js"></script>
    <script src="../src/js/jquery.blah.js"></script>
    <script src="../src/js/jquery.billy.js"></script>
    <script src="../src/js/jquery.jenkins.js"></script>

<!-- @endif -->

<!-- @if NODE_ENV == 'PRODUCTION' -->

    <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>

    <script src="http://cdn.foo.com/<!-- @echo name -->/<!-- @echo version -->/<!-- @echo now -->/<!-- @echo ver -->/js/<!-- @echo name -->.min.js"></script>

<!-- @endif -->

I'm sure my setup is different than most people, and the usefulness of the above will depend on your situation. For me, while it's an awesome bit of code, the Yeoman grunt-usemin is a more robust than I personally need.

NOTE: I just discovered the above listed tasks today, so I might be missing a feature and/or my process may change down the road. For now, I'm loving the simplicity and features that grunt-preprocess and grunt-env have to offer. :)


Jan 2014 update:

Motivated by a down vote ...

When I posted this answer there weren't many options for Grunt 0.4.x that offered a solution that worked for my needs. Now, months later, I would guess that there are more options out there that could be better than what I have posted here. While I still personally use, and enjoy using, this technique for my builds, I ask that future readers take the time to read the other answers given and to research all the options. If you find a better solution, please post your answer here.

Feb 2014 update:

I'm not sure if it will be of any help to anyone, but I've created this demo repository on GitHub that shows a complete (and more complex setup) using the technique(s) I've outlined above.

Solution 2:

I've come up with my own solution. Not polished yet but I think I'm going to move in that direction.

In essense, I'm using grunt.template.process() to generate my index.html from a template that analyzes current configuration and produces either a list of my original source files or links to a single file with minified code. The below example is for js files but the same approach can be extended to css and any other possible text files.

grunt.js:

/*global module:false*/
module.exports = function(grunt) {
    var   // js files
        jsFiles = [
              'src/module1.js',
              'src/module2.js',
              'src/module3.js',
              'src/awesome.js'
            ];

    // Import custom tasks (see index task below)
    grunt.loadTasks( "build/tasks" );

    // Project configuration.
    grunt.initConfig({
      pkg: '<json:package.json>',
      meta: {
        banner: '/*! <%= pkg.name %> - v<%= pkg.version %> - ' +
          '<%= grunt.template.today("yyyy-mm-dd") %> */'
      },

      jsFiles: jsFiles,

      // file name for concatenated js
      concatJsFile: '<%= pkg.name %>-all.js',

      // file name for concatenated & minified js
      concatJsMinFile: '<%= pkg.name %>-all.min.js',

      concat: {
        dist: {
            src: ['<banner:meta.banner>'].concat(jsFiles),
            dest: 'dist/<%= concatJsFile %>'
        }
      },
      min: {
        dist: {
        src: ['<banner:meta.banner>', '<config:concat.dist.dest>'],
        dest: 'dist/<%= concatJsMinFile %>'
        }
      },
      lint: {
        files: ['grunt.js'].concat(jsFiles)
      },
      // options for index.html builder task
      index: {
        src: 'index.tmpl',  // source template file
        dest: 'index.html'  // destination file (usually index.html)
      }
    });


    // Development setup
    grunt.registerTask('dev', 'Development build', function() {
        // set some global flags that all tasks can access
        grunt.config('isDebug', true);
        grunt.config('isConcat', false);
        grunt.config('isMin', false);

        // run tasks
        grunt.task.run('lint index');
    });

    // Production setup
    grunt.registerTask('prod', 'Production build', function() {
        // set some global flags that all tasks can access
        grunt.config('isDebug', false);
        grunt.config('isConcat', true);
        grunt.config('isMin', true);

        // run tasks
        grunt.task.run('lint concat min index');
    });

    // Default task
    grunt.registerTask('default', 'dev');
};

index.js (the index task):

module.exports = function( grunt ) {
    grunt.registerTask( "index", "Generate index.html depending on configuration", function() {
        var conf = grunt.config('index'),
            tmpl = grunt.file.read(conf.src);

        grunt.file.write(conf.dest, grunt.template.process(tmpl));

        grunt.log.writeln('Generated \'' + conf.dest + '\' from \'' + conf.src + '\'');
    });
}

Finally, index.tmpl, with generation logic baked in:

<doctype html>
<head>
<%
    var jsFiles = grunt.config('jsFiles'),
        isConcat = grunt.config('isConcat');

    if(isConcat) {
        print('<script type="text/javascript" src="' + grunt.config('concat.dist.dest') + '"></script>\n');
    } else {
        for(var i = 0, len = jsFiles.length; i < len; i++) {
            print('<script type="text/javascript" src="' + jsFiles[i] + '"></script>\n');
        }
    }
%>
</head>
<html>
</html>

UPD. Found out that Yeoman, which is based on grunt, has a built-in usemin task that integrates with Yeoman's build system. It generates a production version of index.html from information in development version of index.html as well as other environment settings. A bit sophisticated but interesting to look at.

Solution 3:

I dislike the solutions here (including the one I previously gave) and here's why:

  • The problem with the highest voted answer is that you have to manually sync the list of script tags when you add/rename/delete a JS file.
  • The problem with the accepted answer is that your list of JS files can't have pattern matching. This means you've got to update it by hand in the Gruntfile.

I've figured out how to solve both of these issues. I've set up my grunt task so that every time a file is added or deleted, the script tags automatically get generated to reflect that. This way, you don't need to modify your html file or your grunt file when you add/remove/rename your JS files.

To summarize how that works, I have a html template with a variable for the script tags. I use https://github.com/alanshaw/grunt-include-replace to populate that variable. In dev mode, that variable comes from a globbing pattern of all my JS files. The watch task recalculates this value when a JS file is added or removed.

Now, to get different results in dev or prod mode, you simply populate that variable with a different value. Here's some code:

var jsSrcFileArray = [
    'src/main/scripts/app/js/Constants.js',
    'src/main/scripts/app/js/Random.js',
    'src/main/scripts/app/js/Vector.js',
    'src/main/scripts/app/js/scripts.js',
    'src/main/scripts/app/js/StatsData.js',
    'src/main/scripts/app/js/Dialog.js',
    'src/main/scripts/app/**/*.js',
    '!src/main/scripts/app/js/AuditingReport.js'
];

var jsScriptTags = function (srcPattern, destPath) {
    if (srcPattern === undefined) {
        throw new Error("srcPattern undefined");
    }
    if (destPath === undefined) {
        throw new Error("destPath undefined");
    }
    return grunt.util._.reduce(
        grunt.file.expandMapping(srcPattern, destPath, {
            filter: 'isFile',
            flatten: true,
            expand: true,
            cwd: '.'
        }),
        function (sum, file) {
            return sum + '\n<script src="' + file.dest + '" type="text/javascript"></script>';
        },
        ''
    );
};

...

grunt.initConfig({

    includereplace: {
        dev: {
            options: {
                globals: {
                    scriptsTags: '<%= jsScriptTags(jsSrcFileArray, "../../main/scripts/app/js")%>'
                }
            },
            src: [
                'src/**/html-template.html'
            ],
            dest: 'src/main/generated/',
            flatten: true,
            cwd: '.',
            expand: true
        },
        prod: {
            options: {
                globals: {
                    scriptsTags: '<script src="app.min.js" type="text/javascript"></script>'
                }
            },
            src: [
                'src/**/html-template.html'
            ],
            dest: 'src/main/generatedprod/',
            flatten: true,
            cwd: '.',
            expand: true
        }

...

    jsScriptTags: jsScriptTags

jsSrcFileArray is your typical grunt file-globbing pattern. jsScriptTags takes the jsSrcFileArray and concatenates them together with script tags on both sides. destPath is the prefix I want on each file.

And here's what the HTML looks like:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8"/>
    <title>Example</title>

</head>

<body>    
@@scriptsTags
</body>
</html>

Now, as you can see in the config, I generate the value of that variable as a hard coded script tag when it's run in prod mode. In dev mode, this variable will expand to a value like this:

<script src="../../main/scripts/app/js/Constants.js" type="text/javascript"></script>
<script src="../../main/scripts/app/js/Random.js" type="text/javascript"></script>
<script src="../../main/scripts/app/js/Vector.js" type="text/javascript"></script>
<script src="../../main/scripts/app/js/StatsData.js" type="text/javascript"></script>
<script src="../../main/scripts/app/js/Dialog.js" type="text/javascript"></script>

Let me know if you have any questions.

PS: This is a crazy amount of code for something I'd want to do in every client-side JS app. I hope someone can turn this into a reusable plugin. Maybe I will some day.

Solution 4:

I have been asking myself the same question for a while, and I think this grunt plugin could be configured to do what you want: https://npmjs.org/package/grunt-targethtml. It implements conditional html tags, that depend on the grunt target.