change-creator.js (6035B)
1 var fs = require('fs'); 2 var path = require('path'); 3 var crypto = require('crypto'); 4 /** 5 * Configuration Options: 6 * - write file location 7 * - read file location 8 */ 9 10 function checkProperty(obj, prop) { 11 return Object.prototype.hasOwnProperty.call(obj, prop); 12 } 13 14 /** 15 * Generates a 'random' hex value. 16 * More 'random' than Math.random without depending on a GUID module. 17 */ 18 function generateRandomIdentifier() { 19 return crypto.randomBytes(4).toString('hex'); 20 } 21 22 var CHANGES_DIR = path.join(process.cwd(), '.changes'); 23 24 /** 25 * A map of valid change types. 26 * Can be referenced outside of this module. 27 */ 28 var VALID_TYPES = Object.create(null); 29 VALID_TYPES['bugfix'] = true; 30 VALID_TYPES['feature'] = true; 31 32 33 /** 34 * Handles creating a change log entry JSON file. 35 */ 36 function ChangeCreator(config) { 37 this._config = config || {}; 38 this._type = ''; 39 this._category = ''; 40 this._description = ''; 41 } 42 43 ChangeCreator.prototype = { 44 getChangeType: function getChangeType() { 45 return this._type; 46 }, 47 48 setChangeType: function setChangeType(type) { 49 this._type = type; 50 }, 51 52 getChangeCategory: function getChangeCategory() { 53 return this._category; 54 }, 55 56 setChangeCategory: function setChangeCategory(category) { 57 this._category = category; 58 }, 59 60 getChangeDescription: function getChangeDescription() { 61 return this._description; 62 }, 63 64 setChangeDescription: function setChangeDescription(description) { 65 this._description = description; 66 }, 67 68 /** 69 * Validates the entire change entry. 70 */ 71 validateChange: function validateChange() { 72 var type = this.getChangeType(); 73 var category = this.getChangeCategory(); 74 var description = this.getChangeDescription(); 75 76 var missingFields = []; 77 this.validateChangeType(type); 78 this.validateChangeCategory(category); 79 this.validateChangeDescription(description); 80 81 return this; 82 }, 83 84 /** 85 * Validates a change entry type. 86 */ 87 validateChangeType: function validateChangeType(type) { 88 var type = type || this._type; 89 90 if (!type) { 91 throw new Error('ValidationError: Missing `type` field.'); 92 } 93 94 if (VALID_TYPES[type]) { 95 return this; 96 } 97 98 var validTypes = Object.keys(VALID_TYPES).join(','); 99 100 throw new Error('ValidationError: `type` set as "' + type + '" but must be one of [' + validTypes + '].'); 101 }, 102 103 /** 104 * Validates a change entry category. 105 */ 106 validateChangeCategory: function validateChangeCategory(category) { 107 var category = category || this._category; 108 109 if (!category) { 110 throw new Error('ValidationError: Missing `category` field.'); 111 } 112 113 return this; 114 }, 115 116 /** 117 * Validates a change entry description. 118 */ 119 validateChangeDescription: function validateChangeDescription(description) { 120 var description = description || this._description; 121 122 if (!description) { 123 throw new Error('ValidationError: Missing `description` field.'); 124 } 125 126 return this; 127 }, 128 129 /** 130 * Creates the output directory if it doesn't exist. 131 */ 132 createOutputDirectory: function createOutputDirectory(outPath) { 133 var pathObj = path.parse(outPath); 134 var sep = path.sep; 135 var directoryStructure = pathObj.dir.split(sep) || []; 136 for (var i = 0; i < directoryStructure.length; i++) { 137 var pathToCheck = directoryStructure.slice(0, i + 1).join(sep); 138 if (!pathToCheck) { 139 continue; 140 } 141 try { 142 var stats = fs.statSync(pathToCheck); 143 } catch (err) { 144 if (err.code === 'ENOENT') { 145 // Directory doesn't exist, so create it 146 fs.mkdirSync(pathToCheck); 147 } else { 148 throw err; 149 } 150 } 151 } 152 return this; 153 }, 154 155 /** 156 * Returns a path to the future change entry file. 157 */ 158 determineWriteLocation: function determineWriteLocation() { 159 /* Order for determining write location: 160 1) Check configuration for `outFile` location. 161 2) Check configuration for `inFile` location. 162 3) Create a new file using default location. 163 */ 164 var config = this._config || {}; 165 if (checkProperty(config, 'outFile') && config['outFile']) { 166 return config['outFile']; 167 } 168 if (checkProperty(config, 'inFile') && config['inFile']) { 169 return config['inFile']; 170 } 171 // Determine default location 172 var newFileName = this._type + '-' + this._category + '-' + generateRandomIdentifier() + '.json'; 173 return path.join(process.cwd(), '.changes', 'next-release', newFileName); 174 }, 175 176 /** 177 * Writes a change entry as a JSON file. 178 */ 179 writeChanges: function writeChanges(callback) { 180 var hasCallback = typeof callback === 'function'; 181 var fileLocation = this.determineWriteLocation(); 182 183 try { 184 // Will throw an error if the change is not valid 185 this.validateChange().createOutputDirectory(fileLocation); 186 var change = { 187 type: this.getChangeType(), 188 category: this.getChangeCategory(), 189 description: this.getChangeDescription() 190 } 191 fs.writeFileSync(fileLocation, JSON.stringify(change, null, 2)); 192 var data = { 193 file: fileLocation 194 }; 195 if (hasCallback) { 196 return callback(null, data); 197 } else { 198 return data; 199 } 200 } catch (err) { 201 if (hasCallback) { 202 return callback(err, null); 203 } else { 204 throw err; 205 } 206 } 207 } 208 } 209 210 module.exports = { 211 ChangeCreator: ChangeCreator, 212 VALID_TYPES: VALID_TYPES 213 };