1/* 2 * Jake JavaScript build tool 3 * Copyright 2112 Matthew Eernisse (mde@fleegix.org) 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 * 17*/ 18 19let fs = require('fs'); 20let path = require('path'); 21let exec = require('child_process').execSync; 22let FileList = require('filelist').FileList; 23 24let PublishTask = function () { 25 let args = Array.prototype.slice.call(arguments).filter(function (item) { 26 return typeof item != 'undefined'; 27 }); 28 let arg; 29 let opts = {}; 30 let definition; 31 let prereqs = []; 32 let createDef = function (arg) { 33 return function () { 34 this.packageFiles.include(arg); 35 }; 36 }; 37 38 this.name = args.shift(); 39 40 // Old API, just name + list of files 41 if (args.length == 1 && (Array.isArray(args[0]) || typeof args[0] == 'string')) { 42 definition = createDef(args.pop()); 43 } 44 // Current API, name + [prereqs] + [opts] + definition 45 else { 46 while ((arg = args.pop())) { 47 // Definition func 48 if (typeof arg == 'function') { 49 definition = arg; 50 } 51 // Prereqs 52 else if (Array.isArray(arg) || typeof arg == 'string') { 53 prereqs = arg; 54 } 55 // Opts 56 else { 57 opts = arg; 58 } 59 } 60 } 61 62 this.prereqs = prereqs; 63 this.packageFiles = new FileList(); 64 this.publishCmd = opts.publishCmd || 'npm publish %filename'; 65 this.publishMessage = opts.publishMessage || 'BOOM! Published.'; 66 this.gitCmd = opts.gitCmd || 'git'; 67 this.versionFiles = opts.versionFiles || ['package.json']; 68 this.scheduleDelay = 5000; 69 70 // Override utility funcs for testing 71 this._ensureRepoClean = function (stdout) { 72 if (stdout.length) { 73 fail(new Error('Git repository is not clean.')); 74 } 75 }; 76 this._getCurrentBranch = function (stdout) { 77 return String(stdout).trim(); 78 }; 79 80 if (typeof definition == 'function') { 81 definition.call(this); 82 } 83 this.define(); 84}; 85 86 87PublishTask.prototype = new (function () { 88 89 let _currentBranch = null; 90 91 let getPackage = function () { 92 let pkg = JSON.parse(fs.readFileSync(path.join(process.cwd(), 93 '/package.json')).toString()); 94 return pkg; 95 }; 96 let getPackageVersionNumber = function () { 97 return getPackage().version; 98 }; 99 100 this.define = function () { 101 let self = this; 102 103 namespace('publish', function () { 104 task('fetchTags', function () { 105 // Make sure local tags are up to date 106 exec(self.gitCmd + ' fetch --tags'); 107 console.log('Fetched remote tags.'); 108 }); 109 110 task('getCurrentBranch', function () { 111 // Figure out what branch to push to 112 let stdout = exec(self.gitCmd + ' symbolic-ref --short HEAD').toString(); 113 if (!stdout) { 114 throw new Error('No current Git branch found'); 115 } 116 _currentBranch = self._getCurrentBranch(stdout); 117 console.log('On branch ' + _currentBranch); 118 }); 119 120 task('ensureClean', function () { 121 // Only bump, push, and tag if the Git repo is clean 122 let stdout = exec(self.gitCmd + ' status --porcelain --untracked-files=no').toString(); 123 // Throw if there's output 124 self._ensureRepoClean(stdout); 125 }); 126 127 task('updateVersionFiles', function () { 128 let pkg; 129 let version; 130 let arr; 131 let patch; 132 133 // Grab the current version-string 134 pkg = getPackage(); 135 version = pkg.version; 136 // Increment the patch-number for the version 137 arr = version.split('.'); 138 patch = parseInt(arr.pop(), 10) + 1; 139 arr.push(patch); 140 version = arr.join('.'); 141 142 // Update package.json or other files with the new version-info 143 self.versionFiles.forEach(function (file) { 144 let p = path.join(process.cwd(), file); 145 let data = JSON.parse(fs.readFileSync(p).toString()); 146 data.version = version; 147 fs.writeFileSync(p, JSON.stringify(data, true, 2) + '\n'); 148 }); 149 // Return the version string so that listeners for the 'complete' event 150 // for this task can use it (e.g., to update other files before pushing 151 // to Git) 152 return version; 153 }); 154 155 task('pushVersion', ['ensureClean', 'updateVersionFiles'], function () { 156 let version = getPackageVersionNumber(); 157 let message = 'Version ' + version; 158 let cmds = [ 159 self.gitCmd + ' commit -a -m "' + message + '"', 160 self.gitCmd + ' push origin ' + _currentBranch, 161 self.gitCmd + ' tag -a v' + version + ' -m "' + message + '"', 162 self.gitCmd + ' push --tags' 163 ]; 164 cmds.forEach((cmd) => { 165 exec(cmd); 166 }); 167 version = getPackageVersionNumber(); 168 console.log('Bumped version number to v' + version + '.'); 169 }); 170 171 let defineTask = task('definePackage', function () { 172 let version = getPackageVersionNumber(); 173 new jake.PackageTask(self.name, 'v' + version, self.prereqs, function () { 174 // Replace the PackageTask's FileList with the PublishTask's FileList 175 this.packageFiles = self.packageFiles; 176 this.needTarGz = true; // Default to tar.gz 177 // If any of the need<CompressionFormat> or archive opts are set 178 // proxy them to the PackageTask 179 for (let p in this) { 180 if (p.indexOf('need') === 0 || p.indexOf('archive') === 0) { 181 if (typeof self[p] != 'undefined') { 182 this[p] = self[p]; 183 } 184 } 185 } 186 }); 187 }); 188 defineTask._internal = true; 189 190 task('package', function () { 191 let definePack = jake.Task['publish:definePackage']; 192 let pack = jake.Task['package']; 193 let version = getPackageVersionNumber(); 194 195 // May have already been run 196 if (definePack.taskStatus == jake.Task.runStatuses.DONE) { 197 definePack.reenable(true); 198 } 199 definePack.execute(); 200 definePack.on('complete', function () { 201 pack.invoke(); 202 console.log('Created package for ' + self.name + ' v' + version); 203 }); 204 }); 205 206 task('publish', function () { 207 return new Promise((resolve) => { 208 let version = getPackageVersionNumber(); 209 let filename; 210 let cmd; 211 212 console.log('Publishing ' + self.name + ' v' + version); 213 214 if (typeof self.createPublishCommand == 'function') { 215 cmd = self.createPublishCommand(version); 216 } 217 else { 218 filename = './pkg/' + self.name + '-v' + version + '.tar.gz'; 219 cmd = self.publishCmd.replace(/%filename/gi, filename); 220 } 221 222 if (typeof cmd == 'function') { 223 cmd(function (err) { 224 if (err) { 225 throw err; 226 } 227 console.log(self.publishMessage); 228 resolve(); 229 }); 230 } 231 else { 232 // Hackity hack -- NPM publish sometimes returns errror like: 233 // Error sending version data\nnpm ERR! 234 // Error: forbidden 0.2.4 is modified, should match modified time 235 setTimeout(function () { 236 let stdout = exec(cmd).toString() || ''; 237 stdout = stdout.trim(); 238 if (stdout) { 239 console.log(stdout); 240 } 241 console.log(self.publishMessage); 242 resolve(); 243 }, self.scheduleDelay); 244 } 245 }); 246 }); 247 248 task('cleanup', function () { 249 return new Promise((resolve) => { 250 let clobber = jake.Task.clobber; 251 clobber.reenable(true); 252 clobber.on('complete', function () { 253 console.log('Cleaned up package'); 254 resolve(); 255 }); 256 clobber.invoke(); 257 }); 258 }); 259 260 }); 261 262 let prefixNs = function (item) { 263 return 'publish:' + item; 264 }; 265 266 // Create aliases in the default namespace 267 desc('Create a new version and release.'); 268 task('publish', self.prereqs.concat(['version', 'release'] 269 .map(prefixNs))); 270 271 desc('Release the existing version.'); 272 task('publishExisting', self.prereqs.concat(['release'] 273 .map(prefixNs))); 274 275 task('version', ['fetchTags', 'getCurrentBranch', 'pushVersion'] 276 .map(prefixNs)); 277 278 task('release', ['package', 'publish', 'cleanup'] 279 .map(prefixNs)); 280 281 // Invoke proactively so there will be a callable 'package' task 282 // which can be used apart from 'publish' 283 jake.Task['publish:definePackage'].invoke(); 284 }; 285 286})(); 287 288jake.PublishTask = PublishTask; 289exports.PublishTask = PublishTask; 290 291