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*/ 18var fs = require('fs') 19, path = require('path') 20, minimatch = require('minimatch') 21, escapeRegExpChars 22, merge 23, basedir 24, _readDir 25, readdirR 26, globSync; 27var hasOwnProperty = Object.prototype.hasOwnProperty; 28var hasOwn = function (obj, key) { return hasOwnProperty.apply(obj, [key]); }; 29 30 /** 31 @name escapeRegExpChars 32 @function 33 @return {String} A string of escaped characters 34 @description Escapes regex control-characters in strings 35 used to build regexes dynamically 36 @param {String} string The string of chars to escape 37 */ 38 escapeRegExpChars = (function () { 39 var specials = [ '^', '$', '/', '.', '*', '+', '?', '|', '(', ')', 40 '[', ']', '{', '}', '\\' ]; 41 var sRE = new RegExp('(\\' + specials.join('|\\') + ')', 'g'); 42 return function (string) { 43 var str = string || ''; 44 str = String(str); 45 return str.replace(sRE, '\\$1'); 46 }; 47 })(); 48 49 /** 50 @name merge 51 @function 52 @return {Object} Returns the merged object 53 @description Merge merges `otherObject` into `object` and takes care of deep 54 merging of objects 55 @param {Object} object Object to merge into 56 @param {Object} otherObject Object to read from 57 */ 58 merge = function (object, otherObject) { 59 var obj = object || {} 60 , otherObj = otherObject || {} 61 , key, value; 62 63 for (key in otherObj) { 64 65 if (!hasOwn(otherObj, key)) { 66 continue; 67 } 68 if (key === '__proto__' || key === 'constructor') { 69 continue; 70 } 71 72 value = otherObj[key]; 73 // Check if a value is an Object, if so recursively add it's key/values 74 if (typeof value === 'object' && !(value instanceof Array)) { 75 // Update value of object to the one from otherObj 76 obj[key] = merge(obj[key], value); 77 } 78 // Value is anything other than an Object, so just add it 79 else { 80 obj[key] = value; 81 } 82 } 83 84 return obj; 85 }; 86 /** 87 Given a patern, return the base directory of it (ie. the folder 88 that will contain all the files matching the path). 89 eg. file.basedir('/test/**') => '/test/' 90 Path ending by '/' are considerd as folder while other are considerd 91 as files, eg.: 92 file.basedir('/test/a/') => '/test/a' 93 file.basedir('/test/a') => '/test' 94 The returned path always end with a '/' so we have: 95 file.basedir(file.basedir(x)) == file.basedir(x) 96 */ 97 basedir = function (pathParam) { 98 var bd = '' 99 , parts 100 , part 101 , pos = 0 102 , p = pathParam || ''; 103 104 // If the path has a leading asterisk, basedir is the current dir 105 if (p.indexOf('*') == 0 || p.indexOf('**') == 0) { 106 return '.'; 107 } 108 109 // always consider .. at the end as a folder and not a filename 110 if (/(?:^|\/|\\)\.\.$/.test(p.slice(-3))) { 111 p += '/'; 112 } 113 114 parts = p.split(/\\|\//); 115 for (var i = 0, l = parts.length - 1; i < l; i++) { 116 part = parts[i]; 117 if (part.indexOf('*') > -1 || part.indexOf('**') > -1) { 118 break; 119 } 120 pos += part.length + 1; 121 bd += part + p[pos - 1]; 122 } 123 if (!bd) { 124 bd = '.'; 125 } 126 // Strip trailing slashes 127 if (!(bd == '\\' || bd == '/')) { 128 bd = bd.replace(/\\$|\/$/, ''); 129 } 130 return bd; 131 132 }; 133 134 // Return the contents of a given directory 135 _readDir = function (dirPath) { 136 var dir = path.normalize(dirPath) 137 , paths = [] 138 , ret = [dir] 139 , msg; 140 141 try { 142 paths = fs.readdirSync(dir); 143 } 144 catch (e) { 145 msg = 'Could not read path ' + dir + '\n'; 146 if (e.stack) { 147 msg += e.stack; 148 } 149 throw new Error(msg); 150 } 151 152 paths.forEach(function (p) { 153 var curr = path.join(dir, p); 154 var stat = fs.statSync(curr); 155 if (stat.isDirectory()) { 156 ret = ret.concat(_readDir(curr)); 157 } 158 else { 159 ret.push(curr); 160 } 161 }); 162 163 return ret; 164 }; 165 166 /** 167 @name file#readdirR 168 @function 169 @return {Array} Returns the contents as an Array, can be configured via opts.format 170 @description Reads the given directory returning it's contents 171 @param {String} dir The directory to read 172 @param {Object} opts Options to use 173 @param {String} [opts.format] Set the format to return(Default: Array) 174 */ 175 readdirR = function (dir, opts) { 176 var options = opts || {} 177 , format = options.format || 'array' 178 , ret; 179 ret = _readDir(dir); 180 return format == 'string' ? ret.join('\n') : ret; 181 }; 182 183 184globSync = function (pat, opts) { 185 var dirname = basedir(pat) 186 , files 187 , matches; 188 189 try { 190 files = readdirR(dirname).map(function(file){ 191 return file.replace(/\\/g, '/'); 192 }); 193 } 194 // Bail if path doesn't exist -- assume no files 195 catch(e) { 196 if (FileList.verbose) console.error(e.message); 197 } 198 199 if (files) { 200 pat = path.normalize(pat); 201 matches = minimatch.match(files, pat, opts || {}); 202 } 203 return matches || []; 204}; 205 206// Constants 207// --------------- 208// List of all the builtin Array methods we want to override 209var ARRAY_METHODS = Object.getOwnPropertyNames(Array.prototype) 210// Array methods that return a copy instead of affecting the original 211 , SPECIAL_RETURN = { 212 'concat': true 213 , 'slice': true 214 , 'filter': true 215 , 'map': true 216 } 217// Default file-patterns we want to ignore 218 , DEFAULT_IGNORE_PATTERNS = [ 219 /(^|[\/\\])CVS([\/\\]|$)/ 220 , /(^|[\/\\])\.svn([\/\\]|$)/ 221 , /(^|[\/\\])\.git([\/\\]|$)/ 222 , /\.bak$/ 223 , /~$/ 224 ] 225// Ignore core files 226 , DEFAULT_IGNORE_FUNCS = [ 227 function (name) { 228 var isDir = false 229 , stats; 230 try { 231 stats = fs.statSync(name); 232 isDir = stats.isDirectory(); 233 } 234 catch(e) {} 235 return (/(^|[\/\\])core$/).test(name) && !isDir; 236 } 237 ]; 238 239var FileList = function () { 240 var self = this 241 , wrap; 242 243 // List of glob-patterns or specific filenames 244 this.pendingAdd = []; 245 // Switched to false after lazy-eval of files 246 this.pending = true; 247 // Used to calculate exclusions from the list of files 248 this.excludes = { 249 pats: DEFAULT_IGNORE_PATTERNS.slice() 250 , funcs: DEFAULT_IGNORE_FUNCS.slice() 251 , regex: null 252 }; 253 this.items = []; 254 255 // Wrap the array methods with the delegates 256 wrap = function (prop) { 257 var arr; 258 self[prop] = function () { 259 if (self.pending) { 260 self.resolve(); 261 } 262 if (typeof self.items[prop] == 'function') { 263 // Special method that return a copy 264 if (SPECIAL_RETURN[prop]) { 265 arr = self.items[prop].apply(self.items, arguments); 266 return FileList.clone(self, arr); 267 } 268 else { 269 return self.items[prop].apply(self.items, arguments); 270 } 271 } 272 else { 273 return self.items[prop]; 274 } 275 }; 276 }; 277 for (var i = 0, ii = ARRAY_METHODS.length; i < ii; i++) { 278 wrap(ARRAY_METHODS[i]); 279 } 280 281 // Include whatever files got passed to the constructor 282 this.include.apply(this, arguments); 283 284 // Fix constructor linkage 285 this.constructor = FileList; 286}; 287 288FileList.prototype = new (function () { 289 var globPattern = /[*?\[\{]/; 290 291 var _addMatching = function (item) { 292 var matches = globSync(item.path, item.options); 293 this.items = this.items.concat(matches); 294 } 295 296 , _resolveAdd = function (item) { 297 if (globPattern.test(item.path)) { 298 _addMatching.call(this, item); 299 } 300 else { 301 this.push(item.path); 302 } 303 } 304 305 , _calculateExcludeRe = function () { 306 var pats = this.excludes.pats 307 , pat 308 , excl = [] 309 , matches = []; 310 311 for (var i = 0, ii = pats.length; i < ii; i++) { 312 pat = pats[i]; 313 if (typeof pat == 'string') { 314 // Glob, look up files 315 if (/[*?]/.test(pat)) { 316 matches = globSync(pat); 317 matches = matches.map(function (m) { 318 return escapeRegExpChars(m); 319 }); 320 excl = excl.concat(matches); 321 } 322 // String for regex 323 else { 324 excl.push(escapeRegExpChars(pat)); 325 } 326 } 327 // Regex, grab the string-representation 328 else if (pat instanceof RegExp) { 329 excl.push(pat.toString().replace(/^\/|\/$/g, '')); 330 } 331 } 332 if (excl.length) { 333 this.excludes.regex = new RegExp('(' + excl.join(')|(') + ')'); 334 } 335 else { 336 this.excludes.regex = /^$/; 337 } 338 } 339 340 , _resolveExclude = function () { 341 var self = this; 342 _calculateExcludeRe.call(this); 343 // No `reject` method, so use reverse-filter 344 this.items = this.items.filter(function (name) { 345 return !self.shouldExclude(name); 346 }); 347 }; 348 349 /** 350 * Includes file-patterns in the FileList. Should be called with one or more 351 * pattern for finding file to include in the list. Arguments should be strings 352 * for either a glob-pattern or a specific file-name, or an array of them 353 */ 354 this.include = function () { 355 var args = Array.prototype.slice.call(arguments) 356 , arg 357 , includes = { items: [], options: {} }; 358 359 for (var i = 0, ilen = args.length; i < ilen; i++) { 360 arg = args[i]; 361 362 if (typeof arg === 'object' && !Array.isArray(arg)) { 363 merge(includes.options, arg); 364 } else { 365 includes.items = includes.items.concat(arg).filter(function (item) { 366 return !!item; 367 }); 368 } 369 } 370 371 var items = includes.items.map(function(item) { 372 return { path: item, options: includes.options }; 373 }); 374 375 this.pendingAdd = this.pendingAdd.concat(items); 376 377 return this; 378 }; 379 380 /** 381 * Indicates whether a particular file would be filtered out by the current 382 * exclusion rules for this FileList. 383 * @param {String} name The filename to check 384 * @return {Boolean} Whether or not the file should be excluded 385 */ 386 this.shouldExclude = function (name) { 387 if (!this.excludes.regex) { 388 _calculateExcludeRe.call(this); 389 } 390 var excl = this.excludes; 391 return excl.regex.test(name) || excl.funcs.some(function (f) { 392 return !!f(name); 393 }); 394 }; 395 396 /** 397 * Excludes file-patterns from the FileList. Should be called with one or more 398 * pattern for finding file to include in the list. Arguments can be: 399 * 1. Strings for either a glob-pattern or a specific file-name 400 * 2. Regular expression literals 401 * 3. Functions to be run on the filename that return a true/false 402 */ 403 this.exclude = function () { 404 var args = Array.isArray(arguments[0]) ? arguments[0] : arguments 405 , arg; 406 for (var i = 0, ii = args.length; i < ii; i++) { 407 arg = args[i]; 408 if (typeof arg == 'function' && !(arg instanceof RegExp)) { 409 this.excludes.funcs.push(arg); 410 } 411 else { 412 this.excludes.pats.push(arg); 413 } 414 } 415 if (!this.pending) { 416 _resolveExclude.call(this); 417 } 418 return this; 419 }; 420 421 /** 422 * Populates the FileList from the include/exclude rules with a list of 423 * actual files 424 */ 425 this.resolve = function () { 426 var item 427 , uniqueFunc = function (p, c) { 428 if (p.indexOf(c) < 0) { 429 p.push(c); 430 } 431 return p; 432 }; 433 if (this.pending) { 434 this.pending = false; 435 while ((item = this.pendingAdd.shift())) { 436 _resolveAdd.call(this, item); 437 } 438 // Reduce to a unique list 439 this.items = this.items.reduce(uniqueFunc, []); 440 // Remove exclusions 441 _resolveExclude.call(this); 442 } 443 return this; 444 }; 445 446 /** 447 * Convert to a plain-jane array 448 */ 449 this.toArray = function () { 450 // Call slice to ensure lazy-resolution before slicing items 451 var ret = this.slice().items.slice(); 452 return ret; 453 }; 454 455 /** 456 * Clear any pending items -- only useful before 457 * calling `resolve` 458 */ 459 this.clearInclusions = function () { 460 this.pendingAdd = []; 461 return this; 462 }; 463 464 /** 465 * Clear any current exclusion rules 466 */ 467 this.clearExclusions = function () { 468 this.excludes = { 469 pats: [] 470 , funcs: [] 471 , regex: null 472 }; 473 return this; 474 }; 475 476})(); 477 478// Static method, used to create copy returned by special 479// array methods 480FileList.clone = function (list, items) { 481 var clone = new FileList(); 482 if (items) { 483 clone.items = items; 484 } 485 clone.pendingAdd = list.pendingAdd; 486 clone.pending = list.pending; 487 for (var p in list.excludes) { 488 clone.excludes[p] = list.excludes[p]; 489 } 490 return clone; 491}; 492 493FileList.verbose = true 494 495exports.FileList = FileList; 496