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' ]); };