UNPKG

@nx/eslint

Version:

The ESLint plugin for Nx contains executors, generators and utilities used for linting JavaScript/TypeScript projects within an Nx workspace.

476 lines (475 loc) • 21 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.findEslintFile = findEslintFile; exports.isEslintConfigSupported = isEslintConfigSupported; exports.updateRelativePathsInConfig = updateRelativePathsInConfig; exports.determineEslintConfigFormat = determineEslintConfigFormat; exports.addOverrideToLintConfig = addOverrideToLintConfig; exports.updateOverrideInLintConfig = updateOverrideInLintConfig; exports.lintConfigHasOverride = lintConfigHasOverride; exports.replaceOverridesInLintConfig = replaceOverridesInLintConfig; exports.addExtendsToLintConfig = addExtendsToLintConfig; exports.addPredefinedConfigToFlatLintConfig = addPredefinedConfigToFlatLintConfig; exports.addPluginsToLintConfig = addPluginsToLintConfig; exports.addIgnoresToLintConfig = addIgnoresToLintConfig; exports.getPluginImport = getPluginImport; const devkit_1 = require("@nx/devkit"); const semver_1 = require("semver"); const config_file_1 = require("../../utils/config-file"); const flat_config_1 = require("../../utils/flat-config"); const version_utils_1 = require("../../utils/version-utils"); const versions_1 = require("../../utils/versions"); const ast_utils_1 = require("./flat-config/ast-utils"); const path_utils_1 = require("./flat-config/path-utils"); const ts = require("typescript"); const posix_1 = require("node:path/posix"); function findEslintFile(tree, projectRoot) { if (projectRoot === undefined) { for (const file of [ config_file_1.baseEsLintConfigFile, ...config_file_1.BASE_ESLINT_CONFIG_FILENAMES, ]) { if (tree.exists(file)) { return file; } } } projectRoot ??= ''; for (const file of config_file_1.ESLINT_CONFIG_FILENAMES) { if (tree.exists((0, devkit_1.joinPathFragments)(projectRoot, file))) { return file; } } return null; } function isEslintConfigSupported(tree, projectRoot = '') { const eslintFile = findEslintFile(tree, projectRoot); if (!eslintFile) { return false; } return (eslintFile.endsWith('.json') || eslintFile.endsWith('.config.js') || eslintFile.endsWith('.config.cjs') || eslintFile.endsWith('.config.mjs')); } function updateRelativePathsInConfig(tree, sourcePath, destinationPath) { if (sourcePath === destinationPath || !isEslintConfigSupported(tree, destinationPath)) { return; } const configPath = (0, devkit_1.joinPathFragments)(destinationPath, findEslintFile(tree, destinationPath)); const offset = (0, devkit_1.offsetFromRoot)(destinationPath); if ((0, flat_config_1.useFlatConfig)(tree)) { const config = tree.read(configPath, 'utf-8'); tree.write(configPath, replaceFlatConfigPaths(config, sourcePath, offset, destinationPath, tree)); } else { (0, devkit_1.updateJson)(tree, configPath, (json) => { if (typeof json.extends === 'string') { json.extends = offsetFilePath(sourcePath, json.extends, offset, tree); } else if (json.extends) { json.extends = json.extends.map((extend) => offsetFilePath(sourcePath, extend, offset, tree)); } json.overrides?.forEach((o) => { if (o.parserOptions?.project) { o.parserOptions.project = Array.isArray(o.parserOptions.project) ? o.parserOptions.project.map((p) => p.replace(sourcePath, destinationPath)) : o.parserOptions.project.replace(sourcePath, destinationPath); } }); return json; }); } } function replaceFlatConfigPaths(config, sourceRoot, offset, destinationRoot, tree) { let match; let newConfig = config; // replace requires const requireRegex = RegExp(/require\(['"](.*)['"]\)/g); while ((match = requireRegex.exec(newConfig)) !== null) { const newPath = offsetFilePath(sourceRoot, match[1], offset, tree); newConfig = newConfig.slice(0, match.index) + `require('${newPath}')` + newConfig.slice(match.index + match[0].length); } // Handle import statements const importRegex = RegExp(/import\s+.*?\s+from\s+['"](.*)['"]/g); while ((match = importRegex.exec(newConfig)) !== null) { const oldPath = match[1]; const newPath = offsetFilePath(sourceRoot, oldPath, offset, tree); // Replace the old path with the updated path newConfig = newConfig.slice(0, match.index + match[0].indexOf(oldPath)) + newPath + newConfig.slice(match.index + match[0].indexOf(oldPath) + oldPath.length); } // replace projects const projectRegex = RegExp(/project:\s?\[?['"](.*)['"]\]?/g); while ((match = projectRegex.exec(newConfig)) !== null) { const newProjectDef = match[0].replaceAll(sourceRoot, destinationRoot); newConfig = newConfig.slice(0, match.index) + newProjectDef + newConfig.slice(match.index + match[0].length); } return newConfig; } function offsetFilePath(projectRoot, pathToFile, offset, tree) { if (config_file_1.ESLINT_CONFIG_FILENAMES.some((eslintFile) => pathToFile.includes(eslintFile))) { // if the file is point to base eslint const rootEslint = findEslintFile(tree); if (rootEslint) { return (0, devkit_1.joinPathFragments)(offset, rootEslint); } } if (!pathToFile.startsWith('..')) { // not a relative path return pathToFile; } return (0, devkit_1.joinPathFragments)(offset, projectRoot, pathToFile); } function determineEslintConfigFormat(content) { const sourceFile = ts.createSourceFile('', content, ts.ScriptTarget.Latest, true); // Check if there's an `export default` in the AST const hasExportDefault = sourceFile.statements.some((statement) => ts.isExportAssignment(statement) && !statement.isExportEquals); return hasExportDefault ? 'mjs' : 'cjs'; } function addOverrideToLintConfig(tree, root, override, options = { insertAtTheEnd: true, }) { const isBase = options.checkBaseConfig && findEslintFile(tree, root).includes('.base'); if ((0, flat_config_1.useFlatConfig)(tree)) { let fileName; if (isBase) { for (const file of config_file_1.BASE_ESLINT_CONFIG_FILENAMES) { if (tree.exists((0, devkit_1.joinPathFragments)(root, file))) { fileName = (0, devkit_1.joinPathFragments)(root, file); break; } } } else { for (const f of flat_config_1.eslintFlatConfigFilenames) { if (tree.exists((0, devkit_1.joinPathFragments)(root, f))) { fileName = (0, devkit_1.joinPathFragments)(root, f); break; } } } let content = tree.read(fileName, 'utf8'); const format = content.includes('export default') ? 'mjs' : 'cjs'; const flatOverride = (0, ast_utils_1.generateFlatOverride)(override, format); // Check if the provided override using legacy eslintrc properties or plugins, if so we need to add compat if ((0, ast_utils_1.overrideNeedsCompat)(override)) { content = (0, ast_utils_1.addFlatCompatToFlatConfig)(content); } tree.write(fileName, (0, ast_utils_1.addBlockToFlatConfigExport)(content, flatOverride, options)); } else { const fileName = (0, devkit_1.joinPathFragments)(root, isBase ? config_file_1.baseEsLintConfigFile : '.eslintrc.json'); (0, devkit_1.updateJson)(tree, fileName, (json) => { json.overrides ??= []; if (options.insertAtTheEnd) { json.overrides.push(override); } else { json.overrides.unshift(override); } return json; }); } } function updateOverrideInLintConfig(tree, rootOrFile, lookup, update) { let fileName; let root = rootOrFile; if (tree.exists(rootOrFile) && tree.isFile(rootOrFile)) { fileName = rootOrFile; root = (0, posix_1.dirname)(rootOrFile); } if ((0, flat_config_1.useFlatConfig)(tree)) { if (!fileName) { for (const f of flat_config_1.eslintFlatConfigFilenames) { if (tree.exists((0, devkit_1.joinPathFragments)(root, f))) { fileName = (0, devkit_1.joinPathFragments)(root, f); break; } } } let content = tree.read(fileName, 'utf8'); content = (0, ast_utils_1.replaceOverride)(content, root, lookup, update); tree.write(fileName, content); } else { fileName ??= (0, devkit_1.joinPathFragments)(root, '.eslintrc.json'); if (!tree.exists(fileName)) { return; } const existingJson = (0, devkit_1.readJson)(tree, fileName); if (!existingJson.overrides || !existingJson.overrides.some(lookup)) { return; } (0, devkit_1.updateJson)(tree, fileName, (json) => { const index = json.overrides.findIndex(lookup); if (index !== -1) { const newOverride = update(json.overrides[index]); if (newOverride) { json.overrides[index] = newOverride; } else { json.overrides.splice(index, 1); } } return json; }); } } function lintConfigHasOverride(tree, rootOrFile, lookup, checkBaseConfig = false) { let fileName; let root = rootOrFile; if (tree.exists(rootOrFile) && tree.isFile(rootOrFile)) { fileName = rootOrFile; root = (0, posix_1.dirname)(rootOrFile); } if (!fileName && !isEslintConfigSupported(tree, root)) { return false; } const isBase = !fileName && checkBaseConfig && findEslintFile(tree, root).includes('.base'); if (isBase) { for (const file of config_file_1.BASE_ESLINT_CONFIG_FILENAMES) { if (tree.exists((0, devkit_1.joinPathFragments)(root, file))) { fileName = (0, devkit_1.joinPathFragments)(root, file); break; } } } if ((0, flat_config_1.useFlatConfig)(tree)) { if (!fileName) { for (const f of flat_config_1.eslintFlatConfigFilenames) { if (tree.exists((0, devkit_1.joinPathFragments)(root, f))) { fileName = (0, devkit_1.joinPathFragments)(root, f); break; } } } const content = tree.read(fileName, 'utf8'); return (0, ast_utils_1.hasOverride)(content, lookup); } else { fileName ??= (0, devkit_1.joinPathFragments)(root, isBase ? config_file_1.baseEsLintConfigFile : '.eslintrc.json'); return (0, devkit_1.readJson)(tree, fileName).overrides?.some(lookup) || false; } } function replaceOverridesInLintConfig(tree, root, overrides) { if ((0, flat_config_1.useFlatConfig)(tree)) { let fileName; for (const f of flat_config_1.eslintFlatConfigFilenames) { if (tree.exists((0, devkit_1.joinPathFragments)(root, f))) { fileName = (0, devkit_1.joinPathFragments)(root, f); break; } } let content = tree.read(fileName, 'utf8'); const format = content.includes('export default') ? 'mjs' : 'cjs'; // Check if any of the provided overrides using legacy eslintrc properties or plugins, if so we need to add compat if (overrides.some(ast_utils_1.overrideNeedsCompat)) { content = (0, ast_utils_1.addFlatCompatToFlatConfig)(content); } content = (0, ast_utils_1.removeOverridesFromLintConfig)(content); overrides.forEach((override) => { const flatOverride = (0, ast_utils_1.generateFlatOverride)(override, format); content = (0, ast_utils_1.addBlockToFlatConfigExport)(content, flatOverride); }); tree.write(fileName, content); } else { const fileName = (0, devkit_1.joinPathFragments)(root, '.eslintrc.json'); (0, devkit_1.updateJson)(tree, fileName, (json) => { json.overrides = overrides; return json; }); } } function addExtendsToLintConfig(tree, root, plugin, insertAtTheEnd = false) { if ((0, flat_config_1.useFlatConfig)(tree)) { const pluginExtends = []; let fileName; for (const f of flat_config_1.eslintFlatConfigFilenames) { if (tree.exists((0, devkit_1.joinPathFragments)(root, f))) { fileName = (0, devkit_1.joinPathFragments)(root, f); break; } } // Check the file extension to determine the format of the config if it is .js we look for the export const eslintConfigFormat = fileName.endsWith('.mjs') ? 'mjs' : fileName.endsWith('.cjs') ? 'cjs' : tree.read(fileName, 'utf-8').includes('module.exports') ? 'cjs' : 'mjs'; let shouldImportEslintCompat = false; // assume eslint version is 9 if not found, as it's what we'd be generating by default const eslintVersion = (0, version_utils_1.getInstalledEslintVersion)(tree) ?? versions_1.eslint9__eslintVersion; if ((0, semver_1.gte)(eslintVersion, '9.0.0')) { // eslint v9 requires the incompatible plugins to be wrapped with a helper from @eslint/compat const plugins = (Array.isArray(plugin) ? plugin : [plugin]).map((p) => typeof p === 'string' ? { name: p, needCompatFixup: false } : p); let compatiblePluginsBatch = []; plugins.forEach(({ name, needCompatFixup }) => { if (needCompatFixup) { if (compatiblePluginsBatch.length > 0) { // flush the current batch of compatible plugins and reset it pluginExtends.push((0, ast_utils_1.generatePluginExtendsElement)(compatiblePluginsBatch)); compatiblePluginsBatch = []; } // generate the extends for the incompatible plugin pluginExtends.push((0, ast_utils_1.generatePluginExtendsElementWithCompatFixup)(name)); shouldImportEslintCompat = true; } else { // add the compatible plugin to the current batch compatiblePluginsBatch.push(name); } }); if (compatiblePluginsBatch.length > 0) { // flush the batch of compatible plugins pluginExtends.push((0, ast_utils_1.generatePluginExtendsElement)(compatiblePluginsBatch)); } } else { const plugins = (Array.isArray(plugin) ? plugin : [plugin]).map((p) => typeof p === 'string' ? p : p.name); pluginExtends.push((0, ast_utils_1.generatePluginExtendsElement)(plugins)); } let content = tree.read(fileName, 'utf8'); if (shouldImportEslintCompat) { content = (0, ast_utils_1.addImportToFlatConfig)(content, ['fixupConfigRules'], '@eslint/compat'); } content = (0, ast_utils_1.addFlatCompatToFlatConfig)(content); // reverse the order to ensure they are added in the correct order at the // start of the `extends` array for (const pluginExtend of pluginExtends.reverse()) { content = (0, ast_utils_1.addBlockToFlatConfigExport)(content, pluginExtend, { insertAtTheEnd, }); } tree.write(fileName, content); if (shouldImportEslintCompat) { return (0, devkit_1.addDependenciesToPackageJson)(tree, {}, { '@eslint/compat': versions_1.eslintCompat, '@eslint/eslintrc': versions_1.eslintrcVersion }, undefined, true); } return (0, devkit_1.addDependenciesToPackageJson)(tree, {}, { '@eslint/eslintrc': versions_1.eslintrcVersion }, undefined, true); } else { const plugins = (Array.isArray(plugin) ? plugin : [plugin]).map((p) => typeof p === 'string' ? p : p.name); const fileName = (0, devkit_1.joinPathFragments)(root, '.eslintrc.json'); (0, devkit_1.updateJson)(tree, fileName, (json) => { json.extends ??= []; json.extends = [ ...plugins, ...(Array.isArray(json.extends) ? json.extends : [json.extends]), ]; return json; }); return () => { }; } } function addPredefinedConfigToFlatLintConfig(tree, root, predefinedConfigName, moduleName = 'nx', moduleImportPath = '@nx/eslint-plugin', spread = true, insertAtTheEnd = true) { if (!(0, flat_config_1.useFlatConfig)(tree)) throw new Error('Predefined configs can only be used with flat configs'); let fileName; for (const f of flat_config_1.eslintFlatConfigFilenames) { if (tree.exists((0, devkit_1.joinPathFragments)(root, f))) { fileName = (0, devkit_1.joinPathFragments)(root, f); break; } } let content = tree.read(fileName, 'utf8'); content = (0, ast_utils_1.addImportToFlatConfig)(content, moduleName, moduleImportPath); content = (0, ast_utils_1.addBlockToFlatConfigExport)(content, (0, ast_utils_1.generateFlatPredefinedConfig)(predefinedConfigName, moduleName, spread), { insertAtTheEnd }); tree.write(fileName, content); } function addPluginsToLintConfig(tree, root, plugin) { const plugins = Array.isArray(plugin) ? plugin : [plugin]; if ((0, flat_config_1.useFlatConfig)(tree)) { let fileName; for (const f of flat_config_1.eslintFlatConfigFilenames) { if (tree.exists((0, devkit_1.joinPathFragments)(root, f))) { fileName = (0, devkit_1.joinPathFragments)(root, f); break; } } let content = tree.read(fileName, 'utf8'); const mappedPlugins = []; plugins.forEach((name) => { const imp = getPluginImport(name); const varName = (0, devkit_1.names)(imp).propertyName; mappedPlugins.push({ name, varName, imp }); }); mappedPlugins.forEach(({ varName, imp }) => { content = (0, ast_utils_1.addImportToFlatConfig)(content, varName, imp); }); content = (0, ast_utils_1.addPluginsToExportsBlock)(content, mappedPlugins); tree.write(fileName, content); } else { const fileName = (0, devkit_1.joinPathFragments)(root, '.eslintrc.json'); (0, devkit_1.updateJson)(tree, fileName, (json) => { json.plugins = [...plugins, ...(json.plugins ?? [])]; return json; }); } } function addIgnoresToLintConfig(tree, root, ignorePatterns) { if ((0, flat_config_1.useFlatConfig)(tree)) { let fileName; for (const f of flat_config_1.eslintFlatConfigFilenames) { if (tree.exists((0, devkit_1.joinPathFragments)(root, f))) { fileName = (0, devkit_1.joinPathFragments)(root, f); break; } } if (!fileName) { return; } let content = tree.read(fileName, 'utf8'); if ((0, ast_utils_1.hasFlatConfigIgnoresBlock)(content)) { content = (0, ast_utils_1.addPatternsToFlatConfigIgnoresBlock)(content, ignorePatterns); tree.write(fileName, content); } else { const block = (0, ast_utils_1.generateAst)({ ignores: ignorePatterns.map((path) => (0, path_utils_1.mapFilePath)(path)), }); tree.write(fileName, (0, ast_utils_1.addBlockToFlatConfigExport)(content, block)); } } else { const fileName = (0, devkit_1.joinPathFragments)(root, '.eslintrc.json'); if (!tree.exists(fileName)) { return; } (0, devkit_1.updateJson)(tree, fileName, (json) => { const ignoreSet = new Set([ ...(json.ignorePatterns ?? []), ...ignorePatterns, ]); json.ignorePatterns = Array.from(ignoreSet); return json; }); } } function getPluginImport(pluginName) { if (pluginName.includes('eslint-plugin-')) { return pluginName; } if (!pluginName.startsWith('@')) { return `eslint-plugin-${pluginName}`; } if (!pluginName.includes('/')) { return `${pluginName}/eslint-plugin`; } const [scope, name] = pluginName.split('/'); return `${scope}/eslint-plugin-${name}`; }