| 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 | /**
|
|---|
| 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 |
|
|---|
| 184 | globSync = 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
|
|---|
| 209 | var 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 |
|
|---|
| 239 | var 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 |
|
|---|
| 288 | FileList.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
|
|---|
| 480 | FileList.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 |
|
|---|
| 493 | FileList.verbose = true
|
|---|
| 494 |
|
|---|
| 495 | exports.FileList = FileList;
|
|---|