UNPKG

@nx/eslint

Version:

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

1,060 lines (1,059 loc) • 52.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.removeOverridesFromLintConfig = removeOverridesFromLintConfig; exports.addPatternsToFlatConfigIgnoresBlock = addPatternsToFlatConfigIgnoresBlock; exports.hasFlatConfigIgnoresBlock = hasFlatConfigIgnoresBlock; exports.hasOverride = hasOverride; exports.replaceOverride = replaceOverride; exports.addImportToFlatConfig = addImportToFlatConfig; exports.removeImportFromFlatConfig = removeImportFromFlatConfig; exports.addBlockToFlatConfigExport = addBlockToFlatConfigExport; exports.removePlugin = removePlugin; exports.removeCompatExtends = removeCompatExtends; exports.removePredefinedConfigs = removePredefinedConfigs; exports.addPluginsToExportsBlock = addPluginsToExportsBlock; exports.addFlatCompatToFlatConfig = addFlatCompatToFlatConfig; exports.createNodeList = createNodeList; exports.generateSpreadElement = generateSpreadElement; exports.generatePluginExtendsElement = generatePluginExtendsElement; exports.generatePluginExtendsElementWithCompatFixup = generatePluginExtendsElementWithCompatFixup; exports.stringifyNodeList = stringifyNodeList; exports.generateRequire = generateRequire; exports.generateESMImport = generateESMImport; exports.overrideNeedsCompat = overrideNeedsCompat; exports.generateFlatOverride = generateFlatOverride; exports.generateFlatPredefinedConfig = generateFlatPredefinedConfig; exports.mapFilePaths = mapFilePaths; exports.generateAst = generateAst; const devkit_1 = require("@nx/devkit"); const ts = require("typescript"); const path_utils_1 = require("./path-utils"); /** * Supports direct identifiers, and those nested within an object (of arbitrary depth) * E.g. `...foo` and `...foo.bar.baz.qux` etc */ const SPREAD_ELEMENTS_REGEXP = /\s*\.\.\.[a-zA-Z0-9_]+(\.[a-zA-Z0-9_]+)*,?\n?/g; /** * Remove all overrides from the config file */ function removeOverridesFromLintConfig(content) { const source = ts.createSourceFile('', content, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS); const format = content.includes('export default') ? 'mjs' : 'cjs'; const exportsArray = format === 'mjs' ? findExportDefault(source) : findModuleExports(source); if (!exportsArray) { return content; } const changes = []; exportsArray.forEach((node, i) => { if (isOverride(node)) { const commaOffset = i < exportsArray.length - 1 || exportsArray.hasTrailingComma ? 1 : 0; changes.push({ type: devkit_1.ChangeType.Delete, start: node.pos, length: node.end - node.pos + commaOffset, }); } }); return (0, devkit_1.applyChangesToString)(content, changes); } // TODO Change name function findExportDefault(source) { return ts.forEachChild(source, function analyze(node) { if (ts.isExportAssignment(node) && ts.isArrayLiteralExpression(node.expression)) { return node.expression.elements; } }); } function findModuleExports(source) { return ts.forEachChild(source, function analyze(node) { if (ts.isExpressionStatement(node) && ts.isBinaryExpression(node.expression) && node.expression.left.getText() === 'module.exports' && ts.isArrayLiteralExpression(node.expression.right)) { return node.expression.right.elements; } }); } function addPatternsToFlatConfigIgnoresBlock(content, ignorePatterns) { const source = ts.createSourceFile('', content, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS); const format = content.includes('export default') ? 'mjs' : 'cjs'; const exportsArray = format === 'mjs' ? findExportDefault(source) : findModuleExports(source); if (!exportsArray) { return content; } const changes = []; for (const node of exportsArray) { if (!isFlatConfigIgnoresBlock(node)) { continue; } const start = node.properties.pos + 1; // keep leading line break const data = parseTextToJson(node.getFullText()); changes.push({ type: devkit_1.ChangeType.Delete, start, length: node.properties.end - start, }); data.ignores = Array.from(new Set([...(data.ignores ?? []), ...ignorePatterns])); changes.push({ type: devkit_1.ChangeType.Insert, index: start, text: ' ' + JSON.stringify(data, null, 2) .slice(2, -2) // Remove curly braces and start/end line breaks .replaceAll(/\n/g, '\n '), // Maintain indentation }); break; } return (0, devkit_1.applyChangesToString)(content, changes); } function hasFlatConfigIgnoresBlock(content) { const source = ts.createSourceFile('', content, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS); const format = content.includes('export default') ? 'mjs' : 'cjs'; const exportsArray = format === 'mjs' ? findExportDefault(source) : findModuleExports(source); if (!exportsArray) { return false; } return exportsArray.some(isFlatConfigIgnoresBlock); } function isFlatConfigIgnoresBlock(node) { return (ts.isObjectLiteralExpression(node) && node.properties.length === 1 && (node.properties[0].name.getText() === 'ignores' || node.properties[0].name.getText() === '"ignores"') && ts.isPropertyAssignment(node.properties[0]) && ts.isArrayLiteralExpression(node.properties[0].initializer)); } function isOverride(node) { return ((ts.isObjectLiteralExpression(node) && node.properties.some((p) => p.name.getText() === 'files' || p.name.getText() === '"files"')) || // detect ...compat.config(...).map(...) (ts.isSpreadElement(node) && ts.isCallExpression(node.expression) && ts.isPropertyAccessExpression(node.expression.expression) && ts.isArrowFunction(node.expression.arguments[0]) && ts.isParenthesizedExpression(node.expression.arguments[0].body))); } function hasOverride(content, lookup) { const source = ts.createSourceFile('', content, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS); const format = content.includes('export default') ? 'mjs' : 'cjs'; const exportsArray = format === 'mjs' ? findExportDefault(source) : findModuleExports(source); if (!exportsArray) { return false; } for (const node of exportsArray) { if (isOverride(node)) { let objSource; if (ts.isObjectLiteralExpression(node)) { objSource = node.getFullText(); // strip any spread elements objSource = objSource.replace(SPREAD_ELEMENTS_REGEXP, ''); } else { const fullNodeText = node['expression'].arguments[0].body.expression.getFullText(); // strip any spread elements objSource = fullNodeText.replace(SPREAD_ELEMENTS_REGEXP, ''); } const data = parseTextToJson(objSource); if (lookup(data)) { return true; } } } return false; } function parseTextToJson(text) { return (0, devkit_1.parseJson)(text // ensure property names have double quotes so that JSON.parse works .replace(/'/g, '"') .replace(/\s([a-zA-Z0-9_]+)\s*:/g, ' "$1": ') // stringify any require calls to avoid JSON parsing errors, turn them into just the string value being required .replace(/require\(['"]([^'"]+)['"]\)/g, '"$1"') .replace(/\(?await import\(['"]([^'"]+)['"]\)\)?/g, '"$1"')); } /** * Finds an override matching the lookup function and applies the update function to it */ function replaceOverride(content, root, lookup, update) { const source = ts.createSourceFile('', content, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS); const format = content.includes('export default') ? 'mjs' : 'cjs'; const exportsArray = format === 'mjs' ? findExportDefault(source) : findModuleExports(source); if (!exportsArray) { return content; } const changes = []; exportsArray.forEach((node) => { if (isOverride(node)) { let objSource; let start, end; if (ts.isObjectLiteralExpression(node)) { objSource = node.getFullText(); start = node.properties.pos + 1; // keep leading line break end = node.properties.end; } else { const fullNodeText = node['expression'].arguments[0].body.expression.getFullText(); // strip any spread elements objSource = fullNodeText.replace(SPREAD_ELEMENTS_REGEXP, ''); start = node['expression'].arguments[0].body.expression.properties.pos + (fullNodeText.length - objSource.length); end = node['expression'].arguments[0].body.expression.properties.end; } const data = parseTextToJson(objSource); if (lookup(data)) { changes.push({ type: devkit_1.ChangeType.Delete, start, length: end - start, }); let updatedData = update(data); if (updatedData) { updatedData = mapFilePaths(updatedData); const parserReplacement = format === 'mjs' ? (parser) => `(await import('${parser}'))` : (parser) => `require('${parser}')`; changes.push({ type: devkit_1.ChangeType.Insert, index: start, text: ' ' + JSON.stringify(updatedData, null, 2) .replace(/"parser": "([^"]+)"/g, (_, parser) => `"parser": ${parserReplacement(parser)}`) .slice(2, -2) // Remove curly braces and start/end line breaks .replaceAll(/\n/g, '\n '), // Maintain indentation }); } } } }); return (0, devkit_1.applyChangesToString)(content, changes); } /** * Adding import statement to the top of the file * The imports are added based on a few rules: * 1. If it's a default import and matches the variable, return content unchanged. * 2. If it's a named import and the variables are not part of the import object, add them. * 3. If no existing import and variable is a string, add a default import. * 4. If no existing import and variable is an array, add it as an object import. */ function addImportToFlatConfig(content, variable, imp) { const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); const source = ts.createSourceFile('', content, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS); const format = content.includes('export default') ? 'mjs' : 'cjs'; if (format === 'mjs') { return addESMImportToFlatConfig(source, printer, content, variable, imp); } return addCJSImportToFlatConfig(source, printer, content, variable, imp); } function addESMImportToFlatConfig(source, printer, content, variable, imp) { let existingImport; ts.forEachChild(source, (node) => { if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text === imp) { existingImport = node; } }); // Rule 1: if (existingImport && typeof variable === 'string' && existingImport.importClause?.name?.getText() === variable) { return content; } // Rule 2: if (existingImport && existingImport.importClause?.namedBindings && Array.isArray(variable)) { const namedImports = existingImport.importClause .namedBindings; const existingElements = namedImports.elements; // Filter out variables that are already imported const newVariables = variable.filter((v) => !existingElements.some((e) => e.name.getText() === v)); if (newVariables.length === 0) { return content; } const newImportSpecifiers = newVariables.map((v) => ts.factory.createImportSpecifier(false, undefined, ts.factory.createIdentifier(v))); const lastElement = existingElements[existingElements.length - 1]; const insertIndex = lastElement ? lastElement.getEnd() : namedImports.getEnd(); const insertText = printer.printList(ts.ListFormat.NamedImportsOrExportsElements, ts.factory.createNodeArray(newImportSpecifiers), source); return (0, devkit_1.applyChangesToString)(content, [ { type: devkit_1.ChangeType.Insert, index: insertIndex, text: `, ${insertText}`, }, ]); } // Rule 3: if (!existingImport && typeof variable === 'string') { const defaultImport = ts.factory.createImportDeclaration(undefined, ts.factory.createImportClause(false, ts.factory.createIdentifier(variable), undefined), ts.factory.createStringLiteral(imp)); const insert = printer.printNode(ts.EmitHint.Unspecified, defaultImport, source); return (0, devkit_1.applyChangesToString)(content, [ { type: devkit_1.ChangeType.Insert, index: 0, text: `${insert}\n`, }, ]); } // Rule 4: if (!existingImport && Array.isArray(variable)) { const objectImport = ts.factory.createImportDeclaration(undefined, ts.factory.createImportClause(false, undefined, ts.factory.createNamedImports(variable.map((v) => ts.factory.createImportSpecifier(false, undefined, ts.factory.createIdentifier(v))))), ts.factory.createStringLiteral(imp)); const insert = printer.printNode(ts.EmitHint.Unspecified, objectImport, source); return (0, devkit_1.applyChangesToString)(content, [ { type: devkit_1.ChangeType.Insert, index: 0, text: `${insert}\n`, }, ]); } } function addCJSImportToFlatConfig(source, printer, content, variable, imp) { const foundBindingVars = ts.forEachChild(source, function analyze(node) { // we can only combine object binding patterns if (!Array.isArray(variable)) { return; } if (ts.isVariableStatement(node) && ts.isVariableDeclaration(node.declarationList.declarations[0]) && ts.isObjectBindingPattern(node.declarationList.declarations[0].name) && ts.isCallExpression(node.declarationList.declarations[0].initializer) && node.declarationList.declarations[0].initializer.expression.getText() === 'require' && ts.isStringLiteral(node.declarationList.declarations[0].initializer.arguments[0]) && node.declarationList.declarations[0].initializer.arguments[0].text === imp) { return node.declarationList.declarations[0].name.elements; } }); if (foundBindingVars && Array.isArray(variable)) { const newVariables = variable.filter((v) => !foundBindingVars.some((fv) => v === fv.name.getText())); if (newVariables.length === 0) { return content; } const isMultiLine = foundBindingVars.hasTrailingComma; const pos = foundBindingVars.end; const nodes = ts.factory.createNodeArray(newVariables.map((v) => ts.factory.createBindingElement(undefined, undefined, v))); const insert = printer.printList(ts.ListFormat.ObjectBindingPatternElements, nodes, source); return (0, devkit_1.applyChangesToString)(content, [ { type: devkit_1.ChangeType.Insert, index: pos, text: isMultiLine ? `,\n${insert}` : `,${insert}`, }, ]); } const hasSameIdentifierVar = ts.forEachChild(source, function analyze(node) { // we are searching for a single variable if (Array.isArray(variable)) { return; } if (ts.isVariableStatement(node) && ts.isVariableDeclaration(node.declarationList.declarations[0]) && ts.isIdentifier(node.declarationList.declarations[0].name) && node.declarationList.declarations[0].name.getText() === variable && ts.isCallExpression(node.declarationList.declarations[0].initializer) && node.declarationList.declarations[0].initializer.expression.getText() === 'require' && ts.isStringLiteral(node.declarationList.declarations[0].initializer.arguments[0]) && node.declarationList.declarations[0].initializer.arguments[0].text === imp) { return true; } }); if (hasSameIdentifierVar) { return content; } // the import was not found, create a new one const requireStatement = generateRequire(typeof variable === 'string' ? variable : ts.factory.createObjectBindingPattern(variable.map((v) => ts.factory.createBindingElement(undefined, undefined, v))), imp); const insert = printer.printNode(ts.EmitHint.Unspecified, requireStatement, source); return (0, devkit_1.applyChangesToString)(content, [ { type: devkit_1.ChangeType.Insert, index: 0, text: `${insert}\n`, }, ]); } function existsAsNamedOrDefaultImport(node, variable) { const isNamed = node.importClause.namedBindings && ts.isNamedImports(node.importClause.namedBindings); if (Array.isArray(variable)) { return isNamed || variable.includes(node.importClause?.name?.getText()); } return ((node.importClause.namedBindings && ts.isNamedImports(node.importClause.namedBindings)) || node.importClause?.name?.getText() === variable); } /** * Remove an import from flat config */ function removeImportFromFlatConfig(content, variable, imp) { const source = ts.createSourceFile('', content, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS); const format = content.includes('export default') ? 'mjs' : 'cjs'; if (format === 'mjs') { return removeImportFromFlatConfigESM(source, content, variable, imp); } else { return removeImportFromFlatConfigCJS(source, content, variable, imp); } } function removeImportFromFlatConfigESM(source, content, variable, imp) { const changes = []; ts.forEachChild(source, (node) => { // we can only combine object binding patterns if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text === imp && node.importClause && existsAsNamedOrDefaultImport(node, variable)) { changes.push({ type: devkit_1.ChangeType.Delete, start: node.pos, length: node.end - node.pos, }); } }); return (0, devkit_1.applyChangesToString)(content, changes); } function removeImportFromFlatConfigCJS(source, content, variable, imp) { const changes = []; ts.forEachChild(source, (node) => { // we can only combine object binding patterns if (ts.isVariableStatement(node) && ts.isVariableDeclaration(node.declarationList.declarations[0]) && ts.isIdentifier(node.declarationList.declarations[0].name) && node.declarationList.declarations[0].name.getText() === variable && ts.isCallExpression(node.declarationList.declarations[0].initializer) && node.declarationList.declarations[0].initializer.expression.getText() === 'require' && ts.isStringLiteral(node.declarationList.declarations[0].initializer.arguments[0]) && node.declarationList.declarations[0].initializer.arguments[0].text === imp) { changes.push({ type: devkit_1.ChangeType.Delete, start: node.pos, length: node.end - node.pos, }); } }); return (0, devkit_1.applyChangesToString)(content, changes); } /** * Injects new ts.expression to the end of the module.exports or export default array. */ function addBlockToFlatConfigExport(content, config, options = { insertAtTheEnd: true, }) { const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); const source = ts.createSourceFile('', content, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS); const format = content.includes('export default') ? 'mjs' : 'cjs'; // find the export default array statement if (format === 'mjs') { return addBlockToFlatConfigExportESM(content, config, source, printer, options); } else { return addBlockToFlatConfigExportCJS(content, config, source, printer, options); } } function addBlockToFlatConfigExportESM(content, config, source, printer, options = { insertAtTheEnd: true, }) { const exportDefaultStatement = source.statements.find((statement) => ts.isExportAssignment(statement) && ts.isArrayLiteralExpression(statement.expression)); if (!exportDefaultStatement) return content; const exportArrayLiteral = exportDefaultStatement.expression; const updatedArrayElements = options.insertAtTheEnd ? [...exportArrayLiteral.elements, config] : [config, ...exportArrayLiteral.elements]; const updatedExportDefault = ts.factory.createExportAssignment(undefined, false, ts.factory.createArrayLiteralExpression(updatedArrayElements, true)); // update the existing export default array const updatedStatements = source.statements.map((statement) => statement === exportDefaultStatement ? updatedExportDefault : statement); const updatedSource = ts.factory.updateSourceFile(source, updatedStatements); return printer .printFile(updatedSource) .replace(/export default/, '\nexport default'); } function addBlockToFlatConfigExportCJS(content, config, source, printer, options = { insertAtTheEnd: true, }) { const exportsArray = ts.forEachChild(source, function analyze(node) { if (ts.isExpressionStatement(node) && ts.isBinaryExpression(node.expression) && node.expression.left.getText() === 'module.exports' && ts.isArrayLiteralExpression(node.expression.right)) { return node.expression.right.elements; } }); // The config is not in the format that we generate with, skip update. // This could happen during `init-migration` when extracting config from the base, but // base config was not generated by Nx. if (!exportsArray) return content; const insert = ' ' + printer .printNode(ts.EmitHint.Expression, config, source) .replaceAll(/\n/g, '\n '); if (options.insertAtTheEnd) { const index = exportsArray.length > 0 ? exportsArray.at(exportsArray.length - 1).end : exportsArray.pos; return (0, devkit_1.applyChangesToString)(content, [ { type: devkit_1.ChangeType.Insert, index, text: `,\n${insert}`, }, ]); } else { const index = exportsArray.length > 0 ? exportsArray.at(0).pos : exportsArray.pos; return (0, devkit_1.applyChangesToString)(content, [ { type: devkit_1.ChangeType.Insert, index, text: `\n${insert},\n`, }, ]); } } function removePlugin(content, pluginName, pluginImport) { const source = ts.createSourceFile('', content, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS); const format = content.includes('export default') ? 'mjs' : 'cjs'; const changes = []; if (format === 'mjs') { ts.forEachChild(source, function analyze(node) { if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text === pluginImport) { const importClause = node.importClause; if ((importClause && importClause.name) || (importClause.namedBindings && ts.isNamedImports(importClause.namedBindings))) { changes.push({ type: devkit_1.ChangeType.Delete, start: node.pos, length: node.end - node.pos, }); } } }); } else { ts.forEachChild(source, function analyze(node) { if (ts.isVariableStatement(node) && ts.isVariableDeclaration(node.declarationList.declarations[0]) && ts.isCallExpression(node.declarationList.declarations[0].initializer) && node.declarationList.declarations[0].initializer.arguments.length && ts.isStringLiteral(node.declarationList.declarations[0].initializer.arguments[0]) && node.declarationList.declarations[0].initializer.arguments[0].text === pluginImport) { changes.push({ type: devkit_1.ChangeType.Delete, start: node.pos, length: node.end - node.pos, }); } }); } ts.forEachChild(source, function analyze(node) { if (ts.isExportAssignment(node) && ts.isArrayLiteralExpression(node.expression)) { const blockElements = node.expression.elements; blockElements.forEach((element) => { if (ts.isObjectLiteralExpression(element)) { const pluginsElem = element.properties.find((prop) => prop.name?.getText() === 'plugins'); if (!pluginsElem) { return; } if (ts.isArrayLiteralExpression(pluginsElem.initializer)) { const pluginsArray = pluginsElem.initializer; const plugins = parseTextToJson(pluginsElem.initializer .getText() .replace(SPREAD_ELEMENTS_REGEXP, '')); if (plugins.length > 1) { changes.push({ type: devkit_1.ChangeType.Delete, start: pluginsArray.pos, length: pluginsArray.end - pluginsArray.pos, }); changes.push({ type: devkit_1.ChangeType.Insert, index: pluginsArray.pos, text: JSON.stringify(plugins.filter((p) => p !== pluginName)), }); } else { const keys = element.properties.map((prop) => prop.name?.getText()); if (keys.length > 1) { const removeComma = keys.indexOf('plugins') < keys.length - 1 || element.properties.hasTrailingComma; changes.push({ type: devkit_1.ChangeType.Delete, start: pluginsElem.pos + (removeComma ? 1 : 0), length: pluginsElem.end - pluginsElem.pos + (removeComma ? 1 : 0), }); } else { const removeComma = blockElements.indexOf(element) < blockElements.length - 1 || blockElements.hasTrailingComma; changes.push({ type: devkit_1.ChangeType.Delete, start: element.pos + (removeComma ? 1 : 0), length: element.end - element.pos + (removeComma ? 1 : 0), }); } } } else if (ts.isObjectLiteralExpression(pluginsElem.initializer)) { const pluginsObj = pluginsElem.initializer; if (pluginsElem.initializer.properties.length > 1) { const plugin = pluginsObj.properties.find((prop) => prop.name?.['text'] === pluginName); const removeComma = pluginsObj.properties.indexOf(plugin) < pluginsObj.properties.length - 1 || pluginsObj.properties.hasTrailingComma; changes.push({ type: devkit_1.ChangeType.Delete, start: plugin.pos + (removeComma ? 1 : 0), length: plugin.end - plugin.pos + (removeComma ? 1 : 0), }); } else { const keys = element.properties.map((prop) => prop.name?.getText()); if (keys.length > 1) { const removeComma = keys.indexOf('plugins') < keys.length - 1 || element.properties.hasTrailingComma; changes.push({ type: devkit_1.ChangeType.Delete, start: pluginsElem.pos + (removeComma ? 1 : 0), length: pluginsElem.end - pluginsElem.pos + (removeComma ? 1 : 0), }); } else { const removeComma = blockElements.indexOf(element) < blockElements.length - 1 || blockElements.hasTrailingComma; changes.push({ type: devkit_1.ChangeType.Delete, start: element.pos + (removeComma ? 1 : 0), length: element.end - element.pos + (removeComma ? 1 : 0), }); } } } } }); } }); return (0, devkit_1.applyChangesToString)(content, changes); } function removeCompatExtends(content, compatExtends) { const source = ts.createSourceFile('', content, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS); const changes = []; const format = content.includes('export default') ? 'mjs' : 'cjs'; const exportsArray = format === 'mjs' ? findExportDefault(source) : findModuleExports(source); if (!exportsArray) { return content; } exportsArray.forEach((node) => { if (ts.isSpreadElement(node) && ts.isCallExpression(node.expression) && ts.isArrowFunction(node.expression.arguments[0]) && ts.isParenthesizedExpression(node.expression.arguments[0].body) && ts.isPropertyAccessExpression(node.expression.expression) && ts.isCallExpression(node.expression.expression.expression)) { const callExp = node.expression.expression.expression; if (((callExp.expression.getText() === 'compat.config' && callExp.arguments[0].getText().includes('extends')) || callExp.expression.getText() === 'compat.extends') && compatExtends.some((ext) => callExp.arguments[0].getText().includes(ext))) { // remove the whole node changes.push({ type: devkit_1.ChangeType.Delete, start: node.pos, length: node.end - node.pos, }); // and replace it with new one const paramName = node.expression.arguments[0].parameters[0].name.getText(); const body = node.expression.arguments[0].body.expression.getFullText(); changes.push({ type: devkit_1.ChangeType.Insert, index: node.pos, text: '\n' + body.replace(new RegExp('[ \t]s*...' + paramName + '(\\.rules)?[ \t]*,?\\s*', 'g'), ''), }); } } }); return (0, devkit_1.applyChangesToString)(content, changes); } function removePredefinedConfigs(content, moduleImport, moduleVariable, configs) { const source = ts.createSourceFile('', content, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS); const format = content.includes('export default') ? 'mjs' : 'cjs'; const changes = []; let removeImport = true; const exportsArray = format === 'mjs' ? findExportDefault(source) : findModuleExports(source); if (!exportsArray) { return content; } exportsArray.forEach((node) => { if (ts.isSpreadElement(node) && ts.isElementAccessExpression(node.expression) && ts.isPropertyAccessExpression(node.expression.expression) && ts.isIdentifier(node.expression.expression.expression) && node.expression.expression.expression.getText() === moduleVariable && ts.isStringLiteral(node.expression.argumentExpression)) { const config = node.expression.argumentExpression.getText(); // Check the text without quotes if (configs.includes(config.substring(1, config.length - 1))) { changes.push({ type: devkit_1.ChangeType.Delete, start: node.pos, length: node.end - node.pos + 1, // trailing comma }); } else { // If there is still a config used, do not remove import removeImport = false; } } }); let updated = (0, devkit_1.applyChangesToString)(content, changes); if (removeImport) { updated = removeImportFromFlatConfig(updated, moduleVariable, moduleImport); } return updated; } /** * Add plugins block to the top of the export blocks */ function addPluginsToExportsBlock(content, plugins) { const pluginsBlock = ts.factory.createObjectLiteralExpression([ ts.factory.createPropertyAssignment('plugins', ts.factory.createObjectLiteralExpression(plugins.map(({ name, varName }) => { return ts.factory.createPropertyAssignment(ts.factory.createStringLiteral(name), ts.factory.createIdentifier(varName)); }))), ], false); return addBlockToFlatConfigExport(content, pluginsBlock, { insertAtTheEnd: false, }); } /** * Adds compat if missing to flat config */ function addFlatCompatToFlatConfig(content) { const result = addImportToFlatConfig(content, 'js', '@eslint/js'); const format = content.includes('export default') ? 'mjs' : 'cjs'; if (result.includes('const compat = new FlatCompat')) { return result; } if (format === 'mjs') { return addFlatCompatToFlatConfigESM(result); } else { return addFlatCompatToFlatConfigCJS(result); } } function addFlatCompatToFlatConfigCJS(content) { content = addImportToFlatConfig(content, ['FlatCompat'], '@eslint/eslintrc'); const index = content.indexOf('module.exports'); return (0, devkit_1.applyChangesToString)(content, [ { type: devkit_1.ChangeType.Insert, index: index - 1, text: ` const compat = new FlatCompat({ baseDirectory: __dirname, recommendedConfig: js.configs.recommended, }); `, }, ]); } function addFlatCompatToFlatConfigESM(content) { const importsToAdd = [ { variable: 'js', module: '@eslint/js' }, { variable: ['fileURLToPath'], module: 'url' }, { variable: ['dirname'], module: 'path' }, { variable: ['FlatCompat'], module: '@eslint/eslintrc' }, ]; for (const { variable, module } of importsToAdd) { content = addImportToFlatConfig(content, variable, module); } const index = content.indexOf('export default'); return (0, devkit_1.applyChangesToString)(content, [ { type: devkit_1.ChangeType.Insert, index: index - 1, text: ` const compat = new FlatCompat({ baseDirectory: dirname(fileURLToPath(import.meta.url)), recommendedConfig: js.configs.recommended, });\n `, }, ]); } /** * Generate node list representing the imports and the exports blocks * Optionally add flat compat initialization */ function createNodeList(importsMap, exportElements, format) { const importsList = []; Array.from(importsMap.entries()).forEach(([imp, varName]) => { if (format === 'mjs') { importsList.push(generateESMImport(varName, imp)); } else { importsList.push(generateRequire(varName, imp)); } }); const exports = format === 'mjs' ? generateESMExport(exportElements) : generateCJSExport(exportElements); return ts.factory.createNodeArray([ // add plugin imports ...importsList, ts.createSourceFile('', '', ts.ScriptTarget.Latest, false, ts.ScriptKind.JS), exports, ]); } function generateESMExport(elements) { // creates: export default = [...] return ts.factory.createExportAssignment(undefined, false, ts.factory.createArrayLiteralExpression(elements, true)); } function generateCJSExport(elements) { // creates: module.exports = [...] return ts.factory.createExpressionStatement(ts.factory.createBinaryExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('module'), ts.factory.createIdentifier('exports')), ts.factory.createToken(ts.SyntaxKind.EqualsToken), ts.factory.createArrayLiteralExpression(elements, true))); } function generateSpreadElement(name) { return ts.factory.createSpreadElement(ts.factory.createIdentifier(name)); } function generatePluginExtendsElement(plugins) { return ts.factory.createSpreadElement(ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('compat'), ts.factory.createIdentifier('extends')), undefined, plugins.map((plugin) => ts.factory.createStringLiteral(plugin)))); } function generatePluginExtendsElementWithCompatFixup(plugin) { return ts.factory.createSpreadElement(ts.factory.createCallExpression(ts.factory.createIdentifier('fixupConfigRules'), undefined, [ ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('compat'), ts.factory.createIdentifier('extends')), undefined, [ts.factory.createStringLiteral(plugin)]), ])); } /** * Stringifies TS nodes to file content string */ function stringifyNodeList(nodes) { const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); const resultFile = ts.createSourceFile('', '', ts.ScriptTarget.Latest, true, ts.ScriptKind.JS); const result = printer .printList(ts.ListFormat.MultiLine, nodes, resultFile) // add new line before compat initialization .replace(/const compat = new FlatCompat/, '\nconst compat = new FlatCompat'); if (result.includes('export default')) { return result // add new line before export default = ... .replace(/export default/, '\nexport default'); } else { return result.replace(/module.exports/, '\nmodule.exports'); } } /** * generates AST require statement */ function generateRequire(variableName, imp) { return ts.factory.createVariableStatement(undefined, ts.factory.createVariableDeclarationList([ ts.factory.createVariableDeclaration(variableName, undefined, undefined, ts.factory.createCallExpression(ts.factory.createIdentifier('require'), undefined, [ts.factory.createStringLiteral(imp)])), ], ts.NodeFlags.Const)); } // Top level imports function generateESMImport(variableName, imp) { let importClause; if (typeof variableName === 'string') { // For single variable import e.g import foo from 'module'; importClause = ts.factory.createImportClause(false, ts.factory.createIdentifier(variableName), undefined); } else { // For object binding pattern import e.g import { a, b, c } from 'module'; importClause = ts.factory.createImportClause(false, undefined, ts.factory.createNamedImports(variableName.elements.map((element) => { const propertyName = element.propertyName ? ts.isIdentifier(element.propertyName) ? element.propertyName : ts.factory.createIdentifier(element.propertyName.getText()) : undefined; return ts.factory.createImportSpecifier(false, propertyName, element.name); }))); } return ts.factory.createImportDeclaration(undefined, importClause, ts.factory.createStringLiteral(imp)); } /** * FROM: https://github.com/eslint/rewrite/blob/e2a7ec809db20e638abbad250d105ddbde88a8d5/packages/migrate-config/src/migrate-config.js#L222 * * Converts a glob pattern to a format that can be used in a flat config. * @param {string} pattern The glob pattern to convert. * @returns {string} The converted glob pattern. */ function convertGlobPattern(pattern) { const isNegated = pattern.startsWith('!'); const patternToTest = isNegated ? pattern.slice(1) : pattern; // if the pattern is already in the correct format, return it if (patternToTest === '**' || patternToTest.includes('/')) { return pattern; } return `${isNegated ? '!' : ''}**/${patternToTest}`; } // FROM: https://github.com/eslint/rewrite/blob/e2a7ec809db20e638abbad250d105ddbde88a8d5/packages/migrate-config/src/migrate-config.js#L38 const keysToCopy = ['settings', 'rules', 'processor']; function overrideNeedsCompat(override) { return override.env || override.extends || override.plugins; } /** * Generates an AST object or spread element representing a modern flat config entry, * based on a given legacy eslintrc JSON override object */ function generateFlatOverride(_override, format) { const override = mapFilePaths(_override); // We do not need the compat tooling for this override if (!overrideNeedsCompat(override)) { // Ensure files is an array let files = override.files; if (typeof files === 'string') { files = [files]; } const flatConfigOverride = { files, }; if (override.ignores) { flatConfigOverride.ignores = override.ignores; } if (override.rules) { flatConfigOverride.rules = override.rules; } // Copy over everything that stays the same keysToCopy.forEach((key) => { if (override[key]) { flatConfigOverride[key] = override[key]; } }); if (override.parser || override.parserOptions) { const languageOptions = {}; if (override.parser) { languageOptions['parser'] = override.parser; } if (override.parserOptions) { languageOptions['parserOptions'] = override.parserOptions; } if (Object.keys(languageOptions).length) { flatConfigOverride.languageOptions = languageOptions; } } if (override['languageOptions']) { flatConfigOverride.languageOptions = override['languageOptions']; } if (override.excludedFiles) { flatConfigOverride.ignores = (Array.isArray(override.excludedFiles) ? override.excludedFiles : [override.excludedFiles]).map((p) => convertGlobPattern(p)); } return generateAst(flatConfigOverride, { keyToMatch: /^(parser|rules)$/, replacer: (propertyAssignment, propertyName) => { if (propertyName === 'rules') { // Add comment that user can override rules if there are no overrides. if (ts.isObjectLiteralExpression(propertyAssignment.initializer) && propertyAssignment.initializer.properties.length === 0) { return ts.addSyntheticLeadingComment(ts.factory.createPropertyAssignment(propertyAssignment.name, ts.factory.createObjectLiteralExpression([])), ts.SyntaxKind.SingleLineCommentTrivia, ' Override or add rules here'); } return propertyAssignment; } else { // Change parser to import statement. return format === 'mjs' ? generateESMParserImport(override) : generateCJSParserImport(override); } }, }); } // At this point we are applying the flat config compat tooling to the override let { excludedFiles, parser, parserOptions, rules, files, ...rest } = override; const objectLiteralElements = [ ts.factory.createSpreadAssignment(ts.factory.createIdentifier('config')), ]; // If converting the JS rule, then we need to match ESLint default and also include .cjs and .mjs files. if ((Array.isArray(rest.extends) && rest.extends.includes('plugin:@nx/javascript')) || rest.extends === 'plugin:@nx/javascript') { const newFiles = new Set(files); newFiles.add('**/*.js'); newFiles.add('**/*.jsx'); newFiles.add('**/*.cjs'); newFiles.add('**/*.mjs'); files = Array.from(newFiles); } // If converting the TS rule, then we need to match ESLint default and also include .cts and .mts files. if ((Array.isArray(rest.extends) && rest.extends.includes('plugin:@nx/typescript')) || rest.extends === 'plugin:@nx/typescript') { const newFiles = new Set(files); newFiles.add('**/*.ts'); newFiles.add('**/*.tsx'); newFiles.add('**/*.cts'); newFiles.add('**/*.mts'); files = Array.from(newFiles); } addTSObjectProperty(objectLiteralElements, 'files', files); addTSObjectProperty(objectLiteralElements, 'excludedFiles', excludedFiles); // Apply rules (and spread ...config.rules into it as the first assignment) addTSObjectProperty(objectLiteralElements, 'rules', rules || {}); const rulesObjectAST = objectLiteralElements.pop(); const rulesObjectInitializer = rulesObjectAST.initializer; const spreadAssignment = ts.factory.createSpreadAssignment(ts.factory.createIdentifier('config.rules')); const updatedRulesProperties = [ spreadAssignment, ...rulesObjectInitializer.properties, ]; objectLiteralElements.push(ts.factory.createPropertyAssignment('rules', ts.factory.createObjectLiteralExpression(updatedRulesProperties, true))); if (parserOptions) { addTSObjectProperty(objectLiteralElements, 'languageOptions', { parserOptions, }); } return ts.factory.createSpreadElement(ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('compat'), ts.factory.createIdentifier('config')), undefined, [generateAst(rest)]), ts.factory.createIdentifier('map')), undefined, [ ts.factory.createArrowFunction(undefined, undefined, [ ts.factory.createParameterDeclaration(undefined, undefined, 'config'), ], undefined, ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), ts.factory.createParenthesizedExpression(ts.factory.createObjectLiteralExpression(objectLiteralElements, true))), ])); } function generateESMParserImport(override) { return ts.factory.createPropertyAssignment('parser', ts.factory.createAwaitExpression(ts.factory.createCallExpression(ts.factory.createIdentifier('import'), undefined, [ ts.factory.createStringLiteral(override['languageOptions']?.['parserOptions']?.parser ?? override['languageOptions']?.parser ?? override.parser), ]))); } function generateCJSParserImport(override) { return ts.factory.createPropertyAssignment('parser', ts.factory.createCallExpression(ts.factory.createIdentifier('require'), undefined, [ ts.factory.createStringLiteral(override['languageOptions']?.['parserOptions']?.parser ?? override['languageOptions']?.parser ?? override.parser), ])); } function generateFlatPredefinedConfig(predefinedConfigName, moduleName = 'nx', spread = true) { const node = ts.factory.createElementAccessExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier(moduleName), ts.factory.createIdentifier('configs')), ts.factory.createStringLiteral(predefinedConfigName)); return spread ? ts.factory.createSpreadElement(node) : node; } function mapFilePaths(_override) { const override = { ..._override, }; if (override.files) { override.files = Array.isArray(override.files) ? override.files : [override.files]; override.files = override.files.map((file) => (0, path_utils_1.mapFilePath)(file)); } if (override.excludedFiles) { override.excludedFiles = Array.isArray(override.excludedFiles) ? override.excludedFiles : [override.excludedFiles];