You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

496 lines
13KB

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