backbone.select
Version:
Selecting Backbone models; handling selections in Backbone collections.
393 lines (359 loc) • 14.9 kB
JavaScript
/*global module:false*/
module.exports = function(grunt) {
var LIVERELOAD_PORT = 35731,
HTTP_PORT = 9400,
KARMA_PORT = 9877,
WATCHED_FILES_SRC = [
'src/**/*'
],
WATCHED_FILES_SPEC = [
'spec/**/*'
],
WATCHED_FILES_DIST = [
'dist/**/*'
],
WATCHED_FILES_DEMO = [
'demo/**/*'
],
path = require( "path" ),
requireFromString = require( "require-from-string" ),
/**
* Receives an object and the name of a path property on that object. Translates the path property to a new path,
* based on a directory prefix. Does not return anything, modifies the object itself.
*
* The directory prefix can be relative (e.g. "../../"). It may or may not end in a slash.
*
* @param {string} dirPrefix
* @param {Object} object
* @param {string} propertyName
* @param {boolean} [verbose=false]
*/
translatePathProperty = function ( dirPrefix, object, propertyName, verbose ) {
var originalPath = object[propertyName];
if ( originalPath ) {
object[propertyName] = path.normalize( dirPrefix + path.sep + originalPath );
if ( verbose ) grunt.log.writeln( 'Translating path property "' + propertyName + '": ' + originalPath + " => " + object[propertyName] );
}
},
/**
* Reads an r.js build profile and returns it as an options set for a grunt-contrib-requirejs task.
*
* For a discussion, see https://github.com/gruntjs/grunt-contrib-requirejs/issues/13
*
* Paths in the build profile are relative to the profile location. In the returned options object, they are
* transformed to be relative to the Gruntfile. (The list is nowhere near complete. More properties need to be
* transformed as build profiles become more complex.)
*
* @param {string} profilePath relative to the Gruntfile
* @param {boolean} [verbose=false]
* @returns {Object}
*/
getRequirejsBuildProfile = function ( profilePath, verbose ) {
var profileContent = grunt.file.read( profilePath ),
profile = requireFromString( "module.exports = " + profileContent ),
dirPrefix = path.dirname( profilePath );
if ( verbose ) grunt.log.writeln( "Loading r.js build profile " + profilePath );
// Add more paths here as needed.
translatePathProperty( dirPrefix, profile, "mainConfigFile", verbose );
translatePathProperty( dirPrefix, profile, "out", verbose );
if ( verbose ) grunt.log.writeln();
return profile;
};
// Project configuration.
grunt.config.init({
pkg: grunt.file.readJSON('package.json'),
meta: {
version: '<%= pkg.version %>',
banner: '// Backbone.Select, v<%= meta.version %>\n' +
'// Copyright (c) 2014-<%= grunt.template.today("yyyy") %> Michael Heim, Zeilenwechsel.de\n' +
'// (c) 2013 Derick Bailey, Muted Solutions, LLC.\n' +
'// Distributed under MIT license\n' +
'// http://github.com/hashchange/backbone.select\n' +
'\n'
},
preprocess: {
build: {
files: {
'dist/backbone.select.js' : 'src/backbone.select.js'
}
},
interactive: {
files: {
'web-mocha/index.html': 'web-mocha/_index.html'
}
}
},
concat: {
options: {
banner: "<%= meta.banner %>",
process: function( src, filepath ) {
var bowerVersion = grunt.file.readJSON( "bower.json" ).version,
npmVersion = grunt.file.readJSON( "package.json" ).version;
if ( npmVersion === undefined || npmVersion === "" ) grunt.fail.fatal( "Version number not specified in package.json. Specify it in bower.json and package.json" );
if ( npmVersion !== bowerVersion ) grunt.fail.fatal( "Version numbers in package.json and bower.json are not identical. Make them match." + " " + npmVersion );
if ( ! /^\d+\.\d+.\d+$/.test( npmVersion ) ) grunt.fail.fatal( 'Version numbers in package.json and bower.json are not semantic. Provide a version number in the format n.n.n, e.g "1.2.3"' );
return src.replace( "__COMPONENT_VERSION_PLACEHOLDER__", npmVersion );
}
},
build: {
src: 'dist/backbone.select.js',
dest: 'dist/backbone.select.js'
}
},
uglify: {
options: {
banner: "<%= meta.banner %>",
mangle: {
except: ['jQuery', 'Zepto', 'Backbone', '_']
},
sourceMap: true
},
core: {
src: 'dist/backbone.select.js',
dest: 'dist/backbone.select.min.js'
}
},
karma: {
options: {
browsers: ['PhantomJS'],
reporters: ['progress'],
singleRun: true,
port: KARMA_PORT
},
"test-select": {
configFile: 'karma.conf.js'
},
"test-backbone": {
configFile: 'karma.backbone.conf.js'
}
},
jshint: {
components: {
// Workaround for merging .jshintrc with Gruntfile options, see http://goo.gl/Of8QoR
options: grunt.util._.merge({
globals: {
// Add vars which are shared between various sub-components
// (before concatenation makes them local)
}
}, grunt.file.readJSON('.jshintrc')),
files: {
src: ['src/**/*.js']
}
},
concatenated: {
options: grunt.util._.merge({
// Suppressing 'W034: Unnecessary directive "use strict"'.
// Redundant nested "use strict" is ok in concatenated file,
// no adverse effects.
'-W034': true
}, grunt.file.readJSON('.jshintrc')),
files: {
src: 'dist/**/__COMPONENT_NAME_LC__.js'
}
}
},
'sails-linker': {
options: {
startTag: '<!--SCRIPTS-->',
endTag: '<!--SCRIPTS END-->',
fileTmpl: '<script src="../%s"></script>',
// relative doesn't seem to have any effect, ever
relative: true,
// appRoot is a misnomer for "strip out this prefix from the file path before inserting",
// should be stripPrefix
appRoot: ''
},
interactive_spec: {
options: {
startTag: '<!--SPEC SCRIPTS-->',
endTag: '<!--SPEC SCRIPTS END-->'
},
files: {
// the target file is changed in place; for generating copies, run preprocess first
'web-mocha/index.html': ['spec/**/*.+(spec|test|tests).js']
}
}
},
requirejs : {
unifiedBasicBuild : {
options : getRequirejsBuildProfile( 'demo/amd/rjs/config/unified/basic-build-config.js', false )
},
unifiedBasketsBuild : {
options : getRequirejsBuildProfile( 'demo/amd/rjs/config/unified/baskets-build-config.js', false )
},
unifiedFocusExclusiveBuild : {
options : getRequirejsBuildProfile( 'demo/amd/rjs/config/unified/focus-exclusive-build-config.js', false )
},
unifiedFocusLabelBuild : {
options : getRequirejsBuildProfile( 'demo/amd/rjs/config/unified/focus-label-build-config.js', false )
},
unifiedTrailingBuild : {
options : getRequirejsBuildProfile( 'demo/amd/rjs/config/unified/trailing-build-config.js', false )
},
splitBuildBasicApp : {
options : getRequirejsBuildProfile( 'demo/amd/rjs/config/jsbin-parts/basic-app-config.js', false )
},
splitBuildBasketsApp : {
options : getRequirejsBuildProfile( 'demo/amd/rjs/config/jsbin-parts/baskets-app-config.js', false )
},
splitBuildFocusExclusiveApp : {
options : getRequirejsBuildProfile( 'demo/amd/rjs/config/jsbin-parts/focus-exclusive-app-config.js', false )
},
splitBuildFocusLabelApp : {
options : getRequirejsBuildProfile( 'demo/amd/rjs/config/jsbin-parts/focus-label-app-config.js', false )
},
splitBuildTrailingApp : {
options : getRequirejsBuildProfile( 'demo/amd/rjs/config/jsbin-parts/trailing-app-config.js', false )
},
splitBuildVendor : {
options : getRequirejsBuildProfile( 'demo/amd/rjs/config/jsbin-parts/vendor-config.js', false )
}
},
// Use focus to run Grunt watch with a hand-picked set of simultaneous watch targets.
focus: {
demo: {
include: ['livereloadDemo']
},
demoCi: {
include: ['build', 'livereloadDemo']
},
demoCiDirty: {
include: ['buildDirty', 'livereloadDemo']
}
},
// Use watch to monitor files for changes, and to kick off a task then.
watch: {
options: {
nospawn: false
},
// Live-reloads the web page when the source files or the spec files change. Meant for test pages.
livereloadTest: {
options: {
livereload: LIVERELOAD_PORT
},
files: WATCHED_FILES_SRC.concat( WATCHED_FILES_SPEC )
},
// Live-reloads the web page when the dist files or the demo files change. Meant for demo pages.
livereloadDemo: {
options: {
livereload: LIVERELOAD_PORT
},
files: WATCHED_FILES_DEMO.concat( WATCHED_FILES_DIST )
},
// Runs the "build" task (ie, runs linter and tests, then compiles the dist files) when the source files or the
// spec files change. Meant for continuous integration tasks ("ci", "demo-ci").
build: {
tasks: ['build'],
files: WATCHED_FILES_SRC.concat( WATCHED_FILES_SPEC )
},
// Runs the "build-dirty" task (ie, compiles the dist files without running linter and tests) when the source
// files change. Meant for "dirty" continuous integration tasks ("ci-dirty", "demo-ci-dirty").
buildDirty: {
tasks: ['build-dirty'],
files: WATCHED_FILES_SRC
}
},
// Spins up a web server.
connect: {
options: {
port: HTTP_PORT,
// For restricting access to localhost only, change the hostname from '*' to 'localhost'
hostname: '*',
open: true,
base: '.'
},
livereload: {
livereload: LIVERELOAD_PORT
},
test: {
options: {
open: 'http://localhost:<%= connect.options.port %>/web-mocha/',
livereload: LIVERELOAD_PORT
}
},
testNoReload: {
options: {
open: 'http://localhost:<%= connect.options.port %>/web-mocha/',
keepalive: true
}
},
demo: {
options: {
open: 'http://localhost:<%= connect.options.port %>/demo/',
livereload: LIVERELOAD_PORT
}
}
},
replace: {
version: {
src: ['bower.json', 'package.json'],
overwrite: true,
replacements: [{
from: /"version"\s*:\s*"((\d+\.\d+\.)(\d+))"\s*,/,
to: function (matchedWord, index, fullText, regexMatches) {
var version = grunt.option('inc') ? regexMatches[1] + (parseInt(regexMatches[2], 10) + 1) : grunt.option('to');
if (version === undefined) grunt.fail.fatal('Version number not specified. Use the --to option, e.g. --to=1.2.3, or the --inc option to increment the revision');
if (typeof version !== "string") grunt.fail.fatal('Version number is not a string. Provide a semantic version number, e.g. --to=1.2.3');
if (!/^\d+\.\d+.\d+$/.test(version)) grunt.fail.fatal('Version number is not semantic. Provide a version number in the format n.n.n, e.g. --to=1.2.3');
grunt.log.writeln('Modifying file: Changing the version number from ' + regexMatches[0] + ' to ' + version);
return '"version": "' + version + '",';
}
}]
}
},
getver: {
files: ['bower.json', 'package.json']
}
});
grunt.loadNpmTasks('grunt-preprocess');
grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-karma');
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('grunt-contrib-connect');
grunt.loadNpmTasks('grunt-sails-linker');
grunt.loadNpmTasks('grunt-text-replace');
grunt.loadNpmTasks('grunt-contrib-requirejs');
grunt.loadNpmTasks('grunt-focus');
grunt.registerTask('lint', ['jshint:components']);
grunt.registerTask('hint', ['jshint:components']); // alias
grunt.registerTask('test', ['jshint:components', 'karma']);
grunt.registerTask('test-select', ['jshint:components', 'karma:test-select']);
grunt.registerTask('test-backbone', ['jshint:components', 'karma:test-backbone']);
grunt.registerTask('webtest', ['preprocess:interactive', 'sails-linker:interactive_spec', 'connect:testNoReload']);
grunt.registerTask('interactive', ['preprocess:interactive', 'sails-linker:interactive_spec', 'connect:test', 'watch:livereloadTest']);
grunt.registerTask('demo', ['connect:demo', 'focus:demo']);
grunt.registerTask('build', ['jshint:components', 'karma', 'preprocess:build', 'concat', 'uglify', 'jshint:concatenated', 'requirejs']);
grunt.registerTask('ci', ['build', 'watch:build']);
grunt.registerTask('setver', ['replace:version']);
grunt.registerTask('getver', function () {
grunt.config.get('getver.files').forEach(function (file) {
var config = grunt.file.readJSON(file);
grunt.log.writeln('Version number in ' + file + ': ' + config.version);
});
});
// Special tasks, not mentioned in Readme documentation:
//
// - test-backbone:
// runs a (modified) selection of Backbone unit tests for checking compliance with the Backbone spec
// - test-select:
// runs the Backbone.Select test suite, without the Backbone unit tests (ie, without the test-backbone task)
// - requirejs:
// creates build files for the AMD demo with r.js
// - build-dirty:
// builds the project without running checks (no linter, no tests)
// - ci-dirty:
// builds the project without running checks (no linter, no tests) on every source change
// - demo-ci:
// Runs the demo (= "demo" task), and also rebuilds the project on every source change (= "ci" task)
// - demo-ci-dirty:
// Runs the demo (= "demo" task), and also rebuilds the project "dirty", without tests or linter, on every source
// change (= "ci-dirty" task)
grunt.registerTask('build-dirty', ['preprocess:build', 'concat', 'uglify', 'requirejs']);
grunt.registerTask('ci-dirty', ['build-dirty', 'watch:buildDirty']);
grunt.registerTask('demo-ci', ['build', 'connect:demo', 'focus:demoCi']);
grunt.registerTask('demo-ci-dirty', ['build-dirty', 'connect:demo', 'focus:demoCiDirty']);
// Make 'build' the default task.
grunt.registerTask('default', ['build']);
};