3 Object.defineProperty(exports, '__esModule', { value: true });
5 function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
7 var path = _interopDefault(require('path'));
8 var minimatch = _interopDefault(require('minimatch'));
9 var createDebug = _interopDefault(require('debug'));
10 var objectSchema = require('@humanwhocodes/object-schema');
13 * @fileoverview ConfigSchema
14 * @author Nicholas C. Zakas
17 //------------------------------------------------------------------------------
19 //------------------------------------------------------------------------------
22 * Assets that a given value is an array.
23 * @param {*} value The value to check.
25 * @throws {TypeError} When the value is not an array.
27 function assertIsArray(value) {
28 if (!Array.isArray(value)) {
29 throw new TypeError('Expected value to be an array.');
34 * Assets that a given value is an array containing only strings and functions.
35 * @param {*} value The value to check.
37 * @throws {TypeError} When the value is not an array of strings and functions.
39 function assertIsArrayOfStringsAndFunctions(value, name) {
42 if (value.some(item => typeof item !== 'string' && typeof item !== 'function')) {
43 throw new TypeError('Expected array to only contain strings.');
47 //------------------------------------------------------------------------------
49 //------------------------------------------------------------------------------
52 * The base schema that every ConfigArray uses.
55 const baseSchema = Object.freeze({
62 if (typeof value !== 'string') {
63 throw new TypeError('Property must be a string.');
74 // first check if it's an array
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.');
93 validate: assertIsArrayOfStringsAndFunctions
98 * @fileoverview ConfigArray
99 * @author Nicholas C. Zakas
102 //------------------------------------------------------------------------------
104 //------------------------------------------------------------------------------
106 const debug = createDebug('@hwc/config-array');
108 const MINIMATCH_OPTIONS = {
113 * Shorthand for checking if a value is a string.
114 * @param {any} value The value to check.
115 * @returns {boolean} True if a string, false if not.
117 function isString(value) {
118 return typeof value === 'string';
122 * Normalizes a `ConfigArray` by flattening it and executing any functions
123 * that are found inside.
124 * @param {Array} items The items in a `ConfigArray`.
125 * @param {Object} context The context object to pass into any function
127 * @returns {Array} A flattened array containing only config objects.
128 * @throws {TypeError} When a config function returns a function.
130 async function normalize(items, context) {
132 // TODO: Allow async config functions
134 function *flatTraverse(array) {
135 for (let item of array) {
136 if (typeof item === 'function') {
137 item = item(context);
140 if (Array.isArray(item)) {
141 yield * flatTraverse(item);
142 } else if (typeof item === 'function') {
143 throw new TypeError('A config function can only return an object or array.');
150 return [...flatTraverse(items)];
154 * Determines if a given file path is matched by a config. If the config
155 * has no `files` field, then it matches; otherwise, if a `files` field
156 * is present then we match the globs in `files` and exclude any globs in
158 * @param {string} filePath The absolute file path to check.
159 * @param {Object} config The config object to check.
160 * @returns {boolean} True if the file path is matched by the config,
163 function pathMatches(filePath, basePath, config) {
165 // a config without a `files` field always matches
170 // if files isn't an array, throw an error
171 if (!Array.isArray(config.files) || config.files.length === 0) {
172 throw new TypeError('The files key must be a non-empty array.');
175 const relativeFilePath = path.relative(basePath, filePath);
177 // match both strings and functions
178 const match = pattern => {
179 if (isString(pattern)) {
180 return minimatch(relativeFilePath, pattern, MINIMATCH_OPTIONS);
183 if (typeof pattern === 'function') {
184 return pattern(filePath);
188 // check for all matches to config.files
189 let matches = config.files.some(pattern => {
190 if (Array.isArray(pattern)) {
191 return pattern.every(match);
194 return match(pattern);
198 * If the file path matches the config.files patterns, then check to see
199 * if there are any files to ignore.
201 if (matches && config.ignores) {
202 matches = !config.ignores.some(pattern => {
203 return minimatch(filePath, pattern, MINIMATCH_OPTIONS);
211 * Ensures that a ConfigArray has been normalized.
212 * @param {ConfigArray} configArray The ConfigArray to check.
214 * @throws {Error} When the `ConfigArray` is not normalized.
216 function assertNormalized(configArray) {
217 // TODO: Throw more verbose error
218 if (!configArray.isNormalized()) {
219 throw new Error('ConfigArray must be normalized to perform this operation.');
223 //------------------------------------------------------------------------------
225 //------------------------------------------------------------------------------
227 const ConfigArraySymbol = {
228 isNormalized: Symbol('isNormalized'),
229 configCache: Symbol('configCache'),
230 schema: Symbol('schema'),
231 finalizeConfig: Symbol('finalizeConfig'),
232 preprocessConfig: Symbol('preprocessConfig')
236 * Represents an array of config objects and provides method for working with
237 * those config objects.
239 class ConfigArray extends Array {
242 * Creates a new instance of ConfigArray.
243 * @param {Iterable|Function|Object} configs An iterable yielding config
244 * objects, or a config function, or a config object.
245 * @param {string} [options.basePath=""] The path of the config file
246 * @param {boolean} [options.normalized=false] Flag indicating if the
247 * configs have already been normalized.
248 * @param {Object} [options.schema] The additional schema
249 * definitions to use for the ConfigArray schema.
251 constructor(configs, { basePath = '', normalized = false, schema: customSchema } = {}) {
255 * Tracks if the array has been normalized.
256 * @property isNormalized
260 this[ConfigArraySymbol.isNormalized] = normalized;
263 * The schema used for validating and merging configs.
268 this[ConfigArraySymbol.schema] = new objectSchema.ObjectSchema({
274 * The path of the config file that this array was loaded from.
275 * This is used to calculate filename matches.
279 this.basePath = basePath;
282 * A cache to store calculated configs for faster repeat lookup.
283 * @property configCache
287 this[ConfigArraySymbol.configCache] = new Map();
289 // load the configs into this array
290 if (Array.isArray(configs)) {
291 this.push(...configs);
299 * Prevent normal array methods from creating a new `ConfigArray` instance.
300 * This is to ensure that methods such as `slice()` won't try to create a
301 * new instance of `ConfigArray` behind the scenes as doing so may throw
302 * an error due to the different constructor signature.
303 * @returns {Function} The `Array` constructor.
305 static get [Symbol.species]() {
310 * Returns the `files` globs from every config object in the array.
311 * Negated patterns (those beginning with `!`) are not returned.
312 * This can be used to determine which files will be matched by a
313 * config array or to use as a glob pattern when no patterns are provided
314 * for a command line interface.
315 * @returns {string[]} An array of string patterns.
319 assertNormalized(this);
323 for (const config of this) {
325 config.files.forEach(filePattern => {
326 if (Array.isArray(filePattern)) {
327 result.push(...filePattern.filter(pattern => {
328 return isString(pattern) && !pattern.startsWith('!');
330 } else if (isString(filePattern) && !filePattern.startsWith('!')) {
331 result.push(filePattern);
341 * Returns the file globs that should always be ignored regardless of
342 * the matching `files` fields in any configs. This is necessary to mimic
343 * the behavior of things like .gitignore and .eslintignore, allowing a
344 * globbing operation to be faster.
345 * @returns {string[]} An array of string patterns to be ignored.
349 assertNormalized(this);
353 for (const config of this) {
354 if (config.ignores && !config.files) {
355 result.push(...config.ignores.filter(isString));
363 * Indicates if the config array has been normalized.
364 * @returns {boolean} True if the config array is normalized, false if not.
367 return this[ConfigArraySymbol.isNormalized];
371 * Normalizes a config array by flattening embedded arrays and executing
373 * @param {ConfigContext} context The context object for config functions.
374 * @returns {ConfigArray} A new ConfigArray instance that is normalized.
376 async normalize(context = {}) {
378 if (!this.isNormalized()) {
379 const normalizedConfigs = await normalize(this, context);
381 this.push(...normalizedConfigs.map(this[ConfigArraySymbol.preprocessConfig]));
382 this[ConfigArraySymbol.isNormalized] = true;
384 // prevent further changes
392 * Finalizes the state of a config before being cached and returned by
393 * `getConfig()`. Does nothing by default but is provided to be
394 * overridden by subclasses as necessary.
395 * @param {Object} config The config to finalize.
396 * @returns {Object} The finalized config.
398 [ConfigArraySymbol.finalizeConfig](config) {
403 * Preprocesses a config during the normalization process. This is the
404 * method to override if you want to convert an array item before it is
405 * validated for the first time. For example, if you want to replace a
406 * string with an object, this is the method to override.
407 * @param {Object} config The config to preprocess.
408 * @returns {Object} The config to use in place of the argument.
410 [ConfigArraySymbol.preprocessConfig](config) {
415 * Returns the config object for a given file path.
416 * @param {string} filePath The complete path of a file to get a config for.
417 * @returns {Object} The config object for this file.
419 getConfig(filePath) {
421 assertNormalized(this);
423 // first check the cache to avoid duplicate work
424 let finalConfig = this[ConfigArraySymbol.configCache].get(filePath);
430 // No config found in cache, so calculate a new one
432 const matchingConfigs = [];
434 for (const config of this) {
435 if (pathMatches(filePath, this.basePath, config)) {
436 debug(`Matching config found for ${filePath}`);
437 matchingConfigs.push(config);
439 debug(`No matching config found for ${filePath}`);
443 finalConfig = matchingConfigs.reduce((result, config) => {
444 return this[ConfigArraySymbol.schema].merge(result, config);
447 finalConfig = this[ConfigArraySymbol.finalizeConfig](finalConfig);
449 this[ConfigArraySymbol.configCache].set(filePath, finalConfig);
456 exports.ConfigArray = ConfigArray;
457 exports.ConfigArraySymbol = ConfigArraySymbol;