Socket
Socket
Sign inDemoInstall

@humanwhocodes/config-array

Package Overview
Dependencies
Maintainers
1
Versions
44
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@humanwhocodes/config-array - npm Package Compare versions

Comparing version 0.2.2 to 0.3.0

542

api.js

@@ -28,19 +28,19 @@ 'use strict';

function assertIsArray(value) {
if (!Array.isArray(value)) {
throw new TypeError("Expected value to be an array.");
}
if (!Array.isArray(value)) {
throw new TypeError('Expected value to be an array.');
}
}
/**
* Assets that a given value is an array containing only strings.
* Assets that a given value is an array containing only strings and functions.
* @param {*} value The value to check.
* @returns {void}
* @throws {TypeError} When the value is not an array of strings.
* @throws {TypeError} When the value is not an array of strings and functions.
*/
function assertIsArrayOfStrings(value, name) {
assertIsArray(value);
function assertIsArrayOfStringsAndFunctions(value, name) {
assertIsArray(value);
if (value.some(item => typeof item !== "string")) {
throw new TypeError("Expected array to only contain strings.");
}
if (value.some(item => typeof item !== 'string' && typeof item !== 'function')) {
throw new TypeError('Expected array to only contain strings.');
}
}

@@ -57,41 +57,41 @@

const baseSchema = Object.freeze({
name: {
required: false,
merge() {
return undefined;
},
validate(value) {
if (typeof value !== "string") {
throw new TypeError("Property must be a string.");
}
}
},
files: {
required: false,
merge() {
return undefined;
},
validate(value) {
name: {
required: false,
merge() {
return undefined;
},
validate(value) {
if (typeof value !== 'string') {
throw new TypeError('Property must be a string.');
}
}
},
files: {
required: false,
merge() {
return undefined;
},
validate(value) {
// first check if it's an array
assertIsArray(value);
// first check if it's an array
assertIsArray(value);
// then check each member
value.forEach(item => {
if (Array.isArray(item)) {
assertIsArrayOfStrings(item);
} else if (typeof item !== "string") {
throw new TypeError("Items must be a string or an array of strings.");
}
});
}
},
ignores: {
required: false,
merge() {
return undefined;
},
validate: assertIsArrayOfStrings
}
// then check each member
value.forEach(item => {
if (Array.isArray(item)) {
assertIsArrayOfStringsAndFunctions(item);
} else if (typeof item !== 'string' && typeof item !== 'function') {
throw new TypeError('Items must be a string, a function, or an array of strings and functions.');
}
});
}
},
ignores: {
required: false,
merge() {
return undefined;
},
validate: assertIsArrayOfStringsAndFunctions
}
});

@@ -108,8 +108,16 @@

const debug = createDebug("@hwc/config-array");
const debug = createDebug('@hwc/config-array');
const MINIMATCH_OPTIONS = {
matchBase: true
matchBase: true
};
/**
* Shorthand for checking if a value is a string.
* @param {any} value The value to check.
* @returns {boolean} True if a string, false if not.
*/
function isString(value) {
return typeof value === 'string';
}

@@ -127,21 +135,21 @@ /**

// TODO: Allow async config functions
// TODO: Allow async config functions
function *flatTraverse(array) {
for (let item of array) {
if (typeof item === "function") {
item = item(context);
}
if (Array.isArray(item)) {
yield* flatTraverse(item);
} else if (typeof item === "function") {
throw new TypeError("A config function can only return an object or array.");
} else {
yield item;
}
}
}
function *flatTraverse(array) {
for (let item of array) {
if (typeof item === 'function') {
item = item(context);
}
return [...flatTraverse(items)];
if (Array.isArray(item)) {
yield * flatTraverse(item);
} else if (typeof item === 'function') {
throw new TypeError('A config function can only return an object or array.');
} else {
yield item;
}
}
}
return [...flatTraverse(items)];
}

@@ -154,4 +162,3 @@

* `ignores`.
* @param {string} relativeFilePath The file path to check relative to
* the `ConfigArray` `basePath` option.
* @param {string} filePath The absolute file path to check.
* @param {Object} config The config object to check.

@@ -161,37 +168,47 @@ * @returns {boolean} True if the file path is matched by the config,

*/
function pathMatches(relativeFilePath, config) {
function pathMatches(filePath, basePath, config) {
// a config without a `files` field always matches
if (!config.files) {
return true;
}
// a config without a `files` field always matches
if (!config.files) {
return true;
}
// if files isn't an array, throw an error
if (!Array.isArray(config.files) || config.files.length === 0) {
throw new TypeError("The files key must be a non-empty array.");
}
// if files isn't an array, throw an error
if (!Array.isArray(config.files) || config.files.length === 0) {
throw new TypeError('The files key must be a non-empty array.');
}
// check for all matches to config.files
let matches = config.files.some(pattern => {
if (typeof pattern === "string") {
return minimatch(relativeFilePath, pattern, MINIMATCH_OPTIONS);
}
const relativeFilePath = path.relative(basePath, filePath);
// otherwise it's an array where we need to AND the patterns
return pattern.every(subpattern => {
return minimatch(relativeFilePath, subpattern, MINIMATCH_OPTIONS);
});
});
// match both strings and functions
const match = pattern => {
if (isString(pattern)) {
return minimatch(relativeFilePath, pattern, MINIMATCH_OPTIONS);
}
/*
* If the file path matches the config.files patterns, then check to see
* if there are any files to ignore.
*/
if (matches && config.ignores) {
matches = !config.ignores.some(pattern => {
return minimatch(relativeFilePath, pattern, MINIMATCH_OPTIONS);
});
}
if (typeof pattern === 'function') {
return pattern(filePath);
}
};
return matches;
// check for all matches to config.files
let matches = config.files.some(pattern => {
if (Array.isArray(pattern)) {
return pattern.every(match);
}
return match(pattern);
});
/*
* If the file path matches the config.files patterns, then check to see
* if there are any files to ignore.
*/
if (matches && config.ignores) {
matches = !config.ignores.some(pattern => {
return minimatch(filePath, pattern, MINIMATCH_OPTIONS);
});
}
return matches;
}

@@ -206,6 +223,6 @@

function assertNormalized(configArray) {
// TODO: Throw more verbose error
if (!configArray.isNormalized()) {
throw new Error("ConfigArray must be normalized to perform this operation.");
}
// TODO: Throw more verbose error
if (!configArray.isNormalized()) {
throw new Error('ConfigArray must be normalized to perform this operation.');
}
}

@@ -217,5 +234,5 @@

const isNormalized = Symbol("isNormalized");
const configCache = Symbol("configCache");
const schema = Symbol("schema");
const isNormalized = Symbol('isNormalized');
const configCache = Symbol('configCache');
const schema = Symbol('schema');

@@ -228,193 +245,192 @@ /**

/**
* Creates a new instance of ConfigArray.
* @param {Iterable|Function|Object} configs An iterable yielding config
* objects, or a config function, or a config object.
* @param {string} [options.basePath=""] The path of the config file
* @param {boolean} [options.normalized=false] Flag indicating if the
* configs have already been normalized.
* @param {Object} [options.schema] The additional schema
* definitions to use for the ConfigArray schema.
*/
constructor(configs, { basePath = "", normalized = false, schema: customSchema } = {}) {
super();
/**
* Creates a new instance of ConfigArray.
* @param {Iterable|Function|Object} configs An iterable yielding config
* objects, or a config function, or a config object.
* @param {string} [options.basePath=""] The path of the config file
* @param {boolean} [options.normalized=false] Flag indicating if the
* configs have already been normalized.
* @param {Object} [options.schema] The additional schema
* definitions to use for the ConfigArray schema.
*/
constructor(configs, { basePath = '', normalized = false, schema: customSchema } = {}) {
super();
/**
* Tracks if the array has been normalized.
* @property isNormalized
* @type boolean
* @private
*/
this[isNormalized] = normalized;
/**
* The schema used for validating and merging configs.
* @property schema
* @type ObjectSchema
* @private
*/
this[schema] = new objectSchema.ObjectSchema({
...customSchema,
...baseSchema
});
/**
* Tracks if the array has been normalized.
* @property isNormalized
* @type boolean
* @private
*/
this[isNormalized] = normalized;
/**
* The path of the config file that this array was loaded from.
* This is used to calculate filename matches.
* @property basePath
* @type string
*/
this.basePath = basePath;
/**
* A cache to store calculated configs for faster repeat lookup.
* @property configCache
* @type Map
* @private
*/
this[configCache] = new Map();
// load the configs into this array
if (Array.isArray(configs)) {
this.push(...configs);
} else {
this.push(configs);
}
/**
* The schema used for validating and merging configs.
* @property schema
* @type ObjectSchema
* @private
*/
this[schema] = new objectSchema.ObjectSchema({
...customSchema,
...baseSchema
});
}
/**
* The path of the config file that this array was loaded from.
* This is used to calculate filename matches.
* @property basePath
* @type string
*/
this.basePath = basePath;
/**
* Prevent normal array methods from creating a new `ConfigArray` instance.
* This is to ensure that methods such as `slice()` won't try to create a
* new instance of `ConfigArray` behind the scenes as doing so may throw
* an error due to the different constructor signature.
* @returns {Function} The `Array` constructor.
*/
static get [Symbol.species]() {
return Array;
}
/**
* A cache to store calculated configs for faster repeat lookup.
* @property configCache
* @type Map
* @private
*/
this[configCache] = new Map();
/**
* Returns the `files` globs from every config object in the array.
* Negated patterns (those beginning with `!`) are not returned.
* This can be used to determine which files will be matched by a
* config array or to use as a glob pattern when no patterns are provided
* for a command line interface.
* @returns {string[]} An array of string patterns.
*/
get files() {
// load the configs into this array
if (Array.isArray(configs)) {
this.push(...configs);
} else {
this.push(configs);
}
assertNormalized(this);
}
const result = [];
/**
* Prevent normal array methods from creating a new `ConfigArray` instance.
* This is to ensure that methods such as `slice()` won't try to create a
* new instance of `ConfigArray` behind the scenes as doing so may throw
* an error due to the different constructor signature.
* @returns {Function} The `Array` constructor.
*/
static get [Symbol.species]() {
return Array;
}
for (const config of this) {
if (config.files) {
config.files.forEach(filePattern => {
if (Array.isArray(filePattern)) {
result.push(...filePattern.filter(pattern => {
return !pattern.startsWith("!");
}));
} else if (!filePattern.startsWith("!")) {
result.push(filePattern);
}
});
}
}
/**
* Returns the `files` globs from every config object in the array.
* Negated patterns (those beginning with `!`) are not returned.
* This can be used to determine which files will be matched by a
* config array or to use as a glob pattern when no patterns are provided
* for a command line interface.
* @returns {string[]} An array of string patterns.
*/
get files() {
return result;
}
assertNormalized(this);
/**
* Returns the file globs that should always be ignored regardless of
* the matching `files` fields in any configs. This is necessary to mimic
* the behavior of things like .gitignore and .eslintignore, allowing a
* globbing operation to be faster.
* @returns {string[]} An array of string patterns to be ignored.
*/
get ignores() {
const result = [];
assertNormalized(this);
for (const config of this) {
if (config.files) {
config.files.forEach(filePattern => {
if (Array.isArray(filePattern)) {
result.push(...filePattern.filter(pattern => {
return isString(pattern) && !pattern.startsWith('!');
}));
} else if (isString(filePattern) && !filePattern.startsWith('!')) {
result.push(filePattern);
}
});
}
}
const result = [];
return result;
}
for (const config of this) {
if (config.ignores && !config.files) {
result.push(...config.ignores);
}
}
/**
* Returns the file globs that should always be ignored regardless of
* the matching `files` fields in any configs. This is necessary to mimic
* the behavior of things like .gitignore and .eslintignore, allowing a
* globbing operation to be faster.
* @returns {string[]} An array of string patterns to be ignored.
*/
get ignores() {
return result;
}
assertNormalized(this);
/**
* Indicates if the config array has been normalized.
* @returns {boolean} True if the config array is normalized, false if not.
*/
isNormalized() {
return this[isNormalized];
}
const result = [];
/**
* Normalizes a config array by flattening embedded arrays and executing
* config functions.
* @param {ConfigContext} context The context object for config functions.
* @returns {ConfigArray} A new ConfigArray instance that is normalized.
*/
async normalize(context = {}) {
for (const config of this) {
if (config.ignores && !config.files) {
result.push(...config.ignores.filter(isString));
}
}
if (!this.isNormalized()) {
const normalizedConfigs = await normalize(this, context);
this.length = 0;
this.push(...normalizedConfigs);
this[isNormalized] = true;
return result;
}
// prevent further changes
Object.freeze(this);
}
/**
* Indicates if the config array has been normalized.
* @returns {boolean} True if the config array is normalized, false if not.
*/
isNormalized() {
return this[isNormalized];
}
return this;
}
/**
* Normalizes a config array by flattening embedded arrays and executing
* config functions.
* @param {ConfigContext} context The context object for config functions.
* @returns {ConfigArray} A new ConfigArray instance that is normalized.
*/
async normalize(context = {}) {
/**
* Returns the config object for a given file path.
* @param {string} filePath The complete path of a file to get a config for.
* @returns {Object} The config object for this file.
*/
getConfig(filePath) {
if (!this.isNormalized()) {
const normalizedConfigs = await normalize(this, context);
this.length = 0;
this.push(...normalizedConfigs);
this[isNormalized] = true;
assertNormalized(this);
// prevent further changes
Object.freeze(this);
}
// first check the cache to avoid duplicate work
let finalConfig = this[configCache].get(filePath);
return this;
}
if (finalConfig) {
return finalConfig;
}
/**
* Returns the config object for a given file path.
* @param {string} filePath The complete path of a file to get a config for.
* @returns {Object} The config object for this file.
*/
getConfig(filePath) {
// No config found in cache, so calculate a new one
assertNormalized(this);
const matchingConfigs = [];
const relativeFilePath = path.relative(this.basePath, filePath);
// first check the cache to avoid duplicate work
let finalConfig = this[configCache].get(filePath);
for (const config of this) {
if (pathMatches(relativeFilePath, config)) {
debug(`Matching config found for ${relativeFilePath}`);
matchingConfigs.push(config);
} else {
debug(`No matching config found for ${relativeFilePath}`);
}
}
if (finalConfig) {
return finalConfig;
}
finalConfig = matchingConfigs.reduce((result, config) => {
return this[schema].merge(result, config);
}, {}, this);
// No config found in cache, so calculate a new one
this[configCache].set(filePath, finalConfig);
const matchingConfigs = [];
return finalConfig;
}
for (const config of this) {
if (pathMatches(filePath, this.basePath, config)) {
debug(`Matching config found for ${filePath}`);
matchingConfigs.push(config);
} else {
debug(`No matching config found for ${filePath}`);
}
}
finalConfig = matchingConfigs.reduce((result, config) => {
return this[schema].merge(result, config);
}, {}, this);
this[configCache].set(filePath, finalConfig);
return finalConfig;
}
}
exports.ConfigArray = ConfigArray;
{
"name": "@humanwhocodes/config-array",
"version": "0.2.2",
"version": "0.3.0",
"description": "Glob-based configuration matching.",

@@ -20,3 +20,4 @@ "author": "Nicholas C. Zakas",

"build": "rollup -c",
"lint": "eslint src/*.js",
"format": "nitpik",
"lint": "eslint *.config.js src/*.js tests/*.js",
"prepublish": "npm run build",

@@ -26,2 +27,11 @@ "test:coverage": "nyc --include src/*.js npm run test",

},
"gitHooks": {
"pre-commit": "lint-staged"
},
"lint-staged": {
"*.js": [
"nitpik",
"eslint --fix --ignore-pattern '!.eslintrc.js'"
]
},
"keywords": [

@@ -42,9 +52,13 @@ "configuration",

"devDependencies": {
"@nitpik/javascript": "^0.3.3",
"@nitpik/node": "0.0.5",
"chai": "^4.2.0",
"eslint": "^6.7.1",
"esm": "^3.2.25",
"lint-staged": "^10.2.8",
"mocha": "^6.1.4",
"nyc": "^14.1.1",
"rollup": "^1.12.3"
"rollup": "^1.12.3",
"yorkie": "^2.0.0"
}
}

@@ -145,2 +145,8 @@ # Config Array

// filename must match function
{
files: [ filePath => filePath.endsWith(".md") ],
handler: markdownHandler
},
// filename must match all patterns in subarray

@@ -165,4 +171,6 @@ {

If the `files` array contains an item that is an array of strings, then all patterns must match in order for the config to match. In the preceding examples, both `*.test.*` and `*.js` must match in order for the config object to be used.
If the `files` array contains a function, then that function is called with the absolute path of the file and is expected to return `true` if there is a match and `false` if not. (The `ignores` array can also contain functions.)
If the `files` array contains an item that is an array of strings and functions, then all patterns must match in order for the config to match. In the preceding examples, both `*.test.*` and `*.js` must match in order for the config object to be used.
If a pattern in the files array begins with `!` then it excludes that pattern. In the preceding example, any filename that doesn't end with `.js` will automatically getting a `settings.js` property set to `false`.

@@ -169,0 +177,0 @@

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc