798 lines
23 KiB
JavaScript
798 lines
23 KiB
JavaScript
'use strict';
|
|
|
|
var fs = require('node:fs');
|
|
var path = require('node:path');
|
|
|
|
/**
|
|
* @fileoverview defineConfig helper
|
|
* @author Nicholas C. Zakas
|
|
*/
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Type Definitions
|
|
//-----------------------------------------------------------------------------
|
|
|
|
/** @import * as $eslintcore from "@eslint/core"; */
|
|
/** @typedef {$eslintcore.ConfigObject} Config */
|
|
/** @typedef {$eslintcore.LegacyConfigObject} LegacyConfig */
|
|
/** @typedef {$eslintcore.Plugin} Plugin */
|
|
/** @typedef {$eslintcore.RuleConfig} RuleConfig */
|
|
/** @import * as $typests from "./types.ts"; */
|
|
/** @typedef {$typests.ExtendsElement} ExtendsElement */
|
|
/** @typedef {$typests.ExtensionConfigObject} ExtensionConfigObject */
|
|
/** @typedef {$typests.SimpleExtendsElement} SimpleExtendsElement */
|
|
/** @typedef {$typests.ConfigWithExtends} ConfigWithExtends */
|
|
/** @typedef {$typests.InfiniteArray<Config>} InfiniteConfigArray */
|
|
/** @typedef {$typests.ConfigWithExtendsArray} ConfigWithExtendsArray */
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Helpers
|
|
//-----------------------------------------------------------------------------
|
|
|
|
const eslintrcKeys = [
|
|
"env",
|
|
"extends",
|
|
"globals",
|
|
"ignorePatterns",
|
|
"noInlineConfig",
|
|
"overrides",
|
|
"parser",
|
|
"parserOptions",
|
|
"reportUnusedDisableDirectives",
|
|
"root",
|
|
];
|
|
|
|
const allowedGlobalIgnoreKeys = new Set(["basePath", "ignores", "name"]);
|
|
|
|
/**
|
|
* Gets the name of a config object.
|
|
* @param {Config} config The config object.
|
|
* @param {string} indexPath The index path of the config object.
|
|
* @return {string} The name of the config object.
|
|
*/
|
|
function getConfigName(config, indexPath) {
|
|
if (config.name) {
|
|
return config.name;
|
|
}
|
|
|
|
return `UserConfig${indexPath}`;
|
|
}
|
|
|
|
/**
|
|
* Gets the name of an extension.
|
|
* @param {SimpleExtendsElement} extension The extension.
|
|
* @param {string} indexPath The index of the extension.
|
|
* @return {string} The name of the extension.
|
|
*/
|
|
function getExtensionName(extension, indexPath) {
|
|
if (typeof extension === "string") {
|
|
return extension;
|
|
}
|
|
|
|
if (extension.name) {
|
|
return extension.name;
|
|
}
|
|
|
|
return `ExtendedConfig${indexPath}`;
|
|
}
|
|
|
|
/**
|
|
* Determines if a config object is a legacy config.
|
|
* @param {Config|LegacyConfig} config The config object to check.
|
|
* @return {config is LegacyConfig} `true` if the config object is a legacy config.
|
|
*/
|
|
function isLegacyConfig(config) {
|
|
// eslintrc's plugins must be an array; while flat config's must be an object.
|
|
if (Array.isArray(config.plugins)) {
|
|
return true;
|
|
}
|
|
|
|
for (const key of eslintrcKeys) {
|
|
if (key in config) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Determines if a config object is a global ignores config.
|
|
* @param {Config} config The config object to check.
|
|
* @return {boolean} `true` if the config object is a global ignores config.
|
|
*/
|
|
function isGlobalIgnores(config) {
|
|
return Object.keys(config).every(key => allowedGlobalIgnoreKeys.has(key));
|
|
}
|
|
|
|
/**
|
|
* Parses a plugin member ID (rule, processor, etc.) and returns
|
|
* the namespace and member name.
|
|
* @param {string} id The ID to parse.
|
|
* @returns {{namespace:string, name:string}} The namespace and member name.
|
|
*/
|
|
function getPluginMember(id) {
|
|
const firstSlashIndex = id.indexOf("/");
|
|
|
|
if (firstSlashIndex === -1) {
|
|
return { namespace: "", name: id };
|
|
}
|
|
|
|
let namespace = id.slice(0, firstSlashIndex);
|
|
|
|
/*
|
|
* Special cases:
|
|
* 1. The namespace is `@`, that means it's referring to the
|
|
* core plugin so `@` is the full namespace.
|
|
* 2. The namespace starts with `@`, that means it's referring to
|
|
* an npm scoped package. That means the namespace is the scope
|
|
* and the package name (i.e., `@eslint/core`).
|
|
*/
|
|
if (namespace[0] === "@" && namespace !== "@") {
|
|
const secondSlashIndex = id.indexOf("/", firstSlashIndex + 1);
|
|
if (secondSlashIndex !== -1) {
|
|
namespace = id.slice(0, secondSlashIndex);
|
|
return { namespace, name: id.slice(secondSlashIndex + 1) };
|
|
}
|
|
}
|
|
|
|
const name = id.slice(firstSlashIndex + 1);
|
|
|
|
return { namespace, name };
|
|
}
|
|
|
|
/**
|
|
* Normalizes the plugin config by replacing the namespace with the plugin namespace.
|
|
* @param {string} userNamespace The namespace of the plugin.
|
|
* @param {Plugin} plugin The plugin config object.
|
|
* @param {Config} config The config object to normalize.
|
|
* @return {Config} The normalized config object.
|
|
*/
|
|
function normalizePluginConfig(userNamespace, plugin, config) {
|
|
const pluginNamespace = plugin.meta?.namespace;
|
|
|
|
// don't do anything if the plugin doesn't have a namespace or rules
|
|
if (
|
|
!pluginNamespace ||
|
|
pluginNamespace === userNamespace ||
|
|
(!config.rules && !config.processor && !config.language)
|
|
) {
|
|
return config;
|
|
}
|
|
|
|
const result = { ...config };
|
|
|
|
// update the rules
|
|
if (result.rules) {
|
|
const ruleIds = Object.keys(result.rules);
|
|
|
|
/** @type {Record<string,RuleConfig|undefined>} */
|
|
const newRules = {};
|
|
|
|
for (let i = 0; i < ruleIds.length; i++) {
|
|
const ruleId = ruleIds[i];
|
|
const { namespace: ruleNamespace, name: ruleName } =
|
|
getPluginMember(ruleId);
|
|
|
|
if (ruleNamespace === pluginNamespace) {
|
|
newRules[`${userNamespace}/${ruleName}`] = result.rules[ruleId];
|
|
} else {
|
|
newRules[ruleId] = result.rules[ruleId];
|
|
}
|
|
}
|
|
|
|
result.rules = newRules;
|
|
}
|
|
|
|
// update the processor
|
|
|
|
if (typeof result.processor === "string") {
|
|
const { namespace: processorNamespace, name: processorName } =
|
|
getPluginMember(result.processor);
|
|
|
|
if (processorNamespace) {
|
|
if (processorNamespace === pluginNamespace) {
|
|
result.processor = `${userNamespace}/${processorName}`;
|
|
}
|
|
}
|
|
}
|
|
|
|
// update the language
|
|
if (typeof result.language === "string") {
|
|
const { namespace: languageNamespace, name: languageName } =
|
|
getPluginMember(result.language);
|
|
|
|
if (languageNamespace === pluginNamespace) {
|
|
result.language = `${userNamespace}/${languageName}`;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Deeply normalizes a plugin config, traversing recursively into an arrays.
|
|
* @param {string} userPluginNamespace The namespace of the plugin.
|
|
* @param {Plugin} plugin The plugin object.
|
|
* @param {Config|LegacyConfig|(Config|LegacyConfig)[]} pluginConfig The plugin config to normalize.
|
|
* @param {string} pluginConfigName The name of the plugin config.
|
|
* @return {InfiniteConfigArray} The normalized plugin config.
|
|
* @throws {TypeError} If the plugin config is a legacy config.
|
|
*/
|
|
function deepNormalizePluginConfig(
|
|
userPluginNamespace,
|
|
plugin,
|
|
pluginConfig,
|
|
pluginConfigName,
|
|
) {
|
|
// if it's an array then it's definitely a new config
|
|
if (Array.isArray(pluginConfig)) {
|
|
return pluginConfig.map(pluginSubConfig =>
|
|
deepNormalizePluginConfig(
|
|
userPluginNamespace,
|
|
plugin,
|
|
pluginSubConfig,
|
|
pluginConfigName,
|
|
),
|
|
);
|
|
}
|
|
|
|
// if it's a legacy config, throw an error
|
|
if (isLegacyConfig(pluginConfig)) {
|
|
throw new TypeError(
|
|
`Plugin config "${pluginConfigName}" is an eslintrc config and cannot be used in this context.`,
|
|
);
|
|
}
|
|
|
|
return normalizePluginConfig(userPluginNamespace, plugin, pluginConfig);
|
|
}
|
|
|
|
/**
|
|
* Finds a plugin config by name in the given config.
|
|
* @param {Config} config The config object.
|
|
* @param {string} pluginConfigName The name of the plugin config.
|
|
* @return {InfiniteConfigArray} The plugin config.
|
|
* @throws {TypeError} If the plugin config is not found or is a legacy config.
|
|
*/
|
|
function findPluginConfig(config, pluginConfigName) {
|
|
const { namespace: userPluginNamespace, name: configName } =
|
|
getPluginMember(pluginConfigName);
|
|
const plugin = config.plugins?.[userPluginNamespace];
|
|
|
|
if (!plugin) {
|
|
throw new TypeError(`Plugin "${userPluginNamespace}" not found.`);
|
|
}
|
|
|
|
const directConfig = plugin.configs?.[configName];
|
|
|
|
// Prefer direct config, but fall back to flat config if available
|
|
if (directConfig) {
|
|
// Arrays are always flat configs, and non-legacy configs can be used directly
|
|
if (Array.isArray(directConfig) || !isLegacyConfig(directConfig)) {
|
|
return deepNormalizePluginConfig(
|
|
userPluginNamespace,
|
|
plugin,
|
|
directConfig,
|
|
pluginConfigName,
|
|
);
|
|
}
|
|
}
|
|
|
|
// If it's a legacy config, or the config does not exist => look for the flat version
|
|
const flatConfig = plugin.configs?.[`flat/${configName}`];
|
|
if (
|
|
flatConfig &&
|
|
(Array.isArray(flatConfig) || !isLegacyConfig(flatConfig))
|
|
) {
|
|
return deepNormalizePluginConfig(
|
|
userPluginNamespace,
|
|
plugin,
|
|
flatConfig,
|
|
pluginConfigName,
|
|
);
|
|
}
|
|
|
|
// If we get here, then the config was either not found or is a legacy config
|
|
const message =
|
|
directConfig || flatConfig
|
|
? `Plugin config "${configName}" in plugin "${userPluginNamespace}" is an eslintrc config and cannot be used in this context.`
|
|
: `Plugin config "${configName}" not found in plugin "${userPluginNamespace}".`;
|
|
throw new TypeError(message);
|
|
}
|
|
|
|
/**
|
|
* Flattens an array while keeping track of the index path.
|
|
* @param {any[]} configList The array to traverse.
|
|
* @param {string} indexPath The index path of the value in a multidimensional array.
|
|
* @return {IterableIterator<{indexPath:string, value:any}>} The flattened list of values.
|
|
*/
|
|
function* flatTraverse(configList, indexPath = "") {
|
|
for (let i = 0; i < configList.length; i++) {
|
|
const newIndexPath = indexPath ? `${indexPath}[${i}]` : `[${i}]`;
|
|
|
|
// if it's an array then traverse it as well
|
|
if (Array.isArray(configList[i])) {
|
|
yield* flatTraverse(configList[i], newIndexPath);
|
|
continue;
|
|
}
|
|
|
|
yield { indexPath: newIndexPath, value: configList[i] };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extends a list of config files by creating every combination of base and extension files.
|
|
* @param {(string|string[])[]} [baseFiles] The base files.
|
|
* @param {(string|string[])[]} [extensionFiles] The extension files.
|
|
* @return {(string|string[])[]} The extended files.
|
|
*/
|
|
function extendConfigFiles(baseFiles = [], extensionFiles = []) {
|
|
if (!extensionFiles.length) {
|
|
return baseFiles.concat();
|
|
}
|
|
|
|
if (!baseFiles.length) {
|
|
return extensionFiles.concat();
|
|
}
|
|
|
|
/** @type {(string|string[])[]} */
|
|
const result = [];
|
|
|
|
for (const baseFile of baseFiles) {
|
|
for (const extensionFile of extensionFiles) {
|
|
/*
|
|
* Each entry can be a string or array of strings. The end result
|
|
* needs to be an array of strings, so we need to be sure to include
|
|
* all of the items when there's an array.
|
|
*/
|
|
|
|
const entry = [];
|
|
|
|
if (Array.isArray(baseFile)) {
|
|
entry.push(...baseFile);
|
|
} else {
|
|
entry.push(baseFile);
|
|
}
|
|
|
|
if (Array.isArray(extensionFile)) {
|
|
entry.push(...extensionFile);
|
|
} else {
|
|
entry.push(extensionFile);
|
|
}
|
|
|
|
result.push(entry);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Extends a config object with another config object.
|
|
* @param {Config} baseConfig The base config object.
|
|
* @param {string} baseConfigName The name of the base config object.
|
|
* @param {Config} extension The extension config object.
|
|
* @param {string} extensionName The index of the extension config object.
|
|
* @return {Config} The extended config object.
|
|
*/
|
|
function extendConfig(baseConfig, baseConfigName, extension, extensionName) {
|
|
const result = { ...extension };
|
|
|
|
// for global ignores there is no further work to be done, we just keep everything
|
|
if (!isGlobalIgnores(extension)) {
|
|
// for files we need to create every combination of base and extension files
|
|
if (baseConfig.files) {
|
|
result.files = extendConfigFiles(baseConfig.files, extension.files);
|
|
}
|
|
|
|
// for ignores we just concatenation the extension ignores onto the base ignores
|
|
if (baseConfig.ignores) {
|
|
result.ignores = baseConfig.ignores.concat(extension.ignores ?? []);
|
|
}
|
|
}
|
|
|
|
result.name = `${baseConfigName} > ${extensionName}`;
|
|
|
|
if (baseConfig.basePath) {
|
|
result.basePath = baseConfig.basePath;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Processes a list of extends elements.
|
|
* @param {ConfigWithExtends} config The config object.
|
|
* @param {WeakMap<Config, string>} configNames The map of config objects to their names.
|
|
* @return {Config[]} The flattened list of config objects.
|
|
* @throws {TypeError} If the `extends` property is not an array or if nested `extends` is found.
|
|
*/
|
|
function processExtends(config, configNames) {
|
|
if (!config.extends) {
|
|
return [config];
|
|
}
|
|
|
|
if (!Array.isArray(config.extends)) {
|
|
throw new TypeError("The `extends` property must be an array.");
|
|
}
|
|
|
|
const {
|
|
/** @type {Config[]} */
|
|
extends: extendsList,
|
|
|
|
/** @type {Config} */
|
|
...configObject
|
|
} = config;
|
|
|
|
const extensionNames = new WeakMap();
|
|
|
|
// replace strings with the actual configs
|
|
const objectExtends = extendsList.map(extendsElement => {
|
|
if (typeof extendsElement === "string") {
|
|
const pluginConfig = findPluginConfig(config, extendsElement);
|
|
|
|
// assign names
|
|
if (Array.isArray(pluginConfig)) {
|
|
pluginConfig.forEach((pluginConfigElement, index) => {
|
|
extensionNames.set(
|
|
pluginConfigElement,
|
|
`${extendsElement}[${index}]`,
|
|
);
|
|
});
|
|
} else {
|
|
extensionNames.set(pluginConfig, extendsElement);
|
|
}
|
|
|
|
return pluginConfig;
|
|
}
|
|
|
|
return /** @type {Config} */ (extendsElement);
|
|
});
|
|
|
|
const result = [];
|
|
|
|
for (const { indexPath, value: extendsElement } of flatTraverse(
|
|
objectExtends,
|
|
)) {
|
|
const extension = /** @type {Config} */ (extendsElement);
|
|
|
|
if ("basePath" in extension) {
|
|
throw new TypeError("'basePath' in `extends` is not allowed.");
|
|
}
|
|
|
|
if ("extends" in extension) {
|
|
throw new TypeError("Nested 'extends' is not allowed.");
|
|
}
|
|
|
|
const baseConfigName = /** @type {string} */ (configNames.get(config));
|
|
const extensionName =
|
|
extensionNames.get(extendsElement) ??
|
|
getExtensionName(extendsElement, indexPath);
|
|
|
|
result.push(
|
|
extendConfig(
|
|
configObject,
|
|
baseConfigName,
|
|
extension,
|
|
extensionName,
|
|
),
|
|
);
|
|
}
|
|
|
|
/*
|
|
* If the base config object has only `ignores` and `extends`, then
|
|
* removing `extends` turns it into a global ignores, which is not what
|
|
* we want. So we need to check if the base config object is a global ignores
|
|
* and if so, we don't add it to the array.
|
|
*
|
|
* (The other option would be to add a `files` entry, but that would result
|
|
* in a config that didn't actually do anything because there are no
|
|
* other keys in the config.)
|
|
*/
|
|
if (!isGlobalIgnores(configObject)) {
|
|
result.push(configObject);
|
|
}
|
|
|
|
return result.flat();
|
|
}
|
|
|
|
/**
|
|
* Processes a list of config objects and arrays.
|
|
* @param {ConfigWithExtends[]} configList The list of config objects and arrays.
|
|
* @param {WeakMap<Config, string>} configNames The map of config objects to their names.
|
|
* @return {Config[]} The flattened list of config objects.
|
|
*/
|
|
function processConfigList(configList, configNames) {
|
|
return configList.flatMap(config => processExtends(config, configNames));
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Exports
|
|
//-----------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Helper function to define a config array.
|
|
* @param {ConfigWithExtendsArray} args The arguments to the function.
|
|
* @returns {Config[]} The config array.
|
|
* @throws {TypeError} If no arguments are provided or if an argument is not an object.
|
|
*/
|
|
function defineConfig(...args) {
|
|
const configNames = new WeakMap();
|
|
const configs = [];
|
|
|
|
if (args.length === 0) {
|
|
throw new TypeError("Expected one or more arguments.");
|
|
}
|
|
|
|
// first flatten the list of configs and get the names
|
|
for (const { indexPath, value } of flatTraverse(args)) {
|
|
if (typeof value !== "object" || value === null) {
|
|
throw new TypeError(
|
|
`Expected an object but received ${String(value)}.`,
|
|
);
|
|
}
|
|
|
|
const config = /** @type {ConfigWithExtends} */ (value);
|
|
|
|
// save config name for easy reference later
|
|
configNames.set(config, getConfigName(config, indexPath));
|
|
configs.push(config);
|
|
}
|
|
|
|
return processConfigList(configs, configNames);
|
|
}
|
|
|
|
/**
|
|
* @fileoverview Global ignores helper function.
|
|
* @author Nicholas C. Zakas
|
|
*/
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Type Definitions
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Helpers
|
|
//-----------------------------------------------------------------------------
|
|
|
|
let globalIgnoreCount = 0;
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Exports
|
|
//-----------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Creates a global ignores config with the given patterns.
|
|
* @param {string[]} ignorePatterns The ignore patterns.
|
|
* @param {string} [name] The name of the global ignores config.
|
|
* @returns {Config} The global ignores config.
|
|
* @throws {TypeError} If ignorePatterns is not an array or if it is empty.
|
|
*/
|
|
function globalIgnores(ignorePatterns, name) {
|
|
if (!Array.isArray(ignorePatterns)) {
|
|
throw new TypeError("ignorePatterns must be an array");
|
|
}
|
|
|
|
if (ignorePatterns.length === 0) {
|
|
throw new TypeError("ignorePatterns must contain at least one pattern");
|
|
}
|
|
|
|
const id = globalIgnoreCount++;
|
|
|
|
return {
|
|
name: name || `globalIgnores ${id}`,
|
|
ignores: ignorePatterns,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @fileoverview Ignore file utilities for the config-helpers package.
|
|
* This file was forked from the source code for the compat package.
|
|
*
|
|
* @author Nicholas C. Zakas
|
|
* @author Kirk Waiblinger
|
|
*/
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Types
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
|
/**
|
|
* @typedef {object} IncludeIgnoreFileOptionsObject
|
|
* @property {boolean} [gitignoreResolution] Whether to interpret the contents of an ignore file relative to the config file or the ignore file.
|
|
* - gitignoreResolution: false (default): Interprets ignore patterns relative to the config file
|
|
* - gitignoreResolution: true: Interprets the ignore patterns in a file relative to the ignore file
|
|
* @property {string} [name] The name to give the output config object(s).
|
|
*/
|
|
|
|
/**
|
|
* Options for `includeIgnoreFile()`. May be provided as an object or, for
|
|
* legacy compatibility with `@eslint/compat`, as a string which is treated as
|
|
* the `name` option.
|
|
* @typedef {IncludeIgnoreFileOptionsObject | string} IncludeIgnoreFileOptions
|
|
*/
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Exports
|
|
//-----------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Converts an ESLint ignore pattern to a minimatch pattern.
|
|
* @param {string} pattern The .eslintignore or .gitignore pattern to convert.
|
|
* @returns {string} The converted pattern.
|
|
*/
|
|
function convertIgnorePatternToMinimatch(pattern) {
|
|
const isNegated = pattern.startsWith("!");
|
|
const negatedPrefix = isNegated ? "!" : "";
|
|
const patternToTest = (isNegated ? pattern.slice(1) : pattern).trimEnd();
|
|
|
|
// special cases
|
|
if (["", "**", "/**", "**/"].includes(patternToTest)) {
|
|
return `${negatedPrefix}${patternToTest}`;
|
|
}
|
|
|
|
const firstIndexOfSlash = patternToTest.indexOf("/");
|
|
|
|
const matchEverywherePrefix =
|
|
firstIndexOfSlash < 0 || firstIndexOfSlash === patternToTest.length - 1
|
|
? "**/"
|
|
: "";
|
|
|
|
const patternWithoutLeadingSlash =
|
|
firstIndexOfSlash === 0 ? patternToTest.slice(1) : patternToTest;
|
|
|
|
/*
|
|
* Escape `{` and `(` because in gitignore patterns they are just
|
|
* literal characters without any specific syntactic meaning,
|
|
* while in minimatch patterns they can form brace expansion or extglob syntax.
|
|
*
|
|
* For example, gitignore pattern `src/{a,b}.js` ignores file `src/{a,b}.js`.
|
|
* But, the same minimatch pattern `src/{a,b}.js` ignores files `src/a.js` and `src/b.js`.
|
|
* Minimatch pattern `src/\{a,b}.js` is equivalent to gitignore pattern `src/{a,b}.js`.
|
|
*/
|
|
const escapedPatternWithoutLeadingSlash =
|
|
patternWithoutLeadingSlash.replaceAll(
|
|
// eslint-disable-next-line regexp/no-empty-lookarounds-assertion -- False positive
|
|
/(?=((?:\\.|[^{(])*))\1([{(])/guy,
|
|
"$1\\$2",
|
|
);
|
|
|
|
const matchInsideSuffix = patternToTest.endsWith("/**") ? "/*" : "";
|
|
|
|
return `${negatedPrefix}${matchEverywherePrefix}${escapedPatternWithoutLeadingSlash}${matchInsideSuffix}`;
|
|
}
|
|
|
|
/**
|
|
* @param {string} ignoreFilePath
|
|
* @returns {string[]}
|
|
*/
|
|
function ignoreFilePathToPatterns(ignoreFilePath) {
|
|
const ignoreFile = fs.readFileSync(ignoreFilePath, "utf8");
|
|
const lines = ignoreFile.split(/\r?\n/u);
|
|
|
|
return lines
|
|
.map(line => line.trim())
|
|
.filter(line => line && !line.startsWith("#"))
|
|
.map(convertIgnorePatternToMinimatch);
|
|
}
|
|
|
|
/**
|
|
* Helper to parse and validate the options to `includeIgnoreFile()`
|
|
*
|
|
* @param {string | { gitignoreResolution?: unknown, name?: unknown } | undefined} options
|
|
* @returns {{ gitignoreResolution: boolean, name: string }}
|
|
*/
|
|
function parseOptions(options) {
|
|
// legacy compatibility with @eslint/compat's `includeIgnoreFile`
|
|
if (typeof options === "string") {
|
|
return { gitignoreResolution: false, name: options };
|
|
}
|
|
|
|
const optionsObject = options ?? {};
|
|
if (typeof optionsObject !== "object" || Array.isArray(optionsObject)) {
|
|
throw new TypeError(
|
|
"The options argument to `includeIgnoreFile()` should be an object or a string.",
|
|
);
|
|
}
|
|
|
|
const gitignoreResolution = optionsObject.gitignoreResolution ?? false;
|
|
if (typeof gitignoreResolution !== "boolean") {
|
|
throw new TypeError(
|
|
"The `gitignoreResolution` option must be specified a boolean or omitted",
|
|
);
|
|
}
|
|
|
|
const name = optionsObject.name ?? `Imported .gitignore patterns`;
|
|
if (typeof name !== "string") {
|
|
throw new TypeError(
|
|
"The `name` option must be specified as a string or omitted.",
|
|
);
|
|
}
|
|
|
|
return { gitignoreResolution, name };
|
|
}
|
|
|
|
/**
|
|
* @overload
|
|
*
|
|
* Reads ignore files and returns objects with the ignore patterns.
|
|
*
|
|
* @param {string[]} ignoreFilePathArg The paths of ignore files to include.
|
|
* @param {IncludeIgnoreFileOptions} [options]
|
|
* @returns {Config[]}
|
|
*/
|
|
|
|
/**
|
|
* @overload
|
|
*
|
|
* Reads an ignore file and returns an object with the ignore patterns.
|
|
*
|
|
* @param {string} ignoreFilePathArg The path of the ignore file to include.
|
|
* @param {IncludeIgnoreFileOptions} [options]
|
|
* @returns {Config}
|
|
*/
|
|
|
|
/**
|
|
* @overload
|
|
*
|
|
* Reads an ignore file(s) and returns an object(s) with the ignore patterns.
|
|
*
|
|
* @param {string[] | string} ignoreFilePathArg The path(s) of the ignore file(s) to include.
|
|
* @param {IncludeIgnoreFileOptions} [options]
|
|
* @returns {Config[] | Config}
|
|
*/
|
|
|
|
/**
|
|
* Reads an ignore file(s) and returns an object(s) with the ignore patterns.
|
|
*
|
|
* @param {string[] | string} ignoreFilePathArg The path(s) of the ignore file(s) to include.
|
|
* @param {IncludeIgnoreFileOptions} [options]
|
|
* @returns {Config[] | Config}
|
|
*/
|
|
function includeIgnoreFile(ignoreFilePathArg, options) {
|
|
const returnSingleObject = !Array.isArray(ignoreFilePathArg);
|
|
const ignoreFilePaths = Array.isArray(ignoreFilePathArg)
|
|
? ignoreFilePathArg
|
|
: [ignoreFilePathArg];
|
|
for (const ignorePath of ignoreFilePaths) {
|
|
if (typeof ignorePath !== "string") {
|
|
throw new TypeError(
|
|
"The first argument to `includeIgnoreFile()` should be a string or array of strings",
|
|
);
|
|
}
|
|
if (!path.isAbsolute(ignorePath)) {
|
|
throw new Error(
|
|
`The ignore file location must be an absolute path. Received ${ignorePath}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
const { gitignoreResolution, name } = parseOptions(options);
|
|
|
|
if (returnSingleObject) {
|
|
return {
|
|
name,
|
|
ignores: ignoreFilePathToPatterns(ignoreFilePathArg),
|
|
...(gitignoreResolution
|
|
? { basePath: path.dirname(ignoreFilePathArg) }
|
|
: {}),
|
|
};
|
|
}
|
|
|
|
return ignoreFilePaths.map((ignoreFilePath, i) => ({
|
|
name: `${name} (${i})`,
|
|
ignores: ignoreFilePathToPatterns(ignoreFilePath),
|
|
...(gitignoreResolution
|
|
? { basePath: path.dirname(ignoreFilePath) }
|
|
: {}),
|
|
}));
|
|
}
|
|
|
|
exports.convertIgnorePatternToMinimatch = convertIgnorePatternToMinimatch;
|
|
exports.defineConfig = defineConfig;
|
|
exports.globalIgnores = globalIgnores;
|
|
exports.includeIgnoreFile = includeIgnoreFile;
|