Adventures in Grunt Land

In theory, deploying a Flask app with an Angular frontend should be easy – just use

yo angular

to ask Yeoman to create the app structure for you. But the devil is in the detail, and the output generated by Yeoman isn’t necessarily exactly what you want, even though it’s an excellent starting point.

So here are some notes on deploying a Flask app with Grunt.

Directory structure

The structure suggested by Yeoman seems reasonable, but as there is a Flask backend, what Yeoman calls app should be called static, app being the directory for the Flask app content. Thus the directory structure might look like

src
- app/
- bower_components/
- bower.json
- config.py
- docs/
- manage.py
- migrations/
- node_modules/
- package.json
- README.html
- README.rst
- requirements.txt
- static/
- tests/
- wsgi.py

The static has the following content.

static
- bower_components (symbolic link, see below)
- images/
- karma.conf.js
- scripts/
- styles/
- views/

Flask’s server expects the bower_components directory to be in the static directory. This requires the symbolic link. You should create the link by going to the static directory and executing

ln -s bower_components ../bower_components

Git handles symbolic links, so this link should be created and stored as part of your project.

Changing appConfig

The definition of the appConfig variable should be changed to:

    // Configurable paths for the application
    var appConfig = {
        app: require('./bower.json').appPath || 'static',
        dist: '../dist/static'
    };

Images

The Yeoman Grunt file assumes that all your images (including the ones for your stylesheets) are located in the folder static/images. So if you want to store them somewhere else you'll have to tweak the file.

Using cdnify and usemin tasks

If you are using both the cdnify and the usemin task, you'll notice that usemin seems to happily ignore the efforts of cdnify. Eish.

htmlmin or no htmlmin...

If you don't want the HTML minified, remove htmlmin from the list of tasks in the build task definition.

Copying the Flask stuff (and fonts)

The Grunt file generated by Yeoman won't copy the Flask related content to the distribution directory. Nor does it seem to copy the Bootstrap fonts. This is fixed by adding the relevant files to the configuration of the copy task:

                         copy: {
                             dist: {
                                 files: [
                                     {
                                         expand: true,
                                         dot: true,
                                         cwd: '<%= yeoman.app %>',
                                         dest: '<%= yeoman.dist %>',
                                         src: [
                                             '*.{ico,png,txt}',
                                             '.htaccess',
                                             '*.html',
                                             'views/{,*/}*.html',
                                             'images/{,*/}*.{webp}',
                                             'fonts/*'
                                         ]
                                     },
                                     {
                                         expand: true,
                                         cwd: '.tmp/images',
                                         dest: '<%= yeoman.dist %>/images',
                                         src: ['generated/*']
                                     },
                                     {
                                         expand: true,
                                         cwd: '<%= yeoman.app %>/../bower_components/bootstrap/fonts',
                                         dest: '<%= yeoman.dist %>/fonts',
                                         src: ['**/*']
                                     },
                                     {
                                         expand: true,
                                         dot: true,
                                         cwd: '<%= yeoman.app %>/..',
                                         dest: '<%= yeoman.dist %>/..',
                                         src: ['app/**/*', 'config.py', 'wsgi.py']
                                     }
                                 ]
                             },
                             styles: {
                                 expand: true,
                                 cwd: '<%= yeoman.app %>/styles',
                                 dest: '.tmp/styles/',
                                 src: '{,*/}*.css'
                             }
                         },

You'll also have to create an alias /fonts in your Apache configuration.

Improving your karma

Depending on where you put your Karma configuration file, you might have to change the karma task's settings.

// Test settings
karma: {
    unit: {
        configFile: 'test/karma.conf.js',
        singleRun: true
    }
},

Improved cleaning

By default the clean task will only remove files located in the current working directory or any of its subdirectories. As our distribution directory is outside this scope, we must set the task's force option for cleaning up that directory. And while we are at it, we may also add a postdist target for removing the temporary directory after deploying.

// Empties folders to start fresh
clean: {
    dist: {
        options: {
            force: true
        },
        files: [
            {
                dot: true,
                src: [
                    '.tmp',
                    '<%= yeoman.dist %>/{,*/}*',
                    '!<%= yeoman.dist %>/.git*'
                ]
            }
        ]
    },
    postdist: '.tmp',
    server: '.tmp'
},

Remember to add clean:postdist as the last task in the list of build tasks.

