blob: a7e979a561a0bfc242f93396611eced07477bcfe [file] [log] [blame]
Tim van der Lippe2c891972021-07-29 16:22:50 +01001'use strict';
2
3Object.defineProperty(exports, '__esModule', { value: true });
4
5function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
6
7var path = _interopDefault(require('path'));
8var minimatch = _interopDefault(require('minimatch'));
9var createDebug = _interopDefault(require('debug'));
10var objectSchema = require('@humanwhocodes/object-schema');
11
12/**
13 * @fileoverview ConfigSchema
14 * @author Nicholas C. Zakas
15 */
16
17//------------------------------------------------------------------------------
18// Helpers
19//------------------------------------------------------------------------------
20
21/**
22 * Assets that a given value is an array.
23 * @param {*} value The value to check.
24 * @returns {void}
25 * @throws {TypeError} When the value is not an array.
26 */
27function assertIsArray(value) {
28 if (!Array.isArray(value)) {
29 throw new TypeError('Expected value to be an array.');
30 }
31}
32
33/**
34 * Assets that a given value is an array containing only strings and functions.
35 * @param {*} value The value to check.
36 * @returns {void}
37 * @throws {TypeError} When the value is not an array of strings and functions.
38 */
39function assertIsArrayOfStringsAndFunctions(value, name) {
40 assertIsArray(value);
41
42 if (value.some(item => typeof item !== 'string' && typeof item !== 'function')) {
43 throw new TypeError('Expected array to only contain strings.');
44 }
45}
46
47//------------------------------------------------------------------------------
48// Exports
49//------------------------------------------------------------------------------
50
51/**
52 * The base schema that every ConfigArray uses.
53 * @type Object
54 */
55const baseSchema = Object.freeze({
56 name: {
57 required: false,
58 merge() {
59 return undefined;
60 },
61 validate(value) {
62 if (typeof value !== 'string') {
63 throw new TypeError('Property must be a string.');
64 }
65 }
66 },
67 files: {
68 required: false,
69 merge() {
70 return undefined;
71 },
72 validate(value) {
73
74 // first check if it's an array
75 assertIsArray(value);
76
77 // then check each member
78 value.forEach(item => {
79 if (Array.isArray(item)) {
80 assertIsArrayOfStringsAndFunctions(item);
81 } else if (typeof item !== 'string' && typeof item !== 'function') {
82 throw new TypeError('Items must be a string, a function, or an array of strings and functions.');
83 }
84 });
85
86 }
87 },
88 ignores: {
89 required: false,
90 merge() {
91 return undefined;
92 },
93 validate: assertIsArrayOfStringsAndFunctions
94 }
95});
96
97/**
98 * @fileoverview ConfigArray
99 * @author Nicholas C. Zakas
100 */
101
102//------------------------------------------------------------------------------
103// Helpers
104//------------------------------------------------------------------------------
105
106const debug = createDebug('@hwc/config-array');
107
108const MINIMATCH_OPTIONS = {
109 matchBase: true
110};
111
Tim van der Lippe0ceb4652022-01-06 14:23:36 +0100112const CONFIG_TYPES = new Set(['array', 'function']);
113
Tim van der Lippe2c891972021-07-29 16:22:50 +0100114/**
115 * Shorthand for checking if a value is a string.
116 * @param {any} value The value to check.
117 * @returns {boolean} True if a string, false if not.
118 */
119function isString(value) {
120 return typeof value === 'string';
121}
122
123/**
124 * Normalizes a `ConfigArray` by flattening it and executing any functions
125 * that are found inside.
126 * @param {Array} items The items in a `ConfigArray`.
127 * @param {Object} context The context object to pass into any function
128 * found.
Tim van der Lippe0ceb4652022-01-06 14:23:36 +0100129 * @param {Array<string>} extraConfigTypes The config types to check.
Tim van der Lippe0fb47802021-11-08 16:23:10 +0000130 * @returns {Promise<Array>} A flattened array containing only config objects.
Tim van der Lippe2c891972021-07-29 16:22:50 +0100131 * @throws {TypeError} When a config function returns a function.
132 */
Tim van der Lippe0ceb4652022-01-06 14:23:36 +0100133async function normalize(items, context, extraConfigTypes) {
134
135 const allowFunctions = extraConfigTypes.includes('function');
136 const allowArrays = extraConfigTypes.includes('array');
Tim van der Lippe2c891972021-07-29 16:22:50 +0100137
Tim van der Lippe0fb47802021-11-08 16:23:10 +0000138 async function *flatTraverse(array) {
139 for (let item of array) {
140 if (typeof item === 'function') {
Tim van der Lippe0ceb4652022-01-06 14:23:36 +0100141 if (!allowFunctions) {
142 throw new TypeError('Unexpected function.');
143 }
144
Tim van der Lippe0fb47802021-11-08 16:23:10 +0000145 item = item(context);
146 if (item.then) {
147 item = await item;
148 }
149 }
150
151 if (Array.isArray(item)) {
Tim van der Lippe0ceb4652022-01-06 14:23:36 +0100152 if (!allowArrays) {
153 throw new TypeError('Unexpected array.');
154 }
Tim van der Lippe0fb47802021-11-08 16:23:10 +0000155 yield * flatTraverse(item);
156 } else if (typeof item === 'function') {
157 throw new TypeError('A config function can only return an object or array.');
158 } else {
159 yield item;
160 }
161 }
162 }
163
164 /*
165 * Async iterables cannot be used with the spread operator, so we need to manually
166 * create the array to return.
167 */
168 const asyncIterable = await flatTraverse(items);
169 const configs = [];
170
171 for await (const config of asyncIterable) {
172 configs.push(config);
173 }
174
175 return configs;
176}
177
178/**
179 * Normalizes a `ConfigArray` by flattening it and executing any functions
180 * that are found inside.
181 * @param {Array} items The items in a `ConfigArray`.
182 * @param {Object} context The context object to pass into any function
183 * found.
Tim van der Lippe0ceb4652022-01-06 14:23:36 +0100184 * @param {Array<string>} extraConfigTypes The config types to check.
Tim van der Lippe0fb47802021-11-08 16:23:10 +0000185 * @returns {Array} A flattened array containing only config objects.
186 * @throws {TypeError} When a config function returns a function.
187 */
Tim van der Lippe0ceb4652022-01-06 14:23:36 +0100188function normalizeSync(items, context, extraConfigTypes) {
189
190 const allowFunctions = extraConfigTypes.includes('function');
191 const allowArrays = extraConfigTypes.includes('array');
Tim van der Lippe2c891972021-07-29 16:22:50 +0100192
193 function *flatTraverse(array) {
194 for (let item of array) {
195 if (typeof item === 'function') {
Tim van der Lippe0ceb4652022-01-06 14:23:36 +0100196
197 if (!allowFunctions) {
198 throw new TypeError('Unexpected function.');
199 }
200
Tim van der Lippe2c891972021-07-29 16:22:50 +0100201 item = item(context);
Tim van der Lippe0fb47802021-11-08 16:23:10 +0000202 if (item.then) {
203 throw new TypeError('Async config functions are not supported.');
204 }
Tim van der Lippe2c891972021-07-29 16:22:50 +0100205 }
206
207 if (Array.isArray(item)) {
Tim van der Lippe0ceb4652022-01-06 14:23:36 +0100208
209 if (!allowArrays) {
210 throw new TypeError('Unexpected array.');
211 }
212
Tim van der Lippe2c891972021-07-29 16:22:50 +0100213 yield * flatTraverse(item);
214 } else if (typeof item === 'function') {
215 throw new TypeError('A config function can only return an object or array.');
216 } else {
217 yield item;
218 }
219 }
220 }
221
222 return [...flatTraverse(items)];
223}
224
225/**
Tim van der Lippe0ceb4652022-01-06 14:23:36 +0100226 * Determines if a given file path should be ignored based on the given
227 * matcher.
228 * @param {string|() => boolean} matcher The pattern to match.
229 * @param {string} filePath The absolute path of the file to check.
230 * @param {string} relativeFilePath The relative path of the file to check.
231 * @returns {boolean} True if the path should be ignored and false if not.
232 */
233function shouldIgnoreFilePath(matcher, filePath, relativeFilePath) {
234 if (typeof matcher === 'function') {
235 return matcher(filePath);
236 }
237
238 return minimatch(relativeFilePath, matcher, MINIMATCH_OPTIONS);
239}
240
241/**
Tim van der Lippe2c891972021-07-29 16:22:50 +0100242 * Determines if a given file path is matched by a config. If the config
243 * has no `files` field, then it matches; otherwise, if a `files` field
244 * is present then we match the globs in `files` and exclude any globs in
245 * `ignores`.
246 * @param {string} filePath The absolute file path to check.
247 * @param {Object} config The config object to check.
248 * @returns {boolean} True if the file path is matched by the config,
249 * false if not.
250 */
251function pathMatches(filePath, basePath, config) {
252
Tim van der Lippe0ceb4652022-01-06 14:23:36 +0100253 // a config without `files` field always match
Tim van der Lippe2c891972021-07-29 16:22:50 +0100254 if (!config.files) {
255 return true;
256 }
Tim van der Lippe0ceb4652022-01-06 14:23:36 +0100257
258 /*
259 * For both files and ignores, functions are passed the absolute
260 * file path while strings are compared against the relative
261 * file path.
262 */
263 const relativeFilePath = path.relative(basePath, filePath);
Tim van der Lippe2c891972021-07-29 16:22:50 +0100264
265 // if files isn't an array, throw an error
266 if (!Array.isArray(config.files) || config.files.length === 0) {
267 throw new TypeError('The files key must be a non-empty array.');
268 }
269
Tim van der Lippe2c891972021-07-29 16:22:50 +0100270 // match both strings and functions
271 const match = pattern => {
Tim van der Lippe0ceb4652022-01-06 14:23:36 +0100272
Tim van der Lippe2c891972021-07-29 16:22:50 +0100273 if (isString(pattern)) {
274 return minimatch(relativeFilePath, pattern, MINIMATCH_OPTIONS);
275 }
276
277 if (typeof pattern === 'function') {
278 return pattern(filePath);
279 }
Tim van der Lippe0ceb4652022-01-06 14:23:36 +0100280
281 throw new TypeError(`Unexpected matcher type ${pattern}.`);
282 };
283
284 const isFilePathIgnored = matcher => {
285 return shouldIgnoreFilePath(matcher, filePath, relativeFilePath);
Tim van der Lippe2c891972021-07-29 16:22:50 +0100286 };
287
288 // check for all matches to config.files
Tim van der Lippe0ceb4652022-01-06 14:23:36 +0100289 let filePathMatchesPattern = config.files.some(pattern => {
Tim van der Lippe2c891972021-07-29 16:22:50 +0100290 if (Array.isArray(pattern)) {
291 return pattern.every(match);
292 }
293
294 return match(pattern);
295 });
296
297 /*
298 * If the file path matches the config.files patterns, then check to see
299 * if there are any files to ignore.
300 */
Tim van der Lippe0ceb4652022-01-06 14:23:36 +0100301 if (filePathMatchesPattern && config.ignores) {
302 filePathMatchesPattern = !config.ignores.some(isFilePathIgnored);
Tim van der Lippe2c891972021-07-29 16:22:50 +0100303 }
304
Tim van der Lippe0ceb4652022-01-06 14:23:36 +0100305 return filePathMatchesPattern;
Tim van der Lippe2c891972021-07-29 16:22:50 +0100306}
307
308/**
309 * Ensures that a ConfigArray has been normalized.
310 * @param {ConfigArray} configArray The ConfigArray to check.
311 * @returns {void}
312 * @throws {Error} When the `ConfigArray` is not normalized.
313 */
314function assertNormalized(configArray) {
315 // TODO: Throw more verbose error
316 if (!configArray.isNormalized()) {
317 throw new Error('ConfigArray must be normalized to perform this operation.');
318 }
319}
320
Tim van der Lippe0ceb4652022-01-06 14:23:36 +0100321/**
322 * Ensures that config types are valid.
323 * @param {Array<string>} extraConfigTypes The config types to check.
324 * @returns {void}
325 * @throws {Error} When the config types array is invalid.
326 */
327function assertExtraConfigTypes(extraConfigTypes) {
328 if (extraConfigTypes.length > 2) {
329 throw new TypeError('configTypes must be an array with at most two items.');
330 }
331
332 for (const configType of extraConfigTypes) {
333 if (!CONFIG_TYPES.has(configType)) {
334 throw new TypeError(`Unexpected config type "${configType}" found. Expected one of: "object", "array", "function".`);
335 }
336 }
337}
338
Tim van der Lippe2c891972021-07-29 16:22:50 +0100339//------------------------------------------------------------------------------
340// Public Interface
341//------------------------------------------------------------------------------
342
343const ConfigArraySymbol = {
344 isNormalized: Symbol('isNormalized'),
345 configCache: Symbol('configCache'),
346 schema: Symbol('schema'),
347 finalizeConfig: Symbol('finalizeConfig'),
348 preprocessConfig: Symbol('preprocessConfig')
349};
350
Tim van der Lippe0ceb4652022-01-06 14:23:36 +0100351// used to store calculate data for faster lookup
352const dataCache = new WeakMap();
353
Tim van der Lippe2c891972021-07-29 16:22:50 +0100354/**
355 * Represents an array of config objects and provides method for working with
356 * those config objects.
357 */
358class ConfigArray extends Array {
359
360 /**
361 * Creates a new instance of ConfigArray.
362 * @param {Iterable|Function|Object} configs An iterable yielding config
363 * objects, or a config function, or a config object.
364 * @param {string} [options.basePath=""] The path of the config file
365 * @param {boolean} [options.normalized=false] Flag indicating if the
366 * configs have already been normalized.
367 * @param {Object} [options.schema] The additional schema
368 * definitions to use for the ConfigArray schema.
Tim van der Lippe0ceb4652022-01-06 14:23:36 +0100369 * @param {Array<string>} [options.configTypes] List of config types supported.
Tim van der Lippe2c891972021-07-29 16:22:50 +0100370 */
Tim van der Lippe0ceb4652022-01-06 14:23:36 +0100371 constructor(configs,
372 {
373 basePath = '',
374 normalized = false,
375 schema: customSchema,
376 extraConfigTypes = []
377 } = {}
378) {
Tim van der Lippe2c891972021-07-29 16:22:50 +0100379 super();
380
381 /**
382 * Tracks if the array has been normalized.
383 * @property isNormalized
384 * @type boolean
385 * @private
386 */
387 this[ConfigArraySymbol.isNormalized] = normalized;
388
389 /**
390 * The schema used for validating and merging configs.
391 * @property schema
392 * @type ObjectSchema
393 * @private
394 */
395 this[ConfigArraySymbol.schema] = new objectSchema.ObjectSchema({
396 ...customSchema,
397 ...baseSchema
398 });
399
400 /**
401 * The path of the config file that this array was loaded from.
402 * This is used to calculate filename matches.
403 * @property basePath
404 * @type string
405 */
406 this.basePath = basePath;
407
Tim van der Lippe0ceb4652022-01-06 14:23:36 +0100408 assertExtraConfigTypes(extraConfigTypes);
409
410 /**
411 * The supported config types.
412 * @property configTypes
413 * @type Array<string>
414 */
415 this.extraConfigTypes = Object.freeze([...extraConfigTypes]);
416
Tim van der Lippe2c891972021-07-29 16:22:50 +0100417 /**
418 * A cache to store calculated configs for faster repeat lookup.
419 * @property configCache
420 * @type Map
421 * @private
422 */
423 this[ConfigArraySymbol.configCache] = new Map();
424
Tim van der Lippe0ceb4652022-01-06 14:23:36 +0100425 // init cache
426 dataCache.set(this, {});
427
Tim van der Lippe2c891972021-07-29 16:22:50 +0100428 // load the configs into this array
429 if (Array.isArray(configs)) {
430 this.push(...configs);
431 } else {
432 this.push(configs);
433 }
434
435 }
436
437 /**
438 * Prevent normal array methods from creating a new `ConfigArray` instance.
439 * This is to ensure that methods such as `slice()` won't try to create a
440 * new instance of `ConfigArray` behind the scenes as doing so may throw
441 * an error due to the different constructor signature.
442 * @returns {Function} The `Array` constructor.
443 */
444 static get [Symbol.species]() {
445 return Array;
446 }
447
448 /**
449 * Returns the `files` globs from every config object in the array.
Tim van der Lippe2c891972021-07-29 16:22:50 +0100450 * This can be used to determine which files will be matched by a
451 * config array or to use as a glob pattern when no patterns are provided
452 * for a command line interface.
Tim van der Lippe0ceb4652022-01-06 14:23:36 +0100453 * @returns {Array<string|Function>} An array of matchers.
Tim van der Lippe2c891972021-07-29 16:22:50 +0100454 */
455 get files() {
456
457 assertNormalized(this);
458
Tim van der Lippe0ceb4652022-01-06 14:23:36 +0100459 // if this data has been cached, retrieve it
460 const cache = dataCache.get(this);
461
462 if (cache.files) {
463 return cache.files;
464 }
465
466 // otherwise calculate it
467
Tim van der Lippe2c891972021-07-29 16:22:50 +0100468 const result = [];
469
470 for (const config of this) {
471 if (config.files) {
472 config.files.forEach(filePattern => {
Tim van der Lippe0ceb4652022-01-06 14:23:36 +0100473 result.push(filePattern);
Tim van der Lippe2c891972021-07-29 16:22:50 +0100474 });
475 }
476 }
477
Tim van der Lippe0ceb4652022-01-06 14:23:36 +0100478 // store result
479 cache.files = result;
480 dataCache.set(this, cache);
481
Tim van der Lippe2c891972021-07-29 16:22:50 +0100482 return result;
483 }
484
485 /**
Tim van der Lippe0ceb4652022-01-06 14:23:36 +0100486 * Returns ignore matchers that should always be ignored regardless of
Tim van der Lippe2c891972021-07-29 16:22:50 +0100487 * the matching `files` fields in any configs. This is necessary to mimic
488 * the behavior of things like .gitignore and .eslintignore, allowing a
489 * globbing operation to be faster.
Tim van der Lippe0ceb4652022-01-06 14:23:36 +0100490 * @returns {string[]} An array of string patterns and functions to be ignored.
Tim van der Lippe2c891972021-07-29 16:22:50 +0100491 */
492 get ignores() {
493
494 assertNormalized(this);
495
Tim van der Lippe0ceb4652022-01-06 14:23:36 +0100496 // if this data has been cached, retrieve it
497 const cache = dataCache.get(this);
498
499 if (cache.ignores) {
500 return cache.ignores;
501 }
502
503 // otherwise calculate it
504
Tim van der Lippe2c891972021-07-29 16:22:50 +0100505 const result = [];
506
507 for (const config of this) {
508 if (config.ignores && !config.files) {
Tim van der Lippe0ceb4652022-01-06 14:23:36 +0100509 result.push(...config.ignores);
Tim van der Lippe2c891972021-07-29 16:22:50 +0100510 }
511 }
512
Tim van der Lippe0ceb4652022-01-06 14:23:36 +0100513 // store result
514 cache.ignores = result;
515 dataCache.set(this, cache);
516
Tim van der Lippe2c891972021-07-29 16:22:50 +0100517 return result;
518 }
519
520 /**
521 * Indicates if the config array has been normalized.
522 * @returns {boolean} True if the config array is normalized, false if not.
523 */
524 isNormalized() {
525 return this[ConfigArraySymbol.isNormalized];
526 }
527
528 /**
529 * Normalizes a config array by flattening embedded arrays and executing
530 * config functions.
531 * @param {ConfigContext} context The context object for config functions.
Tim van der Lippe0fb47802021-11-08 16:23:10 +0000532 * @returns {Promise<ConfigArray>} The current ConfigArray instance.
Tim van der Lippe2c891972021-07-29 16:22:50 +0100533 */
534 async normalize(context = {}) {
535
536 if (!this.isNormalized()) {
Tim van der Lippe0ceb4652022-01-06 14:23:36 +0100537 const normalizedConfigs = await normalize(this, context, this.extraConfigTypes);
Tim van der Lippe2c891972021-07-29 16:22:50 +0100538 this.length = 0;
539 this.push(...normalizedConfigs.map(this[ConfigArraySymbol.preprocessConfig]));
540 this[ConfigArraySymbol.isNormalized] = true;
541
542 // prevent further changes
543 Object.freeze(this);
544 }
545
546 return this;
547 }
548
549 /**
Tim van der Lippe0fb47802021-11-08 16:23:10 +0000550 * Normalizes a config array by flattening embedded arrays and executing
551 * config functions.
552 * @param {ConfigContext} context The context object for config functions.
553 * @returns {ConfigArray} The current ConfigArray instance.
554 */
555 normalizeSync(context = {}) {
556
557 if (!this.isNormalized()) {
Tim van der Lippe0ceb4652022-01-06 14:23:36 +0100558 const normalizedConfigs = normalizeSync(this, context, this.extraConfigTypes);
Tim van der Lippe0fb47802021-11-08 16:23:10 +0000559 this.length = 0;
560 this.push(...normalizedConfigs.map(this[ConfigArraySymbol.preprocessConfig]));
561 this[ConfigArraySymbol.isNormalized] = true;
562
563 // prevent further changes
564 Object.freeze(this);
565 }
566
567 return this;
568 }
569
570 /**
Tim van der Lippe2c891972021-07-29 16:22:50 +0100571 * Finalizes the state of a config before being cached and returned by
572 * `getConfig()`. Does nothing by default but is provided to be
573 * overridden by subclasses as necessary.
574 * @param {Object} config The config to finalize.
575 * @returns {Object} The finalized config.
576 */
577 [ConfigArraySymbol.finalizeConfig](config) {
578 return config;
579 }
580
581 /**
582 * Preprocesses a config during the normalization process. This is the
583 * method to override if you want to convert an array item before it is
584 * validated for the first time. For example, if you want to replace a
585 * string with an object, this is the method to override.
586 * @param {Object} config The config to preprocess.
587 * @returns {Object} The config to use in place of the argument.
588 */
589 [ConfigArraySymbol.preprocessConfig](config) {
590 return config;
591 }
592
593 /**
594 * Returns the config object for a given file path.
595 * @param {string} filePath The complete path of a file to get a config for.
596 * @returns {Object} The config object for this file.
597 */
598 getConfig(filePath) {
599
600 assertNormalized(this);
601
602 // first check the cache to avoid duplicate work
603 let finalConfig = this[ConfigArraySymbol.configCache].get(filePath);
604
605 if (finalConfig) {
606 return finalConfig;
607 }
608
Tim van der Lippe0ceb4652022-01-06 14:23:36 +0100609 // TODO: Maybe move elsewhere?
610 const relativeFilePath = path.relative(this.basePath, filePath);
611
612 // if there is a global matcher ignoring this file, just return null
613 for (const shouldIgnore of this.ignores) {
614 if (shouldIgnoreFilePath(shouldIgnore, filePath, relativeFilePath)) {
615
616 // cache and return result - finalConfig is undefined at this point
617 this[ConfigArraySymbol.configCache].set(filePath, finalConfig);
618 return finalConfig;
619 }
620 }
621
622 // filePath isn't automatically ignored, so try to construct config
Tim van der Lippe2c891972021-07-29 16:22:50 +0100623
624 const matchingConfigs = [];
625
626 for (const config of this) {
627 if (pathMatches(filePath, this.basePath, config)) {
628 debug(`Matching config found for ${filePath}`);
629 matchingConfigs.push(config);
630 } else {
631 debug(`No matching config found for ${filePath}`);
632 }
633 }
Tim van der Lippe0ceb4652022-01-06 14:23:36 +0100634
635 // if matching both files and ignores, there will be no config to create
636 if (matchingConfigs.length === 0) {
637 // cache and return result - finalConfig is undefined at this point
638 this[ConfigArraySymbol.configCache].set(filePath, finalConfig);
639 return finalConfig;
640 }
641
642 // otherwise construct the config
Tim van der Lippe2c891972021-07-29 16:22:50 +0100643
644 finalConfig = matchingConfigs.reduce((result, config) => {
645 return this[ConfigArraySymbol.schema].merge(result, config);
646 }, {}, this);
647
648 finalConfig = this[ConfigArraySymbol.finalizeConfig](finalConfig);
649
650 this[ConfigArraySymbol.configCache].set(filePath, finalConfig);
651
652 return finalConfig;
653 }
654
Tim van der Lippe0ceb4652022-01-06 14:23:36 +0100655 /**
656 * Determines if the given filepath is ignored based on the configs.
657 * @param {string} filePath The complete path of a file to check.
658 * @returns {boolean} True if the path is ignored, false if not.
659 */
660 isIgnored(filePath) {
661 return this.getConfig(filePath) === undefined;
662 }
663
Tim van der Lippe2c891972021-07-29 16:22:50 +0100664}
665
666exports.ConfigArray = ConfigArray;
667exports.ConfigArraySymbol = ConfigArraySymbol;