index.cjs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588
  1. 'use strict';
  2. /**
  3. * @fileoverview defineConfig helper
  4. * @author Nicholas C. Zakas
  5. */
  6. //-----------------------------------------------------------------------------
  7. // Type Definitions
  8. //-----------------------------------------------------------------------------
  9. /** @import * as $eslintcore from "@eslint/core"; */
  10. /** @typedef {$eslintcore.ConfigObject} Config */
  11. /** @typedef {$eslintcore.LegacyConfigObject} LegacyConfig */
  12. /** @typedef {$eslintcore.Plugin} Plugin */
  13. /** @typedef {$eslintcore.RuleConfig} RuleConfig */
  14. /** @import * as $typests from "./types.ts"; */
  15. /** @typedef {$typests.ExtendsElement} ExtendsElement */
  16. /** @typedef {$typests.SimpleExtendsElement} SimpleExtendsElement */
  17. /** @typedef {$typests.ConfigWithExtends} ConfigWithExtends */
  18. /** @typedef {$typests.InfiniteArray<Config>} InfiniteConfigArray */
  19. /** @typedef {$typests.ConfigWithExtendsArray} ConfigWithExtendsArray */
  20. //-----------------------------------------------------------------------------
  21. // Helpers
  22. //-----------------------------------------------------------------------------
  23. const eslintrcKeys = [
  24. "env",
  25. "extends",
  26. "globals",
  27. "ignorePatterns",
  28. "noInlineConfig",
  29. "overrides",
  30. "parser",
  31. "parserOptions",
  32. "reportUnusedDisableDirectives",
  33. "root",
  34. ];
  35. const allowedGlobalIgnoreKeys = new Set(["basePath", "ignores", "name"]);
  36. /**
  37. * Gets the name of a config object.
  38. * @param {Config} config The config object.
  39. * @param {string} indexPath The index path of the config object.
  40. * @return {string} The name of the config object.
  41. */
  42. function getConfigName(config, indexPath) {
  43. if (config.name) {
  44. return config.name;
  45. }
  46. return `UserConfig${indexPath}`;
  47. }
  48. /**
  49. * Gets the name of an extension.
  50. * @param {SimpleExtendsElement} extension The extension.
  51. * @param {string} indexPath The index of the extension.
  52. * @return {string} The name of the extension.
  53. */
  54. function getExtensionName(extension, indexPath) {
  55. if (typeof extension === "string") {
  56. return extension;
  57. }
  58. if (extension.name) {
  59. return extension.name;
  60. }
  61. return `ExtendedConfig${indexPath}`;
  62. }
  63. /**
  64. * Determines if a config object is a legacy config.
  65. * @param {Config|LegacyConfig} config The config object to check.
  66. * @return {config is LegacyConfig} `true` if the config object is a legacy config.
  67. */
  68. function isLegacyConfig(config) {
  69. // eslintrc's plugins must be an array; while flat config's must be an object.
  70. if (Array.isArray(config.plugins)) {
  71. return true;
  72. }
  73. for (const key of eslintrcKeys) {
  74. if (key in config) {
  75. return true;
  76. }
  77. }
  78. return false;
  79. }
  80. /**
  81. * Determines if a config object is a global ignores config.
  82. * @param {Config} config The config object to check.
  83. * @return {boolean} `true` if the config object is a global ignores config.
  84. */
  85. function isGlobalIgnores(config) {
  86. return Object.keys(config).every(key => allowedGlobalIgnoreKeys.has(key));
  87. }
  88. /**
  89. * Parses a plugin member ID (rule, processor, etc.) and returns
  90. * the namespace and member name.
  91. * @param {string} id The ID to parse.
  92. * @returns {{namespace:string, name:string}} The namespace and member name.
  93. */
  94. function getPluginMember(id) {
  95. const firstSlashIndex = id.indexOf("/");
  96. if (firstSlashIndex === -1) {
  97. return { namespace: "", name: id };
  98. }
  99. let namespace = id.slice(0, firstSlashIndex);
  100. /*
  101. * Special cases:
  102. * 1. The namespace is `@`, that means it's referring to the
  103. * core plugin so `@` is the full namespace.
  104. * 2. The namespace starts with `@`, that means it's referring to
  105. * an npm scoped package. That means the namespace is the scope
  106. * and the package name (i.e., `@eslint/core`).
  107. */
  108. if (namespace[0] === "@" && namespace !== "@") {
  109. const secondSlashIndex = id.indexOf("/", firstSlashIndex + 1);
  110. if (secondSlashIndex !== -1) {
  111. namespace = id.slice(0, secondSlashIndex);
  112. return { namespace, name: id.slice(secondSlashIndex + 1) };
  113. }
  114. }
  115. const name = id.slice(firstSlashIndex + 1);
  116. return { namespace, name };
  117. }
  118. /**
  119. * Normalizes the plugin config by replacing the namespace with the plugin namespace.
  120. * @param {string} userNamespace The namespace of the plugin.
  121. * @param {Plugin} plugin The plugin config object.
  122. * @param {Config} config The config object to normalize.
  123. * @return {Config} The normalized config object.
  124. */
  125. function normalizePluginConfig(userNamespace, plugin, config) {
  126. const pluginNamespace = plugin.meta?.namespace;
  127. // don't do anything if the plugin doesn't have a namespace or rules
  128. if (
  129. !pluginNamespace ||
  130. pluginNamespace === userNamespace ||
  131. (!config.rules && !config.processor && !config.language)
  132. ) {
  133. return config;
  134. }
  135. const result = { ...config };
  136. // update the rules
  137. if (result.rules) {
  138. const ruleIds = Object.keys(result.rules);
  139. /** @type {Record<string,RuleConfig|undefined>} */
  140. const newRules = {};
  141. for (let i = 0; i < ruleIds.length; i++) {
  142. const ruleId = ruleIds[i];
  143. const { namespace: ruleNamespace, name: ruleName } =
  144. getPluginMember(ruleId);
  145. if (ruleNamespace === pluginNamespace) {
  146. newRules[`${userNamespace}/${ruleName}`] = result.rules[ruleId];
  147. } else {
  148. newRules[ruleId] = result.rules[ruleId];
  149. }
  150. }
  151. result.rules = newRules;
  152. }
  153. // update the processor
  154. if (typeof result.processor === "string") {
  155. const { namespace: processorNamespace, name: processorName } =
  156. getPluginMember(result.processor);
  157. if (processorNamespace) {
  158. if (processorNamespace === pluginNamespace) {
  159. result.processor = `${userNamespace}/${processorName}`;
  160. }
  161. }
  162. }
  163. // update the language
  164. if (typeof result.language === "string") {
  165. const { namespace: languageNamespace, name: languageName } =
  166. getPluginMember(result.language);
  167. if (languageNamespace === pluginNamespace) {
  168. result.language = `${userNamespace}/${languageName}`;
  169. }
  170. }
  171. return result;
  172. }
  173. /**
  174. * Deeply normalizes a plugin config, traversing recursively into an arrays.
  175. * @param {string} userPluginNamespace The namespace of the plugin.
  176. * @param {Plugin} plugin The plugin object.
  177. * @param {Config|LegacyConfig|(Config|LegacyConfig)[]} pluginConfig The plugin config to normalize.
  178. * @param {string} pluginConfigName The name of the plugin config.
  179. * @return {InfiniteConfigArray} The normalized plugin config.
  180. * @throws {TypeError} If the plugin config is a legacy config.
  181. */
  182. function deepNormalizePluginConfig(
  183. userPluginNamespace,
  184. plugin,
  185. pluginConfig,
  186. pluginConfigName,
  187. ) {
  188. // if it's an array then it's definitely a new config
  189. if (Array.isArray(pluginConfig)) {
  190. return pluginConfig.map(pluginSubConfig =>
  191. deepNormalizePluginConfig(
  192. userPluginNamespace,
  193. plugin,
  194. pluginSubConfig,
  195. pluginConfigName,
  196. ),
  197. );
  198. }
  199. // if it's a legacy config, throw an error
  200. if (isLegacyConfig(pluginConfig)) {
  201. throw new TypeError(
  202. `Plugin config "${pluginConfigName}" is an eslintrc config and cannot be used in this context.`,
  203. );
  204. }
  205. return normalizePluginConfig(userPluginNamespace, plugin, pluginConfig);
  206. }
  207. /**
  208. * Finds a plugin config by name in the given config.
  209. * @param {Config} config The config object.
  210. * @param {string} pluginConfigName The name of the plugin config.
  211. * @return {InfiniteConfigArray} The plugin config.
  212. * @throws {TypeError} If the plugin config is not found or is a legacy config.
  213. */
  214. function findPluginConfig(config, pluginConfigName) {
  215. const { namespace: userPluginNamespace, name: configName } =
  216. getPluginMember(pluginConfigName);
  217. const plugin = config.plugins?.[userPluginNamespace];
  218. if (!plugin) {
  219. throw new TypeError(`Plugin "${userPluginNamespace}" not found.`);
  220. }
  221. const directConfig = plugin.configs?.[configName];
  222. // Prefer direct config, but fall back to flat config if available
  223. if (directConfig) {
  224. // Arrays are always flat configs, and non-legacy configs can be used directly
  225. if (Array.isArray(directConfig) || !isLegacyConfig(directConfig)) {
  226. return deepNormalizePluginConfig(
  227. userPluginNamespace,
  228. plugin,
  229. directConfig,
  230. pluginConfigName,
  231. );
  232. }
  233. }
  234. // If it's a legacy config, or the config does not exist => look for the flat version
  235. const flatConfig = plugin.configs?.[`flat/${configName}`];
  236. if (
  237. flatConfig &&
  238. (Array.isArray(flatConfig) || !isLegacyConfig(flatConfig))
  239. ) {
  240. return deepNormalizePluginConfig(
  241. userPluginNamespace,
  242. plugin,
  243. flatConfig,
  244. pluginConfigName,
  245. );
  246. }
  247. // If we get here, then the config was either not found or is a legacy config
  248. const message =
  249. directConfig || flatConfig
  250. ? `Plugin config "${configName}" in plugin "${userPluginNamespace}" is an eslintrc config and cannot be used in this context.`
  251. : `Plugin config "${configName}" not found in plugin "${userPluginNamespace}".`;
  252. throw new TypeError(message);
  253. }
  254. /**
  255. * Flattens an array while keeping track of the index path.
  256. * @param {any[]} configList The array to traverse.
  257. * @param {string} indexPath The index path of the value in a multidimensional array.
  258. * @return {IterableIterator<{indexPath:string, value:any}>} The flattened list of values.
  259. */
  260. function* flatTraverse(configList, indexPath = "") {
  261. for (let i = 0; i < configList.length; i++) {
  262. const newIndexPath = indexPath ? `${indexPath}[${i}]` : `[${i}]`;
  263. // if it's an array then traverse it as well
  264. if (Array.isArray(configList[i])) {
  265. yield* flatTraverse(configList[i], newIndexPath);
  266. continue;
  267. }
  268. yield { indexPath: newIndexPath, value: configList[i] };
  269. }
  270. }
  271. /**
  272. * Extends a list of config files by creating every combination of base and extension files.
  273. * @param {(string|string[])[]} [baseFiles] The base files.
  274. * @param {(string|string[])[]} [extensionFiles] The extension files.
  275. * @return {(string|string[])[]} The extended files.
  276. */
  277. function extendConfigFiles(baseFiles = [], extensionFiles = []) {
  278. if (!extensionFiles.length) {
  279. return baseFiles.concat();
  280. }
  281. if (!baseFiles.length) {
  282. return extensionFiles.concat();
  283. }
  284. /** @type {(string|string[])[]} */
  285. const result = [];
  286. for (const baseFile of baseFiles) {
  287. for (const extensionFile of extensionFiles) {
  288. /*
  289. * Each entry can be a string or array of strings. The end result
  290. * needs to be an array of strings, so we need to be sure to include
  291. * all of the items when there's an array.
  292. */
  293. const entry = [];
  294. if (Array.isArray(baseFile)) {
  295. entry.push(...baseFile);
  296. } else {
  297. entry.push(baseFile);
  298. }
  299. if (Array.isArray(extensionFile)) {
  300. entry.push(...extensionFile);
  301. } else {
  302. entry.push(extensionFile);
  303. }
  304. result.push(entry);
  305. }
  306. }
  307. return result;
  308. }
  309. /**
  310. * Extends a config object with another config object.
  311. * @param {Config} baseConfig The base config object.
  312. * @param {string} baseConfigName The name of the base config object.
  313. * @param {Config} extension The extension config object.
  314. * @param {string} extensionName The index of the extension config object.
  315. * @return {Config} The extended config object.
  316. */
  317. function extendConfig(baseConfig, baseConfigName, extension, extensionName) {
  318. const result = { ...extension };
  319. // for global ignores there is no further work to be done, we just keep everything
  320. if (!isGlobalIgnores(extension)) {
  321. // for files we need to create every combination of base and extension files
  322. if (baseConfig.files) {
  323. result.files = extendConfigFiles(baseConfig.files, extension.files);
  324. }
  325. // for ignores we just concatenation the extension ignores onto the base ignores
  326. if (baseConfig.ignores) {
  327. result.ignores = baseConfig.ignores.concat(extension.ignores ?? []);
  328. }
  329. }
  330. result.name = `${baseConfigName} > ${extensionName}`;
  331. // @ts-ignore -- ESLint types aren't updated yet
  332. if (baseConfig.basePath) {
  333. // @ts-ignore -- ESLint types aren't updated yet
  334. result.basePath = baseConfig.basePath;
  335. }
  336. return result;
  337. }
  338. /**
  339. * Processes a list of extends elements.
  340. * @param {ConfigWithExtends} config The config object.
  341. * @param {WeakMap<Config, string>} configNames The map of config objects to their names.
  342. * @return {Config[]} The flattened list of config objects.
  343. * @throws {TypeError} If the `extends` property is not an array or if nested `extends` is found.
  344. */
  345. function processExtends(config, configNames) {
  346. if (!config.extends) {
  347. return [config];
  348. }
  349. if (!Array.isArray(config.extends)) {
  350. throw new TypeError("The `extends` property must be an array.");
  351. }
  352. const {
  353. /** @type {Config[]} */
  354. extends: extendsList,
  355. /** @type {Config} */
  356. ...configObject
  357. } = config;
  358. const extensionNames = new WeakMap();
  359. // replace strings with the actual configs
  360. const objectExtends = extendsList.map(extendsElement => {
  361. if (typeof extendsElement === "string") {
  362. const pluginConfig = findPluginConfig(config, extendsElement);
  363. // assign names
  364. if (Array.isArray(pluginConfig)) {
  365. pluginConfig.forEach((pluginConfigElement, index) => {
  366. extensionNames.set(
  367. pluginConfigElement,
  368. `${extendsElement}[${index}]`,
  369. );
  370. });
  371. } else {
  372. extensionNames.set(pluginConfig, extendsElement);
  373. }
  374. return pluginConfig;
  375. }
  376. return /** @type {Config} */ (extendsElement);
  377. });
  378. const result = [];
  379. for (const { indexPath, value: extendsElement } of flatTraverse(
  380. objectExtends,
  381. )) {
  382. const extension = /** @type {Config} */ (extendsElement);
  383. if ("basePath" in extension) {
  384. throw new TypeError("'basePath' in `extends` is not allowed.");
  385. }
  386. if ("extends" in extension) {
  387. throw new TypeError("Nested 'extends' is not allowed.");
  388. }
  389. const baseConfigName = /** @type {string} */ (configNames.get(config));
  390. const extensionName =
  391. extensionNames.get(extendsElement) ??
  392. getExtensionName(extendsElement, indexPath);
  393. result.push(
  394. extendConfig(
  395. configObject,
  396. baseConfigName,
  397. extension,
  398. extensionName,
  399. ),
  400. );
  401. }
  402. /*
  403. * If the base config object has only `ignores` and `extends`, then
  404. * removing `extends` turns it into a global ignores, which is not what
  405. * we want. So we need to check if the base config object is a global ignores
  406. * and if so, we don't add it to the array.
  407. *
  408. * (The other option would be to add a `files` entry, but that would result
  409. * in a config that didn't actually do anything because there are no
  410. * other keys in the config.)
  411. */
  412. if (!isGlobalIgnores(configObject)) {
  413. result.push(configObject);
  414. }
  415. return result.flat();
  416. }
  417. /**
  418. * Processes a list of config objects and arrays.
  419. * @param {ConfigWithExtends[]} configList The list of config objects and arrays.
  420. * @param {WeakMap<Config, string>} configNames The map of config objects to their names.
  421. * @return {Config[]} The flattened list of config objects.
  422. */
  423. function processConfigList(configList, configNames) {
  424. return configList.flatMap(config => processExtends(config, configNames));
  425. }
  426. //-----------------------------------------------------------------------------
  427. // Exports
  428. //-----------------------------------------------------------------------------
  429. /**
  430. * Helper function to define a config array.
  431. * @param {ConfigWithExtendsArray} args The arguments to the function.
  432. * @returns {Config[]} The config array.
  433. * @throws {TypeError} If no arguments are provided or if an argument is not an object.
  434. */
  435. function defineConfig(...args) {
  436. const configNames = new WeakMap();
  437. const configs = [];
  438. if (args.length === 0) {
  439. throw new TypeError("Expected one or more arguments.");
  440. }
  441. // first flatten the list of configs and get the names
  442. for (const { indexPath, value } of flatTraverse(args)) {
  443. if (typeof value !== "object" || value === null) {
  444. throw new TypeError(
  445. `Expected an object but received ${String(value)}.`,
  446. );
  447. }
  448. const config = /** @type {ConfigWithExtends} */ (value);
  449. // save config name for easy reference later
  450. configNames.set(config, getConfigName(config, indexPath));
  451. configs.push(config);
  452. }
  453. return processConfigList(configs, configNames);
  454. }
  455. /**
  456. * @fileoverview Global ignores helper function.
  457. * @author Nicholas C. Zakas
  458. */
  459. //-----------------------------------------------------------------------------
  460. // Type Definitions
  461. //-----------------------------------------------------------------------------
  462. //-----------------------------------------------------------------------------
  463. // Helpers
  464. //-----------------------------------------------------------------------------
  465. let globalIgnoreCount = 0;
  466. //-----------------------------------------------------------------------------
  467. // Exports
  468. //-----------------------------------------------------------------------------
  469. /**
  470. * Creates a global ignores config with the given patterns.
  471. * @param {string[]} ignorePatterns The ignore patterns.
  472. * @param {string} [name] The name of the global ignores config.
  473. * @returns {Config} The global ignores config.
  474. * @throws {TypeError} If ignorePatterns is not an array or if it is empty.
  475. */
  476. function globalIgnores(ignorePatterns, name) {
  477. if (!Array.isArray(ignorePatterns)) {
  478. throw new TypeError("ignorePatterns must be an array");
  479. }
  480. if (ignorePatterns.length === 0) {
  481. throw new TypeError("ignorePatterns must contain at least one pattern");
  482. }
  483. const id = globalIgnoreCount++;
  484. return {
  485. name: name || `globalIgnores ${id}`,
  486. ignores: ignorePatterns,
  487. };
  488. }
  489. exports.defineConfig = defineConfig;
  490. exports.globalIgnores = globalIgnores;