diff --git a/.gitignore b/.gitignore index 47a28166fd..2844552a01 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ cjs/* amd/* ie8/bundle.js lib/* +tmp-bower-repo/ +tmp-docs-repo/ diff --git a/Gruntfile.js b/Gruntfile.js index a2f1a0ed86..8f06b9affc 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -26,6 +26,12 @@ module.exports = function (grunt) { dest: 'amd/', cwd: 'tools/amd', expand: true + }, + { + src: ['.gitignore-template'], + dest: 'amd/', + cwd: 'tools/amd', + expand: true } ] }, @@ -156,8 +162,7 @@ module.exports = function (grunt) { src: 'amd/<%= pkg.name %>.js', dest: 'amd/<%= pkg.name %>.min.js' } - } - + }, }); grunt.loadNpmTasks('grunt-contrib-uglify'); @@ -183,6 +188,8 @@ module.exports = function (grunt) { 'clean:transpiled' ]); + require('./tools/release/tasks')(grunt); + grunt.registerTask('default', ['build']); }; diff --git a/docs/.gitignore-template b/docs/.gitignore-template new file mode 100644 index 0000000000..43edecbb22 --- /dev/null +++ b/docs/.gitignore-template @@ -0,0 +1,11 @@ +*~ +node_modules/ +.DS_Store +npm-debug.log +.idea +examples/ +src/ +build.js +client.js +server.js +package.json diff --git a/package.json b/package.json index 0048fb3f65..5028d6746a 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,7 @@ "scripts": { "build": "./node_modules/.bin/grunt build", "test-watch": "./node_modules/.bin/grunt watch 2>&1 >/dev/null & ./node_modules/karma/bin/karma start karma.dev.js", - "test": "./node_modules/.bin/grunt build && ./node_modules/karma/bin/karma start karma.ci.js", - "prepublish": "./node_modules/.bin/grunt build" + "test": "./node_modules/.bin/grunt build && ./node_modules/karma/bin/karma start karma.ci.js" }, "main": "lib/main.js", "directories": { @@ -29,6 +28,7 @@ "react": ">=0.12" }, "devDependencies": { + "async": "~0.2.9", "envify": "~1.2.1", "grunt": "~0.4.2", "grunt-amd-wrap": "^1.0.1", @@ -58,6 +58,7 @@ "react-async": "~2.0.0", "react-router-component": "git://github.com/STRML/react-router-component#react-0.12", "requirejs": "~2.1.9", + "semver": "~2.0.7", "sinon": "^1.10.3" } } diff --git a/tools/amd/.gitignore-template b/tools/amd/.gitignore-template new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tools/release/exec-series.js b/tools/release/exec-series.js new file mode 100644 index 0000000000..b0905aa3a2 --- /dev/null +++ b/tools/release/exec-series.js @@ -0,0 +1,22 @@ +var async = require('async'), + spawnCommand = require('./spawn-command.js'); + +module.exports = function execSeries(args, cb, options) { + async.eachSeries( + args, + function(args, callback) { + console.log(args[0] + ' ' + args[1].join(' ')); + spawnCommand.apply(this, args.concat(options)) + .on('error', function(err) { + throw err; + }) + .on('exit', function(code) { + if (code) { + throw new Error('Failed executing ' + args); + } else { + callback(); + } + }); + }, + cb); +}; \ No newline at end of file diff --git a/tools/release/spawn-command.js b/tools/release/spawn-command.js new file mode 100644 index 0000000000..cfbce28635 --- /dev/null +++ b/tools/release/spawn-command.js @@ -0,0 +1,18 @@ +var spawn = require('child_process').spawn; +var win32 = process.platform === 'win32'; + +// Normalize a command across OS and spawn it +// +// - command - A String containing a command to run +// - arguments - An Array of arguments to pass the command +// +// Returns ChildProcess object (of the spawned command) +module.exports = function spawnCommand(command, args, options) { + var winCommand = win32 ? 'cmd' : command; + var winArgs = win32 ? ['/c ' + command + ' ' + args.join(' ')] : args; + + options = options || {}; + options.stdio = 'inherit'; + + return spawn(winCommand, winArgs, options); +}; \ No newline at end of file diff --git a/tools/release/tasks.js b/tools/release/tasks.js new file mode 100644 index 0000000000..35d6ef2e70 --- /dev/null +++ b/tools/release/tasks.js @@ -0,0 +1,227 @@ +var fs = require('fs'); +var async = require('async'); +var childProcess = require('child_process'); +var path = require('path'); +var semver = require('semver'); +var execSeries = require('./exec-series.js'); +var spawnCommand = require('./spawn-command.js'); + +module.exports = function(grunt) { + grunt.registerTask('release', function(type) { + var complete = this.async(); + + var filesToCopy = grunt.config.get('release-component.options.copy'); + var bowerRepo = 'git@github.com:react-bootstrap/react-bootstrap-bower.git'; + var docsRepo = 'git@github.com:react-bootstrap/react-bootstrap.github.io.git'; + + // Grunt is kind enough to change cwd to the directory the Gruntfile is in + // but double check just in case + var repoRoot = process.cwd(); + var libRoot = path.join(repoRoot, 'lib/'); + var bowerRoot = path.join(repoRoot, 'amd/'); + var docsRoot = path.join(repoRoot, 'docs/'); + var tmpBowerRepo = path.join(repoRoot, 'tmp-bower-repo'); + var tmpDocsRepo = path.join(repoRoot, 'tmp-docs-repo'); + + var version; + + async.series([ + // Ensure git repo is actually ready to release + ensureClean, + ensureFetched, + + // Clean build output + function(next) { + execSeries([ + ['rm', ['-rf', bowerRoot]], + ['rm', ['-rf', libRoot]], + ], next); + }, + + // Bump versions + function(next) { + modifyJSONSync(path.join(repoRoot, 'package.json'), function(packageJSON) { + var oldVersion = packageJSON.version; + if (!type) { + type = 'patch'; + } + if (['major', 'minor', 'patch'].indexOf(type) === -1) { + version = type; + } else { + version = semver.inc(packageJSON.version, type || 'patch'); + } + console.log('version changed from ' + oldVersion + ' to ' + version); + packageJSON.version = version; + }); + next(); + }, + + // Add and commit + function(next) { + execSeries([ + ['git', ['add', path.join(repoRoot, 'package.json')]], + ['git', ['commit', '-m', '"Release v' + version + '"']] + ], next); + }, + + // Build src + function(next) { + execSeries([ + ['grunt', ['build']] + ], next); + }, + + // Build docs + function(next) { + execSeries([ + ['rm', ['-rf', path.join(docsRoot, 'node_modules')]], + ['git', ['clean', '-dfx']], + ['npm', ['install']], + ['npm', ['run', 'build']] + ], next, { + cwd: docsRoot + }); + }, + + // Tag + function(next) { + tag('v' + version, next); + }, + + // Push + function(next) { + execSeries([ + ['git', ['push']], + ['git', ['push', '--tags']] + ], next); + }, + + // Publish to npm + function(next) { + execSeries([ + ['npm', ['publish']] + ], next); + }, + + function(next) { + ReleaseRepo(bowerRepo, bowerRoot, tmpBowerRepo, version, next); + }, + + function(next) { + ReleaseRepo(docsRepo, docsRoot, tmpDocsRepo, version, next); + }, + + ], complete); + }); +}; + +function ReleaseRepo(repo, srcFolder, tmpFolder, version, callback) { + async.series([ + // Clone repo into tmpFolder and copy built files into it + function(next) { + var commands = [ + ['rm', ['-rf', tmpFolder]], + ['git', ['clone', repo, tmpFolder]] + ]; + execSeries(commands, function() { + var additionalCommands = fs.readdirSync(tmpFolder) + .filter(function(f) { return f !== '.git'; }) + .map(function(f) { return ['rm', ['-rf', path.join(tmpFolder, f)]] }); + + additionalCommands.push(['cp', ['-R', srcFolder, tmpFolder]]); + additionalCommands.push(['mv', [path.join(tmpFolder, '.gitignore-template'), path.join(tmpFolder, '.gitignore')]]); + + execSeries(additionalCommands, next) + }); + }, + + // Add and commit in repo + function(next) { + var commands = [ + ['git', ['add', '-A', '.']], + ['git', ['commit', '-m', '"Release v' + version + '"']] + ]; + execSeries(commands, next, { + cwd: tmpFolder + }); + }, + + // Tag in repo + function(next) { + tag('v' + version, next, { + cwd: tmpFolder + }); + }, + + // Push in repo + function(next) { + execSeries([ + ['git', ['push']], + ['git', ['push', '--tags']] + ], next, { + cwd: tmpFolder + }); + }, + + // Delete repo + function(next) { + execSeries([ + ['rm', ['-rf', tmpFolder]] + ], next); + } + ], callback); +} + +function ensureClean(callback) { + childProcess.exec('git diff-index --name-only HEAD --', function(err, stdout, stderr) { + if (err) { + throw err; + } + + if (stdout.length) { + throw new Error('Git repository must be clean'); + } else { + callback(); + } + }); +} + +function tag(name, callback, options) { + spawnCommand('git', ['tag', '-a', '--message=' + name, name], options) + .on('error', function(err) { + throw err; + }) + .on('exit', function(code) { + if (code) { + throw new Error('Failed tagging ' + name + ' code: ' + code); + } else { + callback(); + } + }); +} + +function ensureFetched(callback) { + childProcess.exec('git fetch', function(err, stdout, stderr) { + if (err) { + throw err; + } + + childProcess.exec('git branch -v --no-color | grep -e "^\\*"', function(err, stdout, stderr) { + if (err) { + throw err; + } + + if (/\[behind (.*)\]/.test(stdout)) { + throw new Error('Your repo is behind by ' + RegExp.$1 + ' commits'); + } + + callback(); + }); + }); +} + +function modifyJSONSync(JSONPath, callback) { + var json = JSON.parse(fs.readFileSync(JSONPath).toString()); + callback(json); + fs.writeFileSync(JSONPath, JSON.stringify(json, null, 2)); +}