Installing the Bower packages

You can streamline the deployment process a bit further by using bower-install-simple to install missing Bower components. To do so, you'll first have to install the corresponding module:

npm install grunt-bower-install-simple

Then you can register the task

grunt.loadNpmTasks('grunt-bower-install-simple');

configure it

// Install Bower packages
'bower-install-simple': {
    options: {
        color: true
    },
    dist: {
        options: {
            production: true
        }
    }
}

and include bower-install-simple:dist as the first task in the build task definition.

A full-fledged example

To sum up, here is a Grunt file with all these changes.

// Generated on 2014-10-28 using generator-angular 0.9.8
'use strict';

// # Globbing
// for performance reasons we're only matching one level down:
// 'test/spec/{,*/}*.js'
// use this if you want to recursively match all subfolders:
// 'test/spec/**/*.js'

module.exports = function (grunt) {

    // Load grunt tasks automatically
    require('load-grunt-tasks')(grunt);

    grunt.loadNpmTasks('grunt-bower-install-simple');

    // Time how long tasks take. Can help when optimizing build times
    require('time-grunt')(grunt);

    // Configurable paths for the application
    var appConfig = {
        app: require('./bower.json').appPath || 'static',
        dist: '../dist/static'
    };

    // Define the configuration for all the tasks
    grunt.initConfig({

                         // Project settings
                         yeoman: appConfig,

                         // Watches files for changes and runs tasks based on the changed files
                         watch: {
                             bower: {
                                 files: ['bower.json'],
                                 tasks: ['wiredep']
                             },
                             js: {
                                 files: ['<%= yeoman.app %>/scripts/{,*/}*.js'],
                                 tasks: ['newer:jshint:all'],
                                 options: {
                                     livereload: '<%= connect.options.livereload %>'
                                 }
                             },
                             jsTest: {
                                 files: ['test/spec/{,*/}*.js'],
                                 tasks: ['newer:jshint:test', 'karma']
                             },
                             compass: {
                                 files: ['<%= yeoman.app %>/styles/{,*/}*.{scss,sass}'],
                                 tasks: ['compass:server', 'autoprefixer']
                             },
                             gruntfile: {
                                 files: ['Gruntfile.js']
                             },
                             livereload: {
                                 options: {
                                     livereload: '<%= connect.options.livereload %>'
                                 },
                                 files: [
                                     '<%= yeoman.app %>/{,*/}*.html',
                                     '.tmp/styles/{,*/}*.css',
                                     '<%= yeoman.app %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}'
                                 ]
                             }
                         },

                         // The actual grunt server settings
                         connect: {
                             options: {
                                 port: 9000,
                                 // Change this to '0.0.0.0' to access the server from outside.
                                 hostname: 'localhost',
                                 livereload: 35729
                             },
                             livereload: {
                                 options: {
                                     open: true,
                                     middleware: function (connect) {
                                         return [
                                             connect.static('.tmp'),
                                             connect().use(
                                                     '/bower_components',
                                                     connect.static('./bower_components')
                                             ),
                                             connect.static(appConfig.app)
                                         ];
                                     }
                                 }
                             },
                             test: {
                                 options: {
                                     port: 9001,
                                     middleware: function (connect) {
                                         return [
                                             connect.static('.tmp'),
                                             connect.static('test'),
                                             connect().use(
                                                     '/bower_components',
                                                     connect.static('./bower_components')
                                             ),
                                             connect.static(appConfig.app)
                                         ];
                                     }
                                 }
                             },
                             dist: {
                                 options: {
                                     open: true,
                                     base: '<%= yeoman.dist %>'
                                 }
                             }
                         },

                         // Make sure code styles are up to par and there are no obvious mistakes
                         jshint: {
                             options: {
                                 jshintrc: '.jshintrc',
                                 reporter: require('jshint-stylish')
                             },
                             all: {
                                 src: [
                                     'Gruntfile.js',
                                     '<%= yeoman.app %>/scripts/{,*/}*.js'
                                 ]
                             },
                             test: {
                                 options: {
                                     jshintrc: 'test/.jshintrc'
                                 },
                                 src: ['test/spec/{,*/}*.js']
                             }
                         },

                         // Empties folders to start fresh
                         clean: {
                             dist: {
                                 options: {
                                     force: true
                                 },
                                 files: [
                                     {
                                         dot: true,
                                         src: [
                                             '.tmp',
                                             '<%= yeoman.dist %>/{,*/}*',
                                             '!<%= yeoman.dist %>/.git*'
                                         ]
                                     }
                                 ]
                             },
                             postdist: '.tmp',
                             server: '.tmp'
                         },

                         // Add vendor prefixed styles
                         autoprefixer: {
                             options: {
                                 browsers: ['last 1 version']
                             },
                             dist: {
                                 files: [
                                     {
                                         expand: true,
                                         cwd: '.tmp/styles/',
                                         src: '{,*/}*.css',
                                         dest: '.tmp/styles/'
                                     }
                                 ]
                             }
                         },

                         // Automatically inject Bower components into the app
                         wiredep: {
                             app: {
                                 src: ['<%= yeoman.app %>/index.html'],
                                 ignorePath: /\.\.\//
                             },
                             sass: {
                                 src: ['<%= yeoman.app %>/styles/{,*/}*.{scss,sass}'],
                                 ignorePath: /(\.\.\/){1,2}bower_components\//
                             }
                         },

                         // Compiles Sass to CSS and generates necessary files if requested
                         compass: {
                             options: {
                                 sassDir: '<%= yeoman.app %>/styles',
                                 cssDir: '.tmp/styles',
                                 generatedImagesDir: '.tmp/images/generated',
                                 imagesDir: '<%= yeoman.app %>/images',
                                 javascriptsDir: '<%= yeoman.app %>/scripts',
                                 fontsDir: '<%= yeoman.app %>/styles/fonts',
                                 importPath: './bower_components',
                                 httpImagesPath: '/images',
                                 httpGeneratedImagesPath: '/images/generated',
                                 httpFontsPath: '/styles/fonts',
                                 relativeAssets: false,
                                 assetCacheBuster: false,
                                 raw: 'Sass::Script::Number.precision = 10\n'
                             },
                             dist: {
                                 options: {
                                     generatedImagesDir: '<%= yeoman.dist %>/images/generated'
                                 }
                             },
                             server: {
                                 options: {
                                     debugInfo: true
                                 }
                             }
                         },

                         // Renames files for browser caching purposes
                         filerev: {
                             dist: {
                                 src: [
                                     '<%= yeoman.dist %>/scripts/{,*/}*.js',
                                     '<%= yeoman.dist %>/styles/{,*/}*.css',
                                     '<%= yeoman.dist %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}',
                                     '<%= yeoman.dist %>/styles/fonts/*'
                                 ]
                             }
                         },

                         // Reads HTML for usemin blocks to enable smart builds that automatically
                         // concat, minify and revision files. Creates configurations in memory so
                         // additional tasks can operate on them
                         useminPrepare: {
                             html: '<%= yeoman.app %>/index.html',
                             options: {
                                 dest: '<%= yeoman.dist %>',
                                 flow: {
                                     html: {
                                         steps: {
                                             js: ['concat', 'uglifyjs'],
                                             css: ['cssmin']
                                         },
                                         post: {}
                                     }
                                 }
                             }
                         },

                         // Performs rewrites based on filerev and the useminPrepare configuration
                         usemin: {
                             html: ['<%= yeoman.dist %>/{,*/}*.html'],
                             css: ['<%= yeoman.dist %>/styles/{,*/}*.css'],
                             options: {
                                 assetsDirs: ['<%= yeoman.dist %>', '<%= yeoman.dist %>/images']
                             }
                         },

                         // The following *-min tasks will produce minified files in the dist folder
                         // By default, your `index.html`'s <!-- Usemin block --> will take care of
                         // minification. These next options are pre-configured if you do not wish
                         // to use the Usemin blocks.
                         // cssmin: {
                         //   dist: {
                         //     files: {
                         //       '<%= yeoman.dist %>/styles/main.css': [
                         //         '.tmp/styles/{,*/}*.css'
                         //       ]
                         //     }
                         //   }
                         // },
                         // uglify: {
                         //   dist: {
                         //     files: {
                         //       '<%= yeoman.dist %>/scripts/scripts.js': [
                         //         '<%= yeoman.dist %>/scripts/scripts.js'
                         //       ]
                         //     }
                         //   }
                         // },
                         // concat: {
                         //   dist: {}
                         // },

                         imagemin: {
                             dist: {
                                 files: [
                                     {
                                         expand: true,
                                         cwd: '<%= yeoman.app %>/images',
                                         src: '{,*/}*.{png,jpg,jpeg,gif}',
                                         dest: '<%= yeoman.dist %>/images'
                                     }
                                 ]
                             }
                         },

                         svgmin: {
                             dist: {
                                 files: [
                                     {
                                         expand: true,
                                         cwd: '<%= yeoman.app %>/images',
                                         src: '{,*/}*.svg',
                                         dest: '<%= yeoman.dist %>/images'
                                     }
                                 ]
                             }
                         },

                         htmlmin: {
                             dist: {
                                 options: {
                                     collapseWhitespace: true,
                                     conservativeCollapse: true,
                                     collapseBooleanAttributes: true,
                                     removeCommentsFromCDATA: true,
                                     removeOptionalTags: true
                                 },
                                 files: [
                                     {
                                         expand: true,
                                         cwd: '<%= yeoman.dist %>',
                                         src: ['*.html', 'views/{,*/}*.html'],
                                         dest: '<%= yeoman.dist %>'
                                     }
                                 ]
                             }
                         },

                         // ng-annotate tries to make the code safe for minification automatically
                         // by using the Angular long form for dependency injection.
                         ngAnnotate: {
                             dist: {
                                 files: [
                                     {
                                         expand: true,
                                         cwd: '.tmp/concat/scripts',
                                         src: ['*.js', '!oldieshim.js'],
                                         dest: '.tmp/concat/scripts'
                                     }
                                 ]
                             }
                         },

                         // Replace Google CDN references
                         cdnify: {
                             dist: {
                                 html: ['<%= yeoman.dist %>/*.html']
                             }
                         },

                         // Copies remaining files to places other tasks can use
                         copy: {
                             dist: {
                                 files: [
                                     {
                                         expand: true,
                                         dot: true,
                                         cwd: '<%= yeoman.app %>',
                                         dest: '<%= yeoman.dist %>',
                                         src: [
                                             '*.{ico,png,txt}',
                                             '.htaccess',
                                             '*.html',
                                             'views/{,*/}*.html',
                                             'images/{,*/}*.{webp}',
                                             'fonts/*'
                                         ]
                                     },
                                     {
                                         expand: true,
                                         cwd: '.tmp/images',
                                         dest: '<%= yeoman.dist %>/images',
                                         src: ['generated/*']
                                     },
                                     {
                                         expand: true,
                                         dot: true,
                                         cwd: '<%= yeoman.app %>/..',
                                         dest: '<%= yeoman.dist %>/..',
                                         src: ['app/**/*', 'config.py', 'wsgi.py']
                                     }
                                 ]
                             },
                             styles: {
                                 expand: true,
                                 cwd: '<%= yeoman.app %>/styles',
                                 dest: '.tmp/styles/',
                                 src: '{,*/}*.css'
                             }
                         },

                         // Run some tasks in parallel to speed up the build process
                         concurrent: {
                             server: [
                                 'compass:server'
                             ],
                             test: [
                                 'compass'
                             ],
                             dist: [
                                 'compass:dist',
                                 'imagemin',
                                 'svgmin'
                             ]
                         },

                         // Test settings
                         karma: {
                             unit: {
                                 configFile: 'test/karma.conf.js',
                                 singleRun: true
                             }
                         },

                         // Install Bower packages
                         'bower-install-simple': {
                             options: {
                                 color: true
                             },
                             dist: {
                                 options: {
                                     production: true
                                 }
                             }
                         }
                     });

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

        grunt.task.run([
                           'clean:server',
                           'wiredep',
                           'concurrent:server',
                           'autoprefixer',
                           'connect:livereload',
                           'watch'
                       ]);
    });

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

    grunt.registerTask('test', [
        'clean:server',
        'concurrent:test',
        'autoprefixer',
        'connect:test',
        'karma'
    ]);

    grunt.registerTask('build', [
        'bower-install-simple:dist',
        'clean:dist',
        'wiredep',
        'useminPrepare',
        'concurrent:dist',
        'autoprefixer',
        'concat',
        'ngAnnotate',
        'copy:dist',
        'cdnify',
        'cssmin',
        'uglify',
        'filerev',
        'usemin',
//        'htmlmin',
            'clean:postdist'
    ]);

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