From f5f410516cf76e99a4c69fd0d4a988dfaee47b85 Mon Sep 17 00:00:00 2001 From: Miroslav Jonas Date: Tue, 8 Aug 2023 17:22:47 +0200 Subject: [PATCH 01/30] feat(linter): add flat support to lint-project --- packages/cypress/src/utils/add-linter.ts | 4 +- .../linter/src/executors/eslint/lint.impl.ts | 11 +- .../generators/init/global-eslint-config.ts | 46 ++- .../generators/lint-project/lint-project.ts | 145 +++++---- .../src/generators/utils/eslint-file.ts | 54 +++- .../utils/flat-config/ast-utils.spec.ts | 81 +++++ .../generators/utils/flat-config/ast-utils.ts | 297 ++++++++++++++++++ .../utils/flat-config/path-utils.ts | 31 ++ packages/linter/src/utils/flat-config.ts | 6 + .../src/generators/e2e-project/e2e-project.ts | 6 +- packages/playwright/src/utils/add-linter.ts | 51 ++- 11 files changed, 604 insertions(+), 128 deletions(-) create mode 100644 packages/linter/src/generators/utils/flat-config/ast-utils.spec.ts create mode 100644 packages/linter/src/generators/utils/flat-config/ast-utils.ts create mode 100644 packages/linter/src/generators/utils/flat-config/path-utils.ts create mode 100644 packages/linter/src/utils/flat-config.ts diff --git a/packages/cypress/src/utils/add-linter.ts b/packages/cypress/src/utils/add-linter.ts index 3a476c69c2094..0bb6ff3d560f3 100644 --- a/packages/cypress/src/utils/add-linter.ts +++ b/packages/cypress/src/utils/add-linter.ts @@ -8,7 +8,7 @@ import { updateJson, } from '@nx/devkit'; import { Linter, lintProjectGenerator } from '@nx/linter'; -import { globalJavaScriptOverrides } from '@nx/linter/src/generators/init/global-eslint-config'; +import { javaScriptOverride } from '@nx/linter/src/generators/init/global-eslint-config'; import { installedCypressVersion } from './cypress-version'; import { eslintPluginCypressVersion } from './versions'; @@ -84,7 +84,7 @@ export async function addLinterToCyProject( json.extends = ['plugin:cypress/recommended', ...json.extends]; } json.overrides ??= []; - const globals = options.rootProject ? [globalJavaScriptOverrides] : []; + const globals = options.rootProject ? [javaScriptOverride] : []; const override = { files: ['*.ts', '*.tsx', '*.js', '*.jsx'], parserOptions: !options.setParserOptionsProject diff --git a/packages/linter/src/executors/eslint/lint.impl.ts b/packages/linter/src/executors/eslint/lint.impl.ts index f7e7721ad729e..dfde3df949040 100644 --- a/packages/linter/src/executors/eslint/lint.impl.ts +++ b/packages/linter/src/executors/eslint/lint.impl.ts @@ -1,10 +1,11 @@ import { ExecutorContext, joinPathFragments } from '@nx/devkit'; import { ESLint } from 'eslint'; -import { existsSync, mkdirSync, writeFileSync } from 'fs'; +import { mkdirSync, writeFileSync } from 'fs'; import { dirname, resolve } from 'path'; import type { Schema } from './schema'; import { resolveAndInstantiateESLint } from './utility/eslint-utils'; +import { useFlatConfig } from '../../utils/flat-config'; export default async function run( options: Schema, @@ -46,11 +47,9 @@ export default async function run( * we only want to support it if the user has explicitly opted into it by converting * their root ESLint config to use eslint.config.js */ - const useFlatConfig = existsSync( - joinPathFragments(systemRoot, 'eslint.config.js') - ); + const hasFlatConfig = useFlatConfig(); - if (!eslintConfigPath && useFlatConfig) { + if (!eslintConfigPath && hasFlatConfig) { const projectRoot = context.projectsConfigurations.projects[context.projectName].root; eslintConfigPath = joinPathFragments(projectRoot, 'eslint.config.js'); @@ -59,7 +58,7 @@ export default async function run( const { eslint, ESLint } = await resolveAndInstantiateESLint( eslintConfigPath, normalizedOptions, - useFlatConfig + hasFlatConfig ); const version = ESLint.version?.split('.'); diff --git a/packages/linter/src/generators/init/global-eslint-config.ts b/packages/linter/src/generators/init/global-eslint-config.ts index b3d0d3bb9e934..3d6de20f38d39 100644 --- a/packages/linter/src/generators/init/global-eslint-config.ts +++ b/packages/linter/src/generators/init/global-eslint-config.ts @@ -4,7 +4,7 @@ import { Linter as LinterType } from 'eslint'; * This configuration is intended to apply to all TypeScript source files. * See the eslint-plugin package for what is in the referenced shareable config. */ -export const globalTypeScriptOverrides = { +export const typeScriptOverride = { files: ['*.ts', '*.tsx'], extends: ['plugin:@nx/typescript'], /** @@ -18,7 +18,7 @@ export const globalTypeScriptOverrides = { * This configuration is intended to apply to all JavaScript source files. * See the eslint-plugin package for what is in the referenced shareable config. */ -export const globalJavaScriptOverrides = { +export const javaScriptOverride = { files: ['*.js', '*.jsx'], extends: ['plugin:@nx/javascript'], /** @@ -28,25 +28,11 @@ export const globalJavaScriptOverrides = { rules: {}, }; -/** - * This configuration is intended to apply to all JSON source files. - * See the eslint-plugin package for what is in the referenced shareable config. - */ -export const globalJsonOverrides = { - files: ['*.json'], - parser: 'jsonc-eslint-parser', - /** - * Having an empty rules object present makes it more obvious to the user where they would - * extend things from if they needed to - */ - rules: {}, -}; - /** * This configuration is intended to apply to all "source code" (but not * markup like HTML, or other custom file types like GraphQL) */ -export const moduleBoundariesOverride = { +const moduleBoundariesOverride = { files: ['*.ts', '*.tsx', '*.js', '*.jsx'], rules: { '@nx/enforce-module-boundaries': [ @@ -60,6 +46,18 @@ export const moduleBoundariesOverride = { } as LinterType.RulesRecord, }; +/** + * This configuration is intended to apply to all "source code" (but not + * markup like HTML, or other custom file types like GraphQL) + */ +const jestOverride = { + files: ['*.spec.ts', '*.spec.tsx', '*.spec.js', '*.spec.jsx'], + env: { + jest: true, + }, + rules: {}, +}; + export const getGlobalEsLintConfiguration = ( unitTestRunner?: string, rootProject?: boolean @@ -77,18 +75,10 @@ export const getGlobalEsLintConfiguration = ( */ overrides: [ ...(rootProject ? [] : [moduleBoundariesOverride]), - globalTypeScriptOverrides, - globalJavaScriptOverrides, + typeScriptOverride, + javaScriptOverride, + ...(unitTestRunner === 'jest' ? [jestOverride] : []), ], }; - if (unitTestRunner === 'jest') { - config.overrides.push({ - files: ['*.spec.ts', '*.spec.tsx', '*.spec.js', '*.spec.jsx'], - env: { - jest: true, - }, - rules: {}, - }); - } return config; }; diff --git a/packages/linter/src/generators/lint-project/lint-project.ts b/packages/linter/src/generators/lint-project/lint-project.ts index 0b517238189ed..5b4f2d412d036 100644 --- a/packages/linter/src/generators/lint-project/lint-project.ts +++ b/packages/linter/src/generators/lint-project/lint-project.ts @@ -7,19 +7,27 @@ import { writeJson, } from '@nx/devkit'; -import { Linter } from '../utils/linter'; +import { Linter as LinterEnum } from '../utils/linter'; import { findEslintFile } from '../utils/eslint-file'; import { join } from 'path'; import { lintInitGenerator } from '../init/init'; +import type { Linter } from 'eslint'; import { findLintTarget, migrateConfigToMonorepoStyle, } from '../init/init-migration'; import { getProjects } from 'nx/src/generators/utils/project-configuration'; +import { useFlatConfig } from '../../utils/flat-config'; +import { + createNodeList, + generateFlatOverride, + generateSpreadElement, + stringifyAst as stringifyNodeList, +} from '../utils/flat-config/ast-utils'; interface LintProjectOptions { project: string; - linter?: Linter; + linter?: LinterEnum; eslintFilePatterns?: string[]; tsConfigPaths?: string[]; skipFormat: boolean; @@ -111,60 +119,85 @@ function createEsLintConfiguration( setParserOptionsProject: boolean ) { const eslintConfig = findEslintFile(tree); - writeJson(tree, join(projectConfig.root, `.eslintrc.json`), { - extends: eslintConfig - ? [`${offsetFromRoot(projectConfig.root)}${eslintConfig}`] - : undefined, - // Include project files to be linted since the global one excludes all files. - ignorePatterns: ['!**/*'], - overrides: [ - { - files: ['*.ts', '*.tsx', '*.js', '*.jsx'], - /** - * NOTE: We no longer set parserOptions.project by default when creating new projects. - * - * We have observed that users rarely add rules requiring type-checking to their Nx workspaces, and therefore - * do not actually need the capabilites which parserOptions.project provides. When specifying parserOptions.project, - * typescript-eslint needs to create full TypeScript Programs for you. When omitting it, it can perform a simple - * parse (and AST tranformation) of the source files it encounters during a lint run, which is much faster and much - * less memory intensive. - * - * In the rare case that users attempt to add rules requiring type-checking to their setup later on (and haven't set - * parserOptions.project), the executor will attempt to look for the particular error typescript-eslint gives you - * and provide feedback to the user. - */ - parserOptions: !setParserOptionsProject - ? undefined - : { - project: [`${projectConfig.root}/tsconfig.*?.json`], - }, - /** - * Having an empty rules object present makes it more obvious to the user where they would - * extend things from if they needed to - */ - rules: {}, - }, - { - files: ['*.ts', '*.tsx'], - rules: {}, - }, - { - files: ['*.js', '*.jsx'], - rules: {}, - }, - ...(isBuildableLibraryProject(projectConfig) - ? [ - { - files: ['*.json'], - parser: 'jsonc-eslint-parser', - rules: { - '@nx/dependency-checks': 'error', - }, - }, - ] - : []), - ], - }); + const pathToRootConfig = eslintConfig + ? `${offsetFromRoot(projectConfig.root)}${eslintConfig}` + : undefined; + const addDependencyChecks = isBuildableLibraryProject(projectConfig); + + const overrides: Linter.ConfigOverride[] = [ + { + files: ['*.ts', '*.tsx', '*.js', '*.jsx'], + /** + * NOTE: We no longer set parserOptions.project by default when creating new projects. + * + * We have observed that users rarely add rules requiring type-checking to their Nx workspaces, and therefore + * do not actually need the capabilites which parserOptions.project provides. When specifying parserOptions.project, + * typescript-eslint needs to create full TypeScript Programs for you. When omitting it, it can perform a simple + * parse (and AST tranformation) of the source files it encounters during a lint run, which is much faster and much + * less memory intensive. + * + * In the rare case that users attempt to add rules requiring type-checking to their setup later on (and haven't set + * parserOptions.project), the executor will attempt to look for the particular error typescript-eslint gives you + * and provide feedback to the user. + */ + parserOptions: !setParserOptionsProject + ? undefined + : { + project: [`${projectConfig.root}/tsconfig.*?.json`], + }, + /** + * Having an empty rules object present makes it more obvious to the user where they would + * extend things from if they needed to + */ + rules: {}, + }, + { + files: ['*.ts', '*.tsx'], + rules: {}, + }, + { + files: ['*.js', '*.jsx'], + rules: {}, + }, + ...(isBuildableLibraryProject(projectConfig) + ? [ + { + files: ['*.json'], + parser: 'jsonc-eslint-parser', + rules: { + '@nx/dependency-checks': 'error', + } as Linter.RulesRecord, + }, + ] + : []), + ]; + + if (useFlatConfig()) { + const isCompatNeeded = !!eslintConfig || addDependencyChecks; + const nodes = []; + const importMap = new Map(); + if (eslintConfig) { + importMap.set(pathToRootConfig, 'baseConfig'); + nodes.push(generateSpreadElement('baseConfig')); + } + overrides.forEach((override) => { + nodes.push(generateFlatOverride(override)); + }); + const nodeList = createNodeList(importMap, nodes, isCompatNeeded); + const content = stringifyNodeList( + nodeList, + projectConfig.root, + 'eslint.config.js' + ); + tree.write(join(projectConfig.root, 'eslint.config.js'), content); + } else { + writeJson(tree, join(projectConfig.root, `.eslintrc.json`), { + extends: eslintConfig ? [pathToRootConfig] : undefined, + // Include project files to be linted since the global one excludes all files. + ignorePatterns: ['!**/*'], + overrides, + }); + } } function isBuildableLibraryProject( diff --git a/packages/linter/src/generators/utils/eslint-file.ts b/packages/linter/src/generators/utils/eslint-file.ts index 3867121bcc631..0b479c10e61b8 100644 --- a/packages/linter/src/generators/utils/eslint-file.ts +++ b/packages/linter/src/generators/utils/eslint-file.ts @@ -1,4 +1,11 @@ -import { joinPathFragments, Tree } from '@nx/devkit'; +import { joinPathFragments, Tree, updateJson } from '@nx/devkit'; +import { Linter } from 'eslint'; +import { useFlatConfig } from '../../utils/flat-config'; +import { + addConfigToFlatConfigExport, + generateFlatOverride, + generatePluginExtendsElement, +} from './flat-config/ast-utils'; export const eslintConfigFileWhitelist = [ '.eslintrc', @@ -7,7 +14,7 @@ export const eslintConfigFileWhitelist = [ '.eslintrc.yaml', '.eslintrc.yml', '.eslintrc.json', - 'eslint.config.js', // new format that requires `ESLINT_USE_FLAT_CONFIG=true` + 'eslint.config.js', ]; export const baseEsLintConfigFile = '.eslintrc.base.json'; @@ -24,3 +31,46 @@ export function findEslintFile(tree: Tree, projectRoot = ''): string | null { return null; } + +export function addOverrideToLintConfig( + tree: Tree, + root: string, + override: Linter.ConfigOverride +) { + if (useFlatConfig()) { + const fileName = joinPathFragments(root, 'eslint.config.js'); + const flatOverride = generateFlatOverride(override); + tree.write( + fileName, + addConfigToFlatConfigExport(tree.read(fileName, 'utf8'), flatOverride) + ); + } else { + const fileName = joinPathFragments(root, '.eslintrc.json'); + updateJson(tree, fileName, (json) => { + json.overrides ?? []; + json.overrides.push(override); + return json; + }); + } +} + +export function addExtendsToLintConfig( + tree: Tree, + root: string, + plugin: string +) { + if (useFlatConfig()) { + const fileName = joinPathFragments(root, 'eslint.config.js'); + const pluginExtends = generatePluginExtendsElement(plugin); + tree.write( + fileName, + addConfigToFlatConfigExport(tree.read(fileName, 'utf8'), pluginExtends) + ); + } else { + const fileName = joinPathFragments(root, '.eslintrc.json'); + updateJson(tree, fileName, (json) => { + json.extends = [plugin, ...(json.extends ?? [])]; + return json; + }); + } +} diff --git a/packages/linter/src/generators/utils/flat-config/ast-utils.spec.ts b/packages/linter/src/generators/utils/flat-config/ast-utils.spec.ts new file mode 100644 index 0000000000000..aefd543e33803 --- /dev/null +++ b/packages/linter/src/generators/utils/flat-config/ast-utils.spec.ts @@ -0,0 +1,81 @@ +import ts = require('typescript'); +import { addConfigToFlatConfigExport, generateAst } from './ast-utils'; + +describe('ast-utils', () => { + it('should inject block to the end of the file', () => { + const content = `const baseConfig = require("../../eslint.config.js"); + module.exports = [ + ...baseConfig, + { + files: [ + "mylib/**/*.ts", + "mylib/**/*.tsx" + ], + rules: {} + }, + { ignores: ["mylib/.cache/**/*"] }, + ];`; + const result = addConfigToFlatConfigExport( + content, + generateAst({ + files: ['**/*.svg'], + rules: { + '@nx/do-something-with-svg': 'error', + }, + }) + ); + expect(result).toMatchInlineSnapshot(` + "const baseConfig = require("../../eslint.config.js"); + module.exports = [ + ...baseConfig, + { + files: [ + "mylib/**/*.ts", + "mylib/**/*.tsx" + ], + rules: {} + }, + { ignores: ["mylib/.cache/**/*"] }, + { + files: ["**/*.svg"], + rules: { "@nx/do-something-with-svg": "error" } + }, + ];" + `); + }); + + it('should inject spread to the beginning of the file', () => { + const content = `const baseConfig = require("../../eslint.config.js"); + module.exports = [ + ...baseConfig, + { + files: [ + "mylib/**/*.ts", + "mylib/**/*.tsx" + ], + rules: {} + }, + { ignores: ["mylib/.cache/**/*"] }, + ];`; + const result = addConfigToFlatConfigExport( + content, + ts.factory.createSpreadElement(ts.factory.createIdentifier('config')), + { insertAtTheEnd: false } + ); + expect(result).toMatchInlineSnapshot(` + "const baseConfig = require("../../eslint.config.js"); + module.exports = [ + ...config, + ...baseConfig, + { + files: [ + "mylib/**/*.ts", + "mylib/**/*.tsx" + ], + rules: {} + }, + { ignores: ["mylib/.cache/**/*"] }, + ];" + `); + }); +}); diff --git a/packages/linter/src/generators/utils/flat-config/ast-utils.ts b/packages/linter/src/generators/utils/flat-config/ast-utils.ts new file mode 100644 index 0000000000000..bc9c57993c1c5 --- /dev/null +++ b/packages/linter/src/generators/utils/flat-config/ast-utils.ts @@ -0,0 +1,297 @@ +import { + ChangeType, + applyChangesToString, + joinPathFragments, +} from '@nx/devkit'; +import { Linter } from 'eslint'; +import * as ts from 'typescript'; + +/** + * Injects new ts.expression to the end of the module.exports array. + */ +export function addConfigToFlatConfigExport( + content: string, + config: ts.Expression | ts.SpreadElement, + options: { insertAtTheEnd: boolean } = { insertAtTheEnd: true } +): string { + const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); + const source = ts.createSourceFile( + '', + content, + ts.ScriptTarget.Latest, + true, + ts.ScriptKind.JS + ); + + 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; + } + }); + console.log(exportsArray); + const insert = printer.printNode(ts.EmitHint.Expression, config, source); + if (options.insertAtTheEnd) { + return applyChangesToString(content, [ + { + type: ChangeType.Insert, + index: exportsArray[exportsArray.length - 1].end, + text: `,\n${insert}`, + }, + ]); + } else { + return applyChangesToString(content, [ + { + type: ChangeType.Insert, + index: exportsArray[0].pos, + text: `\n${insert},`, + }, + ]); + } +} + +const DEFAULT_FLAT_CONFIG = ` +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + }); + `; + +export function createNodeList( + importsMap: Map, + exportElements: ts.Expression[], + isFlatCompatNeeded: boolean +): ts.NodeArray< + ts.VariableStatement | ts.Identifier | ts.ExpressionStatement | ts.SourceFile +> { + const importsList = []; + if (isFlatCompatNeeded) { + importsMap.set('@eslint/js', 'js'); + + importsList.push( + generateRequire( + ts.factory.createObjectBindingPattern([ + ts.factory.createBindingElement(undefined, undefined, 'FlatCompat'), + ]), + '@eslint/eslintrc' + ) + ); + } + + // generateRequire(varName, imp, ts.factory); + Array.from(importsMap.entries()).forEach(([imp, varName]) => { + importsList.push(generateRequire(varName, imp)); + }); + + return ts.factory.createNodeArray([ + // add plugin imports + ...importsList, + ts.createSourceFile( + '', + isFlatCompatNeeded ? DEFAULT_FLAT_CONFIG : '', + ts.ScriptTarget.Latest, + false, + ts.ScriptKind.JS + ), + // creates: + // module.exports = [ ... ]; + 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(exportElements, true) + ) + ), + ]); +} + +export function generateSpreadElement(name: string): ts.SpreadElement { + return ts.factory.createSpreadElement(ts.factory.createIdentifier(name)); +} + +export function generatePluginExtendsElement(plugin: string): ts.SpreadElement { + return ts.factory.createSpreadElement( + 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 + */ +export function stringifyAst( + nodes: ts.NodeArray< + | ts.VariableStatement + | ts.Identifier + | ts.ExpressionStatement + | ts.SourceFile + >, + root: string, + fileName: string +): string { + const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); + const resultFile = ts.createSourceFile( + joinPathFragments(root, fileName), + '', + ts.ScriptTarget.Latest, + true, + ts.ScriptKind.JS + ); + return printer.printList(ts.ListFormat.MultiLine, nodes, resultFile); +} + +/** + * generates AST require statement + */ +export function generateRequire( + variableName: string | ts.ObjectBindingPattern, + imp: string +): ts.VariableStatement { + 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 + ) + ); +} + +/** + * Generates AST object or spread element based on JSON override object + */ +export function generateFlatOverride( + override: Linter.ConfigOverride +): ts.ObjectLiteralExpression | ts.SpreadElement { + if ( + !override.env && + !override.extends && + !override.plugins && + !override.parser + ) { + return generateAst(override); + } + + const { files, excludedFiles, rules, ...rest } = override; + + const objectLiteralElements: ts.ObjectLiteralElementLike[] = [ + ts.factory.createSpreadAssignment(ts.factory.createIdentifier('config')), + ]; + addTSObjectProperty(objectLiteralElements, 'files', files); + addTSObjectProperty(objectLiteralElements, 'excludedFiles', excludedFiles); + addTSObjectProperty(objectLiteralElements, 'rules', rules); + + 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 addTSObjectProperty( + elements: ts.ObjectLiteralElementLike[], + key: string, + value: unknown +) { + if (value) { + elements.push(ts.factory.createPropertyAssignment(key, generateAst(value))); + } +} + +/** + * Generates an AST from a JSON-type input + */ +export function generateAst(input: unknown): T { + if (Array.isArray(input)) { + return ts.factory.createArrayLiteralExpression( + input.map((item) => generateAst(item)), + input.length > 1 // multiline only if more than one item + ) as T; + } + if (input === null) { + return ts.factory.createNull() as T; + } + if (typeof input === 'object') { + return ts.factory.createObjectLiteralExpression( + Object.entries(input) + .filter(([_, value]) => value !== undefined) + .map(([key, value]) => + ts.factory.createPropertyAssignment( + isValidKey(key) ? key : ts.factory.createStringLiteral(key), + generateAst(value) + ) + ), + Object.keys(input).length > 1 // multiline only if more than one property + ) as T; + } + if (typeof input === 'string') { + return ts.factory.createStringLiteral(input) as T; + } + if (typeof input === 'number') { + return ts.factory.createNumericLiteral(input) as T; + } + if (typeof input === 'boolean') { + return (input ? ts.factory.createTrue() : ts.factory.createFalse()) as T; + } + // since we are parsing JSON, this should never happen + throw new Error(`Unknown type: ${typeof input} `); +} + +function isValidKey(key: string): boolean { + return /^[a-zA-Z0-9_]+$/.test(key); +} diff --git a/packages/linter/src/generators/utils/flat-config/path-utils.ts b/packages/linter/src/generators/utils/flat-config/path-utils.ts new file mode 100644 index 0000000000000..eae969773c231 --- /dev/null +++ b/packages/linter/src/generators/utils/flat-config/path-utils.ts @@ -0,0 +1,31 @@ +import { joinPathFragments } from '@nx/devkit'; +import type { Linter } from 'eslint'; + +export function updateFiles( + override: Linter.ConfigOverride, + root: string +) { + if (override.files) { + override.files = Array.isArray(override.files) + ? override.files + : [override.files]; + override.files = override.files.map((file) => mapFilePath(file, root)); + } + return override; +} + +function mapFilePath(filePath: string, root: string) { + if (filePath.startsWith('!')) { + const fileWithoutBang = filePath.slice(1); + if (fileWithoutBang.startsWith('*.')) { + return `!${joinPathFragments(root, '**', fileWithoutBang)}`; + } else { + return `!${joinPathFragments(root, fileWithoutBang)}`; + } + } + if (filePath.startsWith('*.')) { + return joinPathFragments(root, '**', filePath); + } else { + return joinPathFragments(root, filePath); + } +} diff --git a/packages/linter/src/utils/flat-config.ts b/packages/linter/src/utils/flat-config.ts new file mode 100644 index 0000000000000..d00f7fca902d6 --- /dev/null +++ b/packages/linter/src/utils/flat-config.ts @@ -0,0 +1,6 @@ +import { joinPathFragments, workspaceRoot } from '@nx/devkit'; +import { existsSync } from 'fs'; + +export function useFlatConfig(): boolean { + return existsSync(joinPathFragments(workspaceRoot, 'eslint.config.js')); +} diff --git a/packages/node/src/generators/e2e-project/e2e-project.ts b/packages/node/src/generators/e2e-project/e2e-project.ts index 4569436ff7747..1e89e9c021c82 100644 --- a/packages/node/src/generators/e2e-project/e2e-project.ts +++ b/packages/node/src/generators/e2e-project/e2e-project.ts @@ -16,8 +16,8 @@ import { import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils'; import { Linter, lintProjectGenerator } from '@nx/linter'; import { - globalJavaScriptOverrides, - globalTypeScriptOverrides, + javaScriptOverride, + typeScriptOverride, } from '@nx/linter/src/generators/init/global-eslint-config'; import * as path from 'path'; import { join } from 'path'; @@ -126,7 +126,7 @@ export async function e2eProjectGeneratorInternal( } json.overrides = [ ...(options.rootProject - ? [globalTypeScriptOverrides, globalJavaScriptOverrides] + ? [typeScriptOverride, javaScriptOverride] : []), /** * In order to ensure maximum efficiency when typescript-eslint generates TypeScript Programs diff --git a/packages/playwright/src/utils/add-linter.ts b/packages/playwright/src/utils/add-linter.ts index 30405da38940b..aae4c8ffef9c2 100644 --- a/packages/playwright/src/utils/add-linter.ts +++ b/packages/playwright/src/utils/add-linter.ts @@ -8,8 +8,12 @@ import { updateJson, } from '@nx/devkit'; import { Linter, lintProjectGenerator } from '@nx/linter'; -import { globalJavaScriptOverrides } from '@nx/linter/src/generators/init/global-eslint-config'; +import { javaScriptOverride } from '@nx/linter/src/generators/init/global-eslint-config'; import { eslintPluginPlaywrightVersion } from './versions'; +import { + addExtendsToLintConfig, + addOverrideToLintConfig, +} from '@nx/linter/src/generators/utils/eslint-file'; export interface PlaywrightLinterOptions { project: string; @@ -66,38 +70,23 @@ export async function addLinterToPlaywrightProject( : () => {} ); - updateJson( + addExtendsToLintConfig( tree, - joinPathFragments(projectConfig.root, '.eslintrc.json'), - (json) => { - if (options.rootProject) { - json.plugins = ['@nx']; - json.extends = ['plugin:playwright/recommended']; - } else { - json.extends = ['plugin:playwright/recommended', ...json.extends]; - } - json.overrides ??= []; - const globals = options.rootProject ? [globalJavaScriptOverrides] : []; - const override = { - files: ['*.ts', '*.tsx', '*.js', '*.jsx'], - parserOptions: !options.setParserOptionsProject - ? undefined - : { - project: `${projectConfig.root}/tsconfig.*?.json`, - }, - rules: {}, - }; - const palywrightFiles = [ - { - ...override, - files: [`${options.directory}/**/*.{ts,js,tsx,jsx}`], - }, - ]; - json.overrides.push(...globals); - json.overrides.push(...palywrightFiles); - return json; - } + projectConfig.root, + 'plugin:playwright/recommended' ); + if (options.rootProject) { + addOverrideToLintConfig(tree, projectConfig.root, javaScriptOverride); + } + addOverrideToLintConfig(tree, projectConfig.root, { + files: [`${options.directory}/**/*.{ts,js,tsx,jsx}`], + parserOptions: !options.setParserOptionsProject + ? undefined + : { + project: `${projectConfig.root}/tsconfig.*?.json`, + }, + rules: {}, + }); return runTasksInSerial(...tasks); } From 8925d82e8ee1ed112e70886a03d5145a532968dc Mon Sep 17 00:00:00 2001 From: Miroslav Jonas Date: Wed, 9 Aug 2023 12:05:11 +0200 Subject: [PATCH 02/30] feat(linter): move ast logic to utils --- .../linter/src/executors/eslint/lint.impl.ts | 7 +- .../converters/generate-ast.ts | 68 ----------- .../converters/json-converter.ts | 115 ++---------------- .../generators/lint-project/lint-project.ts | 22 ++-- .../src/generators/utils/eslint-file.ts | 4 +- .../generators/utils/flat-config/ast-utils.ts | 41 ++++++- 6 files changed, 62 insertions(+), 195 deletions(-) delete mode 100644 packages/linter/src/generators/convert-to-flat-config/converters/generate-ast.ts diff --git a/packages/linter/src/executors/eslint/lint.impl.ts b/packages/linter/src/executors/eslint/lint.impl.ts index dfde3df949040..57c1dc011202f 100644 --- a/packages/linter/src/executors/eslint/lint.impl.ts +++ b/packages/linter/src/executors/eslint/lint.impl.ts @@ -103,9 +103,8 @@ export default async function run( } console.error(` -Error: You have attempted to use a lint rule which requires the full TypeScript type-checker to be available, but you do not have \`parserOptions.project\` configured to point at your project tsconfig.json files in the relevant TypeScript file "overrides" block of your project ESLint config ${ - eslintConfigPath || eslintConfigPathForError - } +Error: You have attempted to use a lint rule which requires the full TypeScript type-checker to be available, but you do not have \`parserOptions.project\` configured to point at your project tsconfig.json files in the relevant TypeScript file "overrides" block of your project ESLint config ${eslintConfigPath || eslintConfigPathForError + } Please see https://nx.dev/guides/eslint for full guidance on how to resolve this issue. `); @@ -129,7 +128,7 @@ Please see https://nx.dev/guides/eslint for full guidance on how to resolve this .filter((pattern) => !!pattern) .map((pattern) => `- '${pattern}'`); if (ignoredPatterns.length) { - const ignoreSection = useFlatConfig + const ignoreSection = hasFlatConfig ? `'ignores' configuration` : `'.eslintignore' file`; throw new Error( diff --git a/packages/linter/src/generators/convert-to-flat-config/converters/generate-ast.ts b/packages/linter/src/generators/convert-to-flat-config/converters/generate-ast.ts deleted file mode 100644 index 5a956bc2ba051..0000000000000 --- a/packages/linter/src/generators/convert-to-flat-config/converters/generate-ast.ts +++ /dev/null @@ -1,68 +0,0 @@ -import * as ts from 'typescript'; - -/** - * Generates an AST from a JSON-type input - */ -export function generateAst(input: unknown): T { - if (Array.isArray(input)) { - return ts.factory.createArrayLiteralExpression( - input.map((item) => generateAst(item)), - input.length > 1 // multiline only if more than one item - ) as T; - } - if (input === null) { - return ts.factory.createNull() as T; - } - if (typeof input === 'object') { - return ts.factory.createObjectLiteralExpression( - Object.entries(input) - .filter(([_, value]) => value !== undefined) - .map(([key, value]) => - ts.factory.createPropertyAssignment( - isValidKey(key) ? key : ts.factory.createStringLiteral(key), - generateAst(value) - ) - ), - Object.keys(input).length > 1 // multiline only if more than one property - ) as T; - } - if (typeof input === 'string') { - return ts.factory.createStringLiteral(input) as T; - } - if (typeof input === 'number') { - return ts.factory.createNumericLiteral(input) as T; - } - if (typeof input === 'boolean') { - return (input ? ts.factory.createTrue() : ts.factory.createFalse()) as T; - } - // since we are parsing JSON, this should never happen - throw new Error(`Unknown type: ${typeof input}`); -} - -export function generateRequire( - variableName: string | ts.ObjectBindingPattern, - imp: string -): ts.VariableStatement { - 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 - ) - ); -} - -function isValidKey(key: string): boolean { - return /^[a-zA-Z0-9_]+$/.test(key); -} diff --git a/packages/linter/src/generators/convert-to-flat-config/converters/json-converter.ts b/packages/linter/src/generators/convert-to-flat-config/converters/json-converter.ts index a745f4efb38ec..1fa0d83a508bd 100644 --- a/packages/linter/src/generators/convert-to-flat-config/converters/json-converter.ts +++ b/packages/linter/src/generators/convert-to-flat-config/converters/json-converter.ts @@ -7,8 +7,8 @@ import { import { join } from 'path'; import { ESLint, Linter } from 'eslint'; import * as ts from 'typescript'; -import { generateAst, generateRequire } from './generate-ast'; import { eslintrcVersion } from '../../../utils/versions'; +import { generateAst, generateFlatOverride, generatePluginExtendsElement, generateRequire, generateSpreadElement, mapFilePath } from '../../utils/flat-config/ast-utils'; /** * Converts an ESLint JSON config to a flat config. @@ -129,7 +129,6 @@ export function convertEslintJsonToFlatConfig( if (config.overrides) { config.overrides.forEach((override) => { - updateFiles(override, root); if ( override.env || override.extends || @@ -137,10 +136,8 @@ export function convertEslintJsonToFlatConfig( override.parser ) { isFlatCompatNeeded = true; - addFlattenedOverride(override, exportElements); - } else { - exportElements.push(generateAst(override)); } + exportElements.push(generateFlatOverride(override, root)); }); } @@ -218,22 +215,6 @@ function updateFiles( return override; } -function mapFilePath(filePath: string, root: string) { - if (filePath.startsWith('!')) { - const fileWithoutBang = filePath.slice(1); - if (fileWithoutBang.startsWith('*.')) { - return `!${join(root, '**', fileWithoutBang)}`; - } else { - return `!${join(root, fileWithoutBang)}`; - } - } - if (filePath.startsWith('*.')) { - return join(root, '**', filePath); - } else { - return join(root, filePath); - } -} - // add parsed extends to export blocks and add import statements function addExtends( importsMap: Map, @@ -255,7 +236,7 @@ function addExtends( if (imp.match(/\.eslintrc(.base)?\.json$/)) { const localName = index ? `baseConfig${index}` : 'baseConfig'; configBlocks.push( - ts.factory.createSpreadElement(ts.factory.createIdentifier(localName)) + generateSpreadElement(localName) ); const newImport = imp.replace( /^(.*)\.eslintrc(.base)?\.json$/, @@ -311,17 +292,7 @@ function addExtends( } ); - const pluginExtendsSpread = ts.factory.createSpreadElement( - ts.factory.createCallExpression( - ts.factory.createPropertyAccessExpression( - ts.factory.createIdentifier('compat'), - ts.factory.createIdentifier('extends') - ), - undefined, - eslintrcConfigs.map((plugin) => ts.factory.createStringLiteral(plugin)) - ) - ); - configBlocks.push(pluginExtendsSpread); + configBlocks.push(generatePluginExtendsElement(eslintrcConfigs)); } return isFlatCompatNeeded; @@ -371,11 +342,11 @@ function addPlugins( ), ...(config.processor ? [ - ts.factory.createPropertyAssignment( - 'processor', - ts.factory.createStringLiteral(config.processor) - ), - ] + ts.factory.createPropertyAssignment( + 'processor', + ts.factory.createStringLiteral(config.processor) + ), + ] : []), ], false @@ -397,74 +368,6 @@ function addParser( ); } -function addFlattenedOverride( - override: Linter.ConfigOverride, - configBlocks: ts.Expression[] -) { - const { files, excludedFiles, rules, ...rest } = override; - - const objectLiteralElements: ts.ObjectLiteralElementLike[] = [ - ts.factory.createSpreadAssignment(ts.factory.createIdentifier('config')), - ]; - if (files) { - objectLiteralElements.push( - ts.factory.createPropertyAssignment('files', generateAst(files)) - ); - } - if (excludedFiles) { - objectLiteralElements.push( - ts.factory.createPropertyAssignment( - 'excludedFiles', - generateAst(excludedFiles) - ) - ); - } - if (rules) { - objectLiteralElements.push( - ts.factory.createPropertyAssignment('rules', generateAst(rules)) - ); - } - - const overrideSpread = 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 - ) - ) - ), - ] - ) - ); - configBlocks.push(overrideSpread); -} - const DEFAULT_FLAT_CONFIG = ` const compat = new FlatCompat({ baseDirectory: __dirname, diff --git a/packages/linter/src/generators/lint-project/lint-project.ts b/packages/linter/src/generators/lint-project/lint-project.ts index 5b4f2d412d036..886a042636a30 100644 --- a/packages/linter/src/generators/lint-project/lint-project.ts +++ b/packages/linter/src/generators/lint-project/lint-project.ts @@ -143,8 +143,8 @@ function createEsLintConfiguration( parserOptions: !setParserOptionsProject ? undefined : { - project: [`${projectConfig.root}/tsconfig.*?.json`], - }, + project: [`${projectConfig.root}/tsconfig.*?.json`], + }, /** * Having an empty rules object present makes it more obvious to the user where they would * extend things from if they needed to @@ -161,14 +161,14 @@ function createEsLintConfiguration( }, ...(isBuildableLibraryProject(projectConfig) ? [ - { - files: ['*.json'], - parser: 'jsonc-eslint-parser', - rules: { - '@nx/dependency-checks': 'error', - } as Linter.RulesRecord, - }, - ] + { + files: ['*.json'], + parser: 'jsonc-eslint-parser', + rules: { + '@nx/dependency-checks': 'error', + } as Linter.RulesRecord, + }, + ] : []), ]; @@ -181,7 +181,7 @@ function createEsLintConfiguration( nodes.push(generateSpreadElement('baseConfig')); } overrides.forEach((override) => { - nodes.push(generateFlatOverride(override)); + nodes.push(generateFlatOverride(override, projectConfig.root)); }); const nodeList = createNodeList(importMap, nodes, isCompatNeeded); const content = stringifyNodeList( diff --git a/packages/linter/src/generators/utils/eslint-file.ts b/packages/linter/src/generators/utils/eslint-file.ts index 0b479c10e61b8..2131add8e0319 100644 --- a/packages/linter/src/generators/utils/eslint-file.ts +++ b/packages/linter/src/generators/utils/eslint-file.ts @@ -39,7 +39,7 @@ export function addOverrideToLintConfig( ) { if (useFlatConfig()) { const fileName = joinPathFragments(root, 'eslint.config.js'); - const flatOverride = generateFlatOverride(override); + const flatOverride = generateFlatOverride(override, root); tree.write( fileName, addConfigToFlatConfigExport(tree.read(fileName, 'utf8'), flatOverride) @@ -61,7 +61,7 @@ export function addExtendsToLintConfig( ) { if (useFlatConfig()) { const fileName = joinPathFragments(root, 'eslint.config.js'); - const pluginExtends = generatePluginExtendsElement(plugin); + const pluginExtends = generatePluginExtendsElement([plugin]); tree.write( fileName, addConfigToFlatConfigExport(tree.read(fileName, 'utf8'), pluginExtends) diff --git a/packages/linter/src/generators/utils/flat-config/ast-utils.ts b/packages/linter/src/generators/utils/flat-config/ast-utils.ts index bc9c57993c1c5..ddcbd30c96baf 100644 --- a/packages/linter/src/generators/utils/flat-config/ast-utils.ts +++ b/packages/linter/src/generators/utils/flat-config/ast-utils.ts @@ -116,7 +116,7 @@ export function generateSpreadElement(name: string): ts.SpreadElement { return ts.factory.createSpreadElement(ts.factory.createIdentifier(name)); } -export function generatePluginExtendsElement(plugin: string): ts.SpreadElement { +export function generatePluginExtendsElement(plugins: string[]): ts.SpreadElement { return ts.factory.createSpreadElement( ts.factory.createCallExpression( ts.factory.createPropertyAccessExpression( @@ -124,7 +124,7 @@ export function generatePluginExtendsElement(plugin: string): ts.SpreadElement { ts.factory.createIdentifier('extends') ), undefined, - [ts.factory.createStringLiteral(plugin)] + plugins.map(plugin => ts.factory.createStringLiteral(plugin)) ) ); } @@ -184,8 +184,10 @@ export function generateRequire( * Generates AST object or spread element based on JSON override object */ export function generateFlatOverride( - override: Linter.ConfigOverride + override: Linter.ConfigOverride, + root: string ): ts.ObjectLiteralExpression | ts.SpreadElement { + mapFilePaths(override, root); if ( !override.env && !override.extends && @@ -194,7 +196,6 @@ export function generateFlatOverride( ) { return generateAst(override); } - const { files, excludedFiles, rules, ...rest } = override; const objectLiteralElements: ts.ObjectLiteralElementLike[] = [ @@ -243,6 +244,38 @@ export function generateFlatOverride( ); } +function mapFilePaths(override: Linter.ConfigOverride, + root: string) { + if (override.files) { + override.files = Array.isArray(override.files) + ? override.files + : [override.files]; + override.files = override.files.map((file) => mapFilePath(file, root)); + } + if (override.excludedFiles) { + override.excludedFiles = Array.isArray(override.excludedFiles) + ? override.excludedFiles + : [override.excludedFiles]; + override.excludedFiles = override.excludedFiles.map((file) => mapFilePath(file, root)); + } +} + +export function mapFilePath(filePath: string, root: string) { + if (filePath.startsWith('!')) { + const fileWithoutBang = filePath.slice(1); + if (fileWithoutBang.startsWith('*.')) { + return `!${joinPathFragments(root, '**', fileWithoutBang)}`; + } else { + return `!${joinPathFragments(root, fileWithoutBang)}`; + } + } + if (filePath.startsWith('*.')) { + return joinPathFragments(root, '**', filePath); + } else { + return joinPathFragments(root, filePath); + } +} + function addTSObjectProperty( elements: ts.ObjectLiteralElementLike[], key: string, From a69f3f2a6736ac1299e6ec3792205c7fda44a3bc Mon Sep 17 00:00:00 2001 From: Miroslav Jonas Date: Wed, 9 Aug 2023 12:10:09 +0200 Subject: [PATCH 03/30] feat(linter): move ast logic to utils --- .../linter/src/executors/eslint/lint.impl.ts | 5 +- .../converters/json-converter.ts | 113 +++--------------- .../generators/lint-project/lint-project.ts | 22 ++-- .../generators/utils/flat-config/ast-utils.ts | 18 ++- 4 files changed, 44 insertions(+), 114 deletions(-) diff --git a/packages/linter/src/executors/eslint/lint.impl.ts b/packages/linter/src/executors/eslint/lint.impl.ts index 57c1dc011202f..84c0527e2c351 100644 --- a/packages/linter/src/executors/eslint/lint.impl.ts +++ b/packages/linter/src/executors/eslint/lint.impl.ts @@ -103,8 +103,9 @@ export default async function run( } console.error(` -Error: You have attempted to use a lint rule which requires the full TypeScript type-checker to be available, but you do not have \`parserOptions.project\` configured to point at your project tsconfig.json files in the relevant TypeScript file "overrides" block of your project ESLint config ${eslintConfigPath || eslintConfigPathForError - } +Error: You have attempted to use a lint rule which requires the full TypeScript type-checker to be available, but you do not have \`parserOptions.project\` configured to point at your project tsconfig.json files in the relevant TypeScript file "overrides" block of your project ESLint config ${ + eslintConfigPath || eslintConfigPathForError + } Please see https://nx.dev/guides/eslint for full guidance on how to resolve this issue. `); diff --git a/packages/linter/src/generators/convert-to-flat-config/converters/json-converter.ts b/packages/linter/src/generators/convert-to-flat-config/converters/json-converter.ts index 1fa0d83a508bd..9f76e47f890d9 100644 --- a/packages/linter/src/generators/convert-to-flat-config/converters/json-converter.ts +++ b/packages/linter/src/generators/convert-to-flat-config/converters/json-converter.ts @@ -5,10 +5,18 @@ import { readJson, } from '@nx/devkit'; import { join } from 'path'; -import { ESLint, Linter } from 'eslint'; +import { ESLint } from 'eslint'; import * as ts from 'typescript'; import { eslintrcVersion } from '../../../utils/versions'; -import { generateAst, generateFlatOverride, generatePluginExtendsElement, generateRequire, generateSpreadElement, mapFilePath } from '../../utils/flat-config/ast-utils'; +import { + createNodeList, + generateAst, + generateFlatOverride, + generatePluginExtendsElement, + generateSpreadElement, + mapFilePath, + stringifyNodeList, +} from '../../utils/flat-config/ast-utils'; /** * Converts an ESLint JSON config to a flat config. @@ -176,20 +184,8 @@ export function convertEslintJsonToFlatConfig( exportElements, isFlatCompatNeeded ); - const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); - const resultFile = ts.createSourceFile( - join(root, destinationFile), - '', - ts.ScriptTarget.Latest, - true, - ts.ScriptKind.JS - ); - const result = printer.printList( - ts.ListFormat.MultiLine, - nodeList, - resultFile - ); - tree.write(join(root, destinationFile), result); + const content = stringifyNodeList(nodeList, root, destinationFile); + tree.write(join(root, destinationFile), content); if (isFlatCompatNeeded) { addDependenciesToPackageJson( @@ -202,19 +198,6 @@ export function convertEslintJsonToFlatConfig( } } -function updateFiles( - override: Linter.ConfigOverride, - root: string -) { - if (override.files) { - override.files = Array.isArray(override.files) - ? override.files - : [override.files]; - override.files = override.files.map((file) => mapFilePath(file, root)); - } - return override; -} - // add parsed extends to export blocks and add import statements function addExtends( importsMap: Map, @@ -235,9 +218,7 @@ function addExtends( .forEach((imp, index) => { if (imp.match(/\.eslintrc(.base)?\.json$/)) { const localName = index ? `baseConfig${index}` : 'baseConfig'; - configBlocks.push( - generateSpreadElement(localName) - ); + configBlocks.push(generateSpreadElement(localName)); const newImport = imp.replace( /^(.*)\.eslintrc(.base)?\.json$/, '$1eslint$2.config.js' @@ -342,11 +323,11 @@ function addPlugins( ), ...(config.processor ? [ - ts.factory.createPropertyAssignment( - 'processor', - ts.factory.createStringLiteral(config.processor) - ), - ] + ts.factory.createPropertyAssignment( + 'processor', + ts.factory.createStringLiteral(config.processor) + ), + ] : []), ], false @@ -367,61 +348,3 @@ function addParser( ts.factory.createIdentifier(parserName) ); } - -const DEFAULT_FLAT_CONFIG = ` -const compat = new FlatCompat({ - baseDirectory: __dirname, - recommendedConfig: js.configs.recommended, -}); -`; - -function createNodeList( - importsMap: Map, - exportElements: ts.Expression[], - isFlatCompatNeeded: boolean -): ts.NodeArray< - ts.VariableStatement | ts.Identifier | ts.ExpressionStatement | ts.SourceFile -> { - const importsList = []; - if (isFlatCompatNeeded) { - importsMap.set('@eslint/js', 'js'); - - importsList.push( - generateRequire( - ts.factory.createObjectBindingPattern([ - ts.factory.createBindingElement(undefined, undefined, 'FlatCompat'), - ]), - '@eslint/eslintrc' - ) - ); - } - - // generateRequire(varName, imp, ts.factory); - Array.from(importsMap.entries()).forEach(([imp, varName]) => { - importsList.push(generateRequire(varName, imp)); - }); - - return ts.factory.createNodeArray([ - // add plugin imports - ...importsList, - ts.createSourceFile( - '', - isFlatCompatNeeded ? DEFAULT_FLAT_CONFIG : '', - ts.ScriptTarget.Latest, - false, - ts.ScriptKind.JS - ), - // creates: - // module.exports = [ ... ]; - 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(exportElements, true) - ) - ), - ]); -} diff --git a/packages/linter/src/generators/lint-project/lint-project.ts b/packages/linter/src/generators/lint-project/lint-project.ts index 886a042636a30..16e9cd9db6707 100644 --- a/packages/linter/src/generators/lint-project/lint-project.ts +++ b/packages/linter/src/generators/lint-project/lint-project.ts @@ -22,7 +22,7 @@ import { createNodeList, generateFlatOverride, generateSpreadElement, - stringifyAst as stringifyNodeList, + stringifyNodeList, } from '../utils/flat-config/ast-utils'; interface LintProjectOptions { @@ -143,8 +143,8 @@ function createEsLintConfiguration( parserOptions: !setParserOptionsProject ? undefined : { - project: [`${projectConfig.root}/tsconfig.*?.json`], - }, + project: [`${projectConfig.root}/tsconfig.*?.json`], + }, /** * Having an empty rules object present makes it more obvious to the user where they would * extend things from if they needed to @@ -161,14 +161,14 @@ function createEsLintConfiguration( }, ...(isBuildableLibraryProject(projectConfig) ? [ - { - files: ['*.json'], - parser: 'jsonc-eslint-parser', - rules: { - '@nx/dependency-checks': 'error', - } as Linter.RulesRecord, - }, - ] + { + files: ['*.json'], + parser: 'jsonc-eslint-parser', + rules: { + '@nx/dependency-checks': 'error', + } as Linter.RulesRecord, + }, + ] : []), ]; diff --git a/packages/linter/src/generators/utils/flat-config/ast-utils.ts b/packages/linter/src/generators/utils/flat-config/ast-utils.ts index ddcbd30c96baf..aa28e29fc0fb5 100644 --- a/packages/linter/src/generators/utils/flat-config/ast-utils.ts +++ b/packages/linter/src/generators/utils/flat-config/ast-utils.ts @@ -116,7 +116,9 @@ export function generateSpreadElement(name: string): ts.SpreadElement { return ts.factory.createSpreadElement(ts.factory.createIdentifier(name)); } -export function generatePluginExtendsElement(plugins: string[]): ts.SpreadElement { +export function generatePluginExtendsElement( + plugins: string[] +): ts.SpreadElement { return ts.factory.createSpreadElement( ts.factory.createCallExpression( ts.factory.createPropertyAccessExpression( @@ -124,7 +126,7 @@ export function generatePluginExtendsElement(plugins: string[]): ts.SpreadElemen ts.factory.createIdentifier('extends') ), undefined, - plugins.map(plugin => ts.factory.createStringLiteral(plugin)) + plugins.map((plugin) => ts.factory.createStringLiteral(plugin)) ) ); } @@ -132,7 +134,7 @@ export function generatePluginExtendsElement(plugins: string[]): ts.SpreadElemen /** * Stringifies TS nodes to file content string */ -export function stringifyAst( +export function stringifyNodeList( nodes: ts.NodeArray< | ts.VariableStatement | ts.Identifier @@ -244,8 +246,10 @@ export function generateFlatOverride( ); } -function mapFilePaths(override: Linter.ConfigOverride, - root: string) { +function mapFilePaths( + override: Linter.ConfigOverride, + root: string +) { if (override.files) { override.files = Array.isArray(override.files) ? override.files @@ -256,7 +260,9 @@ function mapFilePaths(override: Linter.ConfigOverride, override.excludedFiles = Array.isArray(override.excludedFiles) ? override.excludedFiles : [override.excludedFiles]; - override.excludedFiles = override.excludedFiles.map((file) => mapFilePath(file, root)); + override.excludedFiles = override.excludedFiles.map((file) => + mapFilePath(file, root) + ); } } From 146ef896ff7907d570e71cd13812885ea990f914 Mon Sep 17 00:00:00 2001 From: Miroslav Jonas Date: Wed, 9 Aug 2023 14:41:49 +0200 Subject: [PATCH 04/30] feat(linter): add addImportToFlatConfig --- .../generators/lint-project/lint-project.ts | 2 +- .../utils/flat-config/ast-utils.spec.ts | 100 +++++++++++++++++- .../generators/utils/flat-config/ast-utils.ts | 95 ++++++++++++++++- 3 files changed, 194 insertions(+), 3 deletions(-) diff --git a/packages/linter/src/generators/lint-project/lint-project.ts b/packages/linter/src/generators/lint-project/lint-project.ts index 16e9cd9db6707..5c27157c80dda 100644 --- a/packages/linter/src/generators/lint-project/lint-project.ts +++ b/packages/linter/src/generators/lint-project/lint-project.ts @@ -173,7 +173,7 @@ function createEsLintConfiguration( ]; if (useFlatConfig()) { - const isCompatNeeded = !!eslintConfig || addDependencyChecks; + const isCompatNeeded = addDependencyChecks; const nodes = []; const importMap = new Map(); if (eslintConfig) { diff --git a/packages/linter/src/generators/utils/flat-config/ast-utils.spec.ts b/packages/linter/src/generators/utils/flat-config/ast-utils.spec.ts index aefd543e33803..90ecd78db8454 100644 --- a/packages/linter/src/generators/utils/flat-config/ast-utils.spec.ts +++ b/packages/linter/src/generators/utils/flat-config/ast-utils.spec.ts @@ -1,5 +1,10 @@ import ts = require('typescript'); -import { addConfigToFlatConfigExport, generateAst } from './ast-utils'; +import { + addConfigToFlatConfigExport, + generateAst, + addImportToFlatConfig, + generateRequire, +} from './ast-utils'; describe('ast-utils', () => { it('should inject block to the end of the file', () => { @@ -78,4 +83,97 @@ describe('ast-utils', () => { ];" `); }); + + it('should inject import if not found', () => { + const content = `const baseConfig = require("../../eslint.config.js"); + module.exports = [ + ...baseConfig, + { + files: [ + "mylib/**/*.ts", + "mylib/**/*.tsx" + ], + rules: {} + }, + { ignores: ["mylib/.cache/**/*"] }, + ];`; + const result = addImportToFlatConfig( + content, + 'varName', + '@myorg/awesome-config' + ); + expect(result).toMatchInlineSnapshot(` + "const varName = require("@myorg/awesome-config"); + const baseConfig = require("../../eslint.config.js"); + module.exports = [ + ...baseConfig, + { + files: [ + "mylib/**/*.ts", + "mylib/**/*.tsx" + ], + rules: {} + }, + { ignores: ["mylib/.cache/**/*"] }, + ];" + `); + }); + + it('should update import if already found', () => { + const content = `const { varName } = require("@myorg/awesome-config"); + const baseConfig = require("../../eslint.config.js"); + module.exports = [ + ...baseConfig, + { + files: [ + "mylib/**/*.ts", + "mylib/**/*.tsx" + ], + rules: {} + }, + { ignores: ["mylib/.cache/**/*"] }, + ];`; + const result = addImportToFlatConfig( + content, + ['otherName', 'someName'], + '@myorg/awesome-config' + ); + expect(result).toMatchInlineSnapshot(` + "const { varName, otherName, someName } = require("@myorg/awesome-config"); + const baseConfig = require("../../eslint.config.js"); + module.exports = [ + ...baseConfig, + { + files: [ + "mylib/**/*.ts", + "mylib/**/*.tsx" + ], + rules: {} + }, + { ignores: ["mylib/.cache/**/*"] }, + ];" + `); + }); + + it('should not update import if already exists', () => { + const content = `const { varName, otherName } = require("@myorg/awesome-config"); + const baseConfig = require("../../eslint.config.js"); + module.exports = [ + ...baseConfig, + { + files: [ + "mylib/**/*.ts", + "mylib/**/*.tsx" + ], + rules: {} + }, + { ignores: ["mylib/.cache/**/*"] }, + ];`; + const result = addImportToFlatConfig( + content, + ['otherName'], + '@myorg/awesome-config' + ); + expect(result).toEqual(content); + }); }); diff --git a/packages/linter/src/generators/utils/flat-config/ast-utils.ts b/packages/linter/src/generators/utils/flat-config/ast-utils.ts index aa28e29fc0fb5..3e83ce56fa4df 100644 --- a/packages/linter/src/generators/utils/flat-config/ast-utils.ts +++ b/packages/linter/src/generators/utils/flat-config/ast-utils.ts @@ -6,6 +6,100 @@ import { import { Linter } from 'eslint'; import * as ts from 'typescript'; +export function addImportToFlatConfig( + content: string, + variable: string | string[], + imp: string +): string { + const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); + const source = ts.createSourceFile( + '', + content, + ts.ScriptTarget.Latest, + true, + ts.ScriptKind.JS + ); + + const foundImportVars: ts.NodeArray = ts.forEachChild( + source, + function analyze(node) { + // we can only combine object binding patterns + if (!Array.isArray(variable)) { + return; + } + if ( + ts.isVariableStatement(node) && + node.declarationList.declarations.length > 0 && + 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' && + node.declarationList.declarations[0].initializer.arguments.length === + 1 && + 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 (foundImportVars && Array.isArray(variable)) { + const newVariables = variable.filter( + (v) => !foundImportVars.some((fv) => v === fv.name.getText()) + ); + if (newVariables.length === 0) { + return content; + } + const isMultiLine = foundImportVars.hasTrailingComma; + const pos = foundImportVars.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 applyChangesToString(content, [ + { + type: ChangeType.Insert, + index: pos, + text: isMultiLine ? `,\n${insert}` : `,${insert}`, + }, + ]); + } else { + 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 applyChangesToString(content, [ + { + type: ChangeType.Insert, + index: 0, + text: `${insert}\n`, + }, + ]); + } +} + /** * Injects new ts.expression to the end of the module.exports array. */ @@ -33,7 +127,6 @@ export function addConfigToFlatConfigExport( return node.expression.right.elements; } }); - console.log(exportsArray); const insert = printer.printNode(ts.EmitHint.Expression, config, source); if (options.insertAtTheEnd) { return applyChangesToString(content, [ From 7fdfa77a1eb8038e682e02ae0c71db9b53dd4341 Mon Sep 17 00:00:00 2001 From: Miroslav Jonas Date: Wed, 9 Aug 2023 15:08:10 +0200 Subject: [PATCH 05/30] feat(linter): add addCompatToFlatConfig --- .../src/generators/utils/eslint-file.ts | 21 ++- .../utils/flat-config/ast-utils.spec.ts | 133 +++++++++++++++++- .../generators/utils/flat-config/ast-utils.ts | 113 ++++++++++----- 3 files changed, 225 insertions(+), 42 deletions(-) diff --git a/packages/linter/src/generators/utils/eslint-file.ts b/packages/linter/src/generators/utils/eslint-file.ts index 2131add8e0319..887ff8e10e9c1 100644 --- a/packages/linter/src/generators/utils/eslint-file.ts +++ b/packages/linter/src/generators/utils/eslint-file.ts @@ -2,7 +2,8 @@ import { joinPathFragments, Tree, updateJson } from '@nx/devkit'; import { Linter } from 'eslint'; import { useFlatConfig } from '../../utils/flat-config'; import { - addConfigToFlatConfigExport, + addBlockToFlatConfigExport, + addCompatToFlatConfig, generateFlatOverride, generatePluginExtendsElement, } from './flat-config/ast-utils'; @@ -40,10 +41,18 @@ export function addOverrideToLintConfig( if (useFlatConfig()) { const fileName = joinPathFragments(root, 'eslint.config.js'); const flatOverride = generateFlatOverride(override, root); - tree.write( - fileName, - addConfigToFlatConfigExport(tree.read(fileName, 'utf8'), flatOverride) - ); + let content = tree.read(fileName, 'utf8'); + // we will be using compat here so we need to make sure it's added + if ( + !override.env && + !override.extends && + !override.plugins && + !override.parser + ) { + content = addCompatToFlatConfig(content); + } + + tree.write(fileName, addBlockToFlatConfigExport(content, flatOverride)); } else { const fileName = joinPathFragments(root, '.eslintrc.json'); updateJson(tree, fileName, (json) => { @@ -64,7 +73,7 @@ export function addExtendsToLintConfig( const pluginExtends = generatePluginExtendsElement([plugin]); tree.write( fileName, - addConfigToFlatConfigExport(tree.read(fileName, 'utf8'), pluginExtends) + addBlockToFlatConfigExport(tree.read(fileName, 'utf8'), pluginExtends) ); } else { const fileName = joinPathFragments(root, '.eslintrc.json'); diff --git a/packages/linter/src/generators/utils/flat-config/ast-utils.spec.ts b/packages/linter/src/generators/utils/flat-config/ast-utils.spec.ts index 90ecd78db8454..3f05c7b14465b 100644 --- a/packages/linter/src/generators/utils/flat-config/ast-utils.spec.ts +++ b/packages/linter/src/generators/utils/flat-config/ast-utils.spec.ts @@ -1,9 +1,10 @@ import ts = require('typescript'); import { - addConfigToFlatConfigExport, + addBlockToFlatConfigExport, generateAst, addImportToFlatConfig, generateRequire, + addCompatToFlatConfig, } from './ast-utils'; describe('ast-utils', () => { @@ -20,7 +21,7 @@ describe('ast-utils', () => { }, { ignores: ["mylib/.cache/**/*"] }, ];`; - const result = addConfigToFlatConfigExport( + const result = addBlockToFlatConfigExport( content, generateAst({ files: ['**/*.svg'], @@ -62,7 +63,7 @@ describe('ast-utils', () => { }, { ignores: ["mylib/.cache/**/*"] }, ];`; - const result = addConfigToFlatConfigExport( + const result = addBlockToFlatConfigExport( content, ts.factory.createSpreadElement(ts.factory.createIdentifier('config')), { insertAtTheEnd: false } @@ -155,7 +156,7 @@ describe('ast-utils', () => { `); }); - it('should not update import if already exists', () => { + it('should not inject import if already exists', () => { const content = `const { varName, otherName } = require("@myorg/awesome-config"); const baseConfig = require("../../eslint.config.js"); module.exports = [ @@ -176,4 +177,128 @@ describe('ast-utils', () => { ); expect(result).toEqual(content); }); + + it('should not update import if already exists', () => { + const content = `const varName = require("@myorg/awesome-config"); + const baseConfig = require("../../eslint.config.js"); + module.exports = [ + ...baseConfig, + { + files: [ + "mylib/**/*.ts", + "mylib/**/*.tsx" + ], + rules: {} + }, + { ignores: ["mylib/.cache/**/*"] }, + ];`; + const result = addImportToFlatConfig( + content, + 'varName', + '@myorg/awesome-config' + ); + expect(result).toEqual(content); + }); + + it('should add compat to config', () => { + const content = `const baseConfig = require("../../eslint.config.js"); + module.exports = [ + ...baseConfig, + { + files: [ + "mylib/**/*.ts", + "mylib/**/*.tsx" + ], + rules: {} + }, + { ignores: ["mylib/.cache/**/*"] }, + ];`; + const result = addCompatToFlatConfig(content); + expect(result).toMatchInlineSnapshot(` + "const FlatCompat = require("@eslint/eslintrc"); + const js = require("@eslint/js"); + const baseConfig = require("../../eslint.config.js"); + + const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + }); + + module.exports = [ + ...baseConfig, + { + files: [ + "mylib/**/*.ts", + "mylib/**/*.tsx" + ], + rules: {} + }, + { ignores: ["mylib/.cache/**/*"] }, + ];" + `); + }); + + it('should add only partially compat to config if parts exist', () => { + const content = `const baseConfig = require("../../eslint.config.js"); + const js = require("@eslint/js"); + module.exports = [ + ...baseConfig, + { + files: [ + "mylib/**/*.ts", + "mylib/**/*.tsx" + ], + rules: {} + }, + { ignores: ["mylib/.cache/**/*"] }, + ];`; + const result = addCompatToFlatConfig(content); + expect(result).toMatchInlineSnapshot(` + "const FlatCompat = require("@eslint/eslintrc"); + const baseConfig = require("../../eslint.config.js"); + const js = require("@eslint/js"); + + const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + }); + + module.exports = [ + ...baseConfig, + { + files: [ + "mylib/**/*.ts", + "mylib/**/*.tsx" + ], + rules: {} + }, + { ignores: ["mylib/.cache/**/*"] }, + ];" + `); + }); + + it('should not add compat to config if exist', () => { + const content = `const FlatCompat = require("@eslint/eslintrc"); + const baseConfig = require("../../eslint.config.js"); + const js = require("@eslint/js"); + + const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + }); + + module.exports = [ + ...baseConfig, + { + files: [ + "mylib/**/*.ts", + "mylib/**/*.tsx" + ], + rules: {} + }, + { ignores: ["mylib/.cache/**/*"] }, + ];`; + const result = addCompatToFlatConfig(content); + expect(result).toEqual(content); + }); }); diff --git a/packages/linter/src/generators/utils/flat-config/ast-utils.ts b/packages/linter/src/generators/utils/flat-config/ast-utils.ts index 3e83ce56fa4df..aaca39e85b445 100644 --- a/packages/linter/src/generators/utils/flat-config/ast-utils.ts +++ b/packages/linter/src/generators/utils/flat-config/ast-utils.ts @@ -20,7 +20,7 @@ export function addImportToFlatConfig( ts.ScriptKind.JS ); - const foundImportVars: ts.NodeArray = ts.forEachChild( + const foundBindingVars: ts.NodeArray = ts.forEachChild( source, function analyze(node) { // we can only combine object binding patterns @@ -29,14 +29,11 @@ export function addImportToFlatConfig( } if ( ts.isVariableStatement(node) && - node.declarationList.declarations.length > 0 && 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' && - node.declarationList.declarations[0].initializer.arguments.length === - 1 && ts.isStringLiteral( node.declarationList.declarations[0].initializer.arguments[0] ) && @@ -48,15 +45,15 @@ export function addImportToFlatConfig( } ); - if (foundImportVars && Array.isArray(variable)) { + if (foundBindingVars && Array.isArray(variable)) { const newVariables = variable.filter( - (v) => !foundImportVars.some((fv) => v === fv.name.getText()) + (v) => !foundBindingVars.some((fv) => v === fv.name.getText()) ); if (newVariables.length === 0) { return content; } - const isMultiLine = foundImportVars.hasTrailingComma; - const pos = foundImportVars.end; + const isMultiLine = foundBindingVars.hasTrailingComma; + const pos = foundBindingVars.end; const nodes = ts.factory.createNodeArray( newVariables.map((v) => ts.factory.createBindingElement(undefined, undefined, v) @@ -74,36 +71,67 @@ export function addImportToFlatConfig( text: isMultiLine ? `,\n${insert}` : `,${insert}`, }, ]); - } else { - 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 applyChangesToString(content, [ - { - type: ChangeType.Insert, - index: 0, - text: `${insert}\n`, - }, - ]); } + + const hasSameIdentifierVar: boolean = 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 applyChangesToString(content, [ + { + type: ChangeType.Insert, + index: 0, + text: `${insert}\n`, + }, + ]); } /** * Injects new ts.expression to the end of the module.exports array. */ -export function addConfigToFlatConfigExport( +export function addBlockToFlatConfigExport( content: string, config: ts.Expression | ts.SpreadElement, options: { insertAtTheEnd: boolean } = { insertAtTheEnd: true } @@ -147,6 +175,27 @@ export function addConfigToFlatConfigExport( } } +/** + * Adds compat if missing to flat config + */ +export function addCompatToFlatConfig(content: string) { + let result = content; + result = addImportToFlatConfig(result, 'js', '@eslint/js'); + if (result.includes('const compat = new FlatCompat')) { + return result; + } + result = addImportToFlatConfig(result, 'FlatCompat', '@eslint/eslintrc'); + const index = result.indexOf('module.exports'); + return applyChangesToString(result, [ + { + type: ChangeType.Insert, + index: index - 1, + text: `${DEFAULT_FLAT_CONFIG}\n`, + }, + ]); + // TODO DEFAULT_FLAT_CONFIG before module.exports +} + const DEFAULT_FLAT_CONFIG = ` const compat = new FlatCompat({ baseDirectory: __dirname, From 7e0e038decd2b6168d08df7125b59e6fafdfba40 Mon Sep 17 00:00:00 2001 From: Miroslav Jonas Date: Wed, 9 Aug 2023 15:31:03 +0200 Subject: [PATCH 06/30] feat(linter): add flat support to react-native --- .../src/generators/utils/eslint-file.ts | 30 +++++++++++++++++++ .../react-native/src/utils/add-linting.ts | 30 +++++-------------- .../configuration/lib/util-functions.ts | 19 ++++++++---- .../generators/move/lib/move-project-files.ts | 1 + 4 files changed, 51 insertions(+), 29 deletions(-) diff --git a/packages/linter/src/generators/utils/eslint-file.ts b/packages/linter/src/generators/utils/eslint-file.ts index 887ff8e10e9c1..83b82a83fef58 100644 --- a/packages/linter/src/generators/utils/eslint-file.ts +++ b/packages/linter/src/generators/utils/eslint-file.ts @@ -4,9 +4,12 @@ import { useFlatConfig } from '../../utils/flat-config'; import { addBlockToFlatConfigExport, addCompatToFlatConfig, + generateAst, generateFlatOverride, generatePluginExtendsElement, + mapFilePath, } from './flat-config/ast-utils'; +import ts = require('typescript'); export const eslintConfigFileWhitelist = [ '.eslintrc', @@ -83,3 +86,30 @@ export function addExtendsToLintConfig( }); } } + +export function addIgnoresToLintConfig( + tree: Tree, + root: string, + ignorePatterns: string[] +) { + if (useFlatConfig()) { + const fileName = joinPathFragments(root, 'eslint.config.js'); + const block = generateAst({ + ignores: ignorePatterns.map((path) => mapFilePath(path, root)), + }); + tree.write( + fileName, + addBlockToFlatConfigExport(tree.read(fileName, 'utf8'), block) + ); + } else { + const fileName = joinPathFragments(root, '.eslintrc.json'); + updateJson(tree, fileName, (json) => { + const ignoreSet = new Set([ + ...(json.ignorePatterns ?? []), + ...ignorePatterns, + ]); + json.ignorePatterns = Array.from(ignoreSet); + return json; + }); + } +} diff --git a/packages/react-native/src/utils/add-linting.ts b/packages/react-native/src/utils/add-linting.ts index 125d830624609..9abe911c6754c 100644 --- a/packages/react-native/src/utils/add-linting.ts +++ b/packages/react-native/src/utils/add-linting.ts @@ -2,16 +2,11 @@ import { Linter, lintProjectGenerator } from '@nx/linter'; import { addDependenciesToPackageJson, GeneratorCallback, - joinPathFragments, runTasksInSerial, Tree, - updateJson, } from '@nx/devkit'; -import { - extendReactEslintJson, - extraEslintDependencies, -} from '@nx/react/src/utils/lint'; -import type { Linter as ESLintLinter } from 'eslint'; +import { extraEslintDependencies } from '@nx/react/src/utils/lint'; +import { addIgnoresToLintConfig } from '@nx/linter/src/generators/utils/eslint-file'; interface NormalizedSchema { linter?: Linter; @@ -39,22 +34,11 @@ export async function addLinting(host: Tree, options: NormalizedSchema) { tasks.push(lintTask); - updateJson( - host, - joinPathFragments(options.projectRoot, '.eslintrc.json'), - (json: ESLintLinter.Config) => { - json = extendReactEslintJson(json); - - json.ignorePatterns = [ - ...json.ignorePatterns, - 'public', - '.cache', - 'node_modules', - ]; - - return json; - } - ); + addIgnoresToLintConfig(host, options.projectRoot, [ + 'public', + '.cache', + 'node_modules', + ]); if (!options.skipPackageJson) { const installTask = await addDependenciesToPackageJson( diff --git a/packages/storybook/src/generators/configuration/lib/util-functions.ts b/packages/storybook/src/generators/configuration/lib/util-functions.ts index 3150eb448e40e..4b5e16f63b4c5 100644 --- a/packages/storybook/src/generators/configuration/lib/util-functions.ts +++ b/packages/storybook/src/generators/configuration/lib/util-functions.ts @@ -28,7 +28,10 @@ import { import { StorybookConfigureSchema } from '../schema'; import { UiFramework7 } from '../../../utils/models'; import { nxVersion } from '../../../utils/versions'; -import ts = require('typescript'); +import { + addOverrideToLintConfig, + findEslintFile, +} from '@nx/linter/src/generators/utils/eslint-file'; const DEFAULT_PORT = 4400; @@ -173,7 +176,7 @@ export function createStorybookTsconfigFile( if (tree.exists(oldStorybookTsConfigPath)) { logger.warn(`.storybook/tsconfig.json already exists for this project`); logger.warn( - `It will be renamed and moved to tsconfig.storybook.json. + `It will be renamed and moved to tsconfig.storybook.json. Please make sure all settings look correct after this change. Also, please make sure to use "nx migrate" to move from one version of Nx to another. ` @@ -382,6 +385,10 @@ export function updateLintConfig(tree: Tree, schema: StorybookConfigureSchema) { ]); }); + if (!findEslintFile(tree)) { + return; + } + if (tree.exists(join(root, '.eslintrc.json'))) { updateJson(tree, join(root, '.eslintrc.json'), (json) => { if (typeof json.parserOptions?.project === 'string') { @@ -629,15 +636,15 @@ export function rootFileIsTs( ): boolean { if (tree.exists(`.storybook/${rootFileName}.ts`) && !tsConfiguration) { logger.info( - `The root Storybook configuration is in TypeScript, - so Nx will generate TypeScript Storybook configuration files + `The root Storybook configuration is in TypeScript, + so Nx will generate TypeScript Storybook configuration files in this project's .storybook folder as well.` ); return true; } else if (tree.exists(`.storybook/${rootFileName}.js`) && tsConfiguration) { logger.info( - `The root Storybook configuration is in JavaScript, - so Nx will generate JavaScript Storybook configuration files + `The root Storybook configuration is in JavaScript, + so Nx will generate JavaScript Storybook configuration files in this project's .storybook folder as well.` ); return false; diff --git a/packages/workspace/src/generators/move/lib/move-project-files.ts b/packages/workspace/src/generators/move/lib/move-project-files.ts index 5e5f0b066e79e..e10c0ca640f64 100644 --- a/packages/workspace/src/generators/move/lib/move-project-files.ts +++ b/packages/workspace/src/generators/move/lib/move-project-files.ts @@ -24,6 +24,7 @@ export function moveProjectFiles( 'tsconfig.spec.json', '.babelrc', '.eslintrc.json', + 'eslint.config.js', /^jest\.config\.(app|lib)\.[jt]s$/, 'vite.config.ts', /^webpack.*\.js$/, From 00927de8bbfba6bb6a47c658f58f477285ec6467 Mon Sep 17 00:00:00 2001 From: Miroslav Jonas Date: Wed, 9 Aug 2023 15:39:47 +0200 Subject: [PATCH 07/30] feat(linter): add flat support to react, detox and expo --- .../generators/application/lib/add-linting.ts | 10 ++---- packages/expo/src/utils/add-linting.ts | 35 ++++++------------- packages/playwright/src/utils/add-linter.ts | 5 +-- .../src/generators/application/application.ts | 13 ++----- .../src/generators/library/lib/add-linting.ts | 13 ++----- packages/react/src/utils/lint.ts | 16 ++++----- 6 files changed, 30 insertions(+), 62 deletions(-) diff --git a/packages/detox/src/generators/application/lib/add-linting.ts b/packages/detox/src/generators/application/lib/add-linting.ts index d1b578bcf05e9..fb845d57d50e9 100644 --- a/packages/detox/src/generators/application/lib/add-linting.ts +++ b/packages/detox/src/generators/application/lib/add-linting.ts @@ -4,10 +4,10 @@ import { joinPathFragments, runTasksInSerial, Tree, - updateJson, } from '@nx/devkit'; -import { extendReactEslintJson, extraEslintDependencies } from '@nx/react'; +import { extraEslintDependencies } from '@nx/react'; import { NormalizedSchema } from './normalize-options'; +import { addExtendsToLintConfig } from '@nx/linter/src/generators/utils/eslint-file'; export async function addLinting(host: Tree, options: NormalizedSchema) { if (options.linter === Linter.None) { @@ -24,11 +24,7 @@ export async function addLinting(host: Tree, options: NormalizedSchema) { skipFormat: true, }); - updateJson( - host, - joinPathFragments(options.e2eProjectRoot, '.eslintrc.json'), - extendReactEslintJson - ); + addExtendsToLintConfig(host, options.e2eProjectRoot, 'plugin:@nx/react'); const installTask = addDependenciesToPackageJson( host, diff --git a/packages/expo/src/utils/add-linting.ts b/packages/expo/src/utils/add-linting.ts index 438d91f183be6..b76335c8dc598 100644 --- a/packages/expo/src/utils/add-linting.ts +++ b/packages/expo/src/utils/add-linting.ts @@ -2,16 +2,14 @@ import { Linter, lintProjectGenerator } from '@nx/linter'; import { addDependenciesToPackageJson, GeneratorCallback, - joinPathFragments, runTasksInSerial, Tree, - updateJson, } from '@nx/devkit'; +import { extraEslintDependencies } from '@nx/react/src/utils/lint'; import { - extendReactEslintJson, - extraEslintDependencies, -} from '@nx/react/src/utils/lint'; -import type { Linter as ESLintLinter } from 'eslint'; + addExtendsToLintConfig, + addIgnoresToLintConfig, +} from '@nx/linter/src/generators/utils/eslint-file'; interface NormalizedSchema { linter?: Linter; @@ -39,24 +37,13 @@ export async function addLinting(host: Tree, options: NormalizedSchema) { tasks.push(lintTask); - updateJson( - host, - joinPathFragments(options.projectRoot, '.eslintrc.json'), - (json: ESLintLinter.Config) => { - json = extendReactEslintJson(json); - - json.ignorePatterns = [ - ...json.ignorePatterns, - '.expo', - 'node_modules', - 'web-build', - 'cache', - 'dist', - ]; - - return json; - } - ); + addExtendsToLintConfig(host, options.projectRoot, 'plugin:@nx/react'); + addIgnoresToLintConfig(host, options.projectRoot, [ + '.expo', + 'web-build', + 'cache', + 'dist', + ]); if (!options.skipPackageJson) { const installTask = await addDependenciesToPackageJson( diff --git a/packages/playwright/src/utils/add-linter.ts b/packages/playwright/src/utils/add-linter.ts index aae4c8ffef9c2..df619ad8e2c8b 100644 --- a/packages/playwright/src/utils/add-linter.ts +++ b/packages/playwright/src/utils/add-linter.ts @@ -5,7 +5,6 @@ import { readProjectConfiguration, runTasksInSerial, Tree, - updateJson, } from '@nx/devkit'; import { Linter, lintProjectGenerator } from '@nx/linter'; import { javaScriptOverride } from '@nx/linter/src/generators/init/global-eslint-config'; @@ -13,6 +12,7 @@ import { eslintPluginPlaywrightVersion } from './versions'; import { addExtendsToLintConfig, addOverrideToLintConfig, + findEslintFile, } from '@nx/linter/src/generators/utils/eslint-file'; export interface PlaywrightLinterOptions { @@ -39,7 +39,8 @@ export async function addLinterToPlaywrightProject( const tasks: GeneratorCallback[] = []; const projectConfig = readProjectConfiguration(tree, options.project); - if (!tree.exists(joinPathFragments(projectConfig.root, '.eslintrc.json'))) { + const eslintFile = findEslintFile(tree, projectConfig.root); + if (!eslintFile) { tasks.push( await lintProjectGenerator(tree, { project: options.project, diff --git a/packages/react/src/generators/application/application.ts b/packages/react/src/generators/application/application.ts index abba9602efe1f..d3ccec057aa5c 100644 --- a/packages/react/src/generators/application/application.ts +++ b/packages/react/src/generators/application/application.ts @@ -1,7 +1,4 @@ -import { - extendReactEslintJson, - extraEslintDependencies, -} from '../../utils/lint'; +import { extraEslintDependencies } from '../../utils/lint'; import { NormalizedSchema, Schema } from './schema'; import { createApplicationFiles } from './lib/create-application-files'; import { updateSpecConfig } from './lib/update-jest-config'; @@ -22,7 +19,6 @@ import { runTasksInSerial, stripIndents, Tree, - updateJson, } from '@nx/devkit'; import reactInitGenerator from '../init/init'; @@ -39,6 +35,7 @@ import { addSwcDependencies } from '@nx/js/src/utils/swc/add-swc-dependencies'; import * as chalk from 'chalk'; import { showPossibleWarnings } from './lib/show-possible-warnings'; import { addE2e } from './lib/add-e2e'; +import { addExtendsToLintConfig } from '@nx/linter/src/generators/utils/eslint-file'; async function addLinting(host: Tree, options: NormalizedSchema) { const tasks: GeneratorCallback[] = []; @@ -63,11 +60,7 @@ async function addLinting(host: Tree, options: NormalizedSchema) { }); tasks.push(lintTask); - updateJson( - host, - joinPathFragments(options.appProjectRoot, '.eslintrc.json'), - extendReactEslintJson - ); + addExtendsToLintConfig(host, options.appProjectRoot, 'plugin:@nx/react'); if (!options.skipPackageJson) { const installTask = addDependenciesToPackageJson( diff --git a/packages/react/src/generators/library/lib/add-linting.ts b/packages/react/src/generators/library/lib/add-linting.ts index b78b11ba55256..7315870d7ac4d 100644 --- a/packages/react/src/generators/library/lib/add-linting.ts +++ b/packages/react/src/generators/library/lib/add-linting.ts @@ -1,14 +1,11 @@ import { Tree } from 'nx/src/generators/tree'; import { Linter, lintProjectGenerator } from '@nx/linter'; import { joinPathFragments } from 'nx/src/utils/path'; -import { updateJson } from 'nx/src/generators/utils/json'; import { addDependenciesToPackageJson, runTasksInSerial } from '@nx/devkit'; import { NormalizedSchema } from '../schema'; -import { - extendReactEslintJson, - extraEslintDependencies, -} from '../../../utils/lint'; +import { extraEslintDependencies } from '../../../utils/lint'; +import { addExtendsToLintConfig } from '@nx/linter/src/generators/utils/eslint-file'; export async function addLinting(host: Tree, options: NormalizedSchema) { if (options.linter === Linter.EsLint) { @@ -25,11 +22,7 @@ export async function addLinting(host: Tree, options: NormalizedSchema) { setParserOptionsProject: options.setParserOptionsProject, }); - updateJson( - host, - joinPathFragments(options.projectRoot, '.eslintrc.json'), - extendReactEslintJson - ); + addExtendsToLintConfig(host, options.projectRoot, 'plugin:@nx/react'); let installTask = () => {}; if (!options.skipPackageJson) { diff --git a/packages/react/src/utils/lint.ts b/packages/react/src/utils/lint.ts index 88157c6ed63d2..fa79991da4d8e 100644 --- a/packages/react/src/utils/lint.ts +++ b/packages/react/src/utils/lint.ts @@ -1,5 +1,3 @@ -import { offsetFromRoot } from '@nx/devkit'; -import type { Linter } from 'eslint'; import { eslintPluginImportVersion, eslintPluginReactVersion, @@ -17,11 +15,11 @@ export const extraEslintDependencies = { }, }; -export const extendReactEslintJson = (json: Linter.Config) => { - const { extends: pluginExtends, ...config } = json; +// export const extendReactEslintJson = (json: Linter.Config) => { +// const { extends: pluginExtends, ...config } = json; - return { - extends: ['plugin:@nx/react', ...(pluginExtends || [])], - ...config, - }; -}; +// return { +// extends: ['plugin:@nx/react', ...(pluginExtends || [])], +// ...config, +// }; +// }; From f56d2ad6fc60673412406c405cb778ba8c18bc0e Mon Sep 17 00:00:00 2001 From: Miroslav Jonas Date: Wed, 9 Aug 2023 15:49:01 +0200 Subject: [PATCH 08/30] feat(linter): simplify cypress --- packages/cypress/src/utils/add-linter.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/cypress/src/utils/add-linter.ts b/packages/cypress/src/utils/add-linter.ts index 0bb6ff3d560f3..21b1332adea90 100644 --- a/packages/cypress/src/utils/add-linter.ts +++ b/packages/cypress/src/utils/add-linter.ts @@ -11,6 +11,11 @@ import { Linter, lintProjectGenerator } from '@nx/linter'; import { javaScriptOverride } from '@nx/linter/src/generators/init/global-eslint-config'; import { installedCypressVersion } from './cypress-version'; import { eslintPluginCypressVersion } from './versions'; +import { + addExtendsToLintConfig, + addOverrideToLintConfig, + findEslintFile, +} from '@nx/linter/src/generators/utils/eslint-file'; export interface CyLinterOptions { project: string; @@ -42,7 +47,8 @@ export async function addLinterToCyProject( const tasks: GeneratorCallback[] = []; const projectConfig = readProjectConfiguration(tree, options.project); - if (!tree.exists(joinPathFragments(projectConfig.root, '.eslintrc.json'))) { + const eslintFile = findEslintFile(tree, projectConfig.root); + if (!eslintFile) { tasks.push( await lintProjectGenerator(tree, { project: options.project, @@ -73,16 +79,15 @@ export async function addLinterToCyProject( : () => {} ); + addExtendsToLintConfig( + tree, + projectConfig.root, + 'plugin:cypress/recommended' + ); updateJson( tree, joinPathFragments(projectConfig.root, '.eslintrc.json'), (json) => { - if (options.rootProject) { - json.plugins = ['@nx']; - json.extends = ['plugin:cypress/recommended']; - } else { - json.extends = ['plugin:cypress/recommended', ...json.extends]; - } json.overrides ??= []; const globals = options.rootProject ? [javaScriptOverride] : []; const override = { From 67501c475608338a442876da0439cda5edd002df Mon Sep 17 00:00:00 2001 From: Miroslav Jonas Date: Wed, 9 Aug 2023 15:59:01 +0200 Subject: [PATCH 09/30] feat(linter): handle node --- .../generators/application/lib/add-linting.ts | 16 +++---- .../src/generators/e2e-project/e2e-project.ts | 44 +++++++------------ .../angular/standalone-workspace.ts | 5 ++- packages/react/src/utils/lint.ts | 18 +++++--- ...n.spec.ts => update-eslint-config.spec.ts} | 16 +++---- ...lintrc-json.ts => update-eslint-config.ts} | 6 +-- .../move/lib/update-project-root-files.ts | 4 +- .../workspace/src/generators/move/move.ts | 4 +- 8 files changed, 54 insertions(+), 59 deletions(-) rename packages/workspace/src/generators/move/lib/{update-eslintrc-json.spec.ts => update-eslint-config.spec.ts} (93%) rename packages/workspace/src/generators/move/lib/{update-eslintrc-json.ts => update-eslint-config.ts} (98%) diff --git a/packages/next/src/generators/application/lib/add-linting.ts b/packages/next/src/generators/application/lib/add-linting.ts index da143412e36bf..bb95e72c05d36 100644 --- a/packages/next/src/generators/application/lib/add-linting.ts +++ b/packages/next/src/generators/application/lib/add-linting.ts @@ -7,11 +7,12 @@ import { Tree, updateJson, } from '@nx/devkit'; -import { - extendReactEslintJson, - extraEslintDependencies, -} from '@nx/react/src/utils/lint'; +import { extraEslintDependencies } from '@nx/react/src/utils/lint'; import { NormalizedSchema } from './normalize-options'; +import { + addExtendsToLintConfig, + addIgnoresToLintConfig, +} from '@nx/linter/src/generators/utils/eslint-file'; export async function addLinting( host: Tree, @@ -30,17 +31,16 @@ export async function addLinting( }); if (options.linter === Linter.EsLint) { + addExtendsToLintConfig(host, options.appProjectRoot, 'plugin:@nx/react'); + addIgnoresToLintConfig(host, options.appProjectRoot, ['.next/**/*']); + updateJson( host, joinPathFragments(options.appProjectRoot, '.eslintrc.json'), (json) => { - json = extendReactEslintJson(json); - // Turn off @next/next/no-html-link-for-pages since there is an issue with nextjs throwing linting errors // TODO(nicholas): remove after Vercel updates nextjs linter to only lint ["*.ts", "*.tsx", "*.js", "*.jsx"] - json.ignorePatterns = [...json.ignorePatterns, '.next/**/*']; - json.rules = { '@next/next/no-html-link-for-pages': 'off', ...json.rules, diff --git a/packages/node/src/generators/e2e-project/e2e-project.ts b/packages/node/src/generators/e2e-project/e2e-project.ts index 1e89e9c021c82..ad4e49999e50f 100644 --- a/packages/node/src/generators/e2e-project/e2e-project.ts +++ b/packages/node/src/generators/e2e-project/e2e-project.ts @@ -11,7 +11,6 @@ import { readProjectConfiguration, runTasksInSerial, Tree, - updateJson, } from '@nx/devkit'; import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils'; import { Linter, lintProjectGenerator } from '@nx/linter'; @@ -20,9 +19,13 @@ import { typeScriptOverride, } from '@nx/linter/src/generators/init/global-eslint-config'; import * as path from 'path'; -import { join } from 'path'; import { axiosVersion } from '../../utils/versions'; import { Schema } from './schema'; +import { + addPluginsToLintConfig, + isEslintConfigSupported, + replaceOverridesInLintConfig, +} from '@nx/linter/src/generators/utils/eslint-file'; export async function e2eProjectGenerator(host: Tree, options: Schema) { return await e2eProjectGeneratorInternal(host, { @@ -119,32 +122,13 @@ export async function e2eProjectGeneratorInternal( }); tasks.push(linterTask); - updateJson(host, join(options.e2eProjectRoot, '.eslintrc.json'), (json) => { - if (options.rootProject) { - json.plugins = ['@nx']; - json.extends = []; - } - json.overrides = [ - ...(options.rootProject - ? [typeScriptOverride, javaScriptOverride] - : []), - /** - * In order to ensure maximum efficiency when typescript-eslint generates TypeScript Programs - * behind the scenes during lint runs, we need to make sure the project is configured to use its - * own specific tsconfigs, and not fall back to the ones in the root of the workspace. - */ - { - files: ['*.ts', '*.tsx', '*.js', '*.jsx'], - /** - * Having an empty rules object present makes it more obvious to the user where they would - * extend things from if they needed to - */ - rules: {}, - }, - ]; - - return json; - }); + if (options.rootProject && isEslintConfigSupported(host)) { + addPluginsToLintConfig(host, options.e2eProjectRoot, '@nx'); + replaceOverridesInLintConfig(host, options.e2eProjectRoot, [ + typeScriptOverride, + javaScriptOverride, + ]); + } } if (!options.skipFormat) { @@ -183,3 +167,7 @@ async function normalizeOptions( export default e2eProjectGenerator; export const e2eProjectSchematic = convertNxGenerator(e2eProjectGenerator); +function isEslintConfigSupported(host: Tree) { + throw new Error('Function not implemented.'); +} + diff --git a/packages/nx/src/command-line/init/implementation/angular/standalone-workspace.ts b/packages/nx/src/command-line/init/implementation/angular/standalone-workspace.ts index fbf9b8cbda4c2..ad421026cbc86 100644 --- a/packages/nx/src/command-line/init/implementation/angular/standalone-workspace.ts +++ b/packages/nx/src/command-line/init/implementation/angular/standalone-workspace.ts @@ -217,7 +217,10 @@ function projectHasKarmaConfig( function projectHasEslintConfig( project: AngularJsonProjectConfiguration ): boolean { - return fileExists(join(project.root, '.eslintrc.json')); + return ( + fileExists(join(project.root, '.eslintrc.json')) || + fileExists(join(project.root, 'eslint.config.js')) + ); } function replaceNgWithNxInPackageJsonScripts(repoRoot: string): void { diff --git a/packages/react/src/utils/lint.ts b/packages/react/src/utils/lint.ts index fa79991da4d8e..05e8641d6fafb 100644 --- a/packages/react/src/utils/lint.ts +++ b/packages/react/src/utils/lint.ts @@ -1,3 +1,4 @@ +import { Linter } from 'eslint'; import { eslintPluginImportVersion, eslintPluginReactVersion, @@ -15,11 +16,14 @@ export const extraEslintDependencies = { }, }; -// export const extendReactEslintJson = (json: Linter.Config) => { -// const { extends: pluginExtends, ...config } = json; +/** + * @deprecated Use `addExtendsToLintConfig` from `@nx/linter` instead. + */ +export const extendReactEslintJson = (json: Linter.Config) => { + const { extends: pluginExtends, ...config } = json; -// return { -// extends: ['plugin:@nx/react', ...(pluginExtends || [])], -// ...config, -// }; -// }; + return { + extends: ['plugin:@nx/react', ...(pluginExtends || [])], + ...config, + }; +}; diff --git a/packages/workspace/src/generators/move/lib/update-eslintrc-json.spec.ts b/packages/workspace/src/generators/move/lib/update-eslint-config.spec.ts similarity index 93% rename from packages/workspace/src/generators/move/lib/update-eslintrc-json.spec.ts rename to packages/workspace/src/generators/move/lib/update-eslint-config.spec.ts index c627bad8df4cc..06f3f531a5784 100644 --- a/packages/workspace/src/generators/move/lib/update-eslintrc-json.spec.ts +++ b/packages/workspace/src/generators/move/lib/update-eslint-config.spec.ts @@ -7,7 +7,7 @@ import { import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import { Linter } from '../../../utils/lint'; import { NormalizedSchema } from '../schema'; -import { updateEslintrcJson } from './update-eslintrc-json'; +import { updateEslintConfig } from './update-eslint-config'; // nx-ignore-next-line const { libraryGenerator } = require('@nx/js'); @@ -38,7 +38,7 @@ describe('updateEslint', () => { const projectConfig = readProjectConfiguration(tree, 'my-lib'); expect(() => { - updateEslintrcJson(tree, schema, projectConfig); + updateEslintConfig(tree, schema, projectConfig); }).not.toThrow(); }); @@ -54,7 +54,7 @@ describe('updateEslint', () => { ); const projectConfig = readProjectConfiguration(tree, 'my-lib'); - updateEslintrcJson(tree, schema, projectConfig); + updateEslintConfig(tree, schema, projectConfig); expect( readJson(tree, '/libs/shared/my-destination/.eslintrc.json') @@ -84,7 +84,7 @@ describe('updateEslint', () => { relativeToRootDestination: 'libs/test', }; - updateEslintrcJson(tree, newSchema, projectConfig); + updateEslintConfig(tree, newSchema, projectConfig); expect(readJson(tree, '/libs/test/.eslintrc.json')).toEqual( expect.objectContaining({ @@ -113,7 +113,7 @@ describe('updateEslint', () => { ); const projectConfig = readProjectConfiguration(tree, 'my-lib'); - updateEslintrcJson(tree, schema, projectConfig); + updateEslintConfig(tree, schema, projectConfig); expect( readJson(tree, '/libs/shared/my-destination/.eslintrc.json') @@ -141,7 +141,7 @@ describe('updateEslint', () => { ); const projectConfig = readProjectConfiguration(tree, 'my-lib'); - updateEslintrcJson(tree, schema, projectConfig); + updateEslintConfig(tree, schema, projectConfig); expect( readJson(tree, '/libs/shared/my-destination/.eslintrc.json') @@ -181,7 +181,7 @@ describe('updateEslint', () => { ); const projectConfig = readProjectConfiguration(tree, 'my-lib'); - updateEslintrcJson(tree, schema, projectConfig); + updateEslintConfig(tree, schema, projectConfig); expect( readJson(tree, '/libs/shared/my-destination/.eslintrc.json') @@ -222,7 +222,7 @@ describe('updateEslint', () => { ); const projectConfig = readProjectConfiguration(tree, 'my-lib'); - updateEslintrcJson(tree, schema, projectConfig); + updateEslintConfig(tree, schema, projectConfig); expect( readJson(tree, '/libs/shared/my-destination/.eslintrc.json').overrides[0] diff --git a/packages/workspace/src/generators/move/lib/update-eslintrc-json.ts b/packages/workspace/src/generators/move/lib/update-eslint-config.ts similarity index 98% rename from packages/workspace/src/generators/move/lib/update-eslintrc-json.ts rename to packages/workspace/src/generators/move/lib/update-eslint-config.ts index 4dd526cdf3777..0d92c62b52db7 100644 --- a/packages/workspace/src/generators/move/lib/update-eslintrc-json.ts +++ b/packages/workspace/src/generators/move/lib/update-eslint-config.ts @@ -37,11 +37,13 @@ function offsetFilePath( * * @param schema The options provided to the schematic */ -export function updateEslintrcJson( +export function updateEslintConfig( tree: Tree, schema: NormalizedSchema, project: ProjectConfiguration ) { + const offset = offsetFromRoot(schema.relativeToRootDestination); + const eslintRcPath = join(schema.relativeToRootDestination, '.eslintrc.json'); if (!tree.exists(eslintRcPath)) { @@ -49,8 +51,6 @@ export function updateEslintrcJson( return; } - const offset = offsetFromRoot(schema.relativeToRootDestination); - updateJson(tree, eslintRcPath, (eslintRcJson) => { if (typeof eslintRcJson.extends === 'string') { eslintRcJson.extends = offsetFilePath( diff --git a/packages/workspace/src/generators/move/lib/update-project-root-files.ts b/packages/workspace/src/generators/move/lib/update-project-root-files.ts index ee9695dcb5981..d2c68ac287200 100644 --- a/packages/workspace/src/generators/move/lib/update-project-root-files.ts +++ b/packages/workspace/src/generators/move/lib/update-project-root-files.ts @@ -56,7 +56,7 @@ export function updateFilesForRootProjects( if (!allowedExt.includes(ext)) { continue; } - if (file === '.eslintrc.json') { + if (file === '.eslintrc.json' || file === 'eslint.config.js') { continue; } @@ -108,7 +108,7 @@ export function updateFilesForNonRootProjects( if (!allowedExt.includes(ext)) { continue; } - if (file === '.eslintrc.json') { + if (file === '.eslintrc.json' || file === 'eslint.config.js') { continue; } diff --git a/packages/workspace/src/generators/move/move.ts b/packages/workspace/src/generators/move/move.ts index 641287d526aa2..6c868db0a2a30 100644 --- a/packages/workspace/src/generators/move/move.ts +++ b/packages/workspace/src/generators/move/move.ts @@ -12,7 +12,7 @@ import { normalizeSchema } from './lib/normalize-schema'; import { updateBuildTargets } from './lib/update-build-targets'; import { updateCypressConfig } from './lib/update-cypress-config'; import { updateDefaultProject } from './lib/update-default-project'; -import { updateEslintrcJson } from './lib/update-eslintrc-json'; +import { updateEslintConfig } from './lib/update-eslint-config'; import { updateImplicitDependencies } from './lib/update-implicit-dependencies'; import { updateImports } from './lib/update-imports'; import { updateJestConfig } from './lib/update-jest-config'; @@ -48,7 +48,7 @@ export async function moveGenerator(tree: Tree, rawSchema: Schema) { updateCypressConfig(tree, schema, projectConfig); updateJestConfig(tree, schema, projectConfig); updateStorybookConfig(tree, schema, projectConfig); - updateEslintrcJson(tree, schema, projectConfig); + updateEslintConfig(tree, schema, projectConfig); updateReadme(tree, schema); updatePackageJson(tree, schema); updateBuildTargets(tree, schema); From 83fc7ad37cffbe9d6c10b70ab1991837aaa8dc21 Mon Sep 17 00:00:00 2001 From: Miroslav Jonas Date: Wed, 9 Aug 2023 16:18:28 +0200 Subject: [PATCH 10/30] feat(linter): handle js --- packages/js/src/generators/library/library.ts | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/js/src/generators/library/library.ts b/packages/js/src/generators/library/library.ts index 362e302e672db..7eb8c8b339bb6 100644 --- a/packages/js/src/generators/library/library.ts +++ b/packages/js/src/generators/library/library.ts @@ -259,16 +259,16 @@ export async function addLint( // Also update the root .eslintrc.json lintProjectGenerator will not generate it for root projects. // But we need to set the package.json checks. if (options.rootProject) { - updateJson(tree, '.eslintrc.json', (json) => { - json.overrides ??= []; - json.overrides.push({ - files: ['*.json'], - parser: 'jsonc-eslint-parser', - rules: { - '@nx/dependency-checks': 'error', - }, - }); - return json; + const { + addOverrideToLintConfig, + } = require('@nx/linter/src/generators/utils/eslint-file'); + + addOverrideToLintConfig(tree, '', { + files: ['*.json'], + parser: 'jsonc-eslint-parser', + rules: { + '@nx/dependency-checks': 'error', + }, }); } return task; From 21df9c6deb03e6c91a8daad8546f12e4b13bb769 Mon Sep 17 00:00:00 2001 From: Miroslav Jonas Date: Wed, 9 Aug 2023 18:26:01 +0200 Subject: [PATCH 11/30] feat(linter): handle angular --- .../src/generators/add-linting/add-linting.ts | 45 ++++++-- .../lib/create-eslint-configuration.ts | 3 + packages/cypress/src/utils/add-linter.ts | 1 - packages/js/src/generators/library/library.ts | 2 +- .../src/generators/utils/eslint-file.ts | 45 ++++++-- .../utils/flat-config/ast-utils.spec.ts | 106 +++++++++++++++++- .../generators/utils/flat-config/ast-utils.ts | 49 ++++++++ .../workspace-rule/workspace-rule.ts | 4 +- 8 files changed, 232 insertions(+), 23 deletions(-) diff --git a/packages/angular/src/generators/add-linting/add-linting.ts b/packages/angular/src/generators/add-linting/add-linting.ts index c6509bfe4a35c..27af03cdc38e5 100755 --- a/packages/angular/src/generators/add-linting/add-linting.ts +++ b/packages/angular/src/generators/add-linting/add-linting.ts @@ -4,13 +4,13 @@ import { joinPathFragments, runTasksInSerial, Tree, - updateJson, } from '@nx/devkit'; import { Linter, lintProjectGenerator } from '@nx/linter'; import { mapLintPattern } from '@nx/linter/src/generators/lint-project/lint-project'; import { addAngularEsLintDependencies } from './lib/add-angular-eslint-dependencies'; -import { extendAngularEslintJson } from './lib/create-eslint-configuration'; import type { AddLintingGeneratorSchema } from './schema'; +import { replaceOverridesInLintConfig } from '@nx/linter/src/generators/utils/eslint-file'; +import { camelize, dasherize } from '@nx/devkit/src/utils/string-utils'; export async function addLintingGenerator( tree: Tree, @@ -35,11 +35,42 @@ export async function addLintingGenerator( }); tasks.push(lintTask); - updateJson( - tree, - joinPathFragments(options.projectRoot, '.eslintrc.json'), - (json) => extendAngularEslintJson(json, options) - ); + replaceOverridesInLintConfig(tree, options.projectRoot, [ + { + files: ['*.ts'], + extends: [ + 'plugin:@nx/angular', + 'plugin:@angular-eslint/template/process-inline-templates', + ], + rules: { + '@angular-eslint/directive-selector': [ + 'error', + { + type: 'attribute', + prefix: camelize(options.prefix), + style: 'camelCase', + }, + ], + '@angular-eslint/component-selector': [ + 'error', + { + type: 'element', + prefix: dasherize(options.prefix), + style: 'kebab-case', + }, + ], + }, + }, + { + files: ['*.html'], + extends: ['plugin:@nx/angular-template'], + /** + * Having an empty rules object present makes it more obvious to the user where they would + * extend things from if they needed to + */ + rules: {}, + }, + ]); if (!options.skipPackageJson) { const installTask = addAngularEsLintDependencies(tree); diff --git a/packages/angular/src/generators/add-linting/lib/create-eslint-configuration.ts b/packages/angular/src/generators/add-linting/lib/create-eslint-configuration.ts index 5a1b791571b70..eed5f8f3ad672 100644 --- a/packages/angular/src/generators/add-linting/lib/create-eslint-configuration.ts +++ b/packages/angular/src/generators/add-linting/lib/create-eslint-configuration.ts @@ -8,6 +8,9 @@ type EslintExtensionSchema = { prefix: string; }; +/** + * @deprecated Use tools from `@nx/linter/src/generators/utils/eslint-file` instead + */ export const extendAngularEslintJson = ( json: Linter.Config, options: EslintExtensionSchema diff --git a/packages/cypress/src/utils/add-linter.ts b/packages/cypress/src/utils/add-linter.ts index 21b1332adea90..9926d3311c561 100644 --- a/packages/cypress/src/utils/add-linter.ts +++ b/packages/cypress/src/utils/add-linter.ts @@ -13,7 +13,6 @@ import { installedCypressVersion } from './cypress-version'; import { eslintPluginCypressVersion } from './versions'; import { addExtendsToLintConfig, - addOverrideToLintConfig, findEslintFile, } from '@nx/linter/src/generators/utils/eslint-file'; diff --git a/packages/js/src/generators/library/library.ts b/packages/js/src/generators/library/library.ts index 7eb8c8b339bb6..75d03b87041be 100644 --- a/packages/js/src/generators/library/library.ts +++ b/packages/js/src/generators/library/library.ts @@ -256,7 +256,7 @@ export async function addLint( setParserOptionsProject: options.setParserOptionsProject, rootProject: options.rootProject, }); - // Also update the root .eslintrc.json lintProjectGenerator will not generate it for root projects. + // Also update the root ESLint config. The lintProjectGenerator will not generate it for root projects. // But we need to set the package.json checks. if (options.rootProject) { const { diff --git a/packages/linter/src/generators/utils/eslint-file.ts b/packages/linter/src/generators/utils/eslint-file.ts index 83b82a83fef58..ce13c355eb081 100644 --- a/packages/linter/src/generators/utils/eslint-file.ts +++ b/packages/linter/src/generators/utils/eslint-file.ts @@ -8,6 +8,7 @@ import { generateFlatOverride, generatePluginExtendsElement, mapFilePath, + removeRulesFromLintConfig, } from './flat-config/ast-utils'; import ts = require('typescript'); @@ -46,15 +47,9 @@ export function addOverrideToLintConfig( const flatOverride = generateFlatOverride(override, root); let content = tree.read(fileName, 'utf8'); // we will be using compat here so we need to make sure it's added - if ( - !override.env && - !override.extends && - !override.plugins && - !override.parser - ) { + if (overrideNeedsCompat(override)) { content = addCompatToFlatConfig(content); } - tree.write(fileName, addBlockToFlatConfigExport(content, flatOverride)); } else { const fileName = joinPathFragments(root, '.eslintrc.json'); @@ -66,6 +61,42 @@ export function addOverrideToLintConfig( } } +function overrideNeedsCompat( + override: Linter.ConfigOverride +) { + return ( + !override.env && !override.extends && !override.plugins && !override.parser + ); +} + +export function replaceOverridesInLintConfig( + tree: Tree, + root: string, + overrides: Linter.ConfigOverride[] +) { + if (useFlatConfig()) { + const fileName = joinPathFragments(root, 'eslint.config.js'); + let content = tree.read(fileName, 'utf8'); + // we will be using compat here so we need to make sure it's added + if (overrides.some(overrideNeedsCompat)) { + content = addCompatToFlatConfig(content); + } + content = removeRulesFromLintConfig(content); + overrides.forEach((override) => { + const flatOverride = generateFlatOverride(override, root); + addBlockToFlatConfigExport(content, flatOverride); + }); + + tree.write(fileName, content); + } else { + const fileName = joinPathFragments(root, '.eslintrc.json'); + updateJson(tree, fileName, (json) => { + json.overrides = overrides; + return json; + }); + } +} + export function addExtendsToLintConfig( tree: Tree, root: string, diff --git a/packages/linter/src/generators/utils/flat-config/ast-utils.spec.ts b/packages/linter/src/generators/utils/flat-config/ast-utils.spec.ts index 3f05c7b14465b..a538730e17861 100644 --- a/packages/linter/src/generators/utils/flat-config/ast-utils.spec.ts +++ b/packages/linter/src/generators/utils/flat-config/ast-utils.spec.ts @@ -3,8 +3,8 @@ import { addBlockToFlatConfigExport, generateAst, addImportToFlatConfig, - generateRequire, addCompatToFlatConfig, + removeRulesFromLintConfig, } from './ast-utils'; describe('ast-utils', () => { @@ -218,12 +218,12 @@ describe('ast-utils', () => { "const FlatCompat = require("@eslint/eslintrc"); const js = require("@eslint/js"); const baseConfig = require("../../eslint.config.js"); - + const compat = new FlatCompat({ baseDirectory: __dirname, recommendedConfig: js.configs.recommended, }); - + module.exports = [ ...baseConfig, { @@ -257,12 +257,12 @@ describe('ast-utils', () => { "const FlatCompat = require("@eslint/eslintrc"); const baseConfig = require("../../eslint.config.js"); const js = require("@eslint/js"); - + const compat = new FlatCompat({ baseDirectory: __dirname, recommendedConfig: js.configs.recommended, }); - + module.exports = [ ...baseConfig, { @@ -301,4 +301,100 @@ describe('ast-utils', () => { const result = addCompatToFlatConfig(content); expect(result).toEqual(content); }); + + it('should remove all rules from config', () => { + const content = `const FlatCompat = require("@eslint/eslintrc"); + const baseConfig = require("../../eslint.config.js"); + const js = require("@eslint/js"); + + const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + }); + + module.exports = [ + ...baseConfig, + { + files: [ + "mylib/**/*.ts", + "mylib/**/*.tsx" + ], + rules: {} + }, + ...compat.config({ extends: ["plugin:@nx/typescript"] }).map(config => ({ + ...config, + files: [ + "**/*.ts", + "**/*.tsx" + ], + rules: {} + })), + ...compat.config({ env: { jest: true } }).map(config => ({ + ...config, + files: [ + "**/*.spec.ts", + "**/*.spec.tsx", + "**/*.spec.js", + "**/*.spec.jsx" + ], + rules: {} + })), + { ignores: ["mylib/.cache/**/*"] }, + ];`; + const result = removeRulesFromLintConfig(content); + expect(result).toMatchInlineSnapshot(` + "const FlatCompat = require("@eslint/eslintrc"); + const baseConfig = require("../../eslint.config.js"); + const js = require("@eslint/js"); + + const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + }); + + module.exports = [ + ...baseConfig, + { ignores: ["mylib/.cache/**/*"] }, + ];" + `); + }); + + it('should remove all rules from starting with first', () => { + const content = `const baseConfig = require("../../eslint.config.js"); + + module.exports = [ + { + files: [ + "mylib/**/*.ts", + "mylib/**/*.tsx" + ], + rules: {} + }, + ...compat.config({ extends: ["plugin:@nx/typescript"] }).map(config => ({ + ...config, + files: [ + "**/*.ts", + "**/*.tsx" + ], + rules: {} + })), + ...compat.config({ env: { jest: true } }).map(config => ({ + ...config, + files: [ + "**/*.spec.ts", + "**/*.spec.tsx", + "**/*.spec.js", + "**/*.spec.jsx" + ], + rules: {} + })) + ];`; + const result = removeRulesFromLintConfig(content); + expect(result).toMatchInlineSnapshot(` + "const baseConfig = require("../../eslint.config.js"); + + module.exports = [ + ];" + `); + }); }); diff --git a/packages/linter/src/generators/utils/flat-config/ast-utils.ts b/packages/linter/src/generators/utils/flat-config/ast-utils.ts index aaca39e85b445..101264c7de672 100644 --- a/packages/linter/src/generators/utils/flat-config/ast-utils.ts +++ b/packages/linter/src/generators/utils/flat-config/ast-utils.ts @@ -1,11 +1,60 @@ import { ChangeType, + StringChange, applyChangesToString, joinPathFragments, } from '@nx/devkit'; import { Linter } from 'eslint'; import * as ts from 'typescript'; +export function removeRulesFromLintConfig(content: string): string { + const source = ts.createSourceFile( + '', + content, + ts.ScriptTarget.Latest, + true, + ts.ScriptKind.JS + ); + + 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; + } + }); + + if (!exportsArray) { + return content; + } + + const changes: StringChange[] = []; + exportsArray.forEach((node, i) => { + if ( + (ts.isObjectLiteralExpression(node) && + node.properties.some((p) => 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])) + ) { + const commaOffset = + i < exportsArray.length - 1 || exportsArray.hasTrailingComma ? 1 : 0; + changes.push({ + type: ChangeType.Delete, + start: node.pos, + length: node.end - node.pos + commaOffset, + }); + } + }); + + return applyChangesToString(content, changes); +} + export function addImportToFlatConfig( content: string, variable: string | string[], diff --git a/packages/linter/src/generators/workspace-rule/workspace-rule.ts b/packages/linter/src/generators/workspace-rule/workspace-rule.ts index 505fc968f0ef1..ee9a7ea8ffb3d 100644 --- a/packages/linter/src/generators/workspace-rule/workspace-rule.ts +++ b/packages/linter/src/generators/workspace-rule/workspace-rule.ts @@ -44,7 +44,7 @@ export async function lintWorkspaceRuleGenerator( /** * Import the new rule into the workspace plugin index.ts and - * register it ready for use in .eslintrc.json configs. + * register it ready for use in lint configs. */ const pluginIndexPath = joinPathFragments(workspaceLintPluginDir, 'index.ts'); const existingPluginIndexContents = tree.read(pluginIndexPath, 'utf-8'); @@ -106,7 +106,7 @@ export async function lintWorkspaceRuleGenerator( await formatFiles(tree); - logger.info(`NX Reminder: Once you have finished writing your rule logic, you need to actually enable the rule within an appropriate .eslintrc.json in your workspace, for example: + logger.info(`NX Reminder: Once you have finished writing your rule logic, you need to actually enable the rule within an appropriate ESLint config in your workspace, for example: "rules": { "@nx/workspace/${options.name}": "error" From 233e5f6efe43b9e9bd6329278bb8c3242451eb64 Mon Sep 17 00:00:00 2001 From: Miroslav Jonas Date: Wed, 9 Aug 2023 18:33:28 +0200 Subject: [PATCH 12/30] fix(linter): escape circular dependency --- packages/js/src/generators/library/library.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/js/src/generators/library/library.ts b/packages/js/src/generators/library/library.ts index 75d03b87041be..55aa2e7dafa5b 100644 --- a/packages/js/src/generators/library/library.ts +++ b/packages/js/src/generators/library/library.ts @@ -261,6 +261,7 @@ export async function addLint( if (options.rootProject) { const { addOverrideToLintConfig, + // nx-ignore-next-line } = require('@nx/linter/src/generators/utils/eslint-file'); addOverrideToLintConfig(tree, '', { From c8bb741cd9e2319473cd25a8b9b7cdfa2dbf234f Mon Sep 17 00:00:00 2001 From: Miroslav Jonas Date: Wed, 9 Aug 2023 20:07:39 +0200 Subject: [PATCH 13/30] fix(linter): add support to cypress --- packages/cypress/src/utils/add-linter.ts | 90 +++++++++---------- .../generators/application/lib/add-linting.ts | 60 ++++++------- 2 files changed, 71 insertions(+), 79 deletions(-) diff --git a/packages/cypress/src/utils/add-linter.ts b/packages/cypress/src/utils/add-linter.ts index 9926d3311c561..af300f6f580d6 100644 --- a/packages/cypress/src/utils/add-linter.ts +++ b/packages/cypress/src/utils/add-linter.ts @@ -13,7 +13,9 @@ import { installedCypressVersion } from './cypress-version'; import { eslintPluginCypressVersion } from './versions'; import { addExtendsToLintConfig, + addOverrideToLintConfig, findEslintFile, + replaceOverridesInLintConfig, } from '@nx/linter/src/generators/utils/eslint-file'; export interface CyLinterOptions { @@ -83,54 +85,50 @@ export async function addLinterToCyProject( projectConfig.root, 'plugin:cypress/recommended' ); - updateJson( - tree, - joinPathFragments(projectConfig.root, '.eslintrc.json'), - (json) => { - json.overrides ??= []; - const globals = options.rootProject ? [javaScriptOverride] : []; - const override = { - files: ['*.ts', '*.tsx', '*.js', '*.jsx'], - parserOptions: !options.setParserOptionsProject - ? undefined - : { - project: `${projectConfig.root}/tsconfig.*?.json`, - }, - rules: {}, - }; - const cyFiles = [ - { - ...override, - files: [ - '*.cy.{ts,js,tsx,jsx}', - `${options.cypressDir}/**/*.{ts,js,tsx,jsx}`, - ], - }, - ]; - if (options.overwriteExisting) { - json.overrides = [...globals, override]; - } else { - json.overrides.push(...globals); - json.overrides.push(...cyFiles); - } + const overrides = []; + const cyVersion = installedCypressVersion(); + if (cyVersion && cyVersion < 7) { + /** + * We need this override because we enabled allowJS in the tsconfig to allow for JS based Cypress tests. + * That however leads to issues with the CommonJS Cypress plugin file. + */ + overrides.push({ + files: [`${options.cypressDir}/plugins/index.js`], + rules: { + '@typescript-eslint/no-var-requires': 'off', + 'no-undef': 'off', + }, + }); + } - const cyVersion = installedCypressVersion(); - if (cyVersion && cyVersion < 7) { - /** - * We need this override because we enabled allowJS in the tsconfig to allow for JS based Cypress tests. - * That however leads to issues with the CommonJS Cypress plugin file. - */ - json.overrides.push({ - files: [`${options.cypressDir}/plugins/index.js`], - rules: { - '@typescript-eslint/no-var-requires': 'off', - 'no-undef': 'off', + if (options.overwriteExisting) { + overrides.unshift({ + files: ['*.ts', '*.tsx', '*.js', '*.jsx'], + parserOptions: !options.setParserOptionsProject + ? undefined + : { + project: `${projectConfig.root}/tsconfig.*?.json`, }, - }); - } - return json; - } - ); + rules: {}, + }); + replaceOverridesInLintConfig(tree, projectConfig.root, overrides); + } else { + overrides.unshift({ + files: [ + '*.cy.{ts,js,tsx,jsx}', + `${options.cypressDir}/**/*.{ts,js,tsx,jsx}`, + ], + parserOptions: !options.setParserOptionsProject + ? undefined + : { + project: `${projectConfig.root}/tsconfig.*?.json`, + }, + rules: {}, + }); + overrides.forEach((override) => + addOverrideToLintConfig(tree, projectConfig.root, override) + ); + } return runTasksInSerial(...tasks); } diff --git a/packages/next/src/generators/application/lib/add-linting.ts b/packages/next/src/generators/application/lib/add-linting.ts index bb95e72c05d36..77632f66842a4 100644 --- a/packages/next/src/generators/application/lib/add-linting.ts +++ b/packages/next/src/generators/application/lib/add-linting.ts @@ -12,6 +12,7 @@ import { NormalizedSchema } from './normalize-options'; import { addExtendsToLintConfig, addIgnoresToLintConfig, + addOverrideToLintConfig, } from '@nx/linter/src/generators/utils/eslint-file'; export async function addLinting( @@ -31,21 +32,38 @@ export async function addLinting( }); if (options.linter === Linter.EsLint) { - addExtendsToLintConfig(host, options.appProjectRoot, 'plugin:@nx/react'); + addExtendsToLintConfig( + host, + options.appProjectRoot, + 'plugin:@nx/react-typescript' + ); + addExtendsToLintConfig(host, options.appProjectRoot, 'next'); + addExtendsToLintConfig( + host, + options.appProjectRoot, + 'next/core-web-vitals' + ); addIgnoresToLintConfig(host, options.appProjectRoot, ['.next/**/*']); + // Turn off @next/next/no-html-link-for-pages since there is an issue with nextjs throwing linting errors + // TODO(nicholas): remove after Vercel updates nextjs linter to only lint ["*.ts", "*.tsx", "*.js", "*.jsx"] + addOverrideToLintConfig(host, options.appProjectRoot, { + files: ['*.*'], + rules: { + '@next/next/no-html-link-for-pages': 'off', + }, + }); + addOverrideToLintConfig(host, options.appProjectRoot, { + files: ['*.spec.ts', '*.spec.tsx', '*.spec.js', '*.spec.jsx'], + env: { + jest: true, + }, + }); + updateJson( host, joinPathFragments(options.appProjectRoot, '.eslintrc.json'), (json) => { - // Turn off @next/next/no-html-link-for-pages since there is an issue with nextjs throwing linting errors - // TODO(nicholas): remove after Vercel updates nextjs linter to only lint ["*.ts", "*.tsx", "*.js", "*.jsx"] - - json.rules = { - '@next/next/no-html-link-for-pages': 'off', - ...json.rules, - }; - // Find the override that handles both TS and JS files. const commonOverride = json.overrides?.find((o) => ['*.ts', '*.tsx', '*.js', '*.jsx'].every((ext) => @@ -53,12 +71,6 @@ export async function addLinting( ) ); if (commonOverride) { - // Only set parserOptions.project if it already exists (defined by options.setParserOptionsProject) - if (commonOverride.parserOptions?.project) { - commonOverride.parserOptions.project = [ - `${options.appProjectRoot}/tsconfig(.*)?.json`, - ]; - } // Configure custom pages directory for next rule if (commonOverride.rules) { commonOverride.rules = { @@ -71,24 +83,6 @@ export async function addLinting( } } - json.extends ??= []; - if (typeof json.extends === 'string') { - json.extends = [json.extends]; - } - // add next.js configuration - json.extends.unshift(...['next', 'next/core-web-vitals']); - // remove nx/react plugin, as it conflicts with the next.js one - json.extends = json.extends.filter( - (name) => - name !== 'plugin:@nx/react' && name !== 'plugin:@nrwl/nx/react' - ); - - json.extends.unshift('plugin:@nx/react-typescript'); - if (!json.env) { - json.env = {}; - } - json.env.jest = true; - return json; } ); From 42193df66caca0d1c3cbc118d0abe70e0a6a9eef Mon Sep 17 00:00:00 2001 From: Miroslav Jonas Date: Wed, 9 Aug 2023 22:07:42 +0200 Subject: [PATCH 14/30] fix(linter): fix unit tests --- .../src/generators/add-linting/add-linting.ts | 18 ++++++++++++- packages/cypress/src/utils/add-linter.ts | 2 -- .../src/generators/utils/eslint-file.ts | 7 +++--- .../utils/flat-config/ast-utils.spec.ts | 8 +++--- .../application/application.spec.ts | 25 ++++++++++++++----- .../generators/application/lib/add-linting.ts | 16 ++++-------- 6 files changed, 49 insertions(+), 27 deletions(-) diff --git a/packages/angular/src/generators/add-linting/add-linting.ts b/packages/angular/src/generators/add-linting/add-linting.ts index 27af03cdc38e5..a60d7f0bbfa71 100755 --- a/packages/angular/src/generators/add-linting/add-linting.ts +++ b/packages/angular/src/generators/add-linting/add-linting.ts @@ -9,7 +9,10 @@ import { Linter, lintProjectGenerator } from '@nx/linter'; import { mapLintPattern } from '@nx/linter/src/generators/lint-project/lint-project'; import { addAngularEsLintDependencies } from './lib/add-angular-eslint-dependencies'; import type { AddLintingGeneratorSchema } from './schema'; -import { replaceOverridesInLintConfig } from '@nx/linter/src/generators/utils/eslint-file'; +import { + findEslintFile, + replaceOverridesInLintConfig, +} from '@nx/linter/src/generators/utils/eslint-file'; import { camelize, dasherize } from '@nx/devkit/src/utils/string-utils'; export async function addLintingGenerator( @@ -35,9 +38,22 @@ export async function addLintingGenerator( }); tasks.push(lintTask); + const eslintFile = findEslintFile(tree, options.projectRoot); + // keep parser options if they exist + const hasParserOptions = tree + .read(joinPathFragments(options.projectRoot, eslintFile), 'utf8') + .includes(`${options.projectRoot}/tsconfig.*?.json`); + replaceOverridesInLintConfig(tree, options.projectRoot, [ { files: ['*.ts'], + ...(hasParserOptions + ? { + parserOptions: { + project: [`${options.projectRoot}/tsconfig.*?.json`], + }, + } + : {}), extends: [ 'plugin:@nx/angular', 'plugin:@angular-eslint/template/process-inline-templates', diff --git a/packages/cypress/src/utils/add-linter.ts b/packages/cypress/src/utils/add-linter.ts index af300f6f580d6..dfd32afda6291 100644 --- a/packages/cypress/src/utils/add-linter.ts +++ b/packages/cypress/src/utils/add-linter.ts @@ -5,10 +5,8 @@ import { readProjectConfiguration, runTasksInSerial, Tree, - updateJson, } from '@nx/devkit'; import { Linter, lintProjectGenerator } from '@nx/linter'; -import { javaScriptOverride } from '@nx/linter/src/generators/init/global-eslint-config'; import { installedCypressVersion } from './cypress-version'; import { eslintPluginCypressVersion } from './versions'; import { diff --git a/packages/linter/src/generators/utils/eslint-file.ts b/packages/linter/src/generators/utils/eslint-file.ts index ce13c355eb081..5415b49ffc978 100644 --- a/packages/linter/src/generators/utils/eslint-file.ts +++ b/packages/linter/src/generators/utils/eslint-file.ts @@ -100,11 +100,12 @@ export function replaceOverridesInLintConfig( export function addExtendsToLintConfig( tree: Tree, root: string, - plugin: string + plugin: string | string[] ) { + const plugins = Array.isArray(plugin) ? plugin : [plugin]; if (useFlatConfig()) { const fileName = joinPathFragments(root, 'eslint.config.js'); - const pluginExtends = generatePluginExtendsElement([plugin]); + const pluginExtends = generatePluginExtendsElement(plugins); tree.write( fileName, addBlockToFlatConfigExport(tree.read(fileName, 'utf8'), pluginExtends) @@ -112,7 +113,7 @@ export function addExtendsToLintConfig( } else { const fileName = joinPathFragments(root, '.eslintrc.json'); updateJson(tree, fileName, (json) => { - json.extends = [plugin, ...(json.extends ?? [])]; + json.extends = [...plugins, ...(json.extends ?? [])]; return json; }); } diff --git a/packages/linter/src/generators/utils/flat-config/ast-utils.spec.ts b/packages/linter/src/generators/utils/flat-config/ast-utils.spec.ts index a538730e17861..c81c373a2a5e9 100644 --- a/packages/linter/src/generators/utils/flat-config/ast-utils.spec.ts +++ b/packages/linter/src/generators/utils/flat-config/ast-utils.spec.ts @@ -218,12 +218,12 @@ describe('ast-utils', () => { "const FlatCompat = require("@eslint/eslintrc"); const js = require("@eslint/js"); const baseConfig = require("../../eslint.config.js"); - + const compat = new FlatCompat({ baseDirectory: __dirname, recommendedConfig: js.configs.recommended, }); - + module.exports = [ ...baseConfig, { @@ -257,12 +257,12 @@ describe('ast-utils', () => { "const FlatCompat = require("@eslint/eslintrc"); const baseConfig = require("../../eslint.config.js"); const js = require("@eslint/js"); - + const compat = new FlatCompat({ baseDirectory: __dirname, recommendedConfig: js.configs.recommended, }); - + module.exports = [ ...baseConfig, { diff --git a/packages/next/src/generators/application/application.spec.ts b/packages/next/src/generators/application/application.spec.ts index 923ed2aaeb1aa..6d644f689607a 100644 --- a/packages/next/src/generators/application/application.spec.ts +++ b/packages/next/src/generators/application/application.spec.ts @@ -573,9 +573,6 @@ describe('app', () => { const eslintJson = readJson(tree, '/apps/my-app/.eslintrc.json'); expect(eslintJson).toMatchInlineSnapshot(` { - "env": { - "jest": true, - }, "extends": [ "plugin:@nx/react-typescript", "next", @@ -615,10 +612,26 @@ describe('app', () => { ], "rules": {}, }, + { + "files": [ + "*.*", + ], + "rules": { + "@next/next/no-html-link-for-pages": "off", + }, + }, + { + "env": { + "jest": true, + }, + "files": [ + "*.spec.ts", + "*.spec.tsx", + "*.spec.js", + "*.spec.jsx", + ], + }, ], - "rules": { - "@next/next/no-html-link-for-pages": "off", - }, } `); }); diff --git a/packages/next/src/generators/application/lib/add-linting.ts b/packages/next/src/generators/application/lib/add-linting.ts index 77632f66842a4..3b516b64f35f2 100644 --- a/packages/next/src/generators/application/lib/add-linting.ts +++ b/packages/next/src/generators/application/lib/add-linting.ts @@ -32,17 +32,11 @@ export async function addLinting( }); if (options.linter === Linter.EsLint) { - addExtendsToLintConfig( - host, - options.appProjectRoot, - 'plugin:@nx/react-typescript' - ); - addExtendsToLintConfig(host, options.appProjectRoot, 'next'); - addExtendsToLintConfig( - host, - options.appProjectRoot, - 'next/core-web-vitals' - ); + addExtendsToLintConfig(host, options.appProjectRoot, [ + 'plugin:@nx/react-typescript', + 'next', + 'next/core-web-vitals', + ]); addIgnoresToLintConfig(host, options.appProjectRoot, ['.next/**/*']); // Turn off @next/next/no-html-link-for-pages since there is an issue with nextjs throwing linting errors From 634e384cb01507e41c081f4a7256a46ea6e6a6bc Mon Sep 17 00:00:00 2001 From: Miroslav Jonas Date: Thu, 10 Aug 2023 00:05:52 +0200 Subject: [PATCH 15/30] fix(linter): add support for move command --- .../move/lib/update-eslint-config.spec.ts | 221 ++++++++++++++++++ .../move/lib/update-eslint-config.ts | 117 +++++++--- 2 files changed, 307 insertions(+), 31 deletions(-) diff --git a/packages/workspace/src/generators/move/lib/update-eslint-config.spec.ts b/packages/workspace/src/generators/move/lib/update-eslint-config.spec.ts index 06f3f531a5784..4a2c70dd13302 100644 --- a/packages/workspace/src/generators/move/lib/update-eslint-config.spec.ts +++ b/packages/workspace/src/generators/move/lib/update-eslint-config.spec.ts @@ -1,4 +1,6 @@ import { + joinPathFragments, + offsetFromRoot, readJson, readProjectConfiguration, Tree, @@ -230,3 +232,222 @@ describe('updateEslint', () => { ).toEqual({ project: `libs/shared/my-destination/${storybookProject}` }); }); }); + +describe('updateEslint (flat config)', () => { + let tree: Tree; + let schema: NormalizedSchema; + + beforeEach(async () => { + schema = { + projectName: 'my-lib', + destination: 'shared/my-destination', + importPath: '@proj/shared-my-destination', + updateImportPath: true, + newProjectName: 'shared-my-destination', + relativeToRootDestination: 'libs/shared/my-destination', + }; + + tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); + }); + + it('should handle config not existing', async () => { + await libraryGenerator(tree, { + name: 'my-lib', + linter: Linter.None, + }); + + const projectConfig = readProjectConfiguration(tree, 'my-lib'); + + expect(() => { + updateEslintConfig(tree, schema, projectConfig); + }).not.toThrow(); + }); + + it('should update config extends path when project is moved to subdirectory', async () => { + await libraryGenerator(tree, { + name: 'my-lib', + linter: Linter.EsLint, + }); + convertToFlat(tree, 'libs/my-lib'); + // This step is usually handled elsewhere + tree.rename( + 'libs/my-lib/eslint.config.js', + 'libs/shared/my-destination/eslint.config.js' + ); + const projectConfig = readProjectConfiguration(tree, 'my-lib'); + + updateEslintConfig(tree, schema, projectConfig); + + expect( + tree.read('libs/shared/my-destination/eslint.config.js', 'utf-8') + ).toEqual(expect.stringContaining(`require('../../../eslint.config.js')`)); + }); + + it('should update config extends path when project is moved from subdirectory', async () => { + await libraryGenerator(tree, { + name: 'test', + directory: 'api', + linter: Linter.EsLint, + }); + convertToFlat(tree, 'libs/api/test'); + // This step is usually handled elsewhere + tree.rename('libs/api/test/eslint.config.js', 'libs/test/eslint.config.js'); + + const projectConfig = readProjectConfiguration(tree, 'api-test'); + + const newSchema = { + projectName: 'api-test', + destination: 'test', + importPath: '@proj/test', + updateImportPath: true, + newProjectName: 'test', + relativeToRootDestination: 'libs/test', + }; + + updateEslintConfig(tree, newSchema, projectConfig); + + expect(tree.read('libs/test/eslint.config.js', 'utf-8')).toEqual( + expect.stringContaining(`require('../../eslint.config.js')`) + ); + }); + + it('should update config overrides parser project when project is moved', async () => { + await libraryGenerator(tree, { + name: 'my-lib', + linter: Linter.EsLint, + setParserOptionsProject: true, + }); + convertToFlat(tree, 'libs/my-lib', { hasParser: true }); + // This step is usually handled elsewhere + tree.rename( + 'libs/my-lib/eslint.config.js', + 'libs/shared/my-destination/eslint.config.js' + ); + const projectConfig = readProjectConfiguration(tree, 'my-lib'); + + updateEslintConfig(tree, schema, projectConfig); + + expect( + tree.read('libs/shared/my-destination/eslint.config.js', 'utf-8') + ).toEqual( + expect.stringContaining( + `project: ["libs/shared/my-destination/tsconfig.*?.json"]` + ) + ); + }); + + it('should update multiple config overrides parser project when project is moved', async () => { + await libraryGenerator(tree, { + name: 'my-lib', + linter: Linter.EsLint, + setParserOptionsProject: true, + }); + // Add another parser project to eslint.json + const storybookProject = '.storybook/tsconfig.json'; + convertToFlat(tree, 'libs/my-lib', { + hasParser: true, + anotherProject: storybookProject, + }); + // This step is usually handled elsewhere + tree.rename( + 'libs/my-lib/eslint.config.js', + 'libs/shared/my-destination/eslint.config.js' + ); + const projectConfig = readProjectConfiguration(tree, 'my-lib'); + + updateEslintConfig(tree, schema, projectConfig); + + expect( + tree.read('libs/shared/my-destination/eslint.config.js', 'utf-8') + ).toEqual( + expect.stringContaining( + `project: ["libs/shared/my-destination/tsconfig.*?.json", "libs/shared/my-destination/${storybookProject}"]` + ) + ); + }); + + it('should update config parserOptions.project as a string', async () => { + await libraryGenerator(tree, { + name: 'my-lib', + linter: Linter.EsLint, + setParserOptionsProject: true, + }); + + convertToFlat(tree, 'libs/my-lib', { hasParser: true, isString: true }); + // This step is usually handled elsewhere + tree.rename( + 'libs/my-lib/eslint.config.js', + 'libs/shared/my-destination/eslint.config.js' + ); + const projectConfig = readProjectConfiguration(tree, 'my-lib'); + + updateEslintConfig(tree, schema, projectConfig); + + expect( + tree.read('libs/shared/my-destination/eslint.config.js', 'utf-8') + ).toEqual( + expect.stringContaining( + `project: "libs/shared/my-destination/tsconfig.*?.json"` + ) + ); + }); +}); + +function convertToFlat( + tree: Tree, + path: string, + options: { + hasParser?: boolean; + anotherProject?: string; + isString?: boolean; + } = {} +) { + const offset = offsetFromRoot(path); + tree.delete(joinPathFragments(path, '.eslintrc.json')); + + let parserOptions = ''; + if (options.hasParser) { + const paths = options.anotherProject + ? `["${path}/tsconfig.*?.json", "${path}/${options.anotherProject}"]` + : options.isString + ? `"${path}/tsconfig.*?.json"` + : `["${path}/tsconfig.*?.json"]`; + parserOptions = `languageOptions: { + parserOptions: { + project: ${paths} + } + }, + `; + } + + tree.write( + joinPathFragments(path, 'eslint.config.js'), + `const { FlatCompat } = require("@eslint/eslintrc"); + const baseConfig = require("${offset}eslint.config.js"); + const js = require("@eslint/js"); + const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + }); + module.exports = [ + ...baseConfig, + { + files: ["${path}/**/*.ts", "${path}/**/*.tsx", "${path}/**/*.js", "${path}/**/*.jsx"], + ${parserOptions}rules: {} + }, + { + files: ["${path}/**/*.ts", "${path}/**/*.tsx"], + rules: {} + }, + { + files: ["${path}/**/*.js", "${path}/**/*.jsx"], + rules: {} + }, + ...compat.config({ parser: "jsonc-eslint-parser" }).map(config => ({ + ...config, + files: ["${path}/**/*.json"], + rules: { "@nx/dependency-checks": "error" } + })) + ];` + ); +} diff --git a/packages/workspace/src/generators/move/lib/update-eslint-config.ts b/packages/workspace/src/generators/move/lib/update-eslint-config.ts index 0d92c62b52db7..e65a42c75a920 100644 --- a/packages/workspace/src/generators/move/lib/update-eslint-config.ts +++ b/packages/workspace/src/generators/move/lib/update-eslint-config.ts @@ -43,41 +43,96 @@ export function updateEslintConfig( project: ProjectConfiguration ) { const offset = offsetFromRoot(schema.relativeToRootDestination); + const eslintJsonPath = join( + schema.relativeToRootDestination, + '.eslintrc.json' + ); - const eslintRcPath = join(schema.relativeToRootDestination, '.eslintrc.json'); + if (tree.exists(eslintJsonPath)) { + return updateJson( + tree, + eslintJsonPath, + (eslintRcJson) => { + if (typeof eslintRcJson.extends === 'string') { + eslintRcJson.extends = offsetFilePath( + project, + eslintRcJson.extends, + offset + ); + } else if (eslintRcJson.extends) { + eslintRcJson.extends = eslintRcJson.extends.map((extend: string) => + offsetFilePath(project, extend, offset) + ); + } - if (!tree.exists(eslintRcPath)) { - // no .eslintrc found. nothing to do - return; + eslintRcJson.overrides?.forEach( + (o: { parserOptions?: { project?: string | string[] } }) => { + if (o.parserOptions?.project) { + o.parserOptions.project = Array.isArray(o.parserOptions.project) + ? o.parserOptions.project.map((p) => + p.replace(project.root, schema.relativeToRootDestination) + ) + : o.parserOptions.project.replace( + project.root, + schema.relativeToRootDestination + ); + } + } + ); + return eslintRcJson; + } + ); } - updateJson(tree, eslintRcPath, (eslintRcJson) => { - if (typeof eslintRcJson.extends === 'string') { - eslintRcJson.extends = offsetFilePath( + const eslintFlatPath = join( + schema.relativeToRootDestination, + 'eslint.config.js' + ); + if (tree.exists(eslintFlatPath)) { + const config = tree.read(eslintFlatPath, 'utf-8'); + tree.write( + eslintFlatPath, + replaceFlatConfigPaths( + config, project, - eslintRcJson.extends, - offset - ); - } else if (eslintRcJson.extends) { - eslintRcJson.extends = eslintRcJson.extends.map((extend: string) => - offsetFilePath(project, extend, offset) - ); - } - - eslintRcJson.overrides?.forEach( - (o: { parserOptions?: { project?: string | string[] } }) => { - if (o.parserOptions?.project) { - o.parserOptions.project = Array.isArray(o.parserOptions.project) - ? o.parserOptions.project.map((p) => - p.replace(project.root, schema.relativeToRootDestination) - ) - : o.parserOptions.project.replace( - project.root, - schema.relativeToRootDestination - ); - } - } + offset, + schema.relativeToRootDestination + ) + // config.replace( + // /require(['"](.*)['"])/g, + // `require('` + offsetFilePath(project, `$1`, offset) + `')` + // ) ); - return eslintRcJson; - }); + } +} + +function replaceFlatConfigPaths( + config: string, + project: ProjectConfiguration, + offset: string, + pathToDestination: string +): string { + let match; + let newConfig = config; + + // replace requires + const requireRegex = RegExp(/require\(['"](.*)['"]\)/g); + while ((match = requireRegex.exec(newConfig)) !== null) { + const newPath = offsetFilePath(project, match[1], offset); + newConfig = + newConfig.slice(0, match.index) + + `require('${newPath}')` + + newConfig.slice(match.index + match[0].length); + } + // replace projects + const projectRegex = RegExp(/project:\s?\[?['"](.*)['"]\]?/g); + while ((match = projectRegex.exec(newConfig)) !== null) { + const newProjectDef = match[0].replaceAll(project.root, pathToDestination); + newConfig = + newConfig.slice(0, match.index) + + newProjectDef + + newConfig.slice(match.index + match[0].length); + } + + return newConfig; } From 703ce017fbadc5b3ba2231920b12e6f5fb24b393 Mon Sep 17 00:00:00 2001 From: Miroslav Jonas Date: Thu, 10 Aug 2023 11:55:19 +0200 Subject: [PATCH 16/30] fix(linter): fix broken code --- packages/cypress/src/utils/add-linter.ts | 8 +++- .../converters/json-converter.ts | 15 +----- .../src/generators/utils/eslint-file.ts | 47 ++++++++++++++++++- .../generators/utils/flat-config/ast-utils.ts | 25 ++++++++++ packages/playwright/src/utils/add-linter.ts | 2 + .../react-native/src/utils/add-linting.ts | 6 ++- .../configuration/lib/util-functions.ts | 5 +- 7 files changed, 87 insertions(+), 21 deletions(-) diff --git a/packages/cypress/src/utils/add-linter.ts b/packages/cypress/src/utils/add-linter.ts index dfd32afda6291..1714b5766b215 100644 --- a/packages/cypress/src/utils/add-linter.ts +++ b/packages/cypress/src/utils/add-linter.ts @@ -12,9 +12,11 @@ import { eslintPluginCypressVersion } from './versions'; import { addExtendsToLintConfig, addOverrideToLintConfig, + addPluginsToLintConfig, findEslintFile, replaceOverridesInLintConfig, } from '@nx/linter/src/generators/utils/eslint-file'; +import { javaScriptOverride } from '@nx/linter/src/generators/init/global-eslint-config'; export interface CyLinterOptions { project: string; @@ -78,12 +80,16 @@ export async function addLinterToCyProject( : () => {} ); + const overrides = []; + if (options.rootProject) { + addPluginsToLintConfig(tree, projectConfig.root, '@nx'); + overrides.push(javaScriptOverride); + } addExtendsToLintConfig( tree, projectConfig.root, 'plugin:cypress/recommended' ); - const overrides = []; const cyVersion = installedCypressVersion(); if (cyVersion && cyVersion < 7) { /** diff --git a/packages/linter/src/generators/convert-to-flat-config/converters/json-converter.ts b/packages/linter/src/generators/convert-to-flat-config/converters/json-converter.ts index 9f76e47f890d9..c556ddc032283 100644 --- a/packages/linter/src/generators/convert-to-flat-config/converters/json-converter.ts +++ b/packages/linter/src/generators/convert-to-flat-config/converters/json-converter.ts @@ -17,6 +17,7 @@ import { mapFilePath, stringifyNodeList, } from '../../utils/flat-config/ast-utils'; +import { getPluginImport } from '../../utils/eslint-file'; /** * Converts an ESLint JSON config to a flat config. @@ -279,20 +280,6 @@ function addExtends( return isFlatCompatNeeded; } -function getPluginImport(pluginName: string): string { - 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}`; -} - function addPlugins( importsMap: Map, configBlocks: ts.Expression[], diff --git a/packages/linter/src/generators/utils/eslint-file.ts b/packages/linter/src/generators/utils/eslint-file.ts index 5415b49ffc978..51b964638cb91 100644 --- a/packages/linter/src/generators/utils/eslint-file.ts +++ b/packages/linter/src/generators/utils/eslint-file.ts @@ -1,9 +1,11 @@ -import { joinPathFragments, Tree, updateJson } from '@nx/devkit'; +import { joinPathFragments, names, Tree, updateJson } from '@nx/devkit'; import { Linter } from 'eslint'; import { useFlatConfig } from '../../utils/flat-config'; import { addBlockToFlatConfigExport, addCompatToFlatConfig, + addImportToFlatConfig, + addPluginsToExportsBlock, generateAst, generateFlatOverride, generatePluginExtendsElement, @@ -119,6 +121,35 @@ export function addExtendsToLintConfig( } } +export function addPluginsToLintConfig( + tree: Tree, + root: string, + plugin: string | string[] +) { + const plugins = Array.isArray(plugin) ? plugin : [plugin]; + if (useFlatConfig()) { + const fileName = joinPathFragments(root, 'eslint.config.js'); + let content = tree.read(fileName, 'utf8'); + const mappedPlugins: { name: string; varName: string; imp: string }[] = []; + plugins.forEach((name) => { + const imp = getPluginImport(name); + const varName = names(imp).propertyName; + mappedPlugins.push({ name, varName, imp }); + }); + mappedPlugins.forEach(({ varName, imp }) => { + content = addImportToFlatConfig(content, varName, imp); + }); + content = addPluginsToExportsBlock(content, mappedPlugins); + tree.write(fileName, content); + } else { + const fileName = joinPathFragments(root, '.eslintrc.json'); + updateJson(tree, fileName, (json) => { + json.plugins = [...plugins, ...(json.plugins ?? [])]; + return json; + }); + } +} + export function addIgnoresToLintConfig( tree: Tree, root: string, @@ -145,3 +176,17 @@ export function addIgnoresToLintConfig( }); } } + +export function getPluginImport(pluginName: string): string { + 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}`; +} diff --git a/packages/linter/src/generators/utils/flat-config/ast-utils.ts b/packages/linter/src/generators/utils/flat-config/ast-utils.ts index 101264c7de672..d8e5cd0ba247b 100644 --- a/packages/linter/src/generators/utils/flat-config/ast-utils.ts +++ b/packages/linter/src/generators/utils/flat-config/ast-utils.ts @@ -224,6 +224,31 @@ export function addBlockToFlatConfigExport( } } +export function addPluginsToExportsBlock( + content: string, + plugins: { name: string; varName: string; imp: string }[] +): string { + 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 */ diff --git a/packages/playwright/src/utils/add-linter.ts b/packages/playwright/src/utils/add-linter.ts index df619ad8e2c8b..10394fb8c9def 100644 --- a/packages/playwright/src/utils/add-linter.ts +++ b/packages/playwright/src/utils/add-linter.ts @@ -12,6 +12,7 @@ import { eslintPluginPlaywrightVersion } from './versions'; import { addExtendsToLintConfig, addOverrideToLintConfig, + addPluginsToLintConfig, findEslintFile, } from '@nx/linter/src/generators/utils/eslint-file'; @@ -77,6 +78,7 @@ export async function addLinterToPlaywrightProject( 'plugin:playwright/recommended' ); if (options.rootProject) { + addPluginsToLintConfig(tree, projectConfig.root, '@nx'); addOverrideToLintConfig(tree, projectConfig.root, javaScriptOverride); } addOverrideToLintConfig(tree, projectConfig.root, { diff --git a/packages/react-native/src/utils/add-linting.ts b/packages/react-native/src/utils/add-linting.ts index 9abe911c6754c..255cfd5420868 100644 --- a/packages/react-native/src/utils/add-linting.ts +++ b/packages/react-native/src/utils/add-linting.ts @@ -6,7 +6,10 @@ import { Tree, } from '@nx/devkit'; import { extraEslintDependencies } from '@nx/react/src/utils/lint'; -import { addIgnoresToLintConfig } from '@nx/linter/src/generators/utils/eslint-file'; +import { + addExtendsToLintConfig, + addIgnoresToLintConfig, +} from '@nx/linter/src/generators/utils/eslint-file'; interface NormalizedSchema { linter?: Linter; @@ -34,6 +37,7 @@ export async function addLinting(host: Tree, options: NormalizedSchema) { tasks.push(lintTask); + addExtendsToLintConfig(host, options.projectRoot, 'plugin:@nx/react'); addIgnoresToLintConfig(host, options.projectRoot, [ 'public', '.cache', diff --git a/packages/storybook/src/generators/configuration/lib/util-functions.ts b/packages/storybook/src/generators/configuration/lib/util-functions.ts index 4b5e16f63b4c5..308579eb46a6f 100644 --- a/packages/storybook/src/generators/configuration/lib/util-functions.ts +++ b/packages/storybook/src/generators/configuration/lib/util-functions.ts @@ -28,10 +28,7 @@ import { import { StorybookConfigureSchema } from '../schema'; import { UiFramework7 } from '../../../utils/models'; import { nxVersion } from '../../../utils/versions'; -import { - addOverrideToLintConfig, - findEslintFile, -} from '@nx/linter/src/generators/utils/eslint-file'; +import { findEslintFile } from '@nx/linter/src/generators/utils/eslint-file'; const DEFAULT_PORT = 4400; From 9dbd2f9d9c7d13838c0d9b1d308025da65f66be6 Mon Sep 17 00:00:00 2001 From: Miroslav Jonas Date: Thu, 10 Aug 2023 12:11:07 +0200 Subject: [PATCH 17/30] fix(linter): add support for flat config in cypress --- .../configuration/lib/util-functions.ts | 63 ++++++++++++------- 1 file changed, 41 insertions(+), 22 deletions(-) diff --git a/packages/storybook/src/generators/configuration/lib/util-functions.ts b/packages/storybook/src/generators/configuration/lib/util-functions.ts index 308579eb46a6f..0c752528272ac 100644 --- a/packages/storybook/src/generators/configuration/lib/util-functions.ts +++ b/packages/storybook/src/generators/configuration/lib/util-functions.ts @@ -29,6 +29,7 @@ import { StorybookConfigureSchema } from '../schema'; import { UiFramework7 } from '../../../utils/models'; import { nxVersion } from '../../../utils/versions'; import { findEslintFile } from '@nx/linter/src/generators/utils/eslint-file'; +import { useFlatConfig } from '@nx/linter/src/utils/flat-config'; const DEFAULT_PORT = 4400; @@ -365,7 +366,7 @@ export function configureTsSolutionConfig( * which includes *.stories files. * * For TSLint this is done via the builder config, for ESLint this is - * done within the .eslintrc.json file. + * done within the eslint config file. */ export function updateLintConfig(tree: Tree, schema: StorybookConfigureSchema) { const { name: projectName } = schema; @@ -382,12 +383,38 @@ export function updateLintConfig(tree: Tree, schema: StorybookConfigureSchema) { ]); }); - if (!findEslintFile(tree)) { + const eslintFile = findEslintFile(tree, root); + if (!eslintFile) { return; } - if (tree.exists(join(root, '.eslintrc.json'))) { - updateJson(tree, join(root, '.eslintrc.json'), (json) => { + const parserConfigPath = join( + root, + schema.uiFramework === '@storybook/angular' + ? '.storybook/tsconfig.json' + : 'tsconfig.storybook.json' + ); + + if (useFlatConfig()) { + let config = tree.read(eslintFile, 'utf-8'); + const projectRegex = RegExp(/project:\s?\[?['"](.*)['"]\]?/g); + let match; + while ((match = projectRegex.exec(config)) !== null) { + const matchSet = new Set( + match[1].split(',').map((p) => p.trim().replace(/['"]/g, '')) + ); + matchSet.add(parserConfigPath); + const insert = `project: [${Array.from(matchSet) + .map((p) => `'${p}'`) + .join(', ')}]`; + config = + config.slice(0, match.index) + + insert + + config.slice(match.index + match[0].length); + } + tree.write(eslintFile, config); + } else { + updateJson(tree, join(root, eslintFile), (json) => { if (typeof json.parserOptions?.project === 'string') { json.parserOptions.project = [json.parserOptions.project]; } @@ -395,9 +422,7 @@ export function updateLintConfig(tree: Tree, schema: StorybookConfigureSchema) { if (Array.isArray(json.parserOptions?.project)) { json.parserOptions.project = dedupe([ ...json.parserOptions.project, - schema.uiFramework === '@storybook/angular' - ? join(root, '.storybook/tsconfig.json') - : join(root, 'tsconfig.storybook.json'), + parserConfigPath, ]); } @@ -409,9 +434,7 @@ export function updateLintConfig(tree: Tree, schema: StorybookConfigureSchema) { if (Array.isArray(o.parserOptions?.project)) { o.parserOptions.project = dedupe([ ...o.parserOptions.project, - schema.uiFramework === '@storybook/angular' - ? join(root, '.storybook/tsconfig.json') - : join(root, 'tsconfig.storybook.json'), + parserConfigPath, ]); } } @@ -755,17 +778,13 @@ export function renameAndMoveOldTsConfig( }); } - const projectEsLintFile = joinPathFragments(projectRoot, '.eslintrc.json'); - - if (tree.exists(projectEsLintFile)) { - updateJson(tree, projectEsLintFile, (json) => { - const jsonString = JSON.stringify(json); - const newJsonString = jsonString.replace( - /\.storybook\/tsconfig\.json/g, - 'tsconfig.storybook.json' - ); - json = JSON.parse(newJsonString); - return json; - }); + const eslintFile = findEslintFile(tree, projectRoot); + if (eslintFile) { + const fileName = joinPathFragments(projectRoot, eslintFile); + const config = tree.read(fileName, 'utf-8'); + tree.write( + fileName, + config.replace(/\.storybook\/tsconfig\.json/g, 'tsconfig.storybook.json') + ); } } From 25438cbe8fa6dae10bafe416df3a519c3641fadc Mon Sep 17 00:00:00 2001 From: Miroslav Jonas Date: Thu, 10 Aug 2023 13:41:22 +0200 Subject: [PATCH 18/30] fix(linter): fix e2e faulty generation --- packages/cypress/src/utils/add-linter.ts | 33 +++++++++++-------- .../src/generators/e2e-project/e2e-project.ts | 4 --- .../src/generators/application/lib/add-e2e.ts | 1 - 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/packages/cypress/src/utils/add-linter.ts b/packages/cypress/src/utils/add-linter.ts index 1714b5766b215..2fdbd92740090 100644 --- a/packages/cypress/src/utils/add-linter.ts +++ b/packages/cypress/src/utils/add-linter.ts @@ -91,22 +91,21 @@ export async function addLinterToCyProject( 'plugin:cypress/recommended' ); const cyVersion = installedCypressVersion(); - if (cyVersion && cyVersion < 7) { - /** - * We need this override because we enabled allowJS in the tsconfig to allow for JS based Cypress tests. - * That however leads to issues with the CommonJS Cypress plugin file. - */ - overrides.push({ - files: [`${options.cypressDir}/plugins/index.js`], - rules: { - '@typescript-eslint/no-var-requires': 'off', - 'no-undef': 'off', - }, - }); - } + /** + * We need this override because we enabled allowJS in the tsconfig to allow for JS based Cypress tests. + * That however leads to issues with the CommonJS Cypress plugin file. + */ + const cy6Override = { + files: [`${options.cypressDir}/plugins/index.js`], + rules: { + '@typescript-eslint/no-var-requires': 'off', + 'no-undef': 'off', + }, + }; + const addCy6Override = cyVersion && cyVersion < 7; if (options.overwriteExisting) { - overrides.unshift({ + overrides.push({ files: ['*.ts', '*.tsx', '*.js', '*.jsx'], parserOptions: !options.setParserOptionsProject ? undefined @@ -115,6 +114,9 @@ export async function addLinterToCyProject( }, rules: {}, }); + if (addCy6Override) { + overrides.push(cy6Override); + } replaceOverridesInLintConfig(tree, projectConfig.root, overrides); } else { overrides.unshift({ @@ -129,6 +131,9 @@ export async function addLinterToCyProject( }, rules: {}, }); + if (addCy6Override) { + overrides.push(cy6Override); + } overrides.forEach((override) => addOverrideToLintConfig(tree, projectConfig.root, override) ); diff --git a/packages/node/src/generators/e2e-project/e2e-project.ts b/packages/node/src/generators/e2e-project/e2e-project.ts index ad4e49999e50f..d7c2f086b3fe0 100644 --- a/packages/node/src/generators/e2e-project/e2e-project.ts +++ b/packages/node/src/generators/e2e-project/e2e-project.ts @@ -167,7 +167,3 @@ async function normalizeOptions( export default e2eProjectGenerator; export const e2eProjectSchematic = convertNxGenerator(e2eProjectGenerator); -function isEslintConfigSupported(host: Tree) { - throw new Error('Function not implemented.'); -} - diff --git a/packages/react/src/generators/application/lib/add-e2e.ts b/packages/react/src/generators/application/lib/add-e2e.ts index 9a198d42ee829..ad63dedf74c64 100644 --- a/packages/react/src/generators/application/lib/add-e2e.ts +++ b/packages/react/src/generators/application/lib/add-e2e.ts @@ -4,7 +4,6 @@ import { ensurePackage, getPackageManagerCommand, joinPathFragments, - readProjectConfiguration, } from '@nx/devkit'; import { webStaticServeGenerator } from '@nx/web'; From dfb4c1a1f2932c95b0f04705cd5271b9cc80670e Mon Sep 17 00:00:00 2001 From: Miroslav Jonas Date: Thu, 10 Aug 2023 13:56:07 +0200 Subject: [PATCH 19/30] fix(linter): minor storybook improvement --- .../src/generators/configuration/lib/util-functions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/storybook/src/generators/configuration/lib/util-functions.ts b/packages/storybook/src/generators/configuration/lib/util-functions.ts index 0c752528272ac..f928785f8feba 100644 --- a/packages/storybook/src/generators/configuration/lib/util-functions.ts +++ b/packages/storybook/src/generators/configuration/lib/util-functions.ts @@ -419,7 +419,7 @@ export function updateLintConfig(tree: Tree, schema: StorybookConfigureSchema) { json.parserOptions.project = [json.parserOptions.project]; } - if (Array.isArray(json.parserOptions?.project)) { + if (json.parserOptions?.project) { json.parserOptions.project = dedupe([ ...json.parserOptions.project, parserConfigPath, @@ -431,7 +431,7 @@ export function updateLintConfig(tree: Tree, schema: StorybookConfigureSchema) { if (typeof o.parserOptions?.project === 'string') { o.parserOptions.project = [o.parserOptions.project]; } - if (Array.isArray(o.parserOptions?.project)) { + if (o.parserOptions?.project) { o.parserOptions.project = dedupe([ ...o.parserOptions.project, parserConfigPath, From 74833fcee95c2df41c2db72951bdb8790acda21e Mon Sep 17 00:00:00 2001 From: Miroslav Jonas Date: Fri, 11 Aug 2023 11:41:44 +0200 Subject: [PATCH 20/30] fix(linter): support next --- .../linter/src/executors/eslint/lint.impl.ts | 9 +- .../generators/lint-project/lint-project.ts | 2 +- .../src/generators/utils/eslint-file.ts | 54 +++++- .../utils/flat-config/ast-utils.spec.ts | 6 +- .../generators/utils/flat-config/ast-utils.ts | 106 +++++++++-- packages/linter/src/utils/flat-config.ts | 7 +- .../application/lib/add-linting.spec.ts | 180 ++++++++++++++++++ .../generators/application/lib/add-linting.ts | 79 ++++---- .../configuration/lib/util-functions.ts | 2 +- .../move/lib/update-eslint-config.ts | 4 - 10 files changed, 363 insertions(+), 86 deletions(-) create mode 100644 packages/next/src/generators/application/lib/add-linting.spec.ts diff --git a/packages/linter/src/executors/eslint/lint.impl.ts b/packages/linter/src/executors/eslint/lint.impl.ts index 84c0527e2c351..e4953f806ef7d 100644 --- a/packages/linter/src/executors/eslint/lint.impl.ts +++ b/packages/linter/src/executors/eslint/lint.impl.ts @@ -1,11 +1,10 @@ -import { ExecutorContext, joinPathFragments } from '@nx/devkit'; +import { ExecutorContext, joinPathFragments, workspaceRoot } from '@nx/devkit'; import { ESLint } from 'eslint'; -import { mkdirSync, writeFileSync } from 'fs'; +import { existsSync, mkdirSync, writeFileSync } from 'fs'; import { dirname, resolve } from 'path'; import type { Schema } from './schema'; import { resolveAndInstantiateESLint } from './utility/eslint-utils'; -import { useFlatConfig } from '../../utils/flat-config'; export default async function run( options: Schema, @@ -47,7 +46,9 @@ export default async function run( * we only want to support it if the user has explicitly opted into it by converting * their root ESLint config to use eslint.config.js */ - const hasFlatConfig = useFlatConfig(); + const hasFlatConfig = existsSync( + joinPathFragments(workspaceRoot, 'eslint.config.js') + ); if (!eslintConfigPath && hasFlatConfig) { const projectRoot = diff --git a/packages/linter/src/generators/lint-project/lint-project.ts b/packages/linter/src/generators/lint-project/lint-project.ts index 5c27157c80dda..7e96a6cfeaab8 100644 --- a/packages/linter/src/generators/lint-project/lint-project.ts +++ b/packages/linter/src/generators/lint-project/lint-project.ts @@ -172,7 +172,7 @@ function createEsLintConfiguration( : []), ]; - if (useFlatConfig()) { + if (useFlatConfig(tree)) { const isCompatNeeded = addDependencyChecks; const nodes = []; const importMap = new Map(); diff --git a/packages/linter/src/generators/utils/eslint-file.ts b/packages/linter/src/generators/utils/eslint-file.ts index 51b964638cb91..9bb773060cca5 100644 --- a/packages/linter/src/generators/utils/eslint-file.ts +++ b/packages/linter/src/generators/utils/eslint-file.ts @@ -10,7 +10,8 @@ import { generateFlatOverride, generatePluginExtendsElement, mapFilePath, - removeRulesFromLintConfig, + removeOverridesFromLintConfig, + replaceOverride, } from './flat-config/ast-utils'; import ts = require('typescript'); @@ -42,9 +43,10 @@ export function findEslintFile(tree: Tree, projectRoot = ''): string | null { export function addOverrideToLintConfig( tree: Tree, root: string, - override: Linter.ConfigOverride + override: Linter.ConfigOverride, + options: { insertAtTheEnd: boolean } = { insertAtTheEnd: true } ) { - if (useFlatConfig()) { + if (useFlatConfig(tree)) { const fileName = joinPathFragments(root, 'eslint.config.js'); const flatOverride = generateFlatOverride(override, root); let content = tree.read(fileName, 'utf8'); @@ -52,12 +54,19 @@ export function addOverrideToLintConfig( if (overrideNeedsCompat(override)) { content = addCompatToFlatConfig(content); } - tree.write(fileName, addBlockToFlatConfigExport(content, flatOverride)); + tree.write( + fileName, + addBlockToFlatConfigExport(content, flatOverride, options) + ); } else { const fileName = joinPathFragments(root, '.eslintrc.json'); updateJson(tree, fileName, (json) => { json.overrides ?? []; - json.overrides.push(override); + if (options.insertAtTheEnd) { + json.overrides.push(override); + } else { + json.overrides.unshift(override); + } return json; }); } @@ -71,19 +80,44 @@ function overrideNeedsCompat( ); } +export function updateOverrideInLintConfig( + tree: Tree, + root: string, + lookup: (override: Linter.ConfigOverride) => boolean, + update: ( + override: Linter.ConfigOverride + ) => Linter.ConfigOverride +) { + if (useFlatConfig(tree)) { + const fileName = joinPathFragments(root, 'eslint.config.js'); + let content = tree.read(fileName, 'utf8'); + content = replaceOverride(content, lookup, update); + tree.write(fileName, content); + } else { + const fileName = joinPathFragments(root, '.eslintrc.json'); + updateJson(tree, fileName, (json: Linter.Config) => { + const index = json.overrides.findIndex(lookup); + if (index !== -1) { + json.overrides[index] = update(json.overrides[index]); + } + return json; + }); + } +} + export function replaceOverridesInLintConfig( tree: Tree, root: string, overrides: Linter.ConfigOverride[] ) { - if (useFlatConfig()) { + if (useFlatConfig(tree)) { const fileName = joinPathFragments(root, 'eslint.config.js'); let content = tree.read(fileName, 'utf8'); // we will be using compat here so we need to make sure it's added if (overrides.some(overrideNeedsCompat)) { content = addCompatToFlatConfig(content); } - content = removeRulesFromLintConfig(content); + content = removeOverridesFromLintConfig(content); overrides.forEach((override) => { const flatOverride = generateFlatOverride(override, root); addBlockToFlatConfigExport(content, flatOverride); @@ -105,7 +139,7 @@ export function addExtendsToLintConfig( plugin: string | string[] ) { const plugins = Array.isArray(plugin) ? plugin : [plugin]; - if (useFlatConfig()) { + if (useFlatConfig(tree)) { const fileName = joinPathFragments(root, 'eslint.config.js'); const pluginExtends = generatePluginExtendsElement(plugins); tree.write( @@ -127,7 +161,7 @@ export function addPluginsToLintConfig( plugin: string | string[] ) { const plugins = Array.isArray(plugin) ? plugin : [plugin]; - if (useFlatConfig()) { + if (useFlatConfig(tree)) { const fileName = joinPathFragments(root, 'eslint.config.js'); let content = tree.read(fileName, 'utf8'); const mappedPlugins: { name: string; varName: string; imp: string }[] = []; @@ -155,7 +189,7 @@ export function addIgnoresToLintConfig( root: string, ignorePatterns: string[] ) { - if (useFlatConfig()) { + if (useFlatConfig(tree)) { const fileName = joinPathFragments(root, 'eslint.config.js'); const block = generateAst({ ignores: ignorePatterns.map((path) => mapFilePath(path, root)), diff --git a/packages/linter/src/generators/utils/flat-config/ast-utils.spec.ts b/packages/linter/src/generators/utils/flat-config/ast-utils.spec.ts index c81c373a2a5e9..728e65ac773de 100644 --- a/packages/linter/src/generators/utils/flat-config/ast-utils.spec.ts +++ b/packages/linter/src/generators/utils/flat-config/ast-utils.spec.ts @@ -4,7 +4,7 @@ import { generateAst, addImportToFlatConfig, addCompatToFlatConfig, - removeRulesFromLintConfig, + removeOverridesFromLintConfig, } from './ast-utils'; describe('ast-utils', () => { @@ -341,7 +341,7 @@ describe('ast-utils', () => { })), { ignores: ["mylib/.cache/**/*"] }, ];`; - const result = removeRulesFromLintConfig(content); + const result = removeOverridesFromLintConfig(content); expect(result).toMatchInlineSnapshot(` "const FlatCompat = require("@eslint/eslintrc"); const baseConfig = require("../../eslint.config.js"); @@ -389,7 +389,7 @@ describe('ast-utils', () => { rules: {} })) ];`; - const result = removeRulesFromLintConfig(content); + const result = removeOverridesFromLintConfig(content); expect(result).toMatchInlineSnapshot(` "const baseConfig = require("../../eslint.config.js"); diff --git a/packages/linter/src/generators/utils/flat-config/ast-utils.ts b/packages/linter/src/generators/utils/flat-config/ast-utils.ts index d8e5cd0ba247b..87b671c09914e 100644 --- a/packages/linter/src/generators/utils/flat-config/ast-utils.ts +++ b/packages/linter/src/generators/utils/flat-config/ast-utils.ts @@ -7,7 +7,10 @@ import { import { Linter } from 'eslint'; import * as ts from 'typescript'; -export function removeRulesFromLintConfig(content: string): string { +/** + * Remove all overrides from the config file + */ +export function removeOverridesFromLintConfig(content: string): string { const source = ts.createSourceFile( '', content, @@ -16,7 +19,29 @@ export function removeRulesFromLintConfig(content: string): string { ts.ScriptKind.JS ); - const exportsArray = ts.forEachChild(source, function analyze(node) { + const exportsArray = findAllBlocks(source); + if (!exportsArray) { + return content; + } + + const changes: StringChange[] = []; + exportsArray.forEach((node, i) => { + if (isOverride(node)) { + const commaOffset = + i < exportsArray.length - 1 || exportsArray.hasTrailingComma ? 1 : 0; + changes.push({ + type: ChangeType.Delete, + start: node.pos, + length: node.end - node.pos + commaOffset, + }); + } + }); + + return applyChangesToString(content, changes); +} + +function findAllBlocks(source: ts.SourceFile): ts.NodeArray { + return ts.forEachChild(source, function analyze(node) { if ( ts.isExpressionStatement(node) && ts.isBinaryExpression(node.expression) && @@ -26,35 +51,69 @@ export function removeRulesFromLintConfig(content: string): string { return node.expression.right.elements; } }); +} + +function isOverride(node: ts.Node): boolean { + return ( + (ts.isObjectLiteralExpression(node) && + node.properties.some((p) => 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])) + ); +} +export function replaceOverride( + content: string, + lookup: (override: Linter.ConfigOverride) => boolean, + update: ( + override: Linter.ConfigOverride + ) => Linter.ConfigOverride +): string { + const source = ts.createSourceFile( + '', + content, + ts.ScriptTarget.Latest, + true, + ts.ScriptKind.JS + ); + const exportsArray = findAllBlocks(source); if (!exportsArray) { return content; } - const changes: StringChange[] = []; - exportsArray.forEach((node, i) => { - if ( - (ts.isObjectLiteralExpression(node) && - node.properties.some((p) => 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])) - ) { - const commaOffset = - i < exportsArray.length - 1 || exportsArray.hasTrailingComma ? 1 : 0; - changes.push({ - type: ChangeType.Delete, - start: node.pos, - length: node.end - node.pos + commaOffset, - }); + exportsArray.forEach((node) => { + if (isOverride(node)) { + // ensure property names have double quotes so that JSON.parse works + const data = JSON.parse( + node + .getFullText() + .replace(/'/g, '"') + .replace(/\s([a-zA-Z0-9_]+)\s*:/g, ' "$1": ') + ); + if (lookup(data)) { + changes.push({ + type: ChangeType.Delete, + start: node.pos, + length: node.end - node.pos, + }); + changes.push({ + type: ChangeType.Insert, + index: node.pos, + text: JSON.stringify(update(data), null, 2), + }); + } } }); return applyChangesToString(content, changes); } +/** + * Adding require statement to the top of the file + */ export function addImportToFlatConfig( content: string, variable: string | string[], @@ -224,6 +283,9 @@ export function addBlockToFlatConfigExport( } } +/** + * Add plugins block to the top of the export blocks + */ export function addPluginsToExportsBlock( content: string, plugins: { name: string; varName: string; imp: string }[] @@ -277,6 +339,10 @@ const compat = new FlatCompat({ }); `; +/** + * Generate node list representing the imports and the exports blocks + * Optionally add flat compat initialization + */ export function createNodeList( importsMap: Map, exportElements: ts.Expression[], diff --git a/packages/linter/src/utils/flat-config.ts b/packages/linter/src/utils/flat-config.ts index d00f7fca902d6..2943a4948e39a 100644 --- a/packages/linter/src/utils/flat-config.ts +++ b/packages/linter/src/utils/flat-config.ts @@ -1,6 +1,5 @@ -import { joinPathFragments, workspaceRoot } from '@nx/devkit'; -import { existsSync } from 'fs'; +import { Tree } from '@nx/devkit'; -export function useFlatConfig(): boolean { - return existsSync(joinPathFragments(workspaceRoot, 'eslint.config.js')); +export function useFlatConfig(tree: Tree): boolean { + return tree.exists('eslint.config.js'); } diff --git a/packages/next/src/generators/application/lib/add-linting.spec.ts b/packages/next/src/generators/application/lib/add-linting.spec.ts new file mode 100644 index 0000000000000..30a88f3a5f1ed --- /dev/null +++ b/packages/next/src/generators/application/lib/add-linting.spec.ts @@ -0,0 +1,180 @@ +import { + ProjectConfiguration, + Tree, + addProjectConfiguration, + readJson, +} from '@nx/devkit'; +import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; +import { addLinting } from './add-linting'; +import { Linter } from '@nx/linter'; +import { NormalizedSchema } from './normalize-options'; + +describe('updateEslint', () => { + let tree: Tree; + let schema: NormalizedSchema; + + beforeEach(async () => { + schema = { + projectName: 'my-app', + appProjectRoot: 'apps/my-app', + linter: Linter.EsLint, + unitTestRunner: 'jest', + e2eProjectName: 'my-app-e2e', + e2eProjectRoot: 'apps/my-app-e2e', + outputPath: 'dist/apps/my-app', + name: 'my-app', + parsedTags: [], + fileName: 'index', + e2eTestRunner: 'cypress', + styledModule: null, + }; + tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); + const project: ProjectConfiguration = { + root: schema.appProjectRoot, + sourceRoot: schema.appProjectRoot, + projectType: 'application', + targets: {}, + tags: schema.parsedTags, + }; + + addProjectConfiguration(tree, schema.projectName, { + ...project, + }); + }); + + it('should update the eslintrc config', async () => { + tree.write('.eslintrc.json', JSON.stringify({ extends: ['some-config'] })); + + await addLinting(tree, schema); + + expect(readJson(tree, `${schema.appProjectRoot}/.eslintrc.json`)) + .toMatchInlineSnapshot(` + { + "extends": [ + "plugin:@nx/react-typescript", + "next", + "next/core-web-vitals", + "../../.eslintrc.json", + ], + "ignorePatterns": [ + "!**/*", + ".next/**/*", + ], + "overrides": [ + { + "files": [ + "*.*", + ], + "rules": { + "@next/next/no-html-link-for-pages": "off", + }, + }, + { + "files": [ + "*.ts", + "*.tsx", + "*.js", + "*.jsx", + ], + "rules": { + "@next/next/no-html-link-for-pages": [ + "error", + "apps/my-app/pages", + ], + }, + }, + { + "files": [ + "*.ts", + "*.tsx", + ], + "rules": {}, + }, + { + "files": [ + "*.js", + "*.jsx", + ], + "rules": {}, + }, + { + "env": { + "jest": true, + }, + "files": [ + "*.spec.ts", + "*.spec.tsx", + "*.spec.js", + "*.spec.jsx", + ], + }, + ], + } + `); + }); + + it('should update the flat config', async () => { + tree.write('eslint.config.js', `module.exports = []`); + + await addLinting(tree, schema); + + expect(tree.read(`${schema.appProjectRoot}/eslint.config.js`, 'utf-8')) + .toMatchInlineSnapshot(` + "const FlatCompat = require("@eslint/eslintrc"); + const js = require("@eslint/js"); + const baseConfig = require("../../eslint.config.js"); + const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + }); + + + module.exports = [ + { + files: ["apps/my-app/**/*.*"], + rules: { "@next/next/no-html-link-for-pages": "off" } + }, + ...baseConfig,{ + "files": [ + "apps/my-app/**/*.ts", + "apps/my-app/**/*.tsx", + "apps/my-app/**/*.js", + "apps/my-app/**/*.jsx" + ], + "rules": { + "@next/next/no-html-link-for-pages": [ + "error", + "apps/my-app/pages" + ] + } + }, + { + files: [ + "apps/my-app/**/*.ts", + "apps/my-app/**/*.tsx" + ], + rules: {} + }, + { + files: [ + "apps/my-app/**/*.js", + "apps/my-app/**/*.jsx" + ], + rules: {} + }, + ...compat.extends("plugin:@nx/react-typescript", "next", "next/core-web-vitals"), + ...compat.config({ env: { jest: true } }).map(config => ({ + ...config, + files: [ + "apps/my-app/**/*.spec.ts", + "apps/my-app/**/*.spec.tsx", + "apps/my-app/**/*.spec.js", + "apps/my-app/**/*.spec.jsx" + ] + })), + { ignores: ["apps/my-app/.next/**/*"] } + ]; + " + `); + }); +}); diff --git a/packages/next/src/generators/application/lib/add-linting.ts b/packages/next/src/generators/application/lib/add-linting.ts index 3b516b64f35f2..554aa932cc191 100644 --- a/packages/next/src/generators/application/lib/add-linting.ts +++ b/packages/next/src/generators/application/lib/add-linting.ts @@ -5,7 +5,6 @@ import { joinPathFragments, runTasksInSerial, Tree, - updateJson, } from '@nx/devkit'; import { extraEslintDependencies } from '@nx/react/src/utils/lint'; import { NormalizedSchema } from './normalize-options'; @@ -13,6 +12,8 @@ import { addExtendsToLintConfig, addIgnoresToLintConfig, addOverrideToLintConfig, + findEslintFile, + updateOverrideInLintConfig, } from '@nx/linter/src/generators/utils/eslint-file'; export async function addLinting( @@ -30,56 +31,56 @@ export async function addLinting( skipFormat: true, rootProject: options.rootProject, }); - if (options.linter === Linter.EsLint) { addExtendsToLintConfig(host, options.appProjectRoot, [ 'plugin:@nx/react-typescript', 'next', 'next/core-web-vitals', ]); - addIgnoresToLintConfig(host, options.appProjectRoot, ['.next/**/*']); // Turn off @next/next/no-html-link-for-pages since there is an issue with nextjs throwing linting errors // TODO(nicholas): remove after Vercel updates nextjs linter to only lint ["*.ts", "*.tsx", "*.js", "*.jsx"] - addOverrideToLintConfig(host, options.appProjectRoot, { - files: ['*.*'], - rules: { - '@next/next/no-html-link-for-pages': 'off', - }, - }); - addOverrideToLintConfig(host, options.appProjectRoot, { - files: ['*.spec.ts', '*.spec.tsx', '*.spec.js', '*.spec.jsx'], - env: { - jest: true, + addOverrideToLintConfig( + host, + options.appProjectRoot, + { + files: ['*.*'], + rules: { + '@next/next/no-html-link-for-pages': 'off', + }, }, - }); - - updateJson( + { insertAtTheEnd: false } + ); + updateOverrideInLintConfig( host, - joinPathFragments(options.appProjectRoot, '.eslintrc.json'), - (json) => { - // Find the override that handles both TS and JS files. - const commonOverride = json.overrides?.find((o) => - ['*.ts', '*.tsx', '*.js', '*.jsx'].every((ext) => - o.files.includes(ext) - ) - ); - if (commonOverride) { - // Configure custom pages directory for next rule - if (commonOverride.rules) { - commonOverride.rules = { - ...commonOverride.rules, - '@next/next/no-html-link-for-pages': [ - 'error', - `${options.appProjectRoot}/pages`, - ], - }; - } - } - - return json; - } + options.appProjectRoot, + (o) => + Array.isArray(o.files) && + o.files.some((f) => f.match(/\*\.ts$/)) && + o.files.some((f) => f.match(/\*\.tsx$/)) && + o.files.some((f) => f.match(/\*\.js$/)) && + o.files.some((f) => f.match(/\*\.jsx$/)), + (o) => ({ + ...o, + rules: { + ...o.rules, + '@next/next/no-html-link-for-pages': [ + 'error', + `${options.appProjectRoot}/pages`, + ], + }, + }) ); + // add jest specific config + if (options.unitTestRunner === 'jest') { + addOverrideToLintConfig(host, options.appProjectRoot, { + files: ['*.spec.ts', '*.spec.tsx', '*.spec.js', '*.spec.jsx'], + env: { + jest: true, + }, + }); + } + addIgnoresToLintConfig(host, options.appProjectRoot, ['.next/**/*']); } const installTask = addDependenciesToPackageJson( diff --git a/packages/storybook/src/generators/configuration/lib/util-functions.ts b/packages/storybook/src/generators/configuration/lib/util-functions.ts index f928785f8feba..0983b52851fc7 100644 --- a/packages/storybook/src/generators/configuration/lib/util-functions.ts +++ b/packages/storybook/src/generators/configuration/lib/util-functions.ts @@ -395,7 +395,7 @@ export function updateLintConfig(tree: Tree, schema: StorybookConfigureSchema) { : 'tsconfig.storybook.json' ); - if (useFlatConfig()) { + if (useFlatConfig(tree)) { let config = tree.read(eslintFile, 'utf-8'); const projectRegex = RegExp(/project:\s?\[?['"](.*)['"]\]?/g); let match; diff --git a/packages/workspace/src/generators/move/lib/update-eslint-config.ts b/packages/workspace/src/generators/move/lib/update-eslint-config.ts index e65a42c75a920..95c32996a13ad 100644 --- a/packages/workspace/src/generators/move/lib/update-eslint-config.ts +++ b/packages/workspace/src/generators/move/lib/update-eslint-config.ts @@ -98,10 +98,6 @@ export function updateEslintConfig( offset, schema.relativeToRootDestination ) - // config.replace( - // /require(['"](.*)['"])/g, - // `require('` + offsetFilePath(project, `$1`, offset) + `')` - // ) ); } } From 8ae099422897fe39070ede4e08dd2da7185e3eea Mon Sep 17 00:00:00 2001 From: Miroslav Jonas Date: Fri, 11 Aug 2023 12:36:47 +0200 Subject: [PATCH 21/30] fix(linter): support compat for override replace --- .../utils/flat-config/ast-utils.spec.ts | 583 ++++++++++++------ .../generators/utils/flat-config/ast-utils.ts | 35 +- 2 files changed, 416 insertions(+), 202 deletions(-) diff --git a/packages/linter/src/generators/utils/flat-config/ast-utils.spec.ts b/packages/linter/src/generators/utils/flat-config/ast-utils.spec.ts index 728e65ac773de..5a54556d56390 100644 --- a/packages/linter/src/generators/utils/flat-config/ast-utils.spec.ts +++ b/packages/linter/src/generators/utils/flat-config/ast-utils.spec.ts @@ -5,11 +5,13 @@ import { addImportToFlatConfig, addCompatToFlatConfig, removeOverridesFromLintConfig, + replaceOverride, } from './ast-utils'; describe('ast-utils', () => { - it('should inject block to the end of the file', () => { - const content = `const baseConfig = require("../../eslint.config.js"); + describe('addBlockToFlatConfigExport', () => { + it('should inject block to the end of the file', () => { + const content = `const baseConfig = require("../../eslint.config.js"); module.exports = [ ...baseConfig, { @@ -21,37 +23,37 @@ describe('ast-utils', () => { }, { ignores: ["mylib/.cache/**/*"] }, ];`; - const result = addBlockToFlatConfigExport( - content, - generateAst({ - files: ['**/*.svg'], - rules: { - '@nx/do-something-with-svg': 'error', - }, - }) - ); - expect(result).toMatchInlineSnapshot(` - "const baseConfig = require("../../eslint.config.js"); - module.exports = [ - ...baseConfig, + const result = addBlockToFlatConfigExport( + content, + generateAst({ + files: ['**/*.svg'], + rules: { + '@nx/do-something-with-svg': 'error', + }, + }) + ); + expect(result).toMatchInlineSnapshot(` + "const baseConfig = require("../../eslint.config.js"); + module.exports = [ + ...baseConfig, + { + files: [ + "mylib/**/*.ts", + "mylib/**/*.tsx" + ], + rules: {} + }, + { ignores: ["mylib/.cache/**/*"] }, { - files: [ - "mylib/**/*.ts", - "mylib/**/*.tsx" - ], - rules: {} + files: ["**/*.svg"], + rules: { "@nx/do-something-with-svg": "error" } }, - { ignores: ["mylib/.cache/**/*"] }, - { - files: ["**/*.svg"], - rules: { "@nx/do-something-with-svg": "error" } - }, - ];" - `); - }); + ];" + `); + }); - it('should inject spread to the beginning of the file', () => { - const content = `const baseConfig = require("../../eslint.config.js"); + it('should inject spread to the beginning of the file', () => { + const content = `const baseConfig = require("../../eslint.config.js"); module.exports = [ ...baseConfig, { @@ -63,30 +65,32 @@ describe('ast-utils', () => { }, { ignores: ["mylib/.cache/**/*"] }, ];`; - const result = addBlockToFlatConfigExport( - content, - ts.factory.createSpreadElement(ts.factory.createIdentifier('config')), - { insertAtTheEnd: false } - ); - expect(result).toMatchInlineSnapshot(` - "const baseConfig = require("../../eslint.config.js"); - module.exports = [ - ...config, - ...baseConfig, - { - files: [ - "mylib/**/*.ts", - "mylib/**/*.tsx" - ], - rules: {} - }, - { ignores: ["mylib/.cache/**/*"] }, - ];" - `); + const result = addBlockToFlatConfigExport( + content, + ts.factory.createSpreadElement(ts.factory.createIdentifier('config')), + { insertAtTheEnd: false } + ); + expect(result).toMatchInlineSnapshot(` + "const baseConfig = require("../../eslint.config.js"); + module.exports = [ + ...config, + ...baseConfig, + { + files: [ + "mylib/**/*.ts", + "mylib/**/*.tsx" + ], + rules: {} + }, + { ignores: ["mylib/.cache/**/*"] }, + ];" + `); + }); }); - it('should inject import if not found', () => { - const content = `const baseConfig = require("../../eslint.config.js"); + describe('addImportToFlatConfig', () => { + it('should inject import if not found', () => { + const content = `const baseConfig = require("../../eslint.config.js"); module.exports = [ ...baseConfig, { @@ -98,30 +102,30 @@ describe('ast-utils', () => { }, { ignores: ["mylib/.cache/**/*"] }, ];`; - const result = addImportToFlatConfig( - content, - 'varName', - '@myorg/awesome-config' - ); - expect(result).toMatchInlineSnapshot(` - "const varName = require("@myorg/awesome-config"); - const baseConfig = require("../../eslint.config.js"); - module.exports = [ - ...baseConfig, - { - files: [ - "mylib/**/*.ts", - "mylib/**/*.tsx" - ], - rules: {} - }, - { ignores: ["mylib/.cache/**/*"] }, - ];" - `); - }); + const result = addImportToFlatConfig( + content, + 'varName', + '@myorg/awesome-config' + ); + expect(result).toMatchInlineSnapshot(` + "const varName = require("@myorg/awesome-config"); + const baseConfig = require("../../eslint.config.js"); + module.exports = [ + ...baseConfig, + { + files: [ + "mylib/**/*.ts", + "mylib/**/*.tsx" + ], + rules: {} + }, + { ignores: ["mylib/.cache/**/*"] }, + ];" + `); + }); - it('should update import if already found', () => { - const content = `const { varName } = require("@myorg/awesome-config"); + it('should update import if already found', () => { + const content = `const { varName } = require("@myorg/awesome-config"); const baseConfig = require("../../eslint.config.js"); module.exports = [ ...baseConfig, @@ -134,30 +138,30 @@ describe('ast-utils', () => { }, { ignores: ["mylib/.cache/**/*"] }, ];`; - const result = addImportToFlatConfig( - content, - ['otherName', 'someName'], - '@myorg/awesome-config' - ); - expect(result).toMatchInlineSnapshot(` - "const { varName, otherName, someName } = require("@myorg/awesome-config"); - const baseConfig = require("../../eslint.config.js"); - module.exports = [ - ...baseConfig, - { - files: [ - "mylib/**/*.ts", - "mylib/**/*.tsx" - ], - rules: {} - }, - { ignores: ["mylib/.cache/**/*"] }, - ];" - `); - }); + const result = addImportToFlatConfig( + content, + ['otherName', 'someName'], + '@myorg/awesome-config' + ); + expect(result).toMatchInlineSnapshot(` + "const { varName, otherName, someName } = require("@myorg/awesome-config"); + const baseConfig = require("../../eslint.config.js"); + module.exports = [ + ...baseConfig, + { + files: [ + "mylib/**/*.ts", + "mylib/**/*.tsx" + ], + rules: {} + }, + { ignores: ["mylib/.cache/**/*"] }, + ];" + `); + }); - it('should not inject import if already exists', () => { - const content = `const { varName, otherName } = require("@myorg/awesome-config"); + it('should not inject import if already exists', () => { + const content = `const { varName, otherName } = require("@myorg/awesome-config"); const baseConfig = require("../../eslint.config.js"); module.exports = [ ...baseConfig, @@ -170,16 +174,16 @@ describe('ast-utils', () => { }, { ignores: ["mylib/.cache/**/*"] }, ];`; - const result = addImportToFlatConfig( - content, - ['otherName'], - '@myorg/awesome-config' - ); - expect(result).toEqual(content); - }); + const result = addImportToFlatConfig( + content, + ['otherName'], + '@myorg/awesome-config' + ); + expect(result).toEqual(content); + }); - it('should not update import if already exists', () => { - const content = `const varName = require("@myorg/awesome-config"); + it('should not update import if already exists', () => { + const content = `const varName = require("@myorg/awesome-config"); const baseConfig = require("../../eslint.config.js"); module.exports = [ ...baseConfig, @@ -192,16 +196,18 @@ describe('ast-utils', () => { }, { ignores: ["mylib/.cache/**/*"] }, ];`; - const result = addImportToFlatConfig( - content, - 'varName', - '@myorg/awesome-config' - ); - expect(result).toEqual(content); + const result = addImportToFlatConfig( + content, + 'varName', + '@myorg/awesome-config' + ); + expect(result).toEqual(content); + }); }); - it('should add compat to config', () => { - const content = `const baseConfig = require("../../eslint.config.js"); + describe('addCompatToFlatConfig', () => { + it('should add compat to config', () => { + const content = `const baseConfig = require("../../eslint.config.js"); module.exports = [ ...baseConfig, { @@ -213,33 +219,33 @@ describe('ast-utils', () => { }, { ignores: ["mylib/.cache/**/*"] }, ];`; - const result = addCompatToFlatConfig(content); - expect(result).toMatchInlineSnapshot(` - "const FlatCompat = require("@eslint/eslintrc"); - const js = require("@eslint/js"); - const baseConfig = require("../../eslint.config.js"); - - const compat = new FlatCompat({ - baseDirectory: __dirname, - recommendedConfig: js.configs.recommended, - }); - - module.exports = [ - ...baseConfig, - { - files: [ - "mylib/**/*.ts", - "mylib/**/*.tsx" - ], - rules: {} - }, - { ignores: ["mylib/.cache/**/*"] }, - ];" - `); - }); + const result = addCompatToFlatConfig(content); + expect(result).toMatchInlineSnapshot(` + "const FlatCompat = require("@eslint/eslintrc"); + const js = require("@eslint/js"); + const baseConfig = require("../../eslint.config.js"); + + const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + }); + + module.exports = [ + ...baseConfig, + { + files: [ + "mylib/**/*.ts", + "mylib/**/*.tsx" + ], + rules: {} + }, + { ignores: ["mylib/.cache/**/*"] }, + ];" + `); + }); - it('should add only partially compat to config if parts exist', () => { - const content = `const baseConfig = require("../../eslint.config.js"); + it('should add only partially compat to config if parts exist', () => { + const content = `const baseConfig = require("../../eslint.config.js"); const js = require("@eslint/js"); module.exports = [ ...baseConfig, @@ -252,33 +258,33 @@ describe('ast-utils', () => { }, { ignores: ["mylib/.cache/**/*"] }, ];`; - const result = addCompatToFlatConfig(content); - expect(result).toMatchInlineSnapshot(` - "const FlatCompat = require("@eslint/eslintrc"); - const baseConfig = require("../../eslint.config.js"); - const js = require("@eslint/js"); - - const compat = new FlatCompat({ - baseDirectory: __dirname, - recommendedConfig: js.configs.recommended, - }); - - module.exports = [ - ...baseConfig, - { - files: [ - "mylib/**/*.ts", - "mylib/**/*.tsx" - ], - rules: {} - }, - { ignores: ["mylib/.cache/**/*"] }, - ];" - `); - }); + const result = addCompatToFlatConfig(content); + expect(result).toMatchInlineSnapshot(` + "const FlatCompat = require("@eslint/eslintrc"); + const baseConfig = require("../../eslint.config.js"); + const js = require("@eslint/js"); + + const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + }); + + module.exports = [ + ...baseConfig, + { + files: [ + "mylib/**/*.ts", + "mylib/**/*.tsx" + ], + rules: {} + }, + { ignores: ["mylib/.cache/**/*"] }, + ];" + `); + }); - it('should not add compat to config if exist', () => { - const content = `const FlatCompat = require("@eslint/eslintrc"); + it('should not add compat to config if exist', () => { + const content = `const FlatCompat = require("@eslint/eslintrc"); const baseConfig = require("../../eslint.config.js"); const js = require("@eslint/js"); @@ -298,12 +304,14 @@ describe('ast-utils', () => { }, { ignores: ["mylib/.cache/**/*"] }, ];`; - const result = addCompatToFlatConfig(content); - expect(result).toEqual(content); + const result = addCompatToFlatConfig(content); + expect(result).toEqual(content); + }); }); - it('should remove all rules from config', () => { - const content = `const FlatCompat = require("@eslint/eslintrc"); + describe('removeOverridesFromLintConfig', () => { + it('should remove all rules from config', () => { + const content = `const FlatCompat = require("@eslint/eslintrc"); const baseConfig = require("../../eslint.config.js"); const js = require("@eslint/js"); @@ -341,26 +349,26 @@ describe('ast-utils', () => { })), { ignores: ["mylib/.cache/**/*"] }, ];`; - const result = removeOverridesFromLintConfig(content); - expect(result).toMatchInlineSnapshot(` - "const FlatCompat = require("@eslint/eslintrc"); - const baseConfig = require("../../eslint.config.js"); - const js = require("@eslint/js"); + const result = removeOverridesFromLintConfig(content); + expect(result).toMatchInlineSnapshot(` + "const FlatCompat = require("@eslint/eslintrc"); + const baseConfig = require("../../eslint.config.js"); + const js = require("@eslint/js"); - const compat = new FlatCompat({ - baseDirectory: __dirname, - recommendedConfig: js.configs.recommended, - }); + const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + }); - module.exports = [ - ...baseConfig, - { ignores: ["mylib/.cache/**/*"] }, - ];" - `); - }); + module.exports = [ + ...baseConfig, + { ignores: ["mylib/.cache/**/*"] }, + ];" + `); + }); - it('should remove all rules from starting with first', () => { - const content = `const baseConfig = require("../../eslint.config.js"); + it('should remove all rules from starting with first', () => { + const content = `const baseConfig = require("../../eslint.config.js"); module.exports = [ { @@ -389,12 +397,199 @@ describe('ast-utils', () => { rules: {} })) ];`; - const result = removeOverridesFromLintConfig(content); - expect(result).toMatchInlineSnapshot(` - "const baseConfig = require("../../eslint.config.js"); + const result = removeOverridesFromLintConfig(content); + expect(result).toMatchInlineSnapshot(` + "const baseConfig = require("../../eslint.config.js"); - module.exports = [ - ];" - `); + module.exports = [ + ];" + `); + }); + }); + + describe('replaceOverride', () => { + it('should find and replace rules in override', () => { + const content = `const baseConfig = require("../../eslint.config.js"); + + module.exports = [ + { + files: [ + "mylib/**/*.ts", + "mylib/**/*.tsx" + ], + rules: { + 'my-ts-rule': 'error' + } + }, + { + files: [ + "mylib/**/*.ts", + "mylib/**/*.js" + ], + rules: {} + }, + { + files: [ + "mylib/**/*.js", + "mylib/**/*.jsx" + ], + rules: { + 'my-js-rule': 'error' + } + }, + ];`; + + const result = replaceOverride( + content, + (o) => o.files.includes('mylib/**/*.ts'), + (o) => ({ + ...o, + rules: { + 'my-rule': 'error', + }, + }) + ); + expect(result).toMatchInlineSnapshot(` + "const baseConfig = require("../../eslint.config.js"); + + module.exports = [ + { + "files": [ + "mylib/**/*.ts", + "mylib/**/*.tsx" + ], + "rules": { + "my-rule": "error" + } + }, + { + "files": [ + "mylib/**/*.ts", + "mylib/**/*.js" + ], + "rules": { + "my-rule": "error" + } + }, + { + files: [ + "mylib/**/*.js", + "mylib/**/*.jsx" + ], + rules: { + 'my-js-rule': 'error' + } + }, + ];" + `); + }); + + it('should append rules in override', () => { + const content = `const baseConfig = require("../../eslint.config.js"); + + module.exports = [ + { + files: [ + "mylib/**/*.ts", + "mylib/**/*.tsx" + ], + rules: { + 'my-ts-rule': 'error' + } + }, + { + files: [ + "mylib/**/*.js", + "mylib/**/*.jsx" + ], + rules: { + 'my-js-rule': 'error' + } + }, + ];`; + + const result = replaceOverride( + content, + (o) => o.files.includes('mylib/**/*.ts'), + (o) => ({ + ...o, + rules: { + ...o.rules, + 'my-new-rule': 'error', + }, + }) + ); + expect(result).toMatchInlineSnapshot(` + "const baseConfig = require("../../eslint.config.js"); + + module.exports = [ + { + "files": [ + "mylib/**/*.ts", + "mylib/**/*.tsx" + ], + "rules": { + "my-ts-rule": "error", + "my-new-rule": "error" + } + }, + { + files: [ + "mylib/**/*.js", + "mylib/**/*.jsx" + ], + rules: { + 'my-js-rule': 'error' + } + }, + ];" + `); + }); + + it('should work for compat overrides', () => { + const content = `const baseConfig = require("../../eslint.config.js"); + + module.exports = [ + ...compat.config({ extends: ["plugin:@nx/typescript"] }).map(config => ({ + ...config, + files: [ + "mylib/**/*.ts", + "mylib/**/*.tsx" + ], + rules: { + 'my-ts-rule': 'error' + } + }), + ];`; + + const result = replaceOverride( + content, + (o) => o.files.includes('mylib/**/*.ts'), + (o) => ({ + ...o, + rules: { + ...o.rules, + 'my-new-rule': 'error', + }, + }) + ); + expect(result).toMatchInlineSnapshot(` + "const baseConfig = require("../../eslint.config.js"); + + module.exports = [ + ...compat.config({ extends: ["plugin:@nx/typescript"] }).map(config => ({ + ...config, + "files": [ + "mylib/**/*.ts", + "mylib/**/*.tsx" + ], + "rules": { + "my-ts-rule": "error", + "my-new-rule": "error" + } + }), + ];" + `); + }); }); }); diff --git a/packages/linter/src/generators/utils/flat-config/ast-utils.ts b/packages/linter/src/generators/utils/flat-config/ast-utils.ts index 87b671c09914e..32864ddbda9ab 100644 --- a/packages/linter/src/generators/utils/flat-config/ast-utils.ts +++ b/packages/linter/src/generators/utils/flat-config/ast-utils.ts @@ -61,10 +61,14 @@ function isOverride(node: ts.Node): boolean { (ts.isSpreadElement(node) && ts.isCallExpression(node.expression) && ts.isPropertyAccessExpression(node.expression.expression) && - ts.isArrowFunction(node.expression.arguments[0])) + ts.isArrowFunction(node.expression.arguments[0]) && + ts.isParenthesizedExpression(node.expression.arguments[0].body)) ); } +/** + * Finds an override matching the lookup function and applies the update function to it + */ export function replaceOverride( content: string, lookup: (override: Linter.ConfigOverride) => boolean, @@ -86,23 +90,38 @@ export function replaceOverride( const changes: StringChange[] = []; exportsArray.forEach((node) => { if (isOverride(node)) { - // ensure property names have double quotes so that JSON.parse works + 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(/\s*\.\.\.[a-zA-Z0-9_]+,?\n?/, ''); + start = + node['expression'].arguments[0].body.expression.properties.pos + + (fullNodeText.length - objSource.length); + end = node['expression'].arguments[0].body.expression.properties.end; + } const data = JSON.parse( - node - .getFullText() + objSource + // ensure property names have double quotes so that JSON.parse works .replace(/'/g, '"') .replace(/\s([a-zA-Z0-9_]+)\s*:/g, ' "$1": ') ); if (lookup(data)) { changes.push({ type: ChangeType.Delete, - start: node.pos, - length: node.end - node.pos, + start, + length: end - start, }); changes.push({ type: ChangeType.Insert, - index: node.pos, - text: JSON.stringify(update(data), null, 2), + index: start, + text: JSON.stringify(update(data), null, 2).slice(2, -2), // remove curly braces and start/end line breaks since we are injecting just properties }); } } From 4a3dc326d6f0149b2632f8e09e277bc55b360339 Mon Sep 17 00:00:00 2001 From: Miroslav Jonas Date: Fri, 11 Aug 2023 14:38:59 +0200 Subject: [PATCH 22/30] fix(linter): enable plugin --- .../src/generators/utils/eslint-file.ts | 61 ++++++- .../generators/utils/flat-config/ast-utils.ts | 44 ++++- .../src/generators/lint-checks/generator.ts | 153 ++++++++++-------- 3 files changed, 183 insertions(+), 75 deletions(-) diff --git a/packages/linter/src/generators/utils/eslint-file.ts b/packages/linter/src/generators/utils/eslint-file.ts index 9bb773060cca5..d61422fb27477 100644 --- a/packages/linter/src/generators/utils/eslint-file.ts +++ b/packages/linter/src/generators/utils/eslint-file.ts @@ -1,4 +1,10 @@ -import { joinPathFragments, names, Tree, updateJson } from '@nx/devkit'; +import { + joinPathFragments, + names, + readJson, + Tree, + updateJson, +} from '@nx/devkit'; import { Linter } from 'eslint'; import { useFlatConfig } from '../../utils/flat-config'; import { @@ -9,6 +15,7 @@ import { generateAst, generateFlatOverride, generatePluginExtendsElement, + hasOverride, mapFilePath, removeOverridesFromLintConfig, replaceOverride, @@ -26,11 +33,15 @@ export const eslintConfigFileWhitelist = [ ]; export const baseEsLintConfigFile = '.eslintrc.base.json'; +export const baseEsLintFlatConfigFile = 'eslint.base.config.js'; export function findEslintFile(tree: Tree, projectRoot = ''): string | null { if (projectRoot === '' && tree.exists(baseEsLintConfigFile)) { return baseEsLintConfigFile; } + if (projectRoot === '' && tree.exists(baseEsLintFlatConfigFile)) { + return baseEsLintFlatConfigFile; + } for (const file of eslintConfigFileWhitelist) { if (tree.exists(joinPathFragments(projectRoot, file))) { return file; @@ -40,14 +51,29 @@ export function findEslintFile(tree: Tree, projectRoot = ''): string | null { return null; } +export function isMigrationSupported(tree: Tree, projectRoot = ''): boolean { + const eslintFile = findEslintFile(tree, projectRoot); + if (!eslintFile) { + return; + } + return eslintFile.endsWith('.json') || eslintFile.endsWith('.config.js'); +} + export function addOverrideToLintConfig( tree: Tree, root: string, override: Linter.ConfigOverride, - options: { insertAtTheEnd: boolean } = { insertAtTheEnd: true } + options: { insertAtTheEnd?: boolean; checkBaseConfig?: boolean } = { + insertAtTheEnd: true, + } ) { + const isBase = + options.checkBaseConfig && findEslintFile(tree, root).includes('.base'); if (useFlatConfig(tree)) { - const fileName = joinPathFragments(root, 'eslint.config.js'); + const fileName = joinPathFragments( + root, + isBase ? baseEsLintFlatConfigFile : 'eslint.config.js' + ); const flatOverride = generateFlatOverride(override, root); let content = tree.read(fileName, 'utf8'); // we will be using compat here so we need to make sure it's added @@ -59,7 +85,10 @@ export function addOverrideToLintConfig( addBlockToFlatConfigExport(content, flatOverride, options) ); } else { - const fileName = joinPathFragments(root, '.eslintrc.json'); + const fileName = joinPathFragments( + root, + isBase ? baseEsLintConfigFile : '.eslintrc.json' + ); updateJson(tree, fileName, (json) => { json.overrides ?? []; if (options.insertAtTheEnd) { @@ -105,6 +134,30 @@ export function updateOverrideInLintConfig( } } +export function lintConfigHasOverride( + tree: Tree, + root: string, + lookup: (override: Linter.ConfigOverride) => boolean, + checkBaseConfig = false +): boolean { + const isBase = + checkBaseConfig && findEslintFile(tree, root).includes('.base'); + if (useFlatConfig(tree)) { + const fileName = joinPathFragments( + root, + isBase ? baseEsLintFlatConfigFile : 'eslint.config.js' + ); + const content = tree.read(fileName, 'utf8'); + return hasOverride(content, lookup); + } else { + const fileName = joinPathFragments( + root, + isBase ? baseEsLintConfigFile : '.eslintrc.json' + ); + return readJson(tree, fileName).overrides?.some(lookup) || false; + } +} + export function replaceOverridesInLintConfig( tree: Tree, root: string, diff --git a/packages/linter/src/generators/utils/flat-config/ast-utils.ts b/packages/linter/src/generators/utils/flat-config/ast-utils.ts index 32864ddbda9ab..0416f3a709554 100644 --- a/packages/linter/src/generators/utils/flat-config/ast-utils.ts +++ b/packages/linter/src/generators/utils/flat-config/ast-utils.ts @@ -66,6 +66,46 @@ function isOverride(node: ts.Node): boolean { ); } +export function hasOverride( + content: string, + lookup: (override: Linter.ConfigOverride) => boolean +): boolean { + const source = ts.createSourceFile( + '', + content, + ts.ScriptTarget.Latest, + true, + ts.ScriptKind.JS + ); + const exportsArray = findAllBlocks(source); + if (!exportsArray) { + return false; + } + for (const node of exportsArray) { + if (isOverride(node)) { + let objSource; + if (ts.isObjectLiteralExpression(node)) { + objSource = node.getFullText(); + } else { + const fullNodeText = + node['expression'].arguments[0].body.expression.getFullText(); + // strip any spread elements + objSource = fullNodeText.replace(/\s*\.\.\.[a-zA-Z0-9_]+,?\n?/, ''); + } + const data = JSON.parse( + objSource + // ensure property names have double quotes so that JSON.parse works + .replace(/'/g, '"') + .replace(/\s([a-zA-Z0-9_]+)\s*:/g, ' "$1": ') + ); + if (lookup(data)) { + return true; + } + } + } + return false; +} + /** * Finds an override matching the lookup function and applies the update function to it */ @@ -261,7 +301,9 @@ export function addImportToFlatConfig( export function addBlockToFlatConfigExport( content: string, config: ts.Expression | ts.SpreadElement, - options: { insertAtTheEnd: boolean } = { insertAtTheEnd: true } + options: { insertAtTheEnd?: boolean; checkBaseConfig?: boolean } = { + insertAtTheEnd: true, + } ): string { const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); const source = ts.createSourceFile( diff --git a/packages/plugin/src/generators/lint-checks/generator.ts b/packages/plugin/src/generators/lint-checks/generator.ts index c60018e856749..a68ad76e14780 100644 --- a/packages/plugin/src/generators/lint-checks/generator.ts +++ b/packages/plugin/src/generators/lint-checks/generator.ts @@ -8,7 +8,6 @@ import { readProjectConfiguration, TargetConfiguration, Tree, - updateJson, updateProjectConfiguration, writeJson, } from '@nx/devkit'; @@ -20,6 +19,13 @@ import type { Schema as EsLintExecutorOptions } from '@nx/linter/src/executors/e import { PluginLintChecksGeneratorSchema } from './schema'; import { NX_PREFIX } from 'nx/src/utils/logger'; import { PackageJson, readNxMigrateConfig } from 'nx/src/utils/package-json'; +import { + addOverrideToLintConfig, + isMigrationSupported, + lintConfigHasOverride, + updateOverrideInLintConfig, +} from '@nx/linter/src/generators/utils/eslint-file'; +import { useFlatConfig } from '@nx/linter/src/utils/flat-config'; export default async function pluginLintCheckGenerator( host: Tree, @@ -101,22 +107,19 @@ export function addMigrationJsonChecks( updateProjectConfiguration(host, options.projectName, projectConfiguration); // Update project level eslintrc - updateJson( + updateOverrideInLintConfig( host, - `${projectConfiguration.root}/.eslintrc.json`, - (c) => { - const override = c.overrides.find( - (o) => - Object.keys(o.rules ?? {})?.includes('@nx/nx-plugin-checks') || - Object.keys(o.rules ?? {})?.includes('@nrwl/nx/nx-plugin-checks') - ); - if ( - Array.isArray(override?.files) && - !override.files.includes(relativeMigrationsJsonPath) - ) { - override.files.push(relativeMigrationsJsonPath); - } - return c; + projectConfiguration.root, + (o) => + Object.keys(o.rules ?? {})?.includes('@nx/nx-plugin-checks') || + Object.keys(o.rules ?? {})?.includes('@nrwl/nx/nx-plugin-checks'), + (o) => { + const fileSet = new Set(Array.isArray(o.files) ? o.files : [o.files]); + fileSet.add(relativeMigrationsJsonPath); + return { + ...o, + files: Array.from(fileSet), + }; } ); } @@ -179,42 +182,49 @@ function updateProjectEslintConfig( options: ProjectConfiguration, packageJson: PackageJson ) { - // Update the project level lint configuration to specify - // the plugin schema rule for generated files - const eslintPath = `${options.root}/.eslintrc.json`; - if (host.exists(eslintPath)) { - const eslintConfig = readJson(host, eslintPath); - eslintConfig.overrides ??= []; - let entry: ESLint.ConfigOverride = - eslintConfig.overrides.find( - (x) => - Object.keys(x.rules ?? {}).includes('@nx/nx-plugin-checks') || - Object.keys(x.rules ?? {}).includes('@nrwl/nx/nx-plugin-checks') - ); - const newentry = !entry; - entry ??= { files: [] }; - entry.files = [ - ...new Set([ - ...(entry.files ?? []), - ...[ - './package.json', - packageJson.generators, - packageJson.executors, - packageJson.schematics, - packageJson.builders, - ].filter((f) => !!f), - ]), - ]; - entry.parser = 'jsonc-eslint-parser'; - entry.rules ??= { - '@nx/nx-plugin-checks': 'error', - }; + if (isMigrationSupported(host, options.root)) { + const lookup = (o) => + Object.keys(o.rules ?? {}).includes('@nx/nx-plugin-checks') || + Object.keys(o.rules ?? {}).includes('@nrwl/nx/nx-plugin-checks'); - if (newentry) { - eslintConfig.overrides.push(entry); - } + const files = [ + './package.json', + packageJson.generators, + packageJson.executors, + packageJson.schematics, + packageJson.builders, + ].filter((f) => !!f); - writeJson(host, eslintPath, eslintConfig); + const parser = useFlatConfig(host) + ? { languageOptions: { parser: 'jsonc-eslint-parser' } } + : { parser: 'jsonc-eslint-parser' }; + + if (lintConfigHasOverride(host, options.root, lookup)) { + // update it + updateOverrideInLintConfig(host, options.root, lookup, (o) => ({ + ...o, + files: [ + ...new Set([ + ...(Array.isArray(o.files) ? o.files : [o.files]), + ...files, + ]), + ], + ...parser, + rules: { + ...o.rules, + '@nx/nx-plugin-checks': 'error', + }, + })); + } else { + // add it + addOverrideToLintConfig(host, options.root, { + files, + ...parser, + rules: { + '@nx/nx-plugin-checks': 'error', + }, + }); + } } } @@ -222,22 +232,34 @@ function updateProjectEslintConfig( // This is required, otherwise every json file that is not overriden // will display false errors in the IDE function updateRootEslintConfig(host: Tree) { - if (host.exists('.eslintrc.json')) { - const rootESLint = readJson(host, '.eslintrc.json'); - rootESLint.overrides ??= []; - if (!eslintConfigContainsJsonOverride(rootESLint)) { - rootESLint.overrides.push({ - files: '*.json', - parser: 'jsonc-eslint-parser', - rules: {}, - }); - writeJson(host, '.eslintrc.json', rootESLint); + if (isMigrationSupported(host)) { + if ( + !lintConfigHasOverride( + host, + '', + (o) => + Array.isArray(o.files) + ? o.files.some((f) => f.match(/\.json$/)) + : !!o.files?.match(/\.json$/), + true + ) + ) { + addOverrideToLintConfig( + host, + '', + { + files: '*.json', + parser: 'jsonc-eslint-parser', + rules: {}, + }, + { checkBaseConfig: true } + ); } } else { output.note({ title: 'Unable to update root eslint config.', bodyLines: [ - 'We only automatically update the root eslint config if it is json.', + 'We only automatically update the root eslint config if it is json or flat config.', 'If you are using a different format, you will need to update it manually.', 'You need to set the parser to jsonc-eslint-parser for json files.', ], @@ -263,15 +285,6 @@ function setupVsCodeLintingForJsonFiles(host: Tree) { writeJson(host, '.vscode/settings.json', existing); } -function eslintConfigContainsJsonOverride(eslintConfig: ESLint.Config) { - return eslintConfig.overrides.some((x) => { - if (typeof x.files === 'string' && x.files.includes('.json')) { - return true; - } - return Array.isArray(x.files) && x.files.some((f) => f.includes('.json')); - }); -} - function projectIsEsLintEnabled(project: ProjectConfiguration) { return !!getEsLintOptions(project); } From a510d66af9b557d0d4c8df386f712e8aba76d432 Mon Sep 17 00:00:00 2001 From: Miroslav Jonas Date: Fri, 11 Aug 2023 15:04:23 +0200 Subject: [PATCH 23/30] fix(linter): ensure root is set for flat config --- .../src/generators/utils/eslint-file.ts | 2 +- .../utils/flat-config/ast-utils.spec.ts | 159 +++++++++--------- .../generators/utils/flat-config/ast-utils.ts | 13 +- .../application/lib/add-linting.spec.ts | 5 +- 4 files changed, 94 insertions(+), 85 deletions(-) diff --git a/packages/linter/src/generators/utils/eslint-file.ts b/packages/linter/src/generators/utils/eslint-file.ts index d61422fb27477..ec6e8e0e1d426 100644 --- a/packages/linter/src/generators/utils/eslint-file.ts +++ b/packages/linter/src/generators/utils/eslint-file.ts @@ -120,7 +120,7 @@ export function updateOverrideInLintConfig( if (useFlatConfig(tree)) { const fileName = joinPathFragments(root, 'eslint.config.js'); let content = tree.read(fileName, 'utf8'); - content = replaceOverride(content, lookup, update); + content = replaceOverride(content, root, lookup, update); tree.write(fileName, content); } else { const fileName = joinPathFragments(root, '.eslintrc.json'); diff --git a/packages/linter/src/generators/utils/flat-config/ast-utils.spec.ts b/packages/linter/src/generators/utils/flat-config/ast-utils.spec.ts index 5a54556d56390..902c33a0984eb 100644 --- a/packages/linter/src/generators/utils/flat-config/ast-utils.spec.ts +++ b/packages/linter/src/generators/utils/flat-config/ast-utils.spec.ts @@ -16,12 +16,12 @@ describe('ast-utils', () => { ...baseConfig, { files: [ - "mylib/**/*.ts", - "mylib/**/*.tsx" + "my-lib/**/*.ts", + "my-lib/**/*.tsx" ], rules: {} }, - { ignores: ["mylib/.cache/**/*"] }, + { ignores: ["my-lib/.cache/**/*"] }, ];`; const result = addBlockToFlatConfigExport( content, @@ -38,12 +38,12 @@ describe('ast-utils', () => { ...baseConfig, { files: [ - "mylib/**/*.ts", - "mylib/**/*.tsx" + "my-lib/**/*.ts", + "my-lib/**/*.tsx" ], rules: {} }, - { ignores: ["mylib/.cache/**/*"] }, + { ignores: ["my-lib/.cache/**/*"] }, { files: ["**/*.svg"], rules: { "@nx/do-something-with-svg": "error" } @@ -58,12 +58,12 @@ describe('ast-utils', () => { ...baseConfig, { files: [ - "mylib/**/*.ts", - "mylib/**/*.tsx" + "my-lib/**/*.ts", + "my-lib/**/*.tsx" ], rules: {} }, - { ignores: ["mylib/.cache/**/*"] }, + { ignores: ["my-lib/.cache/**/*"] }, ];`; const result = addBlockToFlatConfigExport( content, @@ -77,12 +77,12 @@ describe('ast-utils', () => { ...baseConfig, { files: [ - "mylib/**/*.ts", - "mylib/**/*.tsx" + "my-lib/**/*.ts", + "my-lib/**/*.tsx" ], rules: {} }, - { ignores: ["mylib/.cache/**/*"] }, + { ignores: ["my-lib/.cache/**/*"] }, ];" `); }); @@ -95,12 +95,12 @@ describe('ast-utils', () => { ...baseConfig, { files: [ - "mylib/**/*.ts", - "mylib/**/*.tsx" + "my-lib/**/*.ts", + "my-lib/**/*.tsx" ], rules: {} }, - { ignores: ["mylib/.cache/**/*"] }, + { ignores: ["my-lib/.cache/**/*"] }, ];`; const result = addImportToFlatConfig( content, @@ -114,12 +114,12 @@ describe('ast-utils', () => { ...baseConfig, { files: [ - "mylib/**/*.ts", - "mylib/**/*.tsx" + "my-lib/**/*.ts", + "my-lib/**/*.tsx" ], rules: {} }, - { ignores: ["mylib/.cache/**/*"] }, + { ignores: ["my-lib/.cache/**/*"] }, ];" `); }); @@ -131,12 +131,12 @@ describe('ast-utils', () => { ...baseConfig, { files: [ - "mylib/**/*.ts", - "mylib/**/*.tsx" + "my-lib/**/*.ts", + "my-lib/**/*.tsx" ], rules: {} }, - { ignores: ["mylib/.cache/**/*"] }, + { ignores: ["my-lib/.cache/**/*"] }, ];`; const result = addImportToFlatConfig( content, @@ -150,12 +150,12 @@ describe('ast-utils', () => { ...baseConfig, { files: [ - "mylib/**/*.ts", - "mylib/**/*.tsx" + "my-lib/**/*.ts", + "my-lib/**/*.tsx" ], rules: {} }, - { ignores: ["mylib/.cache/**/*"] }, + { ignores: ["my-lib/.cache/**/*"] }, ];" `); }); @@ -167,12 +167,12 @@ describe('ast-utils', () => { ...baseConfig, { files: [ - "mylib/**/*.ts", - "mylib/**/*.tsx" + "my-lib/**/*.ts", + "my-lib/**/*.tsx" ], rules: {} }, - { ignores: ["mylib/.cache/**/*"] }, + { ignores: ["my-lib/.cache/**/*"] }, ];`; const result = addImportToFlatConfig( content, @@ -189,12 +189,12 @@ describe('ast-utils', () => { ...baseConfig, { files: [ - "mylib/**/*.ts", - "mylib/**/*.tsx" + "my-lib/**/*.ts", + "my-lib/**/*.tsx" ], rules: {} }, - { ignores: ["mylib/.cache/**/*"] }, + { ignores: ["my-lib/.cache/**/*"] }, ];`; const result = addImportToFlatConfig( content, @@ -212,12 +212,12 @@ describe('ast-utils', () => { ...baseConfig, { files: [ - "mylib/**/*.ts", - "mylib/**/*.tsx" + "my-lib/**/*.ts", + "my-lib/**/*.tsx" ], rules: {} }, - { ignores: ["mylib/.cache/**/*"] }, + { ignores: ["my-lib/.cache/**/*"] }, ];`; const result = addCompatToFlatConfig(content); expect(result).toMatchInlineSnapshot(` @@ -234,12 +234,12 @@ describe('ast-utils', () => { ...baseConfig, { files: [ - "mylib/**/*.ts", - "mylib/**/*.tsx" + "my-lib/**/*.ts", + "my-lib/**/*.tsx" ], rules: {} }, - { ignores: ["mylib/.cache/**/*"] }, + { ignores: ["my-lib/.cache/**/*"] }, ];" `); }); @@ -251,12 +251,12 @@ describe('ast-utils', () => { ...baseConfig, { files: [ - "mylib/**/*.ts", - "mylib/**/*.tsx" + "my-lib/**/*.ts", + "my-lib/**/*.tsx" ], rules: {} }, - { ignores: ["mylib/.cache/**/*"] }, + { ignores: ["my-lib/.cache/**/*"] }, ];`; const result = addCompatToFlatConfig(content); expect(result).toMatchInlineSnapshot(` @@ -273,12 +273,12 @@ describe('ast-utils', () => { ...baseConfig, { files: [ - "mylib/**/*.ts", - "mylib/**/*.tsx" + "my-lib/**/*.ts", + "my-lib/**/*.tsx" ], rules: {} }, - { ignores: ["mylib/.cache/**/*"] }, + { ignores: ["my-lib/.cache/**/*"] }, ];" `); }); @@ -297,12 +297,12 @@ describe('ast-utils', () => { ...baseConfig, { files: [ - "mylib/**/*.ts", - "mylib/**/*.tsx" + "my-lib/**/*.ts", + "my-lib/**/*.tsx" ], rules: {} }, - { ignores: ["mylib/.cache/**/*"] }, + { ignores: ["my-lib/.cache/**/*"] }, ];`; const result = addCompatToFlatConfig(content); expect(result).toEqual(content); @@ -324,8 +324,8 @@ describe('ast-utils', () => { ...baseConfig, { files: [ - "mylib/**/*.ts", - "mylib/**/*.tsx" + "my-lib/**/*.ts", + "my-lib/**/*.tsx" ], rules: {} }, @@ -347,7 +347,7 @@ describe('ast-utils', () => { ], rules: {} })), - { ignores: ["mylib/.cache/**/*"] }, + { ignores: ["my-lib/.cache/**/*"] }, ];`; const result = removeOverridesFromLintConfig(content); expect(result).toMatchInlineSnapshot(` @@ -362,7 +362,7 @@ describe('ast-utils', () => { module.exports = [ ...baseConfig, - { ignores: ["mylib/.cache/**/*"] }, + { ignores: ["my-lib/.cache/**/*"] }, ];" `); }); @@ -373,8 +373,8 @@ describe('ast-utils', () => { module.exports = [ { files: [ - "mylib/**/*.ts", - "mylib/**/*.tsx" + "my-lib/**/*.ts", + "my-lib/**/*.tsx" ], rules: {} }, @@ -414,8 +414,8 @@ describe('ast-utils', () => { module.exports = [ { files: [ - "mylib/**/*.ts", - "mylib/**/*.tsx" + "my-lib/**/*.ts", + "my-lib/**/*.tsx" ], rules: { 'my-ts-rule': 'error' @@ -423,15 +423,15 @@ describe('ast-utils', () => { }, { files: [ - "mylib/**/*.ts", - "mylib/**/*.js" + "my-lib/**/*.ts", + "my-lib/**/*.js" ], rules: {} }, { files: [ - "mylib/**/*.js", - "mylib/**/*.jsx" + "my-lib/**/*.js", + "my-lib/**/*.jsx" ], rules: { 'my-js-rule': 'error' @@ -441,7 +441,8 @@ describe('ast-utils', () => { const result = replaceOverride( content, - (o) => o.files.includes('mylib/**/*.ts'), + 'my-lib', + (o) => o.files.includes('my-lib/**/*.ts'), (o) => ({ ...o, rules: { @@ -455,8 +456,8 @@ describe('ast-utils', () => { module.exports = [ { "files": [ - "mylib/**/*.ts", - "mylib/**/*.tsx" + "my-lib/**/*.ts", + "my-lib/**/*.tsx" ], "rules": { "my-rule": "error" @@ -464,8 +465,8 @@ describe('ast-utils', () => { }, { "files": [ - "mylib/**/*.ts", - "mylib/**/*.js" + "my-lib/**/*.ts", + "my-lib/**/*.js" ], "rules": { "my-rule": "error" @@ -473,8 +474,8 @@ describe('ast-utils', () => { }, { files: [ - "mylib/**/*.js", - "mylib/**/*.jsx" + "my-lib/**/*.js", + "my-lib/**/*.jsx" ], rules: { 'my-js-rule': 'error' @@ -490,8 +491,8 @@ describe('ast-utils', () => { module.exports = [ { files: [ - "mylib/**/*.ts", - "mylib/**/*.tsx" + "my-lib/**/*.ts", + "my-lib/**/*.tsx" ], rules: { 'my-ts-rule': 'error' @@ -499,8 +500,8 @@ describe('ast-utils', () => { }, { files: [ - "mylib/**/*.js", - "mylib/**/*.jsx" + "my-lib/**/*.js", + "my-lib/**/*.jsx" ], rules: { 'my-js-rule': 'error' @@ -510,7 +511,8 @@ describe('ast-utils', () => { const result = replaceOverride( content, - (o) => o.files.includes('mylib/**/*.ts'), + 'my-lib', + (o) => o.files.includes('my-lib/**/*.ts'), (o) => ({ ...o, rules: { @@ -525,8 +527,8 @@ describe('ast-utils', () => { module.exports = [ { "files": [ - "mylib/**/*.ts", - "mylib/**/*.tsx" + "my-lib/**/*.ts", + "my-lib/**/*.tsx" ], "rules": { "my-ts-rule": "error", @@ -535,8 +537,8 @@ describe('ast-utils', () => { }, { files: [ - "mylib/**/*.js", - "mylib/**/*.jsx" + "my-lib/**/*.js", + "my-lib/**/*.jsx" ], rules: { 'my-js-rule': 'error' @@ -553,8 +555,8 @@ describe('ast-utils', () => { ...compat.config({ extends: ["plugin:@nx/typescript"] }).map(config => ({ ...config, files: [ - "mylib/**/*.ts", - "mylib/**/*.tsx" + "my-lib/**/*.ts", + "my-lib/**/*.tsx" ], rules: { 'my-ts-rule': 'error' @@ -564,7 +566,8 @@ describe('ast-utils', () => { const result = replaceOverride( content, - (o) => o.files.includes('mylib/**/*.ts'), + 'my-lib', + (o) => o.files.includes('my-lib/**/*.ts'), (o) => ({ ...o, rules: { @@ -580,8 +583,8 @@ describe('ast-utils', () => { ...compat.config({ extends: ["plugin:@nx/typescript"] }).map(config => ({ ...config, "files": [ - "mylib/**/*.ts", - "mylib/**/*.tsx" + "my-lib/**/*.ts", + "my-lib/**/*.tsx" ], "rules": { "my-ts-rule": "error", diff --git a/packages/linter/src/generators/utils/flat-config/ast-utils.ts b/packages/linter/src/generators/utils/flat-config/ast-utils.ts index 0416f3a709554..3a768013f5287 100644 --- a/packages/linter/src/generators/utils/flat-config/ast-utils.ts +++ b/packages/linter/src/generators/utils/flat-config/ast-utils.ts @@ -111,6 +111,7 @@ export function hasOverride( */ export function replaceOverride( content: string, + root: string, lookup: (override: Linter.ConfigOverride) => boolean, update: ( override: Linter.ConfigOverride @@ -158,10 +159,12 @@ export function replaceOverride( start, length: end - start, }); + const updatedData = update(data); + mapFilePaths(updatedData, root); changes.push({ type: ChangeType.Insert, index: start, - text: JSON.stringify(update(data), null, 2).slice(2, -2), // remove curly braces and start/end line breaks since we are injecting just properties + text: JSON.stringify(updatedData, null, 2).slice(2, -2), // remove curly braces and start/end line breaks since we are injecting just properties }); } } @@ -589,7 +592,7 @@ export function generateFlatOverride( ); } -function mapFilePaths( +export function mapFilePaths( override: Linter.ConfigOverride, root: string ) { @@ -614,15 +617,17 @@ export function mapFilePath(filePath: string, root: string) { const fileWithoutBang = filePath.slice(1); if (fileWithoutBang.startsWith('*.')) { return `!${joinPathFragments(root, '**', fileWithoutBang)}`; - } else { + } else if (!fileWithoutBang.startsWith(root)) { return `!${joinPathFragments(root, fileWithoutBang)}`; } + return filePath; } if (filePath.startsWith('*.')) { return joinPathFragments(root, '**', filePath); - } else { + } else if (!filePath.startsWith(root)) { return joinPathFragments(root, filePath); } + return filePath; } function addTSObjectProperty( diff --git a/packages/next/src/generators/application/lib/add-linting.spec.ts b/packages/next/src/generators/application/lib/add-linting.spec.ts index 30a88f3a5f1ed..8b1db70527be1 100644 --- a/packages/next/src/generators/application/lib/add-linting.spec.ts +++ b/packages/next/src/generators/application/lib/add-linting.spec.ts @@ -134,7 +134,8 @@ describe('updateEslint', () => { files: ["apps/my-app/**/*.*"], rules: { "@next/next/no-html-link-for-pages": "off" } }, - ...baseConfig,{ + ...baseConfig, + { "files": [ "apps/my-app/**/*.ts", "apps/my-app/**/*.tsx", @@ -147,7 +148,7 @@ describe('updateEslint', () => { "apps/my-app/pages" ] } - }, + }, { files: [ "apps/my-app/**/*.ts", From b29070f9be79b46cfdadee3244827912f9a9f6dc Mon Sep 17 00:00:00 2001 From: Miroslav Jonas Date: Fri, 11 Aug 2023 15:46:45 +0200 Subject: [PATCH 24/30] fix(linter): ensure all config manipulations are supported --- .../src/generators/add-linting/add-linting.ts | 95 ++++++++------- .../configuration/configuration.spec.ts | 1 + packages/cypress/src/utils/add-linter.ts | 113 +++++++++--------- .../generators/application/lib/add-linting.ts | 9 +- packages/expo/src/utils/add-linting.ts | 17 +-- packages/js/src/generators/library/library.ts | 17 +-- .../src/generators/utils/eslint-file.ts | 2 +- .../application/application.spec.ts | 16 +-- .../generators/application/lib/add-linting.ts | 4 +- packages/playwright/src/utils/add-linter.ts | 37 +++--- .../src/generators/lint-checks/generator.ts | 8 +- .../react-native/src/utils/add-linting.ts | 15 ++- .../src/generators/application/application.ts | 9 +- .../src/generators/library/lib/add-linting.ts | 9 +- 14 files changed, 192 insertions(+), 160 deletions(-) diff --git a/packages/angular/src/generators/add-linting/add-linting.ts b/packages/angular/src/generators/add-linting/add-linting.ts index a60d7f0bbfa71..8ac03674e1525 100755 --- a/packages/angular/src/generators/add-linting/add-linting.ts +++ b/packages/angular/src/generators/add-linting/add-linting.ts @@ -11,6 +11,7 @@ import { addAngularEsLintDependencies } from './lib/add-angular-eslint-dependenc import type { AddLintingGeneratorSchema } from './schema'; import { findEslintFile, + isEslintConfigSupported, replaceOverridesInLintConfig, } from '@nx/linter/src/generators/utils/eslint-file'; import { camelize, dasherize } from '@nx/devkit/src/utils/string-utils'; @@ -38,55 +39,57 @@ export async function addLintingGenerator( }); tasks.push(lintTask); - const eslintFile = findEslintFile(tree, options.projectRoot); - // keep parser options if they exist - const hasParserOptions = tree - .read(joinPathFragments(options.projectRoot, eslintFile), 'utf8') - .includes(`${options.projectRoot}/tsconfig.*?.json`); + if (isEslintConfigSupported(tree)) { + const eslintFile = findEslintFile(tree, options.projectRoot); + // keep parser options if they exist + const hasParserOptions = tree + .read(joinPathFragments(options.projectRoot, eslintFile), 'utf8') + .includes(`${options.projectRoot}/tsconfig.*?.json`); - replaceOverridesInLintConfig(tree, options.projectRoot, [ - { - files: ['*.ts'], - ...(hasParserOptions - ? { - parserOptions: { - project: [`${options.projectRoot}/tsconfig.*?.json`], - }, - } - : {}), - extends: [ - 'plugin:@nx/angular', - 'plugin:@angular-eslint/template/process-inline-templates', - ], - rules: { - '@angular-eslint/directive-selector': [ - 'error', - { - type: 'attribute', - prefix: camelize(options.prefix), - style: 'camelCase', - }, - ], - '@angular-eslint/component-selector': [ - 'error', - { - type: 'element', - prefix: dasherize(options.prefix), - style: 'kebab-case', - }, + replaceOverridesInLintConfig(tree, options.projectRoot, [ + { + files: ['*.ts'], + ...(hasParserOptions + ? { + parserOptions: { + project: [`${options.projectRoot}/tsconfig.*?.json`], + }, + } + : {}), + extends: [ + 'plugin:@nx/angular', + 'plugin:@angular-eslint/template/process-inline-templates', ], + rules: { + '@angular-eslint/directive-selector': [ + 'error', + { + type: 'attribute', + prefix: camelize(options.prefix), + style: 'camelCase', + }, + ], + '@angular-eslint/component-selector': [ + 'error', + { + type: 'element', + prefix: dasherize(options.prefix), + style: 'kebab-case', + }, + ], + }, + }, + { + files: ['*.html'], + extends: ['plugin:@nx/angular-template'], + /** + * Having an empty rules object present makes it more obvious to the user where they would + * extend things from if they needed to + */ + rules: {}, }, - }, - { - files: ['*.html'], - extends: ['plugin:@nx/angular-template'], - /** - * Having an empty rules object present makes it more obvious to the user where they would - * extend things from if they needed to - */ - rules: {}, - }, - ]); + ]); + } if (!options.skipPackageJson) { const installTask = addAngularEsLintDependencies(tree); diff --git a/packages/cypress/src/generators/configuration/configuration.spec.ts b/packages/cypress/src/generators/configuration/configuration.spec.ts index db32cf1cba186..68c0ed93813eb 100644 --- a/packages/cypress/src/generators/configuration/configuration.spec.ts +++ b/packages/cypress/src/generators/configuration/configuration.spec.ts @@ -21,6 +21,7 @@ describe('Cypress e2e configuration', () => { beforeEach(() => { tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); + tree.write('.eslintrc.json', '{}'); // we are explicitly checking for existance of config type }); afterAll(() => { diff --git a/packages/cypress/src/utils/add-linter.ts b/packages/cypress/src/utils/add-linter.ts index 2fdbd92740090..7f333f7b5c96d 100644 --- a/packages/cypress/src/utils/add-linter.ts +++ b/packages/cypress/src/utils/add-linter.ts @@ -14,6 +14,7 @@ import { addOverrideToLintConfig, addPluginsToLintConfig, findEslintFile, + isEslintConfigSupported, replaceOverridesInLintConfig, } from '@nx/linter/src/generators/utils/eslint-file'; import { javaScriptOverride } from '@nx/linter/src/generators/init/global-eslint-config'; @@ -80,63 +81,65 @@ export async function addLinterToCyProject( : () => {} ); - const overrides = []; - if (options.rootProject) { - addPluginsToLintConfig(tree, projectConfig.root, '@nx'); - overrides.push(javaScriptOverride); - } - addExtendsToLintConfig( - tree, - projectConfig.root, - 'plugin:cypress/recommended' - ); - const cyVersion = installedCypressVersion(); - /** - * We need this override because we enabled allowJS in the tsconfig to allow for JS based Cypress tests. - * That however leads to issues with the CommonJS Cypress plugin file. - */ - const cy6Override = { - files: [`${options.cypressDir}/plugins/index.js`], - rules: { - '@typescript-eslint/no-var-requires': 'off', - 'no-undef': 'off', - }, - }; - const addCy6Override = cyVersion && cyVersion < 7; - - if (options.overwriteExisting) { - overrides.push({ - files: ['*.ts', '*.tsx', '*.js', '*.jsx'], - parserOptions: !options.setParserOptionsProject - ? undefined - : { - project: `${projectConfig.root}/tsconfig.*?.json`, - }, - rules: {}, - }); - if (addCy6Override) { - overrides.push(cy6Override); + if (isEslintConfigSupported(tree)) { + const overrides = []; + if (options.rootProject) { + addPluginsToLintConfig(tree, projectConfig.root, '@nx'); + overrides.push(javaScriptOverride); } - replaceOverridesInLintConfig(tree, projectConfig.root, overrides); - } else { - overrides.unshift({ - files: [ - '*.cy.{ts,js,tsx,jsx}', - `${options.cypressDir}/**/*.{ts,js,tsx,jsx}`, - ], - parserOptions: !options.setParserOptionsProject - ? undefined - : { - project: `${projectConfig.root}/tsconfig.*?.json`, - }, - rules: {}, - }); - if (addCy6Override) { - overrides.push(cy6Override); - } - overrides.forEach((override) => - addOverrideToLintConfig(tree, projectConfig.root, override) + addExtendsToLintConfig( + tree, + projectConfig.root, + 'plugin:cypress/recommended' ); + const cyVersion = installedCypressVersion(); + /** + * We need this override because we enabled allowJS in the tsconfig to allow for JS based Cypress tests. + * That however leads to issues with the CommonJS Cypress plugin file. + */ + const cy6Override = { + files: [`${options.cypressDir}/plugins/index.js`], + rules: { + '@typescript-eslint/no-var-requires': 'off', + 'no-undef': 'off', + }, + }; + const addCy6Override = cyVersion && cyVersion < 7; + + if (options.overwriteExisting) { + overrides.push({ + files: ['*.ts', '*.tsx', '*.js', '*.jsx'], + parserOptions: !options.setParserOptionsProject + ? undefined + : { + project: `${projectConfig.root}/tsconfig.*?.json`, + }, + rules: {}, + }); + if (addCy6Override) { + overrides.push(cy6Override); + } + replaceOverridesInLintConfig(tree, projectConfig.root, overrides); + } else { + overrides.unshift({ + files: [ + '*.cy.{ts,js,tsx,jsx}', + `${options.cypressDir}/**/*.{ts,js,tsx,jsx}`, + ], + parserOptions: !options.setParserOptionsProject + ? undefined + : { + project: `${projectConfig.root}/tsconfig.*?.json`, + }, + rules: {}, + }); + if (addCy6Override) { + overrides.push(cy6Override); + } + overrides.forEach((override) => + addOverrideToLintConfig(tree, projectConfig.root, override) + ); + } } return runTasksInSerial(...tasks); diff --git a/packages/detox/src/generators/application/lib/add-linting.ts b/packages/detox/src/generators/application/lib/add-linting.ts index fb845d57d50e9..180ecc0d7f124 100644 --- a/packages/detox/src/generators/application/lib/add-linting.ts +++ b/packages/detox/src/generators/application/lib/add-linting.ts @@ -7,7 +7,10 @@ import { } from '@nx/devkit'; import { extraEslintDependencies } from '@nx/react'; import { NormalizedSchema } from './normalize-options'; -import { addExtendsToLintConfig } from '@nx/linter/src/generators/utils/eslint-file'; +import { + addExtendsToLintConfig, + isEslintConfigSupported, +} from '@nx/linter/src/generators/utils/eslint-file'; export async function addLinting(host: Tree, options: NormalizedSchema) { if (options.linter === Linter.None) { @@ -24,7 +27,9 @@ export async function addLinting(host: Tree, options: NormalizedSchema) { skipFormat: true, }); - addExtendsToLintConfig(host, options.e2eProjectRoot, 'plugin:@nx/react'); + if (isEslintConfigSupported(host)) { + addExtendsToLintConfig(host, options.e2eProjectRoot, 'plugin:@nx/react'); + } const installTask = addDependenciesToPackageJson( host, diff --git a/packages/expo/src/utils/add-linting.ts b/packages/expo/src/utils/add-linting.ts index b76335c8dc598..884f822776eb9 100644 --- a/packages/expo/src/utils/add-linting.ts +++ b/packages/expo/src/utils/add-linting.ts @@ -9,6 +9,7 @@ import { extraEslintDependencies } from '@nx/react/src/utils/lint'; import { addExtendsToLintConfig, addIgnoresToLintConfig, + isEslintConfigSupported, } from '@nx/linter/src/generators/utils/eslint-file'; interface NormalizedSchema { @@ -37,13 +38,15 @@ export async function addLinting(host: Tree, options: NormalizedSchema) { tasks.push(lintTask); - addExtendsToLintConfig(host, options.projectRoot, 'plugin:@nx/react'); - addIgnoresToLintConfig(host, options.projectRoot, [ - '.expo', - 'web-build', - 'cache', - 'dist', - ]); + if (isEslintConfigSupported(host)) { + addExtendsToLintConfig(host, options.projectRoot, 'plugin:@nx/react'); + addIgnoresToLintConfig(host, options.projectRoot, [ + '.expo', + 'web-build', + 'cache', + 'dist', + ]); + } if (!options.skipPackageJson) { const installTask = await addDependenciesToPackageJson( diff --git a/packages/js/src/generators/library/library.ts b/packages/js/src/generators/library/library.ts index 55aa2e7dafa5b..810e7dc4a7344 100644 --- a/packages/js/src/generators/library/library.ts +++ b/packages/js/src/generators/library/library.ts @@ -261,16 +261,19 @@ export async function addLint( if (options.rootProject) { const { addOverrideToLintConfig, + isEslintConfigSupported, // nx-ignore-next-line } = require('@nx/linter/src/generators/utils/eslint-file'); - addOverrideToLintConfig(tree, '', { - files: ['*.json'], - parser: 'jsonc-eslint-parser', - rules: { - '@nx/dependency-checks': 'error', - }, - }); + if (isEslintConfigSupported(tree)) { + addOverrideToLintConfig(tree, '', { + files: ['*.json'], + parser: 'jsonc-eslint-parser', + rules: { + '@nx/dependency-checks': 'error', + }, + }); + } } return task; } diff --git a/packages/linter/src/generators/utils/eslint-file.ts b/packages/linter/src/generators/utils/eslint-file.ts index ec6e8e0e1d426..e67a9a8be36bc 100644 --- a/packages/linter/src/generators/utils/eslint-file.ts +++ b/packages/linter/src/generators/utils/eslint-file.ts @@ -51,7 +51,7 @@ export function findEslintFile(tree: Tree, projectRoot = ''): string | null { return null; } -export function isMigrationSupported(tree: Tree, projectRoot = ''): boolean { +export function isEslintConfigSupported(tree: Tree, projectRoot = ''): boolean { const eslintFile = findEslintFile(tree, projectRoot); if (!eslintFile) { return; diff --git a/packages/next/src/generators/application/application.spec.ts b/packages/next/src/generators/application/application.spec.ts index 6d644f689607a..07d352729ba3c 100644 --- a/packages/next/src/generators/application/application.spec.ts +++ b/packages/next/src/generators/application/application.spec.ts @@ -584,6 +584,14 @@ describe('app', () => { ".next/**/*", ], "overrides": [ + { + "files": [ + "*.*", + ], + "rules": { + "@next/next/no-html-link-for-pages": "off", + }, + }, { "files": [ "*.ts", @@ -612,14 +620,6 @@ describe('app', () => { ], "rules": {}, }, - { - "files": [ - "*.*", - ], - "rules": { - "@next/next/no-html-link-for-pages": "off", - }, - }, { "env": { "jest": true, diff --git a/packages/next/src/generators/application/lib/add-linting.ts b/packages/next/src/generators/application/lib/add-linting.ts index 554aa932cc191..56f5d37a12d4a 100644 --- a/packages/next/src/generators/application/lib/add-linting.ts +++ b/packages/next/src/generators/application/lib/add-linting.ts @@ -12,7 +12,7 @@ import { addExtendsToLintConfig, addIgnoresToLintConfig, addOverrideToLintConfig, - findEslintFile, + isEslintConfigSupported, updateOverrideInLintConfig, } from '@nx/linter/src/generators/utils/eslint-file'; @@ -31,7 +31,7 @@ export async function addLinting( skipFormat: true, rootProject: options.rootProject, }); - if (options.linter === Linter.EsLint) { + if (options.linter === Linter.EsLint && isEslintConfigSupported(host)) { addExtendsToLintConfig(host, options.appProjectRoot, [ 'plugin:@nx/react-typescript', 'next', diff --git a/packages/playwright/src/utils/add-linter.ts b/packages/playwright/src/utils/add-linter.ts index 10394fb8c9def..b14d133411991 100644 --- a/packages/playwright/src/utils/add-linter.ts +++ b/packages/playwright/src/utils/add-linter.ts @@ -14,6 +14,7 @@ import { addOverrideToLintConfig, addPluginsToLintConfig, findEslintFile, + isEslintConfigSupported, } from '@nx/linter/src/generators/utils/eslint-file'; export interface PlaywrightLinterOptions { @@ -72,24 +73,26 @@ export async function addLinterToPlaywrightProject( : () => {} ); - addExtendsToLintConfig( - tree, - projectConfig.root, - 'plugin:playwright/recommended' - ); - if (options.rootProject) { - addPluginsToLintConfig(tree, projectConfig.root, '@nx'); - addOverrideToLintConfig(tree, projectConfig.root, javaScriptOverride); + if (isEslintConfigSupported(tree)) { + addExtendsToLintConfig( + tree, + projectConfig.root, + 'plugin:playwright/recommended' + ); + if (options.rootProject) { + addPluginsToLintConfig(tree, projectConfig.root, '@nx'); + addOverrideToLintConfig(tree, projectConfig.root, javaScriptOverride); + } + addOverrideToLintConfig(tree, projectConfig.root, { + files: [`${options.directory}/**/*.{ts,js,tsx,jsx}`], + parserOptions: !options.setParserOptionsProject + ? undefined + : { + project: `${projectConfig.root}/tsconfig.*?.json`, + }, + rules: {}, + }); } - addOverrideToLintConfig(tree, projectConfig.root, { - files: [`${options.directory}/**/*.{ts,js,tsx,jsx}`], - parserOptions: !options.setParserOptionsProject - ? undefined - : { - project: `${projectConfig.root}/tsconfig.*?.json`, - }, - rules: {}, - }); return runTasksInSerial(...tasks); } diff --git a/packages/plugin/src/generators/lint-checks/generator.ts b/packages/plugin/src/generators/lint-checks/generator.ts index a68ad76e14780..16345f6715375 100644 --- a/packages/plugin/src/generators/lint-checks/generator.ts +++ b/packages/plugin/src/generators/lint-checks/generator.ts @@ -12,8 +12,6 @@ import { writeJson, } from '@nx/devkit'; -import type { Linter as ESLint } from 'eslint'; - import type { Schema as EsLintExecutorOptions } from '@nx/linter/src/executors/eslint/schema'; import { PluginLintChecksGeneratorSchema } from './schema'; @@ -21,7 +19,7 @@ import { NX_PREFIX } from 'nx/src/utils/logger'; import { PackageJson, readNxMigrateConfig } from 'nx/src/utils/package-json'; import { addOverrideToLintConfig, - isMigrationSupported, + isEslintConfigSupported, lintConfigHasOverride, updateOverrideInLintConfig, } from '@nx/linter/src/generators/utils/eslint-file'; @@ -182,7 +180,7 @@ function updateProjectEslintConfig( options: ProjectConfiguration, packageJson: PackageJson ) { - if (isMigrationSupported(host, options.root)) { + if (isEslintConfigSupported(host, options.root)) { const lookup = (o) => Object.keys(o.rules ?? {}).includes('@nx/nx-plugin-checks') || Object.keys(o.rules ?? {}).includes('@nrwl/nx/nx-plugin-checks'); @@ -232,7 +230,7 @@ function updateProjectEslintConfig( // This is required, otherwise every json file that is not overriden // will display false errors in the IDE function updateRootEslintConfig(host: Tree) { - if (isMigrationSupported(host)) { + if (isEslintConfigSupported(host)) { if ( !lintConfigHasOverride( host, diff --git a/packages/react-native/src/utils/add-linting.ts b/packages/react-native/src/utils/add-linting.ts index 255cfd5420868..cc3617e4d5e27 100644 --- a/packages/react-native/src/utils/add-linting.ts +++ b/packages/react-native/src/utils/add-linting.ts @@ -9,6 +9,7 @@ import { extraEslintDependencies } from '@nx/react/src/utils/lint'; import { addExtendsToLintConfig, addIgnoresToLintConfig, + isEslintConfigSupported, } from '@nx/linter/src/generators/utils/eslint-file'; interface NormalizedSchema { @@ -37,12 +38,14 @@ export async function addLinting(host: Tree, options: NormalizedSchema) { tasks.push(lintTask); - addExtendsToLintConfig(host, options.projectRoot, 'plugin:@nx/react'); - addIgnoresToLintConfig(host, options.projectRoot, [ - 'public', - '.cache', - 'node_modules', - ]); + if (isEslintConfigSupported(host)) { + addExtendsToLintConfig(host, options.projectRoot, 'plugin:@nx/react'); + addIgnoresToLintConfig(host, options.projectRoot, [ + 'public', + '.cache', + 'node_modules', + ]); + } if (!options.skipPackageJson) { const installTask = await addDependenciesToPackageJson( diff --git a/packages/react/src/generators/application/application.ts b/packages/react/src/generators/application/application.ts index d3ccec057aa5c..9e6fe7d9e525b 100644 --- a/packages/react/src/generators/application/application.ts +++ b/packages/react/src/generators/application/application.ts @@ -35,7 +35,10 @@ import { addSwcDependencies } from '@nx/js/src/utils/swc/add-swc-dependencies'; import * as chalk from 'chalk'; import { showPossibleWarnings } from './lib/show-possible-warnings'; import { addE2e } from './lib/add-e2e'; -import { addExtendsToLintConfig } from '@nx/linter/src/generators/utils/eslint-file'; +import { + addExtendsToLintConfig, + isEslintConfigSupported, +} from '@nx/linter/src/generators/utils/eslint-file'; async function addLinting(host: Tree, options: NormalizedSchema) { const tasks: GeneratorCallback[] = []; @@ -60,7 +63,9 @@ async function addLinting(host: Tree, options: NormalizedSchema) { }); tasks.push(lintTask); - addExtendsToLintConfig(host, options.appProjectRoot, 'plugin:@nx/react'); + if (isEslintConfigSupported(host)) { + addExtendsToLintConfig(host, options.appProjectRoot, 'plugin:@nx/react'); + } if (!options.skipPackageJson) { const installTask = addDependenciesToPackageJson( diff --git a/packages/react/src/generators/library/lib/add-linting.ts b/packages/react/src/generators/library/lib/add-linting.ts index 7315870d7ac4d..0834a21f8665b 100644 --- a/packages/react/src/generators/library/lib/add-linting.ts +++ b/packages/react/src/generators/library/lib/add-linting.ts @@ -5,7 +5,10 @@ import { addDependenciesToPackageJson, runTasksInSerial } from '@nx/devkit'; import { NormalizedSchema } from '../schema'; import { extraEslintDependencies } from '../../../utils/lint'; -import { addExtendsToLintConfig } from '@nx/linter/src/generators/utils/eslint-file'; +import { + addExtendsToLintConfig, + isEslintConfigSupported, +} from '@nx/linter/src/generators/utils/eslint-file'; export async function addLinting(host: Tree, options: NormalizedSchema) { if (options.linter === Linter.EsLint) { @@ -22,7 +25,9 @@ export async function addLinting(host: Tree, options: NormalizedSchema) { setParserOptionsProject: options.setParserOptionsProject, }); - addExtendsToLintConfig(host, options.projectRoot, 'plugin:@nx/react'); + if (isEslintConfigSupported(host)) { + addExtendsToLintConfig(host, options.projectRoot, 'plugin:@nx/react'); + } let installTask = () => {}; if (!options.skipPackageJson) { From 948e16fe46fd4c3687e8186141914454a400eddf Mon Sep 17 00:00:00 2001 From: Miroslav Jonas Date: Fri, 11 Aug 2023 16:41:24 +0200 Subject: [PATCH 25/30] feat(linter): support flat config migration --- .../generators/init/global-eslint-config.ts | 51 ++++++- .../src/generators/init/init-migration.ts | 125 +++++++++++------- .../generators/lint-project/lint-project.ts | 11 +- 3 files changed, 132 insertions(+), 55 deletions(-) diff --git a/packages/linter/src/generators/init/global-eslint-config.ts b/packages/linter/src/generators/init/global-eslint-config.ts index 3d6de20f38d39..464a080a73f10 100644 --- a/packages/linter/src/generators/init/global-eslint-config.ts +++ b/packages/linter/src/generators/init/global-eslint-config.ts @@ -1,4 +1,13 @@ -import { Linter as LinterType } from 'eslint'; +import { Linter } from 'eslint'; +import { + addBlockToFlatConfigExport, + addImportToFlatConfig, + addPluginsToExportsBlock, + createNodeList, + generateFlatOverride, + stringifyNodeList, +} from '../utils/flat-config/ast-utils'; +import { addPluginsToLintConfig } from '../utils/eslint-file'; /** * This configuration is intended to apply to all TypeScript source files. @@ -43,7 +52,7 @@ const moduleBoundariesOverride = { depConstraints: [{ sourceTag: '*', onlyDependOnLibsWithTags: ['*'] }], }, ], - } as LinterType.RulesRecord, + } as Linter.RulesRecord, }; /** @@ -61,8 +70,8 @@ const jestOverride = { export const getGlobalEsLintConfiguration = ( unitTestRunner?: string, rootProject?: boolean -) => { - const config: LinterType.Config = { +): Linter.Config => { + const config: Linter.Config = { root: true, ignorePatterns: rootProject ? ['!**/*'] : ['**/*'], plugins: ['@nx'], @@ -82,3 +91,37 @@ export const getGlobalEsLintConfiguration = ( }; return config; }; + +export const getGlobalFlatEslintConfiguration = ( + unitTestRunner?: string, + rootProject?: boolean +): string => { + const nodeList = createNodeList(new Map(), [], true); + let content = stringifyNodeList(nodeList, '', 'eslint.config.js'); + content = addImportToFlatConfig(content, 'nxPlugin', '@nx/eslint-plugin'); + content = addPluginsToExportsBlock(content, [ + { name: '@nx', varName: 'nxPlugin', imp: '@nx/eslint-plugin' }, + ]); + if (!rootProject) { + content = addBlockToFlatConfigExport( + content, + generateFlatOverride(moduleBoundariesOverride, '') + ); + } + content = addBlockToFlatConfigExport( + content, + generateFlatOverride(typeScriptOverride, '') + ); + content = addBlockToFlatConfigExport( + content, + generateFlatOverride(javaScriptOverride, '') + ); + if (unitTestRunner === 'jest') { + content = addBlockToFlatConfigExport( + content, + generateFlatOverride(jestOverride, '') + ); + } + + return content; +}; diff --git a/packages/linter/src/generators/init/init-migration.ts b/packages/linter/src/generators/init/init-migration.ts index 1951efab243f3..6b651d873c654 100644 --- a/packages/linter/src/generators/init/init-migration.ts +++ b/packages/linter/src/generators/init/init-migration.ts @@ -1,4 +1,5 @@ import { + addDependenciesToPackageJson, joinPathFragments, offsetFromRoot, ProjectConfiguration, @@ -8,19 +9,39 @@ import { writeJson, } from '@nx/devkit'; import { dirname } from 'path'; -import { findEslintFile } from '../utils/eslint-file'; -import { getGlobalEsLintConfiguration } from './global-eslint-config'; +import { findEslintFile, isEslintConfigSupported } from '../utils/eslint-file'; +import { + getGlobalEsLintConfiguration, + getGlobalFlatEslintConfiguration, +} from './global-eslint-config'; +import { useFlatConfig } from '../../utils/flat-config'; +import { eslintrcVersion } from '../../utils/versions'; export function migrateConfigToMonorepoStyle( projects: ProjectConfiguration[], tree: Tree, unitTestRunner: string ): void { - writeJson( - tree, - '.eslintrc.base.json', - getGlobalEsLintConfiguration(unitTestRunner) - ); + if (useFlatConfig(tree)) { + // we need this for the compat + addDependenciesToPackageJson( + tree, + {}, + { + '@eslint/js': eslintrcVersion, + } + ); + tree.write( + 'eslint.base.config.js', + getGlobalFlatEslintConfiguration(unitTestRunner) + ); + } else { + writeJson( + tree, + '.eslintrc.base.json', + getGlobalEsLintConfiguration(unitTestRunner) + ); + } // update extens in all projects' eslint configs projects.forEach((project) => { @@ -47,49 +68,55 @@ export function findLintTarget( } function migrateEslintFile(projectEslintPath: string, tree: Tree) { - if ( - projectEslintPath.endsWith('.json') || - projectEslintPath.endsWith('.eslintrc') - ) { - updateJson(tree, projectEslintPath, (json) => { - // we have a new root now - delete json.root; - // remove nrwl/nx plugins - if (json.plugins) { - json.plugins = json.plugins.filter( - (p) => p !== '@nx' && p !== '@nrwl/nx' - ); - if (json.plugins.length === 0) { - delete json.plugins; + if (isEslintConfigSupported(tree)) { + if (useFlatConfig(tree)) { + let config = tree.read(projectEslintPath, 'utf-8'); + // TODO 1. remove `@nx` plugin + // TODO 2. extend eslint.base.config.js + // TODO 3. remove @nx/js|ts from extends + console.warn('Flat eslint config is not supported yet for migration'); + tree.write(projectEslintPath, config); + } else { + updateJson(tree, projectEslintPath, (json) => { + // we have a new root now + delete json.root; + // remove nrwl/nx plugins + if (json.plugins) { + json.plugins = json.plugins.filter( + (p) => p !== '@nx' && p !== '@nrwl/nx' + ); + if (json.plugins.length === 0) { + delete json.plugins; + } } - } - // add extends - json.extends = json.extends || []; - const pathToRootConfig = `${offsetFromRoot( - dirname(projectEslintPath) - )}.eslintrc.base.json`; - if (json.extends.indexOf(pathToRootConfig) === -1) { - json.extends.push(pathToRootConfig); - } - // cleanup overrides - if (json.overrides) { - json.overrides.forEach((override) => { - if (override.extends) { - override.extends = override.extends.filter( - (ext) => - ext !== 'plugin:@nx/typescript' && - ext !== 'plugin:@nrwl/nx/typescript' && - ext !== 'plugin:@nx/javascript' && - ext !== 'plugin:@nrwl/nx/javascript' - ); - if (override.extends.length === 0) { - delete override.extends; + // add extends + json.extends = json.extends || []; + const pathToRootConfig = `${offsetFromRoot( + dirname(projectEslintPath) + )}.eslintrc.base.json`; + if (json.extends.indexOf(pathToRootConfig) === -1) { + json.extends.push(pathToRootConfig); + } + // cleanup overrides + if (json.overrides) { + json.overrides.forEach((override) => { + if (override.extends) { + override.extends = override.extends.filter( + (ext) => + ext !== 'plugin:@nx/typescript' && + ext !== 'plugin:@nrwl/nx/typescript' && + ext !== 'plugin:@nx/javascript' && + ext !== 'plugin:@nrwl/nx/javascript' + ); + if (override.extends.length === 0) { + delete override.extends; + } } - } - }); - } - return json; - }); + }); + } + return json; + }); + } return; } if ( @@ -99,6 +126,6 @@ function migrateEslintFile(projectEslintPath: string, tree: Tree) { console.warn('YAML eslint config is not supported yet for migration'); } if (projectEslintPath.endsWith('.js') || projectEslintPath.endsWith('.cjs')) { - console.warn('YAML eslint config is not supported yet for migration'); + console.warn('JS eslint config is not supported yet for migration'); } } diff --git a/packages/linter/src/generators/lint-project/lint-project.ts b/packages/linter/src/generators/lint-project/lint-project.ts index 7e96a6cfeaab8..0cc2012f6b068 100644 --- a/packages/linter/src/generators/lint-project/lint-project.ts +++ b/packages/linter/src/generators/lint-project/lint-project.ts @@ -8,7 +8,11 @@ import { } from '@nx/devkit'; import { Linter as LinterEnum } from '../utils/linter'; -import { findEslintFile } from '../utils/eslint-file'; +import { + baseEsLintConfigFile, + baseEsLintFlatConfigFile, + findEslintFile, +} from '../utils/eslint-file'; import { join } from 'path'; import { lintInitGenerator } from '../init/init'; import type { Linter } from 'eslint'; @@ -219,7 +223,10 @@ function isMigrationToMonorepoNeeded( tree: Tree ): boolean { // the base config is already created, migration has been done - if (tree.exists('.eslintrc.base.json')) { + if ( + tree.exists(baseEsLintConfigFile) || + tree.exists(baseEsLintFlatConfigFile) + ) { return false; } From 5705f26b4b7f09f3067685a1e3c378fca0aba489 Mon Sep 17 00:00:00 2001 From: Miroslav Jonas Date: Sat, 12 Aug 2023 14:08:39 +0200 Subject: [PATCH 26/30] fix(linter): add support for plugin removal and base import --- package.json | 3 +- .../linter/src/executors/eslint/schema.d.ts | 2 +- .../src/generators/init/init-migration.ts | 30 ++- .../utils/flat-config/ast-utils.spec.ts | 238 ++++++++++++++++++ .../generators/utils/flat-config/ast-utils.ts | 217 +++++++++++++++- pnpm-lock.yaml | 44 ++-- 6 files changed, 492 insertions(+), 42 deletions(-) diff --git a/package.json b/package.json index 9cb43050fdd34..af24cde847c7e 100644 --- a/package.json +++ b/package.json @@ -105,10 +105,9 @@ "@swc/core": "^1.3.51", "@swc/jest": "^0.2.20", "@testing-library/react": "13.4.0", - "@types/css-minimizer-webpack-plugin": "^3.2.1", "@types/cytoscape": "^3.18.2", "@types/detect-port": "^1.3.2", - "@types/eslint": "~8.4.1", + "@types/eslint": "~8.44.2", "@types/express": "4.17.14", "@types/flat": "^5.0.1", "@types/fs-extra": "^11.0.0", diff --git a/packages/linter/src/executors/eslint/schema.d.ts b/packages/linter/src/executors/eslint/schema.d.ts index af1d5bdf3ce47..75f3383d2041d 100644 --- a/packages/linter/src/executors/eslint/schema.d.ts +++ b/packages/linter/src/executors/eslint/schema.d.ts @@ -19,7 +19,7 @@ export interface Schema extends JsonObject { cacheStrategy: 'content' | 'metadata' | null; rulesdir: string[]; resolvePluginsRelativeTo: string | null; - reportUnusedDisableDirectives: Linter.RuleLevel | null; + reportUnusedDisableDirectives: Linter.StringSeverity | null; printConfig?: string | null; } diff --git a/packages/linter/src/generators/init/init-migration.ts b/packages/linter/src/generators/init/init-migration.ts index 6b651d873c654..c7205f8ef6288 100644 --- a/packages/linter/src/generators/init/init-migration.ts +++ b/packages/linter/src/generators/init/init-migration.ts @@ -16,6 +16,13 @@ import { } from './global-eslint-config'; import { useFlatConfig } from '../../utils/flat-config'; import { eslintrcVersion } from '../../utils/versions'; +import { + addBlockToFlatConfigExport, + addImportToFlatConfig, + generateSpreadElement, + removeCompatExtends, + removePlugin, +} from '../utils/flat-config/ast-utils'; export function migrateConfigToMonorepoStyle( projects: ProjectConfiguration[], @@ -71,9 +78,26 @@ function migrateEslintFile(projectEslintPath: string, tree: Tree) { if (isEslintConfigSupported(tree)) { if (useFlatConfig(tree)) { let config = tree.read(projectEslintPath, 'utf-8'); - // TODO 1. remove `@nx` plugin - // TODO 2. extend eslint.base.config.js - // TODO 3. remove @nx/js|ts from extends + // remove @nx plugin + config = removePlugin(config, '@nx', '@nx/eslint-plugin-nx'); + // extend eslint.base.config.js + config = addImportToFlatConfig( + config, + 'baseConfig', + `${offsetFromRoot(dirname(projectEslintPath))}eslint.base.config.js` + ); + config = addBlockToFlatConfigExport( + config, + generateSpreadElement('baseConfig'), + { insertAtTheEnd: false } + ); + // cleanup file extends + config = removeCompatExtends(config, [ + 'plugin:@nx/typescript', + 'plugin:@nx/javascript', + 'plugin:@nrwl/typescript', + 'plugin:@nrwl/javascript', + ]); console.warn('Flat eslint config is not supported yet for migration'); tree.write(projectEslintPath, config); } else { diff --git a/packages/linter/src/generators/utils/flat-config/ast-utils.spec.ts b/packages/linter/src/generators/utils/flat-config/ast-utils.spec.ts index 902c33a0984eb..c7576c6c2a666 100644 --- a/packages/linter/src/generators/utils/flat-config/ast-utils.spec.ts +++ b/packages/linter/src/generators/utils/flat-config/ast-utils.spec.ts @@ -6,6 +6,8 @@ import { addCompatToFlatConfig, removeOverridesFromLintConfig, replaceOverride, + removePlugin, + removeCompatExtends, } from './ast-utils'; describe('ast-utils', () => { @@ -595,4 +597,240 @@ describe('ast-utils', () => { `); }); }); + + describe('removePlugin', () => { + it('should remove plugins from config', () => { + const content = `const { FlatCompat } = require("@eslint/eslintrc"); + const nxEslintPlugin = require("@nx/eslint-plugin"); + const js = require("@eslint/js"); + const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + }); + module.exports = [ + { plugins: { "@nx": nxEslintPlugin } }, + { ignores: ["src/ignore/to/keep.ts"] }, + { ignores: ["something/else"] } + ];`; + + const result = removePlugin(content, '@nx', '@nx/eslint-plugin'); + expect(result).toMatchInlineSnapshot(` + "const { FlatCompat } = require("@eslint/eslintrc"); + const js = require("@eslint/js"); + const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + }); + module.exports = [ + { ignores: ["src/ignore/to/keep.ts"] }, + { ignores: ["something/else"] } + ];" + `); + }); + + it('should remove single plugin from config', () => { + const content = `const { FlatCompat } = require("@eslint/eslintrc"); + const nxEslintPlugin = require("@nx/eslint-plugin"); + const otherPlugin = require("other/eslint-plugin"); + const js = require("@eslint/js"); + const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + }); + module.exports = [ + { plugins: { "@nx": nxEslintPlugin, "@other": otherPlugin } }, + { ignores: ["src/ignore/to/keep.ts"] }, + { ignores: ["something/else"] } + ];`; + + const result = removePlugin(content, '@nx', '@nx/eslint-plugin'); + expect(result).toMatchInlineSnapshot(` + "const { FlatCompat } = require("@eslint/eslintrc"); + const otherPlugin = require("other/eslint-plugin"); + const js = require("@eslint/js"); + const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + }); + module.exports = [ + { plugins: { "@other": otherPlugin } }, + { ignores: ["src/ignore/to/keep.ts"] }, + { ignores: ["something/else"] } + ];" + `); + }); + + it('should leave other properties in config', () => { + const content = `const { FlatCompat } = require("@eslint/eslintrc"); + const nxEslintPlugin = require("@nx/eslint-plugin"); + const js = require("@eslint/js"); + const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + }); + module.exports = [ + { plugins: { "@nx": nxEslintPlugin }, rules: {} }, + { ignores: ["src/ignore/to/keep.ts"] }, + { ignores: ["something/else"] } + ];`; + + const result = removePlugin(content, '@nx', '@nx/eslint-plugin'); + expect(result).toMatchInlineSnapshot(` + "const { FlatCompat } = require("@eslint/eslintrc"); + const js = require("@eslint/js"); + const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + }); + module.exports = [ + { rules: {} }, + { ignores: ["src/ignore/to/keep.ts"] }, + { ignores: ["something/else"] } + ];" + `); + }); + + it('should remove single plugin from config array', () => { + const content = `const { FlatCompat } = require("@eslint/eslintrc"); + const nxEslintPlugin = require("@nx/eslint-plugin"); + const js = require("@eslint/js"); + const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + }); + module.exports = [ + { plugins: ["@nx", "something-else"] }, + { ignores: ["src/ignore/to/keep.ts"] }, + { ignores: ["something/else"] } + ];`; + + const result = removePlugin(content, '@nx', '@nx/eslint-plugin'); + expect(result).toMatchInlineSnapshot(` + "const { FlatCompat } = require("@eslint/eslintrc"); + const js = require("@eslint/js"); + const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + }); + module.exports = [ + { plugins:["something-else"] }, + { ignores: ["src/ignore/to/keep.ts"] }, + { ignores: ["something/else"] } + ];" + `); + }); + + it('should leave other fields in the object', () => { + const content = `const { FlatCompat } = require("@eslint/eslintrc"); + const nxEslintPlugin = require("@nx/eslint-plugin"); + const js = require("@eslint/js"); + const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + }); + module.exports = [ + { plugins: ["@nx"], rules: { } }, + { ignores: ["src/ignore/to/keep.ts"] }, + { ignores: ["something/else"] } + ];`; + + const result = removePlugin(content, '@nx', '@nx/eslint-plugin'); + expect(result).toMatchInlineSnapshot(` + "const { FlatCompat } = require("@eslint/eslintrc"); + const js = require("@eslint/js"); + const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + }); + module.exports = [ + { rules: { } }, + { ignores: ["src/ignore/to/keep.ts"] }, + { ignores: ["something/else"] } + ];" + `); + }); + + it('should remove entire plugin when array with single element', () => { + const content = `const { FlatCompat } = require("@eslint/eslintrc"); + const nxEslintPlugin = require("@nx/eslint-plugin"); + const js = require("@eslint/js"); + const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + }); + module.exports = [ + { plugins: ["@nx"] }, + { ignores: ["src/ignore/to/keep.ts"] }, + { ignores: ["something/else"] } + ];`; + + const result = removePlugin(content, '@nx', '@nx/eslint-plugin'); + expect(result).toMatchInlineSnapshot(` + "const { FlatCompat } = require("@eslint/eslintrc"); + const js = require("@eslint/js"); + const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + }); + module.exports = [ + { ignores: ["src/ignore/to/keep.ts"] }, + { ignores: ["something/else"] } + ];" + `); + }); + }); + + describe('removeCompatExtends', () => { + it('should remove compat extends from config', () => { + const content = `const { FlatCompat } = require("@eslint/eslintrc"); + const nxEslintPlugin = require("@nx/eslint-plugin"); + const js = require("@eslint/js"); + const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + }); + module.exports = [ + { plugins: { "@nx": nxEslintPlugin } }, + ...compat.config({ extends: ["plugin:@nx/typescript"] }).map(config => ({ + ...config, + files: ['*.ts', '*.tsx', '*.js', '*.jsx'], + rules: {} + })), + { ignores: ["src/ignore/to/keep.ts"] }, + ...compat.config({ extends: ["plugin:@nrwl/javascript"] }).map(config => ({ + files: ['*.js', '*.jsx'], + ...config, + rules: {} + })) + ];`; + + const result = removeCompatExtends(content, [ + 'plugin:@nx/typescript', + 'plugin:@nx/javascript', + 'plugin:@nrwl/typescript', + 'plugin:@nrwl/javascript', + ]); + expect(result).toMatchInlineSnapshot(` + "const { FlatCompat } = require("@eslint/eslintrc"); + const nxEslintPlugin = require("@nx/eslint-plugin"); + const js = require("@eslint/js"); + const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + }); + module.exports = [ + { plugins: { "@nx": nxEslintPlugin } }, + { + files: ['*.ts', '*.tsx', '*.js', '*.jsx'], + rules: {} + }, + { ignores: ["src/ignore/to/keep.ts"] }, + { + files: ['*.js', '*.jsx'], + rules: {} + } + ];" + `); + }); + }); }); diff --git a/packages/linter/src/generators/utils/flat-config/ast-utils.ts b/packages/linter/src/generators/utils/flat-config/ast-utils.ts index 3a768013f5287..accc1bf652738 100644 --- a/packages/linter/src/generators/utils/flat-config/ast-utils.ts +++ b/packages/linter/src/generators/utils/flat-config/ast-utils.ts @@ -106,6 +106,17 @@ export function hasOverride( return false; } +const STRIP_SPREAD_ELEMENTS = /\s*\.\.\.[a-zA-Z0-9_]+,?\n?/g; + +function parseTextToJson(text: string): any { + return JSON.parse( + text + // ensure property names have double quotes so that JSON.parse works + .replace(/'/g, '"') + .replace(/\s([a-zA-Z0-9_]+)\s*:/g, ' "$1": ') + ); +} + /** * Finds an override matching the lookup function and applies the update function to it */ @@ -141,18 +152,13 @@ export function replaceOverride( const fullNodeText = node['expression'].arguments[0].body.expression.getFullText(); // strip any spread elements - objSource = fullNodeText.replace(/\s*\.\.\.[a-zA-Z0-9_]+,?\n?/, ''); + objSource = fullNodeText.replace(STRIP_SPREAD_ELEMENTS, ''); start = node['expression'].arguments[0].body.expression.properties.pos + (fullNodeText.length - objSource.length); end = node['expression'].arguments[0].body.expression.properties.end; } - const data = JSON.parse( - objSource - // ensure property names have double quotes so that JSON.parse works - .replace(/'/g, '"') - .replace(/\s([a-zA-Z0-9_]+)\s*:/g, ' "$1": ') - ); + const data = parseTextToJson(objSource); if (lookup(data)) { changes.push({ type: ChangeType.Delete, @@ -347,6 +353,203 @@ export function addBlockToFlatConfigExport( } } +export function removePlugin( + content: string, + pluginName: string, + pluginImport: string +) { + const source = ts.createSourceFile( + '', + content, + ts.ScriptTarget.Latest, + true, + ts.ScriptKind.JS + ); + const changes: StringChange[] = []; + 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: ChangeType.Delete, + start: node.pos, + length: node.end - node.pos, + }); + } + }); + 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) + ) { + const blockElements = node.expression.right.elements; + blockElements.forEach((element) => { + if (ts.isObjectLiteralExpression(element)) { + const pluginsElem = element.properties.find( + (prop) => prop.name?.getText() === 'plugins' + ) as ts.PropertyAssignment; + if (!pluginsElem) { + return; + } + if (ts.isArrayLiteralExpression(pluginsElem.initializer)) { + const pluginsArray = pluginsElem.initializer; + const plugins = parseTextToJson( + pluginsElem.initializer + .getText() + .replace(STRIP_SPREAD_ELEMENTS, '') + ); + + if (plugins.length > 1) { + changes.push({ + type: ChangeType.Delete, + start: pluginsArray.pos, + length: pluginsArray.end - pluginsArray.pos, + }); + changes.push({ + type: 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: 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: 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 + ) as ts.PropertyAssignment; + const removeComma = + pluginsObj.properties.indexOf(plugin) < + pluginsObj.properties.length - 1 || + pluginsObj.properties.hasTrailingComma; + changes.push({ + type: 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: 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: ChangeType.Delete, + start: element.pos + (removeComma ? 1 : 0), + length: element.end - element.pos + (removeComma ? 1 : 0), + }); + } + } + } + } + }); + } + }); + return applyChangesToString(content, changes); +} + +export function removeCompatExtends( + content: string, + compatExtends: string[] +): string { + const source = ts.createSourceFile( + '', + content, + ts.ScriptTarget.Latest, + true, + ts.ScriptKind.JS + ); + const changes: StringChange[] = []; + findAllBlocks(source).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: 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: ChangeType.Insert, + index: node.pos, + text: + '\n' + + body.replace( + new RegExp('[ \t]s*...' + paramName + '[ \t]*,?\\s*', 'g'), + '' + ), + }); + } + } + }); + + return applyChangesToString(content, changes); +} + /** * Add plugins block to the top of the export blocks */ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b3931dab86de7..dd2aa0b9dd1c2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -394,9 +394,6 @@ devDependencies: '@testing-library/react': specifier: 13.4.0 version: 13.4.0(react-dom@18.2.0)(react@18.2.0) - '@types/css-minimizer-webpack-plugin': - specifier: ^3.2.1 - version: 3.2.1(esbuild@0.17.18)(webpack@5.88.0) '@types/cytoscape': specifier: ^3.18.2 version: 3.19.9 @@ -404,8 +401,8 @@ devDependencies: specifier: ^1.3.2 version: 1.3.2 '@types/eslint': - specifier: ~8.4.1 - version: 8.4.8 + specifier: ~8.44.2 + version: 8.44.2 '@types/express': specifier: 4.17.14 version: 4.17.14 @@ -10055,21 +10052,6 @@ packages: '@types/node': 18.16.9 dev: true - /@types/css-minimizer-webpack-plugin@3.2.1(esbuild@0.17.18)(webpack@5.88.0): - resolution: {integrity: sha512-MIlnEVQDTX0Y1/ZBY0RyD+F6+ZHlG42qCeSoCVhxI5N1atm+RnmDLQWUCWrdNqebFozUTRLDZJ04v5aYzGG5CA==} - deprecated: This is a stub types definition. css-minimizer-webpack-plugin provides its own type definitions, so you do not need this installed. - dependencies: - css-minimizer-webpack-plugin: 5.0.0(esbuild@0.17.18)(webpack@5.88.0) - transitivePeerDependencies: - - '@parcel/css' - - '@swc/css' - - clean-css - - csso - - esbuild - - lightningcss - - webpack - dev: true - /@types/cytoscape@3.19.9: resolution: {integrity: sha512-oqCx0ZGiBO0UESbjgq052vjDAy2X53lZpMrWqiweMpvVwKw/2IiYDdzPFK6+f4tMfdv9YKEM9raO5bAZc3UYBg==} dev: true @@ -10099,15 +10081,22 @@ packages: /@types/eslint-scope@3.7.4: resolution: {integrity: sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==} dependencies: - '@types/eslint': 8.4.8 + '@types/eslint': 8.4.1 '@types/estree': 1.0.1 dev: true - /@types/eslint@8.4.8: - resolution: {integrity: sha512-zUCKQI1bUCTi+0kQs5ZQzQ/XILWRLIlh15FXWNykJ+NG3TMKMVvwwC6GP3DR1Ylga15fB7iAExSzc4PNlR5i3w==} + /@types/eslint@8.4.1: + resolution: {integrity: sha512-GE44+DNEyxxh2Kc6ro/VkIj+9ma0pO0bwv9+uHSyBrikYOHr8zYcdPvnBOp1aw8s+CjRvuSx7CyWqRrNFQ59mA==} dependencies: - '@types/estree': 1.0.0 - '@types/json-schema': 7.0.11 + '@types/estree': 1.0.1 + '@types/json-schema': 7.0.12 + dev: true + + /@types/eslint@8.44.2: + resolution: {integrity: sha512-sdPRb9K6iL5XZOmBubg8yiFp5yS/JdUDQsq5e6h95km91MCYMuvp7mh1fjPEYUhvHepKpZOjnEaMBR4PxjWDzg==} + dependencies: + '@types/estree': 1.0.1 + '@types/json-schema': 7.0.12 dev: true /@types/estree@0.0.39: @@ -10118,10 +10107,6 @@ packages: resolution: {integrity: sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==} dev: true - /@types/estree@1.0.0: - resolution: {integrity: sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==} - dev: true - /@types/estree@1.0.1: resolution: {integrity: sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==} dev: true @@ -10277,6 +10262,7 @@ packages: /@types/json-schema@7.0.11: resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==} + dev: false /@types/json-schema@7.0.12: resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==} From 8e9073757bf7be89aaa423cb053cefa88f87b1f3 Mon Sep 17 00:00:00 2001 From: Miroslav Jonas Date: Mon, 14 Aug 2023 13:15:47 +0200 Subject: [PATCH 27/30] cleanup(linter): cleanup json converter --- .../converters/json-converter.ts | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/packages/linter/src/generators/convert-to-flat-config/converters/json-converter.ts b/packages/linter/src/generators/convert-to-flat-config/converters/json-converter.ts index c556ddc032283..87cdee1e54680 100644 --- a/packages/linter/src/generators/convert-to-flat-config/converters/json-converter.ts +++ b/packages/linter/src/generators/convert-to-flat-config/converters/json-converter.ts @@ -47,7 +47,16 @@ export function convertEslintJsonToFlatConfig( } if (config.parser) { - languageOptions.push(addParser(importsMap, config)); + const imp = config.parser; + const parserName = names(imp).propertyName; + importsMap.set(imp, parserName); + + languageOptions.push( + ts.factory.createPropertyAssignment( + 'parser', + ts.factory.createIdentifier(parserName) + ) + ); } if (config.parserOptions) { @@ -321,17 +330,3 @@ function addPlugins( ); configBlocks.push(pluginsAst); } - -function addParser( - importsMap: Map, - config: ESLint.ConfigData -): ts.PropertyAssignment { - const imp = config.parser; - const parserName = names(imp).propertyName; - importsMap.set(imp, parserName); - - return ts.factory.createPropertyAssignment( - 'parser', - ts.factory.createIdentifier(parserName) - ); -} From 5c3ab1b82ed007443332f83004b54c2139d4d851 Mon Sep 17 00:00:00 2001 From: Miroslav Jonas Date: Fri, 18 Aug 2023 11:10:34 +0200 Subject: [PATCH 28/30] feat(linter): move move logic to linter --- .../src/generators/utils/eslint-file.ts | 93 ++++++++++++ .../move/lib/update-eslint-config.spec.ts | 2 + .../move/lib/update-eslint-config.ts | 133 ++---------------- 3 files changed, 109 insertions(+), 119 deletions(-) diff --git a/packages/linter/src/generators/utils/eslint-file.ts b/packages/linter/src/generators/utils/eslint-file.ts index e67a9a8be36bc..b4c06b5384d53 100644 --- a/packages/linter/src/generators/utils/eslint-file.ts +++ b/packages/linter/src/generators/utils/eslint-file.ts @@ -1,6 +1,7 @@ import { joinPathFragments, names, + offsetFromRoot, readJson, Tree, updateJson, @@ -59,6 +60,98 @@ export function isEslintConfigSupported(tree: Tree, projectRoot = ''): boolean { return eslintFile.endsWith('.json') || eslintFile.endsWith('.config.js'); } +export function updateRelativePathsInConfig( + tree: Tree, + sourcePath: string, + destinationPath: string +) { + if ( + sourcePath === destinationPath || + !isEslintConfigSupported(tree, destinationPath) + ) { + return; + } + + const configPath = joinPathFragments( + destinationPath, + findEslintFile(tree, destinationPath) + ); + const offset = offsetFromRoot(destinationPath); + + if (useFlatConfig(tree)) { + const config = tree.read(configPath, 'utf-8'); + tree.write( + configPath, + replaceFlatConfigPaths(config, sourcePath, offset, destinationPath) + ); + } else { + updateJson(tree, configPath, (json) => { + if (typeof json.extends === 'string') { + json.extends = offsetFilePath(sourcePath, json.extends, offset); + } else if (json.extends) { + json.extends = json.extends.map((extend: string) => + offsetFilePath(sourcePath, extend, offset) + ); + } + + json.overrides?.forEach( + (o: { parserOptions?: { project?: string | string[] } }) => { + 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: string, + sourceRoot: string, + offset: string, + destinationRoot: string +): string { + 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); + newConfig = + newConfig.slice(0, match.index) + + `require('${newPath}')` + + newConfig.slice(match.index + match[0].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: string, + pathToFile: string, + offset: string +): string { + if (!pathToFile.startsWith('..')) { + // not a relative path + return pathToFile; + } + return joinPathFragments(offset, projectRoot, pathToFile); +} + export function addOverrideToLintConfig( tree: Tree, root: string, diff --git a/packages/workspace/src/generators/move/lib/update-eslint-config.spec.ts b/packages/workspace/src/generators/move/lib/update-eslint-config.spec.ts index 4a2c70dd13302..425864a542e49 100644 --- a/packages/workspace/src/generators/move/lib/update-eslint-config.spec.ts +++ b/packages/workspace/src/generators/move/lib/update-eslint-config.spec.ts @@ -248,6 +248,8 @@ describe('updateEslint (flat config)', () => { }; tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); + tree.delete('.eslintrc.json'); + tree.write('eslint.config.js', `module.exports = [];`); }); it('should handle config not existing', async () => { diff --git a/packages/workspace/src/generators/move/lib/update-eslint-config.ts b/packages/workspace/src/generators/move/lib/update-eslint-config.ts index 95c32996a13ad..d12963fc52817 100644 --- a/packages/workspace/src/generators/move/lib/update-eslint-config.ts +++ b/packages/workspace/src/generators/move/lib/update-eslint-config.ts @@ -1,36 +1,6 @@ -import { - joinPathFragments, - offsetFromRoot, - ProjectConfiguration, - Tree, - updateJson, -} from '@nx/devkit'; -import { join } from 'path'; +import { ensurePackage, ProjectConfiguration, Tree } from '@nx/devkit'; import { NormalizedSchema } from '../schema'; - -interface PartialEsLintrcOverride { - parserOptions?: { - project?: string[]; - }; -} - -interface PartialEsLintRcJson { - extends: string | string[]; - overrides?: PartialEsLintrcOverride[]; -} - -function offsetFilePath( - project: ProjectConfiguration, - pathToFile: string, - offset: string -): string { - if (!pathToFile.startsWith('..')) { - // not a relative path - return pathToFile; - } - const pathFromRoot = join(project.root, pathToFile); - return joinPathFragments(offset, pathFromRoot); -} +import { nxVersion } from '../../../utils/versions'; /** * Update the .eslintrc file of the project if it exists. @@ -42,93 +12,18 @@ export function updateEslintConfig( schema: NormalizedSchema, project: ProjectConfiguration ) { - const offset = offsetFromRoot(schema.relativeToRootDestination); - const eslintJsonPath = join( - schema.relativeToRootDestination, - '.eslintrc.json' - ); - - if (tree.exists(eslintJsonPath)) { - return updateJson( - tree, - eslintJsonPath, - (eslintRcJson) => { - if (typeof eslintRcJson.extends === 'string') { - eslintRcJson.extends = offsetFilePath( - project, - eslintRcJson.extends, - offset - ); - } else if (eslintRcJson.extends) { - eslintRcJson.extends = eslintRcJson.extends.map((extend: string) => - offsetFilePath(project, extend, offset) - ); - } - - eslintRcJson.overrides?.forEach( - (o: { parserOptions?: { project?: string | string[] } }) => { - if (o.parserOptions?.project) { - o.parserOptions.project = Array.isArray(o.parserOptions.project) - ? o.parserOptions.project.map((p) => - p.replace(project.root, schema.relativeToRootDestination) - ) - : o.parserOptions.project.replace( - project.root, - schema.relativeToRootDestination - ); - } - } - ); - return eslintRcJson; - } - ); + // if there is no suitable eslint config, we don't need to do anything + if (!tree.exists('.eslintrc.json') && !tree.exists('eslint.config.js')) { + return; } - - const eslintFlatPath = join( - schema.relativeToRootDestination, - 'eslint.config.js' + ensurePackage('@nx/linter', nxVersion); + const { + updateRelativePathsInConfig, + // nx-ignore-next-line + } = require('@nx/linter/src/generators/utils/eslint-file'); + updateRelativePathsInConfig( + tree, + project.root, + schema.relativeToRootDestination ); - if (tree.exists(eslintFlatPath)) { - const config = tree.read(eslintFlatPath, 'utf-8'); - tree.write( - eslintFlatPath, - replaceFlatConfigPaths( - config, - project, - offset, - schema.relativeToRootDestination - ) - ); - } -} - -function replaceFlatConfigPaths( - config: string, - project: ProjectConfiguration, - offset: string, - pathToDestination: string -): string { - let match; - let newConfig = config; - - // replace requires - const requireRegex = RegExp(/require\(['"](.*)['"]\)/g); - while ((match = requireRegex.exec(newConfig)) !== null) { - const newPath = offsetFilePath(project, match[1], offset); - newConfig = - newConfig.slice(0, match.index) + - `require('${newPath}')` + - newConfig.slice(match.index + match[0].length); - } - // replace projects - const projectRegex = RegExp(/project:\s?\[?['"](.*)['"]\]?/g); - while ((match = projectRegex.exec(newConfig)) !== null) { - const newProjectDef = match[0].replaceAll(project.root, pathToDestination); - newConfig = - newConfig.slice(0, match.index) + - newProjectDef + - newConfig.slice(match.index + match[0].length); - } - - return newConfig; } From 3dae274f67274de346eb1247ccdefe646632a4eb Mon Sep 17 00:00:00 2001 From: Miroslav Jonas Date: Tue, 22 Aug 2023 20:46:49 +0200 Subject: [PATCH 29/30] fix(linter): remove old todo --- packages/linter/src/generators/utils/flat-config/ast-utils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/linter/src/generators/utils/flat-config/ast-utils.ts b/packages/linter/src/generators/utils/flat-config/ast-utils.ts index accc1bf652738..6e74612d62161 100644 --- a/packages/linter/src/generators/utils/flat-config/ast-utils.ts +++ b/packages/linter/src/generators/utils/flat-config/ast-utils.ts @@ -596,7 +596,6 @@ export function addCompatToFlatConfig(content: string) { text: `${DEFAULT_FLAT_CONFIG}\n`, }, ]); - // TODO DEFAULT_FLAT_CONFIG before module.exports } const DEFAULT_FLAT_CONFIG = ` From 37699ef63a95bac7d4dfecd293639c9263b421c2 Mon Sep 17 00:00:00 2001 From: Miroslav Jonas Date: Tue, 22 Aug 2023 20:59:48 +0200 Subject: [PATCH 30/30] feat(linter): add disclaimer for when nx/linter is missing --- .../move/lib/update-eslint-config.ts | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/packages/workspace/src/generators/move/lib/update-eslint-config.ts b/packages/workspace/src/generators/move/lib/update-eslint-config.ts index d12963fc52817..55e6482d5b397 100644 --- a/packages/workspace/src/generators/move/lib/update-eslint-config.ts +++ b/packages/workspace/src/generators/move/lib/update-eslint-config.ts @@ -1,6 +1,5 @@ -import { ensurePackage, ProjectConfiguration, Tree } from '@nx/devkit'; +import { output, ProjectConfiguration, Tree } from '@nx/devkit'; import { NormalizedSchema } from '../schema'; -import { nxVersion } from '../../../utils/versions'; /** * Update the .eslintrc file of the project if it exists. @@ -16,14 +15,22 @@ export function updateEslintConfig( if (!tree.exists('.eslintrc.json') && !tree.exists('eslint.config.js')) { return; } - ensurePackage('@nx/linter', nxVersion); - const { - updateRelativePathsInConfig, - // nx-ignore-next-line - } = require('@nx/linter/src/generators/utils/eslint-file'); - updateRelativePathsInConfig( - tree, - project.root, - schema.relativeToRootDestination - ); + try { + const { + updateRelativePathsInConfig, + // nx-ignore-next-line + } = require('@nx/linter/src/generators/utils/eslint-file'); + updateRelativePathsInConfig( + tree, + project.root, + schema.relativeToRootDestination + ); + } catch { + output.warn({ + title: `Could not update the eslint config file.`, + bodyLines: [ + 'The @nx/linter package could not be loaded. Please update the paths in eslint config manually.', + ], + }); + } }