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