import { dirname, resolve, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { createTypeScriptImportResolver } from 'eslint-import-resolver-typescript'; import type { Config } from 'eslint/config'; import type { LinterPlugin, LinterConfigWithExtends } from '../commons/index.js'; import { resolveFilesList, type ResolveFilesListOptions } from './resolveFilesList.js'; import { configureExtraneousDependencies, type ConfigureExtraneousDependenciesOptions, } from './configureExtraneousDependencies.js'; import { loadIgnoreFile, type LoadIgnoreFileOptions } from './loadIgnoreFile.js'; import { configureNoUnresolved } from './configureNoUnresolved.js'; export type CreateDynamicConfigExtendsOptions = | Config[][] | { before?: Config[][]; after?: Config[][]; }; export type CreateDynamicConfigLoadIgnoreFileOptions = | boolean | number | Partial>; export type CreateDynamicConfigSharedOptions = { importUrl: string; files?: ResolveFilesListOptions['files']; ignores?: string | string[]; sourceType?: 'module' | 'script'; extraneousDependencies?: ConfigureExtraneousDependenciesOptions; ignoreUnresolved?: string[]; extends?: CreateDynamicConfigExtendsOptions; plugins?: Record; rules?: NonNullable; settings?: NonNullable; tsConfigName?: string; tsConfigPath?: string; addTsParser?: boolean; loadIgnoreFile?: CreateDynamicConfigLoadIgnoreFileOptions; languageOptions?: NonNullable; }; export type CreateDynamicConfigOptions> = { name: string; availableConfigs: Configs; selectedConfigs: (keyof Configs)[]; } & CreateDynamicConfigSharedOptions; const addExtendsConfigs = ( configsToApply: Config[][], extendsOption?: CreateDynamicConfigExtendsOptions, ): void => { if (!extendsOption) { return; } if (Array.isArray(extendsOption)) { configsToApply.push(...extendsOption); return; } if (extendsOption.before) { configsToApply.unshift(...extendsOption.before); } if (extendsOption.after) { configsToApply.push(...extendsOption.after); } }; const addExtraneousDependenciesConfig = ( configsToApply: Config[][], extraneousDependencies?: ConfigureExtraneousDependenciesOptions, ): void => { if (!extraneousDependencies) { return; } const extraneousConfig = configureExtraneousDependencies(extraneousDependencies); if (!extraneousConfig) { return; } configsToApply.push([extraneousConfig]); }; const addIgnoreFileConfig = ( configsToApply: Config[][], rootDir: string, loadIgnoreFileOption?: CreateDynamicConfigLoadIgnoreFileOptions, ): void => { if (!loadIgnoreFileOption) { return; } let useLoadIgnoreFileOptions: LoadIgnoreFileOptions; if (loadIgnoreFileOption === true) { useLoadIgnoreFileOptions = { rootDir, limit: '.gitignore', includeGitignore: true, }; } else if (typeof loadIgnoreFileOption === 'object') { useLoadIgnoreFileOptions = { rootDir, limit: '.gitignore', includeGitignore: true, ...loadIgnoreFileOption, }; } else { useLoadIgnoreFileOptions = { rootDir, limit: loadIgnoreFileOption, includeGitignore: true, }; } const ignoreFileConfigs = loadIgnoreFile(useLoadIgnoreFileOptions); configsToApply.push(ignoreFileConfigs); }; const addNoUnresolvedConfig = ( configsToApply: Config[][], ignoreUnresolved?: string[], ): void => { if (!ignoreUnresolved || ignoreUnresolved.length === 0) { return; } const noUnresolvedConfig = configureNoUnresolved({ ignore: ignoreUnresolved }); if (!noUnresolvedConfig) { return; } configsToApply.push([noUnresolvedConfig]); }; export const createDynamicConfig = >( options: CreateDynamicConfigOptions, ): LinterConfigWithExtends => { const { name, importUrl, availableConfigs, selectedConfigs, files = 'all', ignores = [], sourceType = 'module', extraneousDependencies, extends: extendsOption, plugins = {}, rules = {}, settings = {}, tsConfigName = 'tsconfig.json', tsConfigPath = './', addTsParser = true, loadIgnoreFile: loadIgnoreFileOption = true, ignoreUnresolved = [], languageOptions, } = options; const configsToApply = selectedConfigs.map((configName) => { const configs = availableConfigs[configName]; if (!configs) { throw new Error(`Config "${String(configName)}" is not available`); } return configs; }); const rootDir = resolve(dirname(fileURLToPath(importUrl)), '.'); const filesToInclude = resolveFilesList({ files }); const filesToIgnore = Array.isArray(ignores) ? ignores : [ignores]; addExtraneousDependenciesConfig(configsToApply, extraneousDependencies); addExtendsConfigs(configsToApply, extendsOption); addIgnoreFileConfig(configsToApply, rootDir, loadIgnoreFileOption); addNoUnresolvedConfig(configsToApply, ignoreUnresolved); const settingsToUse: Record = { ...settings, }; let languageOptionsToUse: Config['languageOptions']; if (addTsParser) { const tsConfigFullPath = resolve(rootDir, tsConfigPath); languageOptionsToUse = { parserOptions: { sourceType, project: [tsConfigName], tsconfigRootDir: tsConfigFullPath, }, }; settingsToUse['import-x/resolver-next'] = createTypeScriptImportResolver({ project: join(tsConfigFullPath, tsConfigName), }); } else { languageOptionsToUse = languageOptions || { parserOptions: { ecmaVersion: 2023, // Node 20 sourceType, }, }; } const rulesToUse: NonNullable = { ...rules, }; return { name: `@homer0/${name}`, files: filesToInclude, ignores: filesToIgnore, extends: configsToApply.flat(), plugins, rules: rulesToUse, settings: settingsToUse, languageOptions: languageOptionsToUse, }; };