Search code examples
javascriptgruntjsrequiregrunt-contrib-requirejs

yeoman generator-backbone and RequireJS - concat all modules into single file on build?


I'm trying to use Generator-Backbone generator for Yeoman with RequireJS.

I don't need the lazy-loading of RequireJS, I just am using it for dependency management and organization. It's fine if it's used during development, but when I run grunt:build I'd love for it to concat all my modules into a single file to minimize HTTP requests.

Currently when I build I am getting this error:

Running "requirejs:dist" (requirejs) task
>> Error: Error: ENOENT, no such file or directory '/Users/Tom/Code/myApp/.tmp/scripts/templates.js'
>> In module tree:
>>     main
>>       app
>>
>>     at Error (native)

If I watch the directory, it seems the templates.js file is created in the right place by the JST task, however it's later overwritten by another task before the requirejs task can complete.

Below is my directory structure, as well as my Gruntfile:

Directory:

├── Gruntfile.js
├── app
│   ├── bower_components
│   ├── index.html
│   ├── scripts
│   │   ├── main.js
│   │   ├── modules
│   │   │   └── admanager.js
│   │   ├── templates
│   │   │   ├── ads.ejs
│   │   │   ├── app.ejs
│   │   │   ├── content.ejs
│   │   │   └── navigation.ejs
│   │   └── views
│   │       ├── ads.js
│   │       ├── app.js
│   │       ├── content.js
│   │       └── navigation.js
│   └── styles
│       └── main.css
├── bower.json
├── dist
├── node_modules
├── package.json
└── test
    ├── index.html
    └── spec
        └── test.js

Gruntfile:

'use strict';
var LIVERELOAD_PORT = 35729;
var SERVER_PORT = 9000;
var lrSnippet = require('connect-livereload')({port: LIVERELOAD_PORT});
var mountFolder = function (connect, dir) {
    return connect.static(require('path').resolve(dir));
};


module.exports = function (grunt) {

    require('time-grunt')(grunt);
    require('load-grunt-tasks')(grunt);

    // configurable paths
    var yeomanConfig = {
        app: 'app',
        dist: 'dist'
    };

    grunt.initConfig({
        yeoman: yeomanConfig,
        watch: {
            options: {
                nospawn: true,
                livereload: LIVERELOAD_PORT
            },
            livereload: {
                options: {
                    livereload: grunt.option('livereloadport') || LIVERELOAD_PORT
                },
                files: [
                    '<%= yeoman.app %>/*.html',
                    '{.tmp,<%= yeoman.app %>}/styles/{,*/}*.css',
                    '{.tmp,<%= yeoman.app %>}/scripts/{,*/}*.js',
                    '<%= yeoman.app %>/images/{,*/}*.{png,jpg,jpeg,gif,webp}',
                    '<%= yeoman.app %>/scripts/templates/*.{ejs,mustache,hbs}',
                    'test/spec/**/*.js'
                ]
            },
            jst: {
                files: [
                    '<%= yeoman.app %>/scripts/templates/*.ejs'
                ],
                tasks: ['jst']
            },
            test: {
                files: ['<%= yeoman.app %>/scripts/{,*/}*.js', 'test/spec/**/*.js'],
                tasks: ['test:true']
            }
        },
        connect: {
            options: {
                port: grunt.option('port') || SERVER_PORT,
                // change this to '0.0.0.0' to access the server from outside
                hostname: 'localhost',
                livereload: 35729
            },
            livereload: {
                options: {
                    base: [
                        '.tmp',
                        '<%= yeoman.app %>'
                    ],
                    middleware: function (connect) {
                        return [
                            lrSnippet,
                            mountFolder(connect, '.tmp'),
                            mountFolder(connect, yeomanConfig.app)
                        ];
                    },
                }
            },
            test: {
                options: {
                    port: 9001,
                    middleware: function (connect) {
                        return [
                            mountFolder(connect, 'test'),
                            lrSnippet,
                            mountFolder(connect, '.tmp'),
                            mountFolder(connect, yeomanConfig.app)
                        ];
                    }
                }
            },
            dist: {
                options: {
                    middleware: function (connect) {
                        return [
                            mountFolder(connect, yeomanConfig.dist)
                        ];
                    }
                }
            }
        },
        open: {
            server: {
                path: 'http://localhost:9000'
            },
            test: {
                path: 'http://localhost:<%= connect.test.options.port %>'
            }
        },
        clean: {
            dist: ['.tmp', '<%= yeoman.dist %>/*'],
            server: '.tmp'
        },
        jshint: {
            options: {
                jshintrc: '.jshintrc',
                reporter: require('jshint-stylish')
            },
            all: [
                'Gruntfile.js',
                '<%= yeoman.app %>/scripts/{,*/}*.js',
                '!<%= yeoman.app %>/scripts/vendor/*',
                'test/spec/{,*/}*.js'
            ]
        },
        mocha: {
            all: {
                options: {
                    run: true,
                    urls: ['http://localhost:<%= connect.test.options.port %>/index.html']
                }
            }
        },
        requirejs: {
            dist: {
                // Options: https://github.com/jrburke/r.js/blob/master/build/example.build.js
                options: {
                    /*added:*/
                    wrap: true,
                    almond: true,
                    replaceRequireScript: [{
                        files: ['<%= yeoman.dist %>/index.html'],
                        module: 'main'
                    }],
                    modules: [{name: 'main'}],
                    baseUrl: '<%= yeoman.app %>/scripts',
                    mainConfigFile: '<%= yeoman.app %>/scripts/main.js',
                    dir: '.tmp/scripts',
                    optimize: 'none',
                    useStrict: true,
                    paths: {
                        'templates': '../../<%= yeoman.app %>/scripts/templates',
                        'jquery': '../../<%= yeoman.app %>/bower_components/jquery/jquery',
                        'underscore': '../../<%= yeoman.app %>/bower_components/lodash/dist/lodash',
                        'backbone': '../../<%= yeoman.app %>/bower_components/backbone/backbone'
                    }
                    /*end added*/

                    /*
                    baseUrl: '<%= yeoman.app %>/scripts',
                    optimize: 'none',
                    paths: {
                        'templates': '../../.tmp/scripts/templates',
                        'jquery': '../../<%= yeoman.app %>/bower_components/jquery/dist/jquery',
                        'underscore': '../../<%= yeoman.app %>/bower_components/lodash/dist/lodash',
                        'backbone': '../../<%= yeoman.app %>/bower_components/backbone/backbone'
                    },
                    preserveLicenseComments: false,
                    useStrict: true,
                    wrap: true
                    */
                }
            }
        },
        /*added:*/
        uglify: {
            dist: {
                files: {
                    '<%= yeoman.dist %>/scripts/main.js': [
                        '.tmp/scripts/main.js'
                    ]
                }
            }
        },/*end added*/

        useminPrepare: {
            html: '<%= yeoman.app %>/index.html',
            options: {
                dest: '<%= yeoman.dist %>'
            }
        },
        usemin: {
            html: ['<%= yeoman.dist %>/{,*/}*.html'],
            css: ['<%= yeoman.dist %>/styles/{,*/}*.css'],
            options: {
                dirs: ['<%= yeoman.dist %>']
            }
        },
        imagemin: {
            dist: {
                files: [{
                    expand: true,
                    cwd: '<%= yeoman.app %>/images',
                    src: '{,*/}*.{png,jpg,jpeg}',
                    dest: '<%= yeoman.dist %>/images'
                }]
            }
        },
        cssmin: {
            dist: {
                files: {
                    '<%= yeoman.dist %>/styles/main.css': [
                        '.tmp/styles/{,*/}*.css',
                        '<%= yeoman.app %>/styles/{,*/}*.css'
                    ]
                }
            }
        },
        htmlmin: {
            dist: {
                options: {
                    /*removeCommentsFromCDATA: true,
                    // https://github.com/yeoman/grunt-usemin/issues/44
                    //collapseWhitespace: true,
                    collapseBooleanAttributes: true,
                    removeAttributeQuotes: true,
                    removeRedundantAttributes: true,
                    useShortDoctype: true,
                    removeEmptyAttributes: true,
                    removeOptionalTags: true*/
                },
                files: [{
                    expand: true,
                    cwd: '<%= yeoman.app %>',
                    src: '*.html',
                    dest: '<%= yeoman.dist %>'
                }]
            }
        },
        copy: {
            dist: {
                files: [{
                    expand: true,
                    dot: true,
                    cwd: '<%= yeoman.app %>',
                    dest: '<%= yeoman.dist %>',
                    src: [
                        '*.{ico,txt}',
                        'images/{,*/}*.{webp,gif}',
                        'styles/fonts/{,*/}*.*',
                    ]
                }, {
                    src: 'node_modules/apache-server-configs/dist/.htaccess',
                    dest: '<%= yeoman.dist %>/.htaccess'
                }]
            }
        },
        bower: {
            all: {
                rjsConfig: '<%= yeoman.app %>/scripts/main.js'
            }
        },
        jst: {
            options: {
                amd: true
            },
            compile: {
                files: {
                    // '.tmp/scripts/templates.js': ['<%= yeoman.app %>/scripts/templates/*.ejs']
                    '.tmp/scripts/templates.js': ['<%= yeoman.app %>/scripts/templates/*.ejs']
                }
            }
        },
        rev: {
            dist: {
                files: {
                    src: [
                        '<%= yeoman.dist %>/scripts/{,*/}*.js',
                        '<%= yeoman.dist %>/styles/{,*/}*.css',
                        '<%= yeoman.dist %>/images/{,*/}*.{png,jpg,jpeg,gif,webp}',
                        '/styles/fonts/{,*/}*.*',
                    ]
                }
            }
        }
    });

    grunt.registerTask('createDefaultTemplate', function () {
        grunt.file.write('.tmp/scripts/templates.js', 'this.JST = this.JST || {};');
    });

    grunt.registerTask('server', function (target) {
        grunt.log.warn('The `server` task has been deprecated. Use `grunt serve` to start a server.');
        grunt.task.run(['serve' + (target ? ':' + target : '')]);
    });

    grunt.registerTask('serve', function (target) {
        if (target === 'dist') {
            return grunt.task.run(['build', 'open:server', 'connect:dist:keepalive']);
        }

        if (target === 'test') {
            return grunt.task.run([
                'clean:server',
                'createDefaultTemplate',
                'jst',
                'connect:test',
                'open:test',
                'watch'
            ]);
        }

        grunt.task.run([
            'clean:server',
            'createDefaultTemplate',
            'jst',
            'connect:livereload',
            'open:server',
            'watch'
        ]);
    });

    grunt.registerTask('test', function (isConnected) {
        isConnected = Boolean(isConnected);
        var testTasks = [
                'clean:server',
                'createDefaultTemplate',
                'jst',
                'connect:test',
                'mocha',
            ];

        if(!isConnected) {
            return grunt.task.run(testTasks);
        } else {
            // already connected so not going to connect again, remove the connect:test task
            testTasks.splice(testTasks.indexOf('connect:test'), 1);
            return grunt.task.run(testTasks);
        }
    });

    grunt.registerTask('build', [
        'clean:dist',
        'createDefaultTemplate',
        'jst',
        'useminPrepare',
        'imagemin',
        'htmlmin',
        'concat',
        'cssmin',
        'uglify:generated',
        'copy',
        'requirejs',
        'uglify:dist',
        'rev',
        'usemin'
    ]);

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

Main.js:

/*global require*/
'use strict';

require.config({
    shim: {
    },
    paths: {
        // LIBS
        jquery: '../bower_components/jquery/jquery',
        backbone: '../bower_components/backbone/backbone',
        underscore: '../bower_components/lodash/dist/lodash',
        cookies: '../bower_components/js-cookie/src/js.cookie',
        // CUSTOM MODULES
        admanager: './modules/admanager',
        // APP-SPECIFIC
        app: '../scripts/views/app',
        content: '../scripts/views/content',
        ads: '../scripts/views/ads',
        navigation: '../scripts/views/navigation'
    }
});

require([
    'backbone',
    'app',
], function (Backbone, App) {
    Backbone.history.start();
    window.myApp = new App();
});

Solution

  • I spent nearly 2 entire hours to solve this and finally figured it out. Below is how my requireJs task looks like

    options: {
      wrap: true,
      almond: true,
      name: "../../<%= yeoman.app %>/bower_components/almond/almond",
      include: ['main.js'],
      out: "<%= yeoman.dist %>/scripts/app.js",
      replaceRequireScript: [{
        files: ['<%= yeoman.dist %>/index.html'],
        module: 'main'
      }],
      baseUrl: '<%= yeoman.app %>/scripts',
      mainConfigFile: '<%= yeoman.app %>/scripts/main.js',
      optimize: 'uglify2',
      useStrict: true,
      paths: {
        'templates': '../../.tmp/scripts/templates',
        'jquery': '../../<%= yeoman.app %>/bower_components/jquery/dist/jquery',
        'underscore': '../../<%= yeoman.app %>/bower_components/lodash/dist/lodash',
        'backbone': '../../<%= yeoman.app %>/bower_components/backbone/backbone'
      }
    }
    

    You are missing certain options like name, include, out. Also dir should not be used.

    This is how registeredTask 'build' looks like

    grunt.registerTask('build', [
        'clean:dist',
        'createDefaultTemplate',
        'jst',
        'sass:dist',
        'useminPrepare',
        'imagemin',
        'htmlmin',
        'concat',
        'cssmin',
        'uglify',
        'copy',
        'requirejs',
        'rev',
        'usemin'
    ]);
    

    This configuration worked for me. I hope it works for you too